# frozen_string_literal: true
require 'rubygems/test_case'
require 'rubygems'

class TestGemRequire < Gem::TestCase
  class Latch
    def initialize count = 1
      @count = count
      @lock  = Monitor.new
      @cv    = @lock.new_cond
    end

    def release
      @lock.synchronize do
        @count -= 1 if @count > 0
        @cv.broadcast if @count.zero?
      end
    end

    def await
      @lock.synchronize do
        @cv.wait_while { @count > 0 }
      end
    end
  end

  def setup
    super

    @old_loaded_features = $LOADED_FEATURES.dup
    assert_raises LoadError do
      require 'test_gem_require_a'
    end
    $LOADED_FEATURES.replace @old_loaded_features
  end

  def assert_require(path)
    assert require(path), "'#{path}' was already required"
  end

  def append_latch spec
    dir = spec.gem_dir
    Dir.chdir dir do
      spec.files.each do |file|
        File.open file, 'a' do |fp|
          fp.puts "FILE_ENTERED_LATCH.release"
          fp.puts "FILE_EXIT_LATCH.await"
        end
      end
    end
  end

  # Providing -I on the commandline should always beat gems
  def test_dash_i_beats_gems
    a1 = new_spec "a", "1", {"b" => "= 1"}, "lib/test_gem_require_a.rb"
    b1 = new_spec "b", "1", {"c" => "> 0"}, "lib/b/c.rb"
    c1 = new_spec "c", "1", nil, "lib/c/c.rb"
    c2 = new_spec "c", "2", nil, "lib/c/c.rb"

    install_specs c1, c2, b1, a1

    dir = Dir.mktmpdir("test_require", @tempdir)
    dash_i_arg = File.join dir, 'lib'

    c_rb = File.join dash_i_arg, 'b', 'c.rb'

    FileUtils.mkdir_p File.dirname c_rb
    File.open(c_rb, 'w') { |f| f.write "class Object; HELLO = 'world' end" }

    lp = $LOAD_PATH.dup

    # Pretend to provide a commandline argument that overrides a file in gem b
    $LOAD_PATH.unshift dash_i_arg

    assert_require 'test_gem_require_a'
    assert_require 'b/c' # this should be required from -I
    assert_equal "world", ::Object::HELLO
  ensure
    $LOAD_PATH.replace lp
    Object.send :remove_const, :HELLO if Object.const_defined? :HELLO
  end

  def test_concurrent_require
    skip 'deadlock' if /^1\.8\./ =~ RUBY_VERSION

    Object.const_set :FILE_ENTERED_LATCH, Latch.new(2)
    Object.const_set :FILE_EXIT_LATCH, Latch.new(1)

    a1 = new_spec "a", "1", nil, "lib/a.rb"
    b1 = new_spec "b", "1", nil, "lib/b.rb"

    install_specs a1, b1

    append_latch a1
    append_latch b1

    t1 = Thread.new { assert_require 'a' }
    t2 = Thread.new { assert_require 'b' }

    # wait until both files are waiting on the exit latch
    FILE_ENTERED_LATCH.await

    # now let them finish
    FILE_EXIT_LATCH.release

    assert t1.join, "thread 1 should exit"
    assert t2.join, "thread 2 should exit"
  ensure
    return if $! # skipping

    Object.send :remove_const, :FILE_ENTERED_LATCH
    Object.send :remove_const, :FILE_EXIT_LATCH
  end

  def test_require_is_not_lazy_with_exact_req
    a1 = new_spec "a", "1", {"b" => "= 1"}, "lib/test_gem_require_a.rb"
    b1 = new_spec "b", "1", nil, "lib/b/c.rb"
    b2 = new_spec "b", "2", nil, "lib/b/c.rb"

    install_specs b1, b2, a1

    assert_require 'test_gem_require_a'
    assert_equal %w(a-1 b-1), loaded_spec_names
    assert_equal unresolved_names, []

    assert_require "b/c"
    assert_equal %w(a-1 b-1), loaded_spec_names
  end

  def test_require_is_lazy_with_inexact_req
    a1 = new_spec "a", "1", {"b" => ">= 1"}, "lib/test_gem_require_a.rb"
    b1 = new_spec "b", "1", nil, "lib/b/c.rb"
    b2 = new_spec "b", "2", nil, "lib/b/c.rb"

    install_specs b1, b2, a1

    assert_require 'test_gem_require_a'
    assert_equal %w(a-1), loaded_spec_names
    assert_equal unresolved_names, ["b (>= 1)"]

    assert_require "b/c"
    assert_equal %w(a-1 b-2), loaded_spec_names
  end

  def test_require_is_not_lazy_with_one_possible
    a1 = new_spec "a", "1", {"b" => ">= 1"}, "lib/test_gem_require_a.rb"
    b1 = new_spec "b", "1", nil, "lib/b/c.rb"

    install_specs b1, a1

    assert_require 'test_gem_require_a'
    assert_equal %w(a-1 b-1), loaded_spec_names
    assert_equal unresolved_names, []

    assert_require "b/c"
    assert_equal %w(a-1 b-1), loaded_spec_names
  end

  def test_require_can_use_a_pathname_object
    a1 = new_spec "a", "1", nil, "lib/test_gem_require_a.rb"

    install_specs a1

    assert_require Pathname.new 'test_gem_require_a'
    assert_equal %w(a-1), loaded_spec_names
    assert_equal unresolved_names, []
  end

  def test_activate_via_require_respects_loaded_files
    a1 = new_spec "a", "1", {"b" => ">= 1"}, "lib/test_gem_require_a.rb"
    b1 = new_spec "b", "1", nil, "lib/benchmark.rb"
    b2 = new_spec "b", "2", nil, "lib/benchmark.rb"

    install_specs b1, b2, a1

    require 'test_gem_require_a'
    assert_equal unresolved_names, ["b (>= 1)"]

    refute require('benchmark'), "benchmark should have already been loaded"

    # We detected that we should activate b-2, so we did so, but
    # then original_require decided "I've already got benchmark.rb" loaded.
    # This case is fine because our lazy loading is provided exactly
    # the same behavior as eager loading would have.

    assert_equal %w(a-1 b-2), loaded_spec_names
  end

  def test_already_activated_direct_conflict
    a1 = new_spec "a", "1", { "b" => "> 0" }
    b1 = new_spec "b", "1", { "c" => ">= 1" }, "lib/ib.rb"
    b2 = new_spec "b", "2", { "c" => ">= 2" }, "lib/ib.rb"
    c1 = new_spec "c", "1", nil, "lib/d.rb"
    c2 = new_spec("c", "2", nil, "lib/d.rb")

    install_specs c1, c2, b1, b2, a1

    a1.activate
    c1.activate
    assert_equal %w(a-1 c-1), loaded_spec_names
    assert_equal ["b (> 0)"], unresolved_names

    assert require("ib")

    assert_equal %w(a-1 b-1 c-1), loaded_spec_names
    assert_equal [], unresolved_names
  end

  def test_multiple_gems_with_the_same_path
    a1 = new_spec "a", "1", { "b" => "> 0", "x" => "> 0" }
    b1 = new_spec "b", "1", { "c" => ">= 1" }, "lib/ib.rb"
    b2 = new_spec "b", "2", { "c" => ">= 2" }, "lib/ib.rb"
    x1 = new_spec "x", "1", nil, "lib/ib.rb"
    x2 = new_spec "x", "2", nil, "lib/ib.rb"
    c1 = new_spec "c", "1", nil, "lib/d.rb"
    c2 = new_spec("c", "2", nil, "lib/d.rb")

    install_specs c1, c2, x1, x2, b1, b2, a1

    a1.activate
    c1.activate
    assert_equal %w(a-1 c-1), loaded_spec_names
    assert_equal ["b (> 0)", "x (> 0)"], unresolved_names

    e = assert_raises(Gem::LoadError) do
      require("ib")
    end

    assert_equal "ib found in multiple gems: b, x", e.message
  end

  def test_unable_to_find_good_unresolved_version
    a1 = new_spec "a", "1", { "b" => "> 0" }
    b1 = new_spec "b", "1", { "c" => ">= 2" }, "lib/ib.rb"
    b2 = new_spec "b", "2", { "c" => ">= 3" }, "lib/ib.rb"

    c1 = new_spec "c", "1", nil, "lib/d.rb"
    c2 = new_spec "c", "2", nil, "lib/d.rb"
    c3 = new_spec "c", "3", nil, "lib/d.rb"

    install_specs c1, c2, c3, b1, b2, a1

    a1.activate
    c1.activate
    assert_equal %w(a-1 c-1), loaded_spec_names
    assert_equal ["b (> 0)"], unresolved_names

    e = assert_raises(Gem::LoadError) do
      require("ib")
    end

    assert_equal "unable to find a version of 'b' to activate", e.message
  end

  def test_require_works_after_cleanup
    a1 = new_default_spec "a", "1.0", nil, "a/b.rb"
    b1 = new_default_spec "b", "1.0", nil, "b/c.rb"
    b2 = new_default_spec "b", "2.0", nil, "b/d.rb"

    install_default_gems a1
    install_default_gems b1
    install_default_gems b2

    # Load default ruby gems fresh as if we've just started a ruby script.
    Gem::Specification.reset
    require 'rubygems'
    Gem::Specification.stubs

    # Remove an old default gem version directly from disk as if someone ran
    # gem cleanup.
    FileUtils.rm_rf(File.join @default_dir, "#{b1.full_name}")
    FileUtils.rm_rf(File.join @default_spec_dir, "#{b1.full_name}.gemspec")

    # Require gems that have not been removed.
    assert_require 'a/b'
    assert_equal %w(a-1.0), loaded_spec_names
    assert_require 'b/d'
    assert_equal %w(a-1.0 b-2.0), loaded_spec_names
  end

  def test_require_doesnt_traverse_development_dependencies
    a = new_spec("a", "1", nil, "lib/a.rb")
    z = new_spec("z", "1", "w" => "> 0")
    w1 = new_spec("w", "1") { |s| s.add_development_dependency "non-existent" }
    w2 = new_spec("w", "2") { |s| s.add_development_dependency "non-existent" }

    install_specs a, w1, w2, z

    assert gem("z")
    assert_equal %w(z-1), loaded_spec_names
    assert_equal ["w (> 0)"], unresolved_names

    assert require("a")
  end

  def test_default_gem_only
    default_gem_spec = new_default_spec("default", "2.0.0.0",
                                        nil, "default/gem.rb")
    install_default_specs(default_gem_spec)
    assert_require "default/gem"
    assert_equal %w(default-2.0.0.0), loaded_spec_names
  end

  def test_default_gem_and_normal_gem
    default_gem_spec = new_default_spec("default", "2.0.0.0",
                                        nil, "default/gem.rb")
    install_default_specs(default_gem_spec)
    normal_gem_spec = new_spec("default", "3.0", nil,
                               "lib/default/gem.rb")
    install_specs(normal_gem_spec)
    assert_require "default/gem"
    assert_equal %w(default-3.0), loaded_spec_names
  end

  def loaded_spec_names
    Gem.loaded_specs.values.map(&:full_name).sort
  end

  def unresolved_names
    Gem::Specification.unresolved_deps.values.map(&:to_s).sort
  end

  def test_try_activate_error_unlocks_require_monitor
    silence_warnings do
      class << ::Gem
        alias old_try_activate try_activate
        def try_activate(*); raise 'raised from try_activate'; end
      end
    end

    require 'does_not_exist_for_try_activate_test'
  rescue RuntimeError => e
    assert_match(/raised from try_activate/, e.message)
    assert Kernel::RUBYGEMS_ACTIVATION_MONITOR.try_enter, "require monitor was not unlocked when try_activate raised"
  ensure
    silence_warnings do
      class << ::Gem
        alias try_activate old_try_activate
      end
    end
    Kernel::RUBYGEMS_ACTIVATION_MONITOR.exit
  end

  def silence_warnings
    old_verbose, $VERBOSE = $VERBOSE, false
    yield
  ensure
    $VERBOSE = old_verbose
  end
end
