"""Two-lane router: fast-lane sentinel detection, history consistency across
lanes, and the FAST_CHAT=0 path. All LLM I/O is a scripted FakeAdapter — no
network, no real model, no TTS (tests run in chat mode)."""
import asyncio

import brain
import main


class FakeAdapter(brain.LLMAdapter):
    """Replays one scripted event list per stream() call and records usage."""

    def __init__(self, events):
        self._events = list(events)
        self.calls = 0
        self.last_messages = None
        self.last_tools = "unset"

    async def stream(self, messages, tools):
        self.calls += 1
        self.last_messages = [dict(m) for m in messages]
        self.last_tools = tools
        for ev in self._events:
            yield ev


def _text(s):
    return {"type": "text", "text": s}


def _finish():
    return {"type": "finish", "reason": "stop"}


# ===========================================================================
# FastLane — sentinel detection + streaming contract
# ===========================================================================
async def test_fastlane_bare_sentinel_hands_off():
    lane = brain.FastLane(FakeAdapter([_text(brain.FAST_HANDOFF), _finish()]),
                          "fast-sys")
    out = [item async for item in lane.run([], "when is my meeting")]
    assert out == [("handoff", None)]


async def test_fastlane_sentinel_split_across_deltas_hands_off():
    lane = brain.FastLane(
        FakeAdapter([_text("<<"), _text("ACT"), _text(">>"), _finish()]),
        "fast-sys")
    out = [item async for item in lane.run([], "recall something")]
    assert out == [("handoff", None)]


async def test_fastlane_empty_reply_hands_off():
    lane = brain.FastLane(FakeAdapter([_text("   "), _finish()]), "fast-sys")
    out = [item async for item in lane.run([], "hi")]
    assert out == [("handoff", None)]


async def test_fastlane_normal_reply_streams_then_finalises():
    lane = brain.FastLane(
        FakeAdapter([_text("[dry] Right away, sir. "),
                     _text("It is done."), _finish()]),
        "fast-sys")
    out = [item async for item in lane.run([], "sort it")]
    kinds = [k for k, _ in out]
    says = [p for k, p in out if k == "say"]
    finals = [p for k, p in out if k == "final"]
    assert kinds[-1] == "final"                    # final is last
    assert "handoff" not in kinds
    assert says[0].startswith("[dry] Right away, sir.")   # raw tag preserved
    assert any("It is done." in s for s in says)
    # committed text has NO sentinel and carries the whole reply
    assert finals and "It is done." in finals[0]
    assert brain.FAST_HANDOFF not in finals[0]


async def test_fastlane_history_view_strips_tool_scaffolding():
    hist = [
        {"role": "user", "content": "hi"},
        {"role": "assistant", "content": None,        # tool-call turn: dropped
         "tool_calls": [{"id": "c1", "type": "function",
                         "function": {"name": "x", "arguments": "{}"}}]},
        {"role": "tool", "tool_call_id": "c1", "content": "result"},  # dropped
        {"role": "assistant", "content": "Answer, sir."},
    ]
    lane = brain.FastLane(FakeAdapter([]), "fast-sys")
    msgs = lane.build_messages(hist, "next")
    assert [m["role"] for m in msgs] == ["system", "user", "assistant", "user"]
    assert all(isinstance(m["content"], str) for m in msgs)


# ===========================================================================
# Router — Session._run_turn over both lanes (chat mode, no TTS)
# ===========================================================================
class FakeWS:
    def __init__(self):
        self.sent = []
        self.binary = []

    async def send_json(self, frame):
        self.sent.append(frame)

    async def send_bytes(self, data):
        self.binary.append(data)


def _session(fast_events=None, full_events=None):
    sess = main.Session(ws=FakeWS())
    sess.mode = "chat"                                    # no audio frames
    sess.system_msg = {"role": "system", "content": "full-sys"}
    sess.fast_system_msg = {"role": "system", "content": "fast-sys"}
    sess.fast_adapter = FakeAdapter(fast_events or [])
    sess.brain = brain.Brain(FakeAdapter(full_events or []),
                             tool_specs=None, tool_exec=None)
    return sess


def _no_passive(monkeypatch):
    async def _noop(*_a, **_k):
        return None
    monkeypatch.setattr(main.tools, "passive_remember", _noop)


async def test_router_fast_lane_serves_and_commits_history(monkeypatch):
    monkeypatch.setenv("FAST_CHAT", "1")
    _no_passive(monkeypatch)
    sess = _session(fast_events=[_text("Evening, sir."), _finish()])
    await sess._run_turn({"text": "evening"})
    await asyncio.sleep(0)

    types = [f["type"] for f in sess.ws.sent]
    assert "sentence" in types and types[-1] == "turn_end"
    # shared history: exactly the user turn + the fast assistant turn
    assert [m["role"] for m in sess.history] == ["user", "assistant"]
    assert sess.history[0]["content"] == "evening"
    assert "Evening, sir." in sess.history[1]["content"]
    # the full brain was never invoked
    assert sess.brain.adapter.calls == 0
    assert sess.fast_adapter.calls == 1


async def test_router_escalates_on_sentinel_then_full_brain_serves(monkeypatch):
    monkeypatch.setenv("FAST_CHAT", "1")
    _no_passive(monkeypatch)
    sess = _session(
        fast_events=[_text(brain.FAST_HANDOFF), _finish()],
        full_events=[_text("Your meeting is at nine, sir."), _finish()])
    await sess._run_turn({"text": "when is my meeting"})
    await asyncio.sleep(0)

    texts = [f.get("text") for f in sess.ws.sent if f["type"] == "sentence"]
    assert any("nine" in t for t in texts)
    # both lanes ran: fast tried, then full served
    assert sess.fast_adapter.calls == 1
    assert sess.brain.adapter.calls == 1
    # history holds user + FULL assistant answer; the bare sentinel is absent
    assert [m["role"] for m in sess.history] == ["user", "assistant"]
    assert brain.FAST_HANDOFF not in sess.history[1]["content"]
    assert "nine" in sess.history[1]["content"]


async def test_router_fast_chat_disabled_skips_fast_lane(monkeypatch):
    monkeypatch.setenv("FAST_CHAT", "0")
    _no_passive(monkeypatch)
    sess = _session(
        fast_events=[_text("must not be used"), _finish()],
        full_events=[_text("Full brain here, sir."), _finish()])
    await sess._run_turn({"text": "hello"})
    await asyncio.sleep(0)

    assert sess.fast_adapter.calls == 0          # fast lane never touched
    assert sess.brain.adapter.calls == 1
    assert [m["role"] for m in sess.history] == ["user", "assistant"]
    assert "Full brain" in sess.history[1]["content"]


async def test_router_second_turn_sees_first_turn_in_shared_history(monkeypatch):
    monkeypatch.setenv("FAST_CHAT", "1")
    _no_passive(monkeypatch)
    sess = _session(fast_events=[_text("Noted, sir."), _finish()])
    await sess._run_turn({"text": "my car is a Supra"})
    await asyncio.sleep(0)
    # second turn: the fast lane must receive the prior turn in its message view
    sess.fast_adapter = FakeAdapter([_text("A Supra, sir."), _finish()])
    await sess._run_turn({"text": "what did I say"})
    await asyncio.sleep(0)
    seen = sess.fast_adapter.last_messages
    roles = [m["role"] for m in seen]
    # system + (user, assistant from turn 1) + user turn 2
    assert roles == ["system", "user", "assistant", "user"]
    assert seen[1]["content"] == "my car is a Supra"


async def test_router_empty_utterance_just_ends_turn(monkeypatch):
    _no_passive(monkeypatch)
    sess = _session()
    await sess._run_turn({"text": "   "})
    assert sess.ws.sent[-1]["type"] == "turn_end"
    assert sess.history == []
    assert sess.fast_adapter.calls == 0
    assert sess.brain.adapter.calls == 0
