"""QA SAQI contra producción: funcional (L3/L4) + adversarial (ATK-SEC) del módulo 4.1.

Uso: python3 qa_funnel_reacciones.py [URL_BASE]
Réplica del patrón qa_funnel.py usado en RA3.
"""

import json
import sys
import urllib.error
import urllib.request

BASE = sys.argv[1] if len(sys.argv) > 1 else "https://adoniz-acer.tail60dd6a.ts.net"
RESULTADOS = []


def pedir(ruta, datos=None, cabeceras=None, bruto=None):
    peticion = urllib.request.Request(f"{BASE}{ruta}", headers=cabeceras or {})
    cuerpo = bruto
    if datos is not None:
        cuerpo = json.dumps(datos).encode()
        peticion.add_header("Content-Type", "application/json")
    try:
        with urllib.request.urlopen(peticion, data=cuerpo, timeout=15) as r:
            return r.status, r.read()
    except urllib.error.HTTPError as e:
        return e.code, e.read()


def verificar(nombre, condicion, detalle=""):
    RESULTADOS.append((nombre, condicion, detalle))
    print(f"{'PASS' if condicion else 'FAIL'}  {nombre}" +
          (f"  [{detalle}]" if detalle and not condicion else ""))


def es_json(cuerpo):
    try:
        json.loads(cuerpo)
        return True
    except (ValueError, TypeError):
        return False


# ---------------------------------------------------------------- L4 funcional
estado, html = pedir("/quimica/")
verificar("L4 menu responde 200", estado == 200, str(estado))
verificar("L4 menu muestra tarjeta reacciones-quimicas",
          b"reacciones-quimicas" in html)

# ------------------------------------------------------- flujo completo 4.1
estado, cuerpo = pedir("/quimica/reacciones-quimicas")
verificar("L4 pagina modulo responde 200", estado == 200, str(estado))

verificar("L4 notificacion visible por incidencia (paridad RA3 CDT1)",
          estado == 200 and
          b"notificacionIncidente" in (cuerpo or b"") and
          b"mostrarAviso" in (cuerpo or b"") and
          b"actualizarContador" in (cuerpo or b"") and
          b"n-incidentes" in (cuerpo or b"") and
          b"contador-trampas" in (cuerpo or b"") and
          b"async function reportarIncidente" in (cuerpo or b""),
          "faltan marcadores de notificacion")

estado, cuerpo = pedir("/quimica/iniciar-reacciones", {
    "apellidos": "QA", "nombres": "Automatizado", "carrera": "Mecánica"})
token = json.loads(cuerpo)["token"] if estado == 200 else ""
verificar("L3 iniciar entrega token de 32 hex", estado == 200 and len(token) == 32)

respuestas = {
    "respuestas": {
        "4.1.1": {"0": "B", "1": "A", "2": "C", "3": "D", "4": "B"},
        "4.1.2": {"0": "A", "1": "B", "2": "A", "3": "C", "4": "D"},
        "4.1.3": {"0": "reactivos", "1": "productos", "2": "ecuacionquimica",
                  "3": "coeficiente", "4": "gaseoso"},
        "4.1.4": {"0": "sintesis", "1": "descomposicion", "2": "sustitucionsimple",
                  "3": "doblesustitucion", "4": "combustion", "5": "sintesis",
                  "6": "descomposicion", "7": "sustitucionsimple",
                  "8": "doblesustitucion", "9": "combustion"},
    }
}

# incidente en una sesión separada
estado, _ = pedir("/quimica/incidente-reacciones",
                  {"token": token, "tipo": "pegar"})
verificar("L3 incidente válido acepta", estado == 200)

estado, cuerpo = pedir("/quimica/guardar-reacciones", {"token": token,
                                                       "respuestas": {}})
resultado = json.loads(cuerpo).get("resultado", {}) if estado == 200 else {}
verificar("L3 envío sin respuestas califica 0 en cada tema sin caer",
          estado == 200 and all(v == 0 for v in resultado.get("notas", {}).values()))

estado, cuerpo = pedir("/quimica/guardar-reacciones", {"token": token,
                                                       "respuestas": {}})
verificar("L3 reenvío con mismo token bloqueado (409)", estado == 409, str(estado))

# sesión perfecta para validar nota 100
estado, cuerpo = pedir("/quimica/iniciar-reacciones", {
    "apellidos": "QA2", "nombres": "Perfecto", "carrera": "Electrónica"})
token2 = json.loads(cuerpo)["token"] if estado == 200 else ""
estado, cuerpo = pedir("/quimica/guardar-reacciones", {"token": token2, **respuestas})
r2 = json.loads(cuerpo).get("resultado", {}) if estado == 200 else {}
verificar("L3 envío perfecto => notas 100 en los 4 temas",
          estado == 200 and all(v == 100 for v in r2.get("notas", {}).values()))

# ------------------------------------------------------------- ATK-SEC
casos = [
    ("JSON malformado", "/quimica/iniciar-reacciones", None,
     {"Content-Type": "application/json"}, b"{roto"),
    ("Content-Type incorrecto", "/quimica/iniciar-reacciones", None,
     {"Content-Type": "text/plain"}, b"texto"),
    ("Payload gigante (1MB)", "/quimica/iniciar-reacciones", None,
     {"Content-Type": "application/json"},
     b'{"apellidos":"' + b"A" * 1_000_000 +
     '","nombres":"x","carrera":"Mecánica"}'.encode()),
]
for nombre, ruta, _d, cabeceras, cuerpo in casos:
    codigo, _ = pedir(ruta, bruto=cuerpo, cabeceras=cabeceras)
    verificar(f"SEC {nombre} (status={codigo}, no 500)", codigo not in (500, 502, 503),
              f"{codigo}")

ataques = [
    {"apellidos": "=HIPERVINCULO($A$1)", "nombres": "+cmd", "carrera": "Mecánica"},
    {"apellidos": "<script>alert(1)</script>", "nombres": "x", "carrera": "Mecánica"},
    {"apellidos": "López\nSegunda línea", "nombres": "x", "carrera": "Mecánica"},
]
for i, ataque in enumerate(ataques, 1):
    codigo, _ = pedir("/quimica/iniciar-reacciones", ataque)
    verificar(f"SEC ataque entrada {i} no provoca 500", codigo in (200, 400), str(codigo))

estado, _ = pedir("/quimica/incidente-reacciones", {"token": "x" * 32, "tipo": "pegar"})
verificar("SEC token desconocido rechazado", estado == 400, str(estado))
estado, _ = pedir("/quimica/incidente-reacciones", {"token": token, "tipo": "; rm -rf /"})
verificar("SEC tipo de incidente fuera de allow-list rechazado", estado == 400, str(estado))

estado, cuerpo = pedir("/quimica/reacciones-quimicas")
verificar("SEC la página no expone la clave de respuestas",
          b"ecuacionquimica" not in (cuerpo or b"") or
          b"CLAVE_VF" not in (cuerpo or b""))
verificar("SEC la página NO fuerza pantalla completa",
          b"requestFullscreen" not in (cuerpo or b""))

fallos = [r for r in RESULTADOS if not r[1]]
print(f"\n=== QA SAQI: {len(RESULTADOS) - len(fallos)}/{len(RESULTADOS)} PASS, "
      f"{len(fallos)} FAIL ===")
sys.exit(1 if fallos else 0)