from __future__ import annotations

from collections import defaultdict
from collections.abc import ItemsView, KeysView
from pathlib import Path
from typing import TYPE_CHECKING
from unittest.mock import MagicMock

import pytest

from codebase_rag import constants as cs
from codebase_rag.graph_updater import GraphUpdater
from codebase_rag.parser_loader import load_parsers
from codebase_rag.parsers.call_resolver import CallResolver
from codebase_rag.parsers.import_processor import ImportProcessor
from codebase_rag.parsers.type_inference import TypeInferenceEngine
from codebase_rag.types_defs import NodeType, QualifiedName

if TYPE_CHECKING:
    from codebase_rag.parsers.call_processor import CallProcessor


class MockFunctionRegistry:
    def __init__(self) -> None:
        self._data: dict[QualifiedName, NodeType] = {}
        self._suffix_index: dict[str, list[QualifiedName]] = defaultdict(list)
        self._properties: set[QualifiedName] = set()
        self._property_names: set[str] = set()
        self._abstracts: set[QualifiedName] = set()

    def __contains__(self, qn: QualifiedName) -> bool:
        return qn in self._data

    def __getitem__(self, qn: QualifiedName) -> NodeType:
        return self._data[qn]

    def __setitem__(self, qn: QualifiedName, func_type: NodeType) -> None:
        self._data[qn] = func_type
        parts = qn.split(cs.SEPARATOR_DOT)
        for i in range(len(parts)):
            suffix = cs.SEPARATOR_DOT.join(parts[i:])
            if qn not in self._suffix_index[suffix]:
                self._suffix_index[suffix].append(qn)

    def get(
        self, qn: QualifiedName, default: NodeType | None = None
    ) -> NodeType | None:
        return self._data.get(qn, default)

    def keys(self) -> KeysView[QualifiedName]:
        return self._data.keys()

    def items(self) -> ItemsView[QualifiedName, NodeType]:
        return self._data.items()

    def find_with_prefix(self, prefix: str) -> list[tuple[QualifiedName, NodeType]]:
        return [(k, v) for k, v in self._data.items() if k.startswith(prefix)]

    def find_ending_with(self, suffix: str) -> list[QualifiedName]:
        return self._suffix_index.get(suffix, [])

    def register_unique_qn(self, natural_qn: QualifiedName, start_line: int) -> str:
        return natural_qn

    def variants(self, qn: QualifiedName) -> list[QualifiedName]:
        return [qn]

    def mark_property(self, qn: QualifiedName) -> None:
        self._properties.add(qn)
        self._property_names.add(qn.rsplit(cs.SEPARATOR_DOT, 1)[-1])

    def is_property(self, qn: QualifiedName) -> bool:
        return qn in self._properties

    def property_names(self) -> set[str]:
        return self._property_names

    def mark_abstract(self, qn: QualifiedName) -> None:
        self._abstracts.add(qn)

    def is_abstract(self, qn: QualifiedName) -> bool:
        return qn in self._abstracts


@pytest.fixture
def mock_function_registry() -> MockFunctionRegistry:
    return MockFunctionRegistry()


@pytest.fixture
def mock_import_processor(temp_repo: Path) -> ImportProcessor:
    processor = ImportProcessor(repo_path=temp_repo, project_name="test_project")
    return processor


@pytest.fixture
def mock_ast_cache() -> MagicMock:
    cache = MagicMock()
    cache.__contains__ = MagicMock(return_value=False)
    cache.__getitem__ = MagicMock(return_value=(None, None))
    cache.__setitem__ = MagicMock()
    return cache


@pytest.fixture
def mock_type_inference(
    mock_import_processor: ImportProcessor,
    mock_function_registry: MockFunctionRegistry,
    mock_ast_cache: MagicMock,
    temp_repo: Path,
) -> TypeInferenceEngine:
    return TypeInferenceEngine(
        import_processor=mock_import_processor,
        function_registry=mock_function_registry,
        repo_path=temp_repo,
        project_name="test_project",
        ast_cache=mock_ast_cache,
        queries={},
        module_qn_to_file_path={},
        class_inheritance={},
        simple_name_lookup=defaultdict(set),
    )


@pytest.fixture
def call_resolver(
    mock_function_registry: MockFunctionRegistry,
    mock_import_processor: ImportProcessor,
    mock_type_inference: TypeInferenceEngine,
) -> CallResolver:
    return CallResolver(
        function_registry=mock_function_registry,
        import_processor=mock_import_processor,
        type_inference=mock_type_inference,
        class_inheritance={},
    )


@pytest.fixture
def call_processor(temp_repo: Path, mock_ingestor: MagicMock) -> CallProcessor:
    parsers, queries = load_parsers()
    updater = GraphUpdater(
        ingestor=mock_ingestor,
        repo_path=temp_repo,
        parsers=parsers,
        queries=queries,
    )
    return updater.factory.call_processor


class TestTryResolveIife:
    def test_resolves_iife_function_prefix(self, call_resolver: CallResolver) -> None:
        call_resolver.function_registry["proj.module.iife_func_1_5"] = NodeType.FUNCTION

        result = call_resolver._try_resolve_iife("iife_func_1_5", "proj.module")
        assert result is not None
        assert result[1] == "proj.module.iife_func_1_5"

    def test_resolves_iife_arrow_prefix(self, call_resolver: CallResolver) -> None:
        call_resolver.function_registry["proj.module.iife_arrow_2_10"] = (
            NodeType.FUNCTION
        )

        result = call_resolver._try_resolve_iife("iife_arrow_2_10", "proj.module")
        assert result is not None
        assert result[1] == "proj.module.iife_arrow_2_10"

    def test_returns_none_for_non_iife(self, call_resolver: CallResolver) -> None:
        call_resolver.function_registry["proj.module.regular_func"] = NodeType.FUNCTION

        result = call_resolver._try_resolve_iife("regular_func", "proj.module")
        assert result is None

    def test_returns_none_for_empty_call_name(
        self, call_resolver: CallResolver
    ) -> None:
        result = call_resolver._try_resolve_iife("", "proj.module")
        assert result is None

    def test_returns_none_for_unregistered_iife(
        self, call_resolver: CallResolver
    ) -> None:
        result = call_resolver._try_resolve_iife("iife_func_99_99", "proj.module")
        assert result is None


class TestIsSuperCall:
    def test_super_keyword_alone(self, call_resolver: CallResolver) -> None:
        assert call_resolver._is_super_call(cs.KEYWORD_SUPER) is True

    def test_super_dot_method(self, call_resolver: CallResolver) -> None:
        assert call_resolver._is_super_call(f"{cs.KEYWORD_SUPER}.method") is True

    def test_super_parens_method(self, call_resolver: CallResolver) -> None:
        assert call_resolver._is_super_call(f"{cs.KEYWORD_SUPER}().method") is True

    def test_regular_call_not_super(self, call_resolver: CallResolver) -> None:
        assert call_resolver._is_super_call("regular_call") is False
        assert call_resolver._is_super_call("self.method") is False
        assert call_resolver._is_super_call("superclass.method") is False


class TestHasSeparator:
    def test_dot_separator(self, call_resolver: CallResolver) -> None:
        assert call_resolver._has_separator("obj.method") is True

    def test_double_colon_separator(self, call_resolver: CallResolver) -> None:
        assert call_resolver._has_separator("Struct::method") is True

    def test_single_colon_separator(self, call_resolver: CallResolver) -> None:
        assert call_resolver._has_separator("module:method") is True

    def test_no_separator(self, call_resolver: CallResolver) -> None:
        assert call_resolver._has_separator("simple_func") is False


class TestGetSeparator:
    def test_returns_double_colon_first(self, call_resolver: CallResolver) -> None:
        assert (
            call_resolver._get_separator("Struct::method") == cs.SEPARATOR_DOUBLE_COLON
        )

    def test_returns_colon_over_dot(self, call_resolver: CallResolver) -> None:
        assert call_resolver._get_separator("module:method") == cs.SEPARATOR_COLON

    def test_returns_dot_as_default(self, call_resolver: CallResolver) -> None:
        assert call_resolver._get_separator("obj.method") == cs.SEPARATOR_DOT

    def test_returns_dot_for_no_separator(self, call_resolver: CallResolver) -> None:
        assert call_resolver._get_separator("simple") == cs.SEPARATOR_DOT


class TestTryResolveDirectImport:
    def test_resolves_direct_import(self, call_resolver: CallResolver) -> None:
        call_resolver.function_registry["external.module.func"] = NodeType.FUNCTION
        import_map = {"func": "external.module.func"}

        result = call_resolver._try_resolve_direct_import("func", import_map)
        assert result is not None
        assert result[1] == "external.module.func"

    def test_returns_none_for_unimported(self, call_resolver: CallResolver) -> None:
        import_map = {"other_func": "external.module.other_func"}

        result = call_resolver._try_resolve_direct_import("func", import_map)
        assert result is None

    def test_returns_none_for_unregistered_import(
        self, call_resolver: CallResolver
    ) -> None:
        import_map = {"func": "external.module.func"}

        result = call_resolver._try_resolve_direct_import("func", import_map)
        assert result is None


class TestTryResolveSameModule:
    def test_resolves_same_module_function(self, call_resolver: CallResolver) -> None:
        call_resolver.function_registry["proj.module.my_func"] = NodeType.FUNCTION

        result = call_resolver._try_resolve_same_module("my_func", "proj.module")
        assert result is not None
        assert result[1] == "proj.module.my_func"

    def test_returns_none_for_unknown_function(
        self, call_resolver: CallResolver
    ) -> None:
        result = call_resolver._try_resolve_same_module("unknown_func", "proj.module")
        assert result is None


class TestTryResolveViaTrie:
    def test_resolves_via_trie_match(self, call_resolver: CallResolver) -> None:
        call_resolver.function_registry["proj.utils.helper"] = NodeType.FUNCTION
        call_resolver.function_registry["proj.other.helper"] = NodeType.FUNCTION

        result = call_resolver._try_resolve_via_trie("helper", "proj.utils")
        assert result is not None
        assert result[1] == "proj.utils.helper"

    def test_returns_none_for_no_match(self, call_resolver: CallResolver) -> None:
        result = call_resolver._try_resolve_via_trie("nonexistent", "proj.module")
        assert result is None

    def test_handles_qualified_call_name(self, call_resolver: CallResolver) -> None:
        call_resolver.function_registry["proj.module.Class.method"] = NodeType.METHOD

        result = call_resolver._try_resolve_via_trie("Class.method", "proj.module")
        assert result is not None
        assert result[1] == "proj.module.Class.method"


class TestTryResolveWildcardImports:
    def test_resolves_wildcard_import(self, call_resolver: CallResolver) -> None:
        call_resolver.function_registry["external.utils.helper"] = NodeType.FUNCTION
        import_map = {"*utils": "external.utils"}

        result = call_resolver._try_resolve_wildcard_imports("helper", import_map)
        assert result is not None
        assert result[1] == "external.utils.helper"

    def test_returns_none_for_no_wildcard_match(
        self, call_resolver: CallResolver
    ) -> None:
        import_map = {"*utils": "external.utils"}

        result = call_resolver._try_resolve_wildcard_imports("helper", import_map)
        assert result is None

    def test_skips_non_wildcard_imports(self, call_resolver: CallResolver) -> None:
        call_resolver.function_registry["external.utils.helper"] = NodeType.FUNCTION
        import_map = {"utils": "external.utils"}

        result = call_resolver._try_resolve_wildcard_imports("helper", import_map)
        assert result is None


class TestTryWildcardQns:
    def test_dot_separated_qn(self, call_resolver: CallResolver) -> None:
        call_resolver.function_registry["external.utils.helper"] = NodeType.FUNCTION

        result = call_resolver._try_wildcard_qns("helper", "external.utils")
        assert result is not None
        assert result[1] == "external.utils.helper"

    def test_double_colon_separated_qn(self, call_resolver: CallResolver) -> None:
        call_resolver.function_registry["crate::module::func"] = NodeType.FUNCTION

        result = call_resolver._try_wildcard_qns("func", "crate::module")
        assert result is not None

    def test_returns_none_for_no_match(self, call_resolver: CallResolver) -> None:
        result = call_resolver._try_wildcard_qns("unknown", "external.utils")
        assert result is None


class TestTryResolveViaImports:
    def test_resolves_direct_import(self, call_resolver: CallResolver) -> None:
        call_resolver.function_registry["external.func"] = NodeType.FUNCTION
        call_resolver.import_processor.import_mapping["proj.module"] = {
            "func": "external.func"
        }

        result = call_resolver._try_resolve_via_imports("func", "proj.module", None)
        assert result is not None
        assert result[1] == "external.func"

    def test_returns_none_for_unknown_module(self, call_resolver: CallResolver) -> None:
        result = call_resolver._try_resolve_via_imports("func", "unknown.module", None)
        assert result is None


class TestResolveTwoPartCall:
    def test_resolves_imported_class_method(self, call_resolver: CallResolver) -> None:
        call_resolver.function_registry["proj.models.MyClass.method"] = NodeType.METHOD
        import_map = {"MyClass": "proj.models.MyClass"}

        result = call_resolver._resolve_two_part_call(
            ["MyClass", "method"],
            "MyClass.method",
            cs.SEPARATOR_DOT,
            import_map,
            "proj.views",
            None,
        )
        assert result is not None
        assert result[1] == "proj.models.MyClass.method"

    def test_resolves_local_variable_method(self, call_resolver: CallResolver) -> None:
        call_resolver.function_registry["proj.models.User.save"] = NodeType.METHOD
        import_map = {"User": "proj.models.User"}
        local_var_types = {"user": "User"}

        result = call_resolver._resolve_two_part_call(
            ["user", "save"],
            "user.save",
            cs.SEPARATOR_DOT,
            import_map,
            "proj.views",
            local_var_types,
        )
        assert result is not None
        assert result[1] == "proj.models.User.save"

    def test_resolves_module_method_fallback(self, call_resolver: CallResolver) -> None:
        call_resolver.function_registry["proj.views.method"] = NodeType.FUNCTION
        import_map: dict[str, str] = {}

        result = call_resolver._resolve_two_part_call(
            ["obj", "method"],
            "obj.method",
            cs.SEPARATOR_DOT,
            import_map,
            "proj.views",
            None,
        )
        assert result is not None
        assert result[1] == "proj.views.method"


class TestTryResolveViaLocalType:
    def test_resolves_via_local_type(self, call_resolver: CallResolver) -> None:
        call_resolver.function_registry["proj.models.User.save"] = NodeType.METHOD
        import_map = {"User": "proj.models.User"}
        local_var_types = {"user": "User"}

        result = call_resolver._try_resolve_via_local_type(
            "user",
            "save",
            cs.SEPARATOR_DOT,
            "user.save",
            import_map,
            "proj.views",
            local_var_types,
        )
        assert result is not None
        assert result[1] == "proj.models.User.save"

    def test_returns_none_for_no_local_types(self, call_resolver: CallResolver) -> None:
        result = call_resolver._try_resolve_via_local_type(
            "user",
            "save",
            cs.SEPARATOR_DOT,
            "user.save",
            {},
            "proj.views",
            None,
        )
        assert result is None

    def test_returns_none_for_unknown_object(self, call_resolver: CallResolver) -> None:
        local_var_types = {"other": "User"}

        result = call_resolver._try_resolve_via_local_type(
            "user",
            "save",
            cs.SEPARATOR_DOT,
            "user.save",
            {},
            "proj.views",
            local_var_types,
        )
        assert result is None

    def test_resolves_js_builtin_type(self, call_resolver: CallResolver) -> None:
        local_var_types = {"arr": "Array"}

        result = call_resolver._try_resolve_via_local_type(
            "arr",
            "push",
            cs.SEPARATOR_DOT,
            "arr.push",
            {},
            "proj.module",
            local_var_types,
        )
        assert result is not None
        assert cs.BUILTIN_PREFIX in result[1]
        assert "Array.prototype.push" in result[1]


class TestTryMethodOnClass:
    def test_resolves_method_on_class(self, call_resolver: CallResolver) -> None:
        call_resolver.function_registry["proj.models.User.save"] = NodeType.METHOD

        result = call_resolver._try_method_on_class(
            "proj.models.User",
            "save",
            cs.SEPARATOR_DOT,
            "user.save",
            "user",
            "User",
        )
        assert result is not None
        assert result[1] == "proj.models.User.save"

    def test_resolves_inherited_method(self, call_resolver: CallResolver) -> None:
        call_resolver.function_registry["proj.models.BaseModel.validate"] = (
            NodeType.METHOD
        )
        call_resolver.class_inheritance["proj.models.User"] = ["proj.models.BaseModel"]

        result = call_resolver._try_method_on_class(
            "proj.models.User",
            "validate",
            cs.SEPARATOR_DOT,
            "user.validate",
            "user",
            "User",
        )
        assert result is not None
        assert result[1] == "proj.models.BaseModel.validate"

    def test_returns_none_for_unknown_method(self, call_resolver: CallResolver) -> None:
        result = call_resolver._try_method_on_class(
            "proj.models.User",
            "unknown",
            cs.SEPARATOR_DOT,
            "user.unknown",
            "user",
            "User",
        )
        assert result is None


class TestTryResolveViaImport:
    def test_resolves_static_method_via_import(
        self, call_resolver: CallResolver
    ) -> None:
        call_resolver.function_registry["proj.utils.StringUtils.format"] = (
            NodeType.METHOD
        )
        import_map = {"StringUtils": "proj.utils.StringUtils"}

        result = call_resolver._try_resolve_via_import(
            "StringUtils",
            "format",
            cs.SEPARATOR_DOT,
            "StringUtils.format",
            import_map,
        )
        assert result is not None
        assert result[1] == "proj.utils.StringUtils.format"

    def test_returns_none_for_unimported(self, call_resolver: CallResolver) -> None:
        import_map: dict[str, str] = {}

        result = call_resolver._try_resolve_via_import(
            "StringUtils",
            "format",
            cs.SEPARATOR_DOT,
            "StringUtils.format",
            import_map,
        )
        assert result is None


class TestResolveImportedClassQn:
    def test_returns_class_qn_for_matching_method(
        self, call_resolver: CallResolver
    ) -> None:
        call_resolver.function_registry["proj.models.User.User.save"] = NodeType.METHOD

        result = call_resolver._resolve_imported_class_qn(
            "proj.models.User",
            "User",
            "save",
            cs.SEPARATOR_DOT,
        )
        assert result == "proj.models.User.User"

    def test_returns_original_for_no_match(self, call_resolver: CallResolver) -> None:
        result = call_resolver._resolve_imported_class_qn(
            "proj.models.User",
            "User",
            "save",
            cs.SEPARATOR_DOT,
        )
        assert result == "proj.models.User"


class TestResolveRustClassQn:
    def test_resolves_rust_class_qn(self, call_resolver: CallResolver) -> None:
        call_resolver.function_registry["crate.module.MyStruct"] = NodeType.CLASS

        result = call_resolver._resolve_rust_class_qn("crate::module::MyStruct")
        assert result == "crate.module.MyStruct"

    def test_returns_original_for_no_match(self, call_resolver: CallResolver) -> None:
        result = call_resolver._resolve_rust_class_qn("crate::module::Unknown")
        assert result == "crate::module::Unknown"


class TestTryResolveModuleMethod:
    def test_resolves_module_method(self, call_resolver: CallResolver) -> None:
        call_resolver.function_registry["proj.module.helper"] = NodeType.FUNCTION

        result = call_resolver._try_resolve_module_method(
            "helper",
            "obj.helper",
            "proj.module",
        )
        assert result is not None
        assert result[1] == "proj.module.helper"

    def test_returns_none_for_unknown_method(self, call_resolver: CallResolver) -> None:
        result = call_resolver._try_resolve_module_method(
            "unknown",
            "obj.unknown",
            "proj.module",
        )
        assert result is None


class TestResolveSelfAttributeCall:
    def test_resolves_self_attribute_method(self, call_resolver: CallResolver) -> None:
        call_resolver.function_registry["proj.models.Database.query"] = NodeType.METHOD
        import_map = {"Database": "proj.models.Database"}
        local_var_types = {"self.db": "Database"}

        result = call_resolver._resolve_self_attribute_call(
            ["self", "db", "query"],
            "self.db.query",
            import_map,
            "proj.service",
            local_var_types,
        )
        assert result is not None
        assert result[1] == "proj.models.Database.query"

    def test_resolves_inherited_self_attribute_method(
        self, call_resolver: CallResolver
    ) -> None:
        call_resolver.function_registry["proj.models.BaseDB.close"] = NodeType.METHOD
        call_resolver.class_inheritance["proj.models.Database"] = ["proj.models.BaseDB"]
        import_map = {"Database": "proj.models.Database"}
        local_var_types = {"self.db": "Database"}

        result = call_resolver._resolve_self_attribute_call(
            ["self", "db", "close"],
            "self.db.close",
            import_map,
            "proj.service",
            local_var_types,
        )
        assert result is not None
        assert result[1] == "proj.models.BaseDB.close"

    def test_returns_none_for_no_local_type(self, call_resolver: CallResolver) -> None:
        result = call_resolver._resolve_self_attribute_call(
            ["self", "db", "query"],
            "self.db.query",
            {},
            "proj.service",
            None,
        )
        assert result is None


class TestResolveMultiPartCall:
    def test_resolves_imported_multi_part_call(
        self, call_resolver: CallResolver
    ) -> None:
        call_resolver.function_registry["proj.utils.Config.settings.get"] = (
            NodeType.METHOD
        )
        import_map = {"Config": "proj.utils.Config"}

        result = call_resolver._resolve_multi_part_call(
            ["Config", "settings", "get"],
            "Config.settings.get",
            import_map,
            "proj.app",
            None,
        )
        assert result is not None
        assert result[1] == "proj.utils.Config.settings.get"

    def test_resolves_via_local_type(self, call_resolver: CallResolver) -> None:
        call_resolver.function_registry["proj.models.User.profile.update"] = (
            NodeType.METHOD
        )
        import_map = {"User": "proj.models.User"}
        local_var_types = {"user": "User"}

        result = call_resolver._resolve_multi_part_call(
            ["user", "profile", "update"],
            "user.profile.update",
            import_map,
            "proj.service",
            local_var_types,
        )
        assert result is not None
        assert result[1] == "proj.models.User.profile.update"

    def test_returns_none_for_unknown_call(self, call_resolver: CallResolver) -> None:
        result = call_resolver._resolve_multi_part_call(
            ["unknown", "method", "call"],
            "unknown.method.call",
            {},
            "proj.module",
            None,
        )
        assert result is None


class TestTryResolveQualifiedCall:
    def test_two_part_call(self, call_resolver: CallResolver) -> None:
        call_resolver.function_registry["proj.models.User.save"] = NodeType.METHOD
        import_map = {"User": "proj.models.User"}

        result = call_resolver._try_resolve_qualified_call(
            "User.save",
            import_map,
            "proj.views",
            None,
        )
        assert result is not None
        assert result[1] == "proj.models.User.save"

    def test_self_attribute_call(self, call_resolver: CallResolver) -> None:
        call_resolver.function_registry["proj.models.DB.query"] = NodeType.METHOD
        import_map = {"DB": "proj.models.DB"}
        local_var_types = {"self.db": "DB"}

        result = call_resolver._try_resolve_qualified_call(
            "self.db.query",
            import_map,
            "proj.service",
            local_var_types,
        )
        assert result is not None
        assert result[1] == "proj.models.DB.query"

    def test_returns_none_for_no_separator(self, call_resolver: CallResolver) -> None:
        result = call_resolver._try_resolve_qualified_call(
            "simple_func",
            {},
            "proj.module",
            None,
        )
        assert result is None


class TestResolveClassQnFromType:
    def test_returns_dotted_type_as_is(self, call_resolver: CallResolver) -> None:
        result = call_resolver._resolve_class_qn_from_type(
            "proj.models.User",
            {},
            "proj.views",
        )
        assert result == "proj.models.User"

    def test_resolves_from_import_map(self, call_resolver: CallResolver) -> None:
        import_map = {"User": "proj.models.User"}

        result = call_resolver._resolve_class_qn_from_type(
            "User",
            import_map,
            "proj.views",
        )
        assert result == "proj.models.User"

    def test_falls_back_to_class_name_resolution(
        self, call_resolver: CallResolver
    ) -> None:
        call_resolver.function_registry["proj.views.LocalClass"] = NodeType.CLASS

        result = call_resolver._resolve_class_qn_from_type(
            "LocalClass",
            {},
            "proj.views",
        )
        assert result == "proj.views.LocalClass"


class TestResolveJavaMethodCall:
    def test_resolves_java_method_call(
        self, temp_repo: Path, mock_ingestor: MagicMock
    ) -> None:
        parsers, queries = load_parsers()
        if cs.SupportedLanguage.JAVA not in parsers:
            pytest.skip("Java parser not available")

        updater = GraphUpdater(
            ingestor=mock_ingestor,
            repo_path=temp_repo,
            parsers=parsers,
            queries=queries,
        )
        resolver = updater.factory.call_processor._resolver
        resolver.function_registry["com.example.MyClass.doSomething"] = NodeType.METHOD
        resolver.import_processor.import_mapping["com.example.App"] = {
            "MyClass": "com.example.MyClass"
        }

        java_code = b"obj.doSomething();"
        parser = parsers[cs.SupportedLanguage.JAVA]
        tree = parser.parse(java_code)
        call_node = tree.root_node.children[0].children[0]

        local_var_types = {"obj": "MyClass"}

        result = resolver.resolve_java_method_call(
            call_node,
            "com.example.App",
            local_var_types,
        )
        assert result is not None
        assert result[1] == "com.example.MyClass.doSomething"

    def test_returns_none_for_unresolved_java_call(
        self, temp_repo: Path, mock_ingestor: MagicMock
    ) -> None:
        parsers, queries = load_parsers()
        if cs.SupportedLanguage.JAVA not in parsers:
            pytest.skip("Java parser not available")

        updater = GraphUpdater(
            ingestor=mock_ingestor,
            repo_path=temp_repo,
            parsers=parsers,
            queries=queries,
        )
        resolver = updater.factory.call_processor._resolver

        java_code = b"unknown.method();"
        parser = parsers[cs.SupportedLanguage.JAVA]
        tree = parser.parse(java_code)
        call_node = tree.root_node.children[0].children[0]

        result = resolver.resolve_java_method_call(
            call_node,
            "com.example.App",
            {},
        )
        assert result is None


class TestResolveInheritedMethod:
    def test_finds_method_in_parent(self, call_resolver: CallResolver) -> None:
        call_resolver.function_registry["proj.base.BaseClass.method"] = NodeType.METHOD
        call_resolver.class_inheritance["proj.module.ChildClass"] = [
            "proj.base.BaseClass"
        ]

        result = call_resolver._resolve_inherited_method(
            "proj.module.ChildClass", "method"
        )
        assert result is not None
        assert result[1] == "proj.base.BaseClass.method"

    def test_finds_method_in_grandparent(self, call_resolver: CallResolver) -> None:
        call_resolver.function_registry["proj.base.GrandparentClass.method"] = (
            NodeType.METHOD
        )
        call_resolver.class_inheritance["proj.module.ChildClass"] = [
            "proj.base.ParentClass"
        ]
        call_resolver.class_inheritance["proj.base.ParentClass"] = [
            "proj.base.GrandparentClass"
        ]

        result = call_resolver._resolve_inherited_method(
            "proj.module.ChildClass", "method"
        )
        assert result is not None
        assert result[1] == "proj.base.GrandparentClass.method"

    def test_returns_none_for_unknown_class(self, call_resolver: CallResolver) -> None:
        result = call_resolver._resolve_inherited_method("unknown.Class", "method")
        assert result is None

    def test_returns_none_for_unknown_method(self, call_resolver: CallResolver) -> None:
        call_resolver.class_inheritance["proj.module.ChildClass"] = [
            "proj.base.ParentClass"
        ]

        result = call_resolver._resolve_inherited_method(
            "proj.module.ChildClass", "unknown_method"
        )
        assert result is None

    def test_handles_diamond_inheritance(self, call_resolver: CallResolver) -> None:
        call_resolver.function_registry["proj.base.Root.method"] = NodeType.METHOD
        call_resolver.class_inheritance["proj.module.Child"] = [
            "proj.base.ParentA",
            "proj.base.ParentB",
        ]
        call_resolver.class_inheritance["proj.base.ParentA"] = ["proj.base.Root"]
        call_resolver.class_inheritance["proj.base.ParentB"] = ["proj.base.Root"]

        result = call_resolver._resolve_inherited_method("proj.module.Child", "method")
        assert result is not None
        assert result[1] == "proj.base.Root.method"


class TestCalculateImportDistance:
    def test_same_module_distance_zero(self, call_resolver: CallResolver) -> None:
        distance = call_resolver._calculate_import_distance(
            "proj.module.func", "proj.module"
        )
        assert distance <= 1

    def test_sibling_module_distance_low(self, call_resolver: CallResolver) -> None:
        distance = call_resolver._calculate_import_distance(
            "proj.other.func", "proj.module"
        )
        assert distance == 1

    def test_distant_module_higher_distance(self, call_resolver: CallResolver) -> None:
        distance_close = call_resolver._calculate_import_distance(
            "proj.utils.func", "proj.module"
        )
        distance_far = call_resolver._calculate_import_distance(
            "external.lib.utils.func", "proj.module"
        )
        assert distance_far > distance_close


class TestIsMethodChain:
    def test_simple_method_not_chain(self, call_resolver: CallResolver) -> None:
        assert call_resolver._is_method_chain("obj.method") is False

    def test_method_with_parens_is_chain(self, call_resolver: CallResolver) -> None:
        assert call_resolver._is_method_chain("obj.method().next") is True

    def test_chained_calls_is_chain(self, call_resolver: CallResolver) -> None:
        assert call_resolver._is_method_chain("obj.first().second().third") is True

    def test_no_dots_not_chain(self, call_resolver: CallResolver) -> None:
        assert call_resolver._is_method_chain("simple()") is False

    def test_empty_string_not_chain(self, call_resolver: CallResolver) -> None:
        assert call_resolver._is_method_chain("") is False


class TestResolveChainedCall:
    def test_resolves_chained_method(
        self, temp_repo: Path, mock_ingestor: MagicMock
    ) -> None:
        parsers, queries = load_parsers()
        updater = GraphUpdater(
            ingestor=mock_ingestor,
            repo_path=temp_repo,
            parsers=parsers,
            queries=queries,
        )
        processor = updater.factory.call_processor
        processor._resolver.function_registry["proj.models.QuerySet.filter"] = (
            NodeType.METHOD
        )
        processor._resolver.function_registry["proj.models.QuerySet.all"] = (
            NodeType.METHOD
        )
        processor._resolver.type_inference.python_type_inference._method_return_type_cache[
            "proj.models.QuerySet.all"
        ] = "QuerySet"
        processor._resolver.import_processor.import_mapping["proj.views"] = {
            "QuerySet": "proj.models.QuerySet"
        }

        result = processor._resolver._resolve_chained_call(
            "qs.all().filter",
            "proj.views",
            {"qs": "QuerySet"},
        )
        if result is not None:
            assert "filter" in result[1] or "QuerySet" in result[1]

    def test_returns_none_for_unresolvable_chain(
        self, call_resolver: CallResolver
    ) -> None:
        result = call_resolver._resolve_chained_call(
            "unknown.method().next",
            "proj.module",
            None,
        )
        assert result is None


class TestResolveSuperCall:
    def test_super_constructor_call(self, call_resolver: CallResolver) -> None:
        call_resolver.function_registry["proj.base.Parent.constructor"] = (
            NodeType.METHOD
        )
        call_resolver.class_inheritance["proj.module.Child"] = ["proj.base.Parent"]

        result = call_resolver._resolve_super_call(
            cs.KEYWORD_SUPER,
            class_context="proj.module.Child",
        )
        assert result is not None
        assert result[1] == "proj.base.Parent.constructor"

    def test_super_method_call(self, call_resolver: CallResolver) -> None:
        call_resolver.function_registry["proj.base.Parent.validate"] = NodeType.METHOD
        call_resolver.class_inheritance["proj.module.Child"] = ["proj.base.Parent"]

        result = call_resolver._resolve_super_call(
            f"{cs.KEYWORD_SUPER}.validate",
            class_context="proj.module.Child",
        )
        assert result is not None
        assert result[1] == "proj.base.Parent.validate"

    def test_returns_none_without_class_context(
        self, call_resolver: CallResolver
    ) -> None:
        result = call_resolver._resolve_super_call(cs.KEYWORD_SUPER, class_context=None)
        assert result is None

    def test_returns_none_for_unknown_class(self, call_resolver: CallResolver) -> None:
        result = call_resolver._resolve_super_call(
            cs.KEYWORD_SUPER,
            class_context="unknown.Class",
        )
        assert result is None


class TestResolveFunctionCallIntegration:
    def test_resolves_iife_priority(self, call_resolver: CallResolver) -> None:
        call_resolver.function_registry["proj.module.__iife_func_1_5"] = (
            NodeType.FUNCTION
        )

        result = call_resolver.resolve_function_call("__iife_func_1_5", "proj.module")
        assert result is not None
        assert result[1] == "proj.module.__iife_func_1_5"

    def test_resolves_super_call(self, call_resolver: CallResolver) -> None:
        call_resolver.function_registry["proj.base.Parent.constructor"] = (
            NodeType.METHOD
        )
        call_resolver.class_inheritance["proj.module.Child"] = ["proj.base.Parent"]

        result = call_resolver.resolve_function_call(
            cs.KEYWORD_SUPER,
            "proj.module",
            class_context="proj.module.Child",
        )
        assert result is not None
        assert result[1] == "proj.base.Parent.constructor"

    def test_resolves_imported_function(self, call_resolver: CallResolver) -> None:
        call_resolver.function_registry["proj.utils.helper"] = NodeType.FUNCTION
        call_resolver.import_processor.import_mapping["proj.module"] = {
            "helper": "proj.utils.helper"
        }

        result = call_resolver.resolve_function_call("helper", "proj.module")
        assert result is not None
        assert result[1] == "proj.utils.helper"

    def test_resolves_same_module_function(self, call_resolver: CallResolver) -> None:
        call_resolver.function_registry["proj.module.local_func"] = NodeType.FUNCTION

        result = call_resolver.resolve_function_call("local_func", "proj.module")
        assert result is not None
        assert result[1] == "proj.module.local_func"

    def test_falls_back_to_trie(self, call_resolver: CallResolver) -> None:
        call_resolver.function_registry["proj.other.helper"] = NodeType.FUNCTION

        result = call_resolver.resolve_function_call("helper", "proj.module")
        assert result is not None
        assert result[1] == "proj.other.helper"

    def test_returns_none_for_unknown(self, call_resolver: CallResolver) -> None:
        result = call_resolver.resolve_function_call("unknown_func", "proj.module")
        assert result is None


class TestDequeBfs:
    def test_bfs_order_prefers_closer_parent(self, call_resolver: CallResolver) -> None:
        call_resolver.function_registry["proj.base.ParentA.method"] = NodeType.METHOD
        call_resolver.function_registry["proj.base.ParentB.method"] = NodeType.METHOD
        call_resolver.class_inheritance["proj.module.Child"] = [
            "proj.base.ParentA",
            "proj.base.ParentB",
        ]

        result = call_resolver._resolve_inherited_method("proj.module.Child", "method")
        assert result is not None
        assert result[1] == "proj.base.ParentA.method"

    def test_bfs_finds_deep_ancestor_method(self, call_resolver: CallResolver) -> None:
        call_resolver.function_registry["proj.base.Root.deep_method"] = NodeType.METHOD
        call_resolver.class_inheritance["proj.module.Child"] = ["proj.mid.Middle"]
        call_resolver.class_inheritance["proj.mid.Middle"] = ["proj.base.Root"]

        result = call_resolver._resolve_inherited_method(
            "proj.module.Child", "deep_method"
        )
        assert result is not None
        assert result[1] == "proj.base.Root.deep_method"

    def test_bfs_no_infinite_loop_on_cycle(self, call_resolver: CallResolver) -> None:
        call_resolver.class_inheritance["proj.A"] = ["proj.B"]
        call_resolver.class_inheritance["proj.B"] = ["proj.A"]

        result = call_resolver._resolve_inherited_method("proj.A", "missing")
        assert result is None


class TestSeparatorPattern:
    def test_splits_on_dot(self) -> None:
        from codebase_rag.parsers.call_resolver import _SEPARATOR_PATTERN

        assert _SEPARATOR_PATTERN.split("a.b.c") == ["a", "b", "c"]

    def test_splits_on_colon(self) -> None:
        from codebase_rag.parsers.call_resolver import _SEPARATOR_PATTERN

        assert _SEPARATOR_PATTERN.split("module:func") == ["module", "func"]

    def test_splits_on_double_colon(self) -> None:
        from codebase_rag.parsers.call_resolver import _SEPARATOR_PATTERN

        assert _SEPARATOR_PATTERN.split("crate::module::func") == [
            "crate",
            "",
            "module",
            "",
            "func",
        ]

    def test_no_separator_returns_single_element(self) -> None:
        from codebase_rag.parsers.call_resolver import _SEPARATOR_PATTERN

        assert _SEPARATOR_PATTERN.split("simple") == ["simple"]

    def test_last_element_matches_function_name(self) -> None:
        from codebase_rag.parsers.call_resolver import _SEPARATOR_PATTERN

        assert _SEPARATOR_PATTERN.split("a.b.func")[-1] == "func"
        assert _SEPARATOR_PATTERN.split("module:method")[-1] == "method"


class TestChainedMethodPattern:
    def test_matches_final_method(self) -> None:
        from codebase_rag.parsers.call_resolver import _CHAINED_METHOD_PATTERN

        match = _CHAINED_METHOD_PATTERN.search("obj.method().next")
        assert match is not None
        assert match[1] == "next"

    def test_no_match_on_parenthesized_suffix(self) -> None:
        from codebase_rag.parsers.call_resolver import _CHAINED_METHOD_PATTERN

        match = _CHAINED_METHOD_PATTERN.search("obj.method()")
        assert match is None

    def test_matches_deeply_chained(self) -> None:
        from codebase_rag.parsers.call_resolver import _CHAINED_METHOD_PATTERN

        match = _CHAINED_METHOD_PATTERN.search("a.b().c().final_method")
        assert match is not None
        assert match[1] == "final_method"


class TestDeterministicResolution:
    def test_trie_tiebreak_by_qualified_name(self, call_resolver: CallResolver) -> None:
        # (H) Register multiple functions with the same simple name in different modules
        # (H) at equal import distance from the caller
        call_resolver.function_registry["proj.alpha.utils.helper"] = NodeType.FUNCTION
        call_resolver.function_registry["proj.beta.utils.helper"] = NodeType.FUNCTION
        call_resolver.function_registry["proj.gamma.utils.helper"] = NodeType.FUNCTION

        results = []
        for _ in range(20):
            result = call_resolver._try_resolve_via_trie("helper", "proj.delta.module")
            assert result is not None
            results.append(result[1])

        # (H) All 20 runs must resolve to the same candidate (lexicographically first)
        assert all(r == results[0] for r in results)
        assert results[0] == "proj.alpha.utils.helper"

    def test_trie_tiebreak_picks_lexicographic_first(
        self, call_resolver: CallResolver
    ) -> None:
        # (H) Deliberately insert in reverse lexicographic order
        call_resolver.function_registry["proj.zoo.compute"] = NodeType.FUNCTION
        call_resolver.function_registry["proj.mid.compute"] = NodeType.FUNCTION
        call_resolver.function_registry["proj.aaa.compute"] = NodeType.FUNCTION

        result = call_resolver._try_resolve_via_trie("compute", "other.module")
        assert result is not None
        assert result[1] == "proj.aaa.compute"

    def test_trie_tiebreak_distance_still_wins(
        self, call_resolver: CallResolver
    ) -> None:
        # (H) Closer module should win even if lexicographically later
        call_resolver.function_registry["proj.far.away.process"] = NodeType.FUNCTION
        call_resolver.function_registry["proj.module.process"] = NodeType.FUNCTION

        result = call_resolver._try_resolve_via_trie("process", "proj.module.caller")
        assert result is not None
        # (H) proj.module.process is closer to proj.module.caller
        assert result[1] == "proj.module.process"

    def test_trie_many_candidates_deterministic(
        self, call_resolver: CallResolver
    ) -> None:
        # (H) Register 10 equidistant candidates
        names = [
            "proj.m09.run",
            "proj.m05.run",
            "proj.m01.run",
            "proj.m07.run",
            "proj.m03.run",
            "proj.m08.run",
            "proj.m02.run",
            "proj.m06.run",
            "proj.m04.run",
            "proj.m10.run",
        ]
        for name in names:
            call_resolver.function_registry[name] = NodeType.FUNCTION

        result = call_resolver._try_resolve_via_trie("run", "other.caller")
        assert result is not None
        assert result[1] == "proj.m01.run"

    def test_resolve_function_call_deterministic_across_runs(
        self, call_resolver: CallResolver
    ) -> None:
        call_resolver.function_registry["pkg.svc_a.validate"] = NodeType.FUNCTION
        call_resolver.function_registry["pkg.svc_b.validate"] = NodeType.FUNCTION
        call_resolver.function_registry["pkg.svc_c.validate"] = NodeType.FUNCTION

        results = set()
        for _ in range(10):
            result = call_resolver.resolve_function_call(
                "validate", "pkg.other.module", {}, None
            )
            assert result is not None
            results.add(result[1])

        # (H) Must resolve to exactly one candidate across all runs
        assert len(results) == 1


class TestDeterministicFileOrder:
    def test_eligible_files_are_sorted(
        self, temp_repo: Path, mock_ingestor: MagicMock
    ) -> None:
        parsers, queries = load_parsers()

        # (H) Create files in non-alphabetical order
        for name in ["zebra.py", "alpha.py", "middle.py", "beta.py"]:
            (temp_repo / name).write_text(f"def func_{name[0]}(): pass\n")

        updater = GraphUpdater(
            ingestor=mock_ingestor,
            repo_path=temp_repo,
            parsers=parsers,
            queries=queries,
        )

        eligible = updater._collect_eligible_files()
        paths_str = [str(f) for f in eligible]

        assert paths_str == sorted(paths_str)

    def test_graph_output_deterministic_across_runs(self, temp_repo: Path) -> None:
        parsers, queries = load_parsers()

        (temp_repo / "mod_a.py").write_text(
            "def shared(): pass\ndef call_a(): shared()\n"
        )
        (temp_repo / "mod_b.py").write_text(
            "def shared(): pass\ndef call_b(): shared()\n"
        )

        results = []
        for _ in range(5):
            ingestor = MagicMock()
            updater = GraphUpdater(
                ingestor=ingestor,
                repo_path=temp_repo,
                parsers=parsers,
                queries=queries,
            )
            updater.run(force=True)

            calls = [
                (c.args[0][2], c.args[1], c.args[2][2])
                for c in ingestor.ensure_relationship_batch.call_args_list
                if c.args[1] == cs.RelationshipType.CALLS
            ]
            calls.sort()
            results.append(calls)

        # (H) All 5 runs must produce identical call graphs
        assert len(results[0]) > 0
        for i in range(1, len(results)):
            assert results[i] == results[0]

    def _run_determinism_check(self, temp_repo: Path, runs: int = 5) -> None:
        parsers, queries = load_parsers()
        results = []
        for _ in range(runs):
            ingestor = MagicMock()
            updater = GraphUpdater(
                ingestor=ingestor,
                repo_path=temp_repo,
                parsers=parsers,
                queries=queries,
            )
            updater.run(force=True)

            calls = [
                (c.args[0][2], c.args[2][2])
                for c in ingestor.ensure_relationship_batch.call_args_list
                if c.args[1] == cs.RelationshipType.CALLS
            ]
            calls.sort()
            results.append(calls)

        assert len(results[0]) > 0
        for i in range(1, len(results)):
            assert results[i] == results[0]

    def test_javascript_deterministic(self, temp_repo: Path) -> None:
        parsers, _ = load_parsers()
        if cs.SupportedLanguage.JS not in parsers:
            pytest.skip("JavaScript parser not available")

        (temp_repo / "utils.js").write_text(
            "function helper() {}\nfunction worker() { helper(); }\n"
        )
        (temp_repo / "main.js").write_text(
            "function helper() {}\nfunction entry() { helper(); }\n"
        )
        self._run_determinism_check(temp_repo)

    def test_typescript_deterministic(self, temp_repo: Path) -> None:
        parsers, _ = load_parsers()
        if cs.SupportedLanguage.TS not in parsers:
            pytest.skip("TypeScript parser not available")

        (temp_repo / "service.ts").write_text(
            "function validate(x: string): boolean { return true; }\n"
            "function process() { validate('test'); }\n"
        )
        (temp_repo / "handler.ts").write_text(
            "function validate(x: string): boolean { return false; }\n"
            "function handle() { validate('input'); }\n"
        )
        self._run_determinism_check(temp_repo)

    def test_rust_deterministic(self, temp_repo: Path) -> None:
        parsers, _ = load_parsers()
        if cs.SupportedLanguage.RUST not in parsers:
            pytest.skip("Rust parser not available")

        (temp_repo / "utils.rs").write_text(
            "fn compute() -> i32 { 42 }\nfn run() { compute(); }\n"
        )
        (temp_repo / "main.rs").write_text(
            "fn compute() -> i32 { 0 }\nfn start() { compute(); }\n"
        )
        self._run_determinism_check(temp_repo)

    def test_java_deterministic(self, temp_repo: Path) -> None:
        parsers, _ = load_parsers()
        if cs.SupportedLanguage.JAVA not in parsers:
            pytest.skip("Java parser not available")

        (temp_repo / "Utils.java").write_text(
            "public class Utils {\n"
            "    public static void process() {}\n"
            "    public static void run() { process(); }\n"
            "}\n"
        )
        (temp_repo / "Helper.java").write_text(
            "public class Helper {\n"
            "    public static void process() {}\n"
            "    public static void execute() { process(); }\n"
            "}\n"
        )
        self._run_determinism_check(temp_repo)

    def test_cpp_deterministic(self, temp_repo: Path) -> None:
        parsers, _ = load_parsers()
        if cs.SupportedLanguage.CPP not in parsers:
            pytest.skip("C++ parser not available")

        (temp_repo / "math.cpp").write_text(
            "int calculate() { return 1; }\nint run() { return calculate(); }\n"
        )
        (temp_repo / "logic.cpp").write_text(
            "int calculate() { return 2; }\nint start() { return calculate(); }\n"
        )
        self._run_determinism_check(temp_repo)

    def test_go_deterministic(self, temp_repo: Path) -> None:
        parsers, _ = load_parsers()
        if cs.SupportedLanguage.GO not in parsers:
            pytest.skip("Go parser not available")

        (temp_repo / "util.go").write_text(
            "package main\nfunc helper() {}\nfunc doWork() { helper() }\n"
        )
        (temp_repo / "main.go").write_text(
            "package main\nfunc helper() {}\nfunc run() { helper() }\n"
        )
        self._run_determinism_check(temp_repo)

    def test_lua_deterministic(self, temp_repo: Path) -> None:
        parsers, _ = load_parsers()
        if cs.SupportedLanguage.LUA not in parsers:
            pytest.skip("Lua parser not available")

        (temp_repo / "utils.lua").write_text(
            "local function process() end\nlocal function run() process() end\n"
        )
        (temp_repo / "main.lua").write_text(
            "local function process() end\nlocal function start() process() end\n"
        )
        self._run_determinism_check(temp_repo)
