# Spanish command coverage test: for every command, take a natural SPANISH
# phrasing and run it through the REAL pipeline offline —
#   detect_lang (must be 'es') -> quick_english (offline canonicalization) ->
#   intent gate -> the actual waterfall trigger regexes (AST-extracted).
# A command "works in Spanish offline" only if quick_english canonicalizes it
# AND the English result routes to the right command. Anything relying on the
# remote translator is a fragility gap to close by extending _ES2EN.
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
try:
    import wivexx_calc as _calc_mod   # real calc/converter (routing uses answer(), not a regex)
except Exception:
    _calc_mod = None

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

REGISTRY = {"discord","spotify","chrome","steam","obs","lightroom","premiere","blender","notepad"}

# ordered (route, trigger regex) — mirrors the waterfall order
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"),("wol","_WOL_QUERY_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"),("untrack","_UNTRACK_SOCIAL_RE"),("social","_SOCIAL_REPORT_RE"),
    ("caption","_CAPTION_RE"),("deliver","_DELIVER_RE"),("disk","_DISK_ANALYZE_RE"),
    ("pending","_PENDING_QUERY_RE"),("lock","_LOCK_RE"),("sleep","_SLEEP_RE"),
    ("shutdown_cancel","_SHUTDOWN_CANCEL_RE"),("shutdown","_SHUTDOWN_RE"),
    ("joke","_JOKE_RE"),("random","_RANDOM_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"),("winctl","_WINCTL_GATE_RE"),
    ("cal_create","_CAL_CREATE_RE"),("cal_query","_CAL_QUERY_RE"),("gaming","_GAMING_MODE_RE"),
    ("lightroom","_LR_IMPORT_RE"),("osd","_OSD_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"),("weather2","_WEATHER_TRIGGER_RE"),
]

def route_en(q_en):
    ql = q_en.lower().strip(" .,!?")
    for name, rxn in ORDER:
        rx = RX.get(rxn)
        if rx and (rx.search(ql) or rx.match(ql)):
            # winctl is a GATE — the handler falls through (returns None) unless a
            # real window verb (move/minimize/maximize/focus) is present, so mimic that
            if name == "winctl":
                wv = any(RX.get(k) and RX[k].search(ql) for k in ("_WIN_MOVE_RE","_WIN_MINALL_RE","_WIN_ACT_RE"))
                if not wv:
                    continue
            return name
    if RX.get("_TIMER_TRIGGER_RE") and RX["_TIMER_TRIGGER_RE"].search(ql): return "timer"
    if re.search(r"\bscreenshot\b|take a screenshot", ql): return "screenshot"
    m = RX.get("_CLOSE_VERB_RE") and RX["_CLOSE_VERB_RE"].search(ql)
    if m:
        tgt = m.group(1).lower()
        if re.match(r"^(all|everything)", tgt): return "close_all"
        if any(v in tgt for v in REGISTRY): 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 REGISTRY): return "open_app"
    if RX.get("_CLIP_RE") and RX["_CLIP_RE"].search(ql): return "clip"
    if RX.get("_CALC_RE") and RX["_CALC_RE"].search(ql): return "calc"
    # the real app routes calc via the module's answer(), not a regex
    if _calc_mod is not None:
        try:
            if _calc_mod.answer(ql): return "calc"
        except Exception:
            pass
    return "CHAT"

def process(spanish):
    # model the REAL pipeline: canonicalize ONLY when detected non-English
    lang = i18n.detect_lang(spanish)
    if lang == "en":
        q, canon = spanish, False       # real code would NOT canonicalize
    else:
        q = i18n.quick_english(spanish)
        canon = q is not None
        q = q or spanish
    if gate.classify(q)["kind"] == "chat":
        return lang, canon, q, "CHAT"
    return lang, canon, q, route_en(q)

# (spanish phrase, expected route) — a natural Spanish phrasing per command
CASES = [
    ("abre discord", "open_app"),
    ("ábreme spotify", "open_app"),
    ("cierra chrome", "close_app"),
    ("cierra todas las apps", "close_all"),
    ("toma una captura de pantalla", "screenshot"),
    ("pon un temporizador de 10 minutos", "timer"),
    ("cuánto tiempo queda en el temporizador", "timer_status"),
    ("qué hora es", "time"),
    ("qué día es hoy", "date"),
    ("qué tiempo hace hoy", "weather"),
    ("cómo está el clima", "weather"),
    ("qué versión eres", "version"),
    ("qué hay de nuevo", "tldr"),
    ("muéstrame el registro de cambios", "changelog"),
    ("cuánta batería queda", "battery"),
    ("estado de la batería", "battery"),
    ("activa el modo trabajo", "workmode_on"),
    ("desactiva el modo trabajo", "workmode_off"),
    ("recuerda que aparco en el nivel 3", "remember"),
    ("olvida lo que te dije del gimnasio", "forget"),
    ("qué sabes de mí", "recall"),
    ("qué recuerdas sobre mis proyectos", "recall"),
    ("añade un objetivo de publicar tres reels", "goal_add"),
    ("completa mi primer objetivo", "goal_done"),
    ("cuáles son mis objetivos", "goals"),
    ("sigue a carlos.foto en instagram", "track"),
    ("dame mi informe de redes sociales", "social"),
    ("crea un subtítulo para este video", "caption"),
    ("entrega las fotos al cliente", "deliver"),
    ("qué está llenando mi disco", "disk"),
    ("tengo tareas pendientes", "pending"),
    ("apaga la pc en 20 minutos", "shutdown"),
    ("cancela el apagado", "shutdown_cancel"),
    ("bloquea la pc", "lock"),
    ("bloquea la pantalla", "lock"),
    ("pon la pc a dormir", "sleep"),
    ("duerme la computadora", "sleep"),
    ("suspende la pc", "sleep"),
    ("hiberna la pc", "sleep"),
    ("corta el sonido", "volume"),
    ("bloquea mi computadora", "lock"),
    ("lanza una moneda", "random"),
    ("échame un volado", "random"),
    ("cara o águila", "random"),
    ("tira un dado", "random"),
    # v1.44 Mexican-Spanish phrasing batch
    ("un volado porfa", "random"),
    ("échame un dado", "random"),
    ("tírame los dados", "random"),
    ("captura la pantalla", "screenshot"),
    ("toma captura de mi build", "screenshot"),
    ("ponle play", "media"),
    ("dale pausa", "media"),
    ("sáltate esta canción", "media"),
    ("regresa la canción", "media"),
    ("quítale el sonido", "volume"),
    ("ponlo más fuerte", "volume"),
    ("menos volumen", "volume"),
    ("ponme una alarma de 5 minutos", "timer"),
    ("qué hora tienes", "time"),
    ("qué fecha es hoy", "date"),
    ("bloquea la compu", "lock"),
    ("activa el modo gamer", "gaming"),
    # v1.46: bilingual politeness + "en" volume connector
    ("sube el volumen please", "volume"),
    ("por favor pon la música", "media"),
    ("pon el volumen en 60", "volume"),
    ("cuéntame un chiste", "joke"),
    ("pon lofi beats en youtube", "youtube"),
    ("busca gatos en youtube", "youtube"),
    ("toma una nota: comprar pilas", "note"),
    ("anota que le debo 20 a marco", "note"),
    ("cambia la salida a los altavoces", "audio_out"),
    ("pausa la música", "media"),
    ("siguiente canción", "media"),
    ("para la música", "media"),
    ("sube el volumen", "volume"),
    ("baja el volumen", "volume"),
    ("más fuerte", "volume"),
    ("pon el volumen al 50 por ciento", "volume"),
    ("resume mi portapapeles", "clip"),
    ("traduce el portapapeles al inglés", "clip"),
    ("mueve esta ventana al otro monitor", "winctl"),
    ("minimiza todo excepto chrome", "winctl"),
    ("agenda un almuerzo mañana a la una", "cal_create"),
    ("qué tengo en el calendario mañana", "cal_query"),
    ("activa el modo juego", "gaming"),
    ("importa a lightroom", "lightroom"),
    ("muestra la superposición de rendimiento", "hud"),
    ("haz una prueba de velocidad", "speedtest"),
    ("transcribe esta grabación", "transcribe"),
    ("libera memoria", "optimize"),
    ("optimiza mi pc", "optimize"),
    ("registra mi voz", "enroll"),
    ("numera la pantalla", "label"),
    ("haz clic en 7", "click"),
    ("cuántos tokens he usado hoy", "usage"),
    ("qué puedes hacer", "capability"),
    ("adiós wivexx", "selfclose"),
    ("añade la app blender", "addapp"),
    ("guarda la conversación", "saveconv"),
    ("repítelo", "repeat"),
    ("cuánto es 15 por ciento de 380", "calc"),
    # unit conversion must translate units so it routes to the deterministic
    # calculator (NOT the brain, which fabricates wrong math)
    ("convierte 5 millas a kilómetros", "convert"),
    ("convierte 32 grados fahrenheit a celsius", "convert"),
    ("convierte 60 millas por hora a kmh", "convert"),
    # fractional-time timers + volume max/min (v1.40)
    ("pon un temporizador de media hora", "timer"),
    ("pon un temporizador de hora y media", "timer"),
    ("pon el volumen al máximo", "volume"),
    ("pon el volumen al mínimo", "volume"),
    ("silencia el volumen", "volume"),
    # more ES synonyms (v1.41)
    ("apaga el equipo en 30 minutos", "shutdown"),
    ("añade una meta de correr cada día", "goal_add"),
    ("apunta que tengo que llamar al dentista", "note"),
    ("qué temperatura hace", "weather"),
    ("ciérrame todo", "close_all"),
    ("qué tengo agendado para mañana", "cal_query"),
    # Spanish math operators (v1.42)
    ("cuánto es 10 menos 3", "calc"),
    ("cuánto es 2 por 8", "calc"),
    ("cuánto es 100 entre 4", "calc"),
]

miss_lang, miss_route, no_canon = [], [], []
for sp, want in CASES:
    lang, canon, q_en, route = process(sp)
    if lang != "es":
        miss_lang.append((sp, lang))
    ok = route.startswith(want) or (want in ("click","repeat","calc") and route in ("CHAT","calc"))
    # click/repeat handled by non-extracted regexes; accept canon-produced english
    if want == "click":  ok = "click" in q_en.lower()
    if want == "repeat": ok = "repeat that" in q_en.lower()
    if want == "calc":   ok = route == "calc" or "percent" in q_en.lower()
    if want == "convert":  # units must be translated to English tokens for calc
        ql = q_en.lower()
        ok = (ql.startswith("convert ") and " to " in ql
              and not re.search(r"\b(millas?|kil[oó]metros?|libras?|litros?|"
                                r"metros?|pies|onzas?|galones?|grados)\b", ql))
    if not canon:
        no_canon.append((sp, want, q_en))
    if not ok:
        miss_route.append((sp, want, route, q_en))

# ── ENGLISH regression: English commands must STAY 'en' (never misdetect es) ──
EN_CMDS = [
    "open discord","close chrome","take a screenshot","set a timer for 10 minutes",
    "what time is it","resume the music","transcribe this recording","check my agenda",
    "import to lightroom","move this window to the other monitor","minimize everything except chrome",
    "free up memory","optimize my pc","number the screen","click 7","enroll my voice",
    "add app blender","save this conversation","complete my first goal","list my goals",
    "what can you do","tell me a joke","turn on work mode","battery status","pause the music",
    "next song","volume up","make it louder","what's on my calendar tomorrow","how many tokens have i used",
    "remember that i park on level 3","forget what i told you about the gym","what do you know about me",
    "schedule lunch with anna tomorrow at 1pm","run a speed test","show the performance hud",
    "i resume my workout every monday","my agenda is full today","transcribe means to write down",
]
en_bad = [(s, i18n.detect_lang(s)) for s in EN_CMDS if i18n.detect_lang(s) == "es"]

print(f"total: {len(CASES)}")
print(f"english stays en: {len(EN_CMDS)-len(en_bad)}/{len(EN_CMDS)}")
if en_bad:
    print("-- ENGLISH misdetected as Spanish:")
    for s,l in en_bad: print(f"   {s!r} -> {l}")
print(f"detected es: {len(CASES)-len(miss_lang)}/{len(CASES)}")
print(f"canonicalized offline: {len(CASES)-len(no_canon)}/{len(CASES)}")
print(f"routed correctly: {len(CASES)-len(miss_route)}/{len(CASES)}")
if miss_lang:
    print("\n-- NOT detected as Spanish:")
    for s,l in miss_lang: print(f"   {s!r} -> {l}")
if no_canon:
    print("\n-- NO offline canonicalization (falls to remote translator):")
    for s,w,q in no_canon: print(f"   [{w}] {s!r}")
if miss_route:
    print("\n-- WRONG route:")
    for s,w,r,q in miss_route: print(f"   [{w}] {s!r} -> {r}   (en={q!r})")

_all_ok = (not miss_lang and not no_canon and not miss_route and not en_bad)
print("\n=== RESULT:", "ALL PASS" if _all_ok else "FAIL")
sys.exit(0 if _all_ok else 1)
