"""ASYNC_BRAIN routing tests — no audio, no models, no Claude session.

Verifies the chat-supervisor routing in VoiceApp.respond():
  handoff -> background dispatch (the floor is freed), chat-while-busy
  carries the task note to the fast lane, a second handoff queues, a
  voice-cancel kills the task + queue, finished replies land in _ready,
  and ASYNC_BRAIN=0 keeps the old blocking path.

Run:  .venv/bin/python -m pytest test_async_brain.py -q
"""
from __future__ import annotations

import asyncio
import os

import numpy as np

os.environ["RESUME"] = "0"
os.environ.setdefault("ASYNC_BRAIN", "1")

import main as appmod
from main import VoiceApp, _CANCEL_TASK_RE


class FakeBrain:
    def __init__(self) -> None:
        self.asked: list[str] = []
        self.interrupts = 0
        self.delay = 0.05
        self.replies = ["Done, sir."]

    async def reply(self, text: str):
        self.asked.append(text)
        await asyncio.sleep(self.delay)
        for s in self.replies:
            yield s

    async def interrupt(self, timeout: float = 1.5) -> None:
        self.interrupts += 1


class FakeFast:
    def __init__(self) -> None:
        self.mode = "handoff"  # or "say"
        self.asked: list[str] = []

    async def classify_and_reply(self, text: str):
        self.asked.append(text)
        if self.mode == "handoff":
            yield ("handoff", None)
        else:
            yield ("say", "Right you are, sir.")

    async def interrupt(self, timeout: float = 1.5) -> None:
        pass


class FakeTTS:
    accepts_tags = True

    def synth(self, text: str):
        return np.zeros(8, dtype=np.int16)


class FakeSpeaker:
    def __init__(self) -> None:
        self.played = 0
        self.speaking = False

    def play(self, pcm) -> None:
        self.played += 1

    def cut(self) -> None:
        pass


def _app() -> VoiceApp:
    app = VoiceApp()
    app.brain = FakeBrain()
    app.fast = FakeFast()
    app.tts = FakeTTS()
    app.speaker = FakeSpeaker()
    app.spoken_turns = []

    async def fake_saw(source, brain=None):
        out = []
        async for s in source:
            out.append(s)
        app.spoken_turns.append((out, brain))
    app._speak_and_watch = fake_saw
    return app


def test_handoff_dispatches_background():
    async def go():
        app = _app()
        t0 = asyncio.get_event_loop().time()
        await app.respond("fix the failing tests")
        took = asyncio.get_event_loop().time() - t0
        assert took < 0.5, "respond() must not block on the brain turn"
        assert app._brain_task is not None
        await app._brain_task
        assert app._ready == [["Done, sir."]]
        assert app.brain.asked == ["fix the failing tests"]  # no note leaked
    asyncio.run(go())


def test_chat_while_busy_gets_task_note():
    async def go():
        app = _app()
        app.brain.delay = 0.4
        await app.respond("rebuild the dashboard")
        app.fast.mode = "say"
        await app.respond("how is it going?")
        assert "background task in flight" in app.fast.asked[-1]
        assert app.spoken_turns[-1][0] == ["Right you are, sir."]
        await app._brain_task
        assert app._ready and app._ready[0] == ["Done, sir."]
    asyncio.run(go())


def test_second_handoff_queues():
    async def go():
        app = _app()
        app.brain.delay = 0.2
        await app.respond("task A")
        await app.respond("task B")
        assert app._brain_queue == ["task B"]
        await app._brain_task
        assert app.brain.asked == ["task A", "task B"]
        assert len(app._ready) == 2
    asyncio.run(go())


def test_voice_cancel_kills_task_and_queue():
    async def go():
        app = _app()
        app.brain.delay = 5.0
        await app.respond("long migration task")
        app._brain_queue.append("queued thing")
        await app.respond("cancel that")
        assert app._brain_task is None
        assert app._brain_queue == []
        assert app.brain.interrupts >= 1
        assert app._ready == []          # nothing delivered
        assert app.speaker.played >= 1   # "Dropped it, sir."
    asyncio.run(go())


def test_flag_off_keeps_blocking_path():
    async def go():
        app = _app()
        old = appmod.ASYNC_BRAIN
        appmod.ASYNC_BRAIN = False
        try:
            await app.respond("do the thing")
            # blocking path: the FULL brain streamed through speak-and-watch
            assert app.spoken_turns[-1][1] is app.brain
            assert app._brain_task is None
        finally:
            appmod.ASYNC_BRAIN = old
    asyncio.run(go())


def test_cancel_regex_matrix():
    yes = ["stop", "stop that", "cancel the task", "Cancel it.",
           "never mind", "jarvis, drop it", "forget it", "abort the job"]
    no = ["stop the music", "cancel my subscription", "stop when you finish",
          "forget what I said about the color", "drop the database table"]
    for t in yes:
        assert _CANCEL_TASK_RE.match(t), f"should match: {t!r}"
    for t in no:
        assert not _CANCEL_TASK_RE.match(t), f"should NOT match: {t!r}"


if __name__ == "__main__":
    for name, fn in sorted(globals().items()):
        if name.startswith("test_") and callable(fn):
            fn()
            print(f"  ok  {name}")
    print("all green")
