"""
run_all.py
==========

Single entry point: run the calibration test and the pytest-style unit
tests, print a unified pass/fail summary, and write
``tests/test_results.md``.

Usage:
    python tests/run_all.py

Exit code: 0 on success, 1 if any block failed.
"""

from __future__ import annotations

import io
import subprocess
import sys
import traceback
from contextlib import redirect_stdout
from pathlib import Path

THIS_DIR = Path(__file__).resolve().parent
ROOT = THIS_DIR.parent
if str(ROOT) not in sys.path:
    sys.path.insert(0, str(ROOT))


# ---------------------------------------------------------------------------
# Block 1 -- calibration
# ---------------------------------------------------------------------------
def run_calibration() -> dict:
    """Run the calibration block and return a result dict."""
    block: dict = {"name": "calibration", "ok": False, "log": "",
                   "summary": None, "error": None}
    try:
        from tests.test_calibration_vs_cohorts import run, write_report
        buf = io.StringIO()
        with redirect_stdout(buf):
            summary = run()
            write_report(summary, THIS_DIR / "calibration_report.md")
        block["summary"] = summary
        block["log"] = buf.getvalue()
        # OK if no FAIL verdicts (WARN allowed)
        fails = sum(1 for r in summary["rows"]
                    if r["verdict"] == "FAIL"
                    and r["cohort"]["include_in_calibration"])
        block["ok"] = fails == 0
    except Exception as exc:                                     # noqa: BLE001
        block["error"] = f"{exc!r}\n{traceback.format_exc()}"
    return block


# ---------------------------------------------------------------------------
# Block 2 -- unit tests
# ---------------------------------------------------------------------------
def run_unit_tests() -> dict:
    """Run pytest if available, else a minimal in-process runner."""
    block: dict = {"name": "unit_tests", "ok": False, "log": "",
                   "passed": 0, "failed": 0, "error": None}
    test_file = THIS_DIR / "test_unit_cases.py"

    # Prefer pytest
    pytest_available = False
    try:
        check = subprocess.run(
            [sys.executable, "-c", "import pytest"],
            capture_output=True, text=True, check=False,
        )
        pytest_available = check.returncode == 0
    except Exception:                                           # noqa: BLE001
        pytest_available = False

    if pytest_available:
        try:
            proc = subprocess.run(
                [sys.executable, "-m", "pytest", str(test_file), "-v", "--tb=short"],
                cwd=str(ROOT),
                capture_output=True,
                text=True,
                check=False,
            )
            block["log"] = proc.stdout + proc.stderr
            block["ok"] = proc.returncode == 0
            for line in block["log"].splitlines():
                if " passed" in line or " failed" in line:
                    parts = line.replace("=", " ").split()
                    for i, tok in enumerate(parts):
                        if tok == "passed" and i > 0:
                            try:
                                block["passed"] = int(parts[i - 1])
                            except ValueError:
                                pass
                        if tok == "failed" and i > 0:
                            try:
                                block["failed"] = int(parts[i - 1])
                            except ValueError:
                                pass
            return block
        except Exception as exc:                                # noqa: BLE001
            block["log"] += (f"\n[pytest run failed: {exc!r}; "
                             "falling back to manual runner]\n")

    # Fallback: import the module and run every callable starting with "test_"
    # The module uses pytest.skip(...) at top level if the predictor is missing;
    # we stub a minimal `pytest` shim so importing succeeds when pytest is
    # absent but the predictor IS available.
    try:
        import importlib.util
        import types
        if "pytest" not in sys.modules:
            shim = types.ModuleType("pytest")

            def _fixture(*args, **kwargs):
                def _wrap(fn):
                    return fn
                if args and callable(args[0]):
                    return args[0]
                return _wrap

            class _Raises:
                def __init__(self, exc): self.exc = exc
                def __enter__(self): return self
                def __exit__(self, typ, val, tb):
                    if typ is None:
                        raise AssertionError(
                            f"expected {self.exc!r} not raised")
                    return issubclass(typ, self.exc)

            def _skip(msg, **kwargs):
                raise RuntimeError(f"pytest.skip not available: {msg}")

            shim.fixture = _fixture
            shim.raises = _Raises
            shim.skip = _skip
            sys.modules["pytest"] = shim

        spec = importlib.util.spec_from_file_location(
            "test_unit_cases_local", str(test_file))
        if spec is None or spec.loader is None:
            raise ImportError("could not load test module spec")
        mod = importlib.util.module_from_spec(spec)
        spec.loader.exec_module(mod)
        from nivel1_calculadora.predictor import LogisticPredictor  # type: ignore
        pred = LogisticPredictor()
        passed = 0
        failed = 0
        log_lines = []
        for name in dir(mod):
            if not name.startswith("test_"):
                continue
            fn = getattr(mod, name)
            if not callable(fn):
                continue
            try:
                try:
                    fn(pred)
                except TypeError:
                    fn()
                passed += 1
                log_lines.append(f"  OK   {name}")
            except Exception as exc:                            # noqa: BLE001
                failed += 1
                log_lines.append(f"  FAIL {name}: {exc!r}")
        block["passed"] = passed
        block["failed"] = failed
        block["log"] += "\n".join(log_lines)
        block["ok"] = failed == 0
    except Exception as exc:                                     # noqa: BLE001
        block["error"] = f"{exc!r}\n{traceback.format_exc()}"
        block["ok"] = False
    return block


# ---------------------------------------------------------------------------
# Reporting
# ---------------------------------------------------------------------------
def write_results_md(blocks: list[dict], path: Path) -> None:
    lines = ["# Test results - Nivel 1 ypN0 predictor", ""]
    overall_ok = all(b["ok"] for b in blocks)
    lines.append(f"**Overall:** {'PASS' if overall_ok else 'FAIL'}")
    lines.append("")
    for b in blocks:
        status = "PASS" if b["ok"] else "FAIL"
        lines.append(f"## {b['name']} -- {status}")
        lines.append("")
        if b["name"] == "calibration" and b.get("summary"):
            s = b["summary"]
            lines.append(f"- MAE = {s['mae']*100:.1f}pp")
            lines.append(f"- slope = {s['slope']:.3f}")
            lines.append(f"- intercept = {s['intercept']:.3f}")
            lines.append(f"- n cohorts = {s['n_calibration']}")
            if s["fallback_used"]:
                lines.append("- using fallback predictor (Agent B module unavailable)")
            lines.append("")
            lines.append("See `calibration_report.md` for the full per-cohort table.")
        if b["name"] == "unit_tests":
            lines.append(f"- passed = {b['passed']}")
            lines.append(f"- failed = {b['failed']}")
        if b.get("error"):
            lines.append("")
            lines.append("```")
            lines.append(b["error"])
            lines.append("```")
        lines.append("")
        lines.append("<details><summary>Log</summary>")
        lines.append("")
        lines.append("```")
        lines.append(b["log"] or "(no output)")
        lines.append("```")
        lines.append("")
        lines.append("</details>")
        lines.append("")
    path.write_text("\n".join(lines), encoding="utf-8")


def main() -> int:
    blocks = [run_calibration(), run_unit_tests()]
    write_results_md(blocks, THIS_DIR / "test_results.md")

    print("=" * 70)
    print("TEST SUMMARY")
    print("=" * 70)
    for b in blocks:
        status = "PASS" if b["ok"] else "FAIL"
        print(f"  {b['name']:20s} {status}")
        if b["name"] == "calibration" and b.get("summary"):
            s = b["summary"]
            print(f"    MAE={s['mae']*100:.1f}pp  slope={s['slope']:.3f}"
                  f"  intercept={s['intercept']:.3f}")
        if b["name"] == "unit_tests":
            print(f"    passed={b['passed']}  failed={b['failed']}")
        if b.get("error"):
            print(f"    error: {b['error'].splitlines()[0]}")
    overall_ok = all(b["ok"] for b in blocks)
    print("=" * 70)
    print(f"OVERALL: {'PASS' if overall_ok else 'FAIL'}")
    print("Results written to tests/test_results.md")
    return 0 if overall_ok else 1


if __name__ == "__main__":
    raise SystemExit(main())
