require "spec_helper"

module Omnibus
  describe Builder do
    let(:software) do
      double(Software,
        name: "chefdk",
        install_dir: project_dir,
        project_dir: project_dir)
    end

    let(:project_dir) { on_windows ? "C:/opscode/chefdk" : "/opt/chefdk" }
    let(:on_windows) { false }
    let(:msys_bash) { "C:\\opscode\\chefdk\\embedded\\msys\\1.0\\bin\\bash.exe" }

    def run_build_command
      subject.send(:build_commands)[0].run(subject)
    end

    subject { described_class.new(software) }

    before do
      allow(subject).to receive(:windows?).and_return(on_windows)
      allow(subject).to receive(:windows_safe_path) do |*args|
        path = File.join(*args)
        path.gsub!(File::SEPARATOR, '\\') if on_windows # rubocop:disable Style/StringLiterals
      end
    end

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    describe "#make" do
      before do
        allow(subject).to receive(:command)
        # Ensure tests are deterministic regardless of the host environment:
        # by default, pretend gmake is not present. Individual contexts may
        # override this with a more specific stub.
        allow(Omnibus).to receive(:which).with("gmake").and_return(nil)
      end

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

      context "when :bin is present" do
        it "uses the custom bin" do
          expect(subject).to receive(:command)
            .with("/path/to/make", { in_msys_bash: true })
          subject.make(bin: "/path/to/make")
        end
      end

      context "when gmake is present" do
        before do
          allow(Omnibus).to receive(:which)
            .with("gmake")
            .and_return("/bin/gmake")
        end

        it "uses gmake and sets MAKE=gmake" do
          expect(subject).to receive(:command)
            .with("gmake", { env: { "MAKE" => "gmake" }, in_msys_bash: true } )
          subject.make
        end
      end

      context "when gmake is not present" do
        before do
          allow(Omnibus).to receive(:which)
            .and_return(nil)
        end

        it "uses make" do
          expect(subject).to receive(:command)
            .with("make", { in_msys_bash: true } )
          subject.make
        end
      end

      it "accepts 0 options" do
        expect(subject).to receive(:command)
          .with("make", { in_msys_bash: true } )
        expect { subject.make }.to_not raise_error
      end

      it "accepts an additional command string" do
        expect(subject).to receive(:command)
          .with("make install", { in_msys_bash: true } )
        expect { subject.make("install") }.to_not raise_error
      end

      it "persists given options" do
        expect(subject).to receive(:command)
          .with("make", { timeout: 3600, in_msys_bash: true } )
        subject.make(timeout: 3600)
      end
    end

    describe "#configure" do
      before do
        allow(subject).to receive(:command)
      end

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

      context "on 64-bit windows" do
        let(:on_windows) { true }
        let(:windows_i386) { false }

        before do
          allow(subject).to receive(:windows_arch_i386?)
            .and_return(windows_i386)
        end

        it "appends platform host to the options" do
          expect(subject).to receive(:command)
            .with("./configure --build=x86_64-w64-mingw32 --prefix=#{project_dir}/embedded", { in_msys_bash: true } )
          subject.configure
        end
      end

      context "on 32-bit windows" do
        let(:on_windows) { true }
        let(:windows_i386) { true }

        before do
          allow(subject).to receive(:windows_arch_i386?)
            .and_return(windows_i386)
        end

        it "appends platform host to the options" do
          expect(subject).to receive(:command)
            .with("./configure --build=i686-w64-mingw32 --prefix=#{project_dir}/embedded", { in_msys_bash: true } )
          subject.configure
        end
      end

      context "when :bin is present" do
        it "uses the custom bin" do
          expect(subject).to receive(:command)
            .with("/path/to/configure --prefix=#{project_dir}/embedded", { in_msys_bash: true } )
          subject.configure(bin: "/path/to/configure")
        end
      end

      context "when :prefix is present" do
        it "emits non-empty prefix" do
          expect(subject).to receive(:command)
            .with("./configure --prefix=/some/prefix", { in_msys_bash: true } )
          subject.configure(prefix: "/some/prefix")
        end

        it "omits prefix if empty" do
          expect(subject).to receive(:command)
            .with("./configure", { in_msys_bash: true } )
          subject.configure(prefix: "")
        end
      end

      it "accepts 0 options" do
        expect(subject).to receive(:command)
          .with("./configure --prefix=#{project_dir}/embedded", { in_msys_bash: true } )
        expect { subject.configure }.to_not raise_error
      end

      it "accepts an additional command string" do
        expect(subject).to receive(:command)
          .with("./configure --prefix=#{project_dir}/embedded --myopt", { in_msys_bash: true } )
        expect { subject.configure("--myopt") }.to_not raise_error
      end

      it "persists given options" do
        expect(subject).to receive(:command)
          .with("./configure --prefix=#{project_dir}/embedded", { timeout: 3600, in_msys_bash: true } )
        subject.configure(timeout: 3600)
      end
    end

    describe "#patch" do
      before do
        allow(subject).to receive(:find_file)
          .with("config/patches", "good_patch")
          .and_return(
            [ ["#{project_dir}/patch_location1/good_patch", "#{project_dir}/patch_location2/good_patch"],
              "#{project_dir}/patch_location2/good_patch" ]
          )
      end

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

      it "invokes patch with patch level 1 unless specified" do
        expect { subject.patch(source: "good_patch") }.to_not raise_error
        expect(subject).to receive(:shellout!)
          .with("patch -p1 -i #{project_dir}/patch_location2/good_patch", { in_msys_bash: true } )
        run_build_command
      end

      it "invokes patch with patch level provided" do
        expect { subject.patch(source: "good_patch", plevel: 0) }.to_not raise_error
        expect(subject).to receive(:shellout!)
          .with("patch -p0 -i #{project_dir}/patch_location2/good_patch", { in_msys_bash: true } )
        run_build_command
      end

      it "invokes patch differently if target is provided" do
        expect { subject.patch(source: "good_patch", target: "target/path") }.to_not raise_error
        expect(subject).to receive(:shellout!)
          .with("cat #{project_dir}/patch_location2/good_patch | patch -p1 target/path", { in_msys_bash: true } )
        run_build_command
      end

      it "persists other options" do
        expect { subject.patch(source: "good_patch", timeout: 3600) }.to_not raise_error
        expect(subject).to receive(:shellout!)
          .with("patch -p1 -i #{project_dir}/patch_location2/good_patch", { timeout: 3600, in_msys_bash: true } )
        run_build_command
      end
    end

    describe "#shasum" do
      let(:build_step) do
        Proc.new do
          block do
            command("true")
          end
        end
      end

      let(:tmp_dir) { Dir.mktmpdir }
      after { FileUtils.rmdir(tmp_dir) }

      let(:software) do
        double(Software,
          name: "chefdk",
          install_dir: tmp_dir,
          project_dir: tmp_dir,
          overridden?: false)
      end

      let(:before_build_shasum) do
        b = described_class.new(software)
        b.evaluate(&build_step)
        b.shasum
      end

      it "returns the same value when called before or after the build" do
        subject.evaluate(&build_step)
        subject.build
        expect(subject.shasum).to eq(before_build_shasum)
      end
    end
  end
end
