# frozen_string_literal: true
require "spec_helper"

describe Bundler::SharedHelpers do
  let(:ext_lock_double) { double(:ext_lock) }

  before do
    allow(Bundler.rubygems).to receive(:ext_lock).and_return(ext_lock_double)
    allow(ext_lock_double).to receive(:synchronize) {|&block| block.call }
  end

  subject { Bundler::SharedHelpers }

  describe "#default_gemfile" do
    before { ENV["BUNDLE_GEMFILE"] = "/path/Gemfile" }

    context "Gemfile is present" do
      let(:expected_gemfile_path) { Pathname.new("/path/Gemfile") }

      it "returns the Gemfile path" do
        expect(subject.default_gemfile).to eq(expected_gemfile_path)
      end
    end

    context "Gemfile is not present" do
      before { ENV["BUNDLE_GEMFILE"] = nil }

      it "raises a GemfileNotFound error" do
        expect { subject.default_gemfile }.to raise_error(
          Bundler::GemfileNotFound, "Could not locate Gemfile"
        )
      end
    end
  end

  describe "#default_lockfile" do
    context "gemfile is gems.rb" do
      let(:gemfile_path)           { Pathname.new("/path/gems.rb") }
      let(:expected_lockfile_path) { Pathname.new("/path/gems.locked") }

      before { allow(subject).to receive(:default_gemfile).and_return(gemfile_path) }

      it "returns the gems.locked path" do
        expect(subject.default_lockfile).to eq(expected_lockfile_path)
      end
    end

    context "is a regular Gemfile" do
      let(:gemfile_path)           { Pathname.new("/path/Gemfile") }
      let(:expected_lockfile_path) { Pathname.new("/path/Gemfile.lock") }

      before { allow(subject).to receive(:default_gemfile).and_return(gemfile_path) }

      it "returns the lock file path" do
        expect(subject.default_lockfile).to eq(expected_lockfile_path)
      end
    end
  end

  describe "#default_bundle_dir" do
    context ".bundle does not exist" do
      it "returns nil" do
        expect(subject.default_bundle_dir).to be_nil
      end
    end

    context ".bundle is global .bundle" do
      let(:global_rubygems_dir) { Pathname.new("#{bundled_app}") }

      before do
        Dir.mkdir ".bundle"
        allow(Bundler.rubygems).to receive(:user_home).and_return(global_rubygems_dir)
      end

      it "returns nil" do
        expect(subject.default_bundle_dir).to be_nil
      end
    end

    context ".bundle is not global .bundle" do
      let(:global_rubygems_dir)      { Pathname.new("/path/rubygems") }
      let(:expected_bundle_dir_path) { Pathname.new("#{bundled_app}/.bundle") }

      before do
        Dir.mkdir ".bundle"
        allow(Bundler.rubygems).to receive(:user_home).and_return(global_rubygems_dir)
      end

      it "returns the .bundle path" do
        expect(subject.default_bundle_dir).to eq(expected_bundle_dir_path)
      end
    end
  end

  describe "#in_bundle?" do
    it "calls the find_gemfile method" do
      expect(subject).to receive(:find_gemfile)
      subject.in_bundle?
    end

    shared_examples_for "correctly determines whether to return a Gemfile path" do
      context "currently in directory with a Gemfile" do
        before { File.new("Gemfile", "w") }

        it "returns path of the bundle gemfile" do
          expect(subject.in_bundle?).to eq("#{bundled_app}/Gemfile")
        end
      end

      context "currently in directory without a Gemfile" do
        it "returns nil" do
          expect(subject.in_bundle?).to be_nil
        end
      end
    end

    context "ENV['BUNDLE_GEMFILE'] set" do
      before { ENV["BUNDLE_GEMFILE"] = "/path/Gemfile" }

      it "returns ENV['BUNDLE_GEMFILE']" do
        expect(subject.in_bundle?).to eq("/path/Gemfile")
      end
    end

    context "ENV['BUNDLE_GEMFILE'] not set" do
      before { ENV["BUNDLE_GEMFILE"] = nil }

      it_behaves_like "correctly determines whether to return a Gemfile path"
    end

    context "ENV['BUNDLE_GEMFILE'] is blank" do
      before { ENV["BUNDLE_GEMFILE"] = "" }

      it_behaves_like "correctly determines whether to return a Gemfile path"
    end
  end

  describe "#chdir" do
    let(:op_block) { proc { Dir.mkdir "nested_dir" } }

    before { Dir.mkdir "chdir_test_dir" }

    it "executes the passed block while in the specified directory" do
      subject.chdir("chdir_test_dir", &op_block)
      expect(Pathname.new("chdir_test_dir/nested_dir")).to exist
    end
  end

  describe "#pwd" do
    it "returns the current absolute path" do
      expect(subject.pwd).to eq(bundled_app)
    end
  end

  describe "#with_clean_git_env" do
    let(:with_clean_git_env_block) { proc { Dir.mkdir "with_clean_git_env_test_dir" } }

    before do
      ENV["GIT_DIR"] = "ORIGINAL_ENV_GIT_DIR"
      ENV["GIT_WORK_TREE"] = "ORIGINAL_ENV_GIT_WORK_TREE"
    end

    it "executes the passed block" do
      subject.with_clean_git_env(&with_clean_git_env_block)
      expect(Pathname.new("with_clean_git_env_test_dir")).to exist
    end

    context "when a block is passed" do
      let(:with_clean_git_env_block) do
        proc do
          Dir.mkdir "git_dir_test_dir" unless ENV["GIT_DIR"].nil?
          Dir.mkdir "git_work_tree_test_dir" unless ENV["GIT_WORK_TREE"].nil?
        end end

      it "uses a fresh git env for execution" do
        subject.with_clean_git_env(&with_clean_git_env_block)
        expect(Pathname.new("git_dir_test_dir")).to_not exist
        expect(Pathname.new("git_work_tree_test_dir")).to_not exist
      end
    end

    context "passed block does not throw errors" do
      let(:with_clean_git_env_block) do
        proc do
          ENV["GIT_DIR"] = "NEW_ENV_GIT_DIR"
          ENV["GIT_WORK_TREE"] = "NEW_ENV_GIT_WORK_TREE"
        end end

      it "restores the git env after" do
        subject.with_clean_git_env(&with_clean_git_env_block)
        expect(ENV["GIT_DIR"]).to eq("ORIGINAL_ENV_GIT_DIR")
        expect(ENV["GIT_WORK_TREE"]).to eq("ORIGINAL_ENV_GIT_WORK_TREE")
      end
    end

    context "passed block throws errors" do
      let(:with_clean_git_env_block) do
        proc do
          ENV["GIT_DIR"] = "NEW_ENV_GIT_DIR"
          ENV["GIT_WORK_TREE"] = "NEW_ENV_GIT_WORK_TREE"
          raise RuntimeError.new
        end end

      it "restores the git env after" do
        expect { subject.with_clean_git_env(&with_clean_git_env_block) }.to raise_error(RuntimeError)
        expect(ENV["GIT_DIR"]).to eq("ORIGINAL_ENV_GIT_DIR")
        expect(ENV["GIT_WORK_TREE"]).to eq("ORIGINAL_ENV_GIT_WORK_TREE")
      end
    end
  end

  describe "#set_bundle_environment" do
    before do
      ENV["BUNDLE_GEMFILE"] = "Gemfile"
    end

    shared_examples_for "ENV['PATH'] gets set correctly" do
      before { Dir.mkdir ".bundle" }

      it "ensures bundle bin path is in ENV['PATH']" do
        subject.set_bundle_environment
        paths = ENV["PATH"].split(File::PATH_SEPARATOR)
        expect(paths).to include("#{Bundler.bundle_path}/bin")
      end
    end

    shared_examples_for "ENV['RUBYOPT'] gets set correctly" do
      it "ensures -rbundler/setup is at the beginning of ENV['RUBYOPT']" do
        subject.set_bundle_environment
        expect(ENV["RUBYOPT"].split(" ")).to start_with("-rbundler/setup")
      end
    end

    shared_examples_for "ENV['RUBYLIB'] gets set correctly" do
      let(:ruby_lib_path) { "stubbed_ruby_lib_dir" }

      before { allow(File).to receive(:expand_path).and_return(ruby_lib_path) }

      it "ensures bundler's ruby version lib path is in ENV['RUBYLIB']" do
        subject.set_bundle_environment
        paths = (ENV["RUBYLIB"]).split(File::PATH_SEPARATOR)
        expect(paths).to include(ruby_lib_path)
      end
    end

    it "calls the appropriate set methods" do
      expect(subject).to receive(:set_path)
      expect(subject).to receive(:set_rubyopt)
      expect(subject).to receive(:set_rubylib)
      subject.set_bundle_environment
    end

    context "ENV['PATH'] does not exist" do
      before { ENV.delete("PATH") }

      it_behaves_like "ENV['PATH'] gets set correctly"
    end

    context "ENV['PATH'] is empty" do
      before { ENV["PATH"] = "" }

      it_behaves_like "ENV['PATH'] gets set correctly"
    end

    context "ENV['PATH'] exists" do
      before { ENV["PATH"] = "/some_path/bin" }

      it_behaves_like "ENV['PATH'] gets set correctly"
    end

    context "ENV['PATH'] already contains the bundle bin path" do
      let(:bundle_path) { "#{Bundler.bundle_path}/bin" }

      before do
        ENV["PATH"] = bundle_path
      end

      it_behaves_like "ENV['PATH'] gets set correctly"

      it "ENV['PATH'] should only contain one instance of bundle bin path" do
        subject.set_bundle_environment
        paths = (ENV["PATH"]).split(File::PATH_SEPARATOR)
        expect(paths.count(bundle_path)).to eq(1)
      end
    end

    context "ENV['RUBYOPT'] does not exist" do
      before { ENV.delete("RUBYOPT") }

      it_behaves_like "ENV['RUBYOPT'] gets set correctly"
    end

    context "ENV['RUBYOPT'] exists without -rbundler/setup" do
      before { ENV["RUBYOPT"] = "-I/some_app_path/lib" }

      it_behaves_like "ENV['RUBYOPT'] gets set correctly"
    end

    context "ENV['RUBYOPT'] exists and contains -rbundler/setup" do
      before { ENV["RUBYOPT"] = "-rbundler/setup" }

      it_behaves_like "ENV['RUBYOPT'] gets set correctly"
    end

    context "ENV['RUBYLIB'] does not exist" do
      before { ENV.delete("RUBYLIB") }

      it_behaves_like "ENV['RUBYLIB'] gets set correctly"
    end

    context "ENV['RUBYLIB'] is empty" do
      before { ENV["PATH"] = "" }

      it_behaves_like "ENV['RUBYLIB'] gets set correctly"
    end

    context "ENV['RUBYLIB'] exists" do
      before { ENV["PATH"] = "/some_path/bin" }

      it_behaves_like "ENV['RUBYLIB'] gets set correctly"
    end

    context "ENV['RUBYLIB'] already contains the bundler's ruby version lib path" do
      let(:ruby_lib_path) { "stubbed_ruby_lib_dir" }

      before do
        allow(File).to receive(:expand_path).and_return(ruby_lib_path)
        ENV["RUBYLIB"] = ruby_lib_path
      end

      it_behaves_like "ENV['RUBYLIB'] gets set correctly"

      it "ENV['RUBYLIB'] should only contain one instance of bundler's ruby version lib path" do
        subject.set_bundle_environment
        paths = (ENV["RUBYLIB"]).split(File::PATH_SEPARATOR)
        expect(paths.count(ruby_lib_path)).to eq(1)
      end
    end
  end

  describe "#filesystem_access" do
    context "system has proper permission access" do
      let(:file_op_block) { proc {|path| FileUtils.mkdir_p(path) } }

      it "performs the operation in the passed block" do
        subject.filesystem_access("./test_dir", &file_op_block)
        expect(Pathname.new("test_dir")).to exist
      end
    end

    context "system throws Errno::EACESS" do
      let(:file_op_block) { proc {|_path| raise Errno::EACCES } }

      it "raises a PermissionError" do
        expect { subject.filesystem_access("/path", &file_op_block) }.to raise_error(
          Bundler::PermissionError
        )
      end
    end

    context "system throws Errno::EAGAIN" do
      let(:file_op_block) { proc {|_path| raise Errno::EAGAIN } }

      it "raises a TemporaryResourceError" do
        expect { subject.filesystem_access("/path", &file_op_block) }.to raise_error(
          Bundler::TemporaryResourceError
        )
      end
    end

    context "system throws Errno::EPROTO" do
      let(:file_op_block) { proc {|_path| raise Errno::EPROTO } }

      it "raises a VirtualProtocolError" do
        expect { subject.filesystem_access("/path", &file_op_block) }.to raise_error(
          Bundler::VirtualProtocolError
        )
      end
    end

    context "system throws Errno::ENOTSUP", :ruby => "1.9" do
      let(:file_op_block) { proc {|_path| raise Errno::ENOTSUP } }

      it "raises a OperationNotSupportedError" do
        expect { subject.filesystem_access("/path", &file_op_block) }.to raise_error(
          Bundler::OperationNotSupportedError
        )
      end
    end
  end

  describe "#const_get_safely" do
    module TargetNamespace
      VALID_CONSTANT = 1
    end

    context "when the namespace does have the requested constant" do
      it "returns the value of the requested constant" do
        expect(subject.const_get_safely(:VALID_CONSTANT, TargetNamespace)).to eq(1)
      end
    end

    context "when the requested constant is passed as a string" do
      it "returns the value of the requested constant" do
        expect(subject.const_get_safely("VALID_CONSTANT", TargetNamespace)).to eq(1)
      end
    end

    context "when the namespace does not have the requested constant" do
      it "returns nil" do
        expect(subject.const_get_safely("INVALID_CONSTANT", TargetNamespace)).to be_nil
      end
    end
  end
end
