# frozen_string_literal: true

RSpec.describe "bundle lock" do
  let(:repo) { gem_repo1 }

  before :each do
    gemfile <<-G
      source "#{file_uri_for(repo)}"
      gem "rails"
      gem "weakling"
      gem "foo"
    G

    checksums = checksums_section_when_existing do |c|
      c.checksum repo, "actionmailer", "2.3.2"
      c.checksum repo, "actionpack", "2.3.2"
      c.checksum repo, "activerecord", "2.3.2"
      c.checksum repo, "activeresource", "2.3.2"
      c.checksum repo, "activesupport", "2.3.2"
      c.checksum repo, "foo", "1.0"
      c.checksum repo, "rails", "2.3.2"
      c.checksum repo, "rake", rake_version
      c.checksum repo, "weakling", "0.0.3"
    end

    @lockfile = <<~L
      GEM
        remote: #{file_uri_for(repo)}/
        specs:
          actionmailer (2.3.2)
            activesupport (= 2.3.2)
          actionpack (2.3.2)
            activesupport (= 2.3.2)
          activerecord (2.3.2)
            activesupport (= 2.3.2)
          activeresource (2.3.2)
            activesupport (= 2.3.2)
          activesupport (2.3.2)
          foo (1.0)
          rails (2.3.2)
            actionmailer (= 2.3.2)
            actionpack (= 2.3.2)
            activerecord (= 2.3.2)
            activeresource (= 2.3.2)
            rake (= #{rake_version})
          rake (#{rake_version})
          weakling (0.0.3)

      PLATFORMS
        #{lockfile_platforms}

      DEPENDENCIES
        foo
        rails
        weakling
      #{checksums}
      BUNDLED WITH
         #{Bundler::VERSION}
    L
  end

  it "prints a lockfile when there is no existing lockfile with --print" do
    bundle "lock --print"

    expect(out).to eq(@lockfile.chomp)
  end

  it "prints a lockfile when there is an existing lockfile with --print" do
    lockfile remove_checksums_section_from_lockfile(@lockfile)

    bundle "lock --print"

    expect(out).to eq(remove_checksums_section_from_lockfile(@lockfile).chomp)
  end

  it "prints a lockfile when there is an existing checksums lockfile with --print" do
    lockfile @lockfile

    bundle "lock --print"

    expect(out).to eq(@lockfile.chomp)
  end

  it "writes a lockfile when there is no existing lockfile" do
    bundle "lock"

    expect(read_lockfile).to eq(@lockfile)
  end

  it "prints a lockfile without fetching new checksums if the existing lockfile had no checksums" do
    lockfile remove_checksums_from_lockfile(@lockfile)

    bundle "lock --print"

    expect(out).to eq(remove_checksums_from_lockfile(@lockfile).chomp)
  end

  it "touches the lockfile when there is an existing lockfile that does not need changes" do
    lockfile @lockfile

    expect do
      bundle "lock"
    end.to change { bundled_app_lock.mtime }
  end

  it "does not touch lockfile with --print" do
    lockfile @lockfile

    expect do
      bundle "lock --print"
    end.not_to change { bundled_app_lock.mtime }
  end

  it "writes a lockfile when there is an outdated lockfile using --update" do
    lockfile remove_checksums_from_lockfile(@lockfile.gsub("2.3.2", "2.3.1"), " (2.3.1)")

    bundle "lock --update"

    expect(read_lockfile).to eq(remove_checksums_from_lockfile(@lockfile))
  end

  it "writes a lockfile with checksums on --update when checksums exist" do
    lockfile @lockfile.gsub("2.3.2", "2.3.1")

    bundle "lock --update"

    expect(read_lockfile).to eq(@lockfile)
  end

  it "writes a lockfile when there is an outdated lockfile and bundle is frozen" do
    lockfile @lockfile.gsub("2.3.2", "2.3.1")

    bundle "lock --update", env: { "BUNDLE_FROZEN" => "true" }

    expect(read_lockfile).to eq(@lockfile)
  end

  it "does not fetch remote specs when using the --local option" do
    bundle "lock --update --local", raise_on_error: false

    expect(err).to match(/locally installed gems/)
  end

  it "does not fetch remote checksums with --local" do
    lockfile remove_checksums_from_lockfile(@lockfile)

    bundle "lock --print --local"

    # No checksums because --local prevents fetching them
    expect(out).to eq(remove_checksums_from_lockfile(@lockfile).chomp)
  end

  it "works with --gemfile flag" do
    create_file "CustomGemfile", <<-G
      source "#{file_uri_for(repo)}"
      gem "foo"
    G
    checksums = checksums_section_when_existing do |c|
      c.no_checksum "foo", "1.0"
    end

    lockfile = <<~L
      GEM
        remote: #{file_uri_for(repo)}/
        specs:
          foo (1.0)

      PLATFORMS
        #{lockfile_platforms}

      DEPENDENCIES
        foo
      #{checksums}
      BUNDLED WITH
         #{Bundler::VERSION}
    L
    bundle "lock --gemfile CustomGemfile"

    expect(out).to match(/Writing lockfile to.+CustomGemfile\.lock/)
    expect(read_lockfile("CustomGemfile.lock")).to eq(lockfile)
    expect { read_lockfile }.to raise_error(Errno::ENOENT)
  end

  it "writes to a custom location using --lockfile" do
    bundle "lock --lockfile=lock"

    expect(out).to match(/Writing lockfile to.+lock/)
    expect(read_lockfile("lock")).to eq(remove_checksums_from_lockfile(@lockfile))
    expect { read_lockfile }.to raise_error(Errno::ENOENT)
  end

  it "writes to custom location using --lockfile when a default lockfile is present" do
    bundle "install"
    bundle "lock --lockfile=lock"

    checksums = checksums_section_when_existing do |c|
      c.checksum repo, "actionmailer", "2.3.2"
      c.checksum repo, "actionpack", "2.3.2"
      c.checksum repo, "activerecord", "2.3.2"
      c.checksum repo, "activeresource", "2.3.2"
      c.checksum repo, "activesupport", "2.3.2"
      c.checksum repo, "foo", "1.0"
      c.checksum repo, "rails", "2.3.2"
      c.checksum repo, "rake", rake_version
      c.checksum repo, "weakling", "0.0.3"
    end

    lockfile = <<~L
      GEM
        remote: #{file_uri_for(repo)}/
        specs:
          actionmailer (2.3.2)
            activesupport (= 2.3.2)
          actionpack (2.3.2)
            activesupport (= 2.3.2)
          activerecord (2.3.2)
            activesupport (= 2.3.2)
          activeresource (2.3.2)
            activesupport (= 2.3.2)
          activesupport (2.3.2)
          foo (1.0)
          rails (2.3.2)
            actionmailer (= 2.3.2)
            actionpack (= 2.3.2)
            activerecord (= 2.3.2)
            activeresource (= 2.3.2)
            rake (= #{rake_version})
          rake (#{rake_version})
          weakling (0.0.3)

      PLATFORMS
        #{lockfile_platforms}

      DEPENDENCIES
        foo
        rails
        weakling
      #{checksums}
      BUNDLED WITH
         #{Bundler::VERSION}
    L

    expect(out).to match(/Writing lockfile to.+lock/)
    expect(read_lockfile("lock")).to eq(lockfile)
  end

  it "update specific gems using --update" do
    lockfile @lockfile.gsub("2.3.2", "2.3.1").gsub(rake_version, "10.0.1")

    bundle "lock --update rails rake"

    expect(read_lockfile).to eq(remove_checksums_from_lockfile(@lockfile, "(2.3.2)", "(#{rake_version})"))
  end

  it "preserves unknown checksum algorithms" do
    lockfile @lockfile.gsub(/(sha256=[a-f0-9]+)$/, "constant=true,\\1,xyz=123")

    previous_lockfile = read_lockfile

    bundle "lock"

    expect(read_lockfile).to eq(previous_lockfile)
  end

  it "does not unlock git sources when only uri shape changes" do
    build_git("foo")

    install_gemfile <<-G
      source "#{file_uri_for(gem_repo1)}"
      gem "foo", :git => "#{file_uri_for(lib_path("foo-1.0"))}"
    G

    # Change uri format to end with "/" and reinstall
    install_gemfile <<-G, verbose: true
      source "#{file_uri_for(gem_repo1)}"
      gem "foo", :git => "#{file_uri_for(lib_path("foo-1.0"))}/"
    G

    expect(out).to include("using resolution from the lockfile")
    expect(out).not_to include("re-resolving dependencies because the list of sources changed")
  end

  it "updates specific gems using --update using the locked revision of unrelated git gems for resolving" do
    ref = build_git("foo").ref_for("HEAD")

    gemfile <<-G
      source "#{file_uri_for(gem_repo1)}"
      gem "rake"
      gem "foo", :git => "#{file_uri_for(lib_path("foo-1.0"))}", :branch => "deadbeef"
    G

    lockfile <<~L
      GIT
        remote: #{file_uri_for(lib_path("foo-1.0"))}
        revision: #{ref}
        branch: deadbeef
        specs:
          foo (1.0)

      GEM
        remote: #{file_uri_for(gem_repo1)}/
        specs:
          rake (10.0.1)

      PLATFORMS
        #{lockfile_platforms}

      DEPENDENCIES
        foo!
        rake

      BUNDLED WITH
         #{Bundler::VERSION}
    L

    bundle "lock --update rake --verbose"
    expect(out).to match(/Writing lockfile to.+lock/)
    expect(lockfile).to include("rake (#{rake_version})")
  end

  it "errors when updating a missing specific gems using --update" do
    lockfile @lockfile

    bundle "lock --update blahblah", raise_on_error: false
    expect(err).to eq("Could not find gem 'blahblah'.")

    expect(read_lockfile).to eq(@lockfile)
  end

  it "can lock without downloading gems" do
    gemfile <<-G
      source "#{file_uri_for(gem_repo1)}"

      gem "thin"
      gem "rack_middleware", :group => "test"
    G
    bundle "config set without test"
    bundle "config set path vendor/bundle"
    bundle "lock", verbose: true
    expect(bundled_app("vendor/bundle")).not_to exist
  end

  # see update_spec for more coverage on same options. logic is shared so it's not necessary
  # to repeat coverage here.
  context "conservative updates" do
    before do
      build_repo4 do
        build_gem "foo", %w[1.4.3 1.4.4] do |s|
          s.add_dependency "bar", "~> 2.0"
        end
        build_gem "foo", %w[1.4.5 1.5.0] do |s|
          s.add_dependency "bar", "~> 2.1"
        end
        build_gem "foo", %w[1.5.1] do |s|
          s.add_dependency "bar", "~> 3.0"
        end
        build_gem "foo", %w[2.0.0.pre] do |s|
          s.add_dependency "bar"
        end
        build_gem "bar", %w[2.0.3 2.0.4 2.0.5 2.1.0 2.1.1 2.1.2.pre 3.0.0 3.1.0.pre 4.0.0.pre]
        build_gem "qux", %w[1.0.0 1.0.1 1.1.0 2.0.0]
      end

      # establish a lockfile set to 1.4.3
      install_gemfile <<-G
        source "#{file_uri_for(gem_repo4)}"
        gem 'foo', '1.4.3'
        gem 'bar', '2.0.3'
        gem 'qux', '1.0.0'
      G

      # remove 1.4.3 requirement and bar altogether
      # to setup update specs below
      gemfile <<-G
        source "#{file_uri_for(gem_repo4)}"
        gem 'foo'
        gem 'qux'
      G

      allow(Bundler::SharedHelpers).to receive(:find_gemfile).and_return(bundled_app_gemfile)
    end

    it "single gem updates dependent gem to minor" do
      bundle "lock --update foo --patch"

      expect(the_bundle.locked_gems.specs.map(&:full_name)).to eq(%w[foo-1.4.5 bar-2.1.1 qux-1.0.0].sort)
    end

    it "minor preferred with strict" do
      bundle "lock --update --minor --strict"

      expect(the_bundle.locked_gems.specs.map(&:full_name)).to eq(%w[foo-1.5.0 bar-2.1.1 qux-1.1.0].sort)
    end

    context "pre" do
      it "defaults to major" do
        bundle "lock --update --pre"

        expect(the_bundle.locked_gems.specs.map(&:full_name)).to eq(%w[foo-2.0.0.pre bar-4.0.0.pre qux-2.0.0].sort)
      end

      it "patch preferred" do
        bundle "lock --update --patch --pre"

        expect(the_bundle.locked_gems.specs.map(&:full_name)).to eq(%w[foo-1.4.5 bar-2.1.2.pre qux-1.0.1].sort)
      end

      it "minor preferred" do
        bundle "lock --update --minor --pre"

        expect(the_bundle.locked_gems.specs.map(&:full_name)).to eq(%w[foo-1.5.1 bar-3.1.0.pre qux-1.1.0].sort)
      end

      it "major preferred" do
        bundle "lock --update --major --pre"

        expect(the_bundle.locked_gems.specs.map(&:full_name)).to eq(%w[foo-2.0.0.pre bar-4.0.0.pre qux-2.0.0].sort)
      end
    end
  end

  context "conservative updates when minor update adds a new dependency" do
    before do
      build_repo4 do
        build_gem "sequel", "5.71.0"
        build_gem "sequel", "5.72.0" do |s|
          s.add_dependency "bigdecimal", ">= 0"
        end
        build_gem "bigdecimal", %w[1.4.4 99.1.4]
      end

      gemfile <<~G
        source "#{file_uri_for(gem_repo4)}"
        gem 'sequel'
      G

      lockfile <<~L
        GEM
          remote: #{file_uri_for(gem_repo4)}/
          specs:
            sequel (5.71.0)

        PLATFORMS
          ruby

        DEPENDENCIES
          sequel

        BUNDLED WITH
           #{Bundler::VERSION}
      L

      allow(Bundler::SharedHelpers).to receive(:find_gemfile).and_return(bundled_app_gemfile)
    end

    it "adds the latest version of the new dependency" do
      bundle "lock --minor --update sequel"

      expect(the_bundle.locked_gems.specs.map(&:full_name)).to eq(%w[sequel-5.72.0 bigdecimal-99.1.4].sort)
    end
  end

  it "updates the bundler version in the lockfile to the latest bundler version" do
    build_repo4 do
      build_gem "bundler", "55"
    end

    system_gems "bundler-55", gem_repo: gem_repo4

    install_gemfile <<-G, artifice: "compact_index", env: { "BUNDLER_SPEC_GEM_REPO" => gem_repo4.to_s }
      source "https://gems.repo4"
    G
    lockfile lockfile.sub(/(^\s*)#{Bundler::VERSION}($)/, '\11.0.0\2')

    bundle "lock --update --bundler --verbose", artifice: "compact_index", env: { "BUNDLER_SPEC_GEM_REPO" => gem_repo4.to_s }
    expect(lockfile).to end_with("BUNDLED WITH\n   55\n")

    update_repo4 do
      build_gem "bundler", "99"
    end

    bundle "lock --update --bundler --verbose", artifice: "compact_index", env: { "BUNDLER_SPEC_GEM_REPO" => gem_repo4.to_s }
    expect(lockfile).to end_with("BUNDLED WITH\n   99\n")
  end

  it "supports adding new platforms" do
    bundle "lock --add-platform java x86-mingw32"

    allow(Bundler::SharedHelpers).to receive(:find_gemfile).and_return(bundled_app_gemfile)
    lockfile = Bundler::LockfileParser.new(read_lockfile)
    expect(lockfile.platforms).to match_array(default_platform_list(java, x86_mingw32))
  end

  it "supports adding new platforms with force_ruby_platform = true" do
    lockfile <<-L
      GEM
        remote: #{file_uri_for(gem_repo1)}/
        specs:
          platform_specific (1.0)
          platform_specific (1.0-x86-64_linux)

      PLATFORMS
        ruby
        x86_64-linux

      DEPENDENCIES
        platform_specific
    L

    bundle "config set force_ruby_platform true"
    bundle "lock --add-platform java x86-mingw32"

    allow(Bundler::SharedHelpers).to receive(:find_gemfile).and_return(bundled_app_gemfile)
    lockfile = Bundler::LockfileParser.new(read_lockfile)
    expect(lockfile.platforms).to contain_exactly(rb, linux, java, x86_mingw32)
  end

  it "supports adding the `ruby` platform" do
    bundle "lock --add-platform ruby"

    allow(Bundler::SharedHelpers).to receive(:find_gemfile).and_return(bundled_app_gemfile)
    lockfile = Bundler::LockfileParser.new(read_lockfile)
    expect(lockfile.platforms).to match_array(default_platform_list("ruby"))
  end

  it "warns when adding an unknown platform" do
    bundle "lock --add-platform foobarbaz"
    expect(err).to include("The platform `foobarbaz` is unknown to RubyGems and adding it will likely lead to resolution errors")
  end

  it "allows removing platforms" do
    bundle "lock --add-platform java x86-mingw32"

    allow(Bundler::SharedHelpers).to receive(:find_gemfile).and_return(bundled_app_gemfile)
    lockfile = Bundler::LockfileParser.new(read_lockfile)
    expect(lockfile.platforms).to match_array(default_platform_list(java, x86_mingw32))

    bundle "lock --remove-platform java"

    lockfile = Bundler::LockfileParser.new(read_lockfile)
    expect(lockfile.platforms).to match_array(default_platform_list(x86_mingw32))
  end

  it "also cleans up redundant platform gems when removing platforms" do
    build_repo4 do
      build_gem "nokogiri", "1.12.0"
      build_gem "nokogiri", "1.12.0" do |s|
        s.platform = "x86_64-darwin"
      end
    end

    checksums = checksums_section_when_existing do |c|
      c.checksum gem_repo4, "nokogiri", "1.12.0"
      c.checksum gem_repo4, "nokogiri", "1.12.0", "x86_64-darwin"
    end

    simulate_platform "x86_64-darwin-22" do
      install_gemfile <<~G
        source "#{file_uri_for(gem_repo4)}"

        gem "nokogiri"
      G
    end

    lockfile <<~L
      GEM
        remote: #{file_uri_for(gem_repo4)}/
        specs:
          nokogiri (1.12.0)
          nokogiri (1.12.0-x86_64-darwin)

      PLATFORMS
        ruby
        x86_64-darwin

      DEPENDENCIES
        nokogiri
      #{checksums}
      BUNDLED WITH
         #{Bundler::VERSION}
    L

    checksums.delete("nokogiri", Gem::Platform::RUBY)

    simulate_platform "x86_64-darwin-22" do
      bundle "lock --remove-platform ruby"
    end

    expect(lockfile).to eq <<~L
      GEM
        remote: #{file_uri_for(gem_repo4)}/
        specs:
          nokogiri (1.12.0-x86_64-darwin)

      PLATFORMS
        x86_64-darwin

      DEPENDENCIES
        nokogiri
      #{checksums}
      BUNDLED WITH
         #{Bundler::VERSION}
    L
  end

  it "errors when removing all platforms" do
    bundle "lock --remove-platform #{local_platform}", raise_on_error: false
    expect(err).to include("Removing all platforms from the bundle is not allowed")
  end

  # from https://github.com/rubygems/bundler/issues/4896
  it "properly adds platforms when platform requirements come from different dependencies" do
    build_repo4 do
      build_gem "ffi", "1.9.14"
      build_gem "ffi", "1.9.14" do |s|
        s.platform = x86_mingw32
      end

      build_gem "gssapi", "0.1"
      build_gem "gssapi", "0.2"
      build_gem "gssapi", "0.3"
      build_gem "gssapi", "1.2.0" do |s|
        s.add_dependency "ffi", ">= 1.0.1"
      end

      build_gem "mixlib-shellout", "2.2.6"
      build_gem "mixlib-shellout", "2.2.6" do |s|
        s.platform = "universal-mingw32"
        s.add_dependency "win32-process", "~> 0.8.2"
      end

      # we need all these versions to get the sorting the same as it would be
      # pulling from rubygems.org
      %w[0.8.3 0.8.2 0.8.1 0.8.0].each do |v|
        build_gem "win32-process", v do |s|
          s.add_dependency "ffi", ">= 1.0.0"
        end
      end
    end

    gemfile <<-G
      source "#{file_uri_for(gem_repo4)}"

      gem "mixlib-shellout"
      gem "gssapi"
    G

    checksums = checksums_section_when_existing do |c|
      c.no_checksum "ffi", "1.9.14", "x86-mingw32"
      c.no_checksum "gssapi", "1.2.0"
      c.no_checksum "mixlib-shellout", "2.2.6", "universal-mingw32"
      c.no_checksum "win32-process", "0.8.3"
    end

    simulate_platform(x86_mingw32) { bundle :lock }

    expect(lockfile).to eq <<~G
      GEM
        remote: #{file_uri_for(gem_repo4)}/
        specs:
          ffi (1.9.14-x86-mingw32)
          gssapi (1.2.0)
            ffi (>= 1.0.1)
          mixlib-shellout (2.2.6-universal-mingw32)
            win32-process (~> 0.8.2)
          win32-process (0.8.3)
            ffi (>= 1.0.0)

      PLATFORMS
        x86-mingw32

      DEPENDENCIES
        gssapi
        mixlib-shellout
      #{checksums}
      BUNDLED WITH
         #{Bundler::VERSION}
    G

    bundle "config set --local force_ruby_platform true"
    bundle :lock

    checksums.no_checksum "ffi", "1.9.14"
    checksums.no_checksum "mixlib-shellout", "2.2.6"

    expect(lockfile).to eq <<~G
      GEM
        remote: #{file_uri_for(gem_repo4)}/
        specs:
          ffi (1.9.14)
          ffi (1.9.14-x86-mingw32)
          gssapi (1.2.0)
            ffi (>= 1.0.1)
          mixlib-shellout (2.2.6)
          mixlib-shellout (2.2.6-universal-mingw32)
            win32-process (~> 0.8.2)
          win32-process (0.8.3)
            ffi (>= 1.0.0)

      PLATFORMS
        ruby
        x86-mingw32

      DEPENDENCIES
        gssapi
        mixlib-shellout
      #{checksums}
      BUNDLED WITH
         #{Bundler::VERSION}
    G
  end

  it "doesn't crash when an update candidate doesn't have any matching platform" do
    build_repo4 do
      build_gem "libv8", "8.4.255.0"
      build_gem "libv8", "8.4.255.0" do |s|
        s.platform = "x86_64-darwin-19"
      end

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

    gemfile <<-G
      source "#{file_uri_for(gem_repo4)}"

      gem "libv8"
    G

    lockfile <<-G
      GEM
        remote: #{file_uri_for(gem_repo4)}/
        specs:
          libv8 (8.4.255.0)
          libv8 (8.4.255.0-x86_64-darwin-19)

      PLATFORMS
        ruby
        x86_64-darwin-19

      DEPENDENCIES
        libv8

      BUNDLED WITH
         #{Bundler::VERSION}
    G

    simulate_platform(Gem::Platform.new("x86_64-darwin-19")) { bundle "lock --update" }

    expect(out).to match(/Writing lockfile to.+Gemfile\.lock/)
  end

  it "adds all more specific candidates when they all have the same dependencies" do
    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-darwin-20"
      end
    end

    gemfile <<-G
      source "#{file_uri_for(gem_repo4)}"

      gem "libv8"
    G

    simulate_platform(Gem::Platform.new("x86_64-darwin-19")) { bundle "lock" }

    checksums = checksums_section_when_existing do |c|
      c.no_checksum "libv8", "8.4.255.0", "x86_64-darwin-19"
      c.no_checksum "libv8", "8.4.255.0", "x86_64-darwin-20"
    end

    expect(lockfile).to eq <<~G
      GEM
        remote: #{file_uri_for(gem_repo4)}/
        specs:
          libv8 (8.4.255.0-x86_64-darwin-19)
          libv8 (8.4.255.0-x86_64-darwin-20)

      PLATFORMS
        x86_64-darwin-19
        x86_64-darwin-20

      DEPENDENCIES
        libv8
      #{checksums}
      BUNDLED WITH
         #{Bundler::VERSION}
    G
  end

  it "respects the previous lockfile if it had a matching less specific platform already locked, and installs the best variant for each platform" do
    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-darwin-20"
      end
    end

    checksums = checksums_section_when_existing do |c|
      c.checksum gem_repo4, "libv8", "8.4.255.0", "x86_64-darwin-19"
      c.checksum gem_repo4, "libv8", "8.4.255.0", "x86_64-darwin-20"
    end

    gemfile <<-G
      source "#{file_uri_for(gem_repo4)}"

      gem "libv8"
    G

    lockfile <<-G
      GEM
        remote: #{file_uri_for(gem_repo4)}/
        specs:
          libv8 (8.4.255.0-x86_64-darwin-19)
          libv8 (8.4.255.0-x86_64-darwin-20)

      PLATFORMS
        x86_64-darwin

      DEPENDENCIES
        libv8
      #{checksums}
      BUNDLED WITH
         #{Bundler::VERSION}
    G

    previous_lockfile = lockfile

    %w[x86_64-darwin-19 x86_64-darwin-20].each do |platform|
      simulate_platform(Gem::Platform.new(platform)) do
        bundle "lock"
        expect(lockfile).to eq(previous_lockfile)

        bundle "install"
        expect(the_bundle).to include_gem("libv8 8.4.255.0 #{platform}")
      end
    end
  end

  it "does not conflict on ruby requirements when adding new platforms" do
    build_repo4 do
      build_gem "raygun-apm", "1.0.78" do |s|
        s.platform = "x86_64-linux"
        s.required_ruby_version = "< #{next_ruby_minor}.dev"
      end

      build_gem "raygun-apm", "1.0.78" do |s|
        s.platform = "universal-darwin"
        s.required_ruby_version = "< #{next_ruby_minor}.dev"
      end

      build_gem "raygun-apm", "1.0.78" do |s|
        s.platform = "x64-mingw32"
        s.required_ruby_version = "< #{next_ruby_minor}.dev"
      end

      build_gem "raygun-apm", "1.0.78" do |s|
        s.platform = "x64-mingw-ucrt"
        s.required_ruby_version = "< #{next_ruby_minor}.dev"
      end
    end

    gemfile <<-G
      source "https://localgemserver.test"

      gem "raygun-apm"
    G

    lockfile <<-L
      GEM
        remote: https://localgemserver.test/
        specs:
          raygun-apm (1.0.78-universal-darwin)

      PLATFORMS
        x86_64-darwin-19

      DEPENDENCIES
        raygun-apm

      BUNDLED WITH
         #{Bundler::VERSION}
    L

    bundle "lock --add-platform x86_64-linux", artifice: "compact_index", env: { "BUNDLER_SPEC_GEM_REPO" => gem_repo4.to_s }
  end

  it "does not crash on conflicting ruby requirements between platform versions in two different gems" do
    build_repo4 do
      build_gem "unf_ext", "0.0.8.2"

      build_gem "unf_ext", "0.0.8.2" do |s|
        s.required_ruby_version = [">= 2.4", "< #{previous_ruby_minor}"]
        s.platform = "x64-mingw32"
      end

      build_gem "unf_ext", "0.0.8.2" do |s|
        s.required_ruby_version = [">= #{previous_ruby_minor}", "< #{current_ruby_minor}"]
        s.platform = "x64-mingw-ucrt"
      end

      build_gem "google-protobuf", "3.21.12"

      build_gem "google-protobuf", "3.21.12" do |s|
        s.required_ruby_version = [">= 2.5", "< #{previous_ruby_minor}"]
        s.platform = "x64-mingw32"
      end

      build_gem "google-protobuf", "3.21.12" do |s|
        s.required_ruby_version = [">= #{previous_ruby_minor}", "< #{current_ruby_minor}"]
        s.platform = "x64-mingw-ucrt"
      end
    end

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

      gem "google-protobuf"
      gem "unf_ext"
    G

    lockfile <<~L
      GEM
        remote: https://gem.repo4/
        specs:
          google-protobuf (3.21.12)
          unf_ext (0.0.8.2)

      PLATFORMS
        x64-mingw-ucrt
        x64-mingw32

      DEPENDENCIES
        google-protobuf
        unf_ext

      BUNDLED WITH
         #{Bundler::VERSION}
    L

    bundle "install --verbose", artifice: "compact_index", env: { "BUNDLER_SPEC_GEM_REPO" => gem_repo4.to_s, "DEBUG_RESOLVER" => "1" }
  end

  it "respects lower bound ruby requirements" do
    build_repo4 do
      build_gem "our_private_gem", "0.1.0" do |s|
        s.required_ruby_version = ">= #{Gem.ruby_version}"
      end
    end

    gemfile <<-G
      source "https://localgemserver.test"

      gem "our_private_gem"
    G

    lockfile <<-L
      GEM
        remote: https://localgemserver.test/
        specs:
          our_private_gem (0.1.0)

      PLATFORMS
        #{lockfile_platforms}

      DEPENDENCIES
        our_private_gem

      BUNDLED WITH
         #{Bundler::VERSION}
    L

    bundle "install", artifice: "compact_index", env: { "BUNDLER_SPEC_GEM_REPO" => gem_repo4.to_s }
  end

  context "when an update is available" do
    let(:repo) do
      build_repo2 do
        build_gem "foo", "2.0"
      end
      gem_repo2
    end

    before do
      lockfile(@lockfile)
    end

    it "does not implicitly update" do
      bundle "lock"

      checksums = checksums_section_when_existing do |c|
        c.checksum repo, "actionmailer", "2.3.2"
        c.checksum repo, "actionpack", "2.3.2"
        c.checksum repo, "activerecord", "2.3.2"
        c.checksum repo, "activeresource", "2.3.2"
        c.checksum repo, "activesupport", "2.3.2"
        c.checksum repo, "foo", "1.0"
        c.checksum repo, "rails", "2.3.2"
        c.checksum repo, "rake", rake_version
        c.checksum repo, "weakling", "0.0.3"
      end

      expected_lockfile = <<~L
        GEM
          remote: #{file_uri_for(repo)}/
          specs:
            actionmailer (2.3.2)
              activesupport (= 2.3.2)
            actionpack (2.3.2)
              activesupport (= 2.3.2)
            activerecord (2.3.2)
              activesupport (= 2.3.2)
            activeresource (2.3.2)
              activesupport (= 2.3.2)
            activesupport (2.3.2)
            foo (1.0)
            rails (2.3.2)
              actionmailer (= 2.3.2)
              actionpack (= 2.3.2)
              activerecord (= 2.3.2)
              activeresource (= 2.3.2)
              rake (= #{rake_version})
            rake (#{rake_version})
            weakling (0.0.3)

        PLATFORMS
          #{lockfile_platforms}

        DEPENDENCIES
          foo
          rails
          weakling
        #{checksums}
        BUNDLED WITH
           #{Bundler::VERSION}
      L

      expect(read_lockfile).to eq(expected_lockfile)
    end

    it "accounts for changes in the gemfile" do
      gemfile gemfile.gsub('"foo"', '"foo", "2.0"')
      bundle "lock"

      checksums = checksums_section_when_existing do |c|
        c.checksum repo, "actionmailer", "2.3.2"
        c.checksum repo, "actionpack", "2.3.2"
        c.checksum repo, "activerecord", "2.3.2"
        c.checksum repo, "activeresource", "2.3.2"
        c.checksum repo, "activesupport", "2.3.2"
        c.no_checksum "foo", "2.0"
        c.checksum repo, "rails", "2.3.2"
        c.checksum repo, "rake", rake_version
        c.checksum repo, "weakling", "0.0.3"
      end

      expected_lockfile = <<~L
        GEM
          remote: #{file_uri_for(repo)}/
          specs:
            actionmailer (2.3.2)
              activesupport (= 2.3.2)
            actionpack (2.3.2)
              activesupport (= 2.3.2)
            activerecord (2.3.2)
              activesupport (= 2.3.2)
            activeresource (2.3.2)
              activesupport (= 2.3.2)
            activesupport (2.3.2)
            foo (2.0)
            rails (2.3.2)
              actionmailer (= 2.3.2)
              actionpack (= 2.3.2)
              activerecord (= 2.3.2)
              activeresource (= 2.3.2)
              rake (= #{rake_version})
            rake (#{rake_version})
            weakling (0.0.3)

        PLATFORMS
          #{lockfile_platforms}

        DEPENDENCIES
          foo (= 2.0)
          rails
          weakling
        #{checksums}
        BUNDLED WITH
           #{Bundler::VERSION}
      L

      expect(read_lockfile).to eq(expected_lockfile)
    end
  end

  context "when a system gem has incorrect dependencies, different from the lockfile" do
    before do
      build_repo4 do
        build_gem "debug", "1.6.3" do |s|
          s.add_dependency "irb", ">= 1.3.6"
        end

        build_gem "irb", "1.5.0"
      end

      system_gems "irb-1.5.0", gem_repo: gem_repo4
      system_gems "debug-1.6.3", gem_repo: gem_repo4

      # simulate gemspec with wrong empty dependencies
      debug_gemspec_path = system_gem_path("specifications/debug-1.6.3.gemspec")
      debug_gemspec = Gem::Specification.load(debug_gemspec_path.to_s)
      debug_gemspec.dependencies.clear
      File.write(debug_gemspec_path, debug_gemspec.to_ruby)
    end

    it "respects the existing lockfile, even when reresolving" do
      gemfile <<~G
        source "#{file_uri_for(gem_repo4)}"

        gem "debug"
      G

      lockfile <<~L
        GEM
          remote: #{file_uri_for(gem_repo4)}/
          specs:
            debug (1.6.3)
              irb (>= 1.3.6)
            irb (1.5.0)

        PLATFORMS
          x86_64-linux

        DEPENDENCIES
          debug
        #{checksums_section}
        BUNDLED WITH
           #{Bundler::VERSION}
      L

      simulate_platform "arm64-darwin-22" do
        bundle "lock"
      end

      checksums = checksums_section do |c|
        c.no_checksum "debug", "1.6.3"
        c.no_checksum "irb", "1.5.0"
      end

      expect(lockfile).to eq <<~L
        GEM
          remote: #{file_uri_for(gem_repo4)}/
          specs:
            debug (1.6.3)
              irb (>= 1.3.6)
            irb (1.5.0)

        PLATFORMS
          arm64-darwin-22
          x86_64-linux

        DEPENDENCIES
          debug
        #{checksums}
        BUNDLED WITH
           #{Bundler::VERSION}
      L
    end
  end

  it "properly shows resolution errors including OR requirements" do
    build_repo4 do
      build_gem "activeadmin", "2.13.1" do |s|
        s.add_dependency "railties", ">= 6.1", "< 7.1"
      end
      build_gem "actionpack", "6.1.4"
      build_gem "actionpack", "7.0.3.1"
      build_gem "actionpack", "7.0.4"
      build_gem "railties", "6.1.4" do |s|
        s.add_dependency "actionpack", "6.1.4"
      end
      build_gem "rails", "7.0.3.1" do |s|
        s.add_dependency "railties", "7.0.3.1"
      end
      build_gem "rails", "7.0.4" do |s|
        s.add_dependency "railties", "7.0.4"
      end
    end

    gemfile <<~G
      source "#{file_uri_for(gem_repo4)}"

      gem "rails", ">= 7.0.3.1"
      gem "activeadmin", "2.13.1"
    G

    bundle "lock", raise_on_error: false

    expect(err).to eq <<~ERR.strip
      Could not find compatible versions

      Because rails >= 7.0.4 depends on railties = 7.0.4
        and rails < 7.0.4 depends on railties = 7.0.3.1,
        railties = 7.0.3.1 OR = 7.0.4 is required.
      So, because railties = 7.0.3.1 OR = 7.0.4 could not be found in rubygems repository #{file_uri_for(gem_repo4)}/ or installed locally,
        version solving has failed.
    ERR
  end

  it "is able to display some explanation on crazy irresolvable cases" do
    build_repo4 do
      build_gem "activeadmin", "2.13.1" do |s|
        s.add_dependency "ransack", "= 3.1.0"
      end

      # Activemodel is missing as a dependency in lockfile
      build_gem "ransack", "3.1.0" do |s|
        s.add_dependency "activemodel", ">= 6.0.4"
        s.add_dependency "activesupport", ">= 6.0.4"
      end

      %w[6.0.4 7.0.2.3 7.0.3.1 7.0.4].each do |version|
        build_gem "activesupport", version

        # Activemodel is only available on 6.0.4
        if version == "6.0.4"
          build_gem "activemodel", version do |s|
            s.add_dependency "activesupport", version
          end
        end

        build_gem "rails", version do |s|
          # Depednencies of Rails 7.0.2.3 are in reverse order
          if version == "7.0.2.3"
            s.add_dependency "activesupport", version
            s.add_dependency "activemodel", version
          else
            s.add_dependency "activemodel", version
            s.add_dependency "activesupport", version
          end
        end
      end
    end

    gemfile <<~G
      source "#{file_uri_for(gem_repo4)}"

      gem "rails", ">= 7.0.2.3"
      gem "activeadmin", "= 2.13.1"
    G

    lockfile <<~L
      GEM
        remote: #{file_uri_for(gem_repo4)}/
        specs:
          activeadmin (2.13.1)
            ransack (= 3.1.0)
          ransack (3.1.0)
            activemodel (>= 6.0.4)

      PLATFORMS
        #{local_platform}

      DEPENDENCIES
        activeadmin (= 2.13.1)
        ransack (= 3.1.0)

      BUNDLED WITH
         #{Bundler::VERSION}
    L

    bundle "lock", raise_on_error: false

    expect(err).to eq <<~ERR.strip
      Could not find compatible versions

          Because every version of activemodel depends on activesupport = 6.0.4
            and rails >= 7.0.2.3, < 7.0.3.1 depends on activesupport = 7.0.2.3,
            every version of activemodel is incompatible with rails >= 7.0.2.3, < 7.0.3.1.
          And because rails >= 7.0.2.3, < 7.0.3.1 depends on activemodel = 7.0.2.3,
            rails >= 7.0.2.3, < 7.0.3.1 cannot be used.
      (1) So, because rails >= 7.0.3.1, < 7.0.4 depends on activemodel = 7.0.3.1
            and rails >= 7.0.4 depends on activemodel = 7.0.4,
            rails >= 7.0.2.3 requires activemodel = 7.0.3.1 OR = 7.0.4.

          Because rails >= 7.0.2.3, < 7.0.3.1 depends on activemodel = 7.0.2.3
            and rails >= 7.0.3.1, < 7.0.4 depends on activesupport = 7.0.3.1,
            rails >= 7.0.2.3, < 7.0.4 requires activemodel = 7.0.2.3 or activesupport = 7.0.3.1.
          And because rails >= 7.0.4 depends on activesupport = 7.0.4
            and every version of activemodel depends on activesupport = 6.0.4,
            activemodel != 7.0.2.3 is incompatible with rails >= 7.0.2.3.
          And because rails >= 7.0.2.3 requires activemodel = 7.0.3.1 OR = 7.0.4 (1),
            rails >= 7.0.2.3 cannot be used.
          So, because Gemfile depends on rails >= 7.0.2.3,
            version solving has failed.
    ERR

    lockfile lockfile.gsub(/PLATFORMS\n  #{local_platform}/m, "PLATFORMS\n  #{lockfile_platforms("ruby")}")

    bundle "lock", raise_on_error: false

    expect(err).to eq <<~ERR.strip
      Could not find compatible versions

      Because rails >= 7.0.3.1, < 7.0.4 depends on activemodel = 7.0.3.1
        and rails >= 7.0.2.3, < 7.0.3.1 depends on activemodel = 7.0.2.3,
        rails >= 7.0.2.3, < 7.0.4 requires activemodel = 7.0.2.3 OR = 7.0.3.1.
      And because every version of activemodel depends on activesupport = 6.0.4,
        rails >= 7.0.2.3, < 7.0.4 requires activesupport = 6.0.4.
      Because rails >= 7.0.3.1, < 7.0.4 depends on activesupport = 7.0.3.1
        and rails >= 7.0.2.3, < 7.0.3.1 depends on activesupport = 7.0.2.3,
        rails >= 7.0.2.3, < 7.0.4 requires activesupport = 7.0.2.3 OR = 7.0.3.1.
      Thus, rails >= 7.0.2.3, < 7.0.4 cannot be used.
      And because rails >= 7.0.4 depends on activemodel = 7.0.4,
        rails >= 7.0.2.3 requires activemodel = 7.0.4.
      So, because activemodel = 7.0.4 could not be found in rubygems repository #{file_uri_for(gem_repo4)}/ or installed locally
        and Gemfile depends on rails >= 7.0.2.3,
        version solving has failed.
    ERR
  end

  it "does not accidentally resolves to prereleases" do
    build_repo4 do
      build_gem "autoproj", "2.0.3" do |s|
        s.add_dependency "autobuild", ">= 1.10.0.a"
        s.add_dependency "tty-prompt"
      end

      build_gem "tty-prompt", "0.6.0"
      build_gem "tty-prompt", "0.7.0"

      build_gem "autobuild", "1.10.0.b3"
      build_gem "autobuild", "1.10.1" do |s|
        s.add_dependency "tty-prompt", "~> 0.6.0"
      end
    end

    gemfile <<~G
      source "#{file_uri_for(gem_repo4)}"
      gem "autoproj", ">= 2.0.0"
    G

    bundle "lock"
    expect(lockfile).to_not include("autobuild (1.10.0.b3)")
    expect(lockfile).to include("autobuild (1.10.1)")
  end

  # Newer rails depends on Bundler, while ancient Rails does not. Bundler tries
  # a first resolution pass that does not consider pre-releases. However, when
  # using a pre-release Bundler (like the .dev version), that results in that
  # pre-release being ignored and resolving to a version that does not depend on
  # Bundler at all. We should avoid that and still consider .dev Bundler.
  #
  it "does not ignore prereleases with there's only one candidate" do
    build_repo4 do
      build_gem "rails", "7.4.0.2" do |s|
        s.add_dependency "bundler", ">= 1.15.0"
      end

      build_gem "rails", "2.3.18"
    end

    gemfile <<~G
      source "#{file_uri_for(gem_repo4)}"
      gem "rails"
    G

    bundle "lock"
    expect(lockfile).to_not include("rails (2.3.18)")
    expect(lockfile).to include("rails (7.4.0.2)")
  end

  it "deals with platform specific incompatibilities" do
    build_repo4 do
      build_gem "activerecord", "6.0.6"
      build_gem "activerecord-jdbc-adapter", "60.4" do |s|
        s.platform = "java"
        s.add_dependency "activerecord", "~> 6.0.0"
      end
      build_gem "activerecord-jdbc-adapter", "61.0" do |s|
        s.platform = "java"
        s.add_dependency "activerecord", "~> 6.1.0"
      end
    end

    gemfile <<~G
      source "#{file_uri_for(gem_repo4)}"
      gem "activerecord", "6.0.6"
      gem "activerecord-jdbc-adapter", "61.0"
    G

    simulate_platform "universal-java-19" do
      bundle "lock", raise_on_error: false
    end

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

  context "when re-resolving to include prereleases" do
    before do
      build_repo4 do
        build_gem "tzinfo-data", "1.2022.7"
        build_gem "rails", "7.1.0.alpha" do |s|
          s.add_dependency "activesupport"
        end
        build_gem "activesupport", "7.1.0.alpha"
      end
    end

    it "does not end up including gems scoped to other platforms in the lockfile" do
      gemfile <<-G
        source "#{file_uri_for(gem_repo4)}"
        gem "rails"
        gem "tzinfo-data", platform: :windows
      G

      simulate_platform "x86_64-darwin-22" do
        bundle "lock"
      end

      expect(lockfile).not_to include("tzinfo-data (1.2022.7)")
    end
  end

  context "when resolving platform specific gems as indirect dependencies on truffleruby", :truffleruby_only do
    before do
      build_lib "foo", path: bundled_app do |s|
        s.add_dependency "nokogiri"
      end

      build_repo4 do
        build_gem "nokogiri", "1.14.2"
        build_gem "nokogiri", "1.14.2" do |s|
          s.platform = "x86_64-linux"
        end
      end

      gemfile <<-G
        source "#{file_uri_for(gem_repo4)}"
        gemspec
      G
    end

    it "locks ruby specs" do
      checksums = checksums_section_when_existing do |c|
        c.no_checksum "foo", "1.0"
        c.no_checksum "nokogiri", "1.14.2"
      end

      simulate_platform "x86_64-linux" do
        bundle "lock"
      end

      expect(lockfile).to eq <<~L
        PATH
          remote: .
          specs:
            foo (1.0)
              nokogiri

        GEM
          remote: #{file_uri_for(gem_repo4)}/
          specs:
            nokogiri (1.14.2)

        PLATFORMS
          #{lockfile_platforms}

        DEPENDENCIES
          foo!
        #{checksums}
        BUNDLED WITH
           #{Bundler::VERSION}
      L
    end
  end

  context "when adding a new gem that requires unlocking other transitive deps" do
    before do
      build_repo4 do
        build_gem "govuk_app_config", "0.1.0"

        build_gem "govuk_app_config", "4.13.0" do |s|
          s.add_dependency "railties", ">= 5.0"
        end

        %w[7.0.4.1 7.0.4.3].each do |v|
          build_gem "railties", v do |s|
            s.add_dependency "actionpack", v
            s.add_dependency "activesupport", v
          end

          build_gem "activesupport", v
          build_gem "actionpack", v
        end
      end

      gemfile <<~G
        source "#{file_uri_for(gem_repo4)}"

        gem "govuk_app_config"
        gem "activesupport", "7.0.4.3"
      G

      # Simulate out of sync lockfile because top level dependency on
      # activesuport has just been added to the Gemfile, and locked to a higher
      # version
      lockfile <<~L
        GEM
          remote: #{file_uri_for(gem_repo4)}/
          specs:
            actionpack (7.0.4.1)
            activesupport (7.0.4.1)
            govuk_app_config (4.13.0)
              railties (>= 5.0)
            railties (7.0.4.1)
              actionpack (= 7.0.4.1)
              activesupport (= 7.0.4.1)

        PLATFORMS
          arm64-darwin-22

        DEPENDENCIES
          govuk_app_config

        BUNDLED WITH
           #{Bundler::VERSION}
      L
    end

    it "does not downgrade top level dependencies" do
      checksums = checksums_section_when_existing do |c|
        c.no_checksum "actionpack", "7.0.4.3"
        c.no_checksum "activesupport", "7.0.4.3"
        c.no_checksum "govuk_app_config", "4.13.0"
        c.no_checksum "railties", "7.0.4.3"
      end

      simulate_platform "arm64-darwin-22" do
        bundle "lock"
      end

      expect(lockfile).to eq <<~L
        GEM
          remote: #{file_uri_for(gem_repo4)}/
          specs:
            actionpack (7.0.4.3)
            activesupport (7.0.4.3)
            govuk_app_config (4.13.0)
              railties (>= 5.0)
            railties (7.0.4.3)
              actionpack (= 7.0.4.3)
              activesupport (= 7.0.4.3)

        PLATFORMS
          arm64-darwin-22

        DEPENDENCIES
          activesupport (= 7.0.4.3)
          govuk_app_config
        #{checksums}
        BUNDLED WITH
           #{Bundler::VERSION}
      L
    end
  end
end
