# typed: true
# frozen_string_literal: true

require_relative "test_case"

module RubyIndexer
  class ClassesAndModulesTest < TestCase
    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_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_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
      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
      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_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_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_doesnt_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_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_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_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_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_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_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_doesnt_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_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_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(IndexablePath.new(nil, "/fake/path/foo.rb"))
      refute_entry("Foo")

      assert_no_indexed_entries
    end

    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"].first
      assert_equal("This is a Foo comment\nThis is another Foo comment", foo_entry.comments.join("\n"))

      bar_entry = @index["Bar"].first
      assert_equal("This Bar comment has 1 line padding", bar_entry.comments.join("\n"))
    end

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

    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"].first
      assert_equal("This is a Foo comment\nThis is another Foo comment", foo_entry.comments.join("\n"))

      bar_entry = @index["Foo::Bar"].first
      assert_equal("This is a Bar comment", bar_entry.comments.join("\n"))
    end

    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 = @index["Foo"][0]
      assert_equal("This is a Foo comment", first_foo_entry.comments.join("\n"))

      second_foo_entry = @index["Foo"][1]
      assert_equal("This is another Foo comment", second_foo_entry.comments.join("\n"))
    end

    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"][0]
      assert_equal("This is a Foo comment", first_foo_entry.comments.join("\n"))

      second_foo_entry = @index["Bar"][0]
      assert_equal("This is a Bar comment", second_foo_entry.comments.join("\n"))
    end

    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"].first
      assert_equal(Entry::Visibility::PRIVATE, b_const.visibility)

      c_const = @index["A::C"].first
      assert_equal(Entry::Visibility::PRIVATE, c_const.visibility)

      d_const = @index["A::D"].first
      assert_equal(Entry::Visibility::PUBLIC, d_const.visibility)
    end

    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 = T.must(@index["Foo"].first)
      assert_equal("Bar", foo.parent_class)

      baz = T.must(@index["Baz"].first)
      assert_equal("::Object", baz.parent_class)

      qux = T.must(@index["Something::Qux"].first)
      assert_equal("::Baz", qux.parent_class)

      final_thing = T.must(@index["FinalThing"].first)
      assert_equal("Something::Baz", final_thing.parent_class)
    end

    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 = T.must(@index["Foo"][0])
      assert_equal(["A1", "A2", "A3", "A4", "A5", "A6"], foo.mixin_operation_module_names)

      qux = T.must(@index["Foo::Qux"][0])
      assert_equal(["Corge", "Corge", "Baz"], qux.mixin_operation_module_names)

      constant_path_references = T.must(@index["ConstantPathReferences"][0])
      assert_equal(["Foo::Bar", "Foo::Bar2"], constant_path_references.mixin_operation_module_names)
    end

    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 = T.must(@index["Foo"][0])
      assert_equal(["A1", "A2", "A3", "A4", "A5", "A6"], foo.mixin_operation_module_names)

      qux = T.must(@index["Foo::Qux"][0])
      assert_equal(["Corge", "Corge", "Baz"], qux.mixin_operation_module_names)

      constant_path_references = T.must(@index["ConstantPathReferences"][0])
      assert_equal(["Foo::Bar", "Foo::Bar2"], constant_path_references.mixin_operation_module_names)
    end

    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 = T.must(@index["Foo::<Class:Foo>"][0])
      assert_equal(["A1", "A2", "A3", "A4", "A5", "A6"], foo.mixin_operation_module_names)

      qux = T.must(@index["Foo::Qux::<Class:Qux>"][0])
      assert_equal(["Corge", "Corge", "Baz"], qux.mixin_operation_module_names)

      constant_path_references = T.must(@index["ConstantPathReferences::<Class:ConstantPathReferences>"][0])
      assert_equal(["Foo::Bar", "Foo::Bar2"], constant_path_references.mixin_operation_module_names)
    end

    def test_tracking_singleton_classes
      index(<<~RUBY)
        class Foo; end
        class Foo
          # Some extra comments
          class << self
          end
        end
      RUBY

      foo = T.must(@index["Foo::<Class:Foo>"].first)
      assert_equal(4, foo.location.start_line)
      assert_equal("Some extra comments", foo.comments.join("\n"))
    end

    def test_dynamic_singleton_class_blocks
      index(<<~RUBY)
        class Foo
          # Some extra comments
          class << bar
          end
        end
      RUBY

      singleton = T.must(@index["Foo::<Class:bar>"].first)

      # 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.join("\n"))
    end

    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_name_location_points_to_constant_path_location
      index(<<~RUBY)
        class Foo
          def foo; end
        end

        module Bar
          def bar; end
        end
      RUBY

      foo = T.must(@index["Foo"].first)
      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 = T.must(@index["Bar"].first)
      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
  end
end