"""Pruebas de integración del blueprint Flask reacciones_cdt (SDD/TDD, formato SAQI).

Cubren: contrato de API, incidentes con descuento, guardado de 4 filas por tema,
bloqueo de reenvíos (409) y saneamiento/seguridad de entradas.
"""

import csv
import os
import tempfile
import unittest

from flask import Flask

import reacciones_cdt
from reacciones_cdt import bp as reacciones_bp


def respuestas_perfectas():
    return {
        "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"},
        }
    }


def leer_csv(ruta):
    if not os.path.exists(ruta):
        return []
    with open(ruta, newline="", encoding="utf-8-sig") as f:
        return list(csv.reader(f))


class BaseTest(unittest.TestCase):

    def setUp(self):
        self.tmp = tempfile.TemporaryDirectory()
        self.orig_res = reacciones_cdt.CSV_RESULTADOS
        self.orig_inc = reacciones_cdt.CSV_INCIDENCIAS
        self.res_path = os.path.join(self.tmp.name, "reacciones_resultados.csv")
        self.inc_path = os.path.join(self.tmp.name, "reacciones_incidencias.csv")
        reacciones_cdt.CSV_RESULTADOS = self.res_path
        reacciones_cdt.CSV_INCIDENCIAS = self.inc_path
        reacciones_cdt._sesiones.clear()

        app = Flask(__name__)
        app.register_blueprint(reacciones_bp)
        app.config["TESTING"] = True
        self.client = app.test_client()

    def tearDown(self):
        reacciones_cdt.CSV_RESULTADOS = self.orig_res
        reacciones_cdt.CSV_INCIDENCIAS = self.orig_inc
        reacciones_cdt._sesiones.clear()
        self.tmp.cleanup()

    def _iniciar(self, **extra):
        datos = {"apellidos": "Pérez", "nombres": "Luis",
                 "carrera": "Mecánica"}
        datos.update(extra)
        return self.client.post("/iniciar-reacciones", json=datos)


class TestRutasBasicas(BaseTest):

    def test_pagina_200(self):
        r = self.client.get("/reacciones-quimicas")
        self.assertEqual(r.status_code, 200)

    def test_pagina_no_expone_clave(self):
        contenido = self.client.get("/reacciones-quimicas")\
            .get_data(as_text=True)
        self.assertNotIn("CLAVE_VF", contenido)


class TestIniciar(BaseTest):

    def test_iniciar_ok_token_32hex(self):
        r = self._iniciar()
        self.assertEqual(r.status_code, 200)
        token = r.get_json()["token"]
        self.assertIsInstance(token, str)
        self.assertEqual(len(token), 32)
        self.assertTrue(all(c in "0123456789abcdef" for c in token))

    def test_iniciar_carrera_no_permitida_400(self):
        r = self._iniciar(carrera="Medicina")
        self.assertEqual(r.status_code, 400)

    def test_iniciar_falta_nombres_400(self):
        r = self._iniciar(nombres="")
        self.assertEqual(r.status_code, 400)
class TestIncidente(BaseTest):

    def test_incidente_valido_descuenta(self):
        token = self._iniciar().get_json()["token"]
        r = self.client.post("/incidente-reacciones",
                             json={"token": token, "tipo": "pegar"})
        self.assertEqual(r.status_code, 200)
        self.assertEqual(r.get_json()["descuento"], 5)

    def test_incidente_tipo_invalido_400(self):
        token = self._iniciar().get_json()["token"]
        r = self.client.post("/incidente-reacciones",
                             json={"token": token, "tipo": "rm -rf /"})
        self.assertEqual(r.status_code, 400)
        self.assertEqual(leer_csv(self.inc_path), [])

    def test_incidente_token_desconocido_400(self):
        r = self.client.post("/incidente-reacciones",
                             json={"token": "x" * 32, "tipo": "pegar"})
        self.assertEqual(r.status_code, 400)

    def test_incidentes_se_acumulan(self):
        token = self._iniciar().get_json()["token"]
        for _ in range(3):
            self.client.post("/incidente-reacciones",
                             json={"token": token, "tipo": "copiar"})
        filas = leer_csv(self.inc_path)
        self.assertEqual(len(filas), 4)  # encabezado + 3 incidentes


class TestGuardar(BaseTest):

    def test_guardar_perfecto_notas_100_y_cuatro_filas(self):
        token = self._iniciar().get_json()["token"]
        r = self.client.post("/guardar-reacciones",
                             json={"token": token, **respuestas_perfectas()})
        self.assertEqual(r.status_code, 200)
        resultado = r.get_json()["resultado"]
        self.assertEqual(resultado["nota_final"], 100)
        filas = leer_csv(self.res_path)
        self.assertEqual(len(filas), 2)  # encabezado + 1 fila (nota unificada)

    def test_guardar_con_incidentes_descuenta(self):
        token = self._iniciar().get_json()["token"]
        self.client.post("/incidente-reacciones",
                         json={"token": token, "tipo": "pegar"})
        r = self.client.post("/guardar-reacciones",
                             json={"token": token, **respuestas_perfectas()})
        notas = r.get_json()["resultado"]["notas"]
        self.assertEqual(notas, {"4.1.1": 95, "4.1.2": 95,
                                 "4.1.3": 95, "4.1.4": 95})

    def test_reenvio_duplicado_409_sin_duplicar(self):
        token = self._iniciar().get_json()["token"]
        payload = {"token": token, **respuestas_perfectas()}
        self.assertEqual(self.client.post("/guardar-reacciones",
                                          json=payload).status_code, 200)
        r = self.client.post("/guardar-reacciones", json=payload)
        self.assertEqual(r.status_code, 409)
        filas = leer_csv(self.res_path)
        self.assertEqual(len(filas), 2)  # no se duplicó

    def test_token_desconocido_400(self):
        r = self.client.post("/guardar-reacciones",
                             json={"token": "y" * 32, **respuestas_perfectas()})
        self.assertEqual(r.status_code, 400)

    def test_respuesta_corta_se_acorta_longitud(self):
        token = self._iniciar().get_json()["token"]
        payload = {"token": token,
                   "respuestas": {"4.1.3": {"2": "E" * 200}}}
        r = self.client.post("/guardar-reacciones", json=payload)
        self.assertEqual(r.status_code, 200)  # no cae; se sana

    def test_inyeccion_csv_en_apellidos_escapada(self):
        token = self._iniciar(apellidos="=HIPERVINCULO($A$1)").get_json()["token"]
        r = self.client.post("/guardar-reacciones",
                             json={"token": token, **respuestas_perfectas()})
        self.assertEqual(r.status_code, 200)
        filas = leer_csv(self.res_path)
        self.assertTrue(filas[1][1].startswith("'"))  # celda escapada

    def test_correlativo_por_estudiante(self):
        # Primer estudiante → No=1
        t1 = self._iniciar(apellidos="Uno", nombres="Primer").get_json()["token"]
        self.client.post("/guardar-reacciones", json={"token": t1, **respuestas_perfectas()})
        filas = leer_csv(self.res_path)
        self.assertEqual(filas[1][0], "1")  # No del primer estudiante
        # Segundo estudiante → No=2
        t2 = self._iniciar(apellidos="Dos", nombres="Segundo").get_json()["token"]
        self.client.post("/guardar-reacciones", json={"token": t2, **respuestas_perfectas()})
        filas = leer_csv(self.res_path)
        self.assertEqual(filas[2][0], "2")

    def test_json_malformado_sin_500(self):
        r = self.client.post("/iniciar-reacciones", data=b"{roto",
                             content_type="application/json")
        self.assertEqual(r.status_code, 400)


if __name__ == "__main__":
    unittest.main()