require "spec_helper"
require "ohai"

module Omnibus
  describe Project do
    subject do
      described_class.new.evaluate do
        name "sample"
        friendly_name "Sample Project"
        install_dir "/sample"
        maintainer "Sample Devs"
        homepage "http://example.com/"

        build_version "1.0"
        build_iteration 1

        extra_package_file "/path/to/sample_dir"
        extra_package_file "/path/to/file.conf"

        resources_path "sample/project/resources"

        allowed_lib(/openssl/)
      end
    end

    it_behaves_like "a cleanroom setter", :name, %{name 'chef'}
    it_behaves_like "a cleanroom setter", :friendly_name, %{friendly_name 'Chef'}
    it_behaves_like "a cleanroom setter", :package_name, %{package_name 'chef.package'}
    it_behaves_like "a cleanroom setter", :maintainer, %{maintainer 'Chef Software, Inc'}
    it_behaves_like "a cleanroom setter", :homepage, %{homepage 'https://getchef.com'}
    it_behaves_like "a cleanroom setter", :description, %{description 'Installs the thing'}
    it_behaves_like "a cleanroom setter", :replace, %{replace 'old-chef'}
    it_behaves_like "a cleanroom setter", :conflict, %{conflict 'puppet'}
    it_behaves_like "a cleanroom setter", :build_version, %{build_version '1.2.3'}
    it_behaves_like "a cleanroom setter", :build_iteration, %{build_iteration 1}
    it_behaves_like "a cleanroom setter", :package_user, %{package_user 'chef'}
    it_behaves_like "a cleanroom setter", :package_group, %{package_group 'chef'}
    it_behaves_like "a cleanroom setter", :override, %{override :chefdk, source: 'foo.com'}
    it_behaves_like "a cleanroom setter", :resources_path, %{resources_path '/path'}
    it_behaves_like "a cleanroom setter", :package_scripts_path, %{package_scripts_path '/path/scripts'}
    it_behaves_like "a cleanroom setter", :dependency, %{dependency 'libxslt-dev'}
    it_behaves_like "a cleanroom setter", :build_dependency, %{build_dependency 'libxslt-dev'}
    it_behaves_like "a cleanroom setter", :system_dependency, %{system_dependency 'libxslt'}
    it_behaves_like "a cleanroom setter", :runtime_dependency, %{runtime_dependency 'libxslt'}
    it_behaves_like "a cleanroom setter", :exclude, %{exclude 'hamlet'}
    it_behaves_like "a cleanroom setter", :config_file, %{config_file '/path/to/config.rb'}
    it_behaves_like "a cleanroom setter", :extra_package_file, %{extra_package_file '/path/to/asset'}
    it_behaves_like "a cleanroom setter", :text_manifest_path, %{text_manifest_path '/path/to/manifest.txt'}
    it_behaves_like "a cleanroom setter", :json_manifest_path, %{json_manifest_path '/path/to/manifest.txt'}
    it_behaves_like "a cleanroom setter", :build_git_revision, %{build_git_revision 'wombats'}
    it_behaves_like "a cleanroom getter", :files_path
    it_behaves_like "a cleanroom setter", :license, %{license 'Apache 2.0'}
    it_behaves_like "a cleanroom setter", :license_file, %{license_file 'LICENSES/artistic.txt'}
    it_behaves_like "a cleanroom setter", :license_file_path, %{license_file_path 'CHEF_LICENSE'}
    it_behaves_like "a cleanroom setter", :allowed_lib, %{allowed_lib /openssl/}

    describe "basics" do
      it "returns a name" do
        expect(subject.name).to eq("sample")
      end

      it "returns an install_dir" do
        expect(subject.install_dir).to eq("/sample")
      end

      it "returns a maintainer" do
        expect(subject.maintainer).to eq("Sample Devs")
      end

      it "returns a homepage" do
        expect(subject.homepage).to eq("http://example.com/")
      end

      it "returns a build version" do
        expect(subject.build_version).to eq("1.0")
      end

      it "returns a build iteration" do
        expect(subject.build_iteration).to eq(1)
      end

      it "returns an array of files and dirs" do
        expect(subject.extra_package_files).to eq(["/path/to/sample_dir", "/path/to/file.conf"])
      end

      it "returns a friendly_name" do
        expect(subject.friendly_name).to eq("Sample Project")
      end

      it "returns a resources_path" do
        expect(subject.resources_path).to include("sample/project/resources")
      end

      it "returns allowed libraries" do
        expect(subject.allowed_libs).to eq([/openssl/])
      end
    end

    describe "#install_dir" do
      it "removes duplicate slashes" do
        subject.install_dir("///opt//chef")
        expect(subject.install_dir).to eq("/opt/chef")
      end

      it "converts Windows slashes to Ruby ones" do
        subject.install_dir('C:\\chef\\chefdk')
        expect(subject.install_dir).to eq("C:/chef/chefdk")
      end

      it "removes trailing slashes" do
        subject.install_dir("/opt/chef//")
        expect(subject.install_dir).to eq("/opt/chef")
      end

      it "is a DSL method" do
        expect(subject).to have_exposed_method(:install_dir)
      end
    end

    describe "#default_root" do
      context "on Windows" do
        before { stub_ohai(platform: "windows", version: "2012R2") }

        it "returns C:/" do
          expect(subject.default_root).to eq("C:")
        end
      end

      context "on non-Windows" do
        before { stub_ohai(platform: "ubuntu", version: "16.04") }

        it "returns /opt" do
          expect(subject.default_root).to eq("/opt")
        end
      end

      it "is a DSL method" do
        expect(subject).to have_exposed_method(:default_root)
      end
    end

    describe "build_git_revision" do
      let(:git_repo_subdir_path) do
        path = local_git_repo("foobar", annotated_tags: ["1.0", "2.0", "3.0"])
        subdir_path = File.join(path, "asubdir")
        Dir.mkdir(subdir_path)
        subdir_path
      end

      it "returns a revision even when running in a subdir" do
        Dir.chdir(git_repo_subdir_path) do
          expect(subject.build_git_revision).to eq("632501dde2c41f3bdd988b818b4c008e2ff398dc")
        end
      end
    end

    describe "#license" do
      it "sets the default to Unspecified" do
        expect(subject.license).to eq ("Unspecified")
      end
    end

    describe "#license_file_path" do
      it "sets the default to LICENSE" do
        expect(subject.license_file_path).to eq ("/sample/LICENSE")
      end
    end

    describe "#dirty!" do
      let(:software) { double(Omnibus::Software) }

      it "dirties the cache" do
        subject.instance_variable_set(:@culprit, nil)
        subject.dirty!(software)
        expect(subject).to be_dirty
      end

      it "sets the culprit" do
        subject.instance_variable_set(:@culprit, nil)
        subject.dirty!(software)
        expect(subject.culprit).to be(software)
      end
    end

    describe "#dirty?" do
      it "returns true by default" do
        subject.instance_variable_set(:@culprit, nil)
        expect(subject).to_not be_dirty
      end

      it "returns true when the cache is dirty" do
        subject.instance_variable_set(:@culprit, true)
        expect(subject).to be_dirty
      end

      it "returns false when the cache is not dirty" do
        subject.instance_variable_set(:@culprit, false)
        expect(subject).to_not be_dirty
      end
    end

    describe "#<=>" do
      let(:chefdk) { described_class.new.tap { |p| p.name("chefdk") } }
      let(:chef)   { described_class.new.tap { |p| p.name("chef") } }
      let(:ruby)   { described_class.new.tap { |p| p.name("ruby") } }

      it "compares projects by name" do
        list = [chefdk, chef, ruby]
        expect(list.sort.map(&:name)).to eq(%w{chef chefdk ruby})
      end
    end

    describe "#build_iteration" do
      let(:fauxhai_options) { {} }

      before { stub_ohai(fauxhai_options) }

      context "when on RHEL" do
        let(:fauxhai_options) { { platform: "redhat", version: "8" } }
        it "returns a RHEL iteration" do
          expect(subject.build_iteration).to eq(1)
        end
      end

      context "when on Debian" do
        let(:fauxhai_options) { { platform: "debian", version: "10" } }
        it "returns a Debian iteration" do
          expect(subject.build_iteration).to eq(1)
        end
      end

      context "when on FreeBSD" do
        let(:fauxhai_options) { { platform: "freebsd", version: "12" } }
        it "returns a FreeBSD iteration" do
          expect(subject.build_iteration).to eq(1)
        end
      end

      context "when on Windows" do
        before { stub_ohai(platform: "windows", version: "2019") }
        before { stub_const("File::ALT_SEPARATOR", '\\') } # rubocop:disable Style/StringLiterals
        it "returns a Windows iteration" do
          expect(subject.build_iteration).to eq(1)
        end
      end

      context "when on macOS" do
        let(:fauxhai_options) { { platform: "mac_os_x" } }
        it "returns a generic iteration" do
          expect(subject.build_iteration).to eq(1)
        end
      end
    end

    describe "#overrides" do
      before { subject.overrides.clear }

      it "sets all the things through #overrides" do
        subject.override(:thing, version: "6.6.6")
        expect(subject.override(:zlib)).to be_nil
      end

      it "retrieves the things set through #overrides" do
        subject.override(:thing, version: "6.6.6")
        expect(subject.override(:thing)[:version]).to eq("6.6.6")
      end

      it "symbolizes #overrides" do
        subject.override("thing", version: "6.6.6")
        [:thing, "thing"].each do |thing|
          expect(subject.override(thing)).not_to be_nil
        end
        expect(subject.override(:thing)[:version]).to eq("6.6.6")
      end
    end

    describe "#ohai" do
      before { stub_ohai(platform: "ubuntu", version: "16.04") }

      it "is a DSL method" do
        expect(subject).to have_exposed_method(:ohai)
      end

      it "delegates to the Ohai class" do
        expect(subject.ohai).to be(Ohai)
      end
    end

    describe "#packagers_for_system" do
      it "returns array of packager objects" do
        subject.packagers_for_system.each do |packager|
          expect(packager).to be_a(Packager::Base)
        end
      end

      it "calls Packager#for_current_system" do
        expect(Packager).to receive(:for_current_system)
          .and_call_original
        subject.packagers_for_system
      end
    end

    describe "#package" do
      it "raises an exception when a block is not given" do
        expect { subject.package(:foo) }.to raise_error(InvalidValue)
      end

      it "adds the block to the list" do
        block = Proc.new {}
        subject.package(:foo, &block)

        expect(subject.packagers[:foo]).to include(block)
      end

      it "allows for multiple invocations, keeping order" do
        block_1, block_2 = Proc.new {}, Proc.new {}
        subject.package(:foo, &block_1)
        subject.package(:foo, &block_2)

        expect(subject.packagers[:foo]).to eq([block_1, block_2])
      end
    end

    describe "#packagers" do
      it "returns a Hash" do
        expect(subject.packagers).to be_a(Hash)
      end

      it "has a default Hash value of an empty array" do
        expect(subject.packagers[:foo]).to be_a(Array)
        expect(subject.packagers[:bar]).to_not be(subject.packagers[:foo])
      end
    end

    describe "#compressor" do
      it "returns a compressor object" do
        expect(subject.compressor).to be_a(Compressor::Base)
      end

      it "calls Compressor#for_current_system" do
        expect(Compressor).to receive(:for_current_system)
          .and_call_original

        subject.compressor
      end

      it "passes in the current compressors" do
        subject.compress(:dmg)
        subject.compress(:tgz)

        expect(Compressor).to receive(:for_current_system)
          .with(%i{dmg tgz})
          .and_call_original

        subject.compressor
      end
    end

    describe "#compress" do
      it "does not raises an exception when a block is not given" do
        expect { subject.compress(:foo) }.to_not raise_error
      end

      it "adds the compressor to the list" do
        subject.compress(:foo)
        expect(subject.compressors).to include(:foo)
      end

      it "adds the block to the list" do
        block = Proc.new {}
        subject.compress(:foo, &block)

        expect(subject.compressors[:foo]).to include(block)
      end

      it "allows for multiple invocations, keeping order" do
        block_1, block_2 = Proc.new {}, Proc.new {}
        subject.compress(:foo, &block_1)
        subject.compress(:foo, &block_2)

        expect(subject.compressors[:foo]).to eq([block_1, block_2])
      end
    end

    describe "#compressors" do
      it "returns a Hash" do
        expect(subject.compressors).to be_a(Hash)
      end

      it "has a default Hash value of an empty array" do
        expect(subject.compressors[:foo]).to be_a(Array)
        expect(subject.compressors[:bar]).to_not be(subject.compressors[:foo])
      end
    end

    describe "#shasum" do
      context "when a filepath is given" do
        let(:path) { "/project.rb" }
        let(:file) { double(File) }

        before do
          subject.instance_variable_set(:@filepath, path)

          allow(File).to receive(:exist?)
            .with(path)
            .and_return(true)
          allow(File).to receive(:open)
            .with(path)
            .and_return(file)
        end

        it "returns the correct shasum" do
          expect(subject.shasum).to eq("2cb8bdd11c766caa11a37607e84ffb51af3ae3da16931988f12f7fc9de98d68e")
        end
      end

      context "when a filepath is not given" do
        before { subject.send(:remove_instance_variable, :@filepath) }

        it "returns the correct shasum" do
          expect(subject.shasum).to eq("3cc6bd98da4d643b79c71be2c93761a458b442e2931f7d421636f526d0c1e8bf")
        end
      end
    end

    describe "#restore_complete_build" do
      let(:cached_build) { double(GitCache) }
      let(:first_software) { double(Omnibus::Software) }
      let(:last_software) { double(Omnibus::Software) }

      before do
        allow(Config).to receive(:use_git_caching).and_return(git_caching)
      end

      context "when git caching is enabled" do
        let(:git_caching) { true }

        it "restores the last software built" do
          expect(subject).to receive(:softwares).and_return([first_software, last_software])
          expect(GitCache).to receive(:new).with(last_software).and_return(cached_build)
          expect(cached_build).to receive(:restore_from_cache)
          subject.restore_complete_build
        end
      end

      context "when git caching is disabled" do
        let(:git_caching) { false }

        it "does nothing" do
          expect(subject).not_to receive(:softwares)
          expect(GitCache).not_to receive(:new)
          subject.restore_complete_build
        end
      end
    end
  end
end
