require "spec_helper"

module Omnibus
  describe Licensing do
    let(:license) { nil }
    let(:license_file_path) { nil }
    let(:license_file) { nil }
    let(:zlib_version_override) { nil }

    let(:install_dir) { File.join(tmp_path, "install_dir") }
    let(:software_project_dir) { File.join(tmp_path, "software_project_dir") }

    let(:expected_project_license_path) { "LICENSE" }
    let(:expected_project_license) { "Unspecified" }
    let(:expected_project_license_content) { "" }

    before do
      FileUtils.mkdir_p(install_dir)
      FileUtils.mkdir_p(software_project_dir)

      allow_any_instance_of(Software).to receive(:project_dir).and_return(software_project_dir)
      %w{LICENSE NOTICE APACHE}.each do |file|
        File.open(File.join(software_project_dir, file), "w+") do |f|
          f.puts "This file is #{file}."
        end
      end
    end

    shared_examples "correctly created licenses" do
      it "creates the main license file for the project correctly" do
        create_licenses
        project_license = File.join(install_dir, expected_project_license_path)
        expect(File.exist?(project_license)).to be(true)
        project_license = File.read(project_license)
        expect(project_license).to match /test-project 1.2.3 license: "#{expected_project_license}"/
        expect(project_license).to match /#{expected_project_license_content}/
        expect(project_license).to match /This product bundles private_code 1.7.2,\nwhich is available under a "Unspecified"/
        expect(project_license).to match /This product bundles snoopy 1.0.0,\nwhich is available under a "GPL v2"/
        expect(project_license).not_to match /preparation/
        expect(project_license).to match /LICENSES\/snoopy-artistic.html/
        expect(project_license).to match /LICENSES\/snoopy-NOTICE/
        if zlib_version_override
          expect(project_license).to match /This product bundles zlib 1.8.0,\nwhich is available under a "Apache-2.0"/
          expect(project_license).to match /LICENSES\/zlib-APACHE/
        else
          expect(project_license).to match /This product bundles zlib 1.7.2,\nwhich is available under a "Zlib"/
          expect(project_license).to match /LICENSES\/zlib-LICENSE/
        end
      end

      it "creates the license files of software components correctly" do
        create_licenses
        license_dir = File.join(install_dir, "LICENSES")
        expect(Dir.glob("#{license_dir}/**/*").length).to be(3)

        license_names = [ "snoopy-NOTICE" ]
        if zlib_version_override
          license_names << "zlib-APACHE"
        else
          license_names << "zlib-LICENSE"
        end

        license_names.each do |software_license|
          license_path = File.join(license_dir, software_license)
          expect(File.exist?(license_path)).to be(true)
          expect(File.world_readable?(license_path)).to be_truthy
          expect(File.read(license_path)).to match /#{software_license.split("-").last}/
        end

        remote_license_file = File.join(license_dir, "snoopy-artistic.html")
        remote_license_file_contents = File.read(remote_license_file)
        expect(File.exist?(remote_license_file)).to be(true)
        expect(remote_license_file_contents).to match /The "Artistic License" - dev.perl.org/
      end

      it "warns for non-standard software license info" do
        output = capture_logging { create_licenses }
        expect(output).to include("Software 'snoopy' uses license 'GPL v2' which is not one of the standard licenses")
      end

      it "warns for missing software license info" do
        output = capture_logging { create_licenses }
        expect(output).to include("Software 'private_code' does not contain licensing information.")
      end
    end

    let(:project) do
      Project.new.tap do |project|
        project.name("test-project")
        project.install_dir(install_dir)
        project.license(license) unless license.nil?
        project.license_file_path(license_file_path) unless license_file_path.nil?
        project.license_file(license_file) unless license_file.nil?
        project.build_version("1.2.3")
        if zlib_version_override
          project.override :zlib, version: zlib_version_override
        end
      end
    end

    let(:private_code) do
      Software.new(project, "private_code.rb").evaluate do
        name "private_code"
        default_version "1.7.2"
        skip_transitive_dependency_licensing true
      end
    end

    let(:zlib) do
      Software.new(project, "zlib.rb").evaluate do
        name "zlib"
        default_version "1.7.2"
        skip_transitive_dependency_licensing true

        license "Zlib"
        license_file "LICENSE"

        version "1.8.0" do
          license "Apache-2.0"
          license_file "APACHE"
        end
      end
    end

    let(:snoopy) do
      Software.new(project, "snoopy.rb").evaluate do
        name "snoopy"
        default_version "1.0.0"
        skip_transitive_dependency_licensing true

        license "GPL v2"
        license_file "http://dev.perl.org/licenses/artistic.html"
        license_file "NOTICE"
      end
    end

    let(:preparation) do
      Software.new(project, "preparation.rb").evaluate do
        name "preparation"
        default_version "1.0.0"
        license :project_license
        skip_transitive_dependency_licensing true
      end
    end

    let(:software_with_warnings) { nil }

    let(:softwares) do
      s = [preparation, snoopy, zlib, private_code]
      s << software_with_warnings if software_with_warnings
      s
    end

    def create_licenses
      softwares.each { |s| project.library.component_added(s) }

      Licensing.create_incrementally(project) do |licensing|
        yield licensing if block_given?

        project.softwares.each do |software|
          licensing.execute_post_build(software)
        end
      end
    end

    describe "prepare step" do

      let(:licenses_dir) { File.join(install_dir, "LICENSES") }
      let(:dot_gitkeep) { File.join(licenses_dir, ".gitkeep") }
      let(:cache_dir) { File.join(install_dir, "license-cache") }
      let(:cache_dot_gitkeep) { File.join(cache_dir, ".gitkeep") }

      it "creates a LICENSES dir with a .gitkeep file inside the install directory" do
        Licensing.new(project).prepare
        expect(File).to exist(licenses_dir)
        expect(File).to exist(dot_gitkeep)
      end

      it "creates a licenses-cache dir with a .gitkeep file inside the install directory" do
        Licensing.new(project).prepare
        expect(File).to exist(cache_dir)
        expect(File).to exist(cache_dot_gitkeep)
      end

    end

    describe "without license definitions in the project" do
      it_behaves_like "correctly created licenses"

      it "warns for missing project license" do
        output = capture_logging { create_licenses }
        expect(output).to include("Project 'test-project' does not contain licensing information.")
      end
    end

    describe "with license definitions in the project" do
      let(:license) { "Custom Chef" }
      let(:license_file_path) { "CHEF.LICENSE" }
      let(:license_file) { "CUSTOM_CHEF" }

      let(:expected_project_license_path) { license_file_path }
      let(:expected_project_license) { license }
      let(:expected_project_license_content) { "Chef Custom License" }

      before do
        File.open(File.join(Config.project_root, license_file), "w+") do |f|
          f.puts "Chef Custom License is awesome."
        end
      end

      after do
        FileUtils.rm_rf(license_file)
      end

      it_behaves_like "correctly created licenses"

      it "warns for non-standard project license" do
        output = capture_logging { create_licenses }
        expect(output).to include("Project 'test-project' is using 'Custom Chef' which is not one of the standard licenses")
      end

      context "with a version override" do
        let(:zlib_version_override) { "1.8.0" }

        it_behaves_like "correctly created licenses"
      end
    end

    describe "with a local license file that does not exist" do
      let(:software_with_warnings) do
        Software.new(project, "problematic.rb").evaluate do
          name "problematic"
          default_version "0.10.2"
          license_file "NOT_EXISTS"
          skip_transitive_dependency_licensing true
        end
      end

      it_behaves_like "correctly created licenses"

      it "should log a warning for the missing file" do
        output = capture_logging { create_licenses }
        expect(output).to match /License file (.*)NOT_EXISTS' does not exist for software 'problematic'./
      end
    end

    describe "with a remote license file that does not exist" do
      before do
        Omnibus::Config.fetcher_retries(1)
      end

      let(:software_with_warnings) do
        Software.new(project, "problematic.rb").evaluate do
          name "problematic"
          default_version "0.10.2"
          license_file "https://downloads.chef.io/LICENSE"
          skip_transitive_dependency_licensing true
        end
      end

      it_behaves_like "correctly created licenses"

      it "should log a warning for the missing file" do
        output = capture_logging { create_licenses }
        expect(output).to match(/Retrying failed download/)
        expect(output).to match(/Can not download license file 'https:\/\/downloads.chef.io\/LICENSE' for software 'problematic'./)
      end
    end

    describe "with a software with no license files" do
      let(:software_with_warnings) do
        Software.new(project, "problematic.rb").evaluate do
          name "problematic"
          default_version "0.10.2"
          license "Zlib"
          skip_transitive_dependency_licensing true
        end
      end

      it_behaves_like "correctly created licenses"

      it "should log a warning for the missing file pointers" do
        output = capture_logging { create_licenses }
        expect(output).to include("Software 'problematic' does not point to any license files.")
      end
    end

    describe "with a project with no license files" do
      let(:license) { "Zlib" }

      let(:expected_project_license_path) { "LICENSE" }
      let(:expected_project_license) { license }
      let(:expected_project_license_content) { "" }

      it_behaves_like "correctly created licenses"

      it "warns for missing license files" do
        output = capture_logging { create_licenses }
        expect(output).to include("Project 'test-project' does not point to a license file.")
      end
    end

    describe "with :fatal_licensing_warnings set and without license definitions in the project" do
      before do
        Omnibus::Config.fatal_licensing_warnings(true)
      end

      it "fails the omnibus build" do
        expect { create_licenses }.to raise_error(Omnibus::LicensingError, /Project 'test-project' does not contain licensing information.\s{1,}Software 'private_code' does not contain licensing information./)
      end
    end

    describe "when all software is setting skip_transitive_dependency_licensing " do
      # This is achieved by the default values of the let() parameters

      it "does not collect transitive licensing info for any software" do
        softwares.each { |s| project.library.component_added(s) }
        create_licenses do |licensing|
          expect(licensing).not_to receive(:collect_transitive_dependency_licenses_for)
        end
      end
    end

    describe "when a project has transitive dependencies" do
      let(:license) { "Custom Chef" }
      let(:license_file_path) { "CHEF.LICENSE" }
      let(:license_file) { "CUSTOM_CHEF" }

      let(:expected_project_license_path) { license_file_path }

      let(:softwares) { [zlib] }

      before do
        File.open(File.join(Config.project_root, license_file), "w+") do |f|
          f.puts "Chef Custom License is awesome."
        end
      end

      after do
        FileUtils.rm_rf(license_file)
      end

      let(:zlib) do
        Software.new(project, "zlib.rb").evaluate do
          name "zlib"
          default_version "1.7.2"

          license "Zlib"
          license_file "LICENSE"
        end
      end

      let(:snoopy) do
        Software.new(project, "snoopy.rb").evaluate do
          name "snoopy"
          default_version "1.0.0"

          license "GPL v2"
          license_file "NOTICE"
        end
      end

      let(:license_fixtures_path) { File.join(fixtures_path, "licensing/license_scout") }

      describe "when project type is not supported" do

        before do
          allow_any_instance_of(LicenseScout::Collector).to receive(:run) do
            raise LicenseScout::Exceptions::UnsupportedProjectType.new("/path/to/project")
          end
        end

        it "does not raise an error" do
          expect { create_licenses }.not_to raise_error
        end

        it "logs a warning message" do
          output = capture_logging { create_licenses }
          expect(output).to include("is not supported project type for transitive dependency license collection")
        end

        context "with :fatal_licensing_warnings" do

          before do
            Omnibus::Config.fatal_licensing_warnings(true)
          end

          it "does not fail omnibus build" do
            expect { create_licenses }.not_to raise_error
          end
        end

        context "with :fatal_transitive_dependency_licensing_warnings" do

          before do
            Omnibus::Config.fatal_transitive_dependency_licensing_warnings(true)
          end

          it "fails omnibus build" do
            expect { create_licenses }.to raise_error(Omnibus::LicensingError, /is not supported project type for transitive dependency license collection/)
          end
        end
      end

      describe "when there are warnings in the licensing info" do
        before do
          allow_any_instance_of(LicenseScout::Collector).to receive(:run) do
            FileUtils.cp_r(File.join(license_fixtures_path, "zlib"), File.join(install_dir, "license-cache/"))
          end
          allow_any_instance_of(LicenseScout::Reporter).to receive(:report).and_return(["This is a licensing warning!!!"])
        end

        it "logs the warnings" do
          output = capture_logging { create_licenses }
          expect(output).to include("This is a licensing warning!!!")
        end

        describe "when :fatal_transitive_dependency_licensing_warnings is set" do
          before do
            Omnibus::Config.fatal_transitive_dependency_licensing_warnings(true)
          end

          it "raises an error after post_build step" do
            expect do
              create_licenses do |licensing|
                expect(licensing).not_to receive(:process_transitive_dependency_licensing_info)
              end
            end.to raise_error(Omnibus::LicensingError)
          end
        end
      end

      describe "when there are no warnings in the licensing info" do
        before do
          allow_any_instance_of(LicenseScout::Collector).to receive(:run) do
            FileUtils.cp_r(File.join(license_fixtures_path, "zlib"), File.join(install_dir, "license-cache/"))
          end

          allow_any_instance_of(LicenseScout::Collector).to receive(:issue_report).and_return([])
        end

        let(:expected_license_files) do
          %w{
            ruby_bundler-inifile-3.0.0-README.md
            ruby_bundler-mime-types-3.1-Licence.rdoc
            ruby_bundler-mini_portile2-2.1.0-LICENSE.txt
          }
        end

        let(:expected_license_texts) do
          [
            <<~EOH,
              This product includes inifile 3.0.0
              which is a 'ruby_bundler' dependency of 'zlib',
              and which is available under a 'MIT' License.
              For details, see:
              #{install_dir}/LICENSES/ruby_bundler-inifile-3.0.0-README.md
            EOH
            <<~EOH,
              This product includes mime-types 3.1
              which is a 'ruby_bundler' dependency of 'zlib',
              and which is available under a 'MIT' License.
              For details, see:
              #{install_dir}/LICENSES/ruby_bundler-mime-types-3.1-Licence.rdoc
            EOH
            <<~EOH,
              This product includes mini_portile2 2.1.0
              which is a 'ruby_bundler' dependency of 'zlib',
              and which is available under a 'MIT' License.
              For details, see:
              #{install_dir}/LICENSES/ruby_bundler-mini_portile2-2.1.0-LICENSE.txt
            EOH
          ]
        end

        it "includes transitive dependency license information in the project license information" do
          create_licenses

          project_license = File.join(install_dir, expected_project_license_path)
          expect(File.exist?(project_license)).to be(true)
          project_license_content = File.read(project_license)

          expected_license_texts.each { |t| expect(project_license_content).to include(t) }
          expected_license_files.each { |f| expect(File).to exist(File.join(install_dir, "LICENSES", f)) }
          expect(File).not_to exist(File.join(install_dir, "license-cache"))
        end
      end

      describe "with multiple softwares that have dependencies" do
        let(:softwares) { [zlib, snoopy] }

        before do
          allow_any_instance_of(LicenseScout::Collector).to receive(:run) do
            FileUtils.cp_r(File.join(license_fixtures_path, "zlib"), File.join(install_dir, "license-cache/"))
            FileUtils.cp_r(File.join(license_fixtures_path, "snoopy"), File.join(install_dir, "license-cache/"))
          end

          allow_any_instance_of(LicenseScout::Collector).to receive(:issue_report).and_return([])
        end

        let(:expected_license_files) do
          %w{
            ruby_bundler-inifile-3.0.0-README.md
            ruby_bundler-mime-types-3.1-Licence.rdoc
            ruby_bundler-mini_portile2-2.1.0-LICENSE.txt
          }
        end

        let(:expected_license_texts) do
          [
            <<~EOH,
              This product includes inifile 3.0.0
              which is a 'ruby_bundler' dependency of 'snoopy', 'zlib',
              and which is available under a 'MIT' License.
              For details, see:
              #{install_dir}/LICENSES/ruby_bundler-inifile-3.0.0-README.md
            EOH
            <<~EOH,
              This product includes mime-types 3.1
              which is a 'ruby_bundler' dependency of 'zlib',
              and which is available under a 'MIT' License.
              For details, see:
              #{install_dir}/LICENSES/ruby_bundler-mime-types-3.1-Licence.rdoc
            EOH
            <<~EOH,
              This product includes mini_portile2 2.1.0
              which is a 'ruby_bundler' dependency of 'zlib',
              and which is available under a 'MIT' License.
              For details, see:
              #{install_dir}/LICENSES/ruby_bundler-mini_portile2-2.1.0-LICENSE.txt
            EOH
            <<~EOH,
              This product includes bundler-audit 0.5.0
              which is a 'ruby_bundler' dependency of 'snoopy',
              and which is available under a 'GPLv3' License.
              For details, see:
              #{install_dir}/LICENSES/ruby_bundler-bundler-audit-0.5.0-COPYING.txt
            EOH

          ]
        end

        it "includes merged licensing information from multiple software definitions" do
          create_licenses

          project_license = File.join(install_dir, expected_project_license_path)
          expect(File.exist?(project_license)).to be(true)
          project_license_content = File.read(project_license)

          expected_license_texts.each { |t| expect(project_license_content).to include(t) }
          expected_license_files.each { |f| expect(File).to exist(File.join(install_dir, "LICENSES", f)) }
          expect(File).not_to exist(File.join(install_dir, "license-cache"))
        end

      end

    end
  end
end
