require "spec_helper"

module Omnibus
  describe Builder do
    include_examples "a software"

    #
    # Fakes the embedded bin path to whatever exists in bundler. This is useful
    # for testing methods like +ruby+ and +rake+ without the need to compile
    # a real Ruby just for functional tests. This strategy does not work on
    # Windows because a) windows doesn't have symlinks and b) the windows
    # omnibus installation has a post installation step that fixes up
    # shebang paths to point to embedded ruby and drops bat files with
    # the correct path. If we need to invoke bundler/appbundler etc. in a
    # manner similar to one that omnibus provides, we would need to emulate
    # the fixup steps here as well, which is a pain the ass.
    #
    # Instead we write batch files that redirect to the batch files
    # corresponding to the system installation and hope it all works out.
    def fake_embedded_bin(name)
      if windows?
        ext = name == "ruby" ? ".exe" : ".bat"
        source = Bundler.which(name + ext)
        raise "Could not find #{name} in bundler environment" unless source

        File.open(File.join(embedded_bin_dir, name + ".bat"), "w") do |f|
          f.write <<-EOH.gsub(/^ {12}/, "")
            @"#{source}" %*
          EOH
        end
      else
        source = Bundler.which(name)
        raise "Could not find #{name} in bundler environment" unless source

        target = File.join(embedded_bin_dir, name)
        create_link(source, target) unless File.exist?(target)
      end
    end

    def shellout_opts(subject)
      # Pass GEM_HOME and GEM_PATH to subprocess so our fake bin works
      options = {}
      options[:env] = {
          "GEM_HOME" => ENV["GEM_HOME"],
          "GEM_PATH" => ENV["GEM_PATH"],
      }
      options[:env].merge!(subject.with_embedded_path)
      options
    end

    def make_gemspec
      gemspec = File.join(project_dir, "#{project_name}.gemspec")
      File.open(gemspec, "w") do |f|
        f.write <<-EOH.gsub(/^ {12}/, "")
            Gem::Specification.new do |gem|
              gem.name           = '#{project_name}'
              gem.version        = '1.0.0'
              gem.author         = 'Chef Software, Inc.'
              gem.email          = 'info@getchef.com'
              gem.description    = 'Installs a thing'
              gem.summary        = gem.description
            end
        EOH
      end
      gemspec
    end

    def make_gemfile
      gemfile = File.join(project_dir, "Gemfile")
      File.open(gemfile, "w") do |f|
        f.write <<-EOH.gsub(/^ {12}/, "")
            gemspec
        EOH
      end
      gemfile
    end

    def make_gemfile_lock
      gemfile_lock = File.join(project_dir, "Gemfile.lock")
      File.open(gemfile_lock, "w") do |f|
        f.write <<-EOH.gsub(/^ {12}/, "")
            PATH
              remote: .
              specs:
                #{project_name} (1.0.0)

            GEM
              specs:

            PLATFORMS
              ruby

            DEPENDENCIES
              #{project_name}!
        EOH
      end
      gemfile_lock
    end

    subject { described_class.new(software) }
    let(:project_name) { "example" }
    let(:project_dir) { File.join(source_dir, project_name) }

    describe "#command" do
      it "executes the command" do
        subject.command("echo 'Hello World!'")

        output = capture_logging { subject.build }
        expect(output).to include("Hello World")
      end
    end

    describe "#make" do
      it "is waiting for a good samaritan to write tests" do
        skip
      end
    end

    describe "#patch" do
      it "applies the patch" do
        configure = File.join(project_dir, "configure")
        File.open(configure, "w") do |f|
          f.write <<-EOH.gsub(/^ {12}/, "")
            THING="-e foo"
            ZIP="zap"
          EOH
        end

        patch = File.join(patches_dir, "apply.patch")
        File.open(patch, "w") do |f|
          f.write <<-EOH.gsub(/^ {12}/, "")
            --- a/configure
            +++ b/configure
            @@ -1,2 +1,3 @@
             THING="-e foo"
            +FOO="bar"
             ZIP="zap"
          EOH
        end

        if windows?
          bash_path = Bundler.which("bash.exe")
          allow(subject).to receive(:embedded_msys_bin)
            .with("bash.exe")
            .and_return("#{bash_path}")
        end

        subject.patch(source: "apply.patch")
        subject.build
      end
    end

    describe "#ruby" do
      it "executes the command as the embdedded ruby" do
        ruby = File.join(scripts_dir, "setup.rb")
        File.open(ruby, "w") do |f|
          f.write <<-EOH.gsub(/^ {12}/, "")
            File.write("#{software.install_dir}/test.txt", 'This is content!')
          EOH
        end

        fake_embedded_bin("ruby")

        subject.ruby(ruby, env: subject.with_embedded_path)
        subject.build

        path = "#{software.install_dir}/test.txt"
        expect(path).to be_a_file
        expect(File.read(path)).to eq("This is content!")
      end
    end

    describe "#gem" do
      it "executes the command as the embedded gem" do
        make_gemspec
        fake_embedded_bin("gem")
        gem_file = "#{project_name}-1.0.0.gem"

        subject.gem("build #{project_name}.gemspec", shellout_opts(subject))
        subject.gem("install #{gem_file}", shellout_opts(subject))
        output = capture_logging { subject.build }

        expect(File.join(project_dir, gem_file)).to be_a_file
        expect(output).to include("gem build")
        expect(output).to include("gem install")

      end
    end

    describe "#bundler" do
      it "executes the command as the embedded bundler" do
        make_gemspec
        make_gemfile
        fake_embedded_bin("bundle")

        subject.bundle("install", shellout_opts(subject))
        output = capture_logging { subject.build }

        expect(File.join(project_dir, "Gemfile.lock")).to be_a_file
        expect(output).to include("bundle install")
      end
    end

    describe "#appbundle" do
      let(:project) { double("Project") }
      let(:project_softwares) { [ double("Software", name: project_name, project_dir: project_dir) ] }
      it "executes the command as the embedded appbundler" do
        make_gemspec
        make_gemfile
        make_gemfile_lock

        fake_embedded_bin("gem")
        fake_embedded_bin("appbundler")

        subject.gem("build #{project_name}.gemspec", shellout_opts(subject))
        subject.gem("install #{project_name}-1.0.0.gem", shellout_opts(subject))
        subject.appbundle(project_name, shellout_opts(subject))

        expect(subject).to receive(:project).and_return(project)
        expect(project).to receive(:softwares).and_return(project_softwares)

        output = capture_logging { subject.build }

        appbundler_path = File.join(embedded_bin_dir, "appbundler")
        appbundler_path.gsub!(%r{/}, '\\') if windows? # rubocop:disable Style/StringLiterals
        expect(output).to include("#{appbundler_path} '#{project_dir}' '#{bin_dir}'")
      end
    end

    describe "#rake" do
      it "executes the command as the embedded rake" do
        rakefile = File.join(project_dir, "Rakefile")
        File.open(rakefile, "w") do |f|
          f.write <<-EOH.gsub(/^ {12}/, "")
            task(:foo) {  }
          EOH
        end

        fake_embedded_bin("rake")

        subject.rake("-T", shellout_opts(subject))
        subject.rake("foo", shellout_opts(subject))
        output = capture_logging { subject.build }

        expect(output).to include("rake -T")
        expect(output).to include("rake foo")
      end
    end

    describe "#block" do
      it "executes the command as a block" do
        subject.block("A complex operation") do
          FileUtils.touch("#{project_dir}/bacon")
        end
        output = capture_logging { subject.build }

        expect(output).to include("A complex operation")
        expect("#{software.project_dir}/bacon").to be_a_file
      end
    end

    describe "#erb" do
      it "renders the erb" do
        erb = File.join(templates_dir, "example.erb")
        File.open(erb, "w") do |f|
          f.write <<-EOH.gsub(/^ {12}/, "")
            <%= a %>
            <%= b %>
          EOH
        end

        destination = File.join(tmp_path, "rendered")

        subject.erb(
          source: "example.erb",
          dest:   destination,
          vars:   { a: "foo", b: "bar" }
        )
        subject.build

        expect(destination).to be_a_file
        expect(File.read(destination)).to eq("foo\nbar\n")
      end
    end

    describe "#mkdir" do
      it "creates the directory" do
        path = File.join(tmp_path, "scratch")
        remove_directory(path)

        subject.mkdir(path)
        subject.build

        expect(path).to be_a_directory
      end
    end

    describe "#touch" do
      it "creates the file" do
        path = File.join(tmp_path, "file")
        remove_file(path)

        subject.touch(path)
        subject.build

        expect(path).to be_a_file
      end

      it "creates the containing directory" do
        path = File.join(tmp_path, "foo", "bar", "file")
        FileUtils.rm_rf(path)

        subject.touch(path)
        subject.build

        expect(path).to be_a_file
      end
    end

    describe "#delete" do
      it "deletes the directory" do
        path = File.join(tmp_path, "scratch")
        create_directory(path)

        subject.delete(path)
        subject.build

        expect(path).to_not be_a_directory
      end

      it "deletes the file" do
        path = File.join(tmp_path, "file")
        create_file(path)

        subject.delete(path)
        subject.build

        expect(path).to_not be_a_file
      end

      it "accepts a glob pattern" do
        path_a = File.join(tmp_path, "file_a")
        path_b = File.join(tmp_path, "file_b")
        FileUtils.touch(path_a)
        FileUtils.touch(path_b)

        subject.delete("#{tmp_path}/**/file_*")
        subject.build

        expect(path_a).to_not be_a_file
        expect(path_b).to_not be_a_file
      end
    end

    describe "#copy" do
      it "copies the file" do
        path_a = File.join(tmp_path, "file1")
        path_b = File.join(tmp_path, "file2")
        create_file(path_a)

        subject.copy(path_a, path_b)
        subject.build

        expect(path_b).to be_a_file
        expect(File.read(path_b)).to eq(File.read(path_a))
      end

      it "copies the directory and entries" do
        destination = File.join(tmp_path, "destination")

        directory = File.join(tmp_path, "scratch")
        FileUtils.mkdir_p(directory)

        path_a = File.join(directory, "file_a")
        path_b = File.join(directory, "file_b")
        FileUtils.touch(path_a)
        FileUtils.touch(path_b)

        subject.copy(directory, destination)
        subject.build

        expect(destination).to be_a_directory
        expect("#{destination}/file_a").to be_a_file
        expect("#{destination}/file_b").to be_a_file
      end

      it "accepts a glob pattern" do
        destination = File.join(tmp_path, "destination")
        FileUtils.mkdir_p(destination)

        directory = File.join(tmp_path, "scratch")
        FileUtils.mkdir_p(directory)

        path_a = File.join(directory, "file_a")
        path_b = File.join(directory, "file_b")
        FileUtils.touch(path_a)
        FileUtils.touch(path_b)

        subject.copy("#{directory}/*", destination)
        subject.build

        expect(destination).to be_a_directory
        expect("#{destination}/file_a").to be_a_file
        expect("#{destination}/file_b").to be_a_file
      end
    end

    describe "#move" do
      it "moves the file" do
        path_a = File.join(tmp_path, "file1")
        path_b = File.join(tmp_path, "file2")
        create_file(path_a)

        subject.move(path_a, path_b)
        subject.build

        expect(path_b).to be_a_file
        expect(path_a).to_not be_a_file
      end

      it "moves the directory and entries" do
        destination = File.join(tmp_path, "destination")

        directory = File.join(tmp_path, "scratch")
        FileUtils.mkdir_p(directory)

        path_a = File.join(directory, "file_a")
        path_b = File.join(directory, "file_b")
        FileUtils.touch(path_a)
        FileUtils.touch(path_b)

        subject.move(directory, destination)
        subject.build

        expect(destination).to be_a_directory
        expect("#{destination}/file_a").to be_a_file
        expect("#{destination}/file_b").to be_a_file

        expect(directory).to_not be_a_directory
      end

      it "accepts a glob pattern" do
        destination = File.join(tmp_path, "destination")
        FileUtils.mkdir_p(destination)

        directory = File.join(tmp_path, "scratch")
        FileUtils.mkdir_p(directory)

        path_a = File.join(directory, "file_a")
        path_b = File.join(directory, "file_b")
        FileUtils.touch(path_a)
        FileUtils.touch(path_b)

        subject.move("#{directory}/*", destination)
        subject.build

        expect(destination).to be_a_directory
        expect("#{destination}/file_a").to be_a_file
        expect("#{destination}/file_b").to be_a_file

        expect(directory).to be_a_directory
      end
    end

    describe "#link", :not_supported_on_windows do
      it "links the file" do
        path_a = File.join(tmp_path, "file1")
        path_b = File.join(tmp_path, "file2")
        create_file(path_a)

        subject.link(path_a, path_b)
        subject.build

        expect(path_b).to be_a_symlink
      end

      it "links the directory" do
        destination = File.join(tmp_path, "destination")
        directory = File.join(tmp_path, "scratch")
        FileUtils.mkdir_p(directory)

        subject.link(directory, destination)
        subject.build

        expect(destination).to be_a_symlink
      end

      it "accepts a glob pattern" do
        destination = File.join(tmp_path, "destination")
        FileUtils.mkdir_p(destination)

        directory = File.join(tmp_path, "scratch")
        FileUtils.mkdir_p(directory)

        path_a = File.join(directory, "file_a")
        path_b = File.join(directory, "file_b")
        FileUtils.touch(path_a)
        FileUtils.touch(path_b)

        subject.link("#{directory}/*", destination)
        subject.build

        expect("#{destination}/file_a").to be_a_symlink
        expect("#{destination}/file_b").to be_a_symlink
      end
    end

    describe "#sync" do
      let(:source) do
        source = File.join(tmp_path, "source")
        FileUtils.mkdir_p(source)

        FileUtils.touch(File.join(source, "file_a"))
        FileUtils.touch(File.join(source, "file_b"))
        FileUtils.touch(File.join(source, "file_c"))

        FileUtils.mkdir_p(File.join(source, "folder"))
        FileUtils.touch(File.join(source, "folder", "file_d"))
        FileUtils.touch(File.join(source, "folder", "file_e"))

        FileUtils.mkdir_p(File.join(source, ".dot_folder"))
        FileUtils.touch(File.join(source, ".dot_folder", "file_f"))

        FileUtils.touch(File.join(source, ".file_g"))
        source
      end

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

      context "when the destination is empty" do
        it "syncs the directories" do
          subject.sync(source, destination)
          subject.build

          expect("#{destination}/file_a").to be_a_file
          expect("#{destination}/file_b").to be_a_file
          expect("#{destination}/file_c").to be_a_file
          expect("#{destination}/folder/file_d").to be_a_file
          expect("#{destination}/folder/file_e").to be_a_file
          expect("#{destination}/.dot_folder/file_f").to be_a_file
          expect("#{destination}/.file_g").to be_a_file
        end
      end

      context "when the directory exists" do
        before { FileUtils.mkdir_p(destination) }

        it "deletes existing files and folders" do
          FileUtils.mkdir_p("#{destination}/existing_folder")
          FileUtils.mkdir_p("#{destination}/.existing_folder")
          FileUtils.touch("#{destination}/existing_file")
          FileUtils.touch("#{destination}/.existing_file")

          subject.sync(source, destination)
          subject.build

          expect("#{destination}/file_a").to be_a_file
          expect("#{destination}/file_b").to be_a_file
          expect("#{destination}/file_c").to be_a_file
          expect("#{destination}/folder/file_d").to be_a_file
          expect("#{destination}/folder/file_e").to be_a_file
          expect("#{destination}/.dot_folder/file_f").to be_a_file
          expect("#{destination}/.file_g").to be_a_file

          expect("#{destination}/existing_folder").to_not be_a_directory
          expect("#{destination}/.existing_folder").to_not be_a_directory
          expect("#{destination}/existing_file").to_not be_a_file
          expect("#{destination}/.existing_file").to_not be_a_file
        end
      end

      context "when :exclude is given" do
        it "does not copy files and folders that match the pattern" do
          subject.sync(source, destination, exclude: ".dot_folder")
          subject.build

          expect("#{destination}/file_a").to be_a_file
          expect("#{destination}/file_b").to be_a_file
          expect("#{destination}/file_c").to be_a_file
          expect("#{destination}/folder/file_d").to be_a_file
          expect("#{destination}/folder/file_e").to be_a_file
          expect("#{destination}/.dot_folder").to_not be_a_directory
          expect("#{destination}/.dot_folder/file_f").to_not be_a_file
          expect("#{destination}/.file_g").to be_a_file
        end

        it "removes existing files and folders in destination" do
          FileUtils.mkdir_p("#{destination}/existing_folder")
          FileUtils.touch("#{destination}/existing_file")
          FileUtils.mkdir_p("#{destination}/.dot_folder")
          FileUtils.touch("#{destination}/.dot_folder/file_f")

          subject.sync(source, destination, exclude: ".dot_folder")
          subject.build

          expect("#{destination}/file_a").to be_a_file
          expect("#{destination}/file_b").to be_a_file
          expect("#{destination}/file_c").to be_a_file
          expect("#{destination}/folder/file_d").to be_a_file
          expect("#{destination}/folder/file_e").to be_a_file
          expect("#{destination}/.dot_folder").to_not be_a_directory
          expect("#{destination}/.dot_folder/file_f").to_not be_a_file
          expect("#{destination}/.file_g").to be_a_file

          expect("#{destination}/existing_folder").to_not be_a_directory
          expect("#{destination}/existing_file").to_not be_a_file
        end
      end
    end

    describe "#update_config_guess", :not_supported_on_windows do
      let(:config_guess_dir) { "#{install_dir}/embedded/lib/config_guess" }

      before do
        FileUtils.mkdir_p(config_guess_dir)
      end

      context "with no config.guess" do
        before do
          File.open("#{config_guess_dir}/config.sub", "w+") do |f|
            f.write("This is config.sub")
          end
        end

        it "fails" do
          subject.update_config_guess
          expect { subject.build }.to raise_error(RuntimeError)
        end
      end

      context "with no config.sub" do
        before do
          File.open("#{config_guess_dir}/config.guess", "w+") do |f|
            f.write("This is config.guess")
          end
        end

        it "fails" do
          subject.update_config_guess
          expect { subject.build }.to raise_error(RuntimeError)
        end
      end

      context "with config_guess dependency" do
        before do
          File.open("#{config_guess_dir}/config.guess", "w+") do |f|
            f.write("This is config.guess")
          end

          File.open("#{config_guess_dir}/config.sub", "w+") do |f|
            f.write("This is config.sub")
          end
        end

        it "update config_guess with defaults" do
          subject.update_config_guess
          subject.build
          expect(File.read("#{project_dir}/config.guess")).to match /config.guess/
          expect(File.read("#{project_dir}/config.sub")).to match /config.sub/
        end

        it "honors :target option" do
          subject.update_config_guess(target: "sub_dir")
          subject.build
          expect(File.read("#{project_dir}/sub_dir/config.guess")).to match /config.guess/
          expect(File.read("#{project_dir}/sub_dir/config.sub")).to match /config.sub/
        end

        it "honors :config_guess in :install option" do
          subject.update_config_guess(install: [:config_guess])
          subject.build
          expect(File.read("#{project_dir}/config.guess")).to match /config.guess/
          expect(File.exist?("#{project_dir}/config.sub")).to be false
        end

        it "honors :config_sub in :install option" do
          subject.update_config_guess(install: [:config_sub])
          subject.build
          expect(File.read("#{project_dir}/config.sub")).to match /config.sub/
          expect(File.exist?("#{project_dir}/config.guess")).to be false
        end
      end
    end
  end
end
