"""Pruebas de los ejemplos de la semana: lo que dicen los apuntes sobre el código."""
import pathlib
import re
import subprocess
import unittest

HERE = pathlib.Path(__file__).parent
FLAGS = ["-Wall", "-Wextra", "-std=c17"]
SANITIZERS = ["-g", "-fsanitize=address,undefined"]
PROGRAMS = ["array_review", "string_review", "struct_review", "file_review", "trips",
            "test_trips", "trips_total", "test_trips_total"]


def run(program, stdin=""):
    done = subprocess.run(
        [str(HERE / "build" / program)], input=stdin, text=True, cwd=HERE,
        stdout=subprocess.PIPE, stderr=subprocess.PIPE, timeout=10,
    )
    return done.stdout, done.stderr, done.returncode


def compile_error_program(name):
    """Compila errors/<name>.c con sanitizers y devuelve lo que dijo gcc."""
    built = subprocess.run(
        ["gcc", *FLAGS, *SANITIZERS, str(HERE / "errors" / f"{name}.c"),
         "-o", str(HERE / "build" / f"{name}_san")],
        cwd=HERE, text=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
    )
    return built


class Compile(unittest.TestCase):
    def test_no_warnings(self):
        for program in PROGRAMS:
            with self.subTest(program=program):
                done = subprocess.run(
                    ["gcc", *FLAGS, "-Werror", "-fsyntax-only", f"{program}.c"], cwd=HERE,
                    text=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
                )
                self.assertEqual(done.returncode, 0, done.stdout)

    def test_no_pointers_beyond_file(self):
        # Los punteros se enseñan en la semana 4: aquí solo se declara FILE *.
        files = [HERE / f"{name}.c" for name in PROGRAMS] + list(HERE.glob("errors/*.c"))
        files += list(HERE.glob("lab/*.c")) + list(HERE.glob("solution/*.c"))
        for path in files:
            for line in path.read_text(encoding="utf-8").splitlines():
                code = line.split("//")[0]
                with self.subTest(path=path.name, line=line):
                    self.assertNotRegex(code, r"\b(?!FILE\b)\w+ \*\w")
                    self.assertNotIn("malloc", code)


class Examples(unittest.TestCase):
    def test_sum_with_part_of_the_array(self):
        output, _, status = run("array_review")
        self.assertEqual(status, 0)
        self.assertIn("Suma: 51\n", output)
        self.assertIn("Suma de las 2 primeras: 23\n", output)
        self.assertIn("Con un punto más: 55\n", output)

    def test_struct_copies(self):
        output, _, _ = run("struct_review")
        self.assertEqual(output.splitlines()[:5], [
            "Ana: 14 9 17", "Ana: 14 20 17", "En la función: 0", "Ana: 14 9 17", "Ana: 16 9 17",
        ])

    def test_file_review_skips_bad_line(self):
        output, errors, status = run("file_review", "paraderos.txt\n")
        self.assertEqual(status, 0)
        self.assertEqual(output, "Personas: 92\n")
        self.assertIn("Línea 4", errors)

    def test_file_review_missing_file(self):
        _, errors, status = run("file_review", "no_hay.txt\n")
        self.assertEqual(status, 1)
        self.assertIn("no_hay.txt", errors)

    def test_trips_by_hand(self):
        # Callao: 25 + 31 + 20 = 76. Chosica: 18 + 22 = 40. Ate: 9 + 14 = 23.
        output, _, _ = run("trips", "viajes.txt\n")
        self.assertRegex(output, r"Chosica\s+2 viajes\s+40 pasajeros")
        self.assertRegex(output, r"Callao\s+3 viajes\s+76 pasajeros")
        self.assertRegex(output, r"Ate\s+2 viajes\s+23 pasajeros")
        self.assertIn("Más pasajeros: Callao", output)

    def test_trips_tests_pass(self):
        for program in ["test_trips", "test_trips_total"]:
            with self.subTest(program=program):
                self.assertEqual(run(program)[2], 0)

    def test_trips_total_adds_one_line(self):
        output, _, _ = run("trips_total", "viajes.txt\n")
        self.assertIn("Total de pasajeros: 139", output)


class Errors(unittest.TestCase):
    def test_bug_version_differs_in_one_line(self):
        # El comentario de la primera línea también cambia.
        ours = (HERE / "trips.c").read_text(encoding="utf-8").splitlines()[1:]
        bug = (HERE / "errors" / "trips_bug.c").read_text(encoding="utf-8").splitlines()[1:]
        self.assertEqual(len(ours), len(bug))
        self.assertEqual(sum(a != b for a, b in zip(ours, bug)), 1)

    def test_bug_version_fails_its_tests(self):
        self.assertEqual(compile_error_program("test_trips_bug").returncode, 0)
        _, errors, status = run("test_trips_bug_san")
        self.assertNotEqual(status, 0)
        self.assertIn("routes[0].passengers == 23", errors)

    def test_no_terminator_is_caught_by_asan(self):
        built = compile_error_program("no_terminator")
        self.assertEqual(built.returncode, 0)
        self.assertEqual(built.stdout, "", "los apuntes dicen que esta versión de gcc no avisa")
        _, errors, status = run("no_terminator_san")
        self.assertNotEqual(status, 0)
        self.assertIn("stack-buffer-overflow", errors)

    def test_copy_overflow_is_caught_by_asan(self):
        built = compile_error_program("copy_overflow")
        self.assertEqual(built.returncode, 0)
        self.assertEqual(built.stdout, "", "los apuntes dicen que gcc no avisa aquí")
        _, errors, status = run("copy_overflow_san")
        self.assertNotEqual(status, 0)
        self.assertIn("stack-buffer-overflow", errors)

    def test_array_overflow_is_caught(self):
        built = compile_error_program("array_overflow")
        self.assertEqual(built.returncode, 0)
        self.assertEqual(built.stdout, "", "los apuntes dicen que compila sin advertencias")
        _, errors, status = run("array_overflow_san")
        self.assertNotEqual(status, 0)
        self.assertTrue(re.search(r"index 4 out of bounds|stack-buffer-overflow", errors), errors)


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