"""mtest --self-check — the supervisor verifies itself.

The contract: a broken supervisor MUST NOT look like "all tests
pass". CI runs `mtest --self-check` BEFORE any real test run for
exactly this reason — if the supervisor itself can't tell a
passing test from a failing one, none of its other claims are
trustworthy.

The check builds a temporary `tests/` tree with four canned
tests:
  1. A passing test → must be reported `passed`.
  2. A failing test → must be reported `failed` with a captured
     traceback containing the assertion message.
  3. A hanging test (sleeps past per-test timeout) → must be
     reported `hung` (via pytest-timeout from inside) or `hung`
     (via the supervisor's outer wall-clock if pytest-timeout
     fails).
  4. A crashing test (calls `os._exit(139)` to fake a segfault) →
     must be reported `crashed`, the chunk must continue.

Each check runs in isolation (its own tmpdir + .gates) so it
doesn't pollute the real run state.
"""

from __future__ import annotations

import shutil
import tempfile
from pathlib import Path

from .records import read_jsonl

_CHECKS = [
    (
        "passing test reported as `passed`",
        "test_canned_pass.py",
        """
def test_pass():
    assert 1 == 1
""",
        "passed",
        None,
    ),
    (
        "failing test reported as `failed` with captured traceback",
        "test_canned_fail.py",
        """
def test_fail():
    assert 2 + 2 == 5, "deliberate failure for self-check"
""",
        "failed",
        "deliberate failure",
    ),
    (
        "crashing test reported as `crashed`, chunk continues",
        "test_canned_crash.py",
        """
import os
def test_crash():
    os._exit(139)  # fake SIGSEGV exit
""",
        "crashed",
        None,
    ),
]


def run_self_check(
    repo_root: Path,
    gates_dir: Path,
    pytest_cmd: list[str],
    verbose: bool,
) -> bool:
    """Run each canned scenario in isolation. Returns True if all
    passed, False otherwise. Logs each step to stdout.

    Example::

        ok = run_self_check(repo_root=Path("."), gates_dir=Path(".gates"),
                            pytest_cmd=["python", "-m", "pytest"], verbose=False)
    """
    print("[mtest --self-check] starting…")
    all_ok = True

    # Check 0 — supervisor can import its own modules.
    print("  [0/4] supervisor imports cleanly …", end="", flush=True)
    try:
        from . import (  # noqa: F401
            diagnostics,
            durations,
            quarantine,
            records,
            selection,
            supervisor,
            tiers,
        )

        print(" OK")
    except ImportError as e:
        print(f" FAIL ({e})")
        return False

    # Checks 1–3 — canned tests.
    for i, (label, fname, body, expected_outcome, expected_in_summary) in enumerate(
        _CHECKS,
        start=1,
    ):
        print(f"  [{i}/4] {label} …", end="", flush=True)
        ok = _run_canned(
            repo_root,
            pytest_cmd,
            fname,
            body,
            expected_outcome,
            expected_in_summary,
            verbose,
        )
        print(" OK" if ok else " FAIL")
        if not ok:
            all_ok = False

    # Check 4 — hang detection (the supervisor's OUTER wall-clock catches a
    # test that pytest-timeout doesn't). A short per-test budget for this one
    # canned run makes the wall-clock fire in ~12s instead of waiting out the
    # default ~40s — same `hung` path, just cheap.
    print("  [4/4] hung test reported as `hung` (~12s) …", end="", flush=True)
    ok = _run_canned(
        repo_root,
        pytest_cmd,
        "test_canned_hang.py",
        """
import time
def test_hang():
    time.sleep(120)
""",
        "hung",
        None,
        verbose,
        per_test_timeout_s=2.0,
    )
    print(" OK" if ok else " FAIL")
    if not ok:
        all_ok = False

    print(f"[mtest --self-check] {'PASS' if all_ok else 'FAIL'}")
    return all_ok


def _run_canned(
    repo_root: Path,
    pytest_cmd: list[str],
    test_filename: str,
    test_body: str,
    expected_outcome: str,
    expected_in_summary: str | None,
    verbose: bool,
    per_test_timeout_s: float | None = None,
) -> bool:
    """Spawn the supervisor on a tmp tests tree containing exactly
    one canned test; assert the resulting JSONL record matches
    expectations."""
    with tempfile.TemporaryDirectory(prefix="mtest-self-check-") as td:
        tmp = Path(td)
        # Copy the project just enough that pytest can run:
        # pyproject.toml (for [tool.pytest.ini_options]) + the
        # canned test file. We avoid copying the real tests/ to
        # keep the check isolated.
        shutil.copy(repo_root / "pyproject.toml", tmp / "pyproject.toml")
        tests_dir = tmp / "tests"
        tests_dir.mkdir()
        (tests_dir / "__init__.py").write_text("")
        (tests_dir / test_filename).write_text(test_body)

        # Need an empty conftest.py to override the project's
        # `from tests import _broker` import (which won't find
        # _broker in our tmp tree).
        (tests_dir / "conftest.py").write_text("")

        # Build a minimal RunConfig + Supervisor, point at the
        # tmp tree.
        gates_tmp = tmp / ".gates"
        gates_tmp.mkdir()
        from .supervisor import (
            DEFAULT_PER_TEST_TIMEOUT_S,
            RunConfig,
            Supervisor,
            collect_tests,
        )

        all_tests = collect_tests(pytest_cmd, tmp)
        if not all_tests:
            if verbose:
                print(f"\n     (collection returned no tests in {tmp})")
            return False

        cfg = RunConfig(
            repo_root=tmp,
            gates_dir=gates_tmp,
            test_ids=[tid for tid, _ in all_tests],
            test_markers={},
            run_id="self-check",
            pytest_cmd=pytest_cmd,
            chunk_size=10,
            verbose=False,
            per_test_timeout_s=(
                per_test_timeout_s if per_test_timeout_s is not None else DEFAULT_PER_TEST_TIMEOUT_S
            ),
        )
        try:
            Supervisor(cfg).run()
        except Exception as e:
            if verbose:
                print(f"\n     (supervisor raised: {e})")
            return False

        # Inspect the JSONL log.
        jsonl_path = gates_tmp / "last-run" / "run.jsonl"
        records = list(read_jsonl(jsonl_path))
        if not records:
            # PAPER-77 (mgf-common FEEDBACK): no records written
            # often means pytest itself failed to start (unknown
            # config option from missing pytest-timeout plugin is
            # the canonical case). Surface diagnostics ALWAYS,
            # not just under --verbose — first-contact adopters
            # need the cause without needing to know the flag.
            print(
                f"\n     (no JSONL records written to {jsonl_path}; "
                f"pytest may have failed before running any test)"
            )
            return False
        rec = records[0]
        if rec.get("outcome") != expected_outcome:
            # PAPER-77 part 3 (2026-05-26): the canonical "missing
            # pytest-timeout" footgun produces CRASHED outcomes for
            # every canned scenario with no hint of why. Surface
            # the failure mode ALWAYS (not just under --verbose),
            # and on `crashed` / `error` outcomes ALSO surface the
            # captured pytest stderr — that's where "Unknown config
            # option: timeout" lives.
            actual_outcome = rec.get("outcome")
            print(f"\n     outcome was {actual_outcome!r}, expected {expected_outcome!r}")
            print(f"     code: {rec.get('code', '(none)')}")
            print(f"     summary: {rec.get('summary', '(none)')}")
            if actual_outcome in ("crashed", "error"):
                captured_stderr = rec.get("captured_stderr", "") or ""
                if captured_stderr:
                    # Last 20 lines — enough for pytest INTERNALERROR
                    # / "Unknown config option" / import-time failures,
                    # short enough to stay readable in the
                    # self-check's compact output.
                    stderr_tail = "\n".join(captured_stderr.splitlines()[-20:])
                    print(
                        "     --- pytest stderr (last 20 lines) ---\n"
                        "     " + stderr_tail.replace("\n", "\n     ")
                    )
                    print("     --- end stderr ---")
                    # Specific pytest-timeout footgun signal —
                    # PAPER-77 hint inline.
                    if "Unknown config option" in captured_stderr and (
                        "timeout" in captured_stderr
                    ):
                        print(
                            "     hint: this is the PAPER-77 footgun — "
                            "your pyproject's `timeout` / `timeout_method`"
                            " keys require the `pytest-timeout` plugin "
                            "in your dev extras. Add `pytest-timeout>=2.3,"
                            "<3` and re-run."
                        )
            return False
        if expected_in_summary:
            blob = (rec.get("summary") or "") + " " + (rec.get("detail") or "")
            if expected_in_summary not in blob:
                # Outcome was right but the captured detail was
                # missing the expected marker. Surface always —
                # consumer needs to know which canned check passed
                # for the wrong reason.
                print(
                    f"\n     expected {expected_in_summary!r} in record's summary+detail; actual:"
                )
                print(f"     summary: {(rec.get('summary') or '(empty)')[:200]}")
                detail = (rec.get("detail") or "(empty)")[:200]
                print(f"     detail:  {detail}")
                return False
        return True


__all__ = [
    "run_self_check",
]
