"""M6 Follow-live (spec §4.9): the In-progress lens' live activity view.

``follow_live`` composes the target's ``live/sessions/<id>.ndjson`` stream into a
view-model — current step, derived progress, last-event age, latest log, the timeline.
The unit tests inject ``now`` so the derived age is deterministic, and write synthetic
NDJSON sessions (live/ is ephemeral, so there's none in the real federation). The route
tests confirm the pane renders honestly through the app: idle when nothing streams, the
live timeline when a session is current, and honest-absence for an unknown project.
"""

from __future__ import annotations

import json
from datetime import UTC, datetime
from typing import TYPE_CHECKING, Any

from mgf.fedres import DiskProvider

from mgf.fed_web.lenses import inprogress_lens
from mgf.fed_web.live import follow_live

if TYPE_CHECKING:
    from pathlib import Path

    from fastapi.testclient import TestClient


def _write_session(
    live_root: Path, sid: str, events: list[dict[str, Any]], *, current: bool = True
) -> None:
    """Write a session NDJSON (and optionally point live/CURRENT at it)."""
    sessions = live_root / "sessions"
    sessions.mkdir(parents=True, exist_ok=True)
    enveloped = [{"v": "0.1", "session": sid, **e} for e in events]
    (sessions / f"{sid}.ndjson").write_text(
        "\n".join(json.dumps(e) for e in enveloped) + "\n", encoding="utf-8"
    )
    if current:
        (live_root / "CURRENT").write_text(sid + "\n", encoding="utf-8")


_RUNNING = [
    {
        "seq": 1,
        "ts": "2026-06-29T12:00:00Z",
        "kind": "session_start",
        "op": "converge",
        "task": "T-1",
    },
    {"seq": 2, "ts": "2026-06-29T12:00:05Z", "kind": "step", "name": "validating schemas"},
    {"seq": 3, "ts": "2026-06-29T12:00:10Z", "kind": "progress", "progress": 0.6},
    {
        "seq": 4,
        "ts": "2026-06-29T12:00:15Z",
        "kind": "log",
        "level": "info",
        "msg": "wrote manifest.json",
    },
]


# --------------------------------------------------------------------------- #
# follow_live — the view-model derivations                                     #
# --------------------------------------------------------------------------- #
def test_idle_when_no_current_session(tmp_path: Path) -> None:
    view = follow_live(DiskProvider(tmp_path))  # no live/CURRENT at all
    assert view["active"] is False and view["ended"] is False
    assert view["timeline"] == []
    assert view["title"] == "Nothing running"


def test_running_session_derives_step_progress_log_and_age(tmp_path: Path) -> None:
    _write_session(tmp_path / "fed-resources" / "live", "sess_run", _RUNNING)
    # now = 30s after the last event → a deterministic derived age.
    now = datetime(2026, 6, 29, 12, 0, 45, tzinfo=UTC)
    view = follow_live(DiskProvider(tmp_path), now=now)

    assert view["active"] is True and view["ended"] is False  # current + not ended
    assert view["op"] == "converge" and view["task"] == "T-1"
    assert view["step"] == "validating schemas"
    assert view["progress"] == 60  # 0.6 normalized to a percent (derived)
    assert view["latest_log"] == {
        "level": "info",
        "level_class": "low",
        "msg": "wrote manifest.json",
    }
    assert view["age"] == "30s"
    assert view["event_count"] == 4 and len(view["timeline"]) == 4
    first = view["timeline"][0]
    assert first["kind_class"] == "session-start" and first["clock"] == "12:00:00"
    assert "started converge" in first["text"] and "T-1" in first["text"]


def test_ended_named_session_carries_result_and_run_record(tmp_path: Path) -> None:
    events = [
        {"seq": 1, "ts": "2026-06-29T09:00:00Z", "kind": "session_start", "op": "check"},
        {"seq": 2, "ts": "2026-06-29T09:00:03Z", "kind": "log", "level": "info", "msg": "diffing"},
        {
            "seq": 3,
            "ts": "2026-06-29T09:00:12Z",
            "kind": "session_end",
            "result": "ok",
            "duration_s": 12,
            "run_record": "runs/sess_done.json",
        },
    ]
    # written but NOT pointed at by CURRENT → a named past session (is_current False).
    _write_session(tmp_path / "fed-resources" / "live", "sess_done", events, current=False)
    view = follow_live(DiskProvider(tmp_path), "sess_done")

    assert view["ended"] is True and view["active"] is False and view["is_current"] is False
    assert view["result"] == "ok" and view["duration_s"] == 12
    assert view["run_record"] == "runs/sess_done.json"
    assert "ended ok in 12s" in view["timeline"][-1]["text"]


def test_present_but_empty_session_is_honest(tmp_path: Path) -> None:
    live = tmp_path / "fed-resources" / "live"
    live.mkdir(parents=True)
    (live / "CURRENT").write_text("sess_ghost\n", encoding="utf-8")  # points nowhere readable
    view = follow_live(DiskProvider(tmp_path))
    assert view["active"] is False and view["timeline"] == []
    assert "no readable events" in view["title"]


# --------------------------------------------------------------------------- #
# the lens marker + the /live route through the app                            #
# --------------------------------------------------------------------------- #
def test_inprogress_lens_reports_live_now(tmp_path: Path) -> None:
    provider = DiskProvider(tmp_path)
    # status.json absent → the lens is honest-absent and carries no live marker...
    assert "live_now" not in inprogress_lens(provider)
    # ...with a status.json present, live_now reflects live/CURRENT's presence.
    gen = tmp_path / "fed-resources" / "generated"
    gen.mkdir(parents=True)
    status = {
        "schema_version": "0.1",
        "kind": "status",
        "project": "x",
        "summary": {"total": 0, "by_state": {}},
        "tasks": [],
    }
    (gen / "status.json").write_text(json.dumps(status), encoding="utf-8")
    assert inprogress_lens(provider)["live_now"] is False
    _write_session(tmp_path / "fed-resources" / "live", "sess_run", _RUNNING)
    assert inprogress_lens(provider)["live_now"] is True


def test_live_route_is_idle_for_a_project_with_no_stream(synthetic_client: TestClient) -> None:
    r = synthetic_client.get("/p/alpha/live")
    assert r.status_code == 200
    assert "Follow live" in r.text
    assert "Nothing running" in r.text  # alpha has no live/ — honest idle


def test_live_route_renders_a_running_session(
    synthetic: Path, synthetic_client: TestClient
) -> None:
    _write_session(synthetic / "alpha" / "fed-resources" / "live", "sess_run", _RUNNING)
    r = synthetic_client.get("/p/alpha/live")
    assert r.status_code == 200
    assert "live now" in r.text  # the live chip
    assert "validating schemas" in r.text  # current step
    assert "wrote manifest.json" in r.text  # latest log in the timeline


def test_inprogress_marker_links_to_the_live_view(
    synthetic: Path, synthetic_client: TestClient
) -> None:
    idle = synthetic_client.get("/p/alpha/inprogress")
    assert idle.status_code == 200
    assert "/p/alpha/live" in idle.text and "Open the live view" in idle.text
    # once a session streams, the marker flips to the active affordance
    _write_session(synthetic / "alpha" / "fed-resources" / "live", "sess_run", _RUNNING)
    live = synthetic_client.get("/p/alpha/inprogress")
    assert "Follow live" in live.text and "streaming now" in live.text


def test_live_on_an_unknown_project_is_honest_absence_not_500(synthetic_client: TestClient) -> None:
    r = synthetic_client.get("/p/ghost/live")
    assert r.status_code == 200
    assert "not wired" in r.text
