"""Vuelve a correr los comandos que aparecen en los apuntes y en las diapositivas
y comprueba que el código citado existe de verdad en esta carpeta."""
import pathlib
import re
import subprocess
import unittest

HERE = pathlib.Path(__file__).parent
ROOT = HERE.parent.parent
NOTES = ROOT / "notes" / "week-02.md"
SLIDES = ROOT / "slides" / "week-02.tex"
# Antes de un bloque cuya salida cambia con el sistema o con la corrida. Solo se
# exigen sus líneas estables, las que contienen alguna de STABLE, sin direcciones.
SYSTEM_DEPENDENT = "<!-- depende del sistema -->"
STABLE = ("runtime error", "Assertion", "warning:", "AddressSanitizer", "READ of size",
          "WRITE of size", " in main errors/", "<== Memory access")


def normalize(text):
    # Según el idioma del entorno, gcc usa comillas tipográficas o rectas.
    return text.replace("‘", "'").replace("’", "'")


def without_addresses(text):
    text = re.sub(r"0x[0-9a-f]+", "0x", text)
    return re.sub(r"==\d+==", "==N==", text)


def run_shell(command):
    done = subprocess.run(
        ["bash", "-c", command], cwd=HERE, text=True,
        stdout=subprocess.PIPE, stderr=subprocess.STDOUT, timeout=60,
    )
    return normalize(done.stdout)


def split_commands(lines):
    """Parte un bloque de terminal en pares (comando, líneas de salida)."""
    pairs = []
    for line in lines:
        if line.startswith("$ "):
            pairs.append((line[2:], []))
        elif pairs:
            pairs[-1][1].append(normalize(line.rstrip()))
    return pairs


def note_blocks(language):
    """Devuelve (líneas, marca) de cada bloque de los apuntes."""
    blocks, current, mark, previous, quoted = [], None, "", "", False
    for raw in NOTES.read_text(encoding="utf-8").splitlines():
        if current is None:
            quoted = raw.startswith(">")
            line = re.sub(r"^> ?", "", raw)
            if line.strip() == "```" + language:
                current, mark = [], previous.strip()
            elif line.strip():
                previous = line
        else:
            line = re.sub(r"^> ?", "", raw) if quoted else raw
            if line.strip() == "```":
                blocks.append((current, mark))
                current = None
            else:
                current.append(line)
    return blocks


def slide_blocks(environments):
    text = SLIDES.read_text(encoding="utf-8")
    pattern = r"\\begin\{(?:%s)\}[^\n]*\n(.*?)\\end\{(?:%s)\}" % (environments, environments)
    return [match.group(1).splitlines() for match in re.finditer(pattern, text, re.S)]


def known_code_lines():
    lines = set()
    sources = list(HERE.glob("*.c")) + list((HERE / "errors").glob("*.c"))
    for source in sources:
        lines.update(line.strip() for line in source.read_text(encoding="utf-8").splitlines())
    return lines


class NotesAreTrue(unittest.TestCase):
    @unittest.skipUnless(NOTES.exists(), "faltan los apuntes")
    def test_terminal_blocks_in_notes(self):
        checked = 0
        for lines, mark in note_blocks("text"):
            if not lines or not lines[0].startswith("$ "):
                continue
            for command, expected in split_commands(lines):
                actual = "\n".join(line.rstrip() for line in run_shell(command).rstrip("\n").split("\n"))
                checked += 1
                with self.subTest(command=command):
                    if mark == SYSTEM_DEPENDENT:
                        wanted = [line for line in expected if any(s in line for s in STABLE)]
                        for line in wanted:
                            self.assertIn(without_addresses(line), without_addresses(actual))
                    else:
                        self.assertEqual(actual, "\n".join(expected))
        self.assertGreater(checked, 10)

    @unittest.skipUnless(NOTES.exists(), "faltan los apuntes")
    def test_code_blocks_in_notes_exist(self):
        known = known_code_lines()
        for lines, _ in note_blocks("c"):
            for line in lines:
                if line.strip() and line.strip() != "...":
                    with self.subTest(line=line):
                        self.assertIn(line.strip(), known)

    @unittest.skipUnless(NOTES.exists(), "faltan los apuntes")
    def test_notes_do_not_show_the_lab_bugs(self):
        text = NOTES.read_text(encoding="utf-8")
        self.assertNotIn("N_GRADES - 1", text)
        self.assertNotIn("total_points(s) / N_GRADES", text)


class SlidesAreTrue(unittest.TestCase):
    @classmethod
    def setUpClass(cls):
        # Una diapositiva corre estos dos sin mostrar cómo se compilan.
        for name in ["trips_bug", "test_trips_bug"]:
            run_shell(f"gcc -Wall -Wextra -std=c17 -g -fsanitize=address,undefined "
                      f"errors/{name}.c -o build/{name}")

    @unittest.skipUnless(SLIDES.exists(), "faltan las diapositivas")
    def test_terminal_blocks_in_slides(self):
        # Las direcciones cambian de una corrida a otra: se comparan sin ellas.
        for block in slide_blocks("terminal|salidalarga|salidamedia"):
            for command, expected in split_commands(block):
                actual = without_addresses(run_shell(command)).splitlines()
                position = 0
                # Una diapositiva puede omitir líneas, pero no cambiarlas ni reordenarlas.
                for line in expected:
                    line = without_addresses(line).strip()
                    if not line:
                        continue
                    with self.subTest(command=command, line=line):
                        found = [i for i in range(position, len(actual)) if actual[i].strip() == line]
                        self.assertTrue(found, f"no aparece en la salida real: {line}")
                        if found:
                            position = found[0] + 1

    @unittest.skipUnless(SLIDES.exists(), "faltan las diapositivas")
    def test_code_blocks_in_slides_exist(self):
        known = known_code_lines()
        for block in slide_blocks("codigo|codigochico|codigomini|codigomicro"):
            for line in block:
                if line.strip() and line.strip() != "...":
                    with self.subTest(line=line):
                        self.assertIn(line.strip(), known)


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