"""The synthesis engine is a choice, and every part of the pipeline must survive making it.

WHY THIS FILE EXISTS. Commentary audio used to reach exactly one vendor by exactly one path, and the
office deployment has no credential for that vendor and is not getting one. Introducing a second
engine is only safe if three things hold, and each of them is a way the change could break quietly
rather than loudly:

  * the default is unchanged, so an existing deployment behaves identically after the upgrade;
  * the renderer keeps finding the audio, which means the index must carry the engine's OWN file
    extension rather than a hard-coded `.mp3`;
  * the broadcast's one-male-one-female voice standard survives the switch, because a voice that
    changes between engines is a different person arriving mid-race.
"""
from __future__ import annotations

import sys
from pathlib import Path

import pytest

_LIVE = Path(__file__).resolve().parents[2] / "live"
if str(_LIVE) not in sys.path:
    sys.path.insert(0, str(_LIVE))

from tts import engines as E  # noqa: E402


@pytest.fixture
def voice_dir(tmp_path):
    """A voice directory with the curated English models present but empty.

    `voices()` only asks whether a model FILE exists, so the selection logic is testable without
    onnxruntime, a 60 MB download, or a machine that can run either.
    """
    for name in E.LOCAL_ROLE_VOICES["en"].values():
        (tmp_path / f"{name}.onnx").write_bytes(b"")
    return tmp_path


def test_the_default_engine_is_still_the_cloud(monkeypatch):
    """An existing deployment must not change behaviour by taking this commit."""
    monkeypatch.delenv("VR_TTS_ENGINE", raising=False)
    called = []
    monkeypatch.setattr(E, "GoogleEngine", lambda: called.append("google") or "engine")
    assert E.make_engine() == "engine"
    assert called == ["google"]


def test_an_unknown_engine_is_refused_rather_than_guessed(monkeypatch):
    """A typo must not silently fall back to the expensive one."""
    monkeypatch.setenv("VR_TTS_ENGINE", "pipper")
    with pytest.raises(RuntimeError, match="pipper"):
        E.make_engine()


def test_the_local_engine_is_selected_by_name(monkeypatch, voice_dir):
    monkeypatch.setenv("VR_TTS_ENGINE", "piper")
    monkeypatch.setattr(E, "VOICE_DIR", voice_dir)
    assert E.make_engine(on_log=lambda *_: None).name == "piper"


def test_a_missing_voice_directory_is_a_clear_error(tmp_path):
    with pytest.raises(RuntimeError, match="VR_PIPER_VOICES"):
        E.PiperEngine(voice_dir=tmp_path / "nowhere", on_log=lambda *_: None)


def test_the_local_engine_writes_wav_and_the_cloud_engine_mp3():
    """The index carries this, and the renderer reads the index rather than assuming a format.

    A local engine that claimed `.mp3` would write WAV bytes under an MP3 name — which most browsers
    would still play, so the mistake would survive a listening check and surface as a mystery later.
    """
    assert E.PiperEngine.suffix == ".wav"
    assert E.GoogleEngine.suffix == ".mp3"


def test_the_local_set_keeps_one_male_and_one_female_voice(voice_dir):
    """The broadcast standard: Male Caller for facts, Female Analyst for the story, held stable."""
    engine = E.PiperEngine(voice_dir=voice_dir, on_log=lambda *_: None)
    voices = engine.voices("en")
    assert voices["lead"] != voices["tactical"]
    assert voices["m"] == voices["lead"] and voices["f"] == voices["tactical"]
    # A rules verdict must not arrive in the voice that just described the geometry.
    assert voices["rules"] not in {voices["lead"], voices["tactical"]}


def test_the_same_set_comes_back_every_time(voice_dir):
    """A voice that drifts between renders reads as a different commentator joining mid-race."""
    engine = E.PiperEngine(voice_dir=voice_dir, on_log=lambda *_: None)
    assert engine.voices("en") == engine.voices("en")


def test_an_override_naming_a_cloud_voice_is_ignored_with_a_word(monkeypatch, voice_dir):
    """The commonest arrival here is an operator carrying `TTS_VOICE_EN_LEAD=en-GB-Studio-B` across
    from the cloud deployment. Refusing to speak because a voice is spelled for the other engine is
    worse than speaking in the curated voice — but doing it silently is worse still."""
    monkeypatch.setenv("TTS_VOICE_EN_LEAD", "en-GB-Studio-B")
    said = []
    engine = E.PiperEngine(voice_dir=voice_dir, on_log=said.append)
    assert engine.voices("en")["lead"] == E.LOCAL_ROLE_VOICES["en"]["lead"]
    assert any("en-GB-Studio-B" in line for line in said)


def test_an_override_naming_a_present_model_is_honoured(monkeypatch, voice_dir):
    (voice_dir / "en_GB-alba-medium.onnx").write_bytes(b"")
    monkeypatch.setenv("TTS_VOICE_EN_TACTICAL", "en_GB-alba-medium")
    engine = E.PiperEngine(voice_dir=voice_dir, on_log=lambda *_: None)
    assert engine.voices("en")["tactical"] == "en_GB-alba-medium"


def test_a_language_with_no_local_voices_says_so(voice_dir):
    """Swedish is configured throughout the product but produces no output today. Asking the local
    engine for it must fail with a sentence, not a KeyError from inside a dictionary."""
    engine = E.PiperEngine(voice_dir=voice_dir, on_log=lambda *_: None)
    with pytest.raises(RuntimeError, match="sv"):
        engine.voices("sv")


def test_a_missing_model_is_named_rather_than_failing_at_synthesis(tmp_path):
    """Half a voice set is the state a partial download leaves behind, and the useful moment to
    notice is when the set is chosen — not three lines into a live programme."""
    only = E.LOCAL_ROLE_VOICES["en"]["lead"]
    (tmp_path / f"{only}.onnx").write_bytes(b"")
    engine = E.PiperEngine(voice_dir=tmp_path, on_log=lambda *_: None)
    with pytest.raises(RuntimeError, match="missing Piper models"):
        engine.voices("en")
