"""Test result parsing for the LangGraph agent.

Parses pytest and jest output into structured pass/fail/skip counts
with failure details. Eliminates the agent needing to manually read
raw test output.

Critical gap identified by effectiveness audit — saves 1-2 turns per
test run by providing structured feedback immediately.
"""

from __future__ import annotations

import json
import re
from typing import Optional


# ---------------------------------------------------------------------------
# pytest parser
# ---------------------------------------------------------------------------

_PYTEST_SUMMARY_RE = re.compile(
    r'=+\s+'
    r'(?:(\d+)\s+passed)?'
    r'(?:,?\s*(\d+)\s+failed)?'
    r'(?:,?\s*(\d+)\s+error(?:s)?)?'
    r'(?:,?\s*(\d+)\s+skipped)?'
    r'(?:,?\s*(\d+)\s+warning(?:s)?)?'
    r'(?:,?\s*(\d+)\s+deselected)?'
    r'\s+.*?in\s+([\d.]+)s'
    r'\s*=+'
)

_PYTEST_FAILURE_HEADER_RE = re.compile(
    r'^_{3,}\s+(.+?)\s+_{3,}$',
    re.MULTILINE,
)

_PYTEST_SHORT_SUMMARY_RE = re.compile(
    r'^(?:FAILED|ERROR)\s+([\w/\\.-]+::[\w.]+(?:\[.+?\])?)'
    r'(?:\s*-\s*(.+))?$',
    re.MULTILINE,
)

_PYTEST_FILE_LINE_RE = re.compile(
    r'([\w/\\.-]+\.py):(\d+):\s+(\w+(?:Error|Exception|Warning)\s*:?\s*.+)',
)


def parse_pytest_output(text: str) -> Optional[dict]:
    """Parse pytest output into structured results.

    Returns dict with: framework, passed, failed, skipped, duration_s, failures[]
    """
    # Try to find summary line
    summary_match = _PYTEST_SUMMARY_RE.search(text)
    if not summary_match:
        # Try simpler pattern
        simple = re.search(r'(\d+)\s+passed', text)
        if not simple:
            return None
        passed = int(simple.group(1))
        failed_m = re.search(r'(\d+)\s+failed', text)
        failed = int(failed_m.group(1)) if failed_m else 0
        skipped_m = re.search(r'(\d+)\s+skipped', text)
        skipped = int(skipped_m.group(1)) if skipped_m else 0
        dur_m = re.search(r'in\s+([\d.]+)s', text)
        duration = float(dur_m.group(1)) if dur_m else 0.0
    else:
        passed = int(summary_match.group(1) or 0)
        failed = int(summary_match.group(2) or 0)
        skipped = int(summary_match.group(4) or 0)
        duration = float(summary_match.group(7) or 0)

    # Extract failure details
    failures = []

    # Method 1: SHORT TEST SUMMARY lines
    for match in _PYTEST_SHORT_SUMMARY_RE.finditer(text):
        test_id = match.group(1)
        message = match.group(2) or ""

        # Parse test_id into file and test name
        parts = test_id.split("::")
        file_path = parts[0] if parts else ""
        test_name = parts[-1] if len(parts) > 1 else test_id

        # Try to extract error type from message
        error_type = "AssertionError"
        if message:
            err_match = re.match(r'(\w+(?:Error|Exception))', message)
            if err_match:
                error_type = err_match.group(1)

        failures.append({
            "test": test_name,
            "file": file_path,
            "line": 0,
            "error_type": error_type,
            "message": message[:300],
            "short_traceback": "",
        })

    # Method 2: File:line patterns if no short summary found
    if not failures:
        for match in _PYTEST_FILE_LINE_RE.finditer(text):
            failures.append({
                "test": "",
                "file": match.group(1),
                "line": int(match.group(2)),
                "error_type": "Error",
                "message": match.group(3)[:300],
                "short_traceback": "",
            })

    return {
        "framework": "pytest",
        "passed": passed,
        "failed": failed,
        "skipped": skipped,
        "duration_s": duration,
        "failures": failures,
    }


# ---------------------------------------------------------------------------
# jest parser
# ---------------------------------------------------------------------------

_JEST_SUMMARY_RE = re.compile(
    r'Tests:\s+'
    r'(?:(\d+)\s+failed,?\s*)?'
    r'(?:(\d+)\s+skipped,?\s*)?'
    r'(?:(\d+)\s+passed,?\s*)?'
    r'(\d+)\s+total',
)

_JEST_FAIL_RE = re.compile(
    r'FAIL\s+([\w/\\.-]+)',
    re.MULTILINE,
)

_JEST_DURATION_RE = re.compile(
    r'Time:\s+([\d.]+)\s*s',
)


def parse_jest_output(text: str) -> Optional[dict]:
    """Parse jest output into structured results."""
    summary_match = _JEST_SUMMARY_RE.search(text)
    if not summary_match:
        return None

    failed = int(summary_match.group(1) or 0)
    skipped = int(summary_match.group(2) or 0)
    passed = int(summary_match.group(3) or 0)

    dur_match = _JEST_DURATION_RE.search(text)
    duration = float(dur_match.group(1)) if dur_match else 0.0

    failures = []
    for match in _JEST_FAIL_RE.finditer(text):
        failures.append({
            "test": "",
            "file": match.group(1),
            "line": 0,
            "error_type": "Error",
            "message": "",
            "short_traceback": "",
        })

    return {
        "framework": "jest",
        "passed": passed,
        "failed": failed,
        "skipped": skipped,
        "duration_s": duration,
        "failures": failures,
    }


# ---------------------------------------------------------------------------
# Generic test output parser
# ---------------------------------------------------------------------------

def parse_generic_test_output(text: str) -> Optional[dict]:
    """Parse generic test output looking for pass/fail patterns."""
    passed = len(re.findall(r'\b(?:PASS|PASSED|ok)\b', text, re.IGNORECASE))
    failed = len(re.findall(r'\b(?:FAIL|FAILED|FAILURE)\b', text, re.IGNORECASE))

    if passed == 0 and failed == 0:
        return None

    return {
        "framework": "unknown",
        "passed": passed,
        "failed": failed,
        "skipped": 0,
        "duration_s": 0.0,
        "failures": [],
    }


# ---------------------------------------------------------------------------
# Framework detection
# ---------------------------------------------------------------------------

def detect_test_framework(project_path: str) -> str:
    """Detect the test framework from project files."""
    from pathlib import Path
    root = Path(project_path)

    # Python
    if (root / "pyproject.toml").exists():
        try:
            content = (root / "pyproject.toml").read_text()
            if "[tool.pytest" in content:
                return "pytest"
        except Exception:
            pass
    if (root / "pytest.ini").exists() or (root / "setup.cfg").exists():
        return "pytest"

    # JavaScript
    if (root / "package.json").exists():
        try:
            content = (root / "package.json").read_text()
            if "jest" in content:
                return "jest"
            if "vitest" in content:
                return "vitest"
            if "mocha" in content:
                return "mocha"
        except Exception:
            pass

    # Rust
    if (root / "Cargo.toml").exists():
        return "cargo"

    # Go
    if (root / "go.mod").exists():
        return "go"

    return "unknown"


def build_test_command(
    framework: str,
    test_path: str = "",
    test_filter: str = "",
) -> str:
    """Build the appropriate test command for the framework."""
    if framework == "pytest":
        cmd = "python -m pytest"
        if test_path:
            cmd += f" {test_path}"
        if test_filter:
            cmd += f" -k '{test_filter}'"
        cmd += " -v --tb=short --no-header -q"
        return cmd

    if framework in ("jest", "vitest"):
        cmd = f"npx {framework}"
        if test_path:
            cmd += f" {test_path}"
        if test_filter:
            cmd += f" -t '{test_filter}'"
        cmd += " --no-coverage"
        return cmd

    if framework == "cargo":
        cmd = "cargo test"
        if test_filter:
            cmd += f" {test_filter}"
        cmd += " -- --nocapture"
        return cmd

    if framework == "go":
        path = test_path or "./..."
        cmd = f"go test {path} -v"
        if test_filter:
            cmd += f" -run '{test_filter}'"
        return cmd

    # Fallback
    return f"python -m pytest {test_path} -v --tb=short" if test_path else "python -m pytest -v --tb=short"


# ---------------------------------------------------------------------------
# Main entry point
# ---------------------------------------------------------------------------

def parse_test_results(output: str) -> str:
    """Parse test output and return structured JSON.

    Tries pytest first, then jest, then generic.
    """
    result = parse_pytest_output(output)
    if result is None:
        result = parse_jest_output(output)
    if result is None:
        result = parse_generic_test_output(output)
    if result is None:
        return json.dumps({
            "framework": "unknown",
            "passed": 0,
            "failed": 0,
            "skipped": 0,
            "duration_s": 0.0,
            "failures": [],
            "raw_output_excerpt": output[:1000],
        }, indent=2)

    return json.dumps(result, indent=2)
