"""Behavioral verification and performance measurement."""

from __future__ import annotations

import logging
import re
import reprlib
import sys
from typing import TYPE_CHECKING

from .._model import VerificationType
from ..test_discovery.models import TestType
from ._comparator import comparator
from .models import BehaviorDiff, BehaviorDiffScope

if TYPE_CHECKING:
    from ..testing.models import TestResults

log = logging.getLogger(__name__)

INCREASED_RECURSION_LIMIT = 5000

_reprlib_repr = reprlib.Repr()
_reprlib_repr.maxstring = 1500
_behavior_diff_repr = _reprlib_repr.repr


def safe_repr(obj: object) -> str:
    """Safely get repr, handling objects with corrupted state."""
    try:
        return repr(obj)
    except (AttributeError, TypeError, RecursionError) as exc:
        return f"<repr failed: {type(exc).__name__}: {exc}>"


def shorten_pytest_error(pytest_error_string: str) -> str:
    """Extract only the ``E`` and ``>`` lines from a pytest traceback."""
    return "\n".join(
        re.findall(
            r"^[E>] +(.*)$",
            pytest_error_string,
            re.MULTILINE,
        ),
    )


def _extract_pytest_error(
    failures: dict[str, str] | None,
    qualified_name: str,
) -> str:
    """Look up and shorten a pytest error for *qualified_name*."""
    raw = failures.get(qualified_name, "") if failures else ""
    return shorten_pytest_error(raw) if raw else ""


def compare_test_results(  # noqa: C901
    original_results: TestResults,
    candidate_results: TestResults,
    pass_fail_only: bool = False,  # noqa: FBT001, FBT002
) -> tuple[bool, list[BehaviorDiff]]:
    """Compare original and candidate test results for behavioral equivalence.

    Returns a tuple of (all_match, diffs).  When *pass_fail_only* is True,
    only pass/fail status is compared (return values and stdout are ignored).
    """
    if len(original_results) == 0 or len(candidate_results) == 0:
        return False, []

    original_recursion_limit = sys.getrecursionlimit()
    if original_recursion_limit < INCREASED_RECURSION_LIMIT:
        sys.setrecursionlimit(INCREASED_RECURSION_LIMIT)

    test_ids_superset = (
        original_results.get_all_unique_invocation_loop_ids()
        | candidate_results.get_all_unique_invocation_loop_ids()
    )

    test_diffs: list[BehaviorDiff] = []
    did_all_timeout = True

    for test_id in test_ids_superset:
        original_test_result = (
            original_results.get_by_unique_invocation_loop_id(test_id)
        )
        cdd_test_result = candidate_results.get_by_unique_invocation_loop_id(
            test_id
        )

        # Candidate has extra results not in original — that's ok
        if cdd_test_result is not None and original_test_result is None:
            continue

        # Helper instance-state verification missing in candidate is ok
        if (
            original_test_result is not None
            and original_test_result.verification_type
            and original_test_result.verification_type
            == VerificationType.INIT_STATE_HELPER
            and cdd_test_result is None
        ):
            continue

        if original_test_result is None or cdd_test_result is None:
            continue

        did_all_timeout = did_all_timeout and bool(
            original_test_result.timed_out
        )
        if original_test_result.timed_out:
            continue

        superset_obj = bool(
            original_test_result.verification_type
            and original_test_result.verification_type
            in {
                VerificationType.INIT_STATE_HELPER,
                VerificationType.INIT_STATE_FTO,
            }
        )

        # Gather pytest error messages
        qname = original_test_result.id.test_fn_qualified_name()
        cdd_pytest_error = _extract_pytest_error(
            candidate_results.test_failures,
            qname,
        )
        original_pytest_error = _extract_pytest_error(
            original_results.test_failures,
            qname,
        )

        # Check pass/fail mismatch
        if original_test_result.test_type in {
            TestType.EXISTING_UNIT_TEST,
            TestType.CONCOLIC_COVERAGE_TEST,
            TestType.GENERATED_REGRESSION,
            TestType.REPLAY_TEST,
        } and (cdd_test_result.did_pass != original_test_result.did_pass):
            test_diffs.append(
                BehaviorDiff(
                    scope=BehaviorDiffScope.DID_PASS,
                    original_value=str(original_test_result.did_pass),
                    candidate_value=str(cdd_test_result.did_pass),
                    test_src_code=(
                        original_test_result.id.get_src_code(
                            original_test_result.file_name,
                        )
                    ),
                    candidate_pytest_error=cdd_pytest_error or None,
                    original_pass=original_test_result.did_pass,
                    candidate_pass=cdd_test_result.did_pass,
                    original_pytest_error=(original_pytest_error or None),
                ),
            )
        elif not pass_fail_only and not comparator(
            original_test_result.return_value,
            cdd_test_result.return_value,
            superset_obj=superset_obj,
        ):
            test_diffs.append(
                BehaviorDiff(
                    scope=BehaviorDiffScope.RETURN_VALUE,
                    original_value=_behavior_diff_repr(
                        safe_repr(
                            original_test_result.return_value,
                        ),
                    ),
                    candidate_value=_behavior_diff_repr(
                        safe_repr(
                            cdd_test_result.return_value,
                        ),
                    ),
                    test_src_code=(
                        original_test_result.id.get_src_code(
                            original_test_result.file_name,
                        )
                    ),
                    candidate_pytest_error=cdd_pytest_error or None,
                    original_pass=original_test_result.did_pass,
                    candidate_pass=cdd_test_result.did_pass,
                    original_pytest_error=(original_pytest_error or None),
                ),
            )
            try:
                log.debug(
                    "File Name: %s\n"
                    "Test Type: %s\n"
                    "Verification Type: %s\n"
                    "Invocation ID: %s\n"
                    "Original return value: %r\n"
                    "Candidate return value: %r",
                    original_test_result.file_name,
                    original_test_result.test_type,
                    original_test_result.verification_type,
                    original_test_result.id,
                    original_test_result.return_value,
                    cdd_test_result.return_value,
                )
            except Exception:
                log.exception("Error logging return value mismatch")
        elif (
            not pass_fail_only
            and original_test_result.stdout
            and cdd_test_result.stdout
            and not comparator(
                original_test_result.stdout,
                cdd_test_result.stdout,
            )
        ):
            test_diffs.append(
                BehaviorDiff(
                    scope=BehaviorDiffScope.STDOUT,
                    original_value=str(original_test_result.stdout),
                    candidate_value=str(cdd_test_result.stdout),
                    test_src_code=(
                        original_test_result.id.get_src_code(
                            original_test_result.file_name,
                        )
                    ),
                    candidate_pytest_error=cdd_pytest_error or None,
                    original_pass=original_test_result.did_pass,
                    candidate_pass=cdd_test_result.did_pass,
                    original_pytest_error=(original_pytest_error or None),
                ),
            )

    sys.setrecursionlimit(original_recursion_limit)

    if did_all_timeout:
        return False, test_diffs

    return len(test_diffs) == 0, test_diffs
