"""
Suite de pruebas nativa para OCR Indexer Pro.
Ejecuta todos los comandos en un directorio temporal aislado.
"""
#!/usr/bin/env python3
import os
import sys
import json
import time
import shutil
import tempfile
import subprocess

GREEN  = '\033[0;32m'
RED    = '\033[0;31m'
YELLOW = '\033[1;33m'
BLUE   = '\033[0;34m'
BOLD   = '\033[1m'
NC     = '\033[0m'


def run_cmd(*args, cwd, input_text=None):
    """Ejecuta ocr-indexer con los args dados. Retorna (stdout+stderr, returncode)."""
    kwargs = dict(cwd=cwd, capture_output=True, text=True, timeout=90)
    if input_text is not None:
        kwargs["input"] = input_text
    else:
        kwargs["stdin"] = subprocess.DEVNULL  # EOF inmediato → manejado por try/except EOFError

    try:
        result = subprocess.run(
            [sys.executable, "-m", "ocr_indexer.cli"] + list(args),
            **kwargs
        )
        return result.stdout + result.stderr, result.returncode
    except subprocess.TimeoutExpired:
        return "[TIMEOUT]", -1


class TestSuite:

    def __init__(self):
        self.passed  = 0
        self.failed  = 0
        self.skipped = 0
        self.test_dir    = None
        self.engine_path = None

    # ── Helpers de reporte ──────────────────────────────────────────

    def ok(self, name):
        print(f"  {GREEN}[✅ PASS]{NC} {name}")
        self.passed += 1

    def fail(self, name, detail="", show_log=False):
        print(f"  {RED}[❌ FAIL]{NC} {name}")
        if detail:
            print(f"  {YELLOW}         {detail}{NC}")
        if show_log and self.engine_path:
            log_path = os.path.join(self.engine_path, 'indexer.log')
            if os.path.exists(log_path):
                try:
                    with open(log_path, encoding='utf-8', errors='replace') as f:
                        lines = f.readlines()
                    tail = lines[-10:]
                    print(f"\n  {BLUE}─── Log del motor (últimas {len(tail)} líneas) ───{NC}")
                    for line in tail:
                        print(f"  {line.rstrip()}")
                    print(f"  {BLUE}{'─' * 46}{NC}\n")
                except Exception:
                    pass
        self.failed += 1

    def skip(self, name, reason=""):
        reason_str = f" ({reason})" if reason else ""
        print(f"  {YELLOW}[⏭  SKIP]{NC} {name}{YELLOW}{reason_str}{NC}")
        self.skipped += 1

    def group(self, name):
        print(f"\n{BOLD}{BLUE}[{name}]{NC}")

    # ── Utilidades ──────────────────────────────────────────────────

    def _wait_for_completed(self, timeout=60):
        """Espera hasta que status.json tenga fase=='Completado'."""
        status_path = os.path.join(self.engine_path, 'status.json')
        inicio = time.time()
        while time.time() - inicio < timeout:
            try:
                with open(status_path) as f:
                    st = json.load(f)
                if st.get("fase") == "Completado":
                    return True
            except Exception:
                pass
            time.sleep(0.5)
        return False

    def _crear_imagen(self, path):
        """Crea PNG con texto legible para OCR (Pillow siempre disponible)."""
        try:
            from PIL import Image, ImageDraw
            img = Image.new('RGB', (500, 250), color='white')
            draw = ImageDraw.Draw(img)
            draw.text((30, 60),  "FACTURA 001",          fill='black')
            draw.text((30, 100), "Cliente: Juan Perez",  fill='black')
            draw.text((30, 140), "Total $1500.00 MXN",   fill='black')
            draw.text((30, 180), "Fecha: 2026-03-18",    fill='black')
            img.save(path)
            return True
        except Exception as e:
            return False

    def _leer_config(self):
        path = os.path.join(self.engine_path, 'config.json')
        with open(path) as f:
            return json.load(f)

    def _leer_index(self):
        path = os.path.join(self.engine_path, 'ocr-indexer.json')
        with open(path) as f:
            return json.load(f)

    # ── Grupos de test ──────────────────────────────────────────────

    def _test_motor(self):
        self.group("GRUPO 1 · Creación del Motor")

        out, rc = run_cmd("dir", self.test_dir, cwd=self.test_dir)
        self.ok("dir: motor creado") if (rc == 0 and os.path.isdir(self.engine_path)) \
            else self.fail("dir: motor creado", f"rc={rc}", show_log=False)

        try:
            cfg = self._leer_config()
            ok = all(k in cfg for k in ("root", "blacklist", "tolerancias"))
            self.ok("config.json con estructura válida") if ok \
                else self.fail("config.json con estructura válida", "faltan claves root/blacklist/tolerancias")
        except Exception as e:
            self.fail("config.json con estructura válida", str(e))

        try:
            idx = self._leer_index()
            self.ok("ocr-indexer.json inicializado vacío") if idx == {} \
                else self.fail("ocr-indexer.json inicializado vacío", f"no está vacío: {list(idx.keys())[:2]}")
        except Exception as e:
            self.fail("ocr-indexer.json inicializado vacío", str(e))

    def _test_tolerancias(self):
        self.group("GRUPO 2 · Tolerancias OCR")

        out, _ = run_cmd("tol-list", cwd=self.test_dir)
        self.ok("tol-list: muestra reglas por defecto") if ("O" in out and "0" in out) \
            else self.fail("tol-list: muestra reglas por defecto", "no contiene O o 0")

        run_cmd("tol-add", cwd=self.test_dir, input_text="X:Y\n\n")
        try:
            cfg = self._leer_config()
            self.ok("tol-add: regla X→Y agregada") if cfg.get("tolerancias", {}).get("X") == "Y" \
                else self.fail("tol-add: regla X→Y agregada", "X no encontrada en tolerancias")
        except Exception as e:
            self.fail("tol-add: regla X→Y agregada", str(e))

        run_cmd("tol-rm", cwd=self.test_dir, input_text="X\ns\n")
        try:
            cfg = self._leer_config()
            self.ok("tol-rm: regla X→Y eliminada") if "X" not in cfg.get("tolerancias", {}) \
                else self.fail("tol-rm: regla X→Y eliminada", "X aún en tolerancias")
        except Exception as e:
            self.fail("tol-rm: regla X→Y eliminada", str(e))

        run_cmd("tol-clear", cwd=self.test_dir, input_text="s\n")
        try:
            cfg = self._leer_config()
            self.ok("tol-clear: tolerancias vacías") if cfg.get("tolerancias", None) == {} \
                else self.fail("tol-clear: tolerancias vacías", f"quedan: {cfg.get('tolerancias')}")
        except Exception as e:
            self.fail("tol-clear: tolerancias vacías", str(e))

    def _test_blacklist(self):
        self.group("GRUPO 3 · Lista Negra")

        out, _ = run_cmd("blacklist-list", cwd=self.test_dir)
        self.ok("blacklist-list: exclusiones por defecto") if ("Talk" in out or "trashbin" in out) \
            else self.fail("blacklist-list: exclusiones por defecto", "no contiene Talk ni trashbin")

        run_cmd("blacklist-add", "carpeta_privada", cwd=self.test_dir)
        try:
            cfg = self._leer_config()
            self.ok("blacklist-add: carpeta_privada agregada") if "carpeta_privada" in cfg.get("blacklist", []) \
                else self.fail("blacklist-add: carpeta_privada agregada", "no en blacklist")
        except Exception as e:
            self.fail("blacklist-add: carpeta_privada agregada", str(e))

        run_cmd("blacklist-rm", "carpeta_privada", cwd=self.test_dir)
        try:
            cfg = self._leer_config()
            self.ok("blacklist-rm: carpeta_privada eliminada") if "carpeta_privada" not in cfg.get("blacklist", []) \
                else self.fail("blacklist-rm: carpeta_privada eliminada", "aún en blacklist")
        except Exception as e:
            self.fail("blacklist-rm: carpeta_privada eliminada", str(e))

    def _test_indexado(self):
        self.group("GRUPO 4 · Indexado y OCR")

        img_path = os.path.join(self.test_dir, "test_factura.png")
        if self._crear_imagen(img_path):
            self.ok("imagen de prueba creada (test_factura.png)")
        else:
            self.fail("imagen de prueba creada", "Pillow no disponible")
            self.skip("start / Fase 2 / stats / last / reindex", "sin imagen de prueba")
            return

        out, rc = run_cmd("start", cwd=self.test_dir)
        self.ok("start: indexador lanzado") if rc == 0 \
            else self.fail("start: indexador lanzado", f"rc={rc}")

        print(f"  {YELLOW}  ⏳ Esperando Fase 2 (hasta 60s)...{NC}", end="\r")
        completado = self._wait_for_completed(timeout=60)
        print(" " * 50, end="\r")  # limpiar línea
        if not completado:
            self.fail("Fase 2 completada (OCR Tesseract)", "Timeout 60s — el indexer no terminó", show_log=True)
            self.skip("stats / last / reindex / search", "Fase 2 no completó")
            return
        self.ok("Fase 2 completada (OCR Tesseract)")

        out, _ = run_cmd("stats", cwd=self.test_dir)
        self.ok("stats: extensión PNG listada") if ("png" in out.lower()) \
            else self.fail("stats: extensión PNG listada", f"output: {out[:120]}")

        out, _ = run_cmd("last", cwd=self.test_dir)
        self.ok("last: muestra archivo indexado") if "test_factura" in out \
            else self.fail("last: muestra archivo indexado", f"'test_factura' no en output:\n{out[:200]}", show_log=True)

        # reindex se prueba en _test_reindex() DESPUÉS de search, para no vaciar texto_ocr
        self._img_path = img_path
        self.skip("refine (Fase 3 IA)", "requiere API key configurada")

    def _test_busqueda(self):
        self.group("GRUPO 5 · Búsqueda")

        try:
            db = self._leer_index()
            tiene_texto = any(
                isinstance(v, dict) and v.get("contenido", {}).get("texto_ocr", "")
                for v in db.values()
            )
        except Exception:
            tiene_texto = False

        if not tiene_texto:
            self.skip("search: búsqueda de texto", "sin contenido OCR en el índice")
            return

        out, _ = run_cmd("search", cwd=self.test_dir, input_text="FACTURA\n")
        self.ok("search 'FACTURA': resultados encontrados") if "coincidencia" in out.lower() \
            else self.fail("search 'FACTURA': resultados encontrados", f"output: {out[:200]}")

    def _test_reindex(self):
        self.group("GRUPO 5b · Re-indexado")

        img_path = getattr(self, "_img_path", None)
        if not img_path:
            self.skip("reindex", "sin imagen de prueba disponible")
            return

        out, rc = run_cmd("reindex", img_path, cwd=self.test_dir)
        self.ok("reindex: rc == 0") if rc == 0 \
            else self.fail("reindex: rc == 0", f"rc={rc}, output: {out[:120]}")

    def _test_logs(self):
        self.group("GRUPO 6 · Logs")

        out, rc = run_cmd("logs", cwd=self.test_dir)
        self.ok("logs: muestra actividad del motor") if (rc == 0 and out.strip()) \
            else self.fail("logs: muestra actividad del motor", f"rc={rc}, vacío={not out.strip()}")

    def _test_estado(self):
        self.group("GRUPO 7 · Estado y Control")

        out, rc = run_cmd("status", cwd=self.test_dir)
        self.ok("status: responde correctamente") if (rc == 0 and ("DETENIDO" in out or "Completado" in out)) \
            else self.fail("status: responde correctamente", f"rc={rc}, output: {out[:120]}")

        out, rc = run_cmd("watch-tmux-status", cwd=self.test_dir)
        self.ok("watch-tmux-status: responde correctamente") if rc == 0 \
            else self.fail("watch-tmux-status: responde correctamente", f"rc={rc}")

    def _test_reset(self):
        self.group("GRUPO 8 · Reset")

        run_cmd("reset", cwd=self.test_dir, input_text="s\n")
        try:
            idx = self._leer_index()
            self.ok("reset: índice limpiado correctamente") if idx == {} \
                else self.fail("reset: índice limpiado correctamente", f"quedan {len(idx)} entradas")
        except Exception as e:
            self.fail("reset: índice limpiado correctamente", str(e))

    def _test_docs(self):
        self.group("GRUPO 9 · Menú y Documentación")

        out, rc = run_cmd("help", cwd=self.test_dir)
        self.ok("help: menú sin crash") if (rc == 0 and ("SISTEMA OCR" in out or "MENÚ" in out.upper())) \
            else self.fail("help: menú sin crash", f"rc={rc}, output: {out[:120]}")

        out, rc = run_cmd("flujo", cwd=self.test_dir)
        self.ok("flujo: FLUJO.md mostrado") if (rc == 0 and "Fase" in out) \
            else self.fail("flujo: FLUJO.md mostrado", f"rc={rc}")

        # "salir" en el primer prompt evita el scan lento de `find /`
        out, rc = run_cmd("engines", cwd=self.test_dir, input_text="salir\n")
        self.ok("engines: ejecuta sin crash") if rc == 0 \
            else self.fail("engines: ejecuta sin crash", f"rc={rc}")

        out, rc = run_cmd("ai-config", cwd=self.test_dir, input_text="\n\n")
        self.ok("ai-config: muestra configuración sin crash") if rc == 0 \
            else self.fail("ai-config: muestra configuración sin crash", f"rc={rc}")

    # ── Runner principal ────────────────────────────────────────────

    def run(self):
        print(f"\n{BOLD}{BLUE}╔════════════════════════════════════════════╗{NC}")
        print(f"{BOLD}{BLUE}║     TEST SUITE — OCR INDEXER PRO           ║{NC}")
        print(f"{BOLD}{BLUE}╚════════════════════════════════════════════╝{NC}")

        self.test_dir    = tempfile.mkdtemp(prefix="ocr-selftest-")
        self.engine_path = os.path.join(self.test_dir, '.ocr-indexer')
        print(f"\n  Directorio de prueba: {YELLOW}{self.test_dir}{NC}")
        print(f"\n  {YELLOW}Comandos omitidos (requieren entorno externo):{NC}")
        skips = [
            ("run",                    "cubre start + watch-tmux (ya probados por separado)"),
            ("watch / watch-tmux",     "requieren tmux instalado"),
            ("watch-tmux-stop",        "requieren sesión tmux activa"),
            ("refine",                 "requiere API key real configurada"),
            ("install-deps",           "invoca apt-get, riesgo en entornos automatizados"),
            ("shell-setup",            "modifica ~/.bashrc"),
        ]
        for cmd, reason in skips:
            print(f"    {YELLOW}⏭  {cmd:<22}{NC} — {reason}")

        try:
            self._test_motor()
            self._test_tolerancias()
            self._test_blacklist()
            self._test_indexado()
            self._test_busqueda()
            self._test_reindex()
            self._test_logs()
            self._test_estado()
            self._test_reset()
            self._test_docs()
        finally:
            shutil.rmtree(self.test_dir, ignore_errors=True)
            print(f"\n  {BLUE}Directorio de prueba limpiado.{NC}")

        total = self.passed + self.failed + self.skipped
        color = GREEN if self.failed == 0 else RED
        print(f"\n{BOLD}{color}{'═' * 46}{NC}")
        print(f"{BOLD}{color}Resultado: {self.passed}/{total} PASS  |  "
              f"{self.skipped} SKIP  |  {self.failed} FAIL{NC}")
        if self.failed == 0:
            print(f"{BOLD}{GREEN}✅ Todos los tests pasaron.{NC}")
        else:
            print(f"{BOLD}{RED}❌ {self.failed} test(s) fallaron. "
                  f"Revisa los logs mostrados arriba.{NC}")
        print(f"{BOLD}{color}{'═' * 46}{NC}\n")

        return self.failed


def main():
    suite = TestSuite()
    failures = suite.run()
    sys.exit(1 if failures > 0 else 0)


if __name__ == "__main__":
    main()
