# frozen_string_literal: true

require 'test_helper'

class TestExplicitNamespace < LoaderTest
  module Namespace; end

  test 'explicit namespaces are loaded correctly (directory first, Object)' do
    files = [
      ['hotel.rb', 'class Hotel; X = 1; end'],
      ['hotel/pricing.rb', 'class Hotel::Pricing; end']
    ]
    with_setup(files) do
      assert_kind_of Class, Hotel
      assert Hotel::X
      assert Hotel::Pricing
    end
  end

  test 'explicit namespaces are loaded correctly (directory first, Namespace)' do
    files = [
      ['hotel.rb', "class #{Namespace}::Hotel; X = 1; end"],
      ['hotel/pricing.rb', "class #{Namespace}::Hotel::Pricing; end"]
    ]
    with_setup(files, namespace: Namespace) do
      assert_kind_of Class, Namespace::Hotel
      assert Namespace::Hotel::X
      assert Namespace::Hotel::Pricing
    end
  end

  test 'explicit namespaces are loaded correctly (file first, Object)' do
    files = [
      ['rd1/hotel.rb', 'class Hotel; X = 1; end'],
      ['rd2/hotel/pricing.rb', 'class Hotel::Pricing; end']
    ]
    with_setup(files) do
      assert_kind_of Class, Hotel
      assert Hotel::X
      assert Hotel::Pricing
    end
  end

  test 'explicit namespaces are loaded correctly (file first, Namespace)' do
    files = [
      ['rd1/hotel.rb', "class #{Namespace}::Hotel; X = 1; end"],
      ['rd2/hotel/pricing.rb', "class #{Namespace}::Hotel::Pricing; end"]
    ]
    with_setup(files, namespace: Namespace) do
      assert_kind_of Class, Namespace::Hotel
      assert Namespace::Hotel::X
      assert Namespace::Hotel::Pricing
    end
  end

  test 'explicit namespaces defined with an explicit constant assignment are loaded correctly' do
    files = [
      ['hotel.rb', 'Hotel = Class.new; Hotel::X = 1'],
      ['hotel/pricing.rb', 'class Hotel::Pricing; end']
    ]
    with_setup(files) do
      assert_kind_of Class, Hotel
      assert Hotel::X
      assert Hotel::Pricing
    end
  end

  test 'explicit namespaces are loaded correctly even if #name is overridden' do
    files = [
      ['hotel.rb', <<~RUBY],
        class Hotel
          def self.name
            "X"
          end
        end
      RUBY
      ['hotel/pricing.rb', 'class Hotel::Pricing; end']
    ]
    with_setup(files) do
      assert Hotel::Pricing
    end
  end

  test 'explicit namespaces managed by different instances' do
    files = [
      ['a/m.rb', 'module M; end'], ['a/m/n.rb', 'M::N = true'],
      ['b/x.rb', 'module X; end'], ['b/x/y.rb', 'X::Y = true'],
    ]
    with_files(files) do
      new_loader(dirs: 'a')
      new_loader(dirs: 'b')

      assert M::N
      assert X::Y
    end
  end

  test 'autoloads are set correctly, even if there are autoloads for the same cname in the superclass' do
    files = [
      ['a.rb', 'class A; end'],
      ['a/x.rb', 'A::X = :A'],
      ['b.rb', 'class B < A; end'],
      ['b/x.rb', 'B::X = :B']
    ]
    with_setup(files) do
      assert_kind_of Class, A
      assert_kind_of Class, B
      assert_equal :B, B::X
    end
  end

  test 'autoloads are set correctly, even if there are autoloads for the same cname in a module prepended to the superclass' do
    files = [
      ['m/x.rb', 'M::X = :M'],
      ['a.rb', 'class A; prepend M; end'],
      ['b.rb', 'class B < A; end'],
      ['b/x.rb', 'B::X = :B']
    ]
    with_setup(files) do
      assert_kind_of Class, A
      assert_kind_of Class, B
      assert_equal :B, B::X
    end
  end

  test 'autoloads are set correctly, even if there are autoloads for the same cname in other ancestors' do
    files = [
      ['m/x.rb', 'M::X = :M'],
      ['a.rb', 'class A; include M; end'],
      ['b.rb', 'class B < A; end'],
      ['b/x.rb', 'B::X = :B']
    ]
    with_setup(files) do
      assert_kind_of Class, A
      assert_kind_of Class, B
      assert_equal :B, B::X
    end
  end

  test 'namespace promotion updates the registry' do
    # We use two root directories to make sure the loader visits the implicit
    # rd1/m first, and the explicit rd2/m.rb after it.
    files = [
      ['rd1/m/x.rb', 'M::X = true'],
      ['rd2/m.rb', 'module M; end']
    ]
    with_setup(files) do
      assert_nil Zeitwerk::Registry.autoloads.registered?(File.expand_path('rd1/m'))
      assert_same loader, Zeitwerk::Registry.autoloads.registered?(File.expand_path('rd2/m.rb'))
    end
  end

  test 'non-hashable explicit namespaces are supported' do
    files = [
      ['m.rb', <<~EOS],
        module M
          # This method is overridden with a different arity. Therefore, M is
          # not hashable. See https://github.com/fxn/zeitwerk/issues/188.
          def self.hash(_)
          end
        end
      EOS
      ['m/x.rb', 'M::X = true']
    ]
    with_setup(files) do
      assert M::X
    end
  end

  test 'if the expected constant does not define a class or module object, we raise a controlled error' do
    on_teardown { remove_const :Hotel }

    files = [
      ['hotel.rb', 'Hotel = 1'],
      ['hotel/pricing.rb', 'class Hotel::Pricing; end']
    ]
    with_setup(files) do
      error = assert_raises(Zeitwerk::Error) { Hotel }
      assert_equal 'Hotel is expected to be a namespace, should be a class or module (got Integer)', error.message
    end
  end
end
