"""Run tests against a file or directory.

Executes pytest or a custom test command and returns structured results.
This is the "Atomic Gene" - deterministic, handles messy subprocess I/O.
"""

import asyncio
import contextlib
import os
import time
from typing import Any

from core.pathways.context import Context
from core.pathways.models.tool import ToolOutput


async def run(
    path: str,
    command: str | None = None,
    timeout: int = 120,
    cwd: str | None = None,
    capture_coverage: bool = False,
    context: Context | None = None,
) -> ToolOutput:
    """Execute tests against a specific file or directory.

    Args:
        path: File or directory to test (e.g., "tests/test_auth.py" or "src/")
        command: Custom test command (default: auto-detect pytest/unittest)
        timeout: Maximum test execution time in seconds (default: 120)
        cwd: Working directory for test execution
        capture_coverage: Include coverage report in output (default: False)
        context: Execution context (injected by framework)

    Returns
    -------
        {
            "success": True/False (tests passed/failed),
            "passed": <number of passed tests>,
            "failed": <number of failed tests>,
            "skipped": <number of skipped tests>,
            "errors": [<list of failure messages with tracebacks>],
            "stdout": <captured stdout>,
            "stderr": <captured stderr>,
            "duration_ms": <execution time>,
            "coverage": <coverage percentage if requested>
        }
    """
    if not path:
        return ToolOutput(
            success=False,
            error="path is required",
            data={"passed": 0, "failed": 0, "skipped": 0, "errors": []},
        )

    # Resolve working directory
    working_dir = cwd
    if working_dir:
        working_dir = os.path.expanduser(os.path.expandvars(working_dir))
    elif context:
        working_dir = context.run.workspace_root

    if working_dir and not os.path.exists(working_dir):
        return ToolOutput(
            success=False,
            error=f"Working directory does not exist: {working_dir}",
            data={"passed": 0, "failed": 0, "skipped": 0, "errors": []},
        )

    # Build test command (use provided command or auto-detect pytest)
    test_cmd = command or _build_pytest_command(path, capture_coverage)

    start_time = time.time()

    try:
        # Run tests asynchronously
        process = await asyncio.create_subprocess_shell(
            test_cmd,
            stdout=asyncio.subprocess.PIPE,
            stderr=asyncio.subprocess.PIPE,
            cwd=working_dir,
            env={**os.environ, "PYTHONDONTWRITEBYTECODE": "1"},
        )

        try:
            stdout, stderr = await asyncio.wait_for(
                process.communicate(),
                timeout=timeout,
            )
        except TimeoutError:
            process.kill()
            await process.wait()
            duration_ms = int((time.time() - start_time) * 1000)
            return ToolOutput(
                success=False,
                error=f"Tests timed out after {timeout}s",
                data={
                    "passed": 0,
                    "failed": 0,
                    "skipped": 0,
                    "errors": [f"Timeout exceeded: {timeout}s"],
                    "stdout": "",
                    "stderr": "",
                    "duration_ms": duration_ms,
                    "coverage": None,
                },
            )

        duration_ms = int((time.time() - start_time) * 1000)
        stdout_str = stdout.decode("utf-8", errors="replace") if stdout else ""
        stderr_str = stderr.decode("utf-8", errors="replace") if stderr else ""

        # Parse test results from output
        results = _parse_test_output(stdout_str, stderr_str)

        # Determine success based on exit code and parsed results
        test_passed = process.returncode == 0

        return ToolOutput(
            success=test_passed,
            error=(None if test_passed else "Tests failed"),
            data={
                "passed": results["passed"],
                "failed": results["failed"],
                "skipped": results["skipped"],
                "errors": results["errors"],
                "stdout": stdout_str,
                "stderr": stderr_str,
                "duration_ms": duration_ms,
                "coverage": results.get("coverage"),
            },
        )

    except FileNotFoundError as e:
        duration_ms = int((time.time() - start_time) * 1000)
        return ToolOutput(
            success=False,
            error=f"Test command not found: {e}",
            data={
                "passed": 0,
                "failed": 0,
                "skipped": 0,
                "errors": [str(e)],
                "stdout": "",
                "stderr": "",
                "duration_ms": duration_ms,
                "coverage": None,
            },
        )
    except Exception as e:
        duration_ms = int((time.time() - start_time) * 1000)
        return ToolOutput(
            success=False,
            error=str(e),
            data={
                "passed": 0,
                "failed": 0,
                "skipped": 0,
                "errors": [str(e)],
                "stdout": "",
                "stderr": "",
                "duration_ms": duration_ms,
                "coverage": None,
            },
        )


def _build_pytest_command(path: str, coverage: bool) -> str:
    """Build pytest command with appropriate flags."""
    cmd_parts = ["python", "-m", "pytest", "-v", "--tb=short"]

    if coverage:
        cmd_parts.extend(["--cov", "--cov-report=term-missing"])

    cmd_parts.append(path)
    return " ".join(cmd_parts)


def _parse_test_output(stdout: str, stderr: str) -> dict[str, Any]:
    """Parse pytest/unittest output for structured results.

    Uses direct string scanning — no regex.  Looks for the well-known
    pytest summary tokens (``N passed``, ``N failed``, ``N skipped``),
    failure/error blocks, and coverage percentage.
    """
    results: dict[str, Any] = {
        "passed": 0,
        "failed": 0,
        "skipped": 0,
        "errors": [],
        "coverage": None,
    }

    combined = stdout + stderr

    # --- counts: "N passed", "N failed", "N skipped" ----------------------
    for word, key in (("passed", "passed"), ("failed", "failed"), ("skipped", "skipped")):
        count = _extract_count_before(combined, word)
        if count is not None:
            results[key] = count

    # --- failure / error blocks -------------------------------------------
    results["errors"] = _extract_failure_blocks(combined)

    # --- coverage: "TOTAL  ...  N%" --------------------------------------
    results["coverage"] = _extract_coverage(combined)

    return results


def _extract_count_before(text: str, word: str) -> int | None:
    """Find the last occurrence of ``<digits> <word>`` and return the int."""
    value: int | None = None
    idx = 0
    while True:
        pos = text.find(word, idx)
        if pos == -1:
            break
        # Walk backwards over optional whitespace
        j = pos - 1
        while j >= 0 and text[j] == " ":
            j -= 1
        if j < 0 or not text[j].isdigit():
            idx = pos + 1
            continue
        # Collect digits
        num_end = j + 1
        while j >= 0 and text[j].isdigit():
            j -= 1
        with contextlib.suppress(ValueError):
            value = int(text[j + 1 : num_end])
        idx = pos + 1
    return value


_STOP_TOKENS = ("PASSED", "FAILED", "ERROR", "=====")


def _extract_failure_blocks(text: str) -> list[str]:
    """Extract FAILED/ERROR test blocks from pytest output."""
    errors: list[str] = []

    for marker in ("FAILED ", "ERROR "):
        idx = 0
        while True:
            pos = text.find(marker, idx)
            if pos == -1:
                break
            # Test name is on the same line
            eol = text.find("\n", pos)
            if eol == -1:
                eol = len(text)
            test_name = text[pos + len(marker) : eol].strip()

            # Traceback runs until the next marker or separator
            block_end = len(text)
            for stop in _STOP_TOKENS:
                candidate = text.find(stop, eol + 1)
                if candidate != -1 and candidate < block_end:
                    block_end = candidate

            traceback = text[eol + 1 : block_end].strip()
            error_msg = f"{test_name}\n{traceback}".strip()
            if error_msg:
                errors.append(error_msg[:2000])

            idx = eol + 1
    return errors


def _extract_coverage(text: str) -> int | None:
    """Parse ``TOTAL  ...  N%`` from coverage output."""
    pos = text.find("TOTAL")
    if pos == -1:
        return None
    eol = text.find("\n", pos)
    if eol == -1:
        eol = len(text)
    line = text[pos:eol]
    pct = line.find("%")
    if pct == -1:
        return None
    j = pct - 1
    while j >= 0 and line[j].isdigit():
        j -= 1
    try:
        return int(line[j + 1 : pct])
    except ValueError:
        return None
