"""Tests for runtime/speech/tts.py.

All offline — uses FakeTTS test double. Live Kokoro/F5 tests require
MENTORCORE_TEST_TTS=1 and installed model weights.
"""

from __future__ import annotations

import struct

import numpy as np
import pytest
from runtime.speech.tts import (
    FakeTTS,
    VoiceConfig,
    _float32_to_pcm16,
    _iter_sentences,
    _split_sentences,
)

# ── Sentence splitter ─────────────────────────────────────────────────────────


def test_split_sentences_single():
    sentences = _split_sentences("Hello world.")
    assert sentences == ["Hello world."]


def test_split_sentences_multiple():
    sentences = _split_sentences("Be virtuous. Avoid excess. Seek truth.")
    assert len(sentences) == 3
    assert sentences[0] == "Be virtuous."


def test_split_sentences_strips_whitespace():
    sentences = _split_sentences("  First.  Second.  ")
    assert all(s == s.strip() for s in sentences)
    assert len(sentences) == 2


def test_split_sentences_empty():
    assert _split_sentences("") == []
    assert _split_sentences("   ") == []


@pytest.mark.asyncio
async def test_iter_sentences_yields_complete_sentences():
    async def tokens():
        for token in ["Be", " virt", "uous.", " ", "Avoid", " excess."]:
            yield token

    results = []
    async for sentence in _iter_sentences(tokens()):
        results.append(sentence)

    assert len(results) == 2
    assert "virtuous" in results[0]
    assert "excess" in results[1]


@pytest.mark.asyncio
async def test_iter_sentences_yields_trailing_text():
    async def tokens():
        yield "No final period"

    results = []
    async for sentence in _iter_sentences(tokens()):
        results.append(sentence)

    assert len(results) == 1
    assert results[0] == "No final period"


# ── VoiceConfig ───────────────────────────────────────────────────────────────


def test_voice_config_defaults():
    cfg = VoiceConfig()
    assert cfg.engine == "kokoro"
    assert cfg.sample_rate == 24000
    assert cfg.speed == 1.0


def test_voice_config_fake_engine():
    cfg = VoiceConfig(engine="fake")
    assert cfg.engine == "fake"


# ── FakeTTS ───────────────────────────────────────────────────────────────────


def test_fake_tts_returns_nonzero_pcm():
    tts = FakeTTS(duration_ms=100)
    pcm = tts.synthesize("Hello.", VoiceConfig())
    assert len(pcm) > 0


def test_fake_tts_returns_valid_pcm():
    """PCM is 16-bit aligned and parseable."""
    tts = FakeTTS(duration_ms=50)
    pcm = tts.synthesize("Test.", VoiceConfig())
    assert len(pcm) % 2 == 0
    samples = struct.unpack(f"<{len(pcm) // 2}h", pcm)
    assert all(s == 0 for s in samples)  # silence fixture


def test_pcm_conversion_accepts_torch_like_tensor():
    """Kokoro returns torch tensors, not NumPy arrays."""
    class TensorLike:
        def detach(self):
            return self

        def cpu(self):
            return self

        def numpy(self):
            return np.array([-1.0, 0.0, 1.0], dtype=np.float32)

    pcm = _float32_to_pcm16(TensorLike())
    assert len(pcm) == 6


def test_fake_tts_duration():
    """100ms at 24kHz = 2400 samples = 4800 bytes."""
    tts = FakeTTS(duration_ms=100)
    pcm = tts.synthesize("Test.", VoiceConfig())
    expected_bytes = int(FakeTTS.SAMPLE_RATE * 0.1) * 2
    assert len(pcm) == expected_bytes


@pytest.mark.asyncio
async def test_fake_tts_stream_yields_one_chunk_per_sentence():
    async def tokens():
        for t in ["First sentence. ", "Second sentence."]:
            yield t

    tts = FakeTTS(duration_ms=50)
    chunks = []
    async for chunk in tts.synthesize_stream(tokens(), VoiceConfig(engine="fake")):
        chunks.append(chunk)

    assert len(chunks) == 2
    assert all(len(c) > 0 for c in chunks)


@pytest.mark.asyncio
async def test_fake_tts_stream_empty_input():
    async def tokens():
        return
        yield  # make it an async generator

    tts = FakeTTS()
    chunks = []
    async for chunk in tts.synthesize_stream(tokens(), VoiceConfig()):
        chunks.append(chunk)

    assert chunks == []


# ── synthesize() with fake engine ────────────────────────────────────────────


def test_synthesize_fake_engine():
    from runtime.speech.tts import synthesize

    pcm = synthesize("Hello.", VoiceConfig(engine="fake"))
    assert len(pcm) > 0


def test_synthesize_unknown_engine_raises():
    from runtime.speech.tts import synthesize

    with pytest.raises(ValueError, match="Unknown TTS engine"):
        synthesize("Hello.", VoiceConfig(engine="unknown"))


def test_synthesize_f5_without_reference_audio_raises():
    from runtime.speech.tts import synthesize

    with pytest.raises(ValueError, match="reference_audio"):
        synthesize("Hello.", VoiceConfig(engine="f5"))
