# typed: true
# frozen_string_literal: true

require_relative "test_case"

module RubyIndexer
  class EnhancementTest < TestCase
    def teardown
      super
      Enhancement.clear
    end

    def test_enhancing_indexing_included_hook
      Class.new(Enhancement) do
        def on_call_node_enter(call_node) # rubocop:disable RubyLsp/UseRegisterWithHandlerMethod
          owner = @listener.current_owner
          return unless owner
          return unless call_node.name == :extend

          arguments = call_node.arguments&.arguments
          return unless arguments

          arguments.each do |node|
            next unless node.is_a?(Prism::ConstantReadNode) || node.is_a?(Prism::ConstantPathNode)

            module_name = node.full_name
            next unless module_name == "ActiveSupport::Concern"

            @listener.register_included_hook do |index, base|
              class_methods_name = "#{owner.name}::ClassMethods"

              if index.indexed?(class_methods_name)
                singleton = index.existing_or_new_singleton_class(base.name)
                singleton.mixin_operations << Entry::Include.new(class_methods_name)
              end
            end

            @listener.add_method(
              "new_method",
              call_node.location,
              [Entry::Signature.new([Entry::RequiredParameter.new(name: :a)])],
            )
          rescue Prism::ConstantPathNode::DynamicPartsInConstantPathError,
                 Prism::ConstantPathNode::MissingNodesInConstantPathError
            # Do nothing
          end
        end
      end

      index(<<~RUBY)
        module ActiveSupport
          module Concern
            def self.extended(base)
              base.class_eval("def new_method(a); end")
            end
          end
        end

        module ActiveRecord
          module Associations
            extend ActiveSupport::Concern

            module ClassMethods
              def belongs_to(something); end
            end
          end

          class Base
            include Associations
          end
        end

        class User < ActiveRecord::Base
        end
      RUBY

      assert_equal(
        [
          "User::<Class:User>",
          "ActiveRecord::Base::<Class:Base>",
          "ActiveRecord::Associations::ClassMethods",
          "Object::<Class:Object>",
          "BasicObject::<Class:BasicObject>",
          "Class",
          "Module",
          "Object",
          "Kernel",
          "BasicObject",
        ],
        @index.linearized_ancestors_of("User::<Class:User>"),
      )

      assert_entry("new_method", Entry::Method, "/fake/path/foo.rb:10-4:10-33")
    end

    def test_enhancing_indexing_configuration_dsl
      Class.new(Enhancement) do
        def on_call_node_enter(node) # rubocop:disable RubyLsp/UseRegisterWithHandlerMethod
          return unless @listener.current_owner

          name = node.name
          return unless name == :has_many

          arguments = node.arguments&.arguments
          return unless arguments

          association_name = arguments.first
          return unless association_name.is_a?(Prism::SymbolNode)

          @listener.add_method(
            T.must(association_name.value),
            association_name.location,
            [],
          )
        end
      end

      index(<<~RUBY)
        module ActiveSupport
          module Concern
            def self.extended(base)
              base.class_eval("def new_method(a); end")
            end
          end
        end

        module ActiveRecord
          module Associations
            extend ActiveSupport::Concern

            module ClassMethods
              def belongs_to(something); end
            end
          end

          class Base
            include Associations
          end
        end

        class User < ActiveRecord::Base
          has_many :posts
        end
      RUBY

      assert_entry("posts", Entry::Method, "/fake/path/foo.rb:23-11:23-17")
    end

    def test_error_handling_in_on_call_node_enter_enhancement
      Class.new(Enhancement) do
        def on_call_node_enter(node) # rubocop:disable RubyLsp/UseRegisterWithHandlerMethod
          raise "Error"
        end

        class << self
          def name
            "TestEnhancement"
          end
        end
      end

      _stdout, stderr = capture_io do
        index(<<~RUBY)
          module ActiveSupport
            module Concern
              def self.extended(base)
                base.class_eval("def new_method(a); end")
              end
            end
          end
        RUBY
      end

      assert_match(
        %r{Indexing error in file:///fake/path/foo\.rb with 'TestEnhancement' on call node enter enhancement},
        stderr,
      )
      # The module should still be indexed
      assert_entry("ActiveSupport::Concern", Entry::Module, "/fake/path/foo.rb:1-2:5-5")
    end

    def test_error_handling_in_on_call_node_leave_enhancement
      Class.new(Enhancement) do
        def on_call_node_leave(node) # rubocop:disable RubyLsp/UseRegisterWithHandlerMethod
          raise "Error"
        end

        class << self
          def name
            "TestEnhancement"
          end
        end
      end

      _stdout, stderr = capture_io do
        index(<<~RUBY)
          module ActiveSupport
            module Concern
              def self.extended(base)
                base.class_eval("def new_method(a); end")
              end
            end
          end
        RUBY
      end

      assert_match(
        %r{Indexing error in file:///fake/path/foo\.rb with 'TestEnhancement' on call node leave enhancement},
        stderr,
      )
      # The module should still be indexed
      assert_entry("ActiveSupport::Concern", Entry::Module, "/fake/path/foo.rb:1-2:5-5")
    end

    def test_advancing_namespace_stack_from_enhancement
      Class.new(Enhancement) do
        def on_call_node_enter(call_node) # rubocop:disable RubyLsp/UseRegisterWithHandlerMethod
          owner = @listener.current_owner
          return unless owner

          case call_node.name
          when :class_methods
            @listener.add_module("ClassMethods", call_node.location, call_node.location)
          when :extend
            arguments = call_node.arguments&.arguments
            return unless arguments

            arguments.each do |node|
              next unless node.is_a?(Prism::ConstantReadNode) || node.is_a?(Prism::ConstantPathNode)

              module_name = node.full_name
              next unless module_name == "ActiveSupport::Concern"

              @listener.register_included_hook do |index, base|
                class_methods_name = "#{owner.name}::ClassMethods"

                if index.indexed?(class_methods_name)
                  singleton = index.existing_or_new_singleton_class(base.name)
                  singleton.mixin_operations << Entry::Include.new(class_methods_name)
                end
              end
            end
          end
        end

        def on_call_node_leave(call_node) # rubocop:disable RubyLsp/UseRegisterWithHandlerMethod
          return unless call_node.name == :class_methods

          @listener.pop_namespace_stack
        end
      end

      index(<<~RUBY)
        module ActiveSupport
          module Concern
          end
        end

        module MyConcern
          extend ActiveSupport::Concern

          class_methods do
            def foo; end
          end
        end

        class User
          include MyConcern
        end
      RUBY

      assert_equal(
        [
          "User::<Class:User>",
          "MyConcern::ClassMethods",
          "Object::<Class:Object>",
          "BasicObject::<Class:BasicObject>",
          "Class",
          "Module",
          "Object",
          "Kernel",
          "BasicObject",
        ],
        @index.linearized_ancestors_of("User::<Class:User>"),
      )

      refute_nil(@index.resolve_method("foo", "User::<Class:User>"))
    end

    def test_creating_anonymous_classes_from_enhancement
      Class.new(Enhancement) do
        def on_call_node_enter(call_node) # rubocop:disable RubyLsp/UseRegisterWithHandlerMethod
          case call_node.name
          when :context
            arguments = call_node.arguments&.arguments
            first_argument = arguments&.first
            return unless first_argument.is_a?(Prism::StringNode)

            @listener.add_class(
              "<RSpec:#{first_argument.content}>",
              call_node.location,
              first_argument.location,
            )
          when :subject
            @listener.add_method("subject", call_node.location, [])
          end
        end

        def on_call_node_leave(call_node) # rubocop:disable RubyLsp/UseRegisterWithHandlerMethod
          return unless call_node.name == :context

          @listener.pop_namespace_stack
        end
      end

      index(<<~RUBY)
        context "does something" do
          subject { call_whatever }
        end
      RUBY

      refute_nil(@index.resolve_method("subject", "<RSpec:does something>"))
    end
  end
end
