# typed: true
# frozen_string_literal: true

require_relative "test_case"

module RubyIndexer
  class EnhancementTest < TestCase
    def test_enhancing_indexing_included_hook
      enhancement_class = Class.new(Enhancement) do
        def on_call_node_enter(owner, node, file_path, code_units_cache)
          return unless owner
          return unless node.name == :extend

          arguments = node.arguments&.arguments
          return unless arguments

          location = Location.from_prism_location(node.location, code_units_cache)

          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"

            @index.register_included_hook(owner.name) 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

            @index.add(Entry::Method.new(
              "new_method",
              file_path,
              location,
              location,
              nil,
              [Entry::Signature.new([Entry::RequiredParameter.new(name: :a)])],
              Entry::Visibility::PUBLIC,
              owner,
            ))
          rescue Prism::ConstantPathNode::DynamicPartsInConstantPathError,
                 Prism::ConstantPathNode::MissingNodesInConstantPathError
            # Do nothing
          end
        end
      end

      @index.register_enhancement(enhancement_class.new(@index))
      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
      enhancement_class = Class.new(Enhancement) do
        def on_call_node_enter(owner, node, file_path, code_units_cache)
          return unless 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)

          location = Location.from_prism_location(association_name.location, code_units_cache)

          @index.add(Entry::Method.new(
            T.must(association_name.value),
            file_path,
            location,
            location,
            nil,
            [],
            Entry::Visibility::PUBLIC,
            owner,
          ))
        end
      end

      @index.register_enhancement(enhancement_class.new(@index))
      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
      enhancement_class = Class.new(Enhancement) do
        def on_call_node_enter(owner, node, file_path, code_units_cache)
          raise "Error"
        end

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

      @index.register_enhancement(enhancement_class.new(@index))

      _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 /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
      enhancement_class = Class.new(Enhancement) do
        def on_call_node_leave(owner, node, file_path, code_units_cache)
          raise "Error"
        end

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

      @index.register_enhancement(enhancement_class.new(@index))

      _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 /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
  end
end