"""Pruebas del laboratorio. LAB_DIR dice dónde está el código: 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. Ninguna prueba mide tiempo: todas cuentan.
"""
import os
import pathlib
import re
import subprocess
import unittest

HERE = pathlib.Path(__file__).parent
LAB_DIR = os.environ.get("LAB_DIR", "solution")
DATA = pathlib.Path(os.environ.get("DATA", "../../data"))
if not DATA.is_absolute():
    DATA = (HERE / DATA).resolve()
FIXTURES = HERE / "lab" / "tests" / "fixtures"
BUILD = HERE / "build" / LAB_DIR
TABLE = [
    "hash_vectors", "new_is_empty", "set_and_get", "overwrite", "contains", "remove_present",
    "remove_absent", "unusual_keys", "growth", "copy_is_independent", "self_assignment",
    "move_leaves_empty_and_usable", "random_small", "random_large", "new_table_has_8_slots",
    "search_passes_tombstones", "set_does_not_duplicate_past_tombstone", "set_reuses_tombstone",
    "lowering_load_factor_rebuilds", "high_load_factor_still_ends",
    "probes_count_the_last_slot", "churn_keeps_probes_low", "rebuild_clears_tombstones",
    "remove_releases_value", "moved_from_is_usable", "string_values", "my_churn_test",
]
WEEK7 = ["week7_table_passes_suite", "fnv1a_of_week7"]
SPELL = [
    "helper_splits_letters", "helper_extracts_words", "candidates_are_the_three_edits",
    "candidates_are_valid_utf8", "suggest_with_sorted", "suggest_with_std", "suggest_with_probing",
    "suggest_with_week7", "suggestions_are_sorted_and_unique", "report_with_probing",
    "report_with_week7", "report_with_std", "report_with_sorted", "suggests_each_word_once",
]


def compiler_lines(output):
    """Quita los avisos de make, como el de relojes desfasados en una carpeta compartida."""
    return "\n".join(line for line in output.splitlines() if not line.startswith("make:"))


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"(probing_hash_map|hash_map|spell_checker|dictionaries)\.h:\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 run_map_case(testcase, name):
    done = subprocess.run(
        [str(BUILD / "map_tests"), name], cwd=HERE, text=True,
        stdout=subprocess.PIPE, stderr=subprocess.STDOUT, timeout=120,
    )
    testcase.assertEqual(done.returncode, 0, short_report(done.stdout))


def run_spell_case(testcase, name):
    done = subprocess.run(
        [str(BUILD / "spell_tests"), name, str(FIXTURES)], cwd=HERE, text=True,
        stdout=subprocess.PIPE, stderr=subprocess.STDOUT, timeout=120,
    )
    testcase.assertEqual(done.returncode, 0, short_report(done.stdout))


@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 ("map_tests", "spell_tests", "spell", "bench_spell"):
            (BUILD / target).unlink(missing_ok=True)
            code, output = build(target)
            self.assertEqual(code, 0, output)
            self.assertNotIn("warning:", compiler_lines(output))


@unittest.skipUnless((HERE / LAB_DIR).is_dir(), f"no existe la carpeta {LAB_DIR}")
class TestTable(unittest.TestCase):
    @classmethod
    def setUpClass(cls):
        code, output = build("map_tests")
        if code != 0:
            raise AssertionError(output)


@unittest.skipUnless((HERE / LAB_DIR).is_dir(), f"no existe la carpeta {LAB_DIR}")
class TestSpell(unittest.TestCase):
    @classmethod
    def setUpClass(cls):
        for target in ("map_tests", "spell_tests", "spell"):
            code, output = build(target)
            if code != 0:
                raise AssertionError(output)

    def test_real_files(self):
        """El corrector completo, con los archivos reales: solo cuenta palabras."""
        done = subprocess.run(
            [str(BUILD / "spell"), str(DATA / "palabras.txt"), str(DATA / "tradiciones.txt"), "0"],
            cwd=HERE, text=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, timeout=120,
        )
        self.assertEqual(done.returncode, 0, short_report(done.stdout))
        self.assertIn("diccionario: 1045757 palabras", done.stdout)
        self.assertIn("texto: 48004 palabras", done.stdout)
        self.assertIn("desconocidas: 1378 apariciones de 708 palabras distintas", done.stdout)


for name in TABLE:
    def table_case(self, name=name):
        run_map_case(self, name)
    setattr(TestTable, f"test_{name}", table_case)
for name in WEEK7:
    def week7_case(self, name=name):
        run_map_case(self, name)
    setattr(TestSpell, f"test_{name}", week7_case)
for name in SPELL:
    def spell_case(self, name=name):
        run_spell_case(self, name)
    setattr(TestSpell, f"test_{name}", spell_case)


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