"""wivexx_inline_cmd_test.py — regression suite for v1.42 conversation-flow work.

Two things it locks down:
  1. `_extract_inline_commands` — mid-sentence quick-controls are pulled out of a
     conversational turn AND leave a chat remainder, while NEVER stealing a chunk
     of pure conversation (the high-precision contract).
  2. `_is_dismiss` — a dismiss must be ~the whole utterance, never a short word
     ("stop"/"sleep"/"goodbye") buried in normal speech.

Pure logic — imports wivexx_api but calls no effectors. Run:  python wivexx_inline_cmd_test.py
"""
import os, sys
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
try:
    sys.stdout.reconfigure(encoding="utf-8")
except Exception:
    pass
import wivexx_api as w

# ── 1. inline extraction ──────────────────────────────────────────────────────
# (utterance, expected commands, substring that MUST survive in the remainder)
_INLINE_POS = [
    ("man this boss is brutal, turn up the volume, got any tips for beating him?",
     ["turn up the volume"], "tips"),
    ("so i was thinking about the roman empire, mute the music, it's distracting",
     ["mute the music"], "roman empire"),
    ("hey pause the music real quick, what were we talking about?",
     ["pause the music"], "talking about"),
    ("i love this song but turn it down a bit, anyway how was your day",
     ["turn it down a bit"], "day"),
    ("ok take a screenshot and then tell me what you think of my build",
     ["take a screenshot"], "build"),
    ("we should play more aggressively, oh and open discord, the team's waiting",
     ["open discord"], "team"),
    ("the squad's about to start, launch steam, are you into shooters at all?",
     ["launch steam"], "shooters"),
    ("this is a great playlist, next song please, what genre is this even",
     ["next song"], "genre"),
    ("my hands are cold today, set a timer for 10 minutes, remind me to stretch",
     ["set a timer for 10 minutes"], "stretch"),
    ("louder please, i can barely hear the podcast over the fan",
     ["louder"], "podcast"),
    # v1.43: media handler broadened to cover video/track — these now execute
    ("pause the video for a sec, what was that scene even about",
     ["pause the video"], "scene"),
    ("stop the video, this documentary is putting me to sleep honestly",
     ["stop the video"], "documentary"),
    # v1.43: lock now has a real LockWorkStation handler → honest to extract
    ("lock the pc real quick, i'm stepping away to grab some food",
     ["lock the pc"], "food"),
    # v1.45: the timer duration must not swallow a following "and <command>"
    ("set a timer for 10 minutes and mute the sound, i really need to focus now",
     ["set a timer for 10 minutes", "mute the sound"], "focus"),
]
# Must NOT extract (pure conversation, or a pure command with no chat remainder).
_INLINE_NEG = [
    "i opened a coffee shop with my brother last year",
    "we played that new game all night and it was amazing",
    "should i play aggressive or passive in this matchup",
    "let's push mid and take their tower before they group",
    # v1.51: an inline "open/close X" whose X names no real app is a GAME callout,
    # not an app launch — must stay chat (used to try to launch a phantom "mid" app).
    "man this raid is tough, open mid and push, what do you think",
    "oh open top now and take the tower, sound good to you",
    "lets open dragon after this wave you ready to rotate",
    "hey close the gap on him we can totally win this fight",
    # v1.51: "pause the game" is not a media-pause (that only pauses your music).
    "pause the game for a sec i need to grab some water",
    "i need to open up to people more honestly",
    "what do you think i should do to close the deal at work",
    "turn up the volume",            # pure command -> normal path owns it
    "mute",
    "take a screenshot",
    "can you believe they nerfed the shotgun again",
    "i love playing jazz on the weekends with my dad",
    "the music in this game is so good honestly",
    "i was going to pause and think about my life choices",
    # v1.43 honesty: no music-content integration → "play some lofi" can't be
    # honored, so it stays chat instead of pressing play-nothing.
    "play some lofi and just vibe with me for a while",
    "put on some jazz while we talk about the weekend",
    # v1.43: Wivexx can't mute a meeting mic → never inline-extract it (bare
    # 'mute' used to leak, garbling the remainder into 'the mic, ...').
    "mute the mic, someone is vacuuming right outside my window",
    "mute the microphone real quick i have a call coming in",
    # v1.45: idioms / figurative uses of command verbs must never extract
    "i really need to open up more to people honestly",
    "lets close the deal before friday",
    "i just want to play it safe this time",
    "we should turn things around this quarter",
    "im gonna lock in my final answer now",
    "she plays the victim way too often",
    "i cant mute my feelings about this",
    "set the record straight for me",
]

# ── 1b. Spanish inline extraction ─────────────────────────────────────────────
# (utterance, expected cleaned-spanish commands, substring surviving in remainder)
_INLINE_ES_POS = [
    ("este jefe es imposible, sube el volumen, algún consejo para vencerlo?",
     ["sube el volumen"], "consejo"),
    ("oye pausa la música, de qué estábamos hablando?",
     ["pausa la música"], "hablando"),
    ("qué buen playlist, siguiente canción por favor, qué género es este",
     ["siguiente canción"], "género"),
    ("el equipo ya va a empezar, abre discord, te gustan los shooters?",
     ["abre discord"], "shooters"),
    ("me encanta esta canción pero bájale un poco, cómo estuvo tu día",
     ["bájale"], "día"),
    ("estoy aburrido, toma una captura, qué opinas de mi setup",
     ["toma una captura"], "setup"),
    ("tengo sueño hoy, sube el volumen tantito, alguna vez te cansas?",
     ["sube el volumen"], "cansas"),
]
_INLINE_ES_NEG = [
    "mi hermano y yo abrimos una cafetería el año pasado",
    "jugamos ese juego nuevo toda la noche y estuvo increíble",
    "debería jugar agresivo o pasivo en esta partida",
    # v1.52: ES game callout that looks like "open X" but names no app → stay chat
    "este jefe es imposible, abre mid y empujamos, algún consejo para ganar",
    "oye abre top ahora y toma la torre, te parece bien",
    "no me gusta que cierres la puerta tan fuerte",
    "el silencio en esta casa es raro honestamente",
    "quiero abrirme más con la gente la verdad",
    "sube el volumen",        # pure command, no chat remainder
    "pausa la música",
    # v1.45: figurative ES uses of command verbs must never extract
    "quiero abrirme más con la gente la verdad",
    "vamos a cerrar el trato mañana temprano",
    "ponle empeño a lo que haces",
    "dale una oportunidad a la vida",
    "tómate un respiro de vez en cuando",
    "no bajes la guardia nunca",
]
# v1.43: ES "COMMAND, <short justification>" — command runs, remainder empty
# (there's no real chat to converse). (utterance, expected cleaned commands)
_INLINE_ES_RUNSILENT = [
    ("activa el modo trabajo, necesito concentrarme", ["activa el modo trabajo"]),
    ("sube el volumen, no escucho", ["sube el volumen"]),
    ("pon la música otra vez, qué aburrido", ["pon la música otra vez"]),
]

# ── 2. dismiss ────────────────────────────────────────────────────────────────
_DISMISS_YES = [
    "stop", "stand down", "stand by", "that's all", "that's all for now",
    "goodbye", "goodbye wivexx", "hey wivexx, that's all", "ok that's all",
    "go to sleep", "go to sleep wivexx", "sleep", "dismissed", "you can go",
    "you can go now", "thanks jarvis", "thank you wivexx", "never mind",
    "goodnight", "stop now", "okay stand down please", "that will be all",
    # v1.43: Spanish dismiss parity (offline path never canonicalizes these)
    "eso es todo", "eso es todo por ahora", "bueno eso es todo por ahora, gracias",
    "adiós wivexx", "hasta luego", "ya puedes descansar", "buenas noches wivexx",
    "nada más gracias", "descansa wivexx",
    # v1.45: farewell trailing a dismiss + "me voy" forms
    "bueno ya me voy, gracias wivexx", "eso es todo por hoy, hasta mañana",
    "ya me voy", "nos vemos wivexx", "me despido por hoy, hasta luego",
]
_DISMISS_NO = [
    "i can't sleep lately any tips", "stop the music", "stop the timer",
    "can you stop the alarm", "we need to stop them from pushing mid",
    "i said goodbye to my friend yesterday", "the bus stop is closed today",
    "don't stop believing", "my sleep schedule is a mess honestly",
    "why do we sleep", "how do i stop procrastinating",
    "that's all i could remember about the trip",
    "goodbye is such a sad word don't you think",
    "i want to go to sleep early but i can't",
    "never mind the details just tell me the plan",
    "stop by the store on your way",
    # v1.43: Spanish conversation that must NOT dismiss (phrase buried in speech)
    "el gato descansa en el sofá todo el día",
    "adiós es una palabra muy triste la verdad",
    "no me quiero despedir todavía la verdad",
    "buenas noches fue la película que vimos ayer",
    "hasta luego le dije a mi amigo en la fiesta",
    "me voy a la tienda por pan rápido",
    "hasta mañana tengo un examen importante",
    "nos vemos en el juego más tarde entonces",
]


def main():
    fails = 0
    print("=== inline extraction: positives ===")
    for text, exp, rem_sub in _INLINE_POS:
        r = w._extract_inline_commands(text)
        ok = (r and [c.lower() for c in r["commands"]] == [c.lower() for c in exp]
              and rem_sub.lower() in r["remainder"].lower())
        if not ok:
            fails += 1
            print(f"  FAIL {text!r} -> {r}")
    print(f"  {len(_INLINE_POS) - sum(1 for t,e,s in _INLINE_POS if not (lambda r: r and [c.lower() for c in r['commands']]==[c.lower() for c in e] and s.lower() in r['remainder'].lower())(w._extract_inline_commands(t)))}/{len(_INLINE_POS)} ok")

    print("=== inline extraction: negatives (must NOT extract) ===")
    nok = 0
    for text in _INLINE_NEG:
        if w._extract_inline_commands(text) is None:
            nok += 1
        else:
            fails += 1
            print(f"  LEAK {text!r} -> {w._extract_inline_commands(text)}")
    print(f"  {nok}/{len(_INLINE_NEG)} ok")

    print("=== spanish inline: positives ===")
    for text, exp, rem_sub in _INLINE_ES_POS:
        r = w._extract_inline_commands_es(text)
        ok = (r and [c.lower() for c in r["commands"]] == [c.lower() for c in exp]
              and rem_sub.lower() in r["remainder"].lower())
        if not ok:
            fails += 1
            print(f"  FAIL {text!r} -> {r}")
    print(f"  {sum(1 for t,e,s in _INLINE_ES_POS if (lambda r: r and [c.lower() for c in r['commands']]==[c.lower() for c in e] and s.lower() in r['remainder'].lower())(w._extract_inline_commands_es(t)))}/{len(_INLINE_ES_POS)} ok")

    print("=== spanish inline: negatives (must NOT extract) ===")
    esn = 0
    for text in _INLINE_ES_NEG:
        if w._extract_inline_commands_es(text) is None:
            esn += 1
        else:
            fails += 1
            print(f"  LEAK {text!r} -> {w._extract_inline_commands_es(text)}")
    print(f"  {esn}/{len(_INLINE_ES_NEG)} ok")

    print("=== spanish inline: run-silent (command runs, empty remainder) ===")
    rsn = 0
    for text, exp in _INLINE_ES_RUNSILENT:
        r = w._extract_inline_commands_es(text)
        ok = (r and [c.lower() for c in r["commands"]] == [c.lower() for c in exp]
              and r["remainder"].strip() == "")
        if ok:
            rsn += 1
        else:
            fails += 1
            print(f"  FAIL {text!r} -> {r}")
    print(f"  {rsn}/{len(_INLINE_ES_RUNSILENT)} ok")

    print("=== dismiss: should dismiss ===")
    dok = 0
    for s in _DISMISS_YES:
        if w._is_dismiss(s):
            dok += 1
        else:
            fails += 1
            print(f"  MISS {s!r}")
    print(f"  {dok}/{len(_DISMISS_YES)} ok")

    print("=== dismiss: must NOT dismiss (conversation) ===")
    cok = 0
    for s in _DISMISS_NO:
        if not w._is_dismiss(s):
            cok += 1
        else:
            fails += 1
            print(f"  FALSE-DISMISS {s!r}")
    print(f"  {cok}/{len(_DISMISS_NO)} ok")

    print("\n=== RESULT:", "ALL PASS" if fails == 0 else f"FAIL ({fails})", "===")
    sys.exit(1 if fails else 0)


if __name__ == "__main__":
    main()
