"""Pruebas reproducibles: núcleo y API real con SQLite temporal."""
from copy import deepcopy
from contextlib import closing
from http.server import ThreadingHTTPServer
import json
from pathlib import Path
import sqlite3
import tempfile
import threading
import unittest
from urllib.error import HTTPError
from urllib.request import Request, urlopen
from nucleo import Dispositivo
from servidor import manejador, preparar


class Control(unittest.TestCase):
    def test_histeresis_y_antiguedad(self):
        d = Dispositivo()
        for valor, salida in [(24, False), (28, True), (27, True), (26, False), (27, False)]:
            d.medir(valor)
            self.assertEqual(d.salida, salida)
        d.medir(29)
        d.avanzar(5)
        self.assertTrue(d.salida)
        d.avanzar(1)
        self.assertFalse(d.salida)
        d.medir(None)
        self.assertIsNone(d.valor)
        self.assertFalse(d.salida)

    def test_cola_no_bloquea_control(self):
        d = Dispositivo()
        for valor in (24, 29, 27):
            self.assertTrue(d.medir(valor))
        self.assertFalse(d.medir(25))
        self.assertEqual((len(d.cola), d.descartes, d.seq, d.salida), (3, 1, 4, False))
        self.assertFalse(d.confirmar("otra", 1))
        self.assertFalse(d.confirmar("demo", 2))
        self.assertTrue(d.confirmar("demo", 1))
        self.assertEqual(d.cola[0]["seq"], 2)

    def test_configuracion_atomica(self):
        d = Dispositivo()
        d.medir(29)
        self.assertFalse(d.configurar(dict(version=1, on=28, off=26)))
        self.assertTrue(d.configurar(dict(version=2, on=30, off=29)))
        self.assertFalse(d.salida)
        for propuesta in (dict(version=3, on=25, off=30), dict(version=1, on=28, off=26),
                          dict(version=2, on=31, off=29), dict(version=True, on=30, off=29)):
            antes = deepcopy(d.__dict__)
            with self.assertRaises(ValueError):
                d.configurar(propuesta)
            self.assertEqual(d.__dict__, antes)

    def test_entrada_invalida_no_muta(self):
        d = Dispositivo()
        for valor in (float("nan"), True, -41, 126, "29"):
            antes = deepcopy(d.__dict__)
            with self.assertRaises(ValueError):
                d.medir(valor)
            self.assertEqual(d.__dict__, antes)


class Integracion(unittest.TestCase):
    def setUp(self):
        self.carpeta = tempfile.TemporaryDirectory()
        self.ruta = Path(self.carpeta.name) / "prueba.sqlite3"
        preparar(self.ruta)
        self.servidor = ThreadingHTTPServer(("127.0.0.1", 0), manejador(self.ruta, "equipo-de-prueba-0001", "admin-de-prueba-0002"))
        self.hilo = threading.Thread(target=self.servidor.serve_forever, daemon=True)
        self.hilo.start()
        self.url = f"http://127.0.0.1:{self.servidor.server_port}"

    def tearDown(self):
        self.servidor.shutdown()
        self.servidor.server_close()
        self.hilo.join()
        self.carpeta.cleanup()

    def api(self, ruta, datos=None, rol="equipo", raw=None):
        token = {"equipo": "equipo-de-prueba-0001", "admin": "admin-de-prueba-0002", "otro": "incorrecto"}[rol]
        cuerpo = raw if raw is not None else None if datos is None else json.dumps(datos).encode()
        req = Request(self.url + ruta, data=cuerpo, headers={"Authorization": "Bearer " + token, "Content-Type": "application/json"})
        try:
            with urlopen(req, timeout=3) as r:
                return r.status, json.loads(r.read())
        except HTTPError as r:
            with r:
                return r.code, json.loads(r.read())

    def muestra(self):
        d = Dispositivo("sesion-prueba")
        d.medir(29)
        return d.cola[0]

    def test_confirmacion_y_duplicados(self):
        r = self.muestra()
        estado, ack = self.api("/api/muestras", r)
        self.assertEqual((estado, ack["boot"], ack["seq"], ack["duplicado"]), (200, "sesion-prueba", 1, False))
        estado, ack = self.api("/api/muestras", r)
        self.assertTrue(ack["duplicado"])
        self.assertEqual(self.api("/api/estado", rol="admin")[1]["total"], 1)
        self.assertEqual(self.api("/api/muestras", dict(r, temperature=30))[0], 409)
        with closing(sqlite3.connect(self.ruta)) as db:
            self.assertEqual(db.execute("SELECT count(*) FROM muestras").fetchone()[0], 1)

    def test_roles(self):
        self.assertEqual(self.api("/api/config", rol="otro")[0], 401)
        self.assertEqual(self.api("/api/estado")[0], 401)
        self.assertEqual(self.api("/api/config", dict(version=2, on=30, off=27))[0], 401)
        self.assertEqual(self.api("/api/muestras", self.muestra(), rol="admin")[0], 401)

    def test_config_deseada_y_aplicada(self):
        p = dict(version=2, on=30, off=27)
        self.assertEqual(self.api("/api/config", p, rol="admin")[0], 200)
        self.assertEqual(self.api("/api/config")[1], p)
        self.assertEqual(self.api("/api/config", p, rol="admin")[0], 409)
        self.assertEqual(self.api("/api/config", dict(version=3, on=25, off=30), rol="admin")[0], 400)
        d = Dispositivo("agente")
        d.configurar(self.api("/api/config")[1])
        d.medir(29)
        self.assertEqual(d.cola[0]["config_version"], 2)
        self.assertEqual(self.api("/api/muestras", d.cola[0])[0], 200)
        preparar(self.ruta)  # Reinicializar tablas no borra la configuración.
        self.assertEqual(self.api("/api/config")[1], p)

    def test_validacion_de_identidad_y_esquema(self):
        r = self.muestra()
        for cambio in (dict(device="otro-equipo"), dict(seq=True), dict(schema=True),
                       dict(quality="invalid"), dict(temperature=float("nan")), dict(boot="../otro")):
            self.assertEqual(self.api("/api/muestras", dict(r, **cambio))[0], 400)
        self.assertEqual(self.api("/api/muestras", dict(r, quality="invalid", temperature=None, output=False))[0], 200)

    def test_limites_del_cuerpo(self):
        self.assertEqual(self.api("/api/muestras", raw=b"{" )[0], 400)
        self.assertEqual(self.api("/api/muestras", raw=b" " * 4097)[0], 400)


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