# Long Spanish conversation sim: a ~5-minute monologue (normal speech, FULL of
# words that are ALSO command verbs — abre/para/pon/busca/cambia/sube/baja/
# cierra/cuenta…) mixed with real commands + multi-commands. Runs the REAL
# offline pipeline and asserts: monologue turns -> CHAT (no command misfires),
# command turns -> the right command, multi -> both.
import ast
import os
import re
import sys

PROJ = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, PROJ)
import wivexx_i18n as i18n
import wivexx_intent_gate as gate

FLAGS = {"IGNORECASE": re.IGNORECASE, "I": re.IGNORECASE, "VERBOSE": re.VERBOSE,
         "X": re.VERBOSE, "DOTALL": re.DOTALL, "MULTILINE": re.MULTILINE}
def flag_val(n):
    if isinstance(n, ast.Attribute): return FLAGS.get(n.attr, 0)
    if isinstance(n, ast.BinOp): return flag_val(n.left) | flag_val(n.right)
    return 0
RX = {}
tree = ast.parse(open(os.path.join(PROJ, "wivexx_api.py"), encoding="utf-8").read())
for n in ast.walk(tree):
    if (isinstance(n, ast.Assign) and len(n.targets) == 1 and isinstance(n.targets[0], ast.Name)
            and n.targets[0].id.endswith("_RE") and isinstance(n.value, ast.Call)
            and getattr(n.value.func, "attr", "") == "compile" and n.value.args
            and isinstance(n.value.args[0], ast.Constant)):
        try: RX[n.targets[0].id] = re.compile(n.value.args[0].value, flag_val(n.value.args[1]) if len(n.value.args) > 1 else 0)
        except re.error: pass

REG = {"discord","spotify","chrome","steam","obs","lightroom","premiere","blender","notepad","word","excel"}
ORDER = [
    ("selfclose","_SELF_CLOSE_RE"),("capability","_CAPABILITY_QUERY_RE"),("timer_status","_ACTIVE_TIMER_QUERY_RE"),
    ("time","_CURRENT_TIME_RE"),("date","_CURRENT_DATE_RE"),("version","_VERSION_QUERY_RE"),("tldr","_LATEST_UPDATE_RE"),
    ("changelog","_CHANGELOG_RE"),("battery","_BATTERY_RE"),("workmode_on","_WORK_MODE_ON_RE"),("workmode_off","_WORK_MODE_OFF_RE"),
    ("remember","_REMEMBER_RE"),("forget","_FORGET_RE"),("recall","_WHAT_REMEMBER_RE"),("goal_add","_ADD_GOAL_RE"),
    ("goal_done","_COMPLETE_GOAL_RE"),("goals","_GOALS_QUERY_RE"),("track","_TRACK_SOCIAL_RE"),("social","_SOCIAL_REPORT_RE"),
    ("caption","_CAPTION_RE"),("deliver","_DELIVER_RE"),("disk","_DISK_ANALYZE_RE"),("pending","_PENDING_QUERY_RE"),
    ("shutdown_cancel","_SHUTDOWN_CANCEL_RE"),("shutdown","_SHUTDOWN_RE"),("joke","_JOKE_RE"),("youtube","_YOUTUBE_RE"),
    ("note","_NOTE_TRIGGER_RE"),("media","_MEDIA_KEY_RE"),("audio_out","_AUDIO_OUTPUT_RE"),("volume","_VOLUME_RE"),
    ("weather","_WEATHER_TRIGGER_RE"),("clip","_CLIP_RE"),("cal_create","_CAL_CREATE_RE"),("cal_query","_CAL_QUERY_RE"),
    ("gaming","_GAMING_MODE_RE"),("lightroom","_LR_IMPORT_RE"),("speedtest","_SPEEDTEST_RE"),("transcribe","_TRANSCRIBE_RE"),
    ("optimize","_OPTIMIZE_RE"),("hud","_PERF_HUD_RE"),("enroll","_ENROLL_RE"),("label","_SCREEN_LABEL_RE"),
    ("usage","_USAGE_QUERY_RE"),("addapp","_ADD_APP_RE"),("saveconv","_SAVE_CONV_RE"),
]
def route_en(q):
    ql = q.lower().strip(" .,!?")
    for name, rxn in ORDER:
        rx = RX.get(rxn)
        if rx and (rx.search(ql) or rx.match(ql)):
            if name == "winctl": continue
            return name
    if RX.get("_TIMER_TRIGGER_RE") and RX["_TIMER_TRIGGER_RE"].search(ql): return "timer"
    if RX.get("_WINCTL_GATE_RE") and RX["_WINCTL_GATE_RE"].search(ql):
        if any(RX.get(k) and RX[k].search(ql) for k in ("_WIN_MOVE_RE","_WIN_MINALL_RE","_WIN_ACT_RE")): return "winctl"
    if re.search(r"\bscreenshot\b", ql): return "screenshot"
    m = RX.get("_CLOSE_VERB_RE") and RX["_CLOSE_VERB_RE"].search(ql)
    if m:
        t = m.group(1).lower()
        if re.match(r"^(all|everything)", t): return "close_all"
        if any(v in t for v in REG): return "close_app"
    m = RX.get("_APP_INTENT_RE") and RX["_APP_INTENT_RE"].search(ql)
    if m and any(v in m.group(1).lower() for v in REG): return "open_app"
    if RX.get("_CALC_RE") and RX["_CALC_RE"].search(ql): return "calc"
    return "CHAT"

def route(s):
    lang = i18n.detect_lang(s)
    if lang == "en":
        q = s
    else:
        q = i18n.quick_english(s) or s   # offline; no API in test
    if gate.classify(q)["kind"] == "chat":
        return "CHAT"
    return route_en(q)

# ── the 5-minute monologue: normal speech, PACKED with command-homograph words.
#    EVERY line here must be CHAT. ──
MONOLOGUE = [
    "déjame contarte cómo me fue el fin de semana en el pueblo de mis abuelos",
    "el sábado me levanté temprano y abrí las ventanas para que entrara el aire fresco",
    "mi abuela ya estaba en la cocina preparando el desayuno para toda la familia",
    "para mí lo más importante de esos días es pasar tiempo con ellos",
    "mi abuelo siempre dice que uno tiene que abrir bien los ojos ante las oportunidades",
    "después del desayuno salimos a caminar y subimos hasta la colina más alta",
    "la vista desde arriba era increíble, se podía ver todo el valle",
    "cuando bajamos, mi primo puso música y nos pusimos a bailar en el patio",
    "mi tía cambió de tema y empezó a hablar de política, como siempre",
    "yo busqué la manera de escaparme de esa conversación tan aburrida",
    "por la tarde jugamos un partido de fútbol en la cancha del pueblo",
    "mi equipo iba ganando pero al final nos remontaron en los últimos minutos",
    "el árbitro paró el partido varias veces porque había mucha gente en la cancha",
    "para el domingo teníamos planeado ir al río a pescar",
    "cerré los ojos un momento en la orilla y casi me quedo dormido",
    "el sol pegaba fuerte así que me puse crema y me senté bajo un árbol",
    "mi hermano cuenta unas historias buenísimas sobre cuando era joven",
    "recuerda mucho los veranos de su infancia con nostalgia",
    "la comida estuvo espectacular, mi abuela cocina como nadie",
    "corté un poco de pan y lo mojé en la salsa, estaba delicioso",
    "por la noche encendimos una fogata y contamos historias de miedo",
    "mi prima apagó las luces de la casa para que se viera mejor el fuego",
    "termina uno esos días agotado pero feliz, con la batería recargada",
    "cambia mucho el ritmo de vida en el campo comparado con la ciudad",
    "sube uno de peso seguro con todo lo que se come allí",
    "el lunes tuve que volver y cerrar ese capítulo tan bonito",
    "guardo esos recuerdos con mucho cariño en el corazón",
    "lo único malo fue el tráfico de regreso, tardamos horas",
    "pon te a pensar lo rápido que pasa el tiempo cuando la estás pasando bien",
    "en fin, fue un fin de semana para no olvidar jamás",
    "creo que voy a organizar otro viaje pronto, quizás para el próximo mes",
    "¿tú qué opinas, debería llevar a más gente la próxima vez?",
    "me encantaría que conocieras a mi familia algún día",
    "son gente muy sencilla pero con un corazón enorme",
    "bueno, ya te conté demasiado, cuéntame ahora tú qué has hecho",
]
# ── real commands interspersed ──
COMMANDS = [
    ("abre spotify", "open_app"), ("ejecuta discord", "open_app"), ("lanza steam", "open_app"),
    ("pon música relajante en youtube", "youtube"), ("sube el volumen", "volume"),
    ("bájale al volumen", "volume"), ("toma una captura de pantalla", "screenshot"),
    ("pon un temporizador de 20 minutos", "timer"), ("qué hora es", "time"),
    ("cuéntame un chiste", "joke"), ("pausa la música", "media"), ("siguiente canción", "media"),
    ("cierra discord", "close_app"), ("cierra todas las aplicaciones", "close_all"),
    ("cuánta batería queda", "battery"), ("activa el modo juego", "gaming"),
    ("recuérdame llamar al dentista mañana", "timer"), ("qué tengo en el calendario mañana", "cal_query"),
    ("apaga la pc en 30 minutos", "shutdown"), ("silencio", "volume"),
    # ── SYNONYM VARIATIONS — many ways to say the same command (like EN) ──
    ("arranca chrome", "open_app"), ("corre obs", "open_app"), ("inicia premiere", "open_app"),
    ("entra a discord", "open_app"), ("dale a spotify", "open_app"),
    ("quita discord", "close_app"), ("mata steam", "close_app"), ("termina chrome", "close_app"),
    ("sal de discord", "close_app"),
    ("reproduce jazz en youtube", "youtube"), ("busca tutoriales en youtube", "youtube"),
    ("pantallazo", "screenshot"), ("haz una captura", "screenshot"), ("hazme una foto de la pantalla", "screenshot"),
    ("aumenta el volumen", "volume"), ("súbele", "volume"), ("más fuerte", "volume"),
    ("reduce el sonido", "volume"), ("bájale", "volume"), ("quita el sonido", "volume"),
    ("pon el volumen al 30 por ciento", "volume"),
    ("programa un temporizador de 5 minutos", "timer"), ("echa un temporizador de una hora", "timer"),
    ("cuéntame otro chiste", "joke"), ("detén la música", "media"), ("canción anterior", "media"),
    ("cierra todas las ventanas", "close_all"),
]
# extra homograph traps (still must be CHAT)
MONOLOGUE += [
    "arranca el coche que ya es tarde para la reunión",
    "corre que se nos hace tarde para el cine",
    "quita esa cara de preocupación, todo va a salir bien",
    "termina de comer antes de que se enfríe la sopa",
    "reproduce en tu mente ese momento tan especial",
    "aumenta la dificultad del juego si te aburres",
    "programa tu día con calma y verás que rinde más",
    "detén te un momento y respira hondo",
    "entra en calor antes de hacer ejercicio",
    "busca siempre el lado bueno de las cosas",
]
MULTI = [
    ("abre discord y pon música en youtube", ("open_app","youtube")),
    ("toma una captura y sube el volumen", ("screenshot","volume")),
    ("abre discord y sube el volumen", ("open_app","volume")),
]

fp = []   # monologue lines that misfired a command
for s in MONOLOGUE:
    r = route(s)
    if r != "CHAT":
        fp.append((s, r, i18n.quick_english(s)))
cmd_bad = [(s,w,route(s)) for s,w in COMMANDS if not route(s).startswith(w)]
# Mirror wivexx_api._split_spanish_compound: split the WHOLE utterance on the ES
# conjunction and require EVERY fragment to canonicalize to a command, THEN route
# each canonicalized English half. (The old test pre-split " y " and routed the
# raw parts — which masked that the real pipeline greedily ate the tail instead
# of splitting, i.e. Spanish compounds silently didn't work.)
_ES_CONJ = re.compile(r"\s+y\s+luego\s+|\s+y\s+despu[eé]s(?:\s+de\s+eso)?\s+|\s+luego\s+|\s+y\s+", re.I)
multi_bad = []
for s,(a,b) in MULTI:
    frags = [p.strip() for p in _ES_CONJ.split(s) if p.strip()]
    canon = [i18n.quick_english(f) for f in frags]
    if len(frags) != 2 or not all(canon):
        multi_bad.append((s, ("NOT-A-COMPOUND", canon))); continue
    rs = tuple(route_en(c) for c in canon)
    if not (rs[0].startswith(a) and rs[1].startswith(b)):
        multi_bad.append((s, rs))

print(f"monologue lines: {len(MONOLOGUE)}   command misfires (FALSE POSITIVES): {len(fp)}")
for s,r,q in fp: print(f"   FALSE-POS: {s!r} -> {r}  (canon={q!r})")
print(f"commands routed: {len(COMMANDS)-len(cmd_bad)}/{len(COMMANDS)}")
for s,w,r in cmd_bad: print(f"   CMD MISS: {s!r} wanted {w} got {r}")
print(f"multi-commands: {len(MULTI)-len(multi_bad)}/{len(MULTI)}")
for s,rs in multi_bad: print(f"   MULTI MISS: {s!r} -> {rs}")
ok = not fp and not cmd_bad and not multi_bad
print("=== RESULT:", "ALL PASS" if ok else "FAIL")
sys.exit(0 if ok else 1)
