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

Los programas se compilan y corren con sanitizers: una fuga o un acceso inválido hace
fallar la prueba, 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")
OUT = HERE / "build" / LAB_DIR
SAMPLES = pathlib.Path("lab") / "samples"
AVAILABLE = (HERE / LAB_DIR).is_dir()


def short_report(text):
    """Deja lo útil de un informe: el error y las líneas de los archivos propios."""
    useful = [line.strip() for line in text.splitlines()
              if "ERROR:" in line or "SUMMARY:" in line or "runtime error" in line
              or "Assertion" in line or "falta implementar" in line
              or re.search(r"(int_stack|stack|brackets|test_\w+)\.(h|cpp):\d+", line)]
    return "\n".join(useful[:12]) or text.strip()


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, timeout=300,
    )
    if done.returncode != 0:
        raise AssertionError(f"no compila {target}:\n{done.stdout}")


def run_program(program, *args):
    return subprocess.run(
        [str(OUT / program), *args], cwd=HERE, text=True, encoding="utf-8", errors="replace",
        stdin=subprocess.DEVNULL, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, timeout=120,
    )


@unittest.skipUnless(AVAILABLE, f"no existe {LAB_DIR}/")
class NamedTests(unittest.TestCase):
    program = None

    @classmethod
    def setUpClass(cls):
        if cls.program:
            build(cls.program)

    def check(self, name):
        done = run_program(self.program, name)
        self.assertEqual(done.returncode, 0, short_report(done.stdout))


class Step1Basics(NamedTests):
    """Paso 1: constructor, destructor y operaciones."""
    program = "test_int_stack"

    def test_lifo_order(self):
        self.check("basic")

    def test_reserves_once(self):
        self.check("reserves_once")

    def test_zero_capacity(self):
        self.check("zero_capacity")


class Step3Copy(NamedTests):
    """Paso 3: copia y autoasignación."""
    program = "test_int_stack"

    def test_copy_constructor(self):
        self.check("copy_constructor")

    def test_copy_assignment(self):
        self.check("copy_assignment")

    def test_self_assignment(self):
        self.check("self_assignment")


class Step4Move(NamedTests):
    """Paso 4: mover no reserva memoria."""
    program = "test_int_stack"

    def test_move_constructor(self):
        self.check("move_constructor")

    def test_move_assignment(self):
        self.check("move_assignment")

    def test_self_move(self):
        self.check("self_move")

    def test_moved_from_is_reusable(self):
        self.check("moved_from_is_reusable")

    def test_noexcept(self):
        self.check("noexcept")


class Step5Template(NamedTests):
    """Paso 5: el template, con int y con std::string."""
    program = "test_stack"

    def test_int_lifo(self):
        self.check("int_lifo")

    def test_string_lifo(self):
        self.check("string_lifo")

    def test_zero_capacity(self):
        self.check("zero_capacity")

    def test_random_int(self):
        self.check("random_int")

    def test_random_string(self):
        self.check("random_string")

    def test_copies_are_independent(self):
        self.check("copies_are_independent")

    def test_move_does_not_copy(self):
        self.check("move_does_not_copy")

    def test_noexcept(self):
        self.check("noexcept")

    def test_plain_struct(self):
        self.check("plain_struct")


@unittest.skipUnless(AVAILABLE, f"no existe {LAB_DIR}/")
class Step6Brackets(unittest.TestCase):
    """Paso 6: el verificador, sobre archivos de verdad."""

    @classmethod
    def setUpClass(cls):
        build("check_brackets")

    def expect(self, sample, code, message):
        path = str(SAMPLES / sample)
        done = run_program("check_brackets", path)
        self.assertEqual(done.stdout.strip(), f"{path}{message}", short_report(done.stdout))
        self.assertEqual(done.returncode, code)

    def test_good_file_with_comments_and_strings(self):
        self.expect("good.cpp", 0, ": ok")

    def test_empty_file(self):
        self.expect("empty.cpp", 0, ": ok")

    def test_extra_close(self):
        self.expect("extra_close.cpp", 1, ":2: ')' sin abrir")

    def test_mismatch(self):
        self.expect("mismatch.cpp", 1, ":6: ')' no cierra el '{' de la línea 4")

    def test_unclosed(self):
        self.expect("unclosed.cpp", 1, ":1: '{' sin cerrar")

    def test_reports_only_the_first_error(self):
        self.expect("first_of_two.cpp", 1, ":2: ']' no cierra el '(' de la línea 2")

    def test_its_own_source_code(self):
        for name in ["check_brackets.cpp", "code_text.cpp", "code_text.h", "brackets.h"]:
            path = f"lab/{name}"
            done = run_program("check_brackets", path)
            self.assertEqual(done.stdout.strip(), f"{path}: ok", short_report(done.stdout))

    def test_deep_nesting_does_not_overflow(self):
        deep = HERE / "build" / f"deep_{LAB_DIR}.cpp"
        deep.write_text("(" * 5000 + ")" * 5000, encoding="utf-8")
        done = run_program("check_brackets", str(deep))
        self.assertEqual(done.returncode, 0, short_report(done.stdout))


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