require "bundled_gems"

require "bundler"
require "fileutils"

require_relative "bundler/support/builders"
require_relative "bundler/support/helpers"
require_relative "bundler/support/path"

module Gem
  def self.ruby=(ruby)
    @ruby = ruby
  end
end

RSpec.configure do |config|
  config.include Spec::Builders
  config.include Spec::Helpers
  config.include Spec::Path

  config.before(:suite) do
    Gem.ruby = ENV["RUBY"] if ENV["RUBY"]

    require_relative "bundler/support/rubygems_ext"
    Spec::Helpers.install_dev_bundler
    FileUtils.mkdir_p Spec::Path.gem_path
  end

  config.around(:each) do |example|
    FileUtils.cp_r Spec::Path.pristine_system_gem_path, Spec::Path.system_gem_path

    with_gem_path_as(system_gem_path) do
      Bundler.ui.silence { example.run }

      all_output = all_commands_output
      if example.exception && !all_output.empty?
        message = all_output + "\n" + example.exception.message
        (class << example.exception; self; end).send(:define_method, :message) do
          message
        end
      end
    end
  ensure
    reset!
  end

  config.after :suite do
    FileUtils.rm_rf Spec::Path.pristine_system_gem_path
  end
end

RSpec.describe "bundled_gems.rb" do
  let(:stub_code) {
    source_lib_dir = File.realpath(Spec::Path.source_lib_dir.to_s)
    <<~STUB
      Gem::BUNDLED_GEMS.send(:remove_const, :LIBDIR)
      Gem::BUNDLED_GEMS.send(:remove_const, :ARCHDIR)
      Gem::BUNDLED_GEMS.send(:remove_const, :SINCE)
      Gem::BUNDLED_GEMS.const_set(:LIBDIR, "#{source_lib_dir}/")
      Gem::BUNDLED_GEMS.const_set(:ARCHDIR, File.expand_path($LOAD_PATH.find{|path| path.include?(".ext/common") }) + "/")
      Gem::BUNDLED_GEMS.const_set(:SINCE, { "openssl" => RUBY_VERSION, "fileutils" => RUBY_VERSION, "csv" => "3.4.0", "net-smtp" => "3.1.0" })
    STUB
  }

  def script(code, options = {})
    options[:artifice] ||= "compact_index"
    code = <<~RUBY
      #{stub_code}
      require 'bundler/inline'

      #{code}
    RUBY
    ruby(code, options)
  end

  it "Show warning require and LoadError" do
    script <<-RUBY
      gemfile do
        source "https://rubygems.org"
      end

      begin
        require "csv"
      rescue LoadError
      end
      require "openssl"
    RUBY

    expect(err).to include(/csv used to be loaded from (.*) since Ruby 3.4.0/)
    expect(err).to include(/-e:15/)
    expect(err).to include(/openssl used to be loaded from (.*) since Ruby #{RUBY_VERSION}/)
    expect(err).to include(/-e:18/)
  end

  it "Show warning to contact the author when the require is issued from a gem" do
    # Install the gem under the system gem path so its require shows up as
    # coming from `<gem_path>/gems/tinygem-1.0.0`, the same shape as the
    # childprocess-5.0.0 case the attribution logic targets.
    gem_dir = system_gem_path("gems/tinygem-1.0.0")
    build_lib "tinygem", "1.0.0", path: gem_dir do |s|
      s.write "lib/tinygem.rb", "require 'openssl'"
    end

    script <<-RUBY
      gemfile do
        source "https://rubygems.org"
        path "#{gem_dir}" do
          gem "tinygem", "1.0.0"
        end
      end

      require "tinygem"
    RUBY

    expect(err).to include(/openssl used to be loaded from (.*) since Ruby #{RUBY_VERSION}/)
    expect(err).to include("Also please contact the author of tinygem-1.0.0 to request adding openssl into its gemspec.")
  end

  it "Show warning when bundled gems called as dependency" do
    build_lib "activesupport", "7.0.7.2" do |s|
      s.write "lib/active_support/all.rb", "require 'openssl'"
    end

    script <<-RUBY, env: { "BUNDLER_SPEC_GEM_REPO" => gem_repo1.to_s }
      gemfile do
        source "https://gem.repo1"
        path "#{lib_path}" do
          gem "activesupport", "7.0.7.2"
        end
      end

      require "active_support/all"
    RUBY

    expect(err).to include(/openssl used to be loaded from (.*) since Ruby #{RUBY_VERSION}/)
    expect(err).to include(/lib\/active_support\/all\.rb:1/)
  end

  it "Show warning dash gem like net/smtp" do
    script <<-RUBY
      gemfile do
        source "https://rubygems.org"
      end

      begin
        require "net/smtp"
      rescue LoadError
      end
    RUBY

    expect(err).to include(/net\/smtp used to be loaded from (.*) since Ruby 3.1.0/)
    expect(err).to include(/-e:15/)
    expect(err).to include("You can add net-smtp")
  end

  it "Show warning sub-feature like openssl/bn" do
    skip "This test is not working on Windows" if Gem.win_platform?

    script <<-RUBY
      gemfile do
        source "https://rubygems.org"
      end

      require "openssl/bn"
    RUBY

    expect(err).to include(/openssl\/bn is found in openssl, (.*) part of the default gems since Ruby #{RUBY_VERSION}/)
    expect(err).to include(/-e:14/)
  end

  it "Show warning when bundle exec with ruby and script" do
    code = <<-RUBY
      #{stub_code}
      require "openssl"
    RUBY
    create_file("script.rb", code)
    create_file("Gemfile", "source 'https://rubygems.org'")

    bundle "exec ruby script.rb"

    expect(err).to include(/openssl used to be loaded from (.*) since Ruby #{RUBY_VERSION}/)
    expect(err).to include(/script\.rb:8/)
  end

  it "Show warning when bundle exec with shebang's script" do
    skip "This test is not working on Windows" if Gem.win_platform?

    code = <<-RUBY
      #!/usr/bin/env ruby
      #{stub_code}
      require "openssl"
    RUBY
    create_file("script.rb", code)
    FileUtils.chmod(0o777, bundled_app("script.rb"))
    create_file("Gemfile", "source 'https://rubygems.org'")

    bundle "exec ./script.rb"

    expect(err).to include(/openssl used to be loaded from (.*) since Ruby #{RUBY_VERSION}/)
    expect(err).to include(/script\.rb:9/)
  end

  it "Show warning when bundle exec with -r option" do
    create_file("stub.rb", stub_code)
    create_file("Gemfile", "source 'https://rubygems.org'")
    bundle "exec ruby -r./stub -ropenssl -e ''"

    expect(err).to include(/openssl used to be loaded from (.*) since Ruby #{RUBY_VERSION}/)
  end

  it "Show warning when warn is not the standard one in the current scope" do
    script <<-RUBY
      module My
        def warn(msg)
        end

        def my
          gemfile do
            source "https://rubygems.org"
          end

          require "openssl"
        end

        extend self
      end

      My.my
    RUBY

    expect(err).to include(/openssl used to be loaded from (.*) since Ruby #{RUBY_VERSION}/)
    expect(err).to include(/-e:19/)
  end

  it "Don't show warning when bundled gems called as dependency" do
    build_lib "activesupport", "7.0.7.2" do |s|
      s.write "lib/active_support/all.rb", "require 'openssl'"
    end
    build_lib "openssl", "1.0.0" do |s|
      s.write "lib/openssl.rb", "puts 'openssl'"
    end

    script <<-RUBY, env: { "BUNDLER_SPEC_GEM_REPO" => gem_repo1.to_s }
      gemfile do
        source "https://gem.repo1"
        path "#{lib_path}" do
          gem "activesupport", "7.0.7.2"
          gem "openssl"
        end
      end

      require "active_support/all"
    RUBY

    expect(err).to be_empty
  end

  it "Show warning with bootsnap cases" do
    script <<-RUBY
      gemfile do
        source "https://rubygems.org"
        # gem "bootsnap", require: false
      end

      # require 'bootsnap'
      # Bootsnap.setup(cache_dir: 'tmp/cache')

      # bootsnap expand required feature to full path
      # require 'openssl'
      require Gem::BUNDLED_GEMS::ARCHDIR + 'openssl'
    RUBY

    expect(err).to include(/openssl used to be loaded from (.*) since Ruby #{RUBY_VERSION}/)
    # TODO: We should assert caller location like below:
    # test_warn_bootsnap.rb:14: warning: ...
  end

  it "Show warning with bootsnap for gem with native extension" do
    script <<-RUBY
      gemfile do
        source "https://rubygems.org"
        # gem "bootsnap", require: false
      end

      # require 'bootsnap'
      # Bootsnap.setup(cache_dir: 'tmp/cache')

      # bootsnap expand required feature to full path
      # require 'openssl'
      require Gem::BUNDLED_GEMS::ARCHDIR + "openssl"
    RUBY

    expect(err).to include(/openssl used to be loaded from (.*) since Ruby #{RUBY_VERSION}/)
    # TODO: We should assert caller location like below:
    # test_warn_bootsnap_rubyarchdir_gem.rb:14: warning: ...
  end

  it "Show warning with bootsnap and some gem in Gemfile" do
    # Original issue is childprocess 5.0.0 and logger.
    build_lib "fileutils2", "5.0.0" do |s|
      # bootsnap expand required feature to full path
      rubylibpath = File.realpath(File.join(__dir__, "..", "lib"))
      s.write "lib/fileutils2.rb", "require '#{rubylibpath}/fileutils'"
    end

    script <<-RUBY
      gemfile do
        source "https://rubygems.org"
        # gem "bootsnap", require: false
        path "#{lib_path}" do
          gem "fileutils2", "5.0.0"
        end
      end

      # require 'bootsnap'
      # Bootsnap.setup(cache_dir: 'tmp/cache')

      # bootsnap expand required feature to full path
      require Gem.loaded_specs["fileutils2"].full_gem_path + '/lib/fileutils2'
    RUBY

    expect(err).to include(/fileutils used to be loaded from (.*) since Ruby #{RUBY_VERSION}/)
    # TODO: We should assert caller location like below:
    # $GEM_HOME/gems/childprocess-5.0.0/lib/childprocess.rb:7: warning:
  end

  it "Show warning with zeitwerk" do
    libpath = Dir[File.expand_path("../.bundle/gems/{zeitwerk}-*/lib", __dir__)].map(&:to_s).first
    code = <<-RUBY
      #{stub_code}
      $LOAD_PATH.unshift("#{libpath}")
      require "zeitwerk"
      loader = Zeitwerk::Loader.for_gem(warn_on_extra_files: false)
      loader.setup

      require 'openssl'
    RUBY
    create_file("script.rb", code)
    create_file("Gemfile", "source 'https://rubygems.org'")
    bundle "exec ruby script.rb"

    expect(err).to include(/openssl used to be loaded from (.*) since Ruby #{RUBY_VERSION}/)
    expect(err).to include(/script\.rb:13/)
  end

  it "Don't show warning openssl/bn when openssl on Gemfile" do
    build_lib "openssl", "1.0.0" do |s|
      s.write "lib/openssl.rb", "puts 'openssl'"
      s.write "lib/openssl/bn.rb", "puts 'openssl/bn'"
    end

    script <<-RUBY, env: { "BUNDLER_SPEC_GEM_REPO" => gem_repo1.to_s }
      gemfile do
        source "https://gem.repo1"
        path "#{lib_path}" do
          gem "openssl"
        end
      end

      require "openssl/bn"
    RUBY

    expect(err).to be_empty
  end

  it "Don't show warning with net/smtp when net-smtp on Gemfile" do
    build_lib "net-smtp", "1.0.0" do |s|
      s.write "lib/net/smtp.rb", "puts 'net-smtp'"
    end

    script <<-RUBY, env: { "BUNDLER_SPEC_GEM_REPO" => gem_repo1.to_s }
      gemfile do
        source "https://gem.repo1"
        path "#{lib_path}" do
          gem "net-smtp"
        end
      end

      require "net/smtp"
    RUBY

    expect(err).to be_empty
  end

  describe ".force_activate" do
    before do
      allow_any_instance_of(Bundler::Runtime).to receive(:setup).and_raise(Bundler::GemNotFound)
    end

    context "with bundle environment" do
      # Windows has no executable bit or shebang dispatch, so running the
      # script directly is rejected by bundler as "not executable". Invoke it
      # through ruby there. What matters here is force_activate's behavior under
      # the bundle environment, not shebang execution (covered by another spec).
      let(:exec_command) { Gem.win_platform? ? "exec ruby ./script.rb" : "exec ./script.rb" }

      before do
        code = <<-RUBY
          #!/usr/bin/env ruby

          Gem::BUNDLED_GEMS.force_activate("csv")
        RUBY
        create_file("script.rb", code)
        create_file("Gemfile", "source 'https://rubygems.org'")
      end

      it "lockfile is available" do
        bundle "install"
        bundle exec_command

        expect(err).to include("gem install csv")
      end

      it "lockfile is not available" do
        bundle exec_command

        expect(err).to include("gem install csv")
      end
    end

    context "with bundler/inline" do
      it "foo is available on LOAD_PATH" do
        build_lib "foo", "1.0.0" do |s|
          s.write "lib/foo.rb", "puts :foo"
        end

        script <<-RUBY, env: { "BUNDLER_SPEC_GEM_REPO" => gem_repo1.to_s }
          #!/usr/bin/env ruby
          gemfile do
            source "https://gem.repo1"
            path "#{lib_path}" do
              gem "foo", "1.0.0"
            end
          end

          Gem::BUNDLED_GEMS.force_activate("csv")
          puts $LOAD_PATH
        RUBY

        expect(err).to include("gem install csv")
        expect(out).to include("foo-1.0.0/lib")
      end
    end

    context "without bundle environment" do
      it "warns about installation requirement" do
        expect_any_instance_of(Object).to receive(:warn)
        Gem::BUNDLED_GEMS.force_activate("csv")
      end
    end
  end
end
