"""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. Las pruebas cuentan comparaciones y
lecturas, nunca tiempos.
"""
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"
SIGNED = HERE / "build" / LAB_DIR / "lab_tests_signed"
FIXTURE = "lab/tests/fixtures/terminos_chicos.tsv"
TERMS = f"{DATA}/terminos.tsv"

SORTS = [
    "merge_small", "merge_random", "merge_strings", "merge_uses_comparator", "merge_is_stable",
    "merge_does_not_copy", "merge_counts", "quick_small", "quick_random", "quick_strings",
    "quick_uses_comparator", "quick_sorted_input_is_fast", "quick_all_equal_is_one_pass",
    "quick_few_values_is_linear", "quick_copies_only_pivots", "slow_input_is_slow",
]
AUTOCOMPLETE = [
    "sort_is_byte_order", "sort_real_file", "prefix_ascii", "prefix_whole_term_is_included",
    "prefix_accents_are_other_bytes", "prefix_empty_is_everything", "prefix_absent",
    "prefix_extreme_bytes", "prefix_real_counts", "prefix_matches_oracle", "prefix_reads_few_terms",
    "top_k_order_and_ties", "top_k_edges", "top_k_keeps_weights", "top_k_real_file_order",
    "top_k_matches_scan", "top_k_reads_only_the_range", "scan_reads_the_whole_file",
]


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"(sorts|autocomplete|lab_tests)\.(h|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 run_case(testcase, binary, name):
    done = subprocess.run(
        [str(binary), name, FIXTURE, TERMS], cwd=HERE, text=True, encoding="utf-8", errors="replace",
        stdout=subprocess.PIPE, stderr=subprocess.STDOUT, timeout=30,
    )
    testcase.assertEqual(done.returncode, 0, short_report(done.stdout))


def add_cases(cls, binary, names):
    for name in names:
        def case(self, name=name):
            run_case(self, binary, name)
        setattr(cls, f"test_{name}", case)
    return cls


class LabCase(unittest.TestCase):
    target = "lab_tests"

    @classmethod
    def setUpClass(cls):
        code, output = build(cls.target)
        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):
        BINARY.unlink(missing_ok=True)
        code, output = build("lab_tests")
        self.assertEqual(code, 0, output)
        # make avisa a veces de relojes desfasados en carpetas compartidas: eso no cuenta.
        warnings = [line for line in output.splitlines() if "warning" in line and not line.startswith("make")]
        self.assertEqual(warnings, [])


@unittest.skipUnless((HERE / LAB_DIR).is_dir(), f"no existe la carpeta {LAB_DIR}")
class TestSorts(LabCase):
    pass


@unittest.skipUnless((HERE / LAB_DIR).is_dir(), f"no existe la carpeta {LAB_DIR}")
class TestAutocomplete(LabCase):
    pass


@unittest.skipUnless((HERE / LAB_DIR).is_dir(), f"no existe la carpeta {LAB_DIR}")
class TestSignedChar(LabCase):
    """El orden por bytes no debe depender de si char tiene signo en la máquina."""
    target = "lab_tests_signed"


add_cases(TestSorts, BINARY, SORTS)
add_cases(TestAutocomplete, BINARY, AUTOCOMPLETE)
add_cases(TestSignedChar, SIGNED, AUTOCOMPLETE)


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