"""BDD tests for runtime/api/voice.py.

Given: compiled test-persona fixture + FakeProvider + FakeSTT (FAKE_PROVIDER=1)
When:  WebSocket voice conversation flow
Then:  correct protocol frames in correct order

All offline — no model downloads, no real API calls.
"""

from __future__ import annotations

import json

import pytest
from fastapi.testclient import TestClient


@pytest.fixture
def client(examples_persona_root, monkeypatch):
    monkeypatch.setenv("FAKE_PROVIDER", "1")
    monkeypatch.setenv("MENTORCORE_PERSONAS_ROOT", str(examples_persona_root))
    from runtime.api.app import app

    return TestClient(app)


def _ws_voice_turn(ws, audio_bytes: bytes = b"\x00" * 100) -> list[dict | bytes]:
    """Send one voice turn and collect all server responses."""
    ws.send_text(json.dumps({"type": "start"}))
    ws.send_bytes(audio_bytes)
    ws.send_text(json.dumps({"type": "stop"}))

    frames: list[dict | bytes] = []
    while True:
        data = ws.receive()
        if data.get("bytes"):
            frames.append(data["bytes"])
        elif data.get("text"):
            msg = json.loads(data["text"])
            frames.append(msg)
            if msg.get("type") == "done":
                break
    return frames


# ── Connection ────────────────────────────────────────────────────────────────


def test_voice_ws_connects_and_sends_session(client):
    """Given: test-persona. When: WS connect. Then: session frame received."""
    with client.websocket_connect("/api/voice/test-persona") as ws:
        msg = json.loads(ws.receive_text())
        assert msg["type"] == "session"
        assert "session_id" in msg


def test_voice_ws_unknown_persona_closes(client):
    """Given: unknown slug. When: WS connect. Then: error + close."""
    with client.websocket_connect("/api/voice/nonexistent") as ws:
        msg = json.loads(ws.receive_text())
        assert msg["type"] == "error"


# ── Full turn protocol ────────────────────────────────────────────────────────


def test_voice_turn_sends_transcript(client):
    """Given: audio bytes. Then: transcript frame before audio."""
    with client.websocket_connect("/api/voice/test-persona") as ws:
        ws.receive_text()  # session frame
        frames = _ws_voice_turn(ws)
        transcript_frames = [
            f for f in frames if isinstance(f, dict) and f.get("type") == "transcript"
        ]
        assert len(transcript_frames) == 1
        assert "text" in transcript_frames[0]


def test_voice_turn_sends_binary_audio(client):
    """Given: non-empty transcript. Then: at least one binary PCM frame."""
    with client.websocket_connect("/api/voice/test-persona") as ws:
        ws.receive_text()  # session frame
        frames = _ws_voice_turn(ws)
        binary_frames = [f for f in frames if isinstance(f, bytes)]
        assert len(binary_frames) > 0


def test_voice_turn_streams_assistant_text(client):
    """Given a voice turn. Then: the assistant reply is available as text frames."""
    with client.websocket_connect("/api/voice/test-persona") as ws:
        ws.receive_text()  # session frame
        frames = _ws_voice_turn(ws)
        text_frames = [
            f for f in frames if isinstance(f, dict) and f.get("type") == "text"
        ]
        assert "".join(f["text"] for f in text_frames)


def test_voice_turn_ends_with_done(client):
    """Then: last text frame is {type: done}."""
    with client.websocket_connect("/api/voice/test-persona") as ws:
        ws.receive_text()  # session frame
        frames = _ws_voice_turn(ws)
        text_frames = [f for f in frames if isinstance(f, dict)]
        assert text_frames[-1]["type"] == "done"


def test_voice_frame_order(client):
    """Protocol order: transcript → (binary…) → done."""
    with client.websocket_connect("/api/voice/test-persona") as ws:
        ws.receive_text()  # session frame
        frames = _ws_voice_turn(ws)

        text_frames = [(i, f) for i, f in enumerate(frames) if isinstance(f, dict)]
        transcript_idx = next(i for i, f in text_frames if f.get("type") == "transcript")
        done_idx = next(i for i, f in text_frames if f.get("type") == "done")

        assert transcript_idx < done_idx


def test_voice_multiturn_accumulates_session(client):
    """Two turns in same WS connection share session history."""
    with client.websocket_connect("/api/voice/test-persona") as ws:
        ws.receive_text()  # session frame
        _ws_voice_turn(ws)  # turn 1
        frames2 = _ws_voice_turn(ws)  # turn 2
        # If session is shared, turn 2 still completes
        done_frames = [f for f in frames2 if isinstance(f, dict) and f.get("type") == "done"]
        assert len(done_frames) == 1


def test_voice_reuses_an_existing_text_session(client):
    """A voice connection can continue a session created by text chat."""
    chat_response = client.post("/api/chat/test-persona", json={"message": "First turn."})
    session_id = next(
        json.loads(line[6:])["session_id"]
        for line in chat_response.text.splitlines()
        if line.startswith("data: ") and json.loads(line[6:]).get("type") == "session"
    )

    with client.websocket_connect(f"/api/voice/test-persona?session_id={session_id}") as ws:
        msg = json.loads(ws.receive_text())
        assert msg == {"type": "session", "session_id": session_id}
        frames = _ws_voice_turn(ws)
        assert any(isinstance(frame, dict) and frame.get("type") == "done" for frame in frames)
