"""The ProofWarden console (TestClient) — read-mostly views, fail-closed, honest-absence.

Catalog renders from a real catalog; Running reflects a *real* warden (a started run
+ a coalesced double-launch shown in the authority audit strip); a down warden shows
the fail-closed ribbon (not a 500); History reads a written run-record; a missing
project is honest UNKNOWN; ``POST /run`` only submits a request and renders the
outcome. Plus: the base stays FastAPI-free, and the three schemas validate clean.
"""

from __future__ import annotations

import json
import subprocess
import sys
import threading
import time
from collections.abc import Iterator
from pathlib import Path
from typing import Any

import pytest
from fastapi.testclient import TestClient
from mgf.fedres import validate_payload
from mgf.fedres.canonical import dumpb

from mgf.test_web.catalog import build_catalog, catalog_path
from mgf.test_web.testctl import _socket
from mgf.test_web.warden import RunRequest, Warden, WardenClient, WardenDaemon
from mgf.test_web.warden.leases import Lease, now_iso
from mgf.test_web.warden.recorder import LiveRecorder
from mgf.test_web.web import create_app

_NODE_ID = "tests/test_demo.py::test_one"


def _write_catalog(root: Path) -> None:
    catalog = build_catalog([_NODE_ID, "tests/test_demo.py::test_two"], root.name)
    path = catalog_path(root)
    path.parent.mkdir(parents=True, exist_ok=True)
    path.write_bytes(dumpb(catalog))


def _write_run_record(root: Path, *, result: str = "pass", origin: str = "ci") -> None:
    runs = root / "fed-resources" / "generated" / "runs"
    runs.mkdir(parents=True, exist_ok=True)
    record = {
        "schema_version": "0.1",
        "kind": "test_run_record",
        "run_id": "run_hist01",
        "project": root.name,
        "test_id": _NODE_ID,
        "result": result,
        "started_at": "2026-06-29T10:00:00Z",
        "duration_s": 2,
        "origin": origin,
        "who": "woodpecker",
        "effective_config": {"seed": 7},
        "config_digest": "sha256:abc",
        "lease": {"acquired_at": "2026-06-29T10:00:00Z", "heartbeats": 1, "reclaimed": False},
        "audit": [
            {"ts": "2026-06-29T10:00:00Z", "event": "lease_acquired"},
            {"ts": "2026-06-29T10:00:01Z", "event": "coalesced", "other_run": "run_w"},
            {"ts": "2026-06-29T10:00:02Z", "event": "lease_released"},
        ],
    }
    (runs / "run_hist01.json").write_bytes(dumpb(record))


def _client(root: Path) -> TestClient:
    return TestClient(create_app(roots=[root]))


# ----------------------------------------------------------------- Catalog view --
def test_catalog_view_renders_the_tree_with_run_buttons(tmp_path: Path) -> None:
    _write_catalog(tmp_path)
    resp = _client(tmp_path).get(f"/p/{tmp_path.name}/catalog")
    assert resp.status_code == 200
    assert _NODE_ID in resp.text
    assert "tests/test_demo.py" in resp.text  # the group label
    assert f'action="/p/{tmp_path.name}/run"' in resp.text  # a Run form per test
    assert ">Run<" in resp.text


def test_catalog_absent_is_honest_not_blank(tmp_path: Path) -> None:
    (tmp_path / "fed-resources").mkdir()  # a project, but no catalog generated yet
    resp = _client(tmp_path).get(f"/p/{tmp_path.name}/catalog")
    assert resp.status_code == 200
    assert "No catalog" in resp.text
    assert "testctl catalog" in resp.text


# ----------------------------------------------------------------- History view --
def test_history_view_renders_a_written_run_record(tmp_path: Path) -> None:
    _write_run_record(tmp_path)
    resp = _client(tmp_path).get(f"/p/{tmp_path.name}/history")
    assert resp.status_code == 200
    assert "run_hist01" in resp.text
    assert _NODE_ID in resp.text
    assert "absorbed 1 double-launch" in resp.text  # the coalesce surfaced from the audit


def test_history_absent_is_honest(tmp_path: Path) -> None:
    (tmp_path / "fed-resources").mkdir()
    resp = _client(tmp_path).get(f"/p/{tmp_path.name}/history")
    assert resp.status_code == 200
    assert "No past runs" in resp.text


# ------------------------------------------------------------- honest-absence ----
def test_unknown_project_is_honest_absence_not_500(tmp_path: Path) -> None:
    resp = _client(tmp_path).get("/p/does-not-exist/catalog")
    assert resp.status_code == 200
    assert "Unknown project" in resp.text


def test_warden_down_running_is_fail_closed_ribbon_not_500(tmp_path: Path) -> None:
    _write_catalog(tmp_path)  # a project whose warden is NOT running
    resp = _client(tmp_path).get(f"/p/{tmp_path.name}/running")
    assert resp.status_code == 200
    assert "no runs can start" in resp.text  # the fail-closed ribbon
    assert "fail-closed" in resp.text


def test_post_run_with_warden_down_refuses_not_500(tmp_path: Path) -> None:
    _write_catalog(tmp_path)
    resp = _client(tmp_path).post(
        f"/p/{tmp_path.name}/run",
        data={"test_id": _NODE_ID, "origin": "web", "independent": "false"},
    )
    assert resp.status_code == 200
    assert "refused" in resp.text.lower() or "fail-closed" in resp.text


# --------------------------------------------------- Running from a REAL warden --
@pytest.fixture
def live_warden(tmp_path: Path) -> Iterator[tuple[Path, WardenClient, threading.Event]]:
    """A real warden on the project's socket, wired to the recorder, holding one run."""
    root = tmp_path
    release = threading.Event()
    started = threading.Event()

    def runner(request: RunRequest, emit: Any) -> str:
        emit({"kind": "test_start", "test_id": request.test_id})
        started.set()
        release.wait(timeout=5)
        emit({"kind": "test_end", "result": "pass"})
        return "pass"

    recorder = LiveRecorder(root, project=root.name)
    warden = Warden(root.name, runner, on_event=recorder.on_event, on_complete=recorder.on_complete)
    daemon = WardenDaemon(warden, _socket(root))
    daemon.start_background()
    client = WardenClient(_socket(root))
    for _ in range(200):
        if client.ping():
            break
        time.sleep(0.01)
    try:
        yield root, client, started
    finally:
        release.set()
        warden.wait_idle(timeout=5)
        daemon.shutdown()


@pytest.mark.concurrency
def test_running_view_shows_real_lease_and_coalesced_audit(
    live_warden: tuple[Path, WardenClient, threading.Event],
) -> None:
    root, client, started = live_warden
    _write_catalog(root)

    a = client.run(RunRequest("rA", "T-1", {"seed": 1}, origin="web"))
    assert a["status"] == "started"
    assert started.wait(2)
    # a second launch of the SAME test+config coalesces — no second run
    b = client.run(RunRequest("rB", "T-1", {"seed": 1}, origin="agent"))
    assert b["status"] == "coalesced"

    resp = _client(root).get(f"/p/{root.name}/running")
    assert resp.status_code == 200
    assert "warden up" in resp.text
    assert "T-1" in resp.text  # the running lease card
    assert "coalesced" in resp.text  # the authority audit strip — visible proof
    assert "test_start" in resp.text  # the live console streaming rA's NDJSON


@pytest.mark.concurrency
def test_post_run_via_web_submits_to_live_warden_and_coalesces(
    live_warden: tuple[Path, WardenClient, threading.Event],
) -> None:
    root, client, started = live_warden
    _write_catalog(root)
    client.run(RunRequest("rA", _NODE_ID, {}, origin="cli"))
    assert started.wait(2)
    # the console's one write: submit a request for the already-running test → coalesce
    resp = _client(root).post(
        f"/p/{root.name}/run",
        data={"test_id": _NODE_ID, "origin": "web", "independent": "false"},
    )
    assert resp.status_code == 200
    assert "coalesced" in resp.text.lower() or "Single-flight" in resp.text


@pytest.mark.concurrency
def test_running_view_offers_cancel_and_web_cancel_is_delivered(
    live_warden: tuple[Path, WardenClient, threading.Event],
) -> None:
    root, client, started = live_warden
    _write_catalog(root)
    client.run(RunRequest("rC", "T-1", {"seed": 9}, origin="web"))
    assert started.wait(2)

    # the Running view offers a Cancel affordance bound to the live lease's run_id
    running = _client(root).get(f"/p/{root.name}/running")
    assert running.status_code == 200
    assert f'action="/p/{root.name}/cancel"' in running.text
    assert 'name="run_id" value="rC"' in running.text

    # the console's cancel forwards to the warden (the sole executor); honest outcome
    resp = _client(root).post(f"/p/{root.name}/cancel", data={"run_id": "rC"})
    assert resp.status_code == 200
    assert "cancel delivered" in resp.text


# ------------------------------------------------------- base stays FastAPI-free -
def test_base_import_does_not_pull_fastapi() -> None:
    # In a clean subprocess (this test process already imported fastapi).
    code = (
        "import sys, mgf.test_web, mgf.test_web.testctl, mgf.test_web.catalog, "
        "mgf.test_web.warden.recorder; "
        "assert 'fastapi' not in sys.modules, sorted(m for m in sys.modules if 'fastapi' in m)"
    )
    proc = subprocess.run(  # noqa: S603
        [sys.executable, "-c", code], capture_output=True, text=True, check=False
    )
    assert proc.returncode == 0, proc.stderr


# ----------------------------------------------------------- schema conformance --
def test_sample_payloads_validate_against_fedres_schemas(tmp_path: Path) -> None:
    rec = LiveRecorder(tmp_path, project="proj")
    live_line = rec.event_to_line(
        {"run_id": "r", "kind": "log", "stream": "stdout", "msg": "hi"}, 1
    )
    lease = Lease(
        run_id="r",
        key="proj",
        test_id="t",
        config_digest="sha256:x",
        origin="web",
        who="founder",
        acquired_at=now_iso(),
        heartbeat_at=now_iso(),
        ttl_s=30,
    )
    run_record = rec.completion_record(
        RunRequest("r", "t", {"seed": 1}, origin="web"), lease, "pass"
    )
    catalog = build_catalog([_NODE_ID], "proj")
    assert validate_payload(live_line) == []
    assert validate_payload(run_record) == []
    assert validate_payload(catalog) == []
    # the fixture file the History test reads is itself a clean run-record
    _write_run_record(tmp_path)
    record = json.loads(
        (tmp_path / "fed-resources" / "generated" / "runs" / "run_hist01.json").read_text()
    )
    assert validate_payload(record) == []


# ----------------------------------------------------- Phase A: SSE live stream --
def _write_live_ndjson(root: Path, run_id: str, events: list[dict[str, Any]]) -> None:
    runs = root / "fed-resources" / "live" / "runs"
    runs.mkdir(parents=True, exist_ok=True)
    (runs / f"{run_id}.ndjson").write_text(
        "\n".join(json.dumps(e) for e in events) + "\n", encoding="utf-8"
    )


def test_stream_run_emits_events_then_end(tmp_path: Path) -> None:
    (tmp_path / "fed-resources").mkdir()
    _write_live_ndjson(
        tmp_path,
        "rX",
        [
            {
                "run_id": "rX",
                "seq": 1,
                "ts": "2026-07-06T10:00:00",
                "kind": "test_start",
                "test_id": "T-9",
            },
            {
                "run_id": "rX",
                "seq": 2,
                "ts": "2026-07-06T10:00:01",
                "kind": "log",
                "msg": "hello from the stream",
            },
            {
                "run_id": "rX",
                "seq": 3,
                "ts": "2026-07-06T10:00:02",
                "kind": "test_end",
                "result": "pass",
            },
        ],
    )
    resp = _client(tmp_path).get(f"/p/{tmp_path.name}/stream/rX")
    assert resp.status_code == 200
    assert resp.headers["content-type"].startswith("text/event-stream")
    body = resp.text
    assert "hello from the stream" in body  # the log line streamed as an SSE data frame
    assert "test_start" in body
    assert "event: end" in body  # the terminal test_end closes the stream


def test_stream_run_since_skips_already_seen(tmp_path: Path) -> None:
    (tmp_path / "fed-resources").mkdir()
    _write_live_ndjson(
        tmp_path,
        "rX",
        [
            {"run_id": "rX", "seq": 1, "ts": "t", "kind": "log", "msg": "early line"},
            {"run_id": "rX", "seq": 2, "ts": "t", "kind": "log", "msg": "late line"},
            {"run_id": "rX", "seq": 3, "ts": "t", "kind": "test_end", "result": "pass"},
        ],
    )
    body = _client(tmp_path).get(f"/p/{tmp_path.name}/stream/rX?since=2").text
    assert "early line" not in body and "late line" not in body  # seq ≤ 2 skipped
    assert "event: end" in body


def test_stream_run_unknown_project_is_404(tmp_path: Path) -> None:
    (tmp_path / "fed-resources").mkdir()
    assert _client(tmp_path).get("/p/nope/stream/rX").status_code == 404


# --------------------------------------------------------- Phase B: CI view --
def _write_project_toml(root: Path, *, ci_host: str = "", ci_repo: str = "") -> None:
    src = root / "fed-resources" / "source"
    src.mkdir(parents=True, exist_ok=True)
    lines = ["[project]", 'name = "Demo"', 'kind = "python"']
    if ci_host or ci_repo:
        lines.append("[ci]")
        if ci_host:
            lines.append(f'host = "{ci_host}"')
        if ci_repo:
            lines.append(f'repo = "{ci_repo}"')
    (src / "project.toml").write_text("\n".join(lines) + "\n", encoding="utf-8")


def test_ci_view_uses_configured_woodpecker_repo_url(tmp_path: Path) -> None:
    _write_run_record(tmp_path, origin="ci")
    _write_project_toml(tmp_path, ci_host="https://ci.example", ci_repo="35")
    resp = _client(tmp_path).get(f"/p/{tmp_path.name}/ci")
    assert resp.status_code == 200
    assert "https://ci.example/repos/35" in resp.text  # the real, per-repo Watch link
    assert "woodpecker.magogi.local" not in resp.text  # the old placeholder is gone
    assert "/repos/mgf/" not in resp.text  # the old wrong pattern is gone
    assert "joined lease" in resp.text


def test_ci_view_honest_absence_without_repo_config(tmp_path: Path) -> None:
    _write_run_record(tmp_path, origin="ci")  # a CI run, but no [ci] repo configured
    resp = _client(tmp_path).get(f"/p/{tmp_path.name}/ci")
    assert resp.status_code == 200
    assert "CI host not linked" in resp.text  # honest absence, not a fabricated link
    assert "195.15.241.48.sslip.io" in resp.text  # the real fleet default is shown in guidance


@pytest.mark.concurrency
def test_running_flags_a_ci_joined_lease(
    live_warden: tuple[Path, WardenClient, threading.Event],
) -> None:
    root, client, started = live_warden
    _write_catalog(root)
    client.run(RunRequest("rCI", "T-ci", {}, origin="ci"))
    assert started.wait(2)
    resp = _client(root).get(f"/p/{root.name}/running")
    assert resp.status_code == 200
    assert "CI · joined lease" in resp.text  # the CI-origin lease is flagged in Running


# ------------------------------------------------ Phase C: History filters --
def _write_record(root: Path, run_id: str, test_id: str, origin: str, result: str = "pass") -> None:
    runs = root / "fed-resources" / "generated" / "runs"
    runs.mkdir(parents=True, exist_ok=True)
    (runs / f"{run_id}.json").write_bytes(
        dumpb(
            {
                "schema_version": "0.1",
                "kind": "test_run_record",
                "run_id": run_id,
                "project": root.name,
                "test_id": test_id,
                "result": result,
                "started_at": "2026-06-29T10:00:00Z",
                "duration_s": 1,
                "origin": origin,
                "who": "x",
                "effective_config": {},
                "config_digest": "sha256:x",
                "lease": {
                    "acquired_at": "2026-06-29T10:00:00Z",
                    "heartbeats": 1,
                    "reclaimed": False,
                },
                "audit": [],
            }
        )
    )


def test_history_filters_by_test(tmp_path: Path) -> None:
    _write_record(tmp_path, "run_alpha", "tests/a.py::t_x", "web")
    _write_record(tmp_path, "run_beta", "tests/b.py::t_y", "cli")
    resp = _client(tmp_path).get(f"/p/{tmp_path.name}/history", params={"test": "tests/b.py::t_y"})
    assert resp.status_code == 200
    # run_id is row-only (the filter selects list test_ids/origins, not run_ids)
    assert "run_beta" in resp.text and "run_alpha" not in resp.text


def test_history_filters_by_origin(tmp_path: Path) -> None:
    _write_record(tmp_path, "run_alpha", "tests/a.py::t_x", "web")
    _write_record(tmp_path, "run_beta", "tests/b.py::t_y", "cli")
    resp = _client(tmp_path).get(f"/p/{tmp_path.name}/history", params={"origin": "cli"})
    assert resp.status_code == 200
    assert "run_beta" in resp.text and "run_alpha" not in resp.text


def test_history_filter_form_offers_tests_and_origins(tmp_path: Path) -> None:
    _write_record(tmp_path, "r1", "tests/a.py::t_x", "web")
    _write_record(tmp_path, "r2", "tests/b.py::t_y", "cli")
    resp = _client(tmp_path).get(f"/p/{tmp_path.name}/history")
    assert 'name="test"' in resp.text and 'name="origin"' in resp.text  # the filter selects
    assert "tests/a.py::t_x" in resp.text and "tests/b.py::t_y" in resp.text  # both offered


# ------------------------------------------------ Phase C.2: Catalog polish --
def _write_catalog_with_config(root: Path) -> None:
    catalog = {
        "schema_version": "0.1",
        "kind": "test_catalog",
        "project": root.name,
        "runners": [{"id": "pytest", "cmd": "pytest", "enumerate": "pytest --collect-only -q"}],
        "categories": [
            {
                "id": "tests",
                "label": "tests",
                "groups": [
                    {
                        "id": "tests/t.py",
                        "label": "t.py",
                        "tests": [
                            {
                                "id": "tests/t.py::t1",
                                "label": "t1",
                                "runner": "pytest",
                                "independent": False,
                                "tags": [],
                                "last_status": "unknown",
                                "config": {"seed": 7},
                                "config_schema": [
                                    {
                                        "name": "seed",
                                        "type": "int",
                                        "default": 7,
                                        "desc": "rng seed",
                                    }
                                ],
                            }
                        ],
                    }
                ],
            }
        ],
    }
    assert validate_payload(catalog) == []
    path = catalog_path(root)
    path.parent.mkdir(parents=True, exist_ok=True)
    path.write_bytes(dumpb(catalog))


def test_catalog_config_panel_has_reset_and_preview(tmp_path: Path) -> None:
    _write_catalog_with_config(tmp_path)
    resp = _client(tmp_path).get(f"/p/{tmp_path.name}/catalog")
    assert resp.status_code == 200
    assert "Reset to defaults" in resp.text and 'type="reset"' in resp.text  # native reset
    assert "pw-cfg-effective" in resp.text  # the preview slot (JS reveals + fills it)
    assert 'name="cfg.seed"' in resp.text  # the config input the preview reads


def test_catalog_config_preview_js_served(tmp_path: Path) -> None:
    (tmp_path / "fed-resources").mkdir()
    js = _client(tmp_path).get("/static/catalog-config.js")
    assert js.status_code == 200 and "effective" in js.text.lower()


# ----------------------------------------------- STALE-catalog banner (provenance) --
def _write_provenance(root: Path, commit: str) -> None:
    (root / "fed-resources" / "generated" / "test_catalog.provenance.json").write_text(
        json.dumps({"source_commit": commit, "generated_at": "2026-07-06T10:00:00+00:00"}),
        encoding="utf-8",
    )


def _set_head(root: Path, commit: str) -> None:
    git = root / ".git"
    git.mkdir(exist_ok=True)
    (git / "HEAD").write_text(commit + "\n", encoding="utf-8")


def test_catalog_stale_banner_when_commit_differs(tmp_path: Path) -> None:
    _write_catalog(tmp_path)
    _write_provenance(tmp_path, "aaaaaaa111")
    _set_head(tmp_path, "bbbbbbb222")  # repo has moved past the catalog's commit
    resp = _client(tmp_path).get(f"/p/{tmp_path.name}/catalog")
    assert resp.status_code == 200
    assert "may be out of date" in resp.text and "testctl catalog" in resp.text


def test_catalog_no_stale_banner_when_commit_matches(tmp_path: Path) -> None:
    _write_catalog(tmp_path)
    _write_provenance(tmp_path, "aaaaaaa111")
    _set_head(tmp_path, "aaaaaaa111")  # same commit → fresh
    resp = _client(tmp_path).get(f"/p/{tmp_path.name}/catalog")
    assert "may be out of date" not in resp.text


def test_catalog_no_stale_banner_without_provenance(tmp_path: Path) -> None:
    _write_catalog(tmp_path)  # no provenance sidecar → can't tell → no banner (honest)
    _set_head(tmp_path, "bbbbbbb222")
    resp = _client(tmp_path).get(f"/p/{tmp_path.name}/catalog")
    assert "may be out of date" not in resp.text


def test_write_provenance_roundtrips_to_reader(tmp_path: Path) -> None:
    # write_provenance must emit the exact `source_commit` key the reader consumes;
    # a just-generated catalog (source_commit == HEAD) reads back as fresh.
    from mgf.test_web.catalog import write_provenance
    from mgf.test_web.web.reads import ProjectReader

    _set_head(tmp_path, "abcdef123456")
    write_provenance(tmp_path)
    st = ProjectReader(tmp_path).catalog_staleness()
    assert st is not None
    assert st["stale"] is False and st["generated_commit"] == "abcdef123"
