class RubyIndexer::ClassesAndModulesTest

def test_class_with_statements

def test_class_with_statements
  index(<<~RUBY)
    class Foo
      def something; end
    end
  RUBY
  assert_entry("Foo", Entry::Class, "/fake/path/foo.rb:0-0:2-3")
end

def test_colon_colon_class

def test_colon_colon_class
  index(<<~RUBY)
    class ::Foo
    end
  RUBY
  assert_entry("Foo", Entry::Class, "/fake/path/foo.rb:0-0:1-3")
end

def test_colon_colon_class_inside_class

def test_colon_colon_class_inside_class
  index(<<~RUBY)
    class Bar
      class ::Foo
      end
    end
  RUBY
  assert_entry("Bar", Entry::Class, "/fake/path/foo.rb:0-0:3-3")
  assert_entry("Foo", Entry::Class, "/fake/path/foo.rb:1-2:2-5")
end

def test_colon_colon_module

def test_colon_colon_module
  index(<<~RUBY)
    module ::Foo
    end
  RUBY
  assert_entry("Foo", Entry::Module, "/fake/path/foo.rb:0-0:1-3")
end

def test_comments_can_be_attached_to_a_class

def test_comments_can_be_attached_to_a_class
  index(<<~RUBY)
    # This is method comment
    def foo; end
    # This is a Foo comment
    # This is another Foo comment
    class Foo
      # This should not be attached
    end
    # Ignore me
    # This Bar comment has 1 line padding
    class Bar; end
  RUBY
  foo_entry = @index["Foo"] #: as !nil
    .first #: as !nil
  assert_equal("This is a Foo comment\nThis is another Foo comment", foo_entry.comments)
  bar_entry = @index["Bar"] #: as !nil
    .first #: as !nil
  assert_equal("This Bar comment has 1 line padding", bar_entry.comments)
end

def test_comments_can_be_attached_to_a_namespaced_class

def test_comments_can_be_attached_to_a_namespaced_class
  index(<<~RUBY)
    # This is a Foo comment
    # This is another Foo comment
    class Foo
      # This is a Bar comment
      class Bar; end
    end
  RUBY
  foo_entry = @index["Foo"] #: as !nil
    .first #: as !nil
  assert_equal("This is a Foo comment\nThis is another Foo comment", foo_entry.comments)
  bar_entry = @index["Foo::Bar"] #: as !nil
    .first #: as !nil
  assert_equal("This is a Bar comment", bar_entry.comments)
end

def test_comments_can_be_attached_to_a_reopened_class

def test_comments_can_be_attached_to_a_reopened_class
  index(<<~RUBY)
    # This is a Foo comment
    class Foo; end
    # This is another Foo comment
    class Foo; end
  RUBY
  first_foo_entry, second_foo_entry = @index["Foo"] #: as !nil
  assert_equal("This is a Foo comment", first_foo_entry&.comments)
  assert_equal("This is another Foo comment", second_foo_entry&.comments)
end

def test_comments_removes_the_leading_pound_and_space

def test_comments_removes_the_leading_pound_and_space
  index(<<~RUBY)
    # This is a Foo comment
    class Foo; end
    #This is a Bar comment
    class Bar; end
  RUBY
  first_foo_entry = @index["Foo"] #: as !nil
    .first #: as !nil
  assert_equal("This is a Foo comment", first_foo_entry.comments)
  second_foo_entry = @index["Bar"] #: as !nil
    .first #: as !nil
  assert_equal("This is a Bar comment", second_foo_entry.comments)
end

def test_conditional_class

def test_conditional_class
  index(<<~RUBY)
    class Foo
    end if condition
  RUBY
  assert_entry("Foo", Entry::Class, "/fake/path/foo.rb:0-0:1-3")
end

def test_conditional_module

def test_conditional_module
  index(<<~RUBY)
    module Foo
    end if condition
  RUBY
  assert_entry("Foo", Entry::Module, "/fake/path/foo.rb:0-0:1-3")
end

def test_deleting_from_index_based_on_file_path

def test_deleting_from_index_based_on_file_path
  index(<<~RUBY)
    class Foo
    end
  RUBY
  assert_entry("Foo", Entry::Class, "/fake/path/foo.rb:0-0:1-3")
  @index.delete(URI::Generic.from_path(path: "/fake/path/foo.rb"))
  refute_entry("Foo")
  assert_no_indexed_entries
end

def test_dynamic_singleton_class_blocks

def test_dynamic_singleton_class_blocks
  index(<<~RUBY)
    class Foo
      # Some extra comments
      class << bar
      end
    end
  RUBY
  singleton = @index["Foo::<Class:bar>"] #: as !nil
    .first #: as Entry::SingletonClass
  # Even though this is not correct, we consider any dynamic singleton class block as a regular singleton class.
  # That pattern cannot be properly analyzed statically and assuming that it's always a regular singleton simplifies
  # the implementation considerably.
  assert_equal(3, singleton.location.start_line)
  assert_equal("Some extra comments", singleton.comments)
end

def test_dynamically_namespaced_class

def test_dynamically_namespaced_class
  index(<<~RUBY)
    class self::Bar
    end
  RUBY
  assert_entry("self::Bar", Entry::Class, "/fake/path/foo.rb:0-0:1-3")
end

def test_dynamically_namespaced_class_does_not_affect_other_classes

def test_dynamically_namespaced_class_does_not_affect_other_classes
  index(<<~RUBY)
    class Foo
      class self::Bar
      end
      class Bar
      end
    end
  RUBY
  refute_entry("self::Bar")
  assert_entry("Foo", Entry::Class, "/fake/path/foo.rb:0-0:6-3")
  assert_entry("Foo::Bar", Entry::Class, "/fake/path/foo.rb:4-2:5-5")
end

def test_dynamically_namespaced_module

def test_dynamically_namespaced_module
  index(<<~RUBY)
    module self::Bar
    end
  RUBY
  assert_entry("self::Bar", Entry::Module, "/fake/path/foo.rb:0-0:1-3")
end

def test_dynamically_namespaced_module_does_not_affect_other_modules

def test_dynamically_namespaced_module_does_not_affect_other_modules
  index(<<~RUBY)
    module Foo
      class self::Bar
      end
      module Bar
      end
    end
  RUBY
  assert_entry("Foo::self::Bar", Entry::Class, "/fake/path/foo.rb:1-2:2-5")
  assert_entry("Foo", Entry::Module, "/fake/path/foo.rb:0-0:6-3")
  assert_entry("Foo::Bar", Entry::Module, "/fake/path/foo.rb:4-2:5-5")
end

def test_empty_statements_class

def test_empty_statements_class
  index(<<~RUBY)
    class Foo
    end
  RUBY
  assert_entry("Foo", Entry::Class, "/fake/path/foo.rb:0-0:1-3")
end

def test_empty_statements_module

def test_empty_statements_module
  index(<<~RUBY)
    module Foo
    end
  RUBY
  assert_entry("Foo", Entry::Module, "/fake/path/foo.rb:0-0:1-3")
end

def test_indexing_namespaces_inside_nested_top_level_references

def test_indexing_namespaces_inside_nested_top_level_references
  index(<<~RUBY)
    class Baz
      module ::Foo
        class Bar
        end
        class ::Qux
        end
      end
    end
  RUBY
  refute_entry("Baz::Foo")
  refute_entry("Baz::Foo::Bar")
  assert_entry("Baz", Entry::Class, "/fake/path/foo.rb:0-0:8-3")
  assert_entry("Foo", Entry::Module, "/fake/path/foo.rb:1-2:7-5")
  assert_entry("Foo::Bar", Entry::Class, "/fake/path/foo.rb:2-4:3-7")
  assert_entry("Qux", Entry::Class, "/fake/path/foo.rb:5-4:6-7")
end

def test_indexing_namespaces_inside_top_level_references

def test_indexing_namespaces_inside_top_level_references
  index(<<~RUBY)
    module ::Foo
      class Bar
      end
    end
  RUBY
  # We want to explicitly verify that we didn't introduce the leading `::` by accident, but `Index#[]` deletes the
  # prefix when we use `refute_entry`
  entries = @index.instance_variable_get(:@entries)
  refute(entries.key?("::Foo"))
  refute(entries.key?("::Foo::Bar"))
  assert_entry("Foo", Entry::Module, "/fake/path/foo.rb:0-0:3-3")
  assert_entry("Foo::Bar", Entry::Class, "/fake/path/foo.rb:1-2:2-5")
end

def test_indexing_singletons_inside_top_level_references

def test_indexing_singletons_inside_top_level_references
  index(<<~RUBY)
    module ::Foo
      class Bar
        class << self
        end
      end
    end
  RUBY
  # We want to explicitly verify that we didn't introduce the leading `::` by accident, but `Index#[]` deletes the
  # prefix when we use `refute_entry`
  entries = @index.instance_variable_get(:@entries)
  refute(entries.key?("::Foo"))
  refute(entries.key?("::Foo::Bar"))
  refute(entries.key?("::Foo::Bar::<Class:Bar>"))
  assert_entry("Foo", Entry::Module, "/fake/path/foo.rb:0-0:5-3")
  assert_entry("Foo::Bar", Entry::Class, "/fake/path/foo.rb:1-2:4-5")
  assert_entry("Foo::Bar::<Class:Bar>", Entry::SingletonClass, "/fake/path/foo.rb:2-4:3-7")
end

def test_keeping_track_of_extended_modules

def test_keeping_track_of_extended_modules
  index(<<~RUBY)
    class Foo
      # valid syntaxes that we can index
      extend A1
      self.extend A2
      extend A3, A4
      self.extend A5, A6
      # valid syntaxes that we cannot index because of their dynamic nature
      extend some_variable_or_method_call
      self.extend some_variable_or_method_call
      def something
        extend A7 # We should not index this because of this dynamic nature
      end
      # Valid inner class syntax definition with its own modules prepended
      class Qux
        extend Corge
        self.extend Corge
        extend Baz
        extend some_variable_or_method_call
      end
    end
    class ConstantPathReferences
      extend Foo::Bar
      self.extend Foo::Bar2
      extend dynamic::Bar
      extend Foo::
    end
  RUBY
  foo = @index["Foo::<Class:Foo>"] #: as !nil
    .first #: as Entry::Class
  assert_equal(["A1", "A2", "A3", "A4", "A5", "A6"], foo.mixin_operation_module_names)
  qux = @index["Foo::Qux::<Class:Qux>"] #: as !nil
    .first #: as Entry::Class
  assert_equal(["Corge", "Corge", "Baz"], qux.mixin_operation_module_names)
  constant_path_references = @index["ConstantPathReferences::<Class:ConstantPathReferences>"] #: as !nil
    .first #: as Entry::Class
  assert_equal(["Foo::Bar", "Foo::Bar2"], constant_path_references.mixin_operation_module_names)
end

def test_keeping_track_of_included_modules

def test_keeping_track_of_included_modules
  index(<<~RUBY)
    class Foo
      # valid syntaxes that we can index
      include A1
      self.include A2
      include A3, A4
      self.include A5, A6
      # valid syntaxes that we cannot index because of their dynamic nature
      include some_variable_or_method_call
      self.include some_variable_or_method_call
      def something
        include A7 # We should not index this because of this dynamic nature
      end
      # Valid inner class syntax definition with its own modules included
      class Qux
        include Corge
        self.include Corge
        include Baz
        include some_variable_or_method_call
      end
    end
    class ConstantPathReferences
      include Foo::Bar
      self.include Foo::Bar2
      include dynamic::Bar
      include Foo::
    end
  RUBY
  foo = @index["Foo"] #: as !nil
    .first #: as Entry::Class
  assert_equal(["A1", "A2", "A3", "A4", "A5", "A6"], foo.mixin_operation_module_names)
  qux = @index["Foo::Qux"] #: as !nil
    .first #: as Entry::Class
  assert_equal(["Corge", "Corge", "Baz"], qux.mixin_operation_module_names)
  constant_path_references = @index["ConstantPathReferences"] #: as !nil
    .first #: as Entry::Class
  assert_equal(["Foo::Bar", "Foo::Bar2"], constant_path_references.mixin_operation_module_names)
end

def test_keeping_track_of_prepended_modules

def test_keeping_track_of_prepended_modules
  index(<<~RUBY)
    class Foo
      # valid syntaxes that we can index
      prepend A1
      self.prepend A2
      prepend A3, A4
      self.prepend A5, A6
      # valid syntaxes that we cannot index because of their dynamic nature
      prepend some_variable_or_method_call
      self.prepend some_variable_or_method_call
      def something
        prepend A7 # We should not index this because of this dynamic nature
      end
      # Valid inner class syntax definition with its own modules prepended
      class Qux
        prepend Corge
        self.prepend Corge
        prepend Baz
        prepend some_variable_or_method_call
      end
    end
    class ConstantPathReferences
      prepend Foo::Bar
      self.prepend Foo::Bar2
      prepend dynamic::Bar
      prepend Foo::
    end
  RUBY
  foo = @index["Foo"] #: as !nil
    .first #: as Entry::Class
  assert_equal(["A1", "A2", "A3", "A4", "A5", "A6"], foo.mixin_operation_module_names)
  qux = @index["Foo::Qux"] #: as !nil
    .first #: as Entry::Class
  assert_equal(["Corge", "Corge", "Baz"], qux.mixin_operation_module_names)
  constant_path_references = @index["ConstantPathReferences"] #: as !nil
    .first #: as Entry::Class
  assert_equal(["Foo::Bar", "Foo::Bar2"], constant_path_references.mixin_operation_module_names)
end

def test_keeping_track_of_super_classes

def test_keeping_track_of_super_classes
  index(<<~RUBY)
    class Foo < Bar
    end
    class Baz
    end
    module Something
      class Baz
      end
      class Qux < ::Baz
      end
    end
    class FinalThing < Something::Baz
    end
  RUBY
  foo = @index["Foo"] #: as !nil
    .first #: as Entry::Class
  assert_equal("Bar", foo.parent_class)
  baz = @index["Baz"] #: as !nil
    .first #: as Entry::Class
  assert_equal("::Object", baz.parent_class)
  qux = @index["Something::Qux"] #: as !nil
    .first #: as Entry::Class
  assert_equal("::Baz", qux.parent_class)
  final_thing = @index["FinalThing"] #: as !nil
    .first #: as Entry::Class
  assert_equal("Something::Baz", final_thing.parent_class)
end

def test_lazy_comment_fetching_does_not_fail_if_file_gets_deleted

def test_lazy_comment_fetching_does_not_fail_if_file_gets_deleted
  uri = URI::Generic.from_path(
    load_path_entry: "#{Dir.pwd}/lib",
    path: "#{Dir.pwd}/lib/ruby_lsp/does_not_exist.rb",
  )
  @index.index_single(uri, <<~RUBY, collect_comments: false)
    class Foo
    end
  RUBY
  entry = @index["Foo"]&.first #: as !nil
  assert_empty(entry.comments)
end

def test_lazy_comment_fetching_uses_correct_line_breaks_for_rendering

def test_lazy_comment_fetching_uses_correct_line_breaks_for_rendering
  uri = URI::Generic.from_path(
    load_path_entry: "#{Dir.pwd}/lib",
    path: "#{Dir.pwd}/lib/ruby_lsp/node_context.rb",
  )
  @index.index_file(uri, collect_comments: false)
  entry = @index["RubyLsp::NodeContext"] #: as !nil
    .first #: as !nil
  assert_equal(<<~COMMENTS.chomp, entry.comments)
    This class allows listeners to access contextual information about a node in the AST, such as its parent,
    its namespace nesting, and the surrounding CallNode (e.g. a method call).
  COMMENTS
end

def test_lazy_comments_with_no_spaces_are_properly_attributed

def test_lazy_comments_with_no_spaces_are_properly_attributed
  path = File.join(Dir.pwd, "lib", "foo.rb")
  source = <<~RUBY
    require "whatever"
    # These comments belong to the declaration below
    # They have to be associated with it
    class Foo
    end
  RUBY
  File.write(path, source)
  @index.index_single(URI::Generic.from_path(path: path), source, collect_comments: false)
  entry = @index["Foo"]&.first #: as !nil
  begin
    assert_equal(<<~COMMENTS.chomp, entry.comments)
      These comments belong to the declaration below
      They have to be associated with it
    COMMENTS
  ensure
    FileUtils.rm(path)
  end
end

def test_lazy_comments_with_spaces_are_properly_attributed

def test_lazy_comments_with_spaces_are_properly_attributed
  path = File.join(Dir.pwd, "lib", "foo.rb")
  source =  <<~RUBY
    require "whatever"
    # These comments belong to the declaration below
    # They have to be associated with it
    class Foo
    end
  RUBY
  File.write(path, source)
  @index.index_single(URI::Generic.from_path(path: path), source, collect_comments: false)
  entry = @index["Foo"]&.first #: as !nil
  begin
    assert_equal(<<~COMMENTS.chomp, entry.comments)
      These comments belong to the declaration below
      They have to be associated with it
    COMMENTS
  ensure
    FileUtils.rm(path)
  end
end

def test_lazy_comments_with_two_extra_spaces_are_properly_ignored

def test_lazy_comments_with_two_extra_spaces_are_properly_ignored
  path = File.join(Dir.pwd, "lib", "foo.rb")
  source = <<~RUBY
    require "whatever"
    # These comments don't belong to the declaration below
    # They will not be associated with it
    class Foo
    end
  RUBY
  File.write(path, source)
  @index.index_single(URI::Generic.from_path(path: path), source, collect_comments: false)
  entry = @index["Foo"]&.first #: as !nil
  begin
    assert_empty(entry.comments)
  ensure
    FileUtils.rm(path)
  end
end

def test_module_with_statements

def test_module_with_statements
  index(<<~RUBY)
    module Foo
      def something; end
    end
  RUBY
  assert_entry("Foo", Entry::Module, "/fake/path/foo.rb:0-0:2-3")
end

def test_name_location_points_to_constant_path_location

def test_name_location_points_to_constant_path_location
  index(<<~RUBY)
    class Foo
      def foo; end
    end
    module Bar
      def bar; end
    end
  RUBY
  foo = @index["Foo"] #: as !nil
    .first #: as Entry::Class
  refute_equal(foo.location, foo.name_location)
  name_location = foo.name_location
  assert_equal(1, name_location.start_line)
  assert_equal(1, name_location.end_line)
  assert_equal(6, name_location.start_column)
  assert_equal(9, name_location.end_column)
  bar = @index["Bar"] #: as !nil
    .first #: as Entry::Module
  refute_equal(bar.location, bar.name_location)
  name_location = bar.name_location
  assert_equal(5, name_location.start_line)
  assert_equal(5, name_location.end_line)
  assert_equal(7, name_location.start_column)
  assert_equal(10, name_location.end_column)
end

def test_namespaced_class

def test_namespaced_class
  index(<<~RUBY)
    class Foo::Bar
    end
  RUBY
  assert_entry("Foo::Bar", Entry::Class, "/fake/path/foo.rb:0-0:1-3")
end

def test_namespaced_module

def test_namespaced_module
  index(<<~RUBY)
    module Foo::Bar
    end
  RUBY
  assert_entry("Foo::Bar", Entry::Module, "/fake/path/foo.rb:0-0:1-3")
end

def test_namespaces_inside_singleton_blocks

def test_namespaces_inside_singleton_blocks
  index(<<~RUBY)
    class Foo
      class << self
        class Bar
        end
      end
    end
  RUBY
  assert_entry("Foo::<Class:Foo>::Bar", Entry::Class, "/fake/path/foo.rb:2-4:3-7")
end

def test_nested_modules_and_classes

def test_nested_modules_and_classes
  index(<<~RUBY)
    module Foo
      class Bar
      end
      module Baz
        class Qux
          class Something
          end
        end
      end
    end
  RUBY
  assert_entry("Foo", Entry::Module, "/fake/path/foo.rb:0-0:10-3")
  assert_entry("Foo::Bar", Entry::Class, "/fake/path/foo.rb:1-2:2-5")
  assert_entry("Foo::Baz", Entry::Module, "/fake/path/foo.rb:4-2:9-5")
  assert_entry("Foo::Baz::Qux", Entry::Class, "/fake/path/foo.rb:5-4:8-7")
  assert_entry("Foo::Baz::Qux::Something", Entry::Class, "/fake/path/foo.rb:6-6:7-9")
end

def test_nested_modules_and_classes_with_multibyte_characters

def test_nested_modules_and_classes_with_multibyte_characters
  index(<<~RUBY)
    module A動物
      class Bねこ; end
    end
  RUBY
  assert_entry("A動物", Entry::Module, "/fake/path/foo.rb:0-0:2-3")
  assert_entry("A動物::Bねこ", Entry::Class, "/fake/path/foo.rb:1-2:1-16")
end

def test_private_class_and_module_indexing

def test_private_class_and_module_indexing
  index(<<~RUBY)
    class A
      class B; end
      private_constant(:B)
      module C; end
      private_constant("C")
      class D; end
    end
  RUBY
  b_const = @index["A::B"] #: as !nil
    .first
  assert_predicate(b_const, :private?)
  c_const = @index["A::C"] #: as !nil
    .first
  assert_predicate(c_const, :private?)
  d_const = @index["A::D"] #: as !nil
    .first
  assert_predicate(d_const, :public?)
end

def test_singleton_inside_compact_namespace

def test_singleton_inside_compact_namespace
  index(<<~RUBY)
    module Foo::Bar
      class << self
        def baz; end
      end
    end
  RUBY
  # Verify we didn't index the incorrect name
  assert_nil(@index["Foo::Bar::<Class:Foo::Bar>"])
  # Verify we indexed the correct name
  assert_entry("Foo::Bar::<Class:Bar>", Entry::SingletonClass, "/fake/path/foo.rb:1-2:3-5")
  method = @index["baz"]&.first #: as Entry::Method
  assert_equal("Foo::Bar::<Class:Bar>", method.owner&.name)
end

def test_skips_comments_containing_invalid_encodings

def test_skips_comments_containing_invalid_encodings
  index(<<~RUBY)
    # comment \xBA
    class Foo
    end
  RUBY
  assert(@index["Foo"]&.first)
end

def test_tracking_singleton_classes

def test_tracking_singleton_classes
  index(<<~RUBY)
    class Foo; end
    class Foo
      # Some extra comments
      class << self
      end
    end
  RUBY
  foo = @index["Foo::<Class:Foo>"] #: as !nil
    .first #: as Entry::SingletonClass
  assert_equal(4, foo.location.start_line)
  assert_equal("Some extra comments", foo.comments)
end