"""Local Playwright test execution for baseline tests.

Generates .py Playwright scripts from BaselineTest objects and runs them
via subprocess. Updates test.status and test.failure_reason in-place.
"""

from __future__ import annotations

import concurrent.futures
import re
import subprocess
import sys
import tempfile
from pathlib import Path

from maeris_mcp.generators.playwright_generator import generate_step_files

from .models import BaselineTest, TestStatus

# ---------------------------------------------------------------------------
# Fragility checks (stability criteria §5)
# ---------------------------------------------------------------------------

_FRAGILE_PATTERNS = [
    re.compile(r"nth-child\s*\("),         # brittle positional selectors
    re.compile(r"nth-of-type\s*\("),
    re.compile(r"time\.sleep\s*\("),        # hardcoded timing hacks
    re.compile(r"asyncio\.sleep\s*\("),
    re.compile(r"sleep\s*\(\s*\d+\s*\)"),
]

_ROBUST_SELECTOR_HINT = re.compile(
    r'data-testid|data-automation-id|aria-label|role=|text='
)


def _has_fragile_patterns(script_content: str) -> bool:
    return any(p.search(script_content) for p in _FRAGILE_PATTERNS)


def _has_robust_selector(script_content: str) -> bool:
    return bool(_ROBUST_SELECTOR_HINT.search(script_content))


# ---------------------------------------------------------------------------
# Script generation
# ---------------------------------------------------------------------------


def generate_scripts(
    tests: list[BaselineTest],
    base_url: str,
    output_dir: Path,
    flow_name: str = "baseline",
) -> None:
    """Write Playwright .py scripts for each test to output_dir.

    Sets test.script_path on success. Marks test as UNSTABLE if the
    generated script contains fragile patterns.
    """
    output_dir.mkdir(parents=True, exist_ok=True)
    base_url = base_url.rstrip("/")

    # Build test_cases in the format playwright_generator expects
    test_cases = []
    for t in tests:
        test_cases.append({
            "name": t.name,
            "description": t.name,
            "steps": t.steps,
            "assertions": t.assertions,
        })

    if not test_cases:
        return

    result = generate_step_files(
        test_cases=test_cases,
        flow_name=flow_name,
        base_url=base_url,
        output_dir=str(output_dir),
    )
    files = result.get("files_generated", [])

    # Map generated files back to BaselineTest objects by slugified name
    from maeris_mcp.generators.playwright_generator import _slugify

    for t in tests:
        slug = _slugify(t.name)
        expected_file = output_dir / f"{slug}.json"
        if expected_file.exists():
            t.script_path = str(expected_file)
        else:
            # Try to find by iterating files
            for f in files:
                if not f.endswith(".json"):
                    continue
                fp = output_dir / f
                if fp.exists() and slug in f:
                    t.script_path = str(fp)
                    break


# ---------------------------------------------------------------------------
# Failure reason extraction
# ---------------------------------------------------------------------------


def _extract_failure_reason(output: str) -> str:
    """Extract concise error from Playwright subprocess stderr/stdout."""
    for line in output.splitlines():
        stripped = line.strip()
        if "Error:" in stripped:
            idx = stripped.find("Error:")
            msg = stripped[idx + len("Error:"):].strip()
            return msg[:120] if msg else stripped[:120]
    # Fallback: last non-empty line
    lines = [l.strip() for l in output.splitlines() if l.strip()]
    return lines[-1][:120] if lines else ""


# ---------------------------------------------------------------------------
# Test execution
# ---------------------------------------------------------------------------


def run_single_test(test: BaselineTest, browser: str = "chromium") -> bool:
    """Run a single Playwright script via subprocess.

    Updates test.status and test.failure_reason in-place.

    Returns:
        True if the test passed, False otherwise.
    """
    if not test.script_path or not Path(test.script_path).exists():
        test.status = TestStatus.FAILING
        test.failure_reason = "script not found"
        return False

    py_exec = sys.executable
    proc = subprocess.run(
        [py_exec, test.script_path, f"--browser={browser}"],
        capture_output=True,
        text=True,
        timeout=120,
    )

    combined = (proc.stdout or "") + "\n" + (proc.stderr or "")

    if proc.returncode == 0:
        test.status = TestStatus.PASSING
        test.failure_reason = ""
        return True
    else:
        test.status = TestStatus.FAILING
        test.failure_reason = _extract_failure_reason(combined)
        return False


def run_tests(
    tests: list[BaselineTest],
    browser: str = "chromium",
    max_workers: int = 4,
) -> None:
    """Run each test's .py script in parallel; update status and failure_reason in-place.

    Each test runs in its own subprocess so there is no shared mutable state
    between workers — ThreadPoolExecutor is safe here.

    Args:
        tests:       Tests to run. Ones already marked UNSTABLE are skipped.
        browser:     Playwright browser engine (chromium / firefox / webkit).
        max_workers: Maximum number of tests to run concurrently.
                     Defaults to 4. Set to 1 to restore serial behaviour.
    """
    active = [t for t in tests if t.status != TestStatus.UNSTABLE]
    if not active:
        return

    def _run_one(test: BaselineTest) -> None:
        try:
            run_single_test(test, browser)
        except subprocess.TimeoutExpired:
            test.status = TestStatus.FAILING
            test.failure_reason = "test timed out after 120s"
        except Exception as exc:
            test.status = TestStatus.FAILING
            test.failure_reason = str(exc)[:120]

    with concurrent.futures.ThreadPoolExecutor(max_workers=max_workers) as executor:
        futures = {executor.submit(_run_one, t): t for t in active}
        for future in concurrent.futures.as_completed(futures):
            # Re-raise unexpected executor-level errors (not test failures).
            future.result()


# ---------------------------------------------------------------------------
# Regenerate a single script after repair
# ---------------------------------------------------------------------------


def regenerate_script(test: BaselineTest, base_url: str, output_dir: Path) -> None:
    """Overwrite a single test's .py script after steps have been repaired."""
    base_url = base_url.rstrip("/")
    from maeris_mcp.generators.playwright_generator import _generate_test_file, _slugify

    slug = _slugify(test.name)
    script_path = output_dir / f"{slug}.py"
    content = _generate_test_file(
        test_case={
            "name": test.name,
            "description": test.name,
            "steps": test.steps,
            "assertions": test.assertions,
        },
        flow_name="baseline",
        base_url=base_url,
        test_number=1,
    )
    script_path.write_text(content, encoding="utf-8")
    test.script_path = str(script_path)

    # Re-check fragility
    if _has_fragile_patterns(content):
        test.status = TestStatus.UNSTABLE
        test.failure_reason = "fragile selector or timing pattern in repaired script"
