from __future__ import annotations

import sqlite3
from pathlib import Path

import pytest

from codeflash_python.test_discovery.models import TestType
from codeflash_python.testing._result_merger import (
    _merge_by_index,
    _merge_by_iteration_id,
    _merge_single_xml,
    merge_test_results,
)
from codeflash_python.testing.models import (
    FunctionTestInvocation,
    InvocationId,
    TestConfig,
    TestFile,
    TestFiles,
    TestResults,
)


def _make_inv_id(
    *,
    module: str = "tests.test_example",
    cls: str | None = "TestExample",
    func: str | None = "test_run",
    target: str = "run",
    iteration: str | None = "0",
) -> InvocationId:
    return InvocationId(
        test_module_path=module,
        test_class_name=cls,
        test_function_name=func,
        function_getting_tested=target,
        iteration_id=iteration,
    )


def _make_invocation(  # noqa: PLR0913
    *,
    loop_index: int = 1,
    inv_id: InvocationId | None = None,
    did_pass: bool = True,
    runtime: int | None = 500,
    cpu_runtime: int = 400,
    test_type: TestType = TestType.EXISTING_UNIT_TEST,
    timed_out: bool = False,
) -> FunctionTestInvocation:
    return FunctionTestInvocation(
        loop_index=loop_index,
        id=inv_id or _make_inv_id(),
        file_name=Path("tests/test_example.py"),
        did_pass=did_pass,
        runtime=runtime,
        test_framework="pytest",
        test_type=test_type,
        return_value=None,
        cpu_runtime=cpu_runtime,
        timed_out=timed_out,
    )


class TestFunctionTestInvocationCpuRuntime:
    """cpu_runtime field on FunctionTestInvocation."""

    def test_cpu_runtime_required(self) -> None:
        with pytest.raises(TypeError):
            FunctionTestInvocation(
                loop_index=1,
                id=_make_inv_id(),
                file_name=Path("tests/test_example.py"),
                did_pass=True,
                runtime=100,
                test_framework="pytest",
                test_type=TestType.EXISTING_UNIT_TEST,
                return_value=None,
                # cpu_runtime omitted
                timed_out=False,
            )

    def test_cpu_runtime_zero_is_valid(self) -> None:
        inv = _make_invocation(cpu_runtime=0)
        assert 0 == inv.cpu_runtime

    def test_cpu_runtime_large_value(self) -> None:
        inv = _make_invocation(cpu_runtime=5_000_000_000)
        assert 5_000_000_000 == inv.cpu_runtime


class TestMergeSingleXmlCpuRuntime:
    """_merge_single_xml preserves data_result's cpu_runtime."""

    def test_data_cpu_runtime_flows_through(self) -> None:
        xml_result = _make_invocation(
            cpu_runtime=0,
            runtime=None,
            inv_id=_make_inv_id(iteration=None),
        )
        data_results = TestResults()
        data_results.add(
            _make_invocation(cpu_runtime=12345, runtime=800),
        )
        merged = TestResults()

        _merge_single_xml(xml_result, data_results, merged)

        assert 1 == len(merged)
        assert 12345 == merged[0].cpu_runtime

    def test_data_zero_cpu_runtime_preserved(self) -> None:
        xml_result = _make_invocation(
            cpu_runtime=99999,
            runtime=None,
            inv_id=_make_inv_id(iteration=None),
        )
        data_results = TestResults()
        data_results.add(
            _make_invocation(cpu_runtime=0, runtime=500),
        )
        merged = TestResults()

        _merge_single_xml(xml_result, data_results, merged)

        assert 1 == len(merged)
        assert 0 == merged[0].cpu_runtime


class TestMergeByIterationIdCpuRuntime:
    """_merge_by_iteration_id preserves data_result's cpu_runtime."""

    def test_data_cpu_runtime_flows_through(self) -> None:
        inv_id = _make_inv_id(iteration="7")

        xml_results = TestResults()
        xml_results.add(
            _make_invocation(cpu_runtime=0, runtime=None, inv_id=inv_id),
        )

        data_results = TestResults()
        data_results.add(
            _make_invocation(cpu_runtime=77777, runtime=600, inv_id=inv_id),
        )

        merged = TestResults()
        _merge_by_iteration_id(xml_results, data_results, merged)

        assert 1 == len(merged)
        assert 77777 == merged[0].cpu_runtime


class TestMergeByIndexCpuRuntime:
    """_merge_by_index preserves data_result's cpu_runtime."""

    def test_data_cpu_runtime_flows_through(self) -> None:
        xml_results = TestResults()
        xml_results.add(
            _make_invocation(
                cpu_runtime=0,
                runtime=None,
                inv_id=_make_inv_id(iteration="a"),
            ),
        )
        xml_results.add(
            _make_invocation(
                cpu_runtime=0,
                runtime=None,
                inv_id=_make_inv_id(iteration="b"),
            ),
        )

        data_results = TestResults()
        data_results.add(
            _make_invocation(
                cpu_runtime=111,
                runtime=300,
                inv_id=_make_inv_id(iteration="a"),
            ),
        )
        data_results.add(
            _make_invocation(
                cpu_runtime=222,
                runtime=400,
                inv_id=_make_inv_id(iteration="b"),
            ),
        )

        merged = TestResults()
        _merge_by_index(xml_results, data_results, merged)

        assert 2 == len(merged)
        assert 111 == merged[0].cpu_runtime
        assert 222 == merged[1].cpu_runtime


class TestSqliteParserCpuRuntime:
    """parse_sqlite_test_results extracts cpu_runtime from the database."""

    def test_cpu_runtime_from_sqlite(self, tmp_path: Path) -> None:
        from codeflash_python.testing._data_parsers import (
            parse_sqlite_test_results,
        )

        test_file = tmp_path / "tests" / "test_example.py"
        test_file.parent.mkdir(parents=True)
        test_file.write_text("def test_run(): pass\n")

        db_path = tmp_path / "results.sqlite"
        db = sqlite3.connect(db_path)
        db.execute(
            "CREATE TABLE test_results ("
            " test_module_path TEXT,"
            " test_class_name TEXT,"
            " test_function_name TEXT,"
            " function_getting_tested TEXT,"
            " loop_index INTEGER,"
            " iteration_id TEXT,"
            " runtime INTEGER,"
            " return_value BLOB,"
            " verification_type TEXT,"
            " cpu_runtime INTEGER"
            ")"
        )
        db.execute(
            "INSERT INTO test_results VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
            (
                "tests.test_example",
                None,
                "test_run",
                "run",
                1,
                "0",
                500,
                None,
                "function_call",
                98765,
            ),
        )
        db.commit()
        db.close()

        test_files = TestFiles(
            test_files=[
                TestFile(
                    original_file_path=test_file,
                    test_type=TestType.EXISTING_UNIT_TEST,
                ),
            ],
        )
        test_config = TestConfig(tests_project_rootdir=tmp_path)

        results = parse_sqlite_test_results(db_path, test_files, test_config)

        assert 1 == len(results)
        assert 98765 == results[0].cpu_runtime


class TestEndToEndCpuRuntime:
    """Full pipeline: SQLite -> parse -> merge with XML -> verify cpu_runtime."""

    def test_sqlite_cpu_runtime_survives_xml_merge(
        self,
        tmp_path: Path,
    ) -> None:
        from codeflash_python.testing._data_parsers import (
            parse_sqlite_test_results,
        )

        test_file = tmp_path / "tests" / "test_example.py"
        test_file.parent.mkdir(parents=True)
        test_file.write_text("def test_run(): pass\n")

        db_path = tmp_path / "results.sqlite"
        db = sqlite3.connect(db_path)
        db.execute(
            "CREATE TABLE test_results ("
            " test_module_path TEXT,"
            " test_class_name TEXT,"
            " test_function_name TEXT,"
            " function_getting_tested TEXT,"
            " loop_index INTEGER,"
            " iteration_id TEXT,"
            " runtime INTEGER,"
            " return_value BLOB,"
            " verification_type TEXT,"
            " cpu_runtime INTEGER"
            ")"
        )
        db.execute(
            "INSERT INTO test_results VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
            (
                "tests.test_example",
                None,
                "test_run",
                "run",
                1,
                "0",
                750,
                None,
                "function_call",
                42000,
            ),
        )
        db.commit()
        db.close()

        test_files = TestFiles(
            test_files=[
                TestFile(
                    original_file_path=test_file,
                    test_type=TestType.EXISTING_UNIT_TEST,
                ),
            ],
        )
        test_config = TestConfig(tests_project_rootdir=tmp_path)

        data_results = parse_sqlite_test_results(
            db_path,
            test_files,
            test_config,
        )
        assert 1 == len(data_results)
        assert 42000 == data_results[0].cpu_runtime

        xml_results = TestResults()
        xml_results.add(
            FunctionTestInvocation(
                loop_index=1,
                id=InvocationId(
                    test_module_path="tests.test_example",
                    test_class_name=None,
                    test_function_name="test_run",
                    function_getting_tested="",
                    iteration_id=None,
                ),
                file_name=test_file,
                did_pass=True,
                runtime=None,
                test_framework="pytest",
                test_type=TestType.EXISTING_UNIT_TEST,
                return_value=None,
                cpu_runtime=0,
                timed_out=False,
            ),
        )

        merged = merge_test_results(
            xml_test_results=xml_results,
            bin_test_results=data_results,
        )

        assert 1 == len(merged)
        assert 42000 == merged[0].cpu_runtime
