# frozen_string_literal: true

RSpec.describe "bundle install with gem sources" do
  describe "the simple case" do
    it "prints output and returns if no dependencies are specified" do
      gemfile <<-G
        source "https://gem.repo1"
      G

      bundle :install
      expect(err).to match(/no dependencies/)
    end

    it "does not make a lockfile if the install fails" do
      install_gemfile <<-G, raise_on_error: false
        raise StandardError, "FAIL"
      G

      expect(err).to include('StandardError, "FAIL"')
      expect(bundled_app_lock).not_to exist
    end

    it "creates a Gemfile.lock" do
      install_gemfile <<-G
        source "https://gem.repo1"
        gem "myrack"
      G

      expect(bundled_app_lock).to exist
    end

    it "does not create ./.bundle by default", bundler: "< 3" do
      gemfile <<-G
        source "https://gem.repo1"
        gem "myrack"
      G

      bundle :install # can't use install_gemfile since it sets retry
      expect(bundled_app(".bundle")).not_to exist
    end

    it "does not create ./.bundle by default when installing to system gems" do
      gemfile <<-G
        source "https://gem.repo1"
        gem "myrack"
      G

      bundle :install, env: { "BUNDLE_PATH__SYSTEM" => "true" } # can't use install_gemfile since it sets retry
      expect(bundled_app(".bundle")).not_to exist
    end

    it "creates lock files based on the Gemfile name" do
      gemfile bundled_app("OmgFile"), <<-G
        source "https://gem.repo1"
        gem "myrack", "1.0"
      G

      bundle "install --gemfile OmgFile"

      expect(bundled_app("OmgFile.lock")).to exist
    end

    it "doesn't delete the lockfile if one already exists" do
      install_gemfile <<-G
        source "https://gem.repo1"
        gem 'myrack'
      G

      lockfile = File.read(bundled_app_lock)

      install_gemfile <<-G, raise_on_error: false
        raise StandardError, "FAIL"
      G

      expect(File.read(bundled_app_lock)).to eq(lockfile)
    end

    it "does not touch the lockfile if nothing changed" do
      install_gemfile <<-G
        source "https://gem.repo1"
        gem "myrack"
      G

      expect { run "1" }.not_to change { File.mtime(bundled_app_lock) }
    end

    it "fetches gems" do
      install_gemfile <<-G
        source "https://gem.repo1"
        gem 'myrack'
      G

      expect(default_bundle_path("gems/myrack-1.0.0")).to exist
      expect(the_bundle).to include_gems("myrack 1.0.0")
    end

    it "auto-heals missing gems" do
      install_gemfile <<-G
        source "https://gem.repo1"
        gem 'myrack'
      G

      FileUtils.rm_rf(default_bundle_path("gems/myrack-1.0.0"))

      bundle "install --verbose"

      expect(out).to include("Installing myrack 1.0.0")
      expect(default_bundle_path("gems/myrack-1.0.0")).to exist
      expect(the_bundle).to include_gems("myrack 1.0.0")
    end

    it "fetches gems when multiple versions are specified" do
      install_gemfile <<-G
        source "https://gem.repo1"
        gem 'myrack', "> 0.9", "< 1.0"
      G

      expect(default_bundle_path("gems/myrack-0.9.1")).to exist
      expect(the_bundle).to include_gems("myrack 0.9.1")
    end

    it "fetches gems when multiple versions are specified take 2" do
      install_gemfile <<-G
        source "https://gem.repo1"
        gem 'myrack', "< 1.0", "> 0.9"
      G

      expect(default_bundle_path("gems/myrack-0.9.1")).to exist
      expect(the_bundle).to include_gems("myrack 0.9.1")
    end

    it "raises an appropriate error when gems are specified using symbols" do
      install_gemfile <<-G, raise_on_error: false
        source "https://gem.repo1"
        gem :myrack
      G
      expect(exitstatus).to eq(4)
    end

    it "pulls in dependencies" do
      install_gemfile <<-G
        source "https://gem.repo1"
        gem "rails"
      G

      expect(the_bundle).to include_gems "actionpack 2.3.2", "rails 2.3.2"
    end

    it "does the right version" do
      install_gemfile <<-G
        source "https://gem.repo1"
        gem "myrack", "0.9.1"
      G

      expect(the_bundle).to include_gems "myrack 0.9.1"
    end

    it "does not install the development dependency" do
      build_repo2 do
        build_gem "with_development_dependency" do |s|
          s.add_development_dependency "activesupport", "= 2.3.5"
        end
      end

      install_gemfile <<-G
        source "https://gem.repo2"
        gem "with_development_dependency"
      G

      expect(the_bundle).to include_gems("with_development_dependency 1.0.0").
        and not_include_gems("activesupport 2.3.5")
    end

    it "resolves correctly" do
      install_gemfile <<-G
        source "https://gem.repo1"
        gem "activemerchant"
        gem "rails"
      G

      expect(the_bundle).to include_gems "activemerchant 1.0", "activesupport 2.3.2", "actionpack 2.3.2"
    end

    it "activates gem correctly according to the resolved gems" do
      install_gemfile <<-G
        source "https://gem.repo1"
        gem "activesupport", "2.3.5"
      G

      install_gemfile <<-G
        source "https://gem.repo1"
        gem "activemerchant"
        gem "rails"
      G

      expect(the_bundle).to include_gems "activemerchant 1.0", "activesupport 2.3.2", "actionpack 2.3.2"
    end

    it "does not reinstall any gem that is already available locally" do
      system_gems "activesupport-2.3.2", path: default_bundle_path

      build_repo2 do
        build_gem "activesupport", "2.3.2" do |s|
          s.write "lib/activesupport.rb", "ACTIVESUPPORT = 'fail'"
        end
      end

      install_gemfile <<-G
        source "https://gem.repo2"
        gem "activerecord", "2.3.2"
      G

      expect(the_bundle).to include_gems "activesupport 2.3.2"
    end

    it "works when the gemfile specifies gems that only exist in the system" do
      build_gem "foo", to_bundle: true
      install_gemfile <<-G
        source "https://gem.repo1"
        gem "myrack"
        gem "foo"
      G

      expect(the_bundle).to include_gems "myrack 1.0.0", "foo 1.0.0"
    end

    it "prioritizes local gems over remote gems" do
      build_gem "myrack", "9.0.0", to_bundle: true

      install_gemfile <<-G
        source "https://gem.repo1"
        gem "myrack"
      G

      expect(the_bundle).to include_gems "myrack 9.0.0"
    end

    it "loads env plugins" do
      plugin_msg = "hello from an env plugin!"
      create_file "plugins/rubygems_plugin.rb", "puts '#{plugin_msg}'"
      rubylib = ENV["RUBYLIB"].to_s.split(File::PATH_SEPARATOR).unshift(bundled_app("plugins").to_s).join(File::PATH_SEPARATOR)
      install_gemfile <<-G, env: { "RUBYLIB" => rubylib }
        source "https://gem.repo1"
        gem "myrack"
      G

      expect(last_command.stdboth).to include(plugin_msg)
    end

    describe "with a gem that installs multiple platforms" do
      it "installs gems for the local platform as first choice" do
        simulate_platform "x86-darwin-100" do
          install_gemfile <<-G
            source "https://gem.repo1"
            gem "platform_specific"
          G

          expect(the_bundle).to include_gems("platform_specific 1.0 x86-darwin-100")
        end
      end

      it "falls back on plain ruby" do
        simulate_platform "foo-bar-baz" do
          install_gemfile <<-G
            source "https://gem.repo1"
            gem "platform_specific"
          G

          expect(the_bundle).to include_gems("platform_specific 1.0 ruby")
        end
      end

      it "installs gems for java" do
        simulate_platform "java" do
          install_gemfile <<-G
            source "https://gem.repo1"
            gem "platform_specific"
          G

          expect(the_bundle).to include_gems("platform_specific 1.0 java")
        end
      end

      it "installs gems for windows" do
        simulate_platform x86_mswin32 do
          install_gemfile <<-G
            source "https://gem.repo1"
            gem "platform_specific"
          G

          expect(the_bundle).to include_gems("platform_specific 1.0 x86-mswin32")
        end
      end
    end

    describe "doing bundle install foo" do
      before do
        gemfile <<-G
          source "https://gem.repo1"
          gem "myrack"
        G
      end

      it "works" do
        bundle "config set --local path vendor"
        bundle "install"
        expect(the_bundle).to include_gems "myrack 1.0"
      end

      it "allows running bundle install --system without deleting foo", bundler: "< 3" do
        bundle "install --path vendor"
        bundle "install --system"
        FileUtils.rm_rf(bundled_app("vendor"))
        expect(the_bundle).to include_gems "myrack 1.0"
      end

      it "allows running bundle install --system after deleting foo", bundler: "< 3" do
        bundle "install --path vendor"
        FileUtils.rm_rf(bundled_app("vendor"))
        bundle "install --system"
        expect(the_bundle).to include_gems "myrack 1.0"
      end
    end

    it "finds gems in multiple sources", bundler: "< 3" do
      build_repo2 do
        build_gem "myrack", "1.2" do |s|
          s.executables = "myrackup"
        end
      end

      install_gemfile <<-G, artifice: "compact_index_extra"
        source "https://gemserver.test"
        source "https://gemserver.test/extra"

        gem "activesupport", "1.2.3"
        gem "myrack", "1.2"
      G

      expect(the_bundle).to include_gems "myrack 1.2", "activesupport 1.2.3"
    end

    it "gives a useful error if no sources are set" do
      install_gemfile <<-G, raise_on_error: false
        gem "myrack"
      G

      expect(err).to include("This Gemfile does not include an explicit global source. " \
        "Not using an explicit global source may result in a different lockfile being generated depending on " \
        "the gems you have installed locally before bundler is run. " \
        "Instead, define a global source in your Gemfile like this: source \"https://rubygems.org\".")
    end

    it "creates a Gemfile.lock on a blank Gemfile" do
      install_gemfile <<-G
        source "https://gem.repo1"
      G

      expect(File.exist?(bundled_app_lock)).to eq(true)
    end

    it "throws a warning if a gem is added twice in Gemfile without version requirements" do
      build_repo2

      install_gemfile <<-G
        source "https://gem.repo2"
        gem "myrack"
        gem "myrack"
      G

      expect(err).to include("Your Gemfile lists the gem myrack (>= 0) more than once.")
      expect(err).to include("Remove any duplicate entries and specify the gem only once.")
      expect(err).to include("While it's not a problem now, it could cause errors if you change the version of one of them later.")
    end

    it "throws a warning if a gem is added twice in Gemfile with same versions" do
      build_repo2

      install_gemfile <<-G
        source "https://gem.repo2"
        gem "myrack", "1.0"
        gem "myrack", "1.0"
      G

      expect(err).to include("Your Gemfile lists the gem myrack (= 1.0) more than once.")
      expect(err).to include("Remove any duplicate entries and specify the gem only once.")
      expect(err).to include("While it's not a problem now, it could cause errors if you change the version of one of them later.")
    end

    it "throws a warning if a gem is added twice under different platforms and does not crash when using the generated lockfile" do
      build_repo2

      install_gemfile <<-G
        source "https://gem.repo2"
        gem "myrack", :platform => :jruby
        gem "myrack"
      G

      bundle "install"

      expect(err).to include("Your Gemfile lists the gem myrack (>= 0) more than once.")
      expect(err).to include("Remove any duplicate entries and specify the gem only once.")
      expect(err).to include("While it's not a problem now, it could cause errors if you change the version of one of them later.")
    end

    it "does not throw a warning if a gem is added once in Gemfile and also inside a gemspec as a development dependency" do
      build_lib "my-gem", path: bundled_app do |s|
        s.add_development_dependency "my-private-gem"
      end

      build_repo2 do
        build_gem "my-private-gem"
      end

      gemfile <<~G
        source "https://gem.repo2"

        gemspec

        gem "my-private-gem", :group => :development
      G

      bundle :install

      expect(err).to be_empty
      expect(the_bundle).to include_gems("my-private-gem 1.0")
    end

    it "throws a warning if a gem is added once in Gemfile and also inside a gemspec as a development dependency, with different requirements" do
      build_lib "my-gem", path: bundled_app do |s|
        s.add_development_dependency "rubocop", "~> 1.36.0"
      end

      build_repo4 do
        build_gem "rubocop", "1.36.0"
        build_gem "rubocop", "1.37.1"
      end

      gemfile <<~G
        source "https://gem.repo4"

        gemspec

        gem "rubocop", group: :development
      G

      bundle :install

      expect(err).to include("A gemspec development dependency (rubocop, ~> 1.36.0) is being overridden by a Gemfile dependency (rubocop, >= 0).")
      expect(err).to include("This behaviour may change in the future. Please remove either of them, or make sure they both have the same requirement")

      # This is not the best behavior I believe, it would be better if both
      # requirements are considered if they are compatible, and a version
      # satisfying both is chosen. But not sure about changing it right now, so
      # I went with a warning for the time being.
      expect(the_bundle).to include_gems("rubocop 1.37.1")
    end

    it "includes the gem without warning if two gemspecs add it with the same requirement" do
      gem1 = tmp("my-gem-1")
      gem2 = tmp("my-gem-2")

      build_lib "my-gem", path: gem1 do |s|
        s.add_development_dependency "rubocop", "~> 1.36.0"
      end

      build_lib "my-gem-2", path: gem2 do |s|
        s.add_development_dependency "rubocop", "~> 1.36.0"
      end

      build_repo4 do
        build_gem "rubocop", "1.36.0"
      end

      gemfile <<~G
        source "https://gem.repo4"

        gemspec path: "#{gem1}"
        gemspec path: "#{gem2}"
      G

      bundle :install

      expect(err).to be_empty
      expect(the_bundle).to include_gems("rubocop 1.36.0")
    end

    it "includes the gem without warning if two gemspecs add it with compatible requirements" do
      gem1 = tmp("my-gem-1")
      gem2 = tmp("my-gem-2")

      build_lib "my-gem", path: gem1 do |s|
        s.add_development_dependency "rubocop", "~> 1.0"
      end

      build_lib "my-gem-2", path: gem2 do |s|
        s.add_development_dependency "rubocop", "~> 1.36.0"
      end

      build_repo4 do
        build_gem "rubocop", "1.36.0"
      end

      gemfile <<~G
        source "https://gem.repo4"

        gemspec path: "#{gem1}"
        gemspec path: "#{gem2}"
      G

      bundle :install

      expect(err).to be_empty
      expect(the_bundle).to include_gems("rubocop 1.36.0")
    end

    it "errors out if two gemspecs add it with incompatible requirements" do
      gem1 = tmp("my-gem-1")
      gem2 = tmp("my-gem-2")

      build_lib "my-gem", path: gem1 do |s|
        s.add_development_dependency "rubocop", "~> 2.0"
      end

      build_lib "my-gem-2", path: gem2 do |s|
        s.add_development_dependency "rubocop", "~> 1.36.0"
      end

      build_repo4 do
        build_gem "rubocop", "1.36.0"
      end

      gemfile <<~G
        source "https://gem.repo4"

        gemspec path: "#{gem1}"
        gemspec path: "#{gem2}"
      G

      bundle :install, raise_on_error: false

      expect(err).to include("Two gemspecs have conflicting requirements on the same gem: rubocop (~> 1.36.0, development) and rubocop (~> 2.0, development). Bundler cannot continue.")
    end

    it "warns when a Gemfile dependency is overriding a gemspec development dependency, with different requirements" do
      build_lib "my-gem", path: bundled_app do |s|
        s.add_development_dependency "rails", ">= 5"
      end

      build_repo4 do
        build_gem "rails", "7.0.8"
      end

      gemfile <<~G
        source "https://gem.repo4"

        gem "rails", "~> 7.0.8"

        gemspec
      G

      bundle :install

      expect(err).to include("A gemspec development dependency (rails, >= 5) is being overridden by a Gemfile dependency (rails, ~> 7.0.8).")
      expect(err).to include("This behaviour may change in the future. Please remove either of them, or make sure they both have the same requirement")

      # This is not the best behavior I believe, it would be better if both
      # requirements are considered if they are compatible, and a version
      # satisfying both is chosen. But not sure about changing it right now, so
      # I went with a warning for the time being.
      expect(the_bundle).to include_gems("rails 7.0.8")
    end

    it "does not warn if a gem is added once in Gemfile and also inside a gemspec as a development dependency, with same requirements, and different sources" do
      build_lib "my-gem", path: bundled_app do |s|
        s.add_development_dependency "activesupport"
      end

      build_repo4 do
        build_gem "activesupport"
      end

      build_git "activesupport", "1.0", path: lib_path("activesupport")

      install_gemfile <<~G
        source "https://gem.repo4"

        gemspec

        gem "activesupport", :git => "#{lib_path("activesupport")}"
      G

      expect(err).to be_empty
      expect(the_bundle).to include_gems "activesupport 1.0", source: "git@#{lib_path("activesupport")}"

      # if the Gemfile dependency is specified first
      install_gemfile <<~G
        source "https://gem.repo4"

        gem "activesupport", :git => "#{lib_path("activesupport")}"

        gemspec
      G

      expect(err).to be_empty
      expect(the_bundle).to include_gems "activesupport 1.0", source: "git@#{lib_path("activesupport")}"
    end

    it "considers both dependencies for resolution if a gem is added once in Gemfile and also inside a local gemspec as a runtime dependency, with different requirements" do
      build_lib "my-gem", path: bundled_app do |s|
        s.add_dependency "rubocop", "~> 1.36.0"
      end

      build_repo4 do
        build_gem "rubocop", "1.36.0"
        build_gem "rubocop", "1.37.1"
      end

      gemfile <<~G
        source "https://gem.repo4"

        gemspec

        gem "rubocop"
      G

      bundle :install

      expect(err).to be_empty
      expect(the_bundle).to include_gems("rubocop 1.36.0")
    end

    it "throws an error if a gem is added twice in Gemfile when version of one dependency is not specified" do
      install_gemfile <<-G, raise_on_error: false
        source "https://gem.repo2"
        gem "myrack"
        gem "myrack", "1.0"
      G

      expect(err).to include("You cannot specify the same gem twice with different version requirements")
      expect(err).to include("You specified: myrack (>= 0) and myrack (= 1.0).")
    end

    it "throws an error if a gem is added twice in Gemfile when different versions of both dependencies are specified" do
      install_gemfile <<-G, raise_on_error: false
        source "https://gem.repo2"
        gem "myrack", "1.0"
        gem "myrack", "1.1"
      G

      expect(err).to include("You cannot specify the same gem twice with different version requirements")
      expect(err).to include("You specified: myrack (= 1.0) and myrack (= 1.1).")
    end

    it "gracefully handles error when rubygems server is unavailable" do
      skip "networking issue" if Gem.win_platform?

      install_gemfile <<-G, artifice: nil, raise_on_error: false
        source "https://gem.repo1"
        source "http://0.0.0.0:9384" do
          gem 'foo'
        end
      G

      expect(err).to include("Could not fetch specs from http://0.0.0.0:9384/")
      expect(err).not_to include("file://")
    end

    it "fails gracefully when downloading an invalid specification from the full index" do
      build_repo2(build_compact_index: false) do
        build_gem "ajp-rails", "0.0.0", gemspec: false, skip_validation: true do |s|
          bad_deps = [["ruby-ajp", ">= 0.2.0"], ["rails", ">= 0.14"]]
          s.
            instance_variable_get(:@spec).
            instance_variable_set(:@dependencies, bad_deps)

          raise "failed to set bad deps" unless s.dependencies == bad_deps
        end
        build_gem "ruby-ajp", "1.0.0"
      end

      install_gemfile <<-G, full_index: true, raise_on_error: false
        source "https://gem.repo2"

        gem "ajp-rails", "0.0.0"
      G

      expect(last_command.stdboth).not_to match(/Error Report/i)
      expect(err).to include("An error occurred while installing ajp-rails (0.0.0), and Bundler cannot continue.").
        and include("Bundler::APIResponseInvalidDependenciesError")
    end

    it "doesn't blow up when the local .bundle/config is empty" do
      FileUtils.mkdir_p(bundled_app(".bundle"))
      FileUtils.touch(bundled_app(".bundle/config"))

      install_gemfile(<<-G)
        source "https://gem.repo1"

        gem 'foo'
      G
    end

    it "doesn't blow up when the global .bundle/config is empty" do
      FileUtils.mkdir_p("#{Bundler.rubygems.user_home}/.bundle")
      FileUtils.touch("#{Bundler.rubygems.user_home}/.bundle/config")

      install_gemfile(<<-G)
        source "https://gem.repo1"

        gem 'foo'
      G
    end
  end

  describe "Ruby version in Gemfile.lock" do
    context "and using an unsupported Ruby version" do
      it "prints an error" do
        install_gemfile <<-G, raise_on_error: false
          ruby '~> 1.2'
          source "https://gem.repo1"
        G
        expect(err).to include("Your Ruby version is #{Gem.ruby_version}, but your Gemfile specified ~> 1.2")
      end
    end

    context "and using a supported Ruby version" do
      before do
        install_gemfile <<-G
          ruby '~> #{Gem.ruby_version}'
          source "https://gem.repo1"
        G
      end

      it "writes current Ruby version to Gemfile.lock" do
        checksums = checksums_section_when_enabled
        expect(lockfile).to eq <<~L
         GEM
           remote: https://gem.repo1/
           specs:

         PLATFORMS
           #{lockfile_platforms}

         DEPENDENCIES
         #{checksums}
         RUBY VERSION
            #{Bundler::RubyVersion.system}

         BUNDLED WITH
            #{Bundler::VERSION}
        L
      end

      it "updates Gemfile.lock with updated yet still compatible ruby version" do
        install_gemfile <<-G
          ruby '~> #{current_ruby_minor}'
          source "https://gem.repo1"
        G

        checksums = checksums_section_when_enabled

        expect(lockfile).to eq <<~L
         GEM
           remote: https://gem.repo1/
           specs:

         PLATFORMS
           #{lockfile_platforms}

         DEPENDENCIES
         #{checksums}
         RUBY VERSION
            #{Bundler::RubyVersion.system}

         BUNDLED WITH
            #{Bundler::VERSION}
        L
      end

      it "does not crash when unlocking" do
        gemfile <<-G
          source "https://gem.repo1"
          ruby '>= 2.1.0'
        G

        bundle "update"

        expect(err).not_to include("Could not find gem 'Ruby")
      end
    end
  end

  describe "when Bundler root contains regex chars" do
    it "doesn't blow up when using the `gem` DSL" do
      root_dir = tmp("foo[]bar")

      FileUtils.mkdir_p(root_dir)

      build_lib "foo"
      gemfile = <<-G
        source "https://gem.repo1"
        gem 'foo', :path => "#{lib_path("foo-1.0")}"
      G
      File.open("#{root_dir}/Gemfile", "w") do |file|
        file.puts gemfile
      end

      bundle :install, dir: root_dir
    end

    it "doesn't blow up when using the `gemspec` DSL" do
      root_dir = tmp("foo[]bar")

      FileUtils.mkdir_p(root_dir)

      build_lib "foo", path: root_dir
      gemfile = <<-G
        source "https://gem.repo1"
        gemspec
      G
      File.open("#{root_dir}/Gemfile", "w") do |file|
        file.puts gemfile
      end

      bundle :install, dir: root_dir
    end
  end

  describe "when requesting a quiet install via --quiet" do
    it "should be quiet if there are no warnings" do
      bundle "config set force_ruby_platform true"

      gemfile <<-G
        source "https://gem.repo1"
        gem 'myrack'
      G

      bundle :install, quiet: true
      expect(out).to be_empty
      expect(err).to be_empty
    end

    it "should still display warnings and errors" do
      bundle "config set force_ruby_platform true"

      create_file("install_with_warning.rb", <<~RUBY)
        require "#{lib_dir}/bundler"
        require "#{lib_dir}/bundler/cli"
        require "#{lib_dir}/bundler/cli/install"

        module RunWithWarning
          def run
            super
          rescue
            Bundler.ui.warn "BOOOOO"
            raise
          end
        end

        Bundler::CLI::Install.prepend(RunWithWarning)
      RUBY

      gemfile <<-G
        source "https://gem.repo1"
        gem 'non-existing-gem'
      G

      bundle :install, quiet: true, raise_on_error: false, env: { "RUBYOPT" => "-r#{bundled_app("install_with_warning.rb")}" }
      expect(out).to be_empty
      expect(err).to include("Could not find gem 'non-existing-gem'")
      expect(err).to include("BOOOOO")
    end
  end

  describe "when bundle path does not have cd permission", :permissions do
    let(:bundle_path) { bundled_app("vendor") }

    before do
      FileUtils.mkdir_p(bundle_path)
      gemfile <<-G
        source "https://gem.repo1"
        gem 'myrack'
      G
    end

    it "should display a proper message to explain the problem" do
      FileUtils.chmod(0o500, bundle_path)

      bundle "config set --local path vendor"
      bundle :install, raise_on_error: false
      expect(err).to include(bundle_path.to_s)
      expect(err).to include("grant executable permissions")
    end
  end

  describe "when bundle gems path does not have cd permission", :permissions do
    let(:gems_path) { bundled_app("vendor/#{Bundler.ruby_scope}/gems") }

    before do
      FileUtils.mkdir_p(gems_path)
      gemfile <<-G
        source "https://gem.repo1"
        gem 'myrack'
      G
    end

    it "should display a proper message to explain the problem" do
      FileUtils.chmod("-x", gems_path)
      bundle "config set --local path vendor"

      begin
        bundle :install, raise_on_error: false
      ensure
        FileUtils.chmod("+x", gems_path)
      end

      expect(err).not_to include("ERROR REPORT TEMPLATE")

      expect(err).to include(
        "There was an error while trying to create `#{gems_path.join("myrack-1.0.0")}`. " \
        "It is likely that you need to grant executable permissions for all parent directories and write permissions for `#{gems_path}`."
      )
    end
  end

  describe "when there's an empty install folder (like with default gems) without cd permissions", :permissions do
    let(:full_gem_path) { bundled_app("vendor/#{Bundler.ruby_scope}/gems/myrack-1.0.0") }

    before do
      FileUtils.mkdir_p(full_gem_path)
      gemfile <<-G
        source "https://gem.repo1"
        gem 'myrack'
      G
    end

    it "should display a proper message to explain the problem" do
      FileUtils.chmod("-x", full_gem_path)
      bundle "config set --local path vendor"

      begin
        bundle :install, raise_on_error: false
      ensure
        FileUtils.chmod("+x", full_gem_path)
      end

      expect(err).not_to include("ERROR REPORT TEMPLATE")

      expect(err).to include(
        "There was an error while trying to write to `#{full_gem_path}`. " \
        "It is likely that you need to grant write permissions for that path."
      )
    end
  end

  describe "when bundle bin dir does not have cd permission", :permissions do
    let(:bin_dir) { bundled_app("vendor/#{Bundler.ruby_scope}/bin") }

    before do
      FileUtils.mkdir_p(bin_dir)
      gemfile <<-G
        source "https://gem.repo1"
        gem 'myrack'
      G
    end

    it "should display a proper message to explain the problem" do
      FileUtils.chmod("-x", bin_dir)
      bundle "config set --local path vendor"

      begin
        bundle :install, raise_on_error: false
      ensure
        FileUtils.chmod("+x", bin_dir)
      end

      expect(err).not_to include("ERROR REPORT TEMPLATE")

      expect(err).to include(
        "There was an error while trying to write to `#{bin_dir}`. " \
        "It is likely that you need to grant write permissions for that path."
      )
    end
  end

  describe "when bundle bin dir does not have write access", :permissions do
    let(:bin_dir) { bundled_app("vendor/#{Bundler.ruby_scope}/bin") }

    before do
      FileUtils.mkdir_p(bin_dir)
      gemfile <<-G
        source "https://gem.repo1"
        gem "myrack"
      G
    end

    it "should display a proper message to explain the problem" do
      FileUtils.chmod("-w", bin_dir)
      bundle "config set --local path vendor"

      begin
        bundle :install, raise_on_error: false
      ensure
        FileUtils.chmod("+w", bin_dir)
      end

      expect(err).not_to include("ERROR REPORT TEMPLATE")

      expect(err).to include(
        "There was an error while trying to write to `#{bin_dir}`. " \
        "It is likely that you need to grant write permissions for that path."
      )
    end
  end

  describe "when bundle extensions path does not have write access", :permissions do
    let(:extensions_path) { bundled_app("vendor/#{Bundler.ruby_scope}/extensions/#{Gem::Platform.local}/#{Gem.extension_api_version}") }

    before do
      FileUtils.mkdir_p(extensions_path)
      gemfile <<-G
        source "https://gem.repo1"
        gem 'simple_binary'
      G
    end

    it "should display a proper message to explain the problem" do
      FileUtils.chmod("-x", extensions_path)
      bundle "config set --local path vendor"

      begin
        bundle :install, raise_on_error: false
      ensure
        FileUtils.chmod("+x", extensions_path)
      end

      expect(err).not_to include("ERROR REPORT TEMPLATE")

      expect(err).to include(
        "There was an error while trying to create `#{extensions_path.join("simple_binary-1.0")}`. " \
        "It is likely that you need to grant executable permissions for all parent directories and write permissions for `#{extensions_path}`."
      )
    end
  end

  describe "when the path of a specific gem does not have cd permission", :permissions do
    let(:gems_path) { bundled_app("vendor/#{Bundler.ruby_scope}/gems") }
    let(:foo_path) { gems_path.join("foo-1.0.0") }

    before do
      build_repo4 do
        build_gem "foo", "1.0.0" do |s|
          s.write "CHANGELOG.md", "foo"
        end
      end

      gemfile <<-G
        source "https://gem.repo4"
        gem 'foo'
      G
    end

    it "should display a proper message to explain the problem" do
      bundle "config set --local path vendor"
      bundle :install
      expect(out).to include("Bundle complete!")
      expect(err).to be_empty

      FileUtils.chmod("-x", foo_path)

      begin
        bundle "install --redownload", raise_on_error: false
      ensure
        FileUtils.chmod("+x", foo_path)
      end

      expect(err).not_to include("ERROR REPORT TEMPLATE")
      expect(err).to include("Could not delete previous installation of `#{foo_path}`.")
      expect(err).to include("The underlying error was Errno::EACCES")
    end
  end

  describe "when gem home does not have the writable bit set, yet it's still writable", :permissions do
    let(:gem_home) { bundled_app("vendor/#{Bundler.ruby_scope}") }

    before do
      build_repo4 do
        build_gem "foo", "1.0.0" do |s|
          s.write "CHANGELOG.md", "foo"
        end
      end

      gemfile <<-G
        source "https://gem.repo4"
        gem 'foo'
      G
    end

    it "should still work" do
      bundle "config set --local path vendor"
      bundle :install
      expect(out).to include("Bundle complete!")
      expect(err).to be_empty

      FileUtils.chmod("-w", gem_home)

      begin
        bundle "install --redownload"
      ensure
        FileUtils.chmod("+w", gem_home)
      end

      expect(out).to include("Bundle complete!")
      expect(err).to be_empty
    end
  end

  describe "when gems path is world writable (no sticky bit set)", :permissions do
    let(:gems_path) { bundled_app("vendor/#{Bundler.ruby_scope}/gems") }

    before do
      build_repo4 do
        build_gem "foo", "1.0.0" do |s|
          s.write "CHANGELOG.md", "foo"
        end
      end

      gemfile <<-G
        source "https://gem.repo4"
        gem 'foo'
      G
    end

    it "should display a proper message to explain the problem" do
      bundle "config set --local path vendor"
      bundle :install
      expect(out).to include("Bundle complete!")
      expect(err).to be_empty

      FileUtils.chmod(0o777, gems_path)

      bundle "install --redownload", raise_on_error: false

      expect(err).to include("Bundler cannot reinstall foo-1.0.0 because there's a previous installation of it at #{gems_path}/foo-1.0.0 that is unsafe to remove")
    end
  end

  describe "when gems path is world writable (no sticky bit set), but previous install is just an empty dir (like it happens with default gems)", :permissions do
    let(:gems_path) { bundled_app("vendor/#{Bundler.ruby_scope}/gems") }
    let(:full_path) { gems_path.join("foo-1.0.0") }

    before do
      build_repo4 do
        build_gem "foo", "1.0.0" do |s|
          s.write "CHANGELOG.md", "foo"
        end
      end

      gemfile <<-G
        source "https://gem.repo4"
        gem 'foo'
      G
    end

    it "does not try to remove the directory and thus don't abort with an error about unsafe directory removal" do
      bundle "config set --local path vendor"

      FileUtils.mkdir_p(gems_path)
      FileUtils.chmod(0o777, gems_path)
      Dir.mkdir(full_path)

      bundle "install"
    end
  end

  describe "when bundle cache path does not have write access", :permissions do
    let(:cache_path) { bundled_app("vendor/#{Bundler.ruby_scope}/cache") }

    before do
      FileUtils.mkdir_p(cache_path)
      gemfile <<-G
        source "https://gem.repo1"
        gem 'myrack'
      G
    end

    it "should display a proper message to explain the problem" do
      FileUtils.chmod(0o500, cache_path)

      bundle "config set --local path vendor"
      bundle :install, raise_on_error: false
      expect(err).to include(cache_path.to_s)
      expect(err).to include("grant write permissions")
    end
  end

  describe "when gemspecs are unreadable", :permissions do
    let(:gemspec_path) { vendored_gems("specifications/myrack-1.0.0.gemspec") }

    before do
      gemfile <<~G
        source "https://gem.repo1"
        gem 'myrack'
      G
      bundle "config path vendor/bundle"
      bundle :install
      expect(out).to include("Bundle complete!")
      expect(err).to be_empty

      FileUtils.chmod("-r", gemspec_path)
    end

    it "shows a good error" do
      bundle :install, raise_on_error: false
      expect(err).to include(gemspec_path.to_s)
      expect(err).to include("grant read permissions")
    end
  end

  describe "when configured path is UTF-8 and a file inside a gem package too" do
    let(:app_path) do
      path = tmp("♥")
      FileUtils.mkdir_p(path)
      path
    end

    let(:path) do
      root.join("vendor/bundle")
    end

    before do
      build_repo4 do
        build_gem "mygem" do |s|
          s.write "spec/fixtures/_posts/2016-04-01-错误.html"
        end
      end
    end

    it "works" do
      bundle "config path #{app_path}/vendor/bundle", dir: app_path

      install_gemfile app_path.join("Gemfile"),<<~G, dir: app_path
        source "https://gem.repo4"
        gem "mygem", "1.0"
      G
    end
  end

  context "after installing with --standalone" do
    before do
      install_gemfile <<-G
        source "https://gem.repo1"
        gem "myrack"
      G
      bundle "config set --local path bundle"
      bundle "install", standalone: true
    end

    it "includes the standalone path" do
      bundle "binstubs myrack", standalone: true
      standalone_line = File.read(bundled_app("bin/myrackup")).each_line.find {|line| line.include? "$:.unshift" }.strip
      expect(standalone_line).to eq %($:.unshift File.expand_path "../bundle", __dir__)
    end
  end

  describe "when bundle install is executed with unencoded authentication" do
    before do
      gemfile <<-G
        source 'https://rubygems.org/'
        gem "."
      G
    end

    it "should display a helpful message explaining how to fix it" do
      bundle :install, env: { "BUNDLE_RUBYGEMS__ORG" => "user:pass{word" }, raise_on_error: false
      expect(exitstatus).to eq(17)
      expect(err).to eq("Please CGI escape your usernames and passwords before " \
                        "setting them for authentication.")
    end
  end

  context "when current platform not included in the lockfile" do
    around do |example|
      build_repo4 do
        build_gem "libv8", "8.4.255.0" do |s|
          s.platform = "x86_64-darwin-19"
        end

        build_gem "libv8", "8.4.255.0" do |s|
          s.platform = "x86_64-linux"
        end
      end

      gemfile <<-G
        source "https://gem.repo4"

        gem "libv8"
      G

      lockfile <<-L
        GEM
          remote: https://gem.repo4/
          specs:
            libv8 (8.4.255.0-x86_64-darwin-19)

        PLATFORMS
          x86_64-darwin-19

        DEPENDENCIES
          libv8

        BUNDLED WITH
           #{Bundler::VERSION}
      L

      simulate_platform("x86_64-linux", &example)
    end

    it "adds the current platform to the lockfile" do
      bundle "install --verbose"

      expect(out).to include("re-resolving dependencies because your lockfile does not include the current platform")

      expect(lockfile).to eq <<~L
        GEM
          remote: https://gem.repo4/
          specs:
            libv8 (8.4.255.0-x86_64-darwin-19)
            libv8 (8.4.255.0-x86_64-linux)

        PLATFORMS
          x86_64-darwin-19
          x86_64-linux

        DEPENDENCIES
          libv8

        BUNDLED WITH
           #{Bundler::VERSION}
      L
    end

    it "fails loudly if frozen mode set" do
      bundle "config set --local deployment true"
      bundle "install", raise_on_error: false

      expect(err).to eq(
        "Your bundle only supports platforms [\"x86_64-darwin-19\"] but your local platform is x86_64-linux. " \
        "Add the current platform to the lockfile with\n`bundle lock --add-platform x86_64-linux` and try again."
      )
    end
  end

  context "with missing platform specific gems in lockfile" do
    before do
      build_repo4 do
        build_gem "racca", "1.5.2"

        build_gem "nokogiri", "1.12.4" do |s|
          s.platform = "x86_64-darwin"
          s.add_dependency "racca", "~> 1.4"
        end

        build_gem "nokogiri", "1.12.4" do |s|
          s.platform = "x86_64-linux"
          s.add_dependency "racca", "~> 1.4"
        end

        build_gem "crass", "1.0.6"

        build_gem "loofah", "2.12.0" do |s|
          s.add_dependency "crass", "~> 1.0.2"
          s.add_dependency "nokogiri", ">= 1.5.9"
        end
      end

      gemfile <<-G
        source "https://gem.repo4"

        ruby "#{Gem.ruby_version}"

        gem "loofah", "~> 2.12.0"
      G

      checksums = checksums_section do |c|
        c.checksum gem_repo4, "crass", "1.0.6"
        c.checksum gem_repo4, "loofah", "2.12.0"
        c.checksum gem_repo4, "nokogiri", "1.12.4", "x86_64-darwin"
        c.checksum gem_repo4, "racca", "1.5.2"
      end

      lockfile <<-L
        GEM
          remote: https://gem.repo4/
          specs:
            crass (1.0.6)
            loofah (2.12.0)
              crass (~> 1.0.2)
              nokogiri (>= 1.5.9)
            nokogiri (1.12.4-x86_64-darwin)
              racca (~> 1.4)
            racca (1.5.2)

        PLATFORMS
          x86_64-darwin-20
          x86_64-linux

        DEPENDENCIES
          loofah (~> 2.12.0)
        #{checksums}
        RUBY VERSION
           #{Bundler::RubyVersion.system}

        BUNDLED WITH
           #{Bundler::VERSION}
      L
    end

    it "automatically fixes the lockfile" do
      bundle "config set --local path vendor/bundle"

      simulate_platform "x86_64-linux" do
        bundle "install", artifice: "compact_index"
      end

      checksums = checksums_section_when_enabled do |c|
        c.checksum gem_repo4, "crass", "1.0.6"
        c.checksum gem_repo4, "loofah", "2.12.0"
        c.checksum gem_repo4, "nokogiri", "1.12.4", "x86_64-darwin"
        c.checksum gem_repo4, "racca", "1.5.2"
        c.checksum gem_repo4, "nokogiri", "1.12.4", "x86_64-linux"
      end

      expect(lockfile).to eq <<~L
        GEM
          remote: https://gem.repo4/
          specs:
            crass (1.0.6)
            loofah (2.12.0)
              crass (~> 1.0.2)
              nokogiri (>= 1.5.9)
            nokogiri (1.12.4-x86_64-darwin)
              racca (~> 1.4)
            nokogiri (1.12.4-x86_64-linux)
              racca (~> 1.4)
            racca (1.5.2)

        PLATFORMS
          x86_64-darwin-20
          x86_64-linux

        DEPENDENCIES
          loofah (~> 2.12.0)
        #{checksums}
        RUBY VERSION
           #{Bundler::RubyVersion.system}

        BUNDLED WITH
           #{Bundler::VERSION}
      L
    end
  end

  context "with --local flag" do
    before do
      system_gems "myrack-1.0.0", path: default_bundle_path
    end

    it "respects installed gems without fetching any remote sources" do
      install_gemfile <<-G, local: true
        source "https://gem.repo1"

        source "https://not-existing-source" do
          gem "myrack"
        end
      G

      expect(last_command).to be_success
    end
  end

  context "with only option" do
    before do
      bundle "config set only a:b"
    end

    it "installs only gems of the specified groups" do
      install_gemfile <<-G
        source "https://gem.repo1"
        gem "rails"
        gem "myrack", group: :a
        gem "rake", group: :b
        gem "yard", group: :c
      G

      expect(out).to include("Installing myrack")
      expect(out).to include("Installing rake")
      expect(out).not_to include("Installing yard")
    end
  end

  context "with --prefer-local flag" do
    before do
      build_repo4 do
        build_gem "foo", "1.0.1"
        build_gem "foo", "1.0.0"
        build_gem "bar", "1.0.0"

        build_gem "a", "1.0.0" do |s|
          s.add_dependency "foo", "~> 1.0.0"
        end

        build_gem "b", "1.0.0" do |s|
          s.add_dependency "foo", "~> 1.0.1"
        end
      end

      system_gems "foo-1.0.0", path: default_bundle_path, gem_repo: gem_repo4
    end

    it "fetches remote sources when not available locally" do
      install_gemfile <<-G, "prefer-local": true, verbose: true
        source "https://gem.repo4"

        gem "foo"
        gem "bar"
      G

      expect(out).to include("Using foo 1.0.0").and include("Fetching bar 1.0.0").and include("Installing bar 1.0.0")
      expect(last_command).to be_success
    end

    it "fetches remote sources when local version does not match requirements" do
      install_gemfile <<-G, "prefer-local": true, verbose: true
        source "https://gem.repo4"

        gem "foo", "1.0.1"
        gem "bar"
      G

      expect(out).to include("Fetching foo 1.0.1").and include("Installing foo 1.0.1").and include("Fetching bar 1.0.0").and include("Installing bar 1.0.0")
      expect(last_command).to be_success
    end

    it "uses the locally available version for sub-dependencies when possible" do
      install_gemfile <<-G, "prefer-local": true, verbose: true
        source "https://gem.repo4"

        gem "a"
      G

      expect(out).to include("Using foo 1.0.0").and include("Fetching a 1.0.0").and include("Installing a 1.0.0")
      expect(last_command).to be_success
    end

    it "fetches remote sources for sub-dependencies when the locally available version does not satisfy the requirement" do
      install_gemfile <<-G, "prefer-local": true, verbose: true
        source "https://gem.repo4"

        gem "b"
      G

      expect(out).to include("Fetching foo 1.0.1").and include("Installing foo 1.0.1").and include("Fetching b 1.0.0").and include("Installing b 1.0.0")
      expect(last_command).to be_success
    end
  end

  context "with a symlinked configured as bundle path and a gem with symlinks" do
    before do
      symlinked_bundled_app = tmp("bundled_app-symlink")
      File.symlink(bundled_app, symlinked_bundled_app)
      bundle "config path #{File.join(symlinked_bundled_app, ".vendor")}"

      binman_path = tmp("binman")
      FileUtils.mkdir_p binman_path

      readme_path = File.join(binman_path, "README.markdown")
      FileUtils.touch(readme_path)

      man_path = File.join(binman_path, "man", "man0")
      FileUtils.mkdir_p man_path

      File.symlink("../../README.markdown", File.join(man_path, "README.markdown"))

      build_repo4 do
        build_gem "binman", path: gem_repo4("gems"), lib_path: binman_path, no_default: true do |s|
          s.files = ["README.markdown", "man/man0/README.markdown"]
        end
      end
    end

    it "installs fine" do
      install_gemfile <<~G
        source "https://gem.repo4"

        gem "binman"
      G
    end
  end

  context "when a gem has equivalent versions with inconsistent dependencies" do
    before do
      build_repo4 do
        build_gem "autobuild", "1.10.rc2" do |s|
          s.add_dependency "utilrb", ">= 1.6.0"
        end

        build_gem "autobuild", "1.10.0.rc2" do |s|
          s.add_dependency "utilrb", ">= 2.0"
        end
      end
    end

    it "does not crash unexpectedly" do
      gemfile <<~G
        source "https://gem.repo4"

        gem "autobuild", "1.10.rc2"
      G

      bundle "install --jobs 1", raise_on_error: false

      expect(err).not_to include("ERROR REPORT TEMPLATE")
      expect(err).to include("Could not find compatible versions")
    end
  end

  context "when a lockfile has unmet dependencies, and the Gemfile has no resolution" do
    before do
      build_repo4 do
        build_gem "aaa", "0.2.0" do |s|
          s.add_dependency "zzz", "< 0.2.0"
        end

        build_gem "zzz", "0.2.0"
      end

      gemfile <<~G
        source "https://gem.repo4"

        gem "aaa"
        gem "zzz"
      G

      lockfile <<~L
        GEM
          remote: https://gem.repo4/
          specs:
            aaa (0.2.0)
              zzz (< 0.2.0)
            zzz (0.2.0)

        PLATFORMS
          #{lockfile_platforms}

        DEPENDENCIES
          aaa!
          zzz!

        BUNDLED WITH
           #{Bundler::VERSION}
      L
    end

    it "does not install, but raises a resolution error" do
      bundle "install", raise_on_error: false
      expect(err).to include("Could not find compatible versions")
    end
  end

  context "when --jobs option given" do
    before do
      install_gemfile "source 'https://gem.repo1'", jobs: 1
    end

    it "does not save the flag to config" do
      expect(bundled_app(".bundle/config")).not_to exist
    end
  end

  context "when bundler installation is corrupt" do
    before do
      system_gems "bundler-9.99.8"

      replace_version_file("9.99.9", dir: system_gem_path("gems/bundler-9.99.8"))
    end

    it "shows a proper error" do
      lockfile <<~L
        GEM
          remote: https://gem.repo1/
          specs:

        PLATFORMS
          #{lockfile_platforms}

        DEPENDENCIES

        BUNDLED WITH
           9.99.8
      L

      install_gemfile "source \"https://gem.repo1\"", env: { "BUNDLER_VERSION" => "9.99.8" }, raise_on_error: false

      expect(err).not_to include("ERROR REPORT TEMPLATE")
      expect(err).to include("The running version of Bundler (9.99.9) does not match the version of the specification installed for it (9.99.8)")
    end
  end

  it "only installs executable files in bin" do
    bundle "config set --local path vendor/bundle"

    install_gemfile <<~G
      source "https://gem.repo1"
      gem "myrack"
    G

    expected_executables = [vendored_gems("bin/myrackup").to_s]
    expected_executables << vendored_gems("bin/myrackup.bat").to_s if Gem.win_platform?
    expect(Dir.glob(vendored_gems("bin/*"))).to eq(expected_executables)
  end
end
