"""Tests for _test_discovery.linking — Jedi-based test linking."""

from __future__ import annotations

import textwrap
from collections import defaultdict
from pathlib import Path

import pytest

from codeflash_python._model import FunctionToOptimize
from codeflash_python.test_discovery.linking import (
    TestFunction,
    add_test_entries,
    module_name_from_file_path,
    process_test_files,
)
from codeflash_python.test_discovery.models import (
    FunctionCalledInTest,
    TestsInFile,
    TestType,
)


class TestModuleNameFromFilePath:
    """module_name_from_file_path dotted-name conversion."""

    def test_simple_module(self, tmp_path: Path) -> None:
        """Single file at root becomes a bare module name."""
        mod = tmp_path / "mod.py"
        mod.write_text("pass\n")
        assert "mod" == module_name_from_file_path(mod, tmp_path)

    def test_nested_module(self, tmp_path: Path) -> None:
        """Nested path becomes a dotted module name."""
        pkg = tmp_path / "pkg" / "sub"
        pkg.mkdir(parents=True)
        mod = pkg / "mod.py"
        mod.write_text("pass\n")
        assert "pkg.sub.mod" == module_name_from_file_path(mod, tmp_path)

    def test_init_file(self, tmp_path: Path) -> None:
        """__init__.py preserves the __init__ component."""
        pkg = tmp_path / "pkg"
        pkg.mkdir()
        init = pkg / "__init__.py"
        init.write_text("pass\n")
        assert "pkg.__init__" == module_name_from_file_path(init, tmp_path)

    def test_raises_on_unrelated_path(self, tmp_path: Path) -> None:
        """Raises ValueError when the file is not under root."""
        unrelated = Path("/some/other/place/mod.py")
        with pytest.raises(ValueError, match="is not in the subpath of"):
            module_name_from_file_path(unrelated, tmp_path)


class TestTestFunction:
    """TestFunction attrs class."""

    def test_construction(self) -> None:
        """Fields are accessible after construction."""
        tf = TestFunction(
            function_name="test_foo",
            test_class="TestBar",
            parameters="param1",
            test_type=TestType.EXISTING_UNIT_TEST,
        )
        assert "test_foo" == tf.function_name
        assert "TestBar" == tf.test_class
        assert "param1" == tf.parameters
        assert TestType.EXISTING_UNIT_TEST == tf.test_type

    def test_frozen(self) -> None:
        """Instances are immutable."""
        tf = TestFunction(
            function_name="test_foo",
            test_class=None,
            parameters=None,
            test_type=TestType.EXISTING_UNIT_TEST,
        )
        with pytest.raises(AttributeError):
            tf.function_name = "other"  # type: ignore[misc]

    def test_none_fields(self) -> None:
        """test_class and parameters can be None."""
        tf = TestFunction(
            function_name="test_foo",
            test_class=None,
            parameters=None,
            test_type=TestType.EXISTING_UNIT_TEST,
        )
        assert tf.test_class is None
        assert tf.parameters is None


class TestAddTestEntries:
    """add_test_entries helper function."""

    def test_adds_entries(self) -> None:
        """Adds a FunctionCalledInTest entry for each test function."""
        function_to_test_map: dict[str, set[FunctionCalledInTest]] = (
            defaultdict(set)
        )
        test_functions = [
            TestFunction(
                function_name="test_a",
                test_class=None,
                parameters=None,
                test_type=TestType.EXISTING_UNIT_TEST,
            ),
            TestFunction(
                function_name="test_b",
                test_class="TestFoo",
                parameters=None,
                test_type=TestType.EXISTING_UNIT_TEST,
            ),
        ]
        test_file = Path("tests/test_core.py")
        add_test_entries(
            function_to_test_map,
            "mypackage.core.target_func",
            test_functions,
            test_file,
            line_no=10,
            col_no=4,
        )
        assert "mypackage.core.target_func" in function_to_test_map
        entries = function_to_test_map["mypackage.core.target_func"]
        assert 2 == len(entries)
        func_names = {e.tests_in_file.test_function for e in entries}
        assert {"test_a", "test_b"} == func_names

    def test_parameterized_pytest(self) -> None:
        """Pytest parameterized names use bracket format."""
        function_to_test_map: dict[str, set[FunctionCalledInTest]] = (
            defaultdict(set)
        )
        test_functions = [
            TestFunction(
                function_name="test_a",
                test_class=None,
                parameters="param1",
                test_type=TestType.EXISTING_UNIT_TEST,
            ),
        ]
        test_file = Path("tests/test_core.py")
        add_test_entries(
            function_to_test_map,
            "mypackage.core.func",
            test_functions,
            test_file,
            line_no=5,
            col_no=0,
        )
        entries = function_to_test_map["mypackage.core.func"]
        entry = next(iter(entries))
        assert "test_a[param1]" == entry.tests_in_file.test_function

    def test_parameterized_unittest(self) -> None:
        """Parameterized names always use bracket format."""
        function_to_test_map: dict[str, set[FunctionCalledInTest]] = (
            defaultdict(set)
        )
        test_functions = [
            TestFunction(
                function_name="test_a",
                test_class=None,
                parameters="0",
                test_type=TestType.EXISTING_UNIT_TEST,
            ),
        ]
        test_file = Path("tests/test_core.py")
        add_test_entries(
            function_to_test_map,
            "mypackage.core.func",
            test_functions,
            test_file,
            line_no=5,
            col_no=0,
        )
        entries = function_to_test_map["mypackage.core.func"]
        entry = next(iter(entries))
        assert "test_a[0]" == entry.tests_in_file.test_function


class TestProcessTestFiles:
    """process_test_files Jedi-based test linking."""

    def test_links_test_to_function(self, tmp_path: Path) -> None:
        """Links a test function to the target function it calls."""
        src = tmp_path / "src"
        pkg = src / "mypackage"
        pkg.mkdir(parents=True)
        (pkg / "__init__.py").write_text("")
        (pkg / "core.py").write_text(
            textwrap.dedent("""\
                def target_func():
                    return 42
            """),
        )

        tests_dir = tmp_path / "tests"
        tests_dir.mkdir()
        test_file = tests_dir / "test_core.py"
        test_file.write_text(
            textwrap.dedent("""\
                from mypackage.core import target_func

                def test_target():
                    assert target_func() == 42
            """),
        )

        tif = TestsInFile(
            test_file=test_file,
            test_class=None,
            test_function="test_target",
            test_type=TestType.EXISTING_UNIT_TEST,
        )
        file_to_test_map = {test_file: [tif]}

        fto = FunctionToOptimize(
            function_name="target_func",
            file_path=pkg / "core.py",
            starting_line=1,
            ending_line=2,
        )

        result = process_test_files(
            file_to_test_map,
            project_root=src,
            functions_to_optimize=[fto],
        )

        assert len(result) > 0
        matched_keys = [k for k in result if "target_func" in k]
        assert len(matched_keys) > 0
        entries = result[matched_keys[0]]
        assert len(entries) >= 1
        entry = next(iter(entries))
        assert "test_target" in entry.tests_in_file.test_function

    def test_empty_map_returns_empty(self, tmp_path: Path) -> None:
        """Empty file_to_test_map returns empty dict."""
        result = process_test_files(
            {},
            project_root=tmp_path,
        )
        assert {} == result

    def test_skips_unresolvable_files(self, tmp_path: Path) -> None:
        """Non-existent test files are skipped gracefully."""
        missing_file = tmp_path / "tests" / "test_missing.py"
        tif = TestsInFile(
            test_file=missing_file,
            test_class=None,
            test_function="test_something",
            test_type=TestType.EXISTING_UNIT_TEST,
        )
        file_to_test_map = {missing_file: [tif]}

        result = process_test_files(
            file_to_test_map,
            project_root=tmp_path,
        )
        assert {} == result
