#!/usr/bin/env python3
"""AI Test Runner — Unified test execution and reporting for LeoCRM.

Runs backend (pytest), frontend (vitest), and E2E (playwright) tests,
produces a structured JSON or CSV report, and exits with code 0 (all pass)
or 1 (any failures).

Usage:
    python scripts/ai_run_tests.py [options]

Options:
    --skip-backend      Skip backend tests
    --skip-frontend    Skip frontend tests
    --skip-e2e         Skip E2E tests (default: skipped unless --run-e2e)
    --run-e2e          Run E2E tests (opt-in)
    --output FORMAT    Output format: json (default) or csv
    --verbose          Show full test output
    --report-file PATH Write report to file instead of stdout
"""

from __future__ import annotations

import argparse
import csv
import io
import json
import os
import subprocess
import sys
import time
from pathlib import Path
from typing import Any

# ─── Constants ───

PROJECT_ROOT = Path(__file__).resolve().parent.parent
FRONTEND_DIR = PROJECT_ROOT / "frontend"
BACKEND_DIR = PROJECT_ROOT

# ─── Data Structures ───


class TestSuiteResult:
    """Result of a single test suite (backend, frontend, e2e)."""

    def __init__(self, suite: str):
        self.suite = suite
        self.total = 0
        self.passed = 0
        self.failed = 0
        self.skipped = 0
        self.duration = 0.0
        self.failures: list[dict[str, str]] = []
        self.errors: list[dict[str, str]] = []
        self.raw_output = ""
        self.exit_code = 0
        self.command = ""

    def to_dict(self) -> dict[str, Any]:
        return {
            "suite": self.suite,
            "total": self.total,
            "passed": self.passed,
            "failed": self.failed,
            "skipped": self.skipped,
            "duration": round(self.duration, 2),
            "exit_code": self.exit_code,
            "command": self.command,
            "failures": self.failures,
            "errors": self.errors,
        }

    @property
    def all_passed(self) -> bool:
        return self.failed == 0 and self.exit_code == 0


# ─── Test Runners ───


def run_backend_tests(verbose: bool = False) -> TestSuiteResult:
    """Run pytest with --tb=short and JSON report."""
    result = TestSuiteResult("backend")
    report_file = BACKEND_DIR / ".test_report_backend.json"

    cmd = [
        sys.executable,
        "-m",
        "pytest",
        "tests/",
        "--tb=short",
        f"--json-report-file={report_file}",
        "-q",
    ]
    if verbose:
        cmd.append("-v")

    result.command = " ".join(cmd)
    start = time.time()

    try:
        proc = subprocess.run(
            cmd,
            cwd=str(BACKEND_DIR),
            capture_output=True,
            text=True,
            timeout=600,
        )
        result.exit_code = proc.returncode
        result.raw_output = proc.stdout + proc.stderr
    except subprocess.TimeoutExpired:
        result.exit_code = 124
        result.raw_output = "Backend tests timed out after 600s"
        result.errors.append({"test": "__timeout__", "error": "Test suite timed out"})
    except FileNotFoundError:
        result.exit_code = 127
        result.raw_output = "pytest not found"
        result.errors.append({"test": "__setup__", "error": "pytest not installed"})

    result.duration = time.time() - start

    # Parse JSON report if available
    if report_file.exists():
        try:
            with open(report_file) as f:
                data = json.load(f)
            result.total = data.get("summary", {}).get("total", 0)
            result.passed = data.get("summary", {}).get("passed", 0)
            result.failed = data.get("summary", {}).get("failed", 0)
            result.skipped = data.get("summary", {}).get("skipped", 0)

            # Extract failure details
            for test in data.get("tests", []):
                if test.get("outcome") == "failed":
                    result.failures.append({
                        "test": test.get("nodeid", "unknown"),
                        "error": test.get("call", {}).get("longrepr", "No error details"),
                    })
        except (json.JSONDecodeError, KeyError):
            pass
        finally:
            report_file.unlink(missing_ok=True)
    else:
        # Fallback: parse from output
        _parse_pytest_output(result)

    return result


def run_frontend_tests(verbose: bool = False) -> TestSuiteResult:
    """Run vitest with JSON output."""
    result = TestSuiteResult("frontend")
    report_file = FRONTEND_DIR / ".test_report_frontend.json"

    cmd = ["npx", "vitest", "run", "src/__tests__/", "--reporter=json"]
    if verbose:
        cmd.append("--reporter=verbose")

    result.command = " ".join(cmd)
    start = time.time()

    try:
        proc = subprocess.run(
            cmd,
            cwd=str(FRONTEND_DIR),
            capture_output=True,
            text=True,
            timeout=300,
        )
        result.exit_code = proc.returncode
        result.raw_output = proc.stdout + proc.stderr
    except subprocess.TimeoutExpired:
        result.exit_code = 124
        result.raw_output = "Frontend tests timed out after 300s"
        result.errors.append({"test": "__timeout__", "error": "Test suite timed out"})
    except FileNotFoundError:
        result.exit_code = 127
        result.raw_output = "npx/vitest not found"
        result.errors.append({"test": "__setup__", "error": "vitest not installed"})

    result.duration = time.time() - start

    # Try to parse JSON from stdout (vitest --reporter=json outputs to stdout)
    _parse_vitest_output(result)

    # Clean up report file if exists
    report_file.unlink(missing_ok=True)

    return result


def run_e2e_tests(verbose: bool = False) -> TestSuiteResult:
    """Run playwright E2E tests."""
    result = TestSuiteResult("e2e")

    cmd = ["npx", "playwright", "test", "--reporter=json"]
    if verbose:
        cmd.append("--reporter=list")

    result.command = " ".join(cmd)
    start = time.time()

    try:
        proc = subprocess.run(
            cmd,
            cwd=str(FRONTEND_DIR),
            capture_output=True,
            text=True,
            timeout=600,
        )
        result.exit_code = proc.returncode
        result.raw_output = proc.stdout + proc.stderr
    except subprocess.TimeoutExpired:
        result.exit_code = 124
        result.raw_output = "E2E tests timed out after 600s"
        result.errors.append({"test": "__timeout__", "error": "Test suite timed out"})
    except FileNotFoundError:
        result.exit_code = 127
        result.raw_output = "playwright not found"
        result.errors.append({"test": "__setup__", "error": "playwright not installed"})

    result.duration = time.time() - start

    # Parse playwright JSON output
    _parse_playwright_output(result)

    return result


# ─── Output Parsers ───


def _parse_pytest_output(result: TestSuiteResult) -> None:
    """Fallback parser for pytest text output."""
    output = result.raw_output
    for line in output.splitlines():
        line = line.strip()
        # Look for summary line like: "5 passed, 2 failed, 1 skipped in 10.5s"
        if "passed" in line or "failed" in line or "skipped" in line:
            parts = line.split()
            for part in parts:
                if part.isdigit():
                    if "passed" in line and "passed" in parts[parts.index(part) + 1:]:
                        result.passed = int(part)
                    elif "failed" in line and "failed" in parts[parts.index(part) + 1:]:
                        result.failed = int(part)
                    elif "skipped" in line and "skipped" in parts[parts.index(part) + 1:]:
                        result.skipped = int(part)
    result.total = result.passed + result.failed + result.skipped


def _parse_vitest_output(result: TestSuiteResult) -> None:
    """Parse vitest JSON output from stdout."""
    output = result.raw_output

    # Try to find JSON block in output
    json_start = output.find("{")
    if json_start == -1:
        # Fallback: parse from summary line
        for line in output.splitlines():
            if "Tests" in line and ("passed" in line or "failed" in line):
                # e.g. "Tests  76 passed (76)" or "Tests  2 failed | 74 passed (76)"
                parts = line.split()
                for i, part in enumerate(parts):
                    if part.isdigit() and i + 1 < len(parts):
                        next_word = parts[i + 1]
                        if next_word == "passed":
                            result.passed = int(part)
                        elif next_word == "failed":
                            result.failed = int(part)
                        elif next_word == "skipped":
                            result.skipped = int(part)
        result.total = result.passed + result.failed + result.skipped
        return

    # Try to extract valid JSON
    try:
        # Find the last valid JSON object in the output
        json_str = output[json_start:]
        # Find matching closing brace
        brace_count = 0
        end_idx = 0
        for i, char in enumerate(json_str):
            if char == "{":
                brace_count += 1
            elif char == "}":
                brace_count -= 1
                if brace_count == 0:
                    end_idx = i + 1
                    break

        if end_idx > 0:
            data = json.loads(json_str[:end_idx])
            result.total = data.get("numTotalTests", 0)
            result.passed = data.get("numPassedTests", 0)
            result.failed = data.get("numFailedTests", 0)
            result.skipped = data.get("numPendingTests", 0)

            # Extract failure details
            for test_file in data.get("testResults", []):
                for assertion in test_file.get("assertionResults", []):
                    if assertion.get("status") == "failed":
                        result.failures.append({
                            "test": assertion.get("fullName", "unknown"),
                            "error": assertion.get("failureMessages", ["No error details"])[0],
                        })
    except (json.JSONDecodeError, KeyError, IndexError):
        # Fallback to text parsing
        for line in output.splitlines():
            if "Tests" in line and ("passed" in line or "failed" in line):
                parts = line.split()
                for i, part in enumerate(parts):
                    if part.isdigit() and i + 1 < len(parts):
                        next_word = parts[i + 1]
                        if next_word == "passed":
                            result.passed = int(part)
                        elif next_word == "failed":
                            result.failed = int(part)
                        elif next_word == "skipped":
                            result.skipped = int(part)
        result.total = result.passed + result.failed + result.skipped


def _parse_playwright_output(result: TestSuiteResult) -> None:
    """Parse playwright JSON output."""
    output = result.raw_output

    json_start = output.find("{")
    if json_start == -1:
        return

    try:
        json_str = output[json_start:]
        brace_count = 0
        end_idx = 0
        for i, char in enumerate(json_str):
            if char == "{":
                brace_count += 1
            elif char == "}":
                brace_count -= 1
                if brace_count == 0:
                    end_idx = i + 1
                    break

        if end_idx > 0:
            data = json.loads(json_str[:end_idx])
            stats = data.get("stats", {})
            result.total = stats.get("total", 0)
            result.passed = stats.get("passed", 0)
            result.failed = stats.get("failed", 0)
            result.skipped = stats.get("skipped", 0)

            # Extract failure details from suites
            for suite in data.get("suites", []):
                for spec in suite.get("specs", []):
                    if spec.get("ok") is False:
                        for test in spec.get("tests", []):
                            if test.get("status") == "failed":
                                result.failures.append({
                                    "test": spec.get("title", "unknown"),
                                    "error": test.get("error", {}).get("message", "No error details"),
                                })
    except (json.JSONDecodeError, KeyError, IndexError):
        pass


# ─── Report Generation ───


def generate_json_report(results: list[TestSuiteResult]) -> dict[str, Any]:
    """Generate unified JSON report."""
    total_all = sum(r.total for r in results)
    passed_all = sum(r.passed for r in results)
    failed_all = sum(r.failed for r in results)
    skipped_all = sum(r.skipped for r in results)
    duration_all = sum(r.duration for r in results)

    all_failures: list[dict[str, str]] = []
    for r in results:
        for f in r.failures:
            all_failures.append({"suite": r.suite, **f})

    return {
        "summary": {
            "total": total_all,
            "passed": passed_all,
            "failed": failed_all,
            "skipped": skipped_all,
            "duration": round(duration_all, 2),
            "all_passed": failed_all == 0,
        },
        "suites": [r.to_dict() for r in results],
        "failures": all_failures,
    }


def generate_csv_report(results: list[TestSuiteResult]) -> str:
    """Generate CSV report."""
    output = io.StringIO()
    writer = csv.writer(output)
    writer.writerow(["suite", "total", "passed", "failed", "skipped", "duration", "exit_code"])
    for r in results:
        writer.writerow([
            r.suite, r.total, r.passed, r.failed, r.skipped,
            round(r.duration, 2), r.exit_code,
        ])
    # Add failures section
    writer.writerow([])
    writer.writerow(["FAILURES"])
    writer.writerow(["suite", "test", "error"])
    for r in results:
        for f in r.failures:
            writer.writerow([r.suite, f.get("test", ""), f.get("error", "")])
    return output.getvalue()


def print_console_summary(results: list[TestSuiteResult]) -> None:
    """Print a human-readable summary to console."""
    print("\n" + "=" * 60)
    print("  AI TEST RUNNER — SUMMARY")
    print("=" * 60)

    for r in results:
        status = "✓ PASS" if r.all_passed else "✗ FAIL"
        print(f"\n  [{status}] {r.suite.upper()}")
        print(f"    Tests: {r.total} total, {r.passed} passed, {r.failed} failed, {r.skipped} skipped")
        print(f"    Duration: {r.duration:.1f}s")
        if r.failures:
            print(f"    Failures ({len(r.failures)}):")
            for f in r.failures[:5]:
                test_name = f.get("test", "unknown")
                if len(test_name) > 70:
                    test_name = test_name[:67] + "..."
                print(f"      - {test_name}")
            if len(r.failures) > 5:
                print(f"      ... and {len(r.failures) - 5} more")

    total_all = sum(r.total for r in results)
    passed_all = sum(r.passed for r in results)
    failed_all = sum(r.failed for r in results)
    duration_all = sum(r.duration for r in results)

    print("\n" + "-" * 60)
    overall = "ALL PASSED" if failed_all == 0 else f"{failed_all} FAILURES"
    print(f"  OVERALL: {overall}")
    print(f"  Total: {passed_all}/{total_all} tests passed in {duration_all:.1f}s")
    print("=" * 60 + "\n")


# ─── Main ───


def main() -> int:
    parser = argparse.ArgumentParser(
        description="AI Test Runner — Unified test execution and reporting for LeoCRM"
    )
    parser.add_argument("--skip-backend", action="store_true", help="Skip backend tests")
    parser.add_argument("--skip-frontend", action="store_true", help="Skip frontend tests")
    parser.add_argument("--skip-e2e", action="store_true", default=True, help="Skip E2E tests (default)")
    parser.add_argument("--run-e2e", action="store_true", help="Run E2E tests (opt-in)")
    parser.add_argument("--output", choices=["json", "csv"], default="json", help="Output format")
    parser.add_argument("--verbose", action="store_true", help="Show full test output")
    parser.add_argument("--report-file", type=str, default=None, help="Write report to file")
    args = parser.parse_args()

    results: list[TestSuiteResult] = []

    # Run backend tests
    if not args.skip_backend:
        print("\n▶ Running backend tests (pytest)...")
        backend_result = run_backend_tests(verbose=args.verbose)
        results.append(backend_result)
        if args.verbose and backend_result.raw_output:
            print(backend_result.raw_output[:5000])

    # Run frontend tests
    if not args.skip_frontend:
        print("\n▶ Running frontend tests (vitest)...")
        frontend_result = run_frontend_tests(verbose=args.verbose)
        results.append(frontend_result)
        if args.verbose and frontend_result.raw_output:
            print(frontend_result.raw_output[:5000])

    # Run E2E tests (opt-in)
    if args.run_e2e and not args.skip_e2e:
        print("\n▶ Running E2E tests (playwright)...")
        e2e_result = run_e2e_tests(verbose=args.verbose)
        results.append(e2e_result)
        if args.verbose and e2e_result.raw_output:
            print(e2e_result.raw_output[:5000])

    if not results:
        print("\n⚠ No test suites were run. Use --skip-* flags to control which suites run.")
        return 0

    # Print console summary
    print_console_summary(results)

    # Generate report
    if args.output == "json":
        report = generate_json_report(results)
        report_str = json.dumps(report, indent=2, ensure_ascii=False)
    else:
        report_str = generate_csv_report(results)

    # Output report
    if args.report_file:
        Path(args.report_file).write_text(report_str, encoding="utf-8")
        print(f"  Report written to: {args.report_file}")
    else:
        print(report_str)

    # Exit code: 0 if all passed, 1 if any failures
    any_failed = any(not r.all_passed for r in results)
    return 1 if any_failed else 0


if __name__ == "__main__":
    sys.exit(main())
