"""Pruebas del laboratorio. LAB_DIR dice dónde está el código: lab o solution.

Ninguna prueba compara tiempos, porque dependen de la máquina. Comparan cuentas de pasos,
resultados y el formato del CSV y del informe.
"""
import csv
import os
import pathlib
import re
import subprocess
import tempfile
import unittest

HERE = pathlib.Path(__file__).parent
LAB_DIR = os.environ.get("LAB_DIR", "solution")
SOURCE = HERE / LAB_DIR
BUILD = HERE / "build" / LAB_DIR
MYSTERIES = ["mystery_" + letter for letter in "abcdef"]
SESSION2 = ["push_back", "insert_front", "linear_search", "binary_search"]
CLASSES = ["1", "log n", "n", "n log n", "n^2", "2^n"]


def run(*command):
    return subprocess.run(
        [str(part) for part in command], cwd=HERE, text=True, encoding="utf-8",
        stdin=subprocess.DEVNULL, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, timeout=180,
    )


def read_csv(path):
    with open(path, encoding="utf-8", newline="") as source:
        return list(csv.DictReader(source))


def class_from_steps(rows):
    """La clase que se deduce de los pasos contados, que no dependen de la máquina."""
    steps = [int(row["steps"]) for row in rows]
    sizes = [int(row["n"]) for row in rows]
    last_ratio = steps[-1] / steps[-2]
    if steps[-1] == steps[-2]:
        return "1"
    if last_ratio > 8:
        return "2^n"
    if last_ratio > 3:
        return "n^2"
    if last_ratio < 1.5:
        return "log n"
    if (steps[-1] / sizes[-1]) / (steps[0] / sizes[0]) > 1.2:
        return "n log n"
    return "n"


def report_rows(text):
    rows = {}
    for line in text.splitlines():
        cells = [cell.strip() for cell in line.strip().strip("|").split("|")]
        if cells and cells[0] in MYSTERIES:
            rows[cells[0]] = cells
    return rows


@unittest.skipUnless(SOURCE.exists(), "falta la carpeta del laboratorio")
class Lab(unittest.TestCase):
    def test_unit_tests(self):
        done = run(BUILD / "lab_tests")
        self.assertEqual(done.returncode, 0, done.stdout)

    def test_doubling_csv(self):
        with tempfile.TemporaryDirectory() as folder:
            path = pathlib.Path(folder) / "doubling.csv"
            done = run(BUILD / "run_doubling", path)
            self.assertEqual(done.returncode, 0, done.stdout)
            rows = read_csv(path)
        names = {row["function"] for row in rows}
        self.assertEqual(names, set(MYSTERIES))
        for name in MYSTERIES:
            own = [row for row in rows if row["function"] == name]
            self.assertGreater(len(own), 3, name)
            sizes = [int(row["n"]) for row in own]
            self.assertEqual(sizes, [sizes[0] * 2 ** i for i in range(len(sizes))], name)
            self.assertTrue(all(float(row["seconds"]) > 0 for row in own), name)

    def test_session2_csv(self):
        with tempfile.TemporaryDirectory() as folder:
            path = pathlib.Path(folder) / "session2.csv"
            done = run(BUILD / "run_session2", path)
            self.assertEqual(done.returncode, 0, done.stdout)
            rows = read_csv(path)
        for name in SESSION2:
            own = [row for row in rows if row["function"] == name]
            self.assertGreater(len(own), 3, f"faltan filas de {name}")
            self.assertTrue(all(float(row["seconds"]) > 0 for row in own), name)
        linear = [row for row in rows if row["function"] == "linear_search"]
        binary = [row for row in rows if row["function"] == "binary_search"]
        self.assertTrue(all(int(row["steps"]) == int(row["n"]) for row in linear),
                        "la lineal debe medirse en su peor caso")
        for row in binary:
            self.assertLess(int(row["steps"]), 2 + len(bin(int(row["n"]))), "binaria: demasiados pasos")

    def test_report(self):
        text = (SOURCE / "report.md").read_text(encoding="utf-8")
        self.assertNotIn("PENDIENTE", text, "todavía hay partes sin llenar en el informe")
        rows = report_rows(text)
        self.assertEqual(set(rows), set(MYSTERIES))
        with tempfile.TemporaryDirectory() as folder:
            path = pathlib.Path(folder) / "doubling.csv"
            run(BUILD / "run_doubling", path)
            measured = read_csv(path)
        for name, cells in rows.items():
            self.assertEqual(len(cells), 5, name)
            self.assertIn(cells[1], CLASSES, f"{name}: la predicción no es una clase de la lista")
            self.assertIn(cells[4], CLASSES, f"{name}: la clase medida no es de la lista")
            self.assertRegex(cells[3], r"\d", f"{name}: faltan las razones medidas")
            own = [row for row in measured if row["function"] == name]
            self.assertEqual(cells[4], class_from_steps(own), f"{name}: revisen la clase medida")
        for title in ["push_back", "lineal", "-O0", "Conclusión"]:
            match = re.search(r"^#+ .*%s.*\n((?:(?!^#).*\n?)*)" % re.escape(title), text, re.M)
            self.assertIsNotNone(match, f"falta la sección de {title}")
            self.assertRegex(match.group(1), r"\d", f"la sección de {title} debe citar números")


@unittest.skipUnless((HERE / "solution").exists(), "falta la solución")
class StarterFails(unittest.TestCase):
    """El laboratorio sin resolver debe compilar y no pasar las pruebas."""

    def test_starter_compiles_and_fails(self):
        built = run("make", "--no-print-directory", "LAB_DIR=lab", "lab")
        self.assertEqual(built.returncode, 0, built.stdout)
        self.assertNotIn("warning", built.stdout)
        done = run(HERE / "build" / "lab" / "lab_tests")
        self.assertNotEqual(done.returncode, 0, "el laboratorio sin resolver pasa las pruebas")
        self.assertIn("PENDIENTE", (HERE / "lab" / "report.md").read_text(encoding="utf-8"))


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