#!/usr/bin/env python3
"""La cadena de voz entera, con altavoces y microfono de verdad.

    ./venv/bin/python pruebas/prueba_voz.py

Las otras pruebas miran piezas: que el emparejador acierte, que el candado
cierre, que el puente mande los campos que el HUD lee. Ninguna contesta a la
pregunta que de verdad importa —**si le hablo, ¿me contesta?**— y por eso el
fallo de que oia bien pero descartaba en silencio llego hasta el usuario.

Esta habla por los altavoces y escucha por el microfono, como una persona. Es
lenta y necesita el volumen puesto, pero es la unica que prueba lo que se usa.

## Como funciona

Se genera la frase con Piper, se reproduce POR FUERA de la boca de JARVIS —con
un reproductor a pelo, para que la guarda del eco no la tape— y se mira que la
cadena entera reaccione. Que el microfono llega a los altavoces ya lo demuestra
prueba_eco.py; aqui se da por hecho y se avisa si falla.

## Lo que se comprueba, en orden

  1. sin llamarle          se oye pero NO se atiende, y se dice por que
  2. llamandole            se atiende y contesta
  3. encadenando           la siguiente sin repetir el nombre
  4. como lo oye Whisper   "Yarvis" y "Charles" tambien valen
"""
import os
import subprocess
import sys
import threading
import time

AQUI = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
sys.path.insert(0, AQUI)

from boca.voz import Boca                                  # noqa: E402
from manos import Manos                                    # noqa: E402
from oido.captura import Captura                           # noqa: E402
from oido.despierta import Centinela, NOMBRE               # noqa: E402
from oido.whisper import Oido, es_ruido                    # noqa: E402
from jarvis import atiende                                 # noqa: E402

ESPERA = 16          # cuanto se escucha por cada frase dicha
fallos = []
ajenas = []          # lo que se oyo y no era de la prueba: ruido de la sala


def _se_parece(oido: str, dicho: str) -> bool:
    """Si lo transcrito es la frase que se reprodujo, aunque venga maltratada.

    Hace falta porque la prueba usa el microfono de la habitacion: si hay una
    tele, una conversacion o un video sonando, el bucle oye ESO y una prueba
    ingenua lo da por bueno. Ha pasado —una pasada dio por valida "¡No te
    olvides de todo ese chico!"— y una prueba que se cree cualquier ruido no
    prueba nada.
    """
    import unicodedata

    def palabras(t):
        t = unicodedata.normalize("NFD", (t or "").lower())
        t = "".join(c for c in t if unicodedata.category(c) != "Mn")
        return {p.strip(".,¡!¿?«»") for p in t.split() if len(p) > 3}

    a, b = palabras(oido), palabras(dicho)
    if not b:
        return False
    return len(a & b) >= max(1, min(2, len(b) - 1))


def afirma(condicion, queja):
    print(f"  {'OK ' if condicion else 'MAL'} {queja}")
    if not condicion:
        fallos.append(queja)


class Escucha:
    """El bucle de voz de jarvis.py, reducido a lo que hace falta probar."""

    def __init__(self, boca, oreja, centinela):
        self.boca, self.oreja, self.centinela = boca, oreja, centinela
        self.manos = Manos()
        self.oido = []          # (texto, atendido, motivo)
        self.dicho = []         # lo que ha contestado
        self._para = threading.Event()
        self._hilo = None

    def arranca(self):
        self._hilo = threading.Thread(target=self._bucle, daemon=True)
        self._hilo.start()

    def _bucle(self):
        cap = Captura(boca=self.boca)
        for wav in cap.frases(para_cuando=lambda: self._para.is_set()):
            try:
                texto = self.oreja.transcribe(wav)
            finally:
                try:
                    os.unlink(wav)
                except OSError:
                    pass
            if es_ruido(texto):
                continue

            atender, orden, _ = self.centinela.filtra(texto)
            if not atender:
                self.oido.append((texto, False, "no le han llamado"))
                continue
            self.oido.append((texto, True, ""))
            respuesta = atiende(orden, self.manos, None, traza=lambda t: None)
            if respuesta:
                self.dicho.append(respuesta)
        cap.para()

    def para(self):
        self._para.set()
        if self._hilo:
            self._hilo.join(timeout=6)

    def suelta(self, frase, espera=ESPERA):
        """Dice la frase POR FUERA de JARVIS y espera a que reaccione.

        Solo cuenta lo que se PARECE a lo reproducido: si hay ruido en la
        habitacion, el bucle lo oye tambien y darlo por bueno haria pasar la
        prueba sin haber probado nada.
        """
        antes_oido, antes_dicho = len(self.oido), len(self.dicho)
        wav = self.boca._genera(frase)
        if not wav:
            return None, None
        time.sleep(0.6)
        subprocess.run(["paplay", wav], capture_output=True)

        fin = time.monotonic() + espera
        mio = None
        while time.monotonic() < fin:
            for entrada in self.oido[antes_oido:]:
                if _se_parece(entrada[0], frase):
                    mio = entrada
                    break
            if mio and (not mio[1] or len(self.dicho) > antes_dicho):
                break
            time.sleep(0.2)

        nuevo_dicho = self.dicho[antes_dicho:]
        ajeno = [o[0] for o in self.oido[antes_oido:] if o is not mio]
        if ajeno:
            ajenas.extend(ajeno)
            print(f"      (ruido de la habitacion, ignorado: {ajeno[0][:44]!r})")
        return mio, (nuevo_dicho[-1] if nuevo_dicho else None)


def main() -> int:
    boca = Boca("davefx")
    oreja = Oido(modelo="small")
    if not oreja.disponible():
        print("  no esta whisper o el modelo; no se puede probar")
        return 2
    print("  arrancando whisper...")
    if not oreja.arranca():
        print("  whisper no arranco")
        return 2

    centinela = Centinela(activo=True)
    esc = Escucha(boca, oreja, centinela)
    esc.arranca()
    time.sleep(1.5)

    print("\n  0 · con la palabra APAGADA (el defecto): atiende directo")
    centinela.activo = False
    oido, dicho = esc.suelta("Cuanta memoria queda.")
    if oido:
        print(f"      oyo: {oido[0]!r}")
    if dicho:
        print(f"      dijo: {dicho}")
    afirma(oido is not None and oido[1],
           "sin exigir el nombre, atiende lo que oiga")
    afirma(dicho is not None, "y contesta")
    centinela.activo = True
    centinela.duerme()

    print("\n  1 · con la palabra puesta y sin llamarle: oye pero no atiende")
    oido, dicho = esc.suelta("Cuanto espacio queda en el disco.")
    if oido is None:
        print("    ··  no se oyo nada: ¿altavoces en silencio? sin esto el resto")
        print("        de la prueba no demuestra nada")
        fallos.append("el microfono no capta los altavoces")
    else:
        print(f"      oyo: {oido[0]!r}")
        afirma(not oido[1], "lo oye pero no lo atiende, porque no le han llamado")
        afirma(dicho is None, "y no contesta")

    centinela.duerme()
    print(f"\n  2 · llamandole por su nombre")
    oido, dicho = esc.suelta(f"{NOMBRE.capitalize()}, cuanto espacio queda.")
    if oido:
        print(f"      oyo: {oido[0]!r}")
    if dicho:
        print(f"      dijo: {dicho}")
    afirma(oido is not None and oido[1], "lo atiende")
    afirma(dicho is not None, "y contesta algo")
    afirma(dicho is not None and any(c.isdigit() for c in dicho),
           "con una cifra de verdad, no una excusa")

    print("\n  3 · encadenando, sin repetir el nombre")
    oido, dicho = esc.suelta("Cuanta memoria queda.")
    if oido:
        print(f"      oyo: {oido[0]!r}")
    afirma(oido is not None and oido[1],
           "sigue despierto y atiende sin que le vuelvan a llamar")

    print("\n  4 · como lo oye Whisper de verdad")
    centinela.duerme()
    oido, dicho = esc.suelta("Yarvis, que hora es.")
    if oido:
        print(f"      oyo: {oido[0]!r}")
    afirma(oido is not None and oido[1],
           "'Yarvis' tambien le despierta (Whisper escribe el nombre de mil formas)")

    esc.para()
    oreja.para()

    # Una prueba que usa el microfono de la sala no puede fallar en silencio por
    # culpa de la sala. Si ha entrado mucho ruido, el resultado no dice nada del
    # codigo y hay que decirlo, no soltar una pared de MAL.
    if fallos and len(ajenas) >= 2:
        print(f"\n  ┌─ SALA RUIDOSA ─────────────────────────────────────────")
        print(f"  │ Han entrado {len(ajenas)} frases que no eran de la prueba.")
        print(f"  │ Con audio sonando cerca del microfono, este resultado NO")
        print(f"  │ dice nada del codigo. Ejemplos de lo que se colo:")
        for a in ajenas[:3]:
            print(f"  │   {a[:52]!r}")
        print(f"  │ Repitelo en silencio.")
        print(f"  └────────────────────────────────────────────────────────")
        print(f"\n  {len(fallos)} fallan, pero no son concluyentes\n")
        return 2

    print(f"\n  {'todo en orden' if not fallos else str(len(fallos)) + ' fallan'}\n")
    return 1 if fallos else 0


if __name__ == "__main__":
    sys.exit(main())
