"""LiveRecorder — schema-clean NDJSON + CURRENT pointer + the durable run-record,
plus the Warden ``on_complete`` hook that drives it."""

from __future__ import annotations

import json
import time
from pathlib import Path
from typing import Any

from mgf.fedres import validate_payload

from mgf.test_web.warden import RunRequest, Warden
from mgf.test_web.warden.leases import Lease, now_iso
from mgf.test_web.warden.recorder import LiveRecorder


def _req(**kw: Any) -> RunRequest:
    base: dict[str, Any] = {"run_id": "run_x", "test_id": "t.py::a", "config": {"seed": 1}}
    base.update(kw)
    return RunRequest(**base)


def test_event_to_line_maps_mtest_assert_into_the_grammar(tmp_path: Path) -> None:
    rec = LiveRecorder(tmp_path, project="proj")
    line = rec.event_to_line(
        {
            "run_id": "run_x",
            "kind": "assert",
            "test_id": "t.py::a",
            "outcome": "passed",  # -> result
            "summary": "ok",  # -> msg
            "code": 0,  # dropped (not in grammar)
            "duration_ms": 12,  # dropped
        },
        5,
    )
    assert line["v"] == "0.1" and line["seq"] == 5 and line["kind"] == "assert"
    assert line["result"] == "passed" and line["msg"] == "ok"
    assert "outcome" not in line and "code" not in line and "duration_ms" not in line
    assert validate_payload(line) == []


def test_on_event_appends_valid_ndjson_and_maintains_current(tmp_path: Path) -> None:
    rec = LiveRecorder(tmp_path, project="proj")
    current = tmp_path / "fed-resources" / "live" / "CURRENT"
    rec.on_event({"run_id": "run_x", "kind": "test_start", "test_id": "t.py::a"})
    assert current.read_text(encoding="utf-8").strip() == "run_x"  # set on test_start
    rec.on_event({"run_id": "run_x", "kind": "log", "stream": "stdout", "msg": "hi"})
    rec.on_event({"run_id": "run_x", "kind": "test_end", "result": "pass"})
    assert not current.exists()  # cleared on test_end

    ndjson = tmp_path / "fed-resources" / "live" / "runs" / "run_x.ndjson"
    lines = [json.loads(line) for line in ndjson.read_text(encoding="utf-8").splitlines()]
    assert [line["kind"] for line in lines] == ["test_start", "log", "test_end"]
    assert [line["seq"] for line in lines] == [1, 2, 3]  # monotonic
    assert all(validate_payload(line) == [] for line in lines)  # each line schema-clean


def test_completion_record_carries_effective_config_and_absorbed_coalesce(tmp_path: Path) -> None:
    rec = LiveRecorder(tmp_path, project="proj")
    req = _req(origin="web", who="founder")
    lease = Lease(
        run_id="run_x",
        key="proj",
        test_id="t.py::a",
        config_digest=req.config_digest,
        origin="web",
        who="founder",
        acquired_at=now_iso(),
        heartbeat_at=now_iso(),
        ttl_s=30,
        watchers=["run_y"],  # a coalesced double-launch this lease absorbed
    )
    record = rec.completion_record(req, lease, "pass")
    assert validate_payload(record) == []
    assert record["effective_config"] == {"seed": 1}
    assert record["config_digest"] == req.config_digest
    events = [a["event"] for a in record["audit"]]
    assert events == ["lease_acquired", "coalesced", "lease_released"]
    assert record["audit"][1]["other_run"] == "run_y"  # the absorbed run is visible proof


def test_on_complete_writes_the_run_record_file(tmp_path: Path) -> None:
    rec = LiveRecorder(tmp_path, project="proj")
    rec.on_complete(_req(), None, "pass")
    path = tmp_path / "fed-resources" / "generated" / "runs" / "run_x.json"
    assert path.is_file()
    record = json.loads(path.read_text(encoding="utf-8"))
    assert record["kind"] == "test_run_record" and record["result"] == "pass"
    assert validate_payload(record) == []


def test_warden_on_complete_hook_fires_after_the_run() -> None:
    calls: list[tuple[str, str | None, str]] = []

    def runner(request: RunRequest, emit: Any) -> str:
        emit({"kind": "test_start"})
        emit({"kind": "test_end", "result": "pass"})
        return "pass"

    def on_complete(request: RunRequest, lease: Lease | None, result: str) -> None:
        calls.append((request.run_id, lease.run_id if lease else None, result))

    warden = Warden("proj", runner, on_complete=on_complete)
    warden.submit(RunRequest("r1", "T-1"))
    assert warden.wait_idle(timeout=5)
    # the hook fires once, with the request, the released lease snapshot, and the result
    for _ in range(200):
        if calls:
            break
        time.sleep(0.01)
    assert calls == [("r1", "r1", "pass")]
