"""Tests for the async SQLite data parser."""

from __future__ import annotations

import sqlite3
from pathlib import Path
from unittest.mock import MagicMock

import dill as pickle
import pytest

from codeflash_python.runtime._codeflash_async_decorators import (
    CREATE_TABLE_SQL,
    VerificationType,
)
from codeflash_python.testing._async_data_parser import (
    calculate_async_throughput,
    parse_async_behavior_results,
    parse_async_concurrency_metrics,
)


@pytest.fixture(name="test_config")
def _test_config(tmp_path):
    """Minimal TestConfig mock."""
    cfg = MagicMock()
    cfg.tests_project_rootdir = tmp_path
    cfg.test_framework = "pytest"
    return cfg


@pytest.fixture(name="test_files")
def _test_files(tmp_path):
    """Minimal TestFiles mock."""
    from codeflash_python.test_discovery.models import TestType

    tf = MagicMock()
    tf.get_test_type_by_original_file_path.return_value = (
        TestType.EXISTING_UNIT_TEST
    )
    tf.get_test_type_by_instrumented_file_path.return_value = None
    return tf


def _create_db(
    db_path: Path,
    rows: list[tuple],
) -> None:
    """Create a codeflash_results SQLite DB with the given rows."""
    conn = sqlite3.connect(db_path)
    conn.execute(CREATE_TABLE_SQL)
    for row in rows:
        conn.execute(
            "INSERT INTO codeflash_results VALUES "
            "(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
            row,
        )
    conn.commit()
    conn.close()


class TestParseAsyncBehaviorResults:
    """parse_async_behavior_results reads behavior rows from SQLite."""

    def test_empty_when_file_missing(
        self, tmp_path, test_files, test_config
    ) -> None:
        """Returns empty TestResults when file does not exist."""
        results = parse_async_behavior_results(
            tmp_path / "nonexistent.sqlite",
            test_files,
            test_config,
        )
        assert 0 == len(list(results))

    def test_reads_behavior_rows(
        self, tmp_path, test_files, test_config
    ) -> None:
        """Parses behavior rows into FunctionTestInvocation objects."""
        db_path = tmp_path / "test_module.py"
        db_path.write_text("", encoding="utf-8")
        db_path = tmp_path / "results.sqlite"
        module_path = "test_module"
        pickled = pickle.dumps(((1, 2), {}, 3))

        _create_db(
            db_path,
            [
                (
                    module_path,
                    None,
                    "test_fn",
                    "target_func",
                    1,
                    "0_0",
                    "behavior",
                    1000,
                    None,
                    pickled,
                    VerificationType.FUNCTION_CALL.value,
                    None,
                    None,
                    None,
                    None,
                ),
            ],
        )

        results = parse_async_behavior_results(
            db_path, test_files, test_config
        )
        invocations = list(results)
        assert 1 == len(invocations)
        inv = invocations[0]
        assert "target_func" == inv.id.function_getting_tested
        assert 1000 == inv.runtime
        assert inv.return_value is not None

    def test_skips_non_behavior_rows(
        self, tmp_path, test_files, test_config
    ) -> None:
        """Ignores performance and concurrency rows."""
        db_path = tmp_path / "test_module.py"
        db_path.write_text("", encoding="utf-8")
        db_path = tmp_path / "results.sqlite"

        _create_db(
            db_path,
            [
                (
                    "test_module",
                    None,
                    "test_fn",
                    "target",
                    1,
                    "0_0",
                    "performance",
                    1000,
                    None,
                    None,
                    None,
                    None,
                    None,
                    None,
                    None,
                ),
            ],
        )

        results = parse_async_behavior_results(
            db_path, test_files, test_config
        )
        assert 0 == len(list(results))

    def test_handles_corrupt_db(
        self, tmp_path, test_files, test_config
    ) -> None:
        """Returns empty results for a corrupted database file."""
        db_path = tmp_path / "corrupt.sqlite"
        db_path.write_text("not a sqlite file", encoding="utf-8")
        results = parse_async_behavior_results(
            db_path, test_files, test_config
        )
        assert 0 == len(list(results))


class TestCalculateAsyncThroughput:
    """calculate_async_throughput counts performance rows."""

    def test_returns_zero_when_file_missing(self, tmp_path) -> None:
        """Returns 0 when SQLite file does not exist."""
        result = calculate_async_throughput(
            tmp_path / "nonexistent.sqlite",
            "func",
        )
        assert 0 == result

    def test_counts_performance_rows(self, tmp_path) -> None:
        """Counts rows matching function name and performance mode."""
        db_path = tmp_path / "results.sqlite"
        _create_db(
            db_path,
            [
                (
                    "mod",
                    None,
                    "test_fn",
                    "target",
                    1,
                    "0_0",
                    "performance",
                    1000,
                    None,
                    None,
                    None,
                    None,
                    None,
                    None,
                    None,
                ),
                (
                    "mod",
                    None,
                    "test_fn",
                    "target",
                    1,
                    "0_1",
                    "performance",
                    2000,
                    None,
                    None,
                    None,
                    None,
                    None,
                    None,
                    None,
                ),
                (
                    "mod",
                    None,
                    "test_fn",
                    "other_func",
                    1,
                    "0_0",
                    "performance",
                    3000,
                    None,
                    None,
                    None,
                    None,
                    None,
                    None,
                    None,
                ),
                (
                    "mod",
                    None,
                    "test_fn",
                    "target",
                    1,
                    "0_0",
                    "behavior",
                    4000,
                    None,
                    None,
                    None,
                    None,
                    None,
                    None,
                    None,
                ),
            ],
        )
        result = calculate_async_throughput(db_path, "target")
        assert 2 == result

    def test_returns_zero_for_no_matches(self, tmp_path) -> None:
        """Returns 0 when no rows match the function name."""
        db_path = tmp_path / "results.sqlite"
        _create_db(db_path, [])
        result = calculate_async_throughput(db_path, "nonexistent")
        assert 0 == result


class TestParseAsyncConcurrencyMetrics:
    """parse_async_concurrency_metrics reads concurrency rows."""

    def test_returns_none_when_file_missing(self, tmp_path) -> None:
        """Returns None when SQLite file does not exist."""
        result = parse_async_concurrency_metrics(
            tmp_path / "nonexistent.sqlite",
            "func",
        )
        assert result is None

    def test_parses_concurrency_metrics(self, tmp_path) -> None:
        """Computes averages from multiple concurrency rows."""
        db_path = tmp_path / "results.sqlite"
        _create_db(
            db_path,
            [
                (
                    "mod",
                    None,
                    "test_fn",
                    "target",
                    1,
                    "",
                    "concurrency",
                    0,
                    None,
                    None,
                    None,
                    100_000,
                    50_000,
                    10,
                    None,
                ),
                (
                    "mod",
                    None,
                    "test_fn",
                    "target",
                    1,
                    "",
                    "concurrency",
                    0,
                    None,
                    None,
                    None,
                    200_000,
                    100_000,
                    10,
                    None,
                ),
            ],
        )
        metrics = parse_async_concurrency_metrics(db_path, "target")
        assert metrics is not None
        assert 150_000 == metrics.sequential_time_ns
        assert 75_000 == metrics.concurrent_time_ns
        assert 10 == metrics.concurrency_factor
        assert 2.0 == metrics.concurrency_ratio

    def test_returns_none_for_wrong_function(self, tmp_path) -> None:
        """Returns None when no rows match the function name."""
        db_path = tmp_path / "results.sqlite"
        _create_db(
            db_path,
            [
                (
                    "mod",
                    None,
                    "test_fn",
                    "other_func",
                    1,
                    "",
                    "concurrency",
                    0,
                    None,
                    None,
                    None,
                    100_000,
                    50_000,
                    10,
                    None,
                ),
            ],
        )
        result = parse_async_concurrency_metrics(db_path, "target")
        assert result is None

    def test_handles_zero_concurrent_time(self, tmp_path) -> None:
        """Returns ratio 1.0 when concurrent time is zero."""
        db_path = tmp_path / "results.sqlite"
        _create_db(
            db_path,
            [
                (
                    "mod",
                    None,
                    "test_fn",
                    "target",
                    1,
                    "",
                    "concurrency",
                    0,
                    None,
                    None,
                    None,
                    100_000,
                    0,
                    5,
                    None,
                ),
            ],
        )
        metrics = parse_async_concurrency_metrics(db_path, "target")
        assert metrics is not None
        assert 1.0 == metrics.concurrency_ratio

    def test_corrupt_db_returns_none(self, tmp_path) -> None:
        """Returns None for a corrupted database file."""
        db_path = tmp_path / "corrupt.sqlite"
        db_path.write_text("not a sqlite file", encoding="utf-8")
        result = parse_async_concurrency_metrics(db_path, "target")
        assert result is None


class TestCalculateAsyncThroughputErrorHandling:
    """Error handling in calculate_async_throughput."""

    def test_corrupt_db_returns_zero(self, tmp_path) -> None:
        """Returns 0 for a corrupted database file."""
        db_path = tmp_path / "corrupt.sqlite"
        db_path.write_text("not a sqlite file", encoding="utf-8")
        result = calculate_async_throughput(db_path, "func")
        assert 0 == result


class TestParseAsyncBehaviorEdgeCases:
    """Edge cases for parse_async_behavior_results."""

    def test_init_state_verification_type(
        self, tmp_path, test_files, test_config
    ) -> None:
        """Rows with INIT_STATE_FTO get INIT_STATE_TEST type."""
        db_path = tmp_path / "test_module.py"
        db_path.write_text("", encoding="utf-8")
        db_path = tmp_path / "results.sqlite"

        _create_db(
            db_path,
            [
                (
                    "test_module",
                    None,
                    "test_fn",
                    "target",
                    1,
                    "0_0",
                    "behavior",
                    1000,
                    None,
                    pickle.dumps(((1,), {}, 2)),
                    VerificationType.INIT_STATE_FTO.value,
                    None,
                    None,
                    None,
                    None,
                ),
            ],
        )

        results = parse_async_behavior_results(
            db_path, test_files, test_config
        )
        invocations = list(results)
        assert 1 == len(invocations)

    def test_test_type_fallback_to_instrumented(
        self, tmp_path, test_config
    ) -> None:
        """Falls back to instrumented file path for test type lookup."""
        from codeflash_python.test_discovery.models import TestType

        db_path = tmp_path / "test_module.py"
        db_path.write_text("", encoding="utf-8")
        db_path = tmp_path / "results.sqlite"

        tf = MagicMock()
        tf.get_test_type_by_original_file_path.return_value = None
        tf.get_test_type_by_instrumented_file_path.return_value = (
            TestType.EXISTING_UNIT_TEST
        )

        _create_db(
            db_path,
            [
                (
                    "test_module",
                    None,
                    "test_fn",
                    "target",
                    1,
                    "0_0",
                    "behavior",
                    1000,
                    None,
                    pickle.dumps(((1,), {}, 2)),
                    VerificationType.FUNCTION_CALL.value,
                    None,
                    None,
                    None,
                    None,
                ),
            ],
        )

        results = parse_async_behavior_results(db_path, tf, test_config)
        assert 1 == len(list(results))

    def test_test_type_not_found_skips_row(
        self, tmp_path, test_config
    ) -> None:
        """Skips rows when test type cannot be determined."""
        db_path = tmp_path / "test_module.py"
        db_path.write_text("", encoding="utf-8")
        db_path = tmp_path / "results.sqlite"

        tf = MagicMock()
        tf.get_test_type_by_original_file_path.return_value = None
        tf.get_test_type_by_instrumented_file_path.return_value = None

        _create_db(
            db_path,
            [
                (
                    "test_module",
                    None,
                    "test_fn",
                    "target",
                    1,
                    "0_0",
                    "behavior",
                    1000,
                    None,
                    pickle.dumps(((1,), {}, 2)),
                    VerificationType.FUNCTION_CALL.value,
                    None,
                    None,
                    None,
                    None,
                ),
            ],
        )

        results = parse_async_behavior_results(db_path, tf, test_config)
        assert 0 == len(list(results))

    def test_bad_pickle_data_skips_row(
        self, tmp_path, test_files, test_config
    ) -> None:
        """Skips rows with unpicklable return value data."""
        db_path = tmp_path / "test_module.py"
        db_path.write_text("", encoding="utf-8")
        db_path = tmp_path / "results.sqlite"

        _create_db(
            db_path,
            [
                (
                    "test_module",
                    None,
                    "test_fn",
                    "target",
                    1,
                    "0_0",
                    "behavior",
                    1000,
                    None,
                    b"definitely not valid pickle",
                    VerificationType.FUNCTION_CALL.value,
                    None,
                    None,
                    None,
                    None,
                ),
            ],
        )

        results = parse_async_behavior_results(
            db_path, test_files, test_config
        )
        assert 0 == len(list(results))

    def test_outer_exception_handler(self, tmp_path, test_config) -> None:
        """Outer exception handler catches errors from inner processing."""
        db_path = tmp_path / "test_module.py"
        db_path.write_text("", encoding="utf-8")
        db_path = tmp_path / "results.sqlite"

        tf = MagicMock()
        tf.get_test_type_by_original_file_path.side_effect = RuntimeError(
            "boom"
        )

        _create_db(
            db_path,
            [
                (
                    "test_module",
                    None,
                    "test_fn",
                    "target",
                    1,
                    "0_0",
                    "behavior",
                    1000,
                    None,
                    None,
                    VerificationType.FUNCTION_CALL.value,
                    None,
                    None,
                    None,
                    None,
                ),
            ],
        )

        results = parse_async_behavior_results(db_path, tf, test_config)
        assert 0 == len(list(results))
