"""Tone-aware TTS (TONE_TTS): the sensing → delivery bridge, in pure logic.

Two halves of the "voice presence" move, tested without any audio or model:
  1. voice.prosody's rolling ToneState — the EMA smooths Ahmed's recent vocal
     energy so one loud word can't flip the mood, and laughter recency fades.
  2. voice.tts_pocket's clip picker (_biased_stem) — a specific brain tag wins;
     a bouncy tag softens to `warm` when he's plainly down (never urgent/
     annoyed/shocked); an untagged line follows his tone; TONE_TTS=0 replays
     today's tag-only pick byte-for-byte.

Run: `python -m pytest test_tone.py`  (or `python test_tone.py` standalone).
"""

from types import SimpleNamespace

from voice import prosody
from voice.prosody import ToneState
from voice import tts_pocket
from voice.tts_pocket import _biased_stem, _NEVER_DAMP, _TAG_TO_STEM


# --- helpers ---------------------------------------------------------------

def _reset() -> None:
    """Neutral tone state — the picker tests that read the global start here."""
    prosody._TONE_STATE = ToneState()


def _pub(*tags: str, laugh: bool = False) -> ToneState:
    """Fold one synthetic read (just its tags) into the rolling tone state."""
    prosody._publish_tone(prosody.Prosody(tags=list(tags),
                                          feats={"laugh": laugh}))
    return prosody.tone_state()


# --- 1. ToneState EMA behaviour -------------------------------------------

def test_ema_settles_and_one_word_cannot_flip():
    _reset()
    for _ in range(3):            # three animated turns
        _pub("animated")
    assert prosody.tone_state().up, "sustained energy should read animated"
    st = _pub("flat")             # one flat word after a settled high
    assert not st.down, "a single flat turn must not flip a settled mood"


def test_flat_and_slow_read_down():
    _reset()
    st = _pub("flat", "slow")
    assert not st.low, "one flat turn is 'down' at most, not yet 'really flat'"
    for _ in range(3):            # sustained flatness deepens toward 'low'
        st = _pub("flat", "slow")
    assert st.down and st.low and st.slow


def test_laughter_recency_decays():
    _reset()
    assert _pub("laughing", laugh=True).laughing
    assert _pub().laughing, "laughter should still register one turn later"
    assert not _pub().laughing, "…then fade"


# --- 2. tag-wins rule ------------------------------------------------------

def test_specific_tag_wins_over_tone():
    up = ToneState(energy=0.9)     # animated
    for tag, stem in [("calm", "calm"), ("dry", "dry"),
                      ("curious", "curious"), ("sad", "sad")]:
        assert _biased_stem(tag, stem, up, 12) == stem


# --- 3. damping matrix (incl. the never-damp list) -------------------------

def test_bounce_softens_only_when_down():
    down = ToneState(energy=-0.6)
    up = ToneState(energy=0.6)
    neutral = ToneState()
    assert _biased_stem("excited", "excited", down, 12) == "warm"
    assert _biased_stem("amused", "amused", down, 12) == "warm"
    assert _biased_stem("excited", "excited", up, 12) == "excited"
    assert _biased_stem("excited", "excited", neutral, 12) == "excited"


def test_never_damp_list_holds_at_deep_down_late_night():
    deep_down = ToneState(energy=-1.2, pace=-1.0)   # flat, tired, 3am
    for tag in _NEVER_DAMP:
        stem = _TAG_TO_STEM.get(tag)
        assert stem is not None, f"{tag} should map to a clip"
        assert _biased_stem(tag, stem, deep_down, 3) == stem, \
            f"{tag} must never soften — the meter earns its shout"


# --- 4. untagged line follows tone ----------------------------------------

def test_untagged_follows_tone():
    assert _biased_stem(None, None, ToneState(), 12) is None      # base voice
    assert _biased_stem(None, None, ToneState(energy=0.6), 12) == "amused"
    assert _biased_stem(None, None, ToneState(energy=1.0), 12) == "excited"
    assert _biased_stem(None, None, ToneState(laugh=1.0), 12) == "amused"
    assert _biased_stem(None, None, ToneState(energy=-0.6), 12) == "warm"
    assert _biased_stem(None, None, ToneState(energy=-1.0), 12) == "tender"


def test_late_night_flat_slow_is_softest():
    tone = ToneState(energy=-0.6, pace=-0.6)     # down + slow, not deep-flat
    assert _biased_stem(None, None, tone, 2) == "calm"    # 2am → soft
    assert _biased_stem(None, None, tone, 12) == "warm"   # noon → ordinary down


# --- 5. flag off = passthrough (today's behaviour, byte-for-byte) ----------

def test_flag_gates_the_bias():
    prosody._TONE_STATE = ToneState(energy=-0.6)   # he's clearly down
    fake = SimpleNamespace(
        _plain=False,
        _states={"": "BASE", "excited": "EXC", "warm": "WARM"})
    # TONE_TTS off: the tag maps straight through, tone ignored
    fake._tone_tts = False
    assert tts_pocket.PocketTTS._state_for(fake, "[excited] hi") == ("hi", "EXC")
    # TONE_TTS on: the bouncy tag softens because he's down
    fake._tone_tts = True
    assert tts_pocket.PocketTTS._state_for(fake, "[excited] hi") == ("hi", "WARM")
    _reset()


if __name__ == "__main__":  # standalone runner (repo convention: python test_x.py)
    import traceback
    fns = [v for k, v in sorted(globals().items()) if k.startswith("test_")]
    fails = 0
    for fn in fns:
        try:
            fn()
            print(f"PASS  {fn.__name__}")
        except Exception:  # noqa: BLE001
            fails += 1
            print(f"FAIL  {fn.__name__}")
            traceback.print_exc()
    print(f"\n{len(fns) - fails}/{len(fns)} passed")
    raise SystemExit(1 if fails else 0)
