require "spec_helper"

module Omnibus
  describe Packager::BFF do
    let(:project) do
      Project.new.tap do |project|
        project.name("project")
        project.homepage("https://example.com")
        project.install_dir("/opt/project")
        project.build_version("1.2.3")
        project.build_iteration("2")
        project.maintainer("Chef Software")
      end
    end

    subject { described_class.new(project) }

    let(:project_root) { File.join(tmp_path, "project/root") }
    let(:package_dir)  { File.join(tmp_path, "package/dir") }
    let(:staging_dir)  { File.join(tmp_path, "staging/dir") }

    before do
      # This is here to allow this unit test to run on windows.
      allow(File).to receive(:expand_path).and_wrap_original do |m, *args|
        m.call(*args).sub(/^[A-Za-z]:/, "")
      end
      Config.project_root(project_root)
      Config.package_dir(package_dir)

      allow(subject).to receive(:staging_dir).and_return(staging_dir)
      create_directory(staging_dir)
      create_directory(subject.scripts_staging_dir)
    end

    describe "#id" do
      it "is :bff" do
        expect(subject.id).to eq(:bff)
      end
    end

    describe "#package_name" do
      before do
        allow(subject).to receive(:safe_architecture).and_return("x86_64")
      end

      it "includes the name and version" do
        expect(subject.package_name).to eq("project-1.2.3-2.x86_64.bff")
      end
    end

    describe "#scripts_install_dir" do
      it "is nested inside the project install_dir" do
        expect(subject.scripts_install_dir).to start_with(project.install_dir)
      end
    end

    describe "#scripts_staging_dir" do
      it "is nested inside the staging_dir" do
        expect(subject.scripts_staging_dir).to start_with(staging_dir)
      end
    end

    describe "#write_scripts" do
      context "when scripts are given" do
        let(:scripts) { %w{ preinst postinst prerm postrm } }
        before do
          scripts.each do |script_name|
            create_file("#{project_root}/package-scripts/project/#{script_name}") do
              "Contents of #{script_name}"
            end
          end
        end

        it "writes the scripts into scripts staging dir" do
          subject.write_scripts

          scripts.each do |script_name|
            script_file = "#{subject.scripts_staging_dir}/#{script_name}"
            contents = File.read(script_file)
            expect(contents).to include("Contents of #{script_name}")
          end
        end
      end
    end

    describe "#write_gen_template" do
      before do
        allow(subject).to receive(:safe_architecture).and_return("x86_64")
      end

      let(:gen_file) { "#{staging_dir}/gen.template" }

      it "generates the file" do
        subject.write_gen_template
        expect(gen_file).to be_a_file
      end

      it "has the correct content" do
        subject.write_gen_template
        contents = File.read(gen_file)

        expect(contents).to include("Package Name: project")
        expect(contents).to include("Package VRMF: 1.2.3.2")
        expect(contents).to include("Update: N")
        expect(contents).to include("Fileset")
        expect(contents).to include("  Fileset Name: project")
        expect(contents).to include("  Fileset VRMF: 1.2.3.2")
        expect(contents).to include("  Fileset Description: The full stack of project")
        expect(contents).to include("  USRLIBLPPFiles")
        expect(contents).to include("  EOUSRLIBLPPFiles")
        expect(contents).to include("  Bosboot required: N")
        expect(contents).to include("  License agreement acceptance required: N")
        expect(contents).to include("  Include license files in this package: N")
        expect(contents).to include("  Requisites:")
        expect(contents).to include("  ROOT Part: Y")
        expect(contents).to include("  USRFiles")
        expect(contents).to include("  EOUSRFiles")
        expect(contents).to include("EOFileset")
      end

      context "when files and directories are present" do
        before do
          create_file("#{staging_dir}/.file1")
          create_file("#{staging_dir}/file2")
          create_directory("#{staging_dir}/.dir1")
          create_directory("#{staging_dir}/dir2")
        end

        it "writes them into the template" do
          subject.write_gen_template
          contents = File.read(gen_file)

          expect(contents).to include("/.dir1")
          expect(contents).to include("/.file1")
          expect(contents).to include("/dir2")
          expect(contents).to include("/file2")
        end
      end

      context "when paths with colons/commas are present", if: !windows? do
        let(:contents) do
          subject.write_gen_template
          File.read(gen_file)
        end

        before do
          create_file("#{staging_dir}/man3/App::Cpan.3")
          create_file("#{staging_dir}/comma,file")
          create_directory("#{staging_dir}/colon::dir/file")
          create_directory("#{staging_dir}/comma,dir/file")
        end

        it "renames colon filenames in the template" do
          expect(contents).to include("/man3/App____Cpan.3")
        end

        it "renames colon directory names in the template" do
          expect(contents).to include("/colon____dir/file")
        end

        it "renames comma filenames in the template" do
          expect(contents).to include("/comma__file")
        end

        it "renames comma directory names in the template" do
          expect(contents).to include("/comma__dir/file")
        end

        context "creates a config script" do
          it "when there wasn't one provided" do
            FileUtils.rm_f("#{subject.scripts_staging_dir}/config")
            subject.write_gen_template
            expect(File).to exist("#{subject.scripts_staging_dir}/config")
          end

          it "when one is provided in the project's def" do
            create_file("#{project_root}/package-scripts/project/config")
            subject.write_gen_template
            contents = File.read("#{subject.scripts_staging_dir}/config")
            expect(contents).to include("mv '/man3/App____Cpan.3' '/man3/App::Cpan.3'")
          end

          it "with mv commands for all the renamed files" do
            subject.write_gen_template
            contents = File.read("#{subject.scripts_staging_dir}/config")
            expect(contents).to include("mv '/man3/App____Cpan.3' '/man3/App::Cpan.3'")
            expect(contents).to include("mv '/comma__file' '/comma,file'")
            expect(contents).to include("mv '/colon____dir/file' '/colon::dir/file'")
            expect(contents).to include("mv '/comma__dir/file' '/comma,dir/file'")
          end
        end
      end

      context "when paths with invalid characters are present", if: !windows? do
        let(:contents) do
          subject.write_gen_template
          File.read(gen_file)
        end

        before do
          create_file("#{staging_dir}/file with_a_space")
          create_file("#{staging_dir}/file_with_{left_brace")
          create_file("#{staging_dir}/file_with_}right_brace")
          create_file("#{staging_dir}/file_that_meets_expectations")
        end

        it "includes a file that does not include invalid characters" do
          expect(contents).to include("/file_that_meets_expectations")
        end

        it "does not include a file with spaces in the path" do
          expect(contents).to_not include("/file with_a_space")
        end

        it "does not include a file with left braces in the path" do
          expect(contents).to_not include("/file_with_{left_brace")
        end

        it "does not include a file with right braces in the path" do
          expect(contents).to_not include("/file_with_}right_brace")
        end
      end

      context "when script files are present" do
        before do
          create_file("#{subject.scripts_staging_dir}/preinst")
          create_file("#{subject.scripts_staging_dir}/postinst")
          create_file("#{subject.scripts_staging_dir}/prerm")
          create_file("#{subject.scripts_staging_dir}/postrm")
          create_file("#{subject.scripts_staging_dir}/config")
        end

        it "writes them into the template" do
          subject.write_gen_template
          contents = File.read(gen_file)

          expect(contents).to include("    Pre-installation Script: #{subject.scripts_staging_dir}/preinst")
          expect(contents).to include("    Post-installation Script: #{subject.scripts_staging_dir}/postinst")
          expect(contents).to include("    Configuration Script: #{subject.scripts_staging_dir}/config")
          expect(contents).to include("    Pre_rm Script: #{subject.scripts_staging_dir}/prerm")
          expect(contents).to include("    Unconfiguration Script: #{subject.scripts_staging_dir}/postrm")
        end
      end

      context "when the log_level is :debug, it" do
        before do
          Omnibus.logger.level = :debug
        end

        it "prints the rendered template" do
          output = capture_logging { subject.write_gen_template }
          expect(output).to include("Package Name: project")
        end
      end
    end

    describe "#create_bff_file" do
      # Need to mock out the id calls
      let(:id_shellout) do
        shellout_mock = double("shellout_mock")
        allow(shellout_mock).to receive(:stdout).and_return("300")
        shellout_mock
      end

      before do
        allow(subject).to receive(:shellout!)
        allow(Dir).to receive(:chdir) { |_, &b| b.call }
        allow(subject).to receive(:shellout!)
          .with("id -u").and_return(id_shellout)
        allow(subject).to receive(:shellout!)
          .with("id -g").and_return(id_shellout)

        create_file(File.join(staging_dir, ".info", "#{project.name}.inventory")) do
          <<-INVENTORY.gsub(/^\s{12}/, "")
            /opt/project/version-manifest.txt:
                      owner = root
                      group = system
                      mode = 644
                      type = FILE
                      class = apply,inventory,angry-omnibus-toolchain
                      size = 1906
                      checksum = "02776    2 "
          INVENTORY
        end
        create_file("#{staging_dir}/file") { "http://goo.gl/TbkO01" }
      end

      it "gets the build uid" do
        expect(subject).to receive(:shellout!)
          .with("id -u")
        subject.create_bff_file
      end

      it "gets the build gid" do
        expect(subject).to receive(:shellout!)
          .with("id -g")
        subject.create_bff_file
      end

      it "chowns the directory to root" do
        # A note - the /opt/ here is essentially project.install_dir one level up.
        # There is nothing magical about 'opt' as a directory.
        expect(subject).to receive(:shellout!)
          .with(%r{chown -Rh 0:0 #{staging_dir}/opt$})
        subject.create_bff_file
      end

      it "logs a message" do
        output = capture_logging { subject.create_bff_file }
        expect(output).to include("Creating .bff file")
      end

      it "uses the correct command" do
        expect(subject).to receive(:shellout!)
          .with(%r{/usr/sbin/mkinstallp -d})
        subject.create_bff_file
      end

      it "chowns the directory back to the build user" do
        # A note - the /opt/ here is essentially project.install_dir one level up.
        # There is nothing magical about 'opt' as a directory.
        # 300 is just what we set the mock for the build uid/gid to return.
        expect(subject).to receive(:shellout!)
          .with(/chown -Rh 300:300 #{staging_dir}/)
        subject.create_bff_file
      end

      context "when the log_level is :debug, it" do
        before do
          Omnibus.logger.level = :debug
        end

        it "prints the inventory file" do
          output = capture_logging { subject.create_bff_file }
          expect(output).to match(%r{^/opt/project})
        end
      end
    end

    describe "#safe_base_package_name" do
      context 'when the project name is "safe"' do
        it "returns the value without logging a message" do
          expect(subject.safe_base_package_name).to eq("project")
          expect(subject).to_not receive(:log)
        end
      end

      context "when the project name has invalid characters" do
        before { project.name("Pro$ject123.for-realz_2") }

        it "returns the value while logging a message" do
          output = capture_logging do
            expect(subject.safe_base_package_name).to eq("pro-ject123.for-realz-2")
          end

          expect(output).to include("The `name' component of BFF package names can only include")
        end
      end
    end

    describe "#create_bff_file_name" do
      it "constructs the proper package name" do
        expect(subject.create_bff_file_name).to eq("project-1.2.3-2.x86_64.bff")
      end

    end

    describe "#bff_version" do
      it "returns the build version up with the build iteration" do
        expect(subject.bff_version).to eq("1.2.3.2")
      end
    end

    describe "#safe_architecture" do
      before do
        stub_ohai(platform: "ubuntu", version: "16.04") do |data|
          data["kernel"]["machine"] = "i386"
        end
      end

      it "returns the value from Ohai" do
        expect(subject.safe_architecture).to eq("i386")
      end
    end
  end
end
