class RubyIndexer::RBSIndexerTest

def parse_rbs_methods(rbs, method_name)

def parse_rbs_methods(rbs, method_name)
  buffer = RBS::Buffer.new(content: rbs, name: "")
  _, _, declarations = RBS::Parser.parse_signature(buffer)
  index = RubyIndexer::Index.new
  indexer = RubyIndexer::RBSIndexer.new(index)
  pathname = Pathname.new("/file.rbs")
  indexer.process_signature(pathname, declarations)
  entry = index[method_name] #: as !nil
    .first #: as Entry::Method
  entry.signatures
end

def test_attaches_correct_owner_to_singleton_methods

def test_attaches_correct_owner_to_singleton_methods
  entries = @index["basename"] #: as Array[Entry::Method]
  refute_nil(entries)
  owner = entries.first&.owner #: as Entry::SingletonClass
  assert_instance_of(Entry::SingletonClass, owner)
  assert_equal("File::<Class:File>", owner.name)
end

def test_index_core_classes

def test_index_core_classes
  entries = @index["Array"] #: as !nil
  refute_nil(entries)
  # Array is a class but also an instance method on Kernel
  assert_equal(2, entries.length)
  entry = entries.find { |entry| entry.is_a?(Entry::Class) } #: as Entry::Class
  assert_match(%r{/gems/rbs-.*/core/array.rbs}, entry.file_path)
  assert_equal("array.rbs", entry.file_name)
  assert_equal("Object", entry.parent_class)
  assert_equal(1, entry.mixin_operations.length)
  enumerable_include = entry.mixin_operations.first #: as !nil
  assert_equal("Enumerable", enumerable_include.module_name)
  # Using fixed positions would be fragile, so let's just check some basics.
  assert_operator(entry.location.start_line, :>, 0)
  assert_operator(entry.location.end_line, :>, entry.location.start_line)
  assert_equal(0, entry.location.start_column)
  assert_operator(entry.location.end_column, :>, 0)
end

def test_index_core_constants

def test_index_core_constants
  entries = @index["RUBY_VERSION"] #: as !nil
  refute_nil(entries)
  assert_equal(1, entries.length)
  entries = @index["Complex::I"] #: as !nil
  refute_nil(entries)
  assert_equal(1, entries.length)
  entries = @index["Encoding::US_ASCII"] #: as !nil
  refute_nil(entries)
  assert_equal(1, entries.length)
end

def test_index_core_modules

def test_index_core_modules
  entries = @index["Kernel"] #: as !nil
  refute_nil(entries)
  assert_equal(1, entries.length)
  entry = entries.first #: as Entry::Module
  assert_match(%r{/gems/rbs-.*/core/kernel.rbs}, entry.file_path)
  assert_equal("kernel.rbs", entry.file_name)
  # Using fixed positions would be fragile, so let's just check some basics.
  assert_operator(entry.location.start_line, :>, 0)
  assert_operator(entry.location.end_line, :>, entry.location.start_line)
  assert_equal(0, entry.location.start_column)
  assert_operator(entry.location.end_column, :>, 0)
end

def test_index_global_declaration

def test_index_global_declaration
  entries = @index["$DEBUG"] #: as Array[Entry::GlobalVariable]
  refute_nil(entries)
  assert_equal(1, entries.length)
  entry = entries.first #: as Entry::GlobalVariable
  assert_instance_of(Entry::GlobalVariable, entry)
  assert_equal("$DEBUG", entry.name)
  assert_match(%r{/gems/rbs-.*/core/global_variables.rbs}, entry.file_path)
  assert_operator(entry.location.start_column, :<, entry.location.end_column)
  assert_equal(entry.location.start_line, entry.location.end_line)
end

def test_index_methods

def test_index_methods
  entries = @index["initialize"] #: as Array[Entry::Method]
  refute_nil(entries)
  entry = entries.find { |entry| entry.owner&.name == "Array" } #: as Entry::Method
  assert_match(%r{/gems/rbs-.*/core/array.rbs}, entry.file_path)
  assert_equal("array.rbs", entry.file_name)
  assert_equal(:public, entry.visibility)
  # Using fixed positions would be fragile, so let's just check some basics.
  assert_operator(entry.location.start_line, :>, 0)
  assert_operator(entry.location.end_line, :>, entry.location.start_line)
  assert_equal(2, entry.location.start_column)
  assert_operator(entry.location.end_column, :>, 0)
end

def test_indexing_untyped_functions

def test_indexing_untyped_functions
  entries = @index.resolve_method("call", "Method") #: as Array[Entry::Method]
  parameters = entries.first&.signatures&.first&.parameters #: as !nil
  assert_equal(1, parameters.length)
  assert_instance_of(Entry::ForwardingParameter, parameters.first)
end

def test_location_and_name_location_are_the_same

def test_location_and_name_location_are_the_same
  # NOTE: RBS does not store the name location for classes, modules or methods. This behavior is not exactly what
  # we would like, but for now we assign the same location to both
  entries = @index["Array"] #: as Array[Entry::Class]
  refute_nil(entries)
  entry = entries.find { |entry| entry.is_a?(Entry::Class) } #: as Entry::Class
  assert_same(entry.location, entry.name_location)
end

def test_parse_simple_rbs

def test_parse_simple_rbs
  rbs = <<~RBS
    class File
      def self?.open: (String name, ?String mode, ?Integer perm) -> IO?
          | [T] (String name, ?String mode, ?Integer perm) { (IO) -> T } -> T
    end
  RBS
  signatures = parse_rbs_methods(rbs, "open")
  assert_equal(2, signatures.length)
  parameters = signatures[0].parameters
  assert_equal([:name, :mode, :perm], parameters.map(&:name))
  assert_kind_of(Entry::RequiredParameter, parameters[0])
  assert_kind_of(Entry::OptionalParameter, parameters[1])
  assert_kind_of(Entry::OptionalParameter, parameters[2])
  parameters = signatures[1].parameters
  assert_equal([:name, :mode, :perm, :"<anonymous block>"], parameters.map(&:name))
  assert_kind_of(Entry::RequiredParameter, parameters[0])
  assert_kind_of(Entry::OptionalParameter, parameters[1])
  assert_kind_of(Entry::OptionalParameter, parameters[2])
  assert_kind_of(Entry::BlockParameter, parameters[3])
end

def test_rbs_anonymous_block_parameter

def test_rbs_anonymous_block_parameter
  entries = @index["open"] #: as Array[Entry::Method]
  entry = entries.find { |entry| entry.owner&.name == "File::<Class:File>" } #: as Entry::Method
  assert_equal(2, entry.signatures.length)
  # (::String name, ?::String mode, ?::Integer perm) -> ::IO?
  # | [T] (::String name, ?::String mode, ?::Integer perm) { (::IO) -> T } -> T
  parameters = entry.signatures[0]&.parameters #: as !nil
  assert_equal([:file_name, :mode, :perm], parameters.map(&:name))
  assert_kind_of(Entry::RequiredParameter, parameters[0])
  assert_kind_of(Entry::OptionalParameter, parameters[1])
  assert_kind_of(Entry::OptionalParameter, parameters[2])
  parameters = entry.signatures[1]&.parameters #: as !nil
  assert_equal([:file_name, :mode, :perm, :"<anonymous block>"], parameters.map(&:name))
  assert_kind_of(Entry::RequiredParameter, parameters[0])
  assert_kind_of(Entry::OptionalParameter, parameters[1])
  assert_kind_of(Entry::OptionalParameter, parameters[2])
  assert_kind_of(Entry::BlockParameter, parameters[3])
end

def test_rbs_method_with_optional_keywords

def test_rbs_method_with_optional_keywords
  entries = @index["step"] #: as Array[Entry::Method]
  entry = entries.find { |entry| entry.owner&.name == "Numeric" } #: as !nil
  signatures = entry.signatures
  assert_equal(4, signatures.length)
  # (?::Numeric limit, ?::Numeric step) { (::Numeric) -> void } -> self
  # | (?::Numeric limit, ?::Numeric step) -> ::Enumerator[::Numeric, self]
  # | (?by: ::Numeric, ?to: ::Numeric) { (::Numeric) -> void } -> self
  # | (?by: ::Numeric, ?to: ::Numeric) -> ::Enumerator[::Numeric, self]
  parameters = signatures[0]&.parameters #: as !nil
  assert_equal([:limit, :step, :"<anonymous block>"], parameters.map(&:name))
  assert_kind_of(Entry::OptionalParameter, parameters[0])
  assert_kind_of(Entry::OptionalParameter, parameters[1])
  assert_kind_of(Entry::BlockParameter, parameters[2])
  parameters = signatures[1]&.parameters #: as !nil
  assert_equal([:limit, :step], parameters.map(&:name))
  assert_kind_of(Entry::OptionalParameter, parameters[0])
  assert_kind_of(Entry::OptionalParameter, parameters[1])
  parameters = signatures[2]&.parameters #: as !nil
  assert_equal([:by, :to, :"<anonymous block>"], parameters.map(&:name))
  assert_kind_of(Entry::OptionalKeywordParameter, parameters[0])
  assert_kind_of(Entry::OptionalKeywordParameter, parameters[1])
  assert_kind_of(Entry::BlockParameter, parameters[2])
  parameters = signatures[3]&.parameters #: as !nil
  assert_equal([:by, :to], parameters.map(&:name))
  assert_kind_of(Entry::OptionalKeywordParameter, parameters[0])
  assert_kind_of(Entry::OptionalKeywordParameter, parameters[1])
end

def test_rbs_method_with_optional_parameter

def test_rbs_method_with_optional_parameter
  entries = @index["chomp"] #: as Array[Entry::Method]
  assert_equal(1, entries.length)
  entry = entries.first #: as Entry::Method
  signatures = entry.signatures
  assert_equal(1, signatures.length)
  first_signature = signatures.first #: as Entry::Signature
  # (?::string? separator) -> ::String
  assert_equal(1, first_signature.parameters.length)
  assert_kind_of(Entry::OptionalParameter, first_signature.parameters[0])
  assert_equal(:separator, first_signature.parameters[0]&.name)
end

def test_rbs_method_with_optional_positionals

def test_rbs_method_with_optional_positionals
  entries = @index["polar"] #: as Array[Entry::Method]
  entry = entries.find { |entry| entry.owner&.name == "Complex::<Class:Complex>" } #: as Entry::Method
  # def self.polar: (Numeric, ?Numeric) -> Complex
  parameters = entry.signatures[0]&.parameters #: as Array[Entry::Parameter]
  assert_equal([:arg0, :arg1], parameters.map(&:name))
  assert_kind_of(Entry::RequiredParameter, parameters[0])
  assert_kind_of(Entry::OptionalParameter, parameters[1])
end

def test_rbs_method_with_required_and_optional_parameters

def test_rbs_method_with_required_and_optional_parameters
  entries = @index["gsub"] #: as Array[Entry::Method]
  assert_equal(1, entries.length)
  entry = entries.first #: as Entry::Method
  signatures = entry.signatures
  assert_equal(3, signatures.length)
  # (::Regexp | ::string pattern, ::string | ::hash[::String, ::_ToS] replacement) -> ::String
  # | (::Regexp | ::string pattern) -> ::Enumerator[::String, ::String]
  # | (::Regexp | ::string pattern) { (::String match) -> ::_ToS } -> ::String
  parameters = signatures[0]&.parameters #: as !nil
  assert_equal([:pattern, :replacement], parameters.map(&:name))
  assert_kind_of(Entry::RequiredParameter, parameters[0])
  assert_kind_of(Entry::RequiredParameter, parameters[1])
  parameters = signatures[1]&.parameters #: as !nil
  assert_equal([:pattern], parameters.map(&:name))
  assert_kind_of(Entry::RequiredParameter, parameters[0])
  parameters = signatures[2]&.parameters #: as !nil
  assert_equal([:pattern, :"<anonymous block>"], parameters.map(&:name))
  assert_kind_of(Entry::RequiredParameter, parameters[0])
  assert_kind_of(Entry::BlockParameter, parameters[1])
end

def test_rbs_method_with_required_keywords

def test_rbs_method_with_required_keywords
  # There are no methods in Core that have required keyword arguments,
  # so we test against RBS directly
  rbs = <<~RBS
    class File
      def foo: (a: ::Numeric sz, b: ::Numeric) -> void
    end
  RBS
  signatures = parse_rbs_methods(rbs, "foo")
  parameters = signatures[0].parameters
  assert_equal([:a, :b], parameters.map(&:name))
  assert_kind_of(Entry::KeywordParameter, parameters[0])
  assert_kind_of(Entry::KeywordParameter, parameters[1])
end

def test_rbs_method_with_required_positionals

def test_rbs_method_with_required_positionals
  entries = @index["crypt"] #: as Array[Entry::Method]
  assert_equal(1, entries.length)
  entry = entries.first #: as Entry::Method
  signatures = entry.signatures
  assert_equal(1, signatures.length)
  first_signature = signatures.first #: as Entry::Signature
  # (::string salt_str) -> ::String
  assert_equal(1, first_signature.parameters.length)
  assert_kind_of(Entry::RequiredParameter, first_signature.parameters[0])
  assert_equal(:salt_str, first_signature.parameters[0]&.name)
end

def test_rbs_method_with_rest_keywords

def test_rbs_method_with_rest_keywords
  entries = @index["method_missing"] #: as Array[Entry::Method]
  entry = entries.find { |entry| entry.owner&.name == "BasicObject" } #: as !nil
  signatures = entry.signatures
  assert_equal(1, signatures.length)
  # (Symbol, *untyped, **untyped) ?{ (*untyped, **untyped) -> untyped } -> untyped
  parameters = signatures[0]&.parameters #: as !nil
  assert_equal([:arg0, :"<anonymous splat>", :"<anonymous keyword splat>"], parameters.map(&:name))
  assert_kind_of(Entry::RequiredParameter, parameters[0])
  assert_kind_of(Entry::RestParameter, parameters[1])
  assert_kind_of(Entry::KeywordRestParameter, parameters[2])
end

def test_rbs_method_with_rest_positionals

def test_rbs_method_with_rest_positionals
  entries = @index["count"] #: as Array[Entry::Method]
  entry = entries.find { |entry| entry.owner&.name == "String" } #: as Entry::Method
  parameters = entry.signatures.first&.parameters #: as !nil
  assert_equal(1, entry.signatures.length)
  # (::String::selector selector_0, *::String::selector more_selectors) -> ::Integer
  assert_equal([:selector_0, :more_selectors], parameters.map(&:name))
  assert_kind_of(RubyIndexer::Entry::RequiredParameter, parameters[0])
  assert_kind_of(RubyIndexer::Entry::RestParameter, parameters[1])
end

def test_rbs_method_with_trailing_positionals

def test_rbs_method_with_trailing_positionals
  entries = @index["select"] #: as Array[Entry::Method]
  entry = entries.find { |entry| entry.owner&.name == "IO::<Class:IO>" } #: as !nil
  signatures = entry.signatures
  assert_equal(2, signatures.length)
  # def self.select: [X, Y, Z] (::Array[X & io]? read_array, ?::Array[Y & io]? write_array, ?::Array[Z & io]? error_array) -> [ Array[X], Array[Y], Array[Z] ]
  #   | [X, Y, Z] (::Array[X & io]? read_array, ?::Array[Y & io]? write_array, ?::Array[Z & io]? error_array, Time::_Timeout? timeout) -> [ Array[X], Array[Y], Array[Z] ]?
  parameters = signatures[0]&.parameters #: as !nil
  assert_equal([:read_array, :write_array, :error_array], parameters.map(&:name))
  assert_kind_of(Entry::RequiredParameter, parameters[0])
  assert_kind_of(Entry::OptionalParameter, parameters[1])
  assert_kind_of(Entry::OptionalParameter, parameters[2])
  parameters = signatures[1]&.parameters #: as !nil
  assert_equal([:read_array, :write_array, :error_array, :timeout], parameters.map(&:name))
  assert_kind_of(Entry::RequiredParameter, parameters[0])
  assert_kind_of(Entry::OptionalParameter, parameters[1])
  assert_kind_of(Entry::OptionalParameter, parameters[2])
  assert_kind_of(Entry::OptionalParameter, parameters[3])
end

def test_rbs_method_with_unnamed_required_positionals

def test_rbs_method_with_unnamed_required_positionals
  entries = @index["try_convert"] #: as Array[Entry::Method]
  entry = entries.find { |entry| entry.owner&.name == "Array::<Class:Array>" } #: as Entry::Method
  parameters = entry.signatures[0]&.parameters #: as Array[Entry::Parameter]
  assert_equal([:arg0], parameters.map(&:name))
  assert_kind_of(Entry::RequiredParameter, parameters[0])
end

def test_signature_alias

def test_signature_alias
  # In RBS, an alias means that two methods have the same signature.
  # It does not mean the same thing as a Ruby alias.
  any_entries = @index["any?"] #: as Array[Entry::UnresolvedMethodAlias]
  assert_equal(["Array", "Enumerable", "Hash"], any_entries.map { _1.owner&.name })
  entry = any_entries.find { |entry| entry.owner&.name == "Array" } #: as !nil
  assert_kind_of(RubyIndexer::Entry::UnresolvedMethodAlias, entry)
  assert_equal("any?", entry.name)
  assert_equal("all?", entry.old_name)
  assert_equal("Array", entry.owner&.name)
  assert(entry.file_path&.end_with?("core/array.rbs"))
  refute_empty(entry.comments)
end