"""The monitor must record step-by-step turns (heard / thought / answered,
per-step timing, ok vs error) and stream them to subscribers — that data is
exactly what the status dashboard renders."""

import asyncio
import json

from wsai.backends.mock import MockBrain, MockSTT, MockTTS
from wsai.monitor import Monitor
from wsai.pipeline import Pipeline


def test_monitor_records_turn_with_timed_steps():
    async def go():
        mon = Monitor()

        pipe = Pipeline(
            brain=MockBrain(),
            stt=MockSTT(script=["안녕"], interval=0.01),
            tts=MockTTS(),
            monitor=mon,
        )
        await asyncio.wait_for(pipe.run(), timeout=5)
        return mon

    mon = asyncio.run(go())
    snap = mon.snapshot()

    assert snap["status"]["turns_total"] == 1
    assert snap["status"]["running"] is False  # cleaned up after run
    assert len(snap["turns"]) == 1

    turn = snap["turns"][0]
    assert turn["heard"] == "안녕"          # what it heard
    assert turn["reply"]                     # what it answered
    assert turn["status"] == "ok"            # it worked
    assert turn["total_ms"] >= 0
    # step-by-step: every stage is named and timed
    names = [s["name"] for s in turn["steps"]]
    assert names == ["화면 맥락", "두뇌(생각)", "응답(TTS/전송)"]
    assert all(s["ok"] is True for s in turn["steps"])
    assert all(s["ms"] >= 0 for s in turn["steps"])


def test_monitor_marks_errors():
    class BoomBrain(MockBrain):
        async def respond(self, user_text, screen, history):
            raise RuntimeError("boom")

    async def go():
        mon = Monitor()
        pipe = Pipeline(
            brain=BoomBrain(),
            stt=MockSTT(script=["안녕"], interval=0.01),
            tts=MockTTS(),
            monitor=mon,
        )
        try:
            await asyncio.wait_for(pipe.run(), timeout=5)
        except BaseException:
            pass  # TaskGroup re-raises; we only care about recorded telemetry
        return mon

    mon = asyncio.run(go())
    snap = mon.snapshot()

    turn = snap["turns"][0]
    assert turn["status"] == "error"
    brain_step = next(s for s in turn["steps"] if s["name"] == "두뇌(생각)")
    assert brain_step["ok"] is False
    assert "boom" in brain_step["error"]
    assert snap["status"]["errors_total"] >= 1


def test_error_turn_increments_errors_total_once():
    """The dashboard voice turn (dashboard.voice_turn) records a turn and calls
    turn.finish(error=...) directly — it never goes through the pipeline's
    log("error") path. That failure must still land in errors_total, and exactly
    once no matter how many times the turn is re-published."""
    mon = Monitor()
    turn = mon.turn(source="discord")
    turn.heard("`코드` 얘기")           # touches/publishes again
    turn.replied("답변")                 # and again
    assert mon.status_snapshot()["errors_total"] == 0

    turn.finish(error="melo synth failed: KeyError '`'")
    assert mon.status_snapshot()["errors_total"] == 1

    # A stray re-finish/re-publish must not double count.
    turn.finish(error="melo synth failed: KeyError '`'")
    turn._touch()
    assert mon.status_snapshot()["errors_total"] == 1

    # The failure is also visible as an error-level event for the live feed.
    events = mon.snapshot()["events"]
    assert any(e["type"] == "log" and e["level"] == "error" for e in events)


def test_subscriber_receives_live_turn_events():
    async def go():
        mon = Monitor()
        q = mon.subscribe()
        pipe = Pipeline(
            brain=MockBrain(),
            stt=MockSTT(script=["안녕"], interval=0.01),
            tts=MockTTS(),
            monitor=mon,
        )
        await asyncio.wait_for(pipe.run(), timeout=5)
        return q

    q = asyncio.run(go())
    events = []
    while not q.empty():
        events.append(json.loads(q.get_nowait()))

    types = {e["type"] for e in events}
    assert "turn" in types   # live turn updates were pushed
    assert "status" in types  # listening/running status changes were pushed
