#! /usr/bin/env rspec
# frozen_string_literal: true

require_relative "../../test_helper"

require "agama/ssl/certificate"

require "pathname"
require "tmpdir"

describe Agama::SSL::Certificate do
  subject { Agama::SSL::Certificate.load_file(File.join(FIXTURES_PATH, "test.pem")) }
  # use "openssl x509 -in test.pem -noout -serial -fingerprint" to get serial and SHA1
  # use "openssl x509 -outform der -in test.pem -out test.der" and then
  #   "sha224sum test.der" and "sha256sum test.der" to get SHA224 and SHA256
  let(:serial) { "B8:AB:F1:73:E4:1F:10:4D" }
  let(:sha1)   { "A8:DE:08:B1:57:52:FE:70:DF:D5:31:EA:E3:53:BB:39:EE:01:FF:B9" }
  let(:sha256) do
    "2A:02:DA:EC:A9:FF:4C:B4:A6:C0:57:08:F6:1C:8B:B0:94:FA:F4:60:96:5E:" \
      "18:48:CA:84:81:48:60:F3:CB:BF"
  end
  let(:initial) { false }

  before do
    allow(Yast::Stage).to receive(:initial).and_return(initial)
  end

  describe ".load_file" do
    it "loads SSL certificate from a file" do
      expect(subject).to be_a(Agama::SSL::Certificate)
    end
  end

  describe ".load" do
    it "loads SSL certificate from data" do
      expect(Agama::SSL::Certificate.load(File.read(File.join(FIXTURES_PATH, "test.pem")))).to \
        be_a(Agama::SSL::Certificate)
    end
  end

  xdescribe ".download" do
    it "downloads a SSL certificate from server" do
      expect(Agama::SSL::Downloader).to receive(:download)\
        .and_return(File.read(File.join(FIXTURES_PATH, "test.pem")))

      expect(Agama::SSL::Certificate.download("http://example.com/smt.crt")).to \
        be_a(Agama::SSL::Certificate)
    end
  end

  describe ".update_instsys_ca" do
    CERT_NAME = "YaST_Team.pem"
    # Names are asigned by "trust" and related to certificate content
    CERT_LINKS = ["8820a2e8.0", "8f13f82e.0"].freeze

    let(:ca_dir) { Pathname.new(Dir.mktmpdir) }
    let(:tmp_ca_dir) { File.join(FIXTURES_PATH, "anchors") }

    before do
      stub_const("Agama::SSL::Certificate::TMP_CA_CERTS_DIR", tmp_ca_dir.to_s)
      stub_const("Agama::SSL::Certificate::CA_CERTS_DIR", ca_dir.to_s)
      allow(Yast::Execute).to receive(:locally).and_call_original
      allow(FileUtils).to receive(:rm_rf).and_call_original
      ["openssl", "pem"].each do |d|
        FileUtils.mkdir_p(File.join(tmp_ca_dir, d))
        CERT_LINKS.each do |l|
          FileUtils.ln_sf(File.join(tmp_ca_dir, d, CERT_NAME), File.join(tmp_ca_dir, d, l))
        end
      end
    end

    after do
      FileUtils.rm_rf(ca_dir.to_s)
      cert_links = Dir[File.join(tmp_ca_dir, "*")].select { |f| File.symlink?(f) }
      FileUtils.rm(cert_links) unless cert_links.empty?
    end

    it "adds new certs under anchors to system CA certificates" do
      expect(Yast::Execute).to receive(:locally).with("trust", "extract",
        "--format=openssl-directory", "--filter=ca-anchors", "--overwrite",
        File.join(tmp_ca_dir, "openssl"))
        .and_return(true)
      expect(Yast::Execute).to receive(:locally).with("trust", "extract",
        "--format=pem-directory-hash", "--filter=ca-anchors", "--overwrite",
        File.join(tmp_ca_dir, "pem"))
        .and_return(true)
      expect(FileUtils).to receive(:rm_rf).with(tmp_ca_dir)
        .and_return(Dir[File.join(tmp_ca_dir, "*")])

      expect(described_class.update_instsys_ca).to eq(true)

      # Check that certificates and symlink exists
      targets = ["pem", "openssl"].map { |d| File.join(ca_dir, d) }
      targets.each do |subdir|
        expect(File.file?(File.join(subdir, CERT_NAME))).to eq true
        CERT_LINKS.each { |l| expect(File.symlink?(File.join(subdir, l))).to eq true }
      end
    end

    context "when updating the system CA certificate fails" do
      before do
        allow(Cheetah).to receive(:run)
        allow(FileUtils).to receive(:rm_rf)
      end

      it "returns false" do
        expect(described_class.update_instsys_ca).to eq(false)
      end
    end
  end

  describe ".default_certificate_path" do
    it "returns the path defined by INSTSYS_SERVER_CERT_FILE" do
      expect(described_class.default_certificate_path)
        .to eq(Agama::SSL::Certificate::INSTSYS_SERVER_CERT_FILE)
    end
  end

  describe "#fingerprint" do
    it "returns SHA1 fingerprint in HEX format" do
      expect(subject.fingerprint(Agama::SSL::Fingerprint::SHA1).value).to eq(sha1)
    end

    it "returns SHA256 fingerprint in HEX format" do
      expect(subject.fingerprint(Agama::SSL::Fingerprint::SHA256).value).to eq(sha256)
    end

    it "raises an exception when unsupported sum is requested" do
      expect { subject.fingerprint("SHA224") }.to raise_error(/Unsupported checksum type/)
    end
  end

  describe "#serial" do
    it "returns serial number in HEX format" do
      expect(subject.serial).to eq(serial)
    end
  end

  describe "#issued_on" do
    it "returns date of issue in human readable form" do
      expect(subject.issued_on).to eq("2014-04-24")
    end
  end

  describe "#expires_on" do
    it "returns date of issue in human readable form" do
      expect(subject.expires_on).to eq("2017-04-23")
    end
  end

  context "current date in the past" do
    before do
      expect(Time).to receive(:now).and_return(Time.new(2010, 1, 1))
    end

    describe "#expired?" do
      it "returns false" do
        expect(subject.expired?).to eq(false)
      end
    end

    describe "#valid_yet?" do
      it "returns false" do
        expect(subject.valid_yet?).to eq(false)
      end
    end
  end

  context "current date in the future" do
    before do
      expect(Time).to receive(:now).and_return(Time.new(2020, 1, 1))
    end

    describe "#expired?" do
      it "returns true" do
        expect(subject.expired?).to eq(true)
      end
    end

    describe "#valid_yet?" do
      it "returns true" do
        expect(subject.valid_yet?).to eq(true)
      end
    end
  end

  context "current date in valid range" do
    before do
      expect(Time).to receive(:now).and_return(Time.new(2015, 1, 1))
    end

    describe "#expired?" do
      it "returns false" do
        expect(subject.expired?).to eq(false)
      end
    end

    describe "#valid_yet?" do
      it "returns true" do
        expect(subject.valid_yet?).to eq(true)
      end
    end
  end

  describe "#subject_name" do
    it "returns subject name" do
      expect(subject.subject_name).to eq("linux-1hyn")
    end
  end

  describe "#subject_organization" do
    it "returns subject organization name" do
      expect(subject.subject_organization).to eq("WebYaST")
    end
  end

  describe "#subject_organization_unit" do
    it "returns subject organization unit name" do
      expect(subject.subject_organization_unit).to eq("WebYaST")
    end
  end

  describe "#issuer_name" do
    it "returns issuer name" do
      expect(subject.issuer_name).to eq("linux-1hyn")
    end
  end

  describe "#issuer_organization" do
    it "returns issuer organization name" do
      expect(subject.issuer_organization).to eq("WebYaST")
    end
  end

  describe "#issuer_organization_unit" do
    it "returns issuer organization unit name" do
      expect(subject.issuer_organization_unit).to eq("WebYaST")
    end
  end

  describe "#import_to_instsys" do
    before do
      allow(subject.x509_cert).to receive(:to_pem).and_return("CERTIFICATE")
    end

    it "copies the certificate to the default instsys path" do
      allow(described_class).to receive(:update_instsys_ca)
      expect(File).to receive(:write)
        .with(described_class.default_certificate_path, "CERTIFICATE")

      subject.import_to_instsys
    end

    context "when successfully updates system CA certificates" do
      before do
        allow(File).to receive(:write)
        expect(described_class).to receive(:update_instsys_ca).and_return(true)
      end

      it "returns true" do
        expect(subject.import_to_instsys).to eq(true)
      end
    end

    context "when fails to update system CA certificates" do
      before do
        allow(File).to receive(:write)
        expect(described_class).to receive(:update_instsys_ca).and_return(false)
      end

      it "returns false" do
        expect(subject.import_to_instsys).to eq(false)
      end
    end
  end

  describe "#import" do
    it "installs the certificate in the instsys" do
      expect(subject).to receive(:import_to_instsys)
      subject.import
    end
  end
end
