"""Tests fuer den lokalen Piper-TTS-Provider (offline, mit Fake-Binary)."""

import asyncio
import json
import shutil
import stat

import pytest

import app.providers.tts.piper as piper_mod
from app.providers.tts.piper import PiperTTSProvider


@pytest.fixture(autouse=True)
def _force_binary_path(monkeypatch):
    """Diese Tests pruefen den Binary-Fallback (Fake-piper). Ist die piper-Python-Lib
    installiert, wuerde sonst der In-Process-Pfad das Fake-Modell zu laden versuchen."""
    monkeypatch.setattr(piper_mod, "_PIPER_LIB", False)


# PCM-Nutzlast des Fake-Binaries. 4000 Bytes = 2000 s16le-Samples.
FAKE_PCM = b"\x01\x02" * 2000


def _run(coro):
    return asyncio.run(coro)


def _make_fake_voice(tmp_path, sample_rate):
    """Legt ein Fake-piper-Binary + Stimmmodell (.onnx/.onnx.json) an."""
    voices = tmp_path / "voices"
    voices.mkdir()
    (voices / "de_DE-test.onnx").write_bytes(b"fake-model")
    (voices / "de_DE-test.onnx.json").write_text(
        json.dumps({"audio": {"sample_rate": sample_rate}})
    )

    # Fake-Binary: ignoriert Argumente, schreibt feste Roh-PCM-Bytes nach stdout.
    binp = tmp_path / "piper"
    binp.write_text(
        "#!/usr/bin/env python3\n"
        "import sys\n"
        f"sys.stdout.buffer.write({FAKE_PCM!r})\n"
    )
    binp.chmod(binp.stat().st_mode | stat.S_IEXEC | stat.S_IXGRP | stat.S_IXOTH)
    return str(binp), str(voices)


def test_piper_returns_pcm_without_resampling(tmp_path):
    # Native Rate == Ziel-Rate -> kein ffmpeg noetig, Bytes unveraendert.
    binp, voices = _make_fake_voice(tmp_path, sample_rate=24000)
    provider = PiperTTSProvider(binp, voices, "de_DE-test", target_rate=24000)
    pcm = _run(provider.synthesize("Hallo Welt"))
    assert pcm == FAKE_PCM


def test_piper_wraps_wav_on_request(tmp_path):
    binp, voices = _make_fake_voice(tmp_path, sample_rate=24000)
    provider = PiperTTSProvider(binp, voices, "de_DE-test", target_rate=24000)
    wav = _run(provider.synthesize("Hallo", audio_format="wav"))
    assert wav.startswith(b"RIFF") and b"WAVE" in wav[:16]


def test_piper_rejects_empty_text(tmp_path):
    binp, voices = _make_fake_voice(tmp_path, sample_rate=24000)
    provider = PiperTTSProvider(binp, voices, "de_DE-test")
    with pytest.raises(ValueError):
        _run(provider.synthesize("   "))


def test_piper_unknown_voice_raises(tmp_path):
    binp, voices = _make_fake_voice(tmp_path, sample_rate=24000)
    provider = PiperTTSProvider(binp, voices, "gibt-es-nicht")
    with pytest.raises(RuntimeError):
        _run(provider.synthesize("Hallo"))


@pytest.mark.skipif(not shutil.which("ffmpeg"), reason="ffmpeg nicht installiert")
def test_piper_resamples_when_rates_differ(tmp_path):
    # Native 16000 -> Ziel 24000: ffmpeg laeuft, Ergebnis ist gueltiges (nicht leeres) PCM.
    binp, voices = _make_fake_voice(tmp_path, sample_rate=16000)
    provider = PiperTTSProvider(binp, voices, "de_DE-test", target_rate=24000)
    pcm = _run(provider.synthesize("Hallo"))
    assert isinstance(pcm, bytes) and len(pcm) > 0


# ---------------------------------------------------------------------------
# Multi-Speaker (#id) und Geschlechts-Code-Aufloesung in synthesize().
# ---------------------------------------------------------------------------

@pytest.mark.parametrize("voice,expected", [
    ("es_ES-sharvard-medium#1", ("es_ES-sharvard-medium", 1)),
    ("es_ES-sharvard-medium#0", ("es_ES-sharvard-medium", 0)),
    ("de_DE-thorsten-high", ("de_DE-thorsten-high", None)),
    ("voice#keine-zahl", ("voice#keine-zahl", None)),   # ungueltige ID -> kein Split
    (None, (None, None)),
])
def test_parse_speaker(voice, expected):
    assert PiperTTSProvider._parse_speaker(voice) == expected


def test_synthesize_resolves_gender_code_to_voice(tmp_path, monkeypatch):
    # voice="f"/"m" + language -> echter geschlechtsspezifischer Stimmname.
    binp, voices = _make_fake_voice(tmp_path, sample_rate=24000)
    provider = PiperTTSProvider(binp, voices, "de_DE-test", target_rate=24000)

    seen = {}
    real_mp = PiperTTSProvider._model_paths

    def capture(self, voice):
        seen["voice"] = voice
        return real_mp(self, "de_DE-test")  # auf vorhandenes Fake-Modell umlenken

    monkeypatch.setattr(PiperTTSProvider, "_model_paths", capture)

    _run(provider.synthesize("Hallo", voice="f", language="de"))
    assert seen["voice"] == "de_DE-kerstin-low"    # f + de -> weiblich

    _run(provider.synthesize("Hallo", voice="m", language="de"))
    assert seen["voice"] == "de_DE-thorsten-high"  # m + de -> maennlich


def test_synthesize_multispeaker_passes_speaker_id(tmp_path, monkeypatch):
    # es:f -> sharvard#1, es:m -> sharvard#0; speaker_id muss durchgereicht werden.
    binp, voices = _make_fake_voice(tmp_path, sample_rate=24000)
    provider = PiperTTSProvider(binp, voices, "de_DE-test", target_rate=24000)

    real_mp = PiperTTSProvider._model_paths
    monkeypatch.setattr(PiperTTSProvider, "_model_paths",
                        lambda self, v: real_mp(self, "de_DE-test"))

    seen = {}

    def fake_sync(self, model, config, text, speaker_id=None):
        seen["speaker_id"] = speaker_id
        return FAKE_PCM, 24000

    monkeypatch.setattr(PiperTTSProvider, "_synthesize_sync", fake_sync)

    _run(provider.synthesize("Hola", voice="f", language="es"))
    assert seen["speaker_id"] == 1

    _run(provider.synthesize("Hola", voice="m", language="es"))
    assert seen["speaker_id"] == 0
