"""Test runner — execute test cases against an MCP server via the simulator."""

from __future__ import annotations

import asyncio
import time
from typing import Any

from hatchdx.harness.assertions import (
    assert_content_contains,
    assert_content_not_contains,
    assert_is_error,
    assert_latency_under,
    assert_no_error,
    assert_schema,
    assert_valid_mcp_response,
)
from hatchdx.harness.models import TestCase, TestExpectation, TestResult
from hatchdx.harness.simulator import SimulatorError, StdioSimulator, ToolCallResult


async def run_all_tests(
    command: list[str],
    test_cases: list[TestCase],
    *,
    timeout: float = 5.0,
) -> list[TestResult]:
    """Start the server, run every test case, and return results.

    The simulator is started once and reused for all test cases.
    """
    async with StdioSimulator(command, timeout=timeout) as sim:
        results: list[TestResult] = []
        for test_case in test_cases:
            result = await _run_single_test(sim, test_case)
            results.append(result)
        return results


async def _run_single_test(
    sim: StdioSimulator,
    test_case: TestCase,
) -> TestResult:
    """Call the tool and evaluate all assertions for a single test case."""
    start = time.perf_counter()
    try:
        tool_result = await sim.call_tool(test_case.tool, test_case.input)
    except SimulatorError as exc:
        elapsed_ms = (time.perf_counter() - start) * 1000
        return TestResult(
            test_case=test_case,
            passed=False,
            actual_response={},
            latency_ms=elapsed_ms,
            failure_reason=f"Tool call failed: {exc}",
        )
    except asyncio.TimeoutError:
        elapsed_ms = (time.perf_counter() - start) * 1000
        return TestResult(
            test_case=test_case,
            passed=False,
            actual_response={},
            latency_ms=elapsed_ms,
            failure_reason="Tool call timed out",
        )
    except Exception as exc:
        elapsed_ms = (time.perf_counter() - start) * 1000
        return TestResult(
            test_case=test_case,
            passed=False,
            actual_response={},
            latency_ms=elapsed_ms,
            failure_reason=f"Unexpected error ({type(exc).__name__}): {exc}",
        )

    elapsed_ms = (time.perf_counter() - start) * 1000

    passed, failure_reason = _evaluate_assertions(
        tool_result, test_case.expect, elapsed_ms
    )

    return TestResult(
        test_case=test_case,
        passed=passed,
        actual_response=_tool_result_to_dict(tool_result),
        latency_ms=elapsed_ms,
        failure_reason=failure_reason,
    )


def _evaluate_assertions(
    result: ToolCallResult,
    expectation: TestExpectation,
    latency_ms: float,
) -> tuple[bool, str | None]:
    """Run all applicable assertions. Returns (passed, first_failure_reason)."""
    failures: list[str] = []

    # Always validate MCP response structure
    passed, reason = assert_valid_mcp_response(result)
    if not passed:
        failures.append(reason)

    # Status assertion
    if expectation.status == "success":
        passed, reason = assert_no_error(result)
        if not passed:
            failures.append(reason)
    elif expectation.status == "error":
        passed, reason = assert_is_error(result)
        if not passed:
            failures.append(reason)

    # Explicit is_error assertion
    if expectation.is_error is True:
        passed, reason = assert_is_error(result)
        if not passed:
            failures.append(reason)
    elif expectation.is_error is False:
        passed, reason = assert_no_error(result)
        if not passed:
            failures.append(reason)

    # Content assertions
    if expectation.content_contains is not None:
        passed, reason = assert_content_contains(result, expectation.content_contains)
        if not passed:
            failures.append(reason)

    if expectation.content_not_contains is not None:
        passed, reason = assert_content_not_contains(
            result, expectation.content_not_contains
        )
        if not passed:
            failures.append(reason)

    # Schema assertion
    if expectation.schema is not None:
        passed, reason = assert_schema(result, expectation.schema)
        if not passed:
            failures.append(reason)

    # Latency assertion
    if expectation.max_latency_ms is not None:
        passed, reason = assert_latency_under(latency_ms, expectation.max_latency_ms)
        if not passed:
            failures.append(reason)

    if failures:
        return False, failures[0]
    return True, None


def _tool_result_to_dict(result: ToolCallResult) -> dict[str, Any]:
    """Convert a ToolCallResult to a plain dict for storage in TestResult."""
    return {
        "content": result.content,
        "isError": result.is_error,
    }
