"""Prueba de carga del modo en vivo (servidor de prueba local).
Escenario A: 100 equipos (un celular cada uno) se unen, consultan el estado y responden a la vez.
Escenario B: 600 celulares consultando el archivo de estado (si cada asistente usara su propio teléfono).
Reinicia la base de datos de prueba."""
import json, os, re, statistics, subprocess, threading, time, random, urllib.request, urllib.error
from concurrent.futures import ThreadPoolExecutor


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, datos_js, BASE, RAIZ_WEB = base['Cliente'], base['sql'], base['datos_js'], base['BASE'], base['RAIZ_WEB']

N_EQUIPOS = int(os.environ.get('N_EQUIPOS', '100'))
N_CELULARES_B = int(os.environ.get('N_CELULARES', '600'))

def pct(xs, p):
    xs = sorted(xs); return xs[min(len(xs) - 1, int(round(p / 100 * (len(xs) - 1))))] if xs else 0

def resumen(nombre, lat, errores, extra=''):
    print('  %-44s n=%4d  mediana %6.1f ms  p95 %6.1f ms  máx %6.1f ms  errores %d %s' % (nombre, len(lat), 1000 * statistics.median(lat) if lat else 0, 1000 * pct(lat, 95), 1000 * max(lat) if lat else 0, errores, extra))

def post_json(url, datos):
    req = urllib.request.Request(url, data=json.dumps(datos).encode(), headers={'Content-Type': 'application/json'})
    t0 = time.time()
    try:
        r = urllib.request.urlopen(req, timeout=30); c = r.status; cuerpo = r.read()
    except urllib.error.HTTPError as e:
        c = e.code; cuerpo = e.read()
    return time.time() - t0, c, json.loads(cuerpo or b'{}')

print('== Preparación (%d equipos)' % N_EQUIPOS)
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', 'Carga'), ('cantidad', str(N_EQUIPOS))])
codigos = sql("SELECT codigo FROM equipos WHERE conjunto='Carga' ORDER BY id").split('\n')
caso = sql("SELECT id FROM casos WHERE titulo LIKE 'Arrebato%'")
c, t, _ = d.req('docente/vivo'); tok = d.csrf(t)
c, t, _ = d.req('docente/vivo', [('_csrf', tok), ('caso_id', caso), ('conjunto', 'Carga'), ('titulo', 'Carga'), ('t_opcion_multiple', '30'), ('t_seleccion_multiple', '45'), ('t_relacion', '60'), ('t_respuesta_breve', '120'), ('abiertas', '1'), ('explicacion', '1'), ('rachas', '1')])
D = datos_js(t)
sid = sql('SELECT MAX(id) FROM vivo_sesiones')
PREG = json.loads(sql('SELECT snapshot FROM vivo_sesiones WHERE id=' + sid))['preguntas']
print('  sesión %s con %d preguntas; %d equipos' % (sid, len(PREG), len(codigos)))

print('== Escenario A: %d equipos simultáneos' % N_EQUIPOS)
lat_u, err_u, tel = [], 0, {}
def unir(cod):
    time.sleep(random.random() * 2)  # llegan en un lapso de 2 s
    return cod, post_json(BASE + '?r=vivo/unirse', {'codigo': cod})
with ThreadPoolExecutor(max_workers=N_EQUIPOS) as ex:
    for cod, (dt, c, j) in ex.map(unir, codigos):
        lat_u.append(dt)
        if c == 200: tel[cod] = j
        else: err_u += 1
resumen('Unirse (PHP)', lat_u, err_u)
archivo = RAIZ_WEB + '/vivo/s-' + next(iter(tel.values()))['token'] + '.json'
token = next(iter(tel.values()))['token']

detener = threading.Event()
metr = {'lat': [], 's200': 0, 's304': 0, 'err': 0}; bloqueo = threading.Lock()
def celular(i, intervalo=(1.5, 2.3)):
    etag = None
    time.sleep(random.random() * 2)
    while not detener.is_set():
        h = {'If-None-Match': etag} if etag else {}
        t0 = time.time()
        try:
            r = urllib.request.urlopen(urllib.request.Request(archivo, headers=h), timeout=20); r.read(); c = r.status; etag = r.headers.get('ETag')
        except urllib.error.HTTPError as e:
            c = e.code
        except Exception:
            c = 0
        dt = time.time() - t0
        with bloqueo:
            metr['lat'].append(dt)
            if c == 200: metr['s200'] += 1
            elif c == 304: metr['s304'] += 1
            else: metr['err'] += 1
        time.sleep(random.uniform(*intervalo))

hilos = [threading.Thread(target=celular, args=(i,), daemon=True) for i in range(len(tel))]
for h in hilos: h.start()
lat_api = []
def api_docente():
    while not detener.is_set():
        t0 = time.time(); d.get_json('docente/vivo-api', {'id': sid}); lat_api.append(time.time() - t0); time.sleep(1.0)
hd = threading.Thread(target=api_docente, daemon=True); hd.start()

def accion(nombre):
    c, j = d.get_json('docente/vivo-api', {'id': sid}); v = j['estado']['v']
    t0 = time.time(); c, j = d.json('docente/vivo-api', {'accion': nombre, 'v': v}, q={'id': sid}, headers={'X-CSRF-Token': D['csrf']}); return time.time() - t0, j['estado']

def correcta(q):
    cl = q['clave']
    if cl.get('insuficiente'): return {'ins': 1}
    return {'opcion_multiple': lambda: {'v': cl.get('correcta')}, 'seleccion_multiple': lambda: {'v': cl.get('correctas')}, 'relacion': lambda: {'v': cl.get('pares')}}.get(q['tipo'], lambda: {'v': 'Análisis del equipo con hechos, figura jurídica y fundamento. ' * 8})()

time.sleep(3)
lat_acc, lat_r, err_r = [], [], 0
dt, E = accion('siguiente'); lat_acc.append(dt)
for n, q in enumerate(PREG[:4]):
    dt, E = accion('siguiente'); lat_acc.append(dt)
    sql('UPDATE vivo_sesiones SET q_inicio_ms = %d WHERE id = %s' % (int(time.time() * 1000) - 500, sid))
    def responder(cod):
        time.sleep(random.random() * 8)  # todos responden en 8 segundos
        r = correcta(q) if random.random() < 0.6 else {'ins': 1} if q['permite_insuficiente'] else correcta(q)
        return post_json(BASE + '?r=vivo/responder', {'token': token, 'codigo': cod, 'k': q['k'], 'r': r})
    with ThreadPoolExecutor(max_workers=len(tel)) as ex:
        for dt, c, j in ex.map(responder, list(tel.keys())):
            lat_r.append(dt)
            if c != 200: err_r += 1
    dt, E = accion('siguiente'); lat_acc.append(dt)   # revelar y calificar
    time.sleep(2)
    dt, E = accion('siguiente'); lat_acc.append(dt)   # marcador
    time.sleep(2)
detener.set(); time.sleep(0.5)
resumen('Responder (PHP), %d preguntas × %d equipos' % (4, len(tel)), lat_r, err_r)
resumen('Consultar estado (archivo estático)', metr['lat'], metr['err'], '(200: %d, 304: %d)' % (metr['s200'], metr['s304']))
resumen('Pantalla del docente: sondeo de la API', lat_api, 0)
resumen('Acciones del docente (incluye calificar)', lat_acc, 0)
n_resp = sql('SELECT COUNT(*) FROM vivo_respuestas WHERE sesion_id=' + sid)
print('  respuestas registradas: %s de %d esperadas; equipos en el marcador: %d' % (n_resp, 4 * len(tel), len(E['equipos'])))
ok_a = err_u == 0 and err_r == 0 and metr['err'] == 0 and int(n_resp) == 4 * len(tel)

print('== Escenario B: %d celulares consultando el estado cada 2–3 s durante 20 s' % N_CELULARES_B)
metr.update({'lat': [], 's200': 0, 's304': 0, 'err': 0}); detener.clear()
hilos = [threading.Thread(target=celular, args=(i, (2.0, 3.0)), daemon=True) for i in range(N_CELULARES_B)]
for h in hilos: h.start()
time.sleep(10)
dt, E = accion('siguiente')  # cambio de fase a mitad de la prueba: todos deben recibir 200 una vez
time.sleep(10); detener.set(); time.sleep(1)
dur = 20
resumen('Consultar estado (archivo estático)', metr['lat'], metr['err'], '(200: %d, 304: %d, ~%d consultas/s)' % (metr['s200'], metr['s304'], (metr['s200'] + metr['s304']) / dur))
ok_b = metr['err'] == 0
print('\nRESULTADO: %s' % ('sin errores' if ok_a and ok_b else 'CON ERRORES'))
