"""Tests for _tracing (tracing, profiling, replay test generation)."""

from __future__ import annotations

import pickle
import sqlite3
import sys
from pathlib import Path
from typing import TYPE_CHECKING

if TYPE_CHECKING:
    from collections.abc import Generator

import attrs
import pytest

from codeflash_python.benchmarking._file_filtering import (
    filter_files_optimized,
    is_test_file_by_pattern,
)
from codeflash_python.benchmarking._replay_gen import create_trace_replay_test
from codeflash_python.benchmarking._trace_db import (
    FUNCTION_CALLS_SCHEMA,
    TOTAL_TIME_SCHEMA,
    get_trace_total_run_time_ns,
    get_traced_arguments,
    sanitize_to_filename,
)
from codeflash_python.benchmarking._trace_models import TracedFunction
from codeflash_python.benchmarking._tracing import Tracer
from codeflash_python.benchmarking.models import get_function_alias
from codeflash_python.test_discovery.linking import module_name_from_file_path


def create_trace_db(
    path: Path,
    calls: list[tuple[str, str, str | None, str, int, int, int, bytes]],
) -> Path:
    """Create a SQLite trace database with function_calls data."""
    conn = sqlite3.connect(str(path))
    conn.execute(FUNCTION_CALLS_SCHEMA)
    for call in calls:
        conn.execute(
            "INSERT INTO function_calls VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
            call,
        )
    conn.commit()
    conn.close()
    return path


def create_total_time_db(path: Path, total_ns: int) -> Path:
    """Create a SQLite trace database with a total_time table."""
    conn = sqlite3.connect(str(path))
    conn.execute(TOTAL_TIME_SCHEMA)
    conn.execute(
        "INSERT INTO total_time VALUES (?)",
        (total_ns,),
    )
    conn.commit()
    conn.close()
    return path


@pytest.fixture(autouse=True)
def _reset_tracer_used_once() -> Generator[None, None, None]:
    """Reset Tracer.used_once after each test to avoid cross-test leakage."""
    yield
    Tracer.used_once = False


class TestTracedFunction:
    """TracedFunction attrs frozen data class."""

    def test_construction_with_required_fields(self) -> None:
        """Constructs with required fields and defaults for optionals."""
        tf = TracedFunction(
            function_name="compute",
            file_name=Path("src/mod.py"),
            module_name="mod",
        )

        assert "compute" == tf.function_name
        assert Path("src/mod.py") == tf.file_name
        assert "mod" == tf.module_name
        assert tf.class_name is None
        assert tf.line_no is None
        assert tf.method_type is None
        assert tf.is_top_level is True

    def test_construction_with_all_fields(self) -> None:
        """Accepts all optional fields."""
        tf = TracedFunction(
            function_name="process",
            file_name=Path("src/mod.py"),
            module_name="mod",
            class_name="MyClass",
            line_no=42,
            method_type="instance",
            is_top_level=False,
        )

        assert "process" == tf.function_name
        assert "MyClass" == tf.class_name
        assert 42 == tf.line_no
        assert "instance" == tf.method_type
        assert tf.is_top_level is False

    def test_file_name_converts_string_to_path(self) -> None:
        """A string file_name is converted to Path via the converter."""
        tf = TracedFunction(
            function_name="f",
            file_name="src/mod.py",  # type: ignore[arg-type]
            module_name="mod",
        )

        assert isinstance(tf.file_name, Path)
        assert Path("src/mod.py") == tf.file_name

    def test_frozen_immutability(self) -> None:
        """TracedFunction is immutable (frozen)."""
        tf = TracedFunction(
            function_name="f",
            file_name=Path("mod.py"),
            module_name="mod",
        )

        with pytest.raises(attrs.exceptions.FrozenInstanceError):
            tf.function_name = "changed"  # type: ignore[misc]

    def test_equality(self) -> None:
        """Two TracedFunctions with the same fields are equal."""
        a = TracedFunction(
            function_name="f",
            file_name=Path("mod.py"),
            module_name="mod",
        )
        b = TracedFunction(
            function_name="f",
            file_name=Path("mod.py"),
            module_name="mod",
        )

        assert a == b

    def test_inequality_different_function_name(self) -> None:
        """TracedFunctions with different function_name are not equal."""
        a = TracedFunction(
            function_name="f",
            file_name=Path("mod.py"),
            module_name="mod",
        )
        b = TracedFunction(
            function_name="g",
            file_name=Path("mod.py"),
            module_name="mod",
        )

        assert a != b

    def test_method_type_values(self) -> None:
        """method_type accepts instance, classmethod, and staticmethod."""
        for mt in ("instance", "classmethod", "staticmethod"):
            tf = TracedFunction(
                function_name="f",
                file_name=Path("mod.py"),
                module_name="mod",
                method_type=mt,
            )
            assert mt == tf.method_type


class TestSanitizeToFilename:
    """sanitize_to_filename sanitizes strings for use as filenames."""

    def test_normal_string(self) -> None:
        """A normal alphanumeric string passes through unchanged."""
        assert "hello_world" == sanitize_to_filename("hello_world")

    def test_replaces_newlines_with_underscores(self) -> None:
        """Newlines are replaced with underscores."""
        result = sanitize_to_filename("hello\nworld")
        assert "\n" not in result
        assert "_" in result

    def test_replaces_whitespace_runs(self) -> None:
        """Multiple whitespace characters are collapsed to underscores."""
        result = sanitize_to_filename("hello   world   here")
        assert "  " not in result

    def test_removes_special_characters(self) -> None:
        """Non-alphanumeric/underscore/dot characters are removed."""
        result = sanitize_to_filename("he!!o@#$%^&*w(o)rld")
        for ch in "!@#$%^&*()":
            assert ch not in result

    def test_very_long_string_truncated(self) -> None:
        """Strings longer than 100 characters are truncated."""
        long_input = "a" * 200
        result = sanitize_to_filename(long_input)
        assert len(result) <= 100

    def test_empty_string_returns_untitled(self) -> None:
        """An empty string returns 'untitled'."""
        assert "untitled" == sanitize_to_filename("")

    def test_string_that_becomes_empty_after_sanitization(self) -> None:
        """A string of only special chars becomes 'untitled'."""
        assert "untitled" == sanitize_to_filename("!@#$%^&*()")

    def test_strips_leading_and_trailing_dots_and_underscores(self) -> None:
        """Leading/trailing dots and underscores are stripped."""
        result = sanitize_to_filename("...hello___")
        assert not result.startswith(".")
        assert not result.startswith("_")
        assert not result.endswith("_")
        assert "hello" in result

    def test_string_with_only_dots_and_underscores(self) -> None:
        """A string of only dots and underscores becomes 'untitled'."""
        assert "untitled" == sanitize_to_filename("...__._.")

    def test_preserves_dots_in_middle(self) -> None:
        """Dots within the string are preserved."""
        result = sanitize_to_filename("module.name.py")
        assert "." in result

    def test_tabs_treated_as_whitespace(self) -> None:
        """Tab characters are treated as whitespace."""
        result = sanitize_to_filename("hello\tworld")
        assert "\t" not in result


class TestIsTestFileByPattern:
    """is_test_file_by_pattern detects test files by naming convention."""

    def test_test_prefix(self) -> None:
        """Files starting with test_ are test files."""
        assert is_test_file_by_pattern(Path("test_module.py")) is True

    def test_test_suffix(self) -> None:
        """Files ending with _test.py are test files."""
        assert is_test_file_by_pattern(Path("module_test.py")) is True

    def test_conftest(self) -> None:
        """conftest.py is a test file."""
        assert is_test_file_by_pattern(Path("conftest.py")) is True

    def test_normal_source_file(self) -> None:
        """Normal source files are not test files."""
        assert is_test_file_by_pattern(Path("module.py")) is False

    def test_file_in_tests_directory(self) -> None:
        """Files under a tests/ directory are test files."""
        assert is_test_file_by_pattern(Path("tests/test_something.py")) is True

    def test_file_in_test_directory(self) -> None:
        """Files under a test/ directory are test files."""
        assert is_test_file_by_pattern(Path("test/test_something.py")) is True

    def test_non_test_file(self) -> None:
        """Files without test naming patterns are not test files."""
        assert is_test_file_by_pattern(Path("readme.md")) is False

    def test_nested_test_file(self) -> None:
        """Deeply nested test files are still detected."""
        assert (
            is_test_file_by_pattern(Path("src/tests/unit/test_core.py"))
            is True
        )


class TestFilterFilesOptimized:
    """filter_files_optimized decides if a file should be traced."""

    def test_file_under_module_root(self, tmp_path: Path) -> None:
        """Files under module_root pass the filter."""
        module_root = tmp_path / "src"
        module_root.mkdir()
        source = module_root / "module.py"
        source.touch()

        result = filter_files_optimized(
            source,
            tests_root=tmp_path / "tests",
            ignore_paths=[],
            module_root=module_root,
        )

        assert result is True

    def test_file_under_tests_root(self, tmp_path: Path) -> None:
        """Files under tests_root are filtered out."""
        tests_root = tmp_path / "tests"
        tests_root.mkdir()
        test_file = tests_root / "test_module.py"
        test_file.touch()

        result = filter_files_optimized(
            test_file,
            tests_root=tests_root,
            ignore_paths=[],
            module_root=tmp_path / "src",
        )

        assert result is False

    def test_file_in_ignore_paths(self, tmp_path: Path) -> None:
        """Files in ignore_paths are filtered out."""
        module_root = tmp_path / "src"
        module_root.mkdir()
        vendor = module_root / "vendor"
        vendor.mkdir()
        vendored = vendor / "lib.py"
        vendored.touch()

        result = filter_files_optimized(
            vendored,
            tests_root=tmp_path / "tests",
            ignore_paths=[vendor],
            module_root=module_root,
        )

        assert result is False

    def test_file_not_under_module_root(self, tmp_path: Path) -> None:
        """Files outside module_root are filtered out."""
        module_root = tmp_path / "src"
        module_root.mkdir()
        outside = tmp_path / "other" / "module.py"
        outside.parent.mkdir()
        outside.touch()

        result = filter_files_optimized(
            outside,
            tests_root=tmp_path / "tests",
            ignore_paths=[],
            module_root=module_root,
        )

        assert result is False

    def test_overlapping_roots_test_file_excluded(
        self, tmp_path: Path
    ) -> None:
        """When tests_root overlaps module_root, test files are excluded."""
        root = tmp_path / "src"
        root.mkdir()
        test_file = root / "test_module.py"
        test_file.touch()

        result = filter_files_optimized(
            test_file,
            tests_root=root,
            ignore_paths=[],
            module_root=root,
        )

        assert result is False

    def test_overlapping_roots_non_test_file_included(
        self, tmp_path: Path
    ) -> None:
        """When tests_root overlaps module_root, non-test files pass."""
        root = tmp_path / "src"
        root.mkdir()
        source = root / "module.py"
        source.touch()

        result = filter_files_optimized(
            source,
            tests_root=root / "nonexistent",
            ignore_paths=[],
            module_root=root,
        )

        assert result is True


class TestModuleNameFromFilePath:
    """module_name_from_file_path converts paths to module names."""

    def test_simple_file(self, tmp_path: Path) -> None:
        """A simple file in the project root becomes a module name."""
        result = module_name_from_file_path(tmp_path / "module.py", tmp_path)

        assert "module" == result

    def test_nested_file(self, tmp_path: Path) -> None:
        """A nested file produces a dotted module path."""
        result = module_name_from_file_path(
            tmp_path / "src" / "package" / "module.py", tmp_path
        )

        assert "src.package.module" == result

    def test_strips_py_extension(self, tmp_path: Path) -> None:
        """The .py extension is stripped from the module name."""
        result = module_name_from_file_path(tmp_path / "mod.py", tmp_path)

        assert not result.endswith(".py")


class TestGetFunctionAlias:
    """get_function_alias creates import aliases from module and function."""

    def test_simple_alias(self) -> None:
        """Module dots are replaced with underscores and joined."""
        result = get_function_alias("my.module", "func")

        assert "my_module_func" == result

    def test_deeply_nested_module(self) -> None:
        """Works for deeply nested module paths."""
        result = get_function_alias("a.b.c.d", "compute")

        assert "a_b_c_d_compute" == result

    def test_single_component_module(self) -> None:
        """A single-component module name works correctly."""
        result = get_function_alias("module", "func")

        assert "module_func" == result


class TestGetTracedArguments:
    """get_traced_arguments reads function call arguments from a trace DB."""

    def test_reads_arguments_for_known_function(self, tmp_path: Path) -> None:
        """Returns pickled arguments for a matching function."""
        args_data = pickle.dumps((42,))
        db_path = create_trace_db(
            tmp_path / "trace.db",
            [
                (
                    "call",
                    "compute",
                    None,
                    "src/mod.py",
                    10,
                    0,
                    1000,
                    args_data,
                ),
            ],
        )

        results = list(get_traced_arguments(db_path, "compute", "src/mod.py"))

        assert 1 == len(results)
        assert (42,) == pickle.loads(results[0])

    def test_returns_empty_for_unknown_function(self, tmp_path: Path) -> None:
        """Returns no results for a function not in the DB."""
        args_data = pickle.dumps((1,))
        db_path = create_trace_db(
            tmp_path / "trace.db",
            [
                (
                    "call",
                    "compute",
                    None,
                    "src/mod.py",
                    10,
                    0,
                    1000,
                    args_data,
                ),
            ],
        )

        results = list(
            get_traced_arguments(db_path, "nonexistent", "src/mod.py")
        )

        assert [] == results

    def test_filters_by_class_name(self, tmp_path: Path) -> None:
        """When class_name is provided, only matching rows are returned."""
        args_a = pickle.dumps(("a",))
        args_b = pickle.dumps(("b",))
        db_path = create_trace_db(
            tmp_path / "trace.db",
            [
                (
                    "call",
                    "process",
                    "ClassA",
                    "mod.py",
                    10,
                    0,
                    1000,
                    args_a,
                ),
                (
                    "call",
                    "process",
                    "ClassB",
                    "mod.py",
                    20,
                    0,
                    2000,
                    args_b,
                ),
            ],
        )

        results = list(
            get_traced_arguments(
                db_path,
                "process",
                "mod.py",
                class_name="ClassA",
            )
        )

        assert 1 == len(results)
        assert ("a",) == pickle.loads(results[0])

    def test_respects_num_to_get_limit(self, tmp_path: Path) -> None:
        """Returns at most num_to_get results."""
        calls = [
            (
                "call",
                "func",
                None,
                "mod.py",
                1,
                0,
                i * 100,
                pickle.dumps((i,)),
            )
            for i in range(10)
        ]
        db_path = create_trace_db(tmp_path / "trace.db", calls)

        results = list(
            get_traced_arguments(db_path, "func", "mod.py", num_to_get=3)
        )

        assert 3 == len(results)

    def test_orders_by_time_ascending(self, tmp_path: Path) -> None:
        """Results are ordered by time_ns ascending."""
        db_path = create_trace_db(
            tmp_path / "trace.db",
            [
                (
                    "call",
                    "func",
                    None,
                    "mod.py",
                    1,
                    0,
                    3000,
                    pickle.dumps(("third",)),
                ),
                (
                    "call",
                    "func",
                    None,
                    "mod.py",
                    1,
                    0,
                    1000,
                    pickle.dumps(("first",)),
                ),
                (
                    "call",
                    "func",
                    None,
                    "mod.py",
                    1,
                    0,
                    2000,
                    pickle.dumps(("second",)),
                ),
            ],
        )

        results = list(get_traced_arguments(db_path, "func", "mod.py"))

        assert 3 == len(results)
        assert ("first",) == pickle.loads(results[0])
        assert ("second",) == pickle.loads(results[1])
        assert ("third",) == pickle.loads(results[2])


class TestGetTraceTotalRunTimeNs:
    """get_trace_total_run_time_ns reads total runtime from a trace DB."""

    def test_returns_time_from_valid_db(self, tmp_path: Path) -> None:
        """Returns the total time in nanoseconds from the DB."""
        db_path = create_total_time_db(tmp_path / "trace.db", 5_000_000)

        result = get_trace_total_run_time_ns(db_path)

        assert 5_000_000 == result

    def test_returns_zero_for_nonexistent_file(self, tmp_path: Path) -> None:
        """Returns 0 when the file does not exist."""
        result = get_trace_total_run_time_ns(tmp_path / "nonexistent.db")

        assert 0 == result

    def test_returns_zero_for_empty_total_time_table(
        self, tmp_path: Path
    ) -> None:
        """Returns 0 when the total_time table exists but is empty."""
        db_path = tmp_path / "trace.db"
        conn = sqlite3.connect(str(db_path))
        conn.execute(TOTAL_TIME_SCHEMA)
        conn.commit()
        conn.close()

        result = get_trace_total_run_time_ns(db_path)

        assert 0 == result

    def test_returns_zero_for_db_without_total_time_table(
        self, tmp_path: Path
    ) -> None:
        """Returns 0 when the DB exists but has no total_time table."""
        db_path = tmp_path / "trace.db"
        conn = sqlite3.connect(str(db_path))
        conn.execute("CREATE TABLE other_table (x INTEGER)")
        conn.commit()
        conn.close()

        result = get_trace_total_run_time_ns(db_path)

        assert 0 == result


class TestCreateTraceReplayTest:
    """create_trace_replay_test generates Python test code from trace data."""

    def test_generates_test_for_plain_function(self, tmp_path: Path) -> None:
        """Generates a test function for a top-level function."""
        db_path = create_trace_db(
            tmp_path / "trace.db",
            [
                (
                    "call",
                    "compute",
                    None,
                    "src/mod.py",
                    10,
                    0,
                    1000,
                    pickle.dumps((42,)),
                ),
            ],
        )
        functions = [
            TracedFunction(
                function_name="compute",
                file_name=Path("src/mod.py"),
                module_name="src.mod",
            ),
        ]

        result = create_trace_replay_test(db_path, functions)

        assert "def test_" in result
        assert "compute" in result
        assert "import pickle" in result

    def test_generates_test_for_class_method(self, tmp_path: Path) -> None:
        """Generates appropriate test code for an instance method."""
        db_path = create_trace_db(
            tmp_path / "trace.db",
            [
                (
                    "call",
                    "process",
                    "MyClass",
                    "src/mod.py",
                    10,
                    0,
                    1000,
                    pickle.dumps(("self_placeholder", 42)),
                ),
            ],
        )
        functions = [
            TracedFunction(
                function_name="process",
                file_name=Path("src/mod.py"),
                module_name="src.mod",
                class_name="MyClass",
                method_type="instance",
            ),
        ]

        result = create_trace_replay_test(db_path, functions)

        assert "MyClass" in result
        assert "process" in result

    def test_skips_non_top_level_functions(self, tmp_path: Path) -> None:
        """Non-top-level functions are skipped in test generation."""
        db_path = create_trace_db(tmp_path / "trace.db", [])
        functions = [
            TracedFunction(
                function_name="inner",
                file_name=Path("src/mod.py"),
                module_name="src.mod",
                is_top_level=False,
            ),
        ]

        result = create_trace_replay_test(db_path, functions)

        assert "inner" not in result or "test_inner" not in result

    def test_contains_get_traced_arguments_import(
        self, tmp_path: Path
    ) -> None:
        """Generated code imports get_traced_arguments from _tracing."""
        db_path = create_trace_db(
            tmp_path / "trace.db",
            [
                (
                    "call",
                    "func",
                    None,
                    "mod.py",
                    1,
                    0,
                    1000,
                    pickle.dumps((1,)),
                ),
            ],
        )
        functions = [
            TracedFunction(
                function_name="func",
                file_name=Path("mod.py"),
                module_name="mod",
            ),
        ]

        result = create_trace_replay_test(db_path, functions)

        assert "get_traced_arguments" in result

    def test_contains_trace_file_path(self, tmp_path: Path) -> None:
        """Generated code references the trace file path."""
        db_path = create_trace_db(
            tmp_path / "trace.db",
            [
                (
                    "call",
                    "func",
                    None,
                    "mod.py",
                    1,
                    0,
                    1000,
                    pickle.dumps((1,)),
                ),
            ],
        )
        functions = [
            TracedFunction(
                function_name="func",
                file_name=Path("mod.py"),
                module_name="mod",
            ),
        ]

        result = create_trace_replay_test(db_path, functions)

        assert str(db_path) in result or "trace_file_path" in result

    def test_handles_init_method(self, tmp_path: Path) -> None:
        """__init__ methods are handled specially (self removed)."""
        db_path = create_trace_db(
            tmp_path / "trace.db",
            [
                (
                    "call",
                    "__init__",
                    "MyClass",
                    "src/mod.py",
                    10,
                    0,
                    1000,
                    pickle.dumps(("self_placeholder", 42)),
                ),
            ],
        )
        functions = [
            TracedFunction(
                function_name="__init__",
                file_name=Path("src/mod.py"),
                module_name="src.mod",
                class_name="MyClass",
                method_type="instance",
            ),
        ]

        result = create_trace_replay_test(db_path, functions)

        assert "MyClass" in result

    def test_handles_classmethod(self, tmp_path: Path) -> None:
        """classmethod functions have cls removed from arguments."""
        db_path = create_trace_db(
            tmp_path / "trace.db",
            [
                (
                    "call",
                    "from_config",
                    "MyClass",
                    "src/mod.py",
                    10,
                    0,
                    1000,
                    pickle.dumps(("cls_placeholder", "config")),
                ),
            ],
        )
        functions = [
            TracedFunction(
                function_name="from_config",
                file_name=Path("src/mod.py"),
                module_name="src.mod",
                class_name="MyClass",
                method_type="classmethod",
            ),
        ]

        result = create_trace_replay_test(db_path, functions)

        assert "MyClass" in result
        assert "from_config" in result

    def test_handles_staticmethod(self, tmp_path: Path) -> None:
        """staticmethod functions are called on the class."""
        db_path = create_trace_db(
            tmp_path / "trace.db",
            [
                (
                    "call",
                    "validate",
                    "MyClass",
                    "src/mod.py",
                    10,
                    0,
                    1000,
                    pickle.dumps((42,)),
                ),
            ],
        )
        functions = [
            TracedFunction(
                function_name="validate",
                file_name=Path("src/mod.py"),
                module_name="src.mod",
                class_name="MyClass",
                method_type="staticmethod",
            ),
        ]

        result = create_trace_replay_test(db_path, functions)

        assert "MyClass" in result
        assert "validate" in result

    def test_generates_valid_python(self, tmp_path: Path) -> None:
        """Generated code is syntactically valid Python."""
        db_path = create_trace_db(
            tmp_path / "trace.db",
            [
                (
                    "call",
                    "compute",
                    None,
                    "src/mod.py",
                    10,
                    0,
                    1000,
                    pickle.dumps((42,)),
                ),
            ],
        )
        functions = [
            TracedFunction(
                function_name="compute",
                file_name=Path("src/mod.py"),
                module_name="src.mod",
            ),
        ]

        result = create_trace_replay_test(db_path, functions)

        compile(result, "<test>", "exec")

    def test_respects_max_run_count(self, tmp_path: Path) -> None:
        """max_run_count parameter limits iterations in generated test."""
        db_path = create_trace_db(
            tmp_path / "trace.db",
            [
                (
                    "call",
                    "func",
                    None,
                    "mod.py",
                    1,
                    0,
                    1000,
                    pickle.dumps((1,)),
                ),
            ],
        )
        functions = [
            TracedFunction(
                function_name="func",
                file_name=Path("mod.py"),
                module_name="mod",
            ),
        ]

        result = create_trace_replay_test(db_path, functions, max_run_count=50)

        assert "50" in result

    def test_multiple_functions(self, tmp_path: Path) -> None:
        """Generates tests for multiple functions."""
        db_path = create_trace_db(
            tmp_path / "trace.db",
            [
                (
                    "call",
                    "alpha",
                    None,
                    "mod.py",
                    1,
                    0,
                    1000,
                    pickle.dumps((1,)),
                ),
                (
                    "call",
                    "beta",
                    None,
                    "mod.py",
                    5,
                    0,
                    2000,
                    pickle.dumps((2,)),
                ),
            ],
        )
        functions = [
            TracedFunction(
                function_name="alpha",
                file_name=Path("mod.py"),
                module_name="mod",
            ),
            TracedFunction(
                function_name="beta",
                file_name=Path("mod.py"),
                module_name="mod",
            ),
        ]

        result = create_trace_replay_test(db_path, functions)

        assert "alpha" in result
        assert "beta" in result


_REPO_ROOT = Path(__file__).resolve().parent.parent


class TestTracer:
    """Tracer captures function calls and profiling data."""

    def test_creates_expected_tables(self, tmp_path: Path) -> None:
        """Trace DB has function_calls, pstats, metadata, total_time."""
        output = tmp_path / "test.trace"

        def target(x: int) -> int:
            return x * 2

        tracer = Tracer(
            project_root=_REPO_ROOT,
            module_root=_REPO_ROOT,
            tests_root=tmp_path / "nonexistent",
            output_file=output,
            file_filter=lambda _: True,
        )
        with tracer:
            target(42)

        conn = sqlite3.connect(str(output))
        tables = {
            row[0]
            for row in conn.execute(
                "SELECT name FROM sqlite_master WHERE type='table'"
            )
        }
        conn.close()
        assert "function_calls" in tables
        assert "pstats" in tables
        assert "metadata" in tables
        assert "total_time" in tables

    def test_captures_function_call(self, tmp_path: Path) -> None:
        """Records a traced function call in the function_calls table."""
        output = tmp_path / "test.trace"

        def target(x: int) -> int:
            return x * 2

        tracer = Tracer(
            project_root=_REPO_ROOT,
            module_root=_REPO_ROOT,
            tests_root=tmp_path / "nonexistent",
            output_file=output,
            file_filter=lambda _: True,
            functions=["target"],
        )
        with tracer:
            target(42)

        conn = sqlite3.connect(str(output))
        rows = conn.execute("SELECT function FROM function_calls").fetchall()
        conn.close()

        func_names = [r[0] for r in rows]
        assert "target" in func_names

    def test_captures_pstats_data(self, tmp_path: Path) -> None:
        """Records profiling data in the pstats table."""
        output = tmp_path / "test.trace"

        def target(x: int) -> int:
            return x * 2

        tracer = Tracer(
            project_root=_REPO_ROOT,
            module_root=_REPO_ROOT,
            tests_root=tmp_path / "nonexistent",
            output_file=output,
            file_filter=lambda _: True,
        )
        with tracer:
            target(42)

        conn = sqlite3.connect(str(output))
        count = conn.execute("SELECT COUNT(*) FROM pstats").fetchone()[0]
        conn.close()

        assert count > 0

    def test_records_total_time(self, tmp_path: Path) -> None:
        """Records a total runtime value in the total_time table."""
        output = tmp_path / "test.trace"

        def target(x: int) -> int:
            return x * 2

        tracer = Tracer(
            project_root=_REPO_ROOT,
            module_root=_REPO_ROOT,
            tests_root=tmp_path / "nonexistent",
            output_file=output,
            file_filter=lambda _: True,
        )
        with tracer:
            target(42)

        conn = sqlite3.connect(str(output))
        rows = conn.execute("SELECT time_ns FROM total_time").fetchall()
        conn.close()

        assert len(rows) > 0
        assert rows[0][0] > 0

    def test_traced_functions_property(self, tmp_path: Path) -> None:
        """traced_functions returns a list of discovered TracedFunction."""
        output = tmp_path / "test.trace"

        def target(x: int) -> int:
            return x * 2

        tracer = Tracer(
            project_root=_REPO_ROOT,
            module_root=_REPO_ROOT,
            tests_root=tmp_path / "nonexistent",
            output_file=output,
            file_filter=lambda _: True,
            functions=["target"],
        )
        with tracer:
            target(42)

        traced = tracer.traced_functions
        assert isinstance(traced, list)
        func_names = [f.function_name for f in traced]
        assert "target" in func_names

    def test_trace_count_property(self, tmp_path: Path) -> None:
        """trace_count reflects the number of captured function calls."""
        output = tmp_path / "test.trace"

        def target(x: int) -> int:
            return x * 2

        tracer = Tracer(
            project_root=_REPO_ROOT,
            module_root=_REPO_ROOT,
            tests_root=tmp_path / "nonexistent",
            output_file=output,
            file_filter=lambda _: True,
            functions=["target"],
        )
        with tracer:
            target(1)
            target(2)
            target(3)

        assert tracer.trace_count >= 3

    def test_respects_max_function_count(self, tmp_path: Path) -> None:
        """Tracer limits the number of distinct functions traced."""
        output = tmp_path / "test.trace"

        def target_a() -> int:
            return 1

        def target_b() -> int:
            return 2

        tracer = Tracer(
            project_root=_REPO_ROOT,
            module_root=_REPO_ROOT,
            tests_root=tmp_path / "nonexistent",
            output_file=output,
            file_filter=lambda _: True,
            max_function_count=1,
        )
        with tracer:
            target_a()
            target_b()

        traced = tracer.traced_functions
        assert len(traced) <= 2

    def test_functions_filter(self, tmp_path: Path) -> None:
        """When functions parameter is set, only named functions are traced."""
        output = tmp_path / "test.trace"

        def wanted(x: int) -> int:
            return x * 2

        def unwanted(x: int) -> int:
            return x + 1

        tracer = Tracer(
            project_root=_REPO_ROOT,
            module_root=_REPO_ROOT,
            tests_root=tmp_path / "nonexistent",
            output_file=output,
            file_filter=lambda _: True,
            functions=["wanted"],
        )
        with tracer:
            wanted(1)
            unwanted(2)

        conn = sqlite3.connect(str(output))
        rows = conn.execute("SELECT function FROM function_calls").fetchall()
        conn.close()

        func_names = [r[0] for r in rows]
        assert "wanted" in func_names
        assert "unwanted" not in func_names

    def test_cleans_up_sys_setprofile(self, tmp_path: Path) -> None:
        """After exiting the context manager, sys.setprofile is cleared."""
        output = tmp_path / "test.trace"

        def target() -> int:
            return 42

        tracer = Tracer(
            project_root=_REPO_ROOT,
            module_root=_REPO_ROOT,
            tests_root=tmp_path / "nonexistent",
            output_file=output,
            file_filter=lambda _: True,
        )
        with tracer:
            target()

        assert sys.getprofile() is None

    def test_metadata_table_populated(self, tmp_path: Path) -> None:
        """The metadata table has at least one row after tracing."""
        output = tmp_path / "test.trace"

        def target() -> int:
            return 42

        tracer = Tracer(
            project_root=_REPO_ROOT,
            module_root=_REPO_ROOT,
            tests_root=tmp_path / "nonexistent",
            output_file=output,
            file_filter=lambda _: True,
        )
        with tracer:
            target()

        conn = sqlite3.connect(str(output))
        count = conn.execute("SELECT COUNT(*) FROM metadata").fetchone()[0]
        conn.close()

        assert count >= 0

    def test_timeout_parameter_accepted(self, tmp_path: Path) -> None:
        """Tracer accepts a timeout parameter without error."""
        output = tmp_path / "test.trace"

        def target() -> int:
            return 42

        tracer = Tracer(
            project_root=_REPO_ROOT,
            module_root=_REPO_ROOT,
            tests_root=tmp_path / "nonexistent",
            output_file=output,
            file_filter=lambda _: True,
            timeout=10.0,
        )
        with tracer:
            target()

        assert output.exists()
