"""SQLite and binary pickle test result parsing."""

from __future__ import annotations

import logging
import sqlite3
from typing import TYPE_CHECKING

from .._model import VerificationType
from ..test_discovery.models import TestType
from ._path_resolution import file_path_from_module_name
from .models import FunctionTestInvocation, InvocationId, TestResults

if TYPE_CHECKING:
    from pathlib import Path

    from .models import TestConfig, TestFiles

log = logging.getLogger(__name__)


def parse_sqlite_test_results(
    sqlite_file_path: Path,
    test_files: TestFiles,
    test_config: TestConfig,
) -> TestResults:
    """Parse test results from a SQLite database."""
    test_results = TestResults()
    if not sqlite_file_path.exists():
        log.warning(
            "No test results for %s found.",
            sqlite_file_path,
        )
        return test_results

    db: sqlite3.Connection | None = None
    try:
        db = sqlite3.connect(sqlite_file_path)
        cur = db.cursor()
        data = cur.execute(
            "SELECT test_module_path, test_class_name,"
            " test_function_name,"
            " function_getting_tested, loop_index,"
            " iteration_id, runtime,"
            " return_value, verification_type"
            " FROM test_results"
        ).fetchall()
    except sqlite3.Error:
        log.warning(
            "Failed to read test results from %s.",
            sqlite_file_path,
            exc_info=True,
        )
        if db is not None:
            db.close()
        return test_results
    except OSError:
        log.warning(
            "Failed to open %s.",
            sqlite_file_path,
            exc_info=True,
        )
        return test_results
    finally:
        if db is not None:
            db.close()

    for val in data:
        _process_sqlite_row(val, test_files, test_config, test_results)

    return test_results


def _process_sqlite_row(
    val: tuple[object, ...],
    test_files: TestFiles,
    test_config: TestConfig,
    test_results: TestResults,
) -> None:
    """Process a single row from the sqlite table."""
    try:
        _process_sqlite_row_inner(val, test_files, test_config, test_results)
    except Exception:
        log.exception("Failed to parse sqlite test result")


def _process_sqlite_row_inner(
    val: tuple[object, ...],
    test_files: TestFiles,
    test_config: TestConfig,
    test_results: TestResults,
) -> None:
    """Inner processing for a single sqlite row."""
    test_module_path = val[0]
    test_class_name = val[1] or None
    test_function_name = val[2] or None
    function_getting_tested = val[3]
    loop_index = val[4]
    iteration_id = val[5]
    runtime = val[6]
    verification_type = val[8]

    test_file_path = file_path_from_module_name(
        test_module_path,  # type: ignore[arg-type]
        test_config.tests_project_rootdir,
    )

    if verification_type in {
        VerificationType.INIT_STATE_FTO,
        VerificationType.INIT_STATE_HELPER,
    }:
        test_type: TestType = TestType.INIT_STATE_TEST
    else:
        found = test_files.get_test_type_by_original_file_path(
            test_file_path,
        )
        if found is None:
            found = test_files.get_test_type_by_instrumented_file_path(
                test_file_path,
            )
        if found is None:
            log.debug(
                "Skipping result for %s: could not determine test type",
                test_function_name,
            )
            return
        test_type = found

    ret_val = None
    if loop_index == 1 and val[7]:
        import dill as pickle  # noqa: PLC0415

        try:
            ret_val = (pickle.loads(val[7]),)  # noqa: S301
        except (pickle.UnpicklingError, EOFError, ValueError, TypeError):
            log.warning(
                "Failed to deserialize return value for %s",
                test_function_name,
                exc_info=True,
            )
            return

    test_results.add(
        FunctionTestInvocation(
            loop_index=loop_index,  # type: ignore[arg-type]
            id=InvocationId(
                test_module_path=test_module_path,  # type: ignore[arg-type]
                test_class_name=test_class_name,  # type: ignore[arg-type]
                test_function_name=test_function_name,  # type: ignore[arg-type]
                function_getting_tested=function_getting_tested,  # type: ignore[arg-type]
                iteration_id=iteration_id,  # type: ignore[arg-type]
            ),
            file_name=test_file_path,
            did_pass=True,
            runtime=runtime,  # type: ignore[arg-type]
            test_framework=test_config.test_framework,
            test_type=test_type,
            return_value=ret_val,
            timed_out=False,
            verification_type=(
                VerificationType(verification_type)
                if verification_type
                else None
            ),
        ),
    )


def parse_test_return_values_bin(
    file_location: Path,
    test_files: TestFiles,
    test_config: TestConfig,
) -> TestResults:
    """Parse test results from a binary pickle file."""
    import dill as pickle  # noqa: PLC0415

    test_results = TestResults()
    if not file_location.exists():
        log.debug("No test results for %s found.", file_location)
        return test_results

    with file_location.open("rb") as fh:
        try:
            while True:
                len_next_bytes = fh.read(4)
                if not len_next_bytes:
                    break
                len_next = int.from_bytes(len_next_bytes, byteorder="big")
                encoded_test_bytes = fh.read(len_next)
                encoded_test_name = encoded_test_bytes.decode("ascii")
                duration_bytes = fh.read(8)
                duration = int.from_bytes(duration_bytes, byteorder="big")
                len_next_bytes = fh.read(4)
                len_next = int.from_bytes(len_next_bytes, byteorder="big")
                test_pickle_bin = fh.read(len_next)
                loop_index_bytes = fh.read(8)
                loop_index = int.from_bytes(loop_index_bytes, byteorder="big")
                len_next_bytes = fh.read(4)
                len_next = int.from_bytes(len_next_bytes, byteorder="big")
                invocation_id_bytes = fh.read(len_next)
                invocation_id = invocation_id_bytes.decode("ascii")

                invocation_id_object = InvocationId.from_str_id(
                    encoded_test_name, invocation_id
                )
                test_file_path = file_path_from_module_name(
                    invocation_id_object.test_module_path,
                    test_config.tests_project_rootdir,
                )
                test_type = test_files.get_test_type_by_instrumented_file_path(
                    test_file_path,
                )

                try:
                    test_pickle = (
                        pickle.loads(  # noqa: S301
                            test_pickle_bin,
                        )
                        if loop_index == 1
                        else None
                    )
                except Exception:  # noqa: BLE001
                    log.debug(
                        "Failed to deserialize pickle for %s",
                        encoded_test_name,
                        exc_info=True,
                    )
                    continue

                if test_type is None:
                    log.debug(
                        "Test type not found for %s, skipping.",
                        test_file_path,
                    )
                    continue

                test_results.add(
                    FunctionTestInvocation(
                        loop_index=loop_index,
                        id=invocation_id_object,
                        file_name=test_file_path,
                        did_pass=True,
                        runtime=duration,
                        test_framework=(test_config.test_framework),
                        test_type=test_type,
                        return_value=test_pickle,
                        timed_out=False,
                        verification_type=(VerificationType.FUNCTION_CALL),
                    ),
                )
        except Exception:  # noqa: BLE001
            log.warning(
                "Failed to parse test results from %s.",
                file_location,
                exc_info=True,
            )

    return test_results
