"""Comprueba los programas de la clase y lo que los apuntes dicen de las herramientas."""
import pathlib
import subprocess
import tempfile
import unittest

HERE = pathlib.Path(__file__).parent
BUILD = HERE / "build"
FLAGS = ["-Wall", "-Wextra", "-std=c17", "-g", "-fsanitize=address,undefined", "-Ilab"]
PROGRAMS = ["logic_parser", "subtract_chain"]


def run(program, *args):
    done = subprocess.run(
        [str(program), *args], cwd=HERE, text=True, encoding="utf-8", errors="replace",
        stdout=subprocess.PIPE, stderr=subprocess.STDOUT, timeout=30,
    )
    return done.stdout, done.returncode


def build(source):
    """Compila y devuelve (ruta del ejecutable, carpeta temporal, mensajes de gcc)."""
    folder = tempfile.TemporaryDirectory()
    binary = pathlib.Path(folder.name) / "program"
    done = subprocess.run(
        ["gcc", *FLAGS, source, "lab/lexer.c", "-o", str(binary)], cwd=HERE, text=True,
        stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
    )
    if done.returncode != 0:
        raise AssertionError(done.stdout)
    return binary, folder, done.stdout


class Programs(unittest.TestCase):
    def test_every_program_compiles_without_warnings(self):
        for name in PROGRAMS:
            with self.subTest(program=name):
                subprocess.run(
                    ["gcc", *FLAGS, "-Werror", "-c", f"{name}.c", "-o", "/dev/null"],
                    cwd=HERE, check=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
                )


class Logic(unittest.TestCase):
    def test_and_binds_tighter_than_or(self):
        out, code = run(BUILD / "logic_parser", "true or false and false")
        self.assertEqual(code, 0, out)
        self.assertIn("(true or (false and false))", out)
        self.assertIn("valor: true", out)

    def test_loop_associates_to_the_left(self):
        out, code = run(BUILD / "logic_parser", "true or false or true")
        self.assertEqual(code, 0, out)
        self.assertIn("((true or false) or true)", out)

    def test_every_error_is_reported_without_leaks(self):
        cases = {
            "true or": "columna 8",
            "true and": "columna 9",
            "not": "columna 4",
            "(true": "columna 6",
            "(true or (false and": "columna 20",
            "true or false and (true or": "columna 27",
            "true maybe": "columna 6",
            "true @": "columna 6",
            "": "columna 1",
        }
        for source, column in cases.items():
            with self.subTest(source=source):
                out, code = run(BUILD / "logic_parser", source)
                self.assertEqual(code, 1, out)
                self.assertIn(column, out)
                self.assertNotIn("Sanitizer", out)

    def test_error_on_a_later_line(self):
        out, code = run(BUILD / "logic_parser", "true or\n  @")
        self.assertEqual(code, 1, out)
        self.assertIn("línea 2, columna 3", out)


class Associativity(unittest.TestCase):
    def test_right_recursion_gets_the_wrong_value(self):
        out, code = run(BUILD / "subtract_chain", "1 - 2 - 3")
        self.assertEqual(code, 0, out)
        self.assertEqual(out.split(), ["recursión", "por", "la", "derecha:", "2", "con", "un", "ciclo:", "-4"])

    def test_they_agree_with_a_single_subtraction(self):
        out, code = run(BUILD / "subtract_chain", "7 - 2")
        self.assertEqual(code, 0, out)
        self.assertEqual(out.split().count("5"), 2)

    def test_bad_input(self):
        out, code = run(BUILD / "subtract_chain", "1 - x")
        self.assertEqual(code, 1, out)
        self.assertNotIn("Sanitizer", out)


class Tools(unittest.TestCase):
    def test_left_recursion_warns_and_overflows(self):
        binary, folder, messages = build("errors/left_recursion.c")
        with folder:
            self.assertIn("infinite recursion", messages)
            out, code = run(binary)
            self.assertNotEqual(code, 0)
            self.assertIn("stack-overflow", out)
            self.assertIn("parse_chain", out)

    def test_forgotten_free_is_a_leak(self):
        binary, folder, messages = build("errors/leak_on_error.c")
        with folder:
            self.assertEqual(messages, "")
            out, code = run(binary)
            self.assertNotEqual(code, 0)
            self.assertIn("detected memory leaks", out)

    def test_too_many_parentheses_overflow_the_stack(self):
        out, code = run(BUILD / "logic_parser", "(" * 100000)
        self.assertNotEqual(code, 0)
        self.assertIn("stack-overflow", out)

    def test_a_thousand_parentheses_are_fine(self):
        out, code = run(BUILD / "logic_parser", "(" * 1000 + "true" + ")" * 1000)
        self.assertEqual(code, 0, out)
        self.assertIn("valor: true", out)


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