"""Headless end-to-end test: drives main.VoiceApp with synthetic mic audio.

Scenario:
  1. feed silence, then a spoken question (macOS `say` recording)
  2. app should transcribe it, get a Claude reply, start speaking
  3. once playback starts, inject speech again -> expect barge-in
  4. the interrupting speech becomes the next user turn
"""

import asyncio
import os
import pathlib
import time

import numpy as np

# keep the e2e run from writing a synthetic-voice profile into models/
os.environ["SPEAKER_PROFILE"] = "/tmp/spk-e2e-profile.npz"
pathlib.Path("/tmp/spk-e2e-profile.npz").unlink(missing_ok=True)
os.environ["RESUME"] = "0"  # test runs never resume a real conversation

import main as appmod
from main import VoiceApp

# ...and never overwrite the real saved session either
import voice.control as control
control.SESSION_FILE = pathlib.Path("/tmp/spk-e2e-session")

SR = 16_000
F = 512  # frame samples
UTTERANCE = np.fromfile("/tmp/claude-voice-test.f32", dtype=np.float32)


class FakeMic:
    def __init__(self):
        self.frames: asyncio.Queue[np.ndarray] = asyncio.Queue()

    def start(self):
        pass

    def stop(self):
        pass


def frames_of(audio: np.ndarray):
    n = len(audio) // F
    return [audio[i * F:(i + 1) * F] for i in range(n)]


async def feed(mic, audio, realtime=True):
    for fr in frames_of(audio):
        await mic.frames.put(fr.copy())
        if realtime:
            await asyncio.sleep(F / SR)


async def driver(app: VoiceApp):
    mic: FakeMic = app.mic
    silence = np.zeros(SR, dtype=np.float32)

    await asyncio.sleep(1.0)  # let startup mic-check consume some frames
    print("[driver] feeding question")
    await feed(mic, silence[: SR // 2])
    await feed(mic, UTTERANCE)
    # trail silence until Claude starts speaking (end-of-utterance + thinking)
    t0 = time.monotonic()
    while not app.speaker.speaking and time.monotonic() - t0 < 30:
        await feed(mic, silence[: F * 4])
    if not app.speaker.speaking:
        print("[driver] FAIL: playback never started")
        return
    print("[driver] playback started; waiting past refractory, then barging in")
    await asyncio.sleep(0.8)
    await feed(mic, UTTERANCE[: 2 * SR])  # 2s of speech over the assistant
    print("[driver] barge-in audio done; trailing silence")
    await feed(mic, silence)  # 1s silence -> ends the interrupting utterance
    # keep feeding silence while the second reply plays out
    t0 = time.monotonic()
    while time.monotonic() - t0 < 25:
        await feed(mic, silence[: F * 4])
        if not app.speaker.speaking and time.monotonic() - t0 > 8:
            break
    print("[driver] done")


async def run_test():
    # startup mic-check needs frames immediately: pre-fill with silence
    app = VoiceApp()
    app.mic = FakeMic()
    for _ in range(20):
        app.mic.frames.put_nowait(np.zeros(F, dtype=np.float32))

    run_task = asyncio.create_task(app.run())
    try:
        await asyncio.wait_for(driver(app), timeout=150)
    except asyncio.TimeoutError:
        print("[driver] timed out")
    finally:
        run_task.cancel()
        await asyncio.gather(run_task, return_exceptions=True)


if __name__ == "__main__":
    asyncio.run(run_test())
