"""Tests for benchmark orchestration (_benchmarking, _benchmark_tracing, model types)."""

from __future__ import annotations

import pickle
import sqlite3
import textwrap
from pathlib import Path

import attrs
import pytest

from codeflash_python._model import FunctionParent, FunctionToOptimize
from codeflash_python.analysis._discovery import (
    FunctionProperties,
    inspect_top_level_functions_or_methods,
)
from codeflash_python.benchmarking._benchmark_tracing import (
    BENCHMARK_TIMINGS_SCHEMA,
    CodeflashTrace,
    codeflash_trace,
)
from codeflash_python.benchmarking._benchmarking import (
    CompareResult,
    add_codeflash_decorator_to_code,
    extract_benchmark_errors,
    fmt_ms,
    get_next_arg_and_return,
    instrument_codeflash_trace_decorator,
    md_bar,
    md_delta,
    md_speedup,
    pct_bar,
    process_benchmark_data,
    validate_and_format_benchmark_table,
)
from codeflash_python.benchmarking.models import (
    BenchmarkKey,
    ProcessedBenchmarkInfo,
    get_function_alias,
    get_unique_test_name,
)


def create_benchmark_db(
    path: Path,
    rows: list[
        tuple[
            str,
            str,
            str,
            str,
            str,
            str,
            int,
            int,
            int,
            bytes | None,
            bytes | None,
        ]
    ],
) -> Path:
    """Create a SQLite benchmark_function_timings database for testing."""
    conn = sqlite3.connect(str(path))
    conn.execute(BENCHMARK_TIMINGS_SCHEMA)
    for row in rows:
        conn.execute(
            "INSERT INTO benchmark_function_timings VALUES "
            "(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
            row,
        )
    conn.commit()
    conn.close()
    return path


class TestBenchmarkKey:
    """Tests for BenchmarkKey."""

    def test_str_representation(self) -> None:
        """__str__ returns module::function format."""
        key = BenchmarkKey(module_path="my.module", function_name="test_func")
        assert "my.module::test_func" == str(key)

    def test_hashable(self) -> None:
        """BenchmarkKey can be used as dict key."""
        k1 = BenchmarkKey(module_path="m", function_name="f")
        k2 = BenchmarkKey(module_path="m", function_name="f")
        d = {k1: 42}
        assert 42 == d[k2]

    def test_equality(self) -> None:
        """Equal keys compare equal."""
        k1 = BenchmarkKey(module_path="m", function_name="f")
        k2 = BenchmarkKey(module_path="m", function_name="f")
        assert k1 == k2

    def test_different_keys_not_equal(self) -> None:
        """Different keys don't compare equal."""
        k1 = BenchmarkKey(module_path="a", function_name="f")
        k2 = BenchmarkKey(module_path="b", function_name="f")
        assert k1 != k2

    def test_frozen(self) -> None:
        """BenchmarkKey is immutable."""
        key = BenchmarkKey(module_path="m", function_name="f")
        with pytest.raises(attrs.exceptions.FrozenInstanceError):
            key.module_path = "other"  # type: ignore[misc]


class TestProcessedBenchmarkInfo:
    """Tests for ProcessedBenchmarkInfo."""

    def test_to_string_empty(self) -> None:
        """to_string with empty details returns empty string."""
        info = ProcessedBenchmarkInfo(benchmark_details=())
        assert "" == info.to_string()

    def test_to_string_with_details(self) -> None:
        """to_string with details produces human-readable output."""
        from codeflash_core import BenchmarkDetail

        detail = BenchmarkDetail(
            benchmark_name="bench.module",
            test_function="test_sort",
            original_timing="100ms",
            expected_new_timing="50ms",
            speedup_percent=50.0,
        )
        info = ProcessedBenchmarkInfo(benchmark_details=(detail,))
        result = info.to_string()
        assert "Benchmark Performance Details:" in result
        assert "bench.module" in result
        assert "test_sort" in result

    def test_frozen(self) -> None:
        """ProcessedBenchmarkInfo is immutable."""
        info = ProcessedBenchmarkInfo(benchmark_details=())
        with pytest.raises(attrs.exceptions.FrozenInstanceError):
            info.benchmark_details = ()  # type: ignore[misc]


class TestFunctionProperties:
    """Tests for FunctionProperties."""

    def test_all_fields_stored(self) -> None:
        """All five fields are stored correctly."""
        fp = FunctionProperties(
            is_top_level=True,
            has_args=True,
            is_staticmethod=False,
            is_classmethod=False,
            staticmethod_class_name=None,
        )
        assert True is fp.is_top_level
        assert True is fp.has_args
        assert False is fp.is_classmethod
        assert False is fp.is_staticmethod
        assert fp.staticmethod_class_name is None

    def test_classmethod_flag(self) -> None:
        """is_classmethod=True is stored correctly."""
        fp = FunctionProperties(
            is_top_level=True,
            has_args=True,
            is_staticmethod=False,
            is_classmethod=True,
            staticmethod_class_name=None,
        )
        assert True is fp.is_classmethod
        assert False is fp.is_staticmethod

    def test_staticmethod_flag(self) -> None:
        """is_staticmethod=True is stored correctly."""
        fp = FunctionProperties(
            is_top_level=True,
            has_args=False,
            is_staticmethod=True,
            is_classmethod=False,
            staticmethod_class_name="MyClass",
        )
        assert False is fp.is_classmethod
        assert True is fp.is_staticmethod
        assert "MyClass" == fp.staticmethod_class_name

    def test_frozen(self) -> None:
        """FunctionProperties is immutable."""
        fp = FunctionProperties(
            is_top_level=True,
            has_args=None,
            is_staticmethod=False,
            is_classmethod=False,
            staticmethod_class_name=None,
        )
        with pytest.raises(attrs.exceptions.FrozenInstanceError):
            fp.is_classmethod = True  # type: ignore[misc]


class TestFmtMs:
    """Tests for fmt_ms."""

    def test_none_returns_dash(self) -> None:
        """None input produces a dash."""
        assert "-" == fmt_ms(None)

    def test_large_value(self) -> None:
        """1.5 billion ns formats with comma separator."""
        assert "1,500" == fmt_ms(1_500_000_000)

    def test_medium_value(self) -> None:
        """150 million ns formats as whole number."""
        assert "150" == fmt_ms(150_000_000)

    def test_small_value(self) -> None:
        """1.5 million ns formats with one decimal."""
        assert "1.5" == fmt_ms(1_500_000)

    def test_tiny_value(self) -> None:
        """500k ns formats with two decimals."""
        assert "0.50" == fmt_ms(500_000)

    def test_zero(self) -> None:
        """Zero nanoseconds formats as 0.00."""
        assert "0.00" == fmt_ms(0)


class TestMdSpeedup:
    """Tests for md_speedup."""

    def test_none_before(self) -> None:
        """None before value returns dash."""
        assert "-" == md_speedup(None, 100)

    def test_none_after(self) -> None:
        """None after value returns dash."""
        assert "-" == md_speedup(100, None)

    def test_zero_after(self) -> None:
        """Zero after value returns dash (division by zero)."""
        assert "-" == md_speedup(100, 0)

    def test_improvement(self) -> None:
        """Improvement (ratio >= 1) shows green emoji."""
        result = md_speedup(200, 100)
        assert "\U0001f7e2" in result
        assert "2.00x" in result

    def test_regression(self) -> None:
        """Regression (ratio < 1) shows red emoji."""
        result = md_speedup(100, 200)
        assert "\U0001f534" in result
        assert "0.50x" in result


class TestMdDelta:
    """Tests for md_delta."""

    def test_none_before(self) -> None:
        """None before returns dash."""
        assert "-" == md_delta(None, 100)

    def test_none_after(self) -> None:
        """None after returns dash."""
        assert "-" == md_delta(100, None)

    def test_improvement(self) -> None:
        """Improvement (after < before) shows negative delta."""
        result = md_delta(2_000_000, 1_000_000)
        assert "ms" in result

    def test_regression(self) -> None:
        """Regression (after > before) shows positive delta."""
        result = md_delta(1_000_000, 2_000_000)
        assert "ms" in result
        assert "+" in result


class TestMdBar:
    """Tests for md_bar."""

    def test_none_before(self) -> None:
        """None before returns dash."""
        assert "-" == md_bar(None, 100)

    def test_none_after(self) -> None:
        """None after returns dash."""
        assert "-" == md_bar(100, None)

    def test_zero_before(self) -> None:
        """Zero before returns dash (division by zero)."""
        assert "-" == md_bar(0, 100)

    def test_improvement(self) -> None:
        """Improvement shows filled bar with positive percentage."""
        result = md_bar(200, 100, width=10)
        assert "\u2588" in result
        assert "+50%" in result

    def test_regression(self) -> None:
        """Regression shows bar with negative percentage."""
        result = md_bar(100, 200, width=10)
        assert "\u2588" in result
        assert "-100%" in result

    def test_no_change(self) -> None:
        """Equal values show empty bar with 0%."""
        result = md_bar(100, 100, width=10)
        assert "+0%" in result


class TestPctBar:
    """Tests for pct_bar."""

    def test_zero_percent(self) -> None:
        """0% renders all light blocks."""
        result = pct_bar(0.0, width=10)
        assert "\u2591" * 10 in result
        assert "0.0%" in result

    def test_hundred_percent(self) -> None:
        """100% renders all filled blocks."""
        result = pct_bar(100.0, width=10)
        assert "\u2588" * 10 in result
        assert "100.0%" in result

    def test_fifty_percent(self) -> None:
        """50% renders half filled, half light."""
        result = pct_bar(50.0, width=10)
        assert "50.0%" in result


class TestAddDecoratorTransformer:
    """Tests for AddDecoratorTransformer CST transformer."""

    def test_adds_decorator_to_target_function(self) -> None:
        """Transformer adds @codeflash_trace to matching function."""
        code = textwrap.dedent("""\
            def target():
                return 42
        """)
        fto = FunctionToOptimize(
            function_name="target",
            file_path=Path("dummy.py"),
            starting_line=1,
            ending_line=2,
        )
        result = add_codeflash_decorator_to_code(code, [fto])
        assert "codeflash_trace" in result
        assert "@codeflash_trace" in result

    def test_adds_decorator_to_class_method(self) -> None:
        """Transformer adds @codeflash_trace to a method inside a class."""
        code = textwrap.dedent("""\
            class MyClass:
                def method(self):
                    return 42
        """)
        fto = FunctionToOptimize(
            function_name="method",
            file_path=Path("dummy.py"),
            parents=(FunctionParent(name="MyClass", type="ClassDef"),),
            starting_line=2,
            ending_line=3,
        )
        result = add_codeflash_decorator_to_code(code, [fto])
        assert "@codeflash_trace" in result

    def test_no_decorator_for_non_target(self) -> None:
        """Functions not in the target list are left unchanged."""
        code = textwrap.dedent("""\
            def other():
                return 42
        """)
        fto = FunctionToOptimize(
            function_name="target",
            file_path=Path("dummy.py"),
            starting_line=1,
            ending_line=2,
        )
        result = add_codeflash_decorator_to_code(code, [fto])
        assert "@codeflash_trace" not in result

    def test_empty_target_list(self) -> None:
        """Empty target list produces no changes."""
        code = textwrap.dedent("""\
            def func():
                return 42
        """)
        result = add_codeflash_decorator_to_code(code, [])
        assert "@codeflash_trace" not in result

    def test_import_added_when_decorator_applied(self) -> None:
        """Import for codeflash_trace is added when decorator is applied."""
        code = textwrap.dedent("""\
            def target():
                return 42
        """)
        fto = FunctionToOptimize(
            function_name="target",
            file_path=Path("dummy.py"),
            starting_line=1,
            ending_line=2,
        )
        result = add_codeflash_decorator_to_code(code, [fto])
        assert "import" in result
        assert "codeflash_trace" in result


class TestExtractBenchmarkErrors:
    """Tests for extract_benchmark_errors."""

    def test_error_collecting(self) -> None:
        """Output with 'ERROR collecting' extracts the ERRORS section."""
        output = (
            "some preamble\n"
            "ERROR collecting something\n"
            "=== ERRORS ===\n"
            "The actual error details\n"
            "=== short test summary ===\n"
        )
        result = extract_benchmark_errors(output)
        assert "The actual error details" in result

    def test_failures_section(self) -> None:
        """Output with 'FAILURES' extracts the FAILURES section."""
        output = (
            "some preamble\n"
            "FAILURES\n"
            "=== FAILURES ===\n"
            "The failure details\n"
            "=== short test summary ===\n"
        )
        result = extract_benchmark_errors(output)
        assert "The failure details" in result

    def test_no_errors(self) -> None:
        """Output with no errors returns the original output."""
        output = "everything passed\nall good"
        result = extract_benchmark_errors(output)
        assert output == result


class TestValidateAndFormatBenchmarkTable:
    """Tests for validate_and_format_benchmark_table."""

    def test_normal_case(self) -> None:
        """Function time < total time produces valid percentages."""
        bk = BenchmarkKey(module_path="bench.mod", function_name="test_fn")
        func_timings = {"mod.func": {bk: 50_000_000}}
        total_timings = {bk: 100_000_000}
        result = validate_and_format_benchmark_table(
            func_timings, total_timings
        )
        assert "mod.func" in result
        entries = result["mod.func"]
        assert 1 == len(entries)
        key, total_ms, func_ms, pct = entries[0]
        assert key == bk
        assert total_ms == pytest.approx(100.0)
        assert func_ms == pytest.approx(50.0)
        assert pct == pytest.approx(50.0)

    def test_func_time_greater_than_total(self) -> None:
        """When func_time > total_time (multithreading), returns zeros."""
        bk = BenchmarkKey(module_path="bench.mod", function_name="test_fn")
        func_timings = {"mod.func": {bk: 200_000_000}}
        total_timings = {bk: 100_000_000}
        result = validate_and_format_benchmark_table(
            func_timings, total_timings
        )
        entries = result["mod.func"]
        _, total_ms, func_ms, pct = entries[0]
        assert 0.0 == total_ms
        assert 0.0 == func_ms
        assert 0.0 == pct

    def test_empty_input(self) -> None:
        """Empty input produces empty output."""
        result = validate_and_format_benchmark_table({}, {})
        assert {} == result


class TestInspectTopLevelFunctionsOrMethods:
    """Tests for inspect_top_level_functions_or_methods."""

    def test_regular_method(self, tmp_path: Path) -> None:
        """Regular method is detected as top-level with no special flags."""
        src = tmp_path / "mod.py"
        src.write_text(
            textwrap.dedent("""\
                class MyClass:
                    def method(self):
                        return 42
            """),
        )
        result = inspect_top_level_functions_or_methods(
            src, "method", class_name="MyClass"
        )
        assert result is not None
        assert True is result.is_top_level
        assert False is result.is_classmethod
        assert False is result.is_staticmethod

    def test_classmethod(self, tmp_path: Path) -> None:
        """@classmethod sets is_classmethod=True."""
        src = tmp_path / "mod.py"
        src.write_text(
            textwrap.dedent("""\
                class MyClass:
                    @classmethod
                    def method(cls):
                        return 42
            """),
        )
        result = inspect_top_level_functions_or_methods(
            src, "method", class_name="MyClass"
        )
        assert result is not None
        assert True is result.is_classmethod
        assert False is result.is_staticmethod

    def test_staticmethod(self, tmp_path: Path) -> None:
        """@staticmethod sets is_staticmethod=True."""
        src = tmp_path / "mod.py"
        src.write_text(
            textwrap.dedent("""\
                class MyClass:
                    @staticmethod
                    def method():
                        return 42
            """),
        )
        result = inspect_top_level_functions_or_methods(
            src, "method", class_name="MyClass"
        )
        assert result is not None
        assert False is result.is_classmethod
        assert True is result.is_staticmethod

    def test_top_level_function(self, tmp_path: Path) -> None:
        """Top-level function is detected with is_top_level=True."""
        src = tmp_path / "mod.py"
        src.write_text(
            textwrap.dedent("""\
                def top_level():
                    return 42
            """),
        )
        result = inspect_top_level_functions_or_methods(
            src, "top_level", class_name=None
        )
        assert result is not None
        assert True is result.is_top_level
        assert False is result.is_classmethod
        assert False is result.is_staticmethod


class TestGetFunctionAlias:
    """Tests for get_function_alias."""

    def test_simple_module(self) -> None:
        """Simple module name produces underscore-joined alias."""
        assert "mymod_func" == get_function_alias("mymod", "func")

    def test_dotted_module(self) -> None:
        """Dotted module name has dots replaced with underscores."""
        assert "my_pkg_mod_func" == get_function_alias("my.pkg.mod", "func")

    def test_single_level(self) -> None:
        """Single-level module stays as-is with function appended."""
        assert "pkg_compute" == get_function_alias("pkg", "compute")


class TestGetUniqueTestName:
    """Tests for get_unique_test_name."""

    def test_without_class(self) -> None:
        """Without class_name, combines module alias and benchmark name."""
        result = get_unique_test_name("my.mod", "func", "test_benchmark_sort")
        assert "my_mod_func" in result
        assert "test_benchmark_sort" in result

    def test_with_class(self) -> None:
        """With class_name, uses class alias instead of function alias."""
        result = get_unique_test_name(
            "my.mod", "method", "test_bm", class_name="MyClass"
        )
        assert "my_mod_MyClass" in result
        assert "method" in result
        assert "test_bm" in result

    def test_special_chars_cleaned(self) -> None:
        """Special characters in benchmark name are replaced."""
        result = get_unique_test_name("mod", "func", "test[param-1]")
        assert "[" not in result
        assert "]" not in result
        assert "-" not in result


class TestInstrumentCodeflashTraceDecorator:
    """Tests for instrument_codeflash_trace_decorator."""

    def test_instruments_file_on_disk(self, tmp_path: Path) -> None:
        """Writes decorated code back to the file."""
        src = tmp_path / "module.py"
        src.write_text(
            textwrap.dedent("""\
                def target():
                    return 42
            """),
        )
        fto = FunctionToOptimize(
            function_name="target",
            file_path=src,
            starting_line=1,
            ending_line=2,
        )
        instrument_codeflash_trace_decorator({src: [fto]})
        modified = src.read_text()
        assert "codeflash_trace" in modified

    def test_skips_benchmarking_submodule(self, tmp_path: Path) -> None:
        """Files under a codeflash/benchmarking path are skipped."""
        bench_dir = tmp_path / "codeflash" / "benchmarking"
        bench_dir.mkdir(parents=True)
        src = bench_dir / "module.py"
        original = textwrap.dedent("""\
            def target():
                return 42
        """)
        src.write_text(original)
        fto = FunctionToOptimize(
            function_name="target",
            file_path=src,
            starting_line=1,
            ending_line=2,
        )
        instrument_codeflash_trace_decorator({src: [fto]})
        assert original == src.read_text()


class TestProcessBenchmarkData:
    """Tests for process_benchmark_data."""

    def test_empty_inputs_returns_none(self) -> None:
        """Empty dictionaries return None."""
        result = process_benchmark_data({}, {}, {})
        assert result is None

    def test_valid_data_returns_info(self) -> None:
        """Valid benchmark data produces ProcessedBenchmarkInfo."""
        bk = BenchmarkKey(module_path="bench.mod", function_name="test_fn")
        replay_gain = {bk: 1.0}
        fto_timings = {bk: 50_000_000}
        total_timings = {bk: 100_000_000}
        result = process_benchmark_data(
            replay_gain, fto_timings, total_timings
        )
        assert result is not None
        assert len(result.benchmark_details) > 0

    def test_zero_total_timing_skipped(self) -> None:
        """Benchmark with zero total timing is skipped."""
        bk = BenchmarkKey(module_path="bench.mod", function_name="test_fn")
        replay_gain = {bk: 1.0}
        fto_timings = {bk: 50_000_000}
        total_timings = {bk: 0}
        result = process_benchmark_data(
            replay_gain, fto_timings, total_timings
        )
        assert result is not None
        assert 0 == len(result.benchmark_details)


class TestCompareResult:
    """Tests for CompareResult."""

    def test_format_markdown_empty(self) -> None:
        """Empty results produce a header but no benchmark tables."""
        cr = CompareResult(base_ref="abc123", head_ref="def456")
        result = cr.format_markdown()
        assert "abc123" in result
        assert "def456" in result

    def test_format_markdown_with_data(self) -> None:
        """Results with data produce markdown with headers and function names."""
        bk = BenchmarkKey(module_path="bench.mod", function_name="test_fn")
        cr = CompareResult(
            base_ref="abc123456789ab",
            head_ref="def456789abcde",
            base_total_ns={bk: 100_000_000},
            head_total_ns={bk: 50_000_000},
        )
        result = cr.format_markdown()
        assert "Benchmark" in result
        assert "abc123456789" in result
        assert "def456789abc" in result

    def test_format_markdown_with_function_breakdown(self) -> None:
        """Results with per-function data include function table."""
        bk = BenchmarkKey(module_path="bench.mod", function_name="test_fn")
        cr = CompareResult(
            base_ref="abc123456789ab",
            head_ref="def456789abcde",
            base_total_ns={bk: 100_000_000},
            head_total_ns={bk: 50_000_000},
            base_function_ns={"mod.compute": {bk: 80_000_000}},
            head_function_ns={"mod.compute": {bk: 40_000_000}},
        )
        result = cr.format_markdown()
        assert "compute" in result
        assert "function" in result


class TestGetNextArgAndReturn:
    """Tests for get_next_arg_and_return."""

    def test_yields_pickled_args(self, tmp_path: Path) -> None:
        """Generator yields (args, kwargs) from the SQLite database."""
        db_path = tmp_path / "trace.db"
        args_blob = pickle.dumps((1, 2, 3))
        kwargs_blob = pickle.dumps({"key": "val"})
        file_path = Path("/src/mod.py").as_posix()
        create_benchmark_db(
            db_path,
            [
                (
                    "func",
                    "",
                    "mod",
                    file_path,
                    "test_bench",
                    "bench.module",
                    10,
                    1000,
                    50,
                    args_blob,
                    kwargs_blob,
                ),
            ],
        )
        results = list(
            get_next_arg_and_return(
                trace_file=str(db_path),
                benchmark_function_name="test_bench",
                function_name="func",
                file_path="/src/mod.py",
            )
        )
        assert 1 == len(results)
        pickled_args, pickled_kwargs = results[0]
        assert (1, 2, 3) == pickle.loads(pickled_args)
        assert {"key": "val"} == pickle.loads(pickled_kwargs)

    def test_with_class_name(self, tmp_path: Path) -> None:
        """Generator filters by class_name when provided."""
        db_path = tmp_path / "trace.db"
        args_blob = pickle.dumps((42,))
        kwargs_blob = pickle.dumps({})
        file_path = Path("/src/mod.py").as_posix()
        create_benchmark_db(
            db_path,
            [
                (
                    "method",
                    "MyClass",
                    "mod",
                    file_path,
                    "test_bench",
                    "bench.module",
                    10,
                    1000,
                    50,
                    args_blob,
                    kwargs_blob,
                ),
                (
                    "method",
                    "",
                    "mod",
                    file_path,
                    "test_bench",
                    "bench.module",
                    10,
                    2000,
                    50,
                    args_blob,
                    kwargs_blob,
                ),
            ],
        )
        results = list(
            get_next_arg_and_return(
                trace_file=str(db_path),
                benchmark_function_name="test_bench",
                function_name="method",
                file_path="/src/mod.py",
                class_name="MyClass",
            )
        )
        assert 1 == len(results)

    def test_empty_db(self, tmp_path: Path) -> None:
        """Empty database yields nothing."""
        db_path = tmp_path / "trace.db"
        create_benchmark_db(db_path, [])
        results = list(
            get_next_arg_and_return(
                trace_file=str(db_path),
                benchmark_function_name="test_bench",
                function_name="func",
                file_path="/src/mod.py",
            )
        )
        assert [] == results


class TestCodeflashTrace:
    """Tests for CodeflashTrace decorator class."""

    def test_construction_defaults(self) -> None:
        """New instance has expected default attributes."""
        trace = CodeflashTrace()
        assert [] == trace.function_calls_data
        assert 0 == trace.function_call_count
        assert 1000 == trace.pickle_count_limit

    def test_setup_creates_table(self, tmp_path: Path) -> None:
        """setup() creates the benchmark_function_timings table."""
        db_path = tmp_path / "trace.db"
        trace = CodeflashTrace()
        trace.setup(str(db_path))
        try:
            conn = sqlite3.connect(str(db_path))
            cursor = conn.execute(
                "SELECT name FROM sqlite_master "
                "WHERE type='table' AND name='benchmark_function_timings'"
            )
            tables = cursor.fetchall()
            conn.close()
            assert 1 == len(tables)
        finally:
            trace.close()

    def test_setup_and_close_cycle(self, tmp_path: Path) -> None:
        """setup() followed by close() completes without error."""
        db_path = tmp_path / "trace.db"
        trace = CodeflashTrace()
        trace.setup(str(db_path))
        trace.close()
        assert trace._connection is None

    def test_singleton_exists(self) -> None:
        """Module-level codeflash_trace singleton is a CodeflashTrace."""
        assert isinstance(codeflash_trace, CodeflashTrace)
