"""Pruebas del laboratorio, pasos 1 a 5. LAB_DIR dice dónde están sus archivos: lab o solution.

Cada prueba corre en su propio proceso, con sanitizers: una fuga o un acceso inválido
la hace fallar aunque el resultado parezca correcto.
"""
import os
import pathlib
import re
import subprocess
import unittest

HERE = pathlib.Path(__file__).parent
LAB_DIR = os.environ.get("LAB_DIR", "solution")
DATA = os.environ.get("DATA", "../../data")
BINARY = HERE / "build" / LAB_DIR / "lab_tests"
# Si una prueba no termina, lo más probable es un ciclo que no se detiene.
LIMIT_SECONDS = 60


def short_report(text):
    """Deja lo útil de un informe: el tipo de error y las líneas de sus archivos."""
    if "ERROR:" not in text and "runtime error" not in text:
        return text.strip()
    useful = [line.strip() for line in text.splitlines()
              if "ERROR:" in line or "SUMMARY:" in line or "runtime error" in line
              or re.search(r"(routes\.h|min_heap\.h|lab_tests\.cpp):\d+", line)]
    return "\n".join(useful[:12])


def build(target):
    done = subprocess.run(
        ["make", "--no-print-directory", f"LAB_DIR={LAB_DIR}", f"build/{LAB_DIR}/{target}"],
        cwd=HERE, text=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
    )
    return done.returncode, done.stdout


def add_cases(cls, names):
    for name in names:
        def case(self, name=name):
            try:
                done = subprocess.run(
                    [str(BINARY), name, DATA], cwd=HERE, text=True, encoding="utf-8", errors="replace",
                    stdout=subprocess.PIPE, stderr=subprocess.STDOUT, timeout=LIMIT_SECONDS,
                )
            except subprocess.TimeoutExpired:
                self.fail("no terminó a tiempo: ¿hay un ciclo que no se detiene?")
            self.assertEqual(done.returncode, 0, short_report(done.stdout))
        setattr(cls, f"test_{name}", case)


class LabCase(unittest.TestCase):
    @classmethod
    def setUpClass(cls):
        code, output = build("lab_tests")
        if code != 0:
            raise AssertionError(output)


@unittest.skipUnless((HERE / LAB_DIR).is_dir(), f"no existe la carpeta {LAB_DIR}")
class Builds(unittest.TestCase):
    def test_builds_without_warnings(self):
        for target in ["lab_tests", "compare_routes", "savings"]:
            (HERE / "build" / LAB_DIR / target).unlink(missing_ok=True)
            code, output = build(target)
            self.assertEqual(code, 0, output)
            self.assertNotIn("warning", output)


@unittest.skipUnless((HERE / LAB_DIR).is_dir(), f"no existe la carpeta {LAB_DIR}")
class TestHeap(LabCase):
    """Antes de empezar: su MinHeap de la semana 13."""


@unittest.skipUnless((HERE / LAB_DIR).is_dir(), f"no existe la carpeta {LAB_DIR}")
class TestMap(LabCase):
    """Paso 1: cargar el mapa en listas de adyacencia."""


@unittest.skipUnless((HERE / LAB_DIR).is_dir(), f"no existe la carpeta {LAB_DIR}")
class TestDijkstra(LabCase):
    """Paso 2: Dijkstra con su MinHeap, y la ruta."""

    def test_uses_min_heap(self):
        text = (HERE / LAB_DIR / "routes.h").read_text(encoding="utf-8")
        self.assertRegex(text, r"MinHeap\s*<", "dijkstra tiene que usar su MinHeap de la semana 13")


@unittest.skipUnless((HERE / LAB_DIR).is_dir(), f"no existe la carpeta {LAB_DIR}")
class TestRoutes(LabCase):
    """Paso 3: BFS frente a Dijkstra."""


@unittest.skipUnless((HERE / LAB_DIR).is_dir(), f"no existe la carpeta {LAB_DIR}")
class TestStop(LabCase):
    """Paso 4: detenerse al sacar el destino."""


@unittest.skipUnless((HERE / LAB_DIR).is_dir(), f"no existe la carpeta {LAB_DIR}")
class TestQueues(LabCase):
    """Paso 5: std::priority_queue y el arreglo sin heap."""


add_cases(TestHeap, ["heap_from_week_13"])
add_cases(TestMap, ["map_graph_small", "map_graph_real"])
add_cases(TestDijkstra, [
    "dijkstra_hand_case", "dijkstra_random_against_brute_force", "dijkstra_unreachable", "dijkstra_large_weights",
    "dijkstra_counts_on_real_map", "dijkstra_many_sources_on_real_map", "route_on_hand_case", "route_on_real_map",
])
add_cases(TestRoutes, ["route_meters_hand_case", "routes_against_bfs"])
add_cases(TestStop, [
    "stop_tiny_case", "stop_hand_case", "stop_random_against_brute_force", "stop_saves_nodes_on_real_map",
])
add_cases(TestQueues, [
    "priority_queue_random_against_brute_force", "priority_queue_same_counts", "array_random_against_brute_force",
    "array_disconnected", "array_reads_the_whole_array",
])


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