"""Prueba en navegador real (Chromium + Playwright) de la pantalla proyectada y de los celulares.
Recorre una sesión completa, toma capturas y registra errores de JavaScript o de la política de seguridad (CSP).
Uso: python3 navegador_vivo.py [carpeta_capturas]"""
import json, os, subprocess, sys, time
from playwright.sync_api import sync_playwright

AQUI = os.path.dirname(os.path.abspath(__file__))
fuente = open(os.path.join(AQUI, 'vivo_e2e.py'), encoding='utf-8').read().split("print('== Preparación')")[0]
base = {'__file__': os.path.join(AQUI, 'vivo_e2e.py')}; exec(fuente, base)
Cliente, sql, BASE, RAIZ_WEB, check, OK, FAIL = base['Cliente'], base['sql'], base['BASE'], base['RAIZ_WEB'], base['check'], base['OK'], base['FAIL']
CAP = sys.argv[1] if len(sys.argv) > 1 else os.path.join(AQUI, 'capturas')
os.makedirs(CAP, exist_ok=True)

print('== Preparación')
subprocess.run([os.path.join(AQUI, 'reiniciar_bd.sh')], check=True, capture_output=True)
d = Cliente()
c, t, _ = d.req('setup'); tok = d.csrf(t)
d.req('setup', [('_csrf', tok), ('token', 'token-de-prueba-largo-2026-cucosta'), ('codigo', 'jprofesor'), ('nombre', 'Juan Profesor'), ('correo', ''), ('clave', 'Docente-Seguro-2026'), ('clave2', 'Docente-Seguro-2026')])
c, t, _ = d.req('login'); tok = d.csrf(t)
d.req('login', [('_csrf', tok), ('codigo', 'jprofesor'), ('clave', 'Docente-Seguro-2026')])
c, t, _ = d.req('docente/equipos'); tok = d.csrf(t)
d.req('docente/equipos', [('_csrf', tok), ('accion', 'generar'), ('conjunto', 'Semana Jurídica 2026'), ('cantidad', '60')])
codigos = sql("SELECT codigo FROM equipos ORDER BY id").split('\n')
caso = sql("SELECT id FROM casos WHERE titulo LIKE 'Arrebato%'")

errores = []
def vigilar(pagina, quien):
    pagina.on('console', lambda m: errores.append('%s consola: %s' % (quien, m.text)) if m.type == 'error' else None)
    pagina.on('pageerror', lambda e: errores.append('%s JS: %s' % (quien, e)))

def foto(pagina, nombre):
    pagina.screenshot(path=os.path.join(CAP, nombre + '.png'))

def respuesta(q, bien):
    cl = q['clave']; ops = [o['id'] for o in q['datos'].get('opciones', [])]
    if q['tipo'] == 'opcion_multiple':
        if cl.get('insuficiente'): return 'ins' if bien else ops[0]
        return cl['correcta'] if bien else [o for o in ops if o != cl['correcta']][0]
    if q['tipo'] == 'seleccion_multiple':
        return cl.get('correctas', []) if bien else [o for o in ops if o not in cl.get('correctas', [])][:1]
    if q['tipo'] == 'relacion':
        return cl['pares'] if bien else {k: [o for o in ops if o != v][0] for k, v in cl['pares'].items()}
    return None

def contestar(tel, q, bien):
    r = respuesta(q, bien)
    tel.wait_for_selector('[data-enviar]', timeout=15000)
    if q['tipo'] == 'relacion':
        for item, op in r.items():
            tel.select_option('select[data-item="%s"]' % item, op)
    elif q['tipo'] == 'respuesta_breve':
        tel.fill('[data-texto]', 'El equipo identifica los hechos observables, propone la clasificación jurídica provisional, cita el fundamento y explica su aplicación al caso.' if bien else 'No sabemos.')
    elif r == 'ins':
        tel.click('[data-ins]')
    else:
        for op in (r if isinstance(r, list) else [r]):
            tel.click('[data-op="%s"]' % op)

with sync_playwright() as p:
    ruta = '/opt/pw-browsers/chromium'
    nav = p.chromium.launch(executable_path=ruta if os.path.isfile(ruta) else None)

    # ---------- Docente: acceso, equipos, tarjetas y creación de la sesión ----------
    ctx = nav.new_context(viewport={'width': 1920, 'height': 1080}, locale='es-MX')
    doc = ctx.new_page(); vigilar(doc, 'docente')
    doc.goto(BASE + '?r=login')
    doc.fill('input[name=codigo]', 'jprofesor'); doc.fill('input[name=clave]', 'Docente-Seguro-2026')
    doc.click('button[type=submit]'); doc.wait_for_load_state()
    doc.goto(BASE + '?r=docente/equipos'); foto(doc, '01-equipos')
    doc.goto(BASE + '?r=docente/equipos-tarjetas&conjunto=' + 'Semana%20Jur%C3%ADdica%202026')
    doc.wait_for_selector('.te-qr svg')
    n_qr = doc.eval_on_selector_all('.te-qr svg', 'e => e.length')
    check(n_qr == 60, 'tarjetas: se dibujan 60 códigos QR en el navegador')
    foto(doc, '02-tarjetas')
    doc.emulate_media(media='print'); foto(doc, '03-tarjetas-impresion'); doc.emulate_media(media='screen')
    doc.goto(BASE + '?r=docente/vivo')
    doc.select_option('#caso_id', caso)
    doc.fill('#titulo', 'Semana Jurídica · Reto de casos')
    foto(doc, '04-nueva-sesion')
    doc.click('form.formulario button[type=submit]')
    doc.wait_for_selector('.lobby')
    time.sleep(1.2)
    token = sql('SELECT token FROM vivo_sesiones ORDER BY id DESC LIMIT 1')
    sid = sql('SELECT id FROM vivo_sesiones ORDER BY id DESC LIMIT 1')
    PREG = json.loads(sql('SELECT snapshot FROM vivo_sesiones WHERE id=' + sid))['preguntas']
    check(doc.eval_on_selector('.lobby-unirse .qr svg', 'e => !!e'), 'pantalla: sala de espera con QR para unirse')
    foto(doc, '10-proyector-lobby-vacio')

    # ---------- Tres celulares ----------
    tels = []
    for i in range(3):
        cx = nav.new_context(viewport={'width': 390, 'height': 844}, device_scale_factor=2, is_mobile=True, has_touch=True, locale='es-MX')
        tp = cx.new_page(); vigilar(tp, 'celular%d' % (i + 1)); tels.append(tp)
    tels[0].goto(RAIZ_WEB + '/vivo/')
    foto(tels[0], '20-celular-unirse')
    tels[0].fill('#eq-codigo', 'ZZZZZ2'); tels[0].click('#eq-form button'); tels[0].wait_for_function("document.querySelector('#eq-error').textContent.length > 0")
    check('No encontramos' in tels[0].inner_text('#eq-error'), 'celular: código incorrecto muestra aviso claro')
    tels[0].fill('#eq-codigo', codigos[0].lower()); tels[0].click('#eq-form button')
    tels[1].goto(RAIZ_WEB + '/vivo/?e=' + codigos[1])          # como si escanearan el QR de su tarjeta
    tels[2].goto(RAIZ_WEB + '/vivo/?e=' + codigos[2])
    for tp in tels: tp.wait_for_selector('.eq-conectado', timeout=15000)
    check(True, 'celulares: se unen con código escrito y con QR')
    foto(tels[0], '21-celular-lobby')
    for cod in codigos[3:45]:   # más equipos para ver la sala llena
        base['Cliente']().json('vivo/unirse', {'codigo': cod})
    time.sleep(2.5)
    n_on = doc.eval_on_selector_all('.eq-grid li.on', 'e => e.length')
    check(n_on == 45, 'pantalla: 45 equipos aparecen conectados en la sala (%d)' % n_on)
    foto(doc, '11-proyector-lobby')

    # ---------- Caso ----------
    doc.keyboard.press('Space'); doc.wait_for_selector('.caso .escena-card')
    time.sleep(0.8); foto(doc, '12-proyector-caso-portada')
    doc.keyboard.press('ArrowRight'); time.sleep(0.8); foto(doc, '13-proyector-caso-escena')
    n_slides = doc.eval_on_selector_all('.puntos-nav i', 'e => e.length')
    for _ in range(n_slides - 2): doc.keyboard.press('ArrowRight'); time.sleep(0.3)
    doc.wait_for_selector('.ficha-grid'); time.sleep(0.6); foto(doc, '14-proyector-caso-ficha')
    check(n_slides >= 4, 'pantalla: el guion se presenta como escenas (%d diapositivas)' % n_slides)
    tels[0].wait_for_selector('text=¡Miren la pantalla!', timeout=6000); foto(tels[0], '22-celular-caso')

    # ---------- Preguntas ----------
    for n, q in enumerate(PREG):
        doc.keyboard.press('Space')
        doc.wait_for_selector('.previo', timeout=5000)
        if n == 0: time.sleep(1.2); foto(doc, '15-proyector-previo')
        doc.wait_for_selector('.preg:not(.espera)', timeout=8000); time.sleep(0.8)
        if n in (0, 1, 4): foto(doc, '16-proyector-pregunta-%d' % (n + 1))
        activos = [0, 1] if n != 0 else [0, 1]
        for i in activos:
            contestar(tels[i], q, bien=(i == 0))
        if n in (0, 1, 4):
            foto(tels[0], '23-celular-pregunta-%d' % (n + 1))
        for i in activos:
            tels[i].click('[data-enviar]')
        tels[0].wait_for_selector('text=¡Respuesta enviada!', timeout=8000)
        if n == 0: foto(tels[0], '24-celular-enviada')
        time.sleep(1.5)
        if n == 1:
            doc.keyboard.press('t'); time.sleep(1.5)
        doc.keyboard.press('r')
        if q['tipo'] == 'respuesta_breve':
            doc.wait_for_selector('.muro-card', timeout=8000); time.sleep(1.5)
            foto(doc, '19-proyector-muro')
            doc.click('.muro-card'); doc.wait_for_selector('.foco-card'); foto(doc, '19b-proyector-muro-foco')
            doc.click('[data-premio="1000"]'); time.sleep(2.5)
            check(doc.eval_on_selector('.premio.act', 'e => e.textContent') == '+1000', 'muro: el docente otorga +1000 a una respuesta abierta')
            doc.keyboard.press('Escape')
            tels[0].wait_for_selector('.eq-resultado .suma', timeout=8000); foto(tels[0], '27-celular-muro-puntos')
        else:
            doc.wait_for_selector('.revelada, .rel-items .sol-rel', timeout=8000); time.sleep(1.8)
            if n in (0, 1): foto(doc, '17-proyector-revelada-%d' % (n + 1))
            tels[0].wait_for_selector('.eq-resultado', timeout=8000); tels[1].wait_for_selector('.eq-resultado', timeout=8000)
            clase0 = tels[0].eval_on_selector('.eq-resultado', 'e => e.className'); clase1 = tels[1].eval_on_selector('.eq-resultado', 'e => e.className')
            check('ok' in clase0 and ('mal' in clase1 or 'parcial' in clase1), 'P%d: el celular del equipo que acertó muestra «¡Correcto!» y el otro no' % (n + 1))
            if n == 0: foto(tels[0], '25-celular-correcto'); foto(tels[1], '26-celular-incorrecto'); foto(tels[2], '26b-celular-sin-respuesta')
        doc.keyboard.press('Space'); doc.wait_for_selector('.tabla-mar', timeout=5000)
        time.sleep(2.6)
        if n in (0, 3): foto(doc, '18-proyector-marcador-%d' % (n + 1))
        if n == 3: tels[0].wait_for_selector('text=Marcador', timeout=6000); foto(tels[0], '28-celular-marcador')
    doc.keyboard.press('Space'); doc.wait_for_selector('.podio', timeout=6000)
    time.sleep(5.5); foto(doc, '30-proyector-podio')
    tels[0].wait_for_selector('text=¡Terminamos!', timeout=8000); foto(tels[0], '31-celular-final')
    primero = doc.inner_text('.podio .p1 .nom')
    esperado = sql('SELECT nombre FROM equipos WHERE codigo=\'%s\'' % codigos[0])
    check(primero == esperado, 'podio: el equipo que acertó todo queda en 1.er lugar (%s)' % primero)
    doc.goto(BASE + '?r=docente/vivo-resultados&id=' + sid); foto(doc, '32-resultados')

    # ---------- Pantalla pequeña (laptop 1366×768) ----------
    doc.set_viewport_size({'width': 1366, 'height': 768})
    doc.goto(BASE + '?r=docente/vivo-pantalla&id=' + sid); doc.wait_for_selector('.podio'); time.sleep(4); foto(doc, '33-proyector-1366-podio')
    nav.close()

# Las respuestas 404/409 de la API (código inexistente, pregunta cerrada) son esperadas y el navegador las anota en consola.
errs_reales = [e for e in errores if 'favicon' not in e and not ('Failed to load resource' in e and ('status of 404' in e or 'status of 409' in e))]
check(not errs_reales, 'sin errores de JavaScript ni de CSP en ninguna pantalla' + ('' if not errs_reales else ': ' + ' | '.join(errs_reales[:5])))
print('\nCapturas en: ' + CAP)
print('RESUMEN: %d correctas, %d fallidas' % (len(OK), len(FAIL)))
for f in FAIL: print('  FALLÓ: ' + f)
