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

Cada grupo compila un programa propio con los archivos de esa carpeta. Corren con ASan y UBSan:
una fuga, o un aviso como un overflow, también hacen fallar la prueba.
"""
import functools
import os
import pathlib
import re
import subprocess
import tempfile
import unittest

HERE = pathlib.Path(__file__).parent
LAB_DIR = os.environ.get("LAB_DIR", "solution")
SOURCE = HERE / LAB_DIR
LAB = HERE / "lab"
FLAGS = ["-Wall", "-Wextra", "-std=c17", "-g", "-fsanitize=address,undefined"]
KEEP = []


def source(name):
    """El archivo de la carpeta que se prueba. Si no está ahí, es uno de los ya hechos de lab/."""
    path = SOURCE / name
    return path if path.exists() else LAB / name


def build(main_source, sources, extra=()):
    """Compila en una carpeta temporal y devuelve la ruta del ejecutable."""
    folder = tempfile.TemporaryDirectory()
    KEEP.append(folder)
    path = pathlib.Path(folder.name)
    inputs = list(map(str, sources))
    if main_source is not None:
        driver = path / "driver.c"
        driver.write_text(main_source, encoding="utf-8")
        inputs.insert(0, str(driver))
    binary = path / "driver"
    built = subprocess.run(
        ["gcc", *FLAGS, f"-I{LAB}", *inputs, *extra, "-o", str(binary)],
        text=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
    )
    if built.returncode != 0:
        raise AssertionError(f"No compila:\n{built.stdout}")
    return binary


def run(binary, *args):
    """Devuelve (salida, código de salida)."""
    try:
        done = subprocess.run(
            [str(binary), *args], text=True, encoding="utf-8", errors="replace",
            stdout=subprocess.PIPE, stderr=subprocess.STDOUT, timeout=20,
        )
    except subprocess.TimeoutExpired:
        raise AssertionError(f"El programa no terminó en 20 segundos, con la entrada {args!r}.") from None
    return done.stdout, done.returncode


def writable_globals(path):
    """Los símbolos de datos que se pueden escribir: las variables globales del archivo."""
    with tempfile.TemporaryDirectory() as name:
        obj = pathlib.Path(name) / "file.o"
        subprocess.run(["gcc", "-std=c17", "-fno-pic", f"-I{LAB}", "-c", str(path), "-o", str(obj)], check=True)
        done = subprocess.run(["nm", str(obj)], text=True, stdout=subprocess.PIPE, check=True)
    return [line.split()[-1] for line in done.stdout.splitlines() if re.search(r" [bBdD] ", line)]


@functools.lru_cache(maxsize=None)
def tokens_program():
    return build(None, [LAB / "show_tokens.c", source("lexer.c")])


# Analiza argv[1]. Escribe "árbol | valor", o "ERROR línea columna | mensaje".
PARSE_MAIN = r"""
#include <stdio.h>
#include "env.h"
#include "expr.h"
#include "parser.h"
int main(int argc, char *argv[]) {
    if (argc != 2) { return 2; }
    ParseError error;
    Expr *tree = parse_expression(argv[1], &error);
    if (tree == NULL) {
        printf("ERROR %d %d | %s\n", error.line, error.column, error.message);
        return 0;
    }
    expr_print(tree, stdout);
    Env *env = env_new(NULL);
    double value = 0.0;
    if (env == NULL || !env_set(env, "x", 2.0) || !env_set(env, "y", 3.0) || !env_set(env, "x1_y", 10.0)
        || !env_set(env, "_z", 20.0)) { return 2; }
    if (expr_eval(tree, env, &value)) { printf(" | %g\n", value); } else { printf(" | no evalúa\n"); }
    env_free(env);
    expr_free(tree);
    return 0;
}
"""


@functools.lru_cache(maxsize=None)
def parse_program():
    return build(PARSE_MAIN, [source("parser.c"), source("lexer.c"), LAB / "expr.c", LAB / "env.c"])


def parsed(text):
    out, code = run(parse_program(), text)
    if code != 0:
        raise AssertionError(f"Con la entrada {text!r} el programa terminó con error:\n{out}")
    return out.strip()


class LabCase(unittest.TestCase):
    longMessage = False


class Lexer(LabCase):
    """Parte 1: comparadores y comentarios."""

    def tokens(self, text):
        out, code = run(tokens_program(), text)
        if code != 0:
            self.fail(f"Con la entrada {text!r} el programa terminó con error:\n{out}")
        found = []
        for line in out.splitlines():
            parts = line.split(None, 2)
            position, kind = parts[0], parts[1]
            found.append((kind, parts[2] if len(parts) > 2 else "", position))
        return found

    def kinds(self, text):
        return [kind for kind, _, _ in self.tokens(text)]

    def check_kinds(self, text, wanted):
        got = self.kinds(text)
        wanted = wanted.split()
        if got != wanted:
            self.fail(f"Tokens de {text!r}\n  se esperaba: {wanted}\n  se obtuvo:   {got}")

    def test_each_comparator(self):
        cases = {"<": "LESS", "<=": "LESS_EQUAL", ">": "GREATER", ">=": "GREATER_EQUAL",
                 "==": "EQUAL", "!=": "NOT_EQUAL", "=": "ASSIGN"}
        for text, kind in cases.items():
            with self.subTest(text=text):
                self.check_kinds(text, f"{kind} END")

    def test_comparator_text(self):
        for text in ("<", "<=", ">", ">=", "==", "!="):
            with self.subTest(text=text):
                self.assertEqual(self.tokens(text)[0][1], text, f"El text del token {text} debe ser {text}.")

    def test_comparators_without_spaces(self):
        self.check_kinds("a<=b", "NAME LESS_EQUAL NAME END")
        self.check_kinds("a!=1", "NAME NOT_EQUAL NUMBER END")
        self.check_kinds("1>2", "NUMBER GREATER NUMBER END")

    def test_longest_token_first(self):
        self.check_kinds("===", "EQUAL ASSIGN END")
        self.check_kinds("<<=", "LESS LESS_EQUAL END")
        self.check_kinds(">==", "GREATER_EQUAL ASSIGN END")

    def test_two_characters_must_be_together(self):
        self.check_kinds("< =", "LESS ASSIGN END")
        self.check_kinds("= =", "ASSIGN ASSIGN END")

    def test_lone_exclamation_is_an_error(self):
        self.check_kinds("! =", "ERROR ASSIGN END")
        self.check_kinds("1 ! 2", "NUMBER ERROR NUMBER END")
        kind, text, position = self.tokens("  !")[0]
        self.assertEqual((kind, position), ("ERROR", "1:3"))
        self.assertIn("!", text, "El mensaje de error debe decir qué carácter encontró.")

    def test_column_after_a_comparator(self):
        self.assertEqual(self.tokens("x <= y")[2][2], "1:6", "Un comparador de dos caracteres debe avanzar dos columnas.")
        self.assertEqual(self.tokens("x != y")[2][2], "1:6", "Un comparador de dos caracteres debe avanzar dos columnas.")

    def test_comments(self):
        self.check_kinds("# solo esto", "END")
        self.check_kinds("1 # uno", "NUMBER END")
        self.check_kinds("1 # < 2 @ !\n2", "NUMBER NUMBER END")
        self.check_kinds("#\n#\n3", "NUMBER END")
        self.check_kinds("x#y\nz", "NAME NAME END")
        self.check_kinds("# año\n1", "NUMBER END")

    def test_a_comment_keeps_the_line_count(self):
        self.assertEqual(self.tokens("1 # uno\n2")[1][2], "2:1", "Después de un comentario, la línea debe seguir contando.")
        self.assertEqual(self.tokens("# a\n# b\n  x")[0][2], "3:3", "Después de un comentario, la línea debe seguir contando.")

    def test_end_after_a_comment_without_newline(self):
        self.assertEqual(self.tokens("# abc")[0][2], "1:6", "TOKEN_END va donde termina el texto.")

    def test_no_globals(self):
        names = writable_globals(source("lexer.c"))
        if names:
            self.fail(f"lexer.c no debe tener variables globales. Encontré: {names}")

    def test_their_tests_pass(self):
        path = source("lexer_tests.c")
        text = re.sub(r"//.*", "", path.read_text(encoding="utf-8"))
        for piece, what in (("TOKEN_LESS_EQUAL", "los comparadores"), ("#", "los comentarios")):
            body = re.sub(r"^\s*#include.*$", "", text, flags=re.M)
            if piece not in body:
                self.fail(f"lexer_tests.c todavía no prueba {what}.")
        out, code = run(build(None, [path, source("lexer.c")]))
        self.assertEqual(code, 0, out)


class Trees(LabCase):
    """Parte 2: lo que se escribe es el árbol, con solo los paréntesis necesarios, y su valor."""

    def check(self, text, wanted, why=""):
        got = parsed(text)
        if got != wanted:
            self.fail(f"Con la entrada {text!r}\n  se esperaba: {wanted}\n  se obtuvo:   {got}\n{why}".rstrip())

    def test_primary(self):
        self.check("42", "42 | 42")
        self.check("3.25", "3.25 | 3.25")
        self.check("x", "x | 2")
        self.check("x1_y + _z", "x1_y + _z | 30")
        self.check("desconocida", "desconocida | no evalúa")

    def test_parentheses_disappear_from_the_tree(self):
        self.check("(7)", "7 | 7")
        self.check("((x))", "x | 2")
        self.check("1 + (2 * 3)", "1 + 2 * 3 | 7")

    def test_precedence(self):
        self.check("1 + 2 * 3", "1 + 2 * 3 | 7", "* debe ir más abajo en el árbol que +.")
        self.check("2 * 3 + 1", "2 * 3 + 1 | 7")
        self.check("(1 + 2) * 3", "(1 + 2) * 3 | 9")
        self.check("1 + 6 / 2 - 1", "1 + 6 / 2 - 1 | 3")

    def test_left_associativity(self):
        why = "Los operadores asocian por la izquierda: se necesita un ciclo, no recursión por la derecha."
        self.check("1 - 2 - 3", "1 - 2 - 3 | -4", why)
        self.check("8 / 4 / 2", "8 / 4 / 2 | 1", why)
        self.check("1 + 2 - 3 + 4", "1 + 2 - 3 + 4 | 4", why)
        self.check("1 - (2 - 3)", "1 - (2 - 3) | 2")

    def test_unary_minus(self):
        why = "-x se construye como 0 - x."
        self.check("-x", "0 - x | -2", why)
        self.check("--x", "0 - (0 - x) | 2", why)
        self.check("-2 * 3", "(0 - 2) * 3 | -6", why)
        self.check("2 * -3", "2 * (0 - 3) | -6", why)
        self.check("-(1 + 2)", "0 - (1 + 2) | -3", why)
        self.check("1 - -1", "1 - (0 - 1) | 2", why)

    def test_comparisons(self):
        self.check("1 < 2", "1 < 2 | 1")
        self.check("2 <= 1", "2 <= 1 | 0")
        self.check("2 > 1", "2 > 1 | 1")
        self.check("1 >= 2", "1 >= 2 | 0")
        self.check("x == 2", "x == 2 | 1")
        self.check("x != 2", "x != 2 | 0")

    def test_comparisons_are_below_sums(self):
        self.check("1 + 2 < 2 * 2", "1 + 2 < 2 * 2 | 1")
        self.check("(1 < 2) + 1", "(1 < 2) + 1 | 2")
        self.check("(1 < 2) == 1", "(1 < 2) == 1 | 1")
        self.check("1 == (2 > 1)", "1 == (2 > 1) | 1")

    def test_spread_over_lines_with_comments(self):
        self.check("1 + # uno\n  2 * # dos\n  3", "1 + 2 * 3 | 7")

    def test_long_expressions(self):
        self.check("+".join(["1"] * 500), " + ".join(["1"] * 500) + " | 500")
        self.check("(" * 200 + "x" + ")" * 200, "x | 2")

    def test_exact_tree(self):
        body = r"""
        #include <stdio.h>
        #include "parser.h"
        int main(void) {
            ParseError error;
            Expr *a = parse_expression("1 + x * -y", &error);
            Expr *b = expr_binary(OP_ADD, expr_number(1), expr_binary(OP_MUL, expr_variable("x"),
                                  expr_binary(OP_SUB, expr_number(0), expr_variable("y"))));
            if (a == NULL || b == NULL) { return 1; }
            printf("%d\n", expr_equal(a, b));
            expr_free(a);
            expr_free(b);
            return 0;
        }
        """
        program = build(body, [source("parser.c"), source("lexer.c"), LAB / "expr.c", LAB / "env.c"])
        out, code = run(program)
        self.assertEqual(code, 0, out)
        self.assertEqual(out.strip(), "1", "El árbol de 1 + x * -y no tiene la forma esperada.")

    def test_no_globals(self):
        names = writable_globals(source("parser.c"))
        if names:
            self.fail(f"parser.c no debe tener variables globales. Encontré: {names}")


class Errors(LabCase):
    """Parte 3: posición, mensaje y nada sin liberar. Las fugas las detecta ASan."""

    def check(self, text, line, column, contains=None):
        got = parsed(text)
        match = re.fullmatch(r"ERROR (-?\d+) (-?\d+) \| (.*)", got, re.S)
        if match is None:
            self.fail(f"La entrada {text!r} debe dar un error, y se obtuvo: {got}")
        where = (int(match.group(1)), int(match.group(2)))
        if where != (line, column):
            self.fail(f"Con la entrada {text!r}, el error debe estar en la línea {line}, columna {column}.\n"
                      f"  se obtuvo: línea {where[0]}, columna {where[1]}: {match.group(3)}")
        if not match.group(3).strip():
            self.fail(f"Con la entrada {text!r}, el mensaje de error está vacío.")
        if contains is not None and contains not in match.group(3):
            self.fail(f"Con la entrada {text!r}, el mensaje debe mencionar {contains}. Se obtuvo: {match.group(3)}")

    def test_missing_operand(self):
        self.check("1 +", 1, 4)
        self.check("1 + * 2", 1, 5)
        self.check("* 2", 1, 1)
        self.check("1 <", 1, 4)
        self.check("-", 1, 2)

    def test_empty(self):
        self.check("", 1, 1)
        self.check("   ", 1, 4)
        self.check("# nada\n", 2, 1)

    def test_parentheses(self):
        self.check("(1 + 2", 1, 7, ")")
        self.check("(", 1, 2)
        self.check("()", 1, 2)
        self.check("((1)", 1, 5, ")")

    def test_leftover_text(self):
        self.check("1 2", 1, 3)
        self.check("1)", 1, 2)
        self.check("x y", 1, 3)
        self.check("x = 1", 1, 3)

    def test_comparisons_do_not_chain(self):
        self.check("1 < 2 < 3", 1, 7)
        self.check("1 == 2 != 3", 1, 8)

    def test_not_this_week(self):
        self.check("if x", 1, 1)
        self.check("let", 1, 1)
        self.check("f(1)", 1, 2)

    def test_position_on_later_lines(self):
        self.check("x\n+\n)", 3, 1)
        self.check("1 +\n", 2, 1)
        self.check("1 + # uno\n# dos\n  *", 3, 3)
        self.check("\t1 +\t)", 1, 6)

    def test_lexer_errors_keep_their_message(self):
        self.check("1 + @", 1, 5, "@")
        self.check("@", 1, 1, "@")
        self.check("(1 $", 1, 4, "$")
        self.check("1 ! 2", 1, 3, "!")
        self.check("1.5.2", 1, 4, ".")

    def test_nothing_leaks_when_it_fails_late(self):
        cases = ("1 + 2 * (3 +", "(1 + 2", "x + y * z +", "x * y *", "-(1 + 2", "- - x +", "1 + 2 <", "1 + 2 < 3 +",
                 "x + @", "(x + y) * (y + x", "((1 + 2) * 3", "x < (y", "1 2", "x + y z")
        for text in cases:
            with self.subTest(text=text):
                self.assertTrue(parsed(text).startswith("ERROR"), f"La entrada {text!r} debe dar un error.")

    def test_first_error_is_the_one_reported(self):
        self.check("(1 + ) )", 1, 6)
        self.check("* @", 1, 1)

    def test_without_memory(self):
        body = r"""
        #include <stdio.h>
        #include <stdlib.h>
        #include "parser.h"
        void *__real_malloc(size_t size);
        static long remaining = -1;
        static long calls = 0;
        void *__wrap_malloc(size_t size) {
            calls++;
            if (remaining == 0) { remaining = -1; return NULL; }
            if (remaining > 0) { remaining--; }
            return __real_malloc(size);
        }
        int main(void) {
            const char *text = "1 + x * (2 - y) < -3";
            ParseError error;
            Expr *tree = parse_expression(text, &error);
            if (tree == NULL) { puts("no analiza"); return 0; }
            expr_free(tree);
            long total = calls;
            for (long i = 0; i < total; i++) {
                remaining = i;
                tree = parse_expression(text, &error);
                remaining = -1;
                if (tree != NULL) { printf("devolvió un árbol sin memoria, en el malloc %ld\n", i); expr_free(tree); return 0; }
                if (error.message[0] == '\0') { printf("sin mensaje, en el malloc %ld\n", i); return 0; }
            }
            printf("bien %s\n", total >= 11 ? "" : "pocas llamadas");
            return 0;
        }
        """
        program = build(body, [source("parser.c"), source("lexer.c"), LAB / "expr.c", LAB / "env.c"],
                        ["-Wl,--wrap=malloc"])
        out, code = run(program)
        if code != 0 or out.strip() != "bien":
            self.fail("Cuando malloc devuelve NULL, parse_expression debe devolver NULL, con un mensaje y sin fugas.\n"
                      "Revisen lo que devuelven expr_number, expr_variable y expr_binary.\n" + out[:800])


# Lo que se les da hecho.
EXPR_MAIN = r"""
#include <stdio.h>
#include "expr.h"
static void show(Expr *e) { expr_print(e, stdout); putchar('\n'); }
static Expr *num(double v) { return expr_number(v); }
static Expr *var(const char *n) { return expr_variable(n); }
static Expr *bin(BinaryOp op, Expr *a, Expr *b) { return expr_binary(op, a, b); }
int main(void) {
    Env *env = env_new(NULL);
    if (env == NULL || !env_set(env, "x", 3.0)) { return 1; }
    double value = -1.0;

    Expr *e = bin(OP_MUL, bin(OP_ADD, var("x"), num(1)), num(2));
    show(e);
    int ok = expr_eval(e, env, &value);
    printf("%d %g\n", ok, value);
    Expr *copy = expr_copy(e);
    printf("%d\n", expr_equal(e, copy));
    expr_free(copy);
    expr_free(e);

    value = -1.0;
    e = bin(OP_DIV, num(1), num(0));
    ok = expr_eval(e, env, &value);
    printf("%d %g\n", ok, value);
    expr_free(e);
    e = var("z");
    ok = expr_eval(e, env, &value);
    printf("%d %g\n", ok, value);
    expr_free(e);
    e = bin(OP_ADD, num(1), bin(OP_ADD, num(2), num(3)));
    show(e);
    expr_free(e);

    e = bin(OP_SUB, var("x"), bin(OP_SUB, var("x"), num(1)));
    show(e);
    expr_free(e);
    e = bin(OP_LESS, bin(OP_LESS, num(1), num(2)), bin(OP_EQUAL, num(1), num(1)));
    show(e);
    ok = expr_eval(e, env, &value);
    printf("%d %g\n", ok, value);
    expr_free(e);

    e = expr_simplify(bin(OP_ADD, bin(OP_MUL, var("x"), num(1)), bin(OP_MUL, var("x"), num(0))));
    show(e);
    expr_free(e);
    e = expr_simplify(bin(OP_LESS, bin(OP_ADD, num(1), num(2)), num(4)));
    show(e);
    expr_free(e);
    e = expr_simplify(bin(OP_DIV, num(1), num(0)));
    show(e);
    expr_free(e);

    Expr *f = bin(OP_DIV, bin(OP_MUL, var("x"), var("x")), bin(OP_ADD, var("x"), num(1)));
    Expr *d = expr_derive(f, "x");
    ok = expr_eval(d, env, &value);
    printf("%d %g\n", ok, value);
    expr_free(d);
    expr_free(f);
    f = bin(OP_LESS, var("x"), num(1));
    printf("%d\n", expr_derive(f, "x") == NULL);
    expr_free(f);

    printf("%d\n", bin(OP_ADD, num(1), NULL) == NULL);
    expr_free(NULL);
    env_free(env);
    return 0;
}
"""


class GivenFiles(LabCase):
    """Lo que ya viene hecho en lab/. Estas pruebas pasan desde el inicio."""

    def test_expr_module(self):
        out, code = run(build(EXPR_MAIN, [LAB / "expr.c", LAB / "env.c"]))
        self.assertEqual(code, 0, out)
        wanted = ["(x + 1) * 2", "1 8", "1", "0 -1", "0 -1", "1 + (2 + 3)", "x - (x - 1)", "(1 < 2) < (1 == 1)", "1 0", "x", "1", "1 / 0",
                  "1 0.9375", "1", "1"]
        self.assertEqual(out.split("\n")[:-1], wanted)

    def lexer_output(self, text):
        out, code = run(build(None, [LAB / "show_tokens.c", LAB / "lexer.c"]), text)
        self.assertEqual(code, 0, out)
        return [line.split() for line in out.splitlines()]

    def test_review_question(self):
        kinds = [line[1] for line in self.lexer_output("let x1 = 3*(y+2)")]
        self.assertEqual(kinds, "LET NAME ASSIGN NUMBER STAR LPAREN NAME PLUS NUMBER RPAREN END".split())

    def test_number_forms(self):
        kinds = [line[1:] for line in self.lexer_output("1e5 0x10 3. .5")]
        self.assertEqual(kinds[:4], [["NUMBER", "1"], ["NAME", "e5"], ["NUMBER", "0"], ["NAME", "x10"]])
        self.assertEqual(kinds[4], ["NUMBER", "3"])
        self.assertEqual(kinds[5][0], "ERROR")
        self.assertEqual(kinds[6][0], "ERROR")
        self.assertEqual(kinds[7], ["NUMBER", "5"])

    def test_token_length(self):
        fits = "a" * 63
        self.assertEqual(self.lexer_output(fits)[0][1:], ["NAME", fits])
        self.assertEqual(self.lexer_output("a" * 64)[0][1], "ERROR")
        self.assertEqual(self.lexer_output("1" * 64)[0][1], "ERROR")

    def test_not_ascii(self):
        lines = self.lexer_output("ñ")
        self.assertEqual(lines[0][1], "ERROR")

    def test_end_repeats(self):
        body = r"""
        #include <stdio.h>
        #include "lexer.h"
        int main(void) {
            Lexer *lexer = lexer_new("");
            if (lexer == NULL) { return 1; }
            int first = lexer_next(lexer).kind == TOKEN_END;
            int second = lexer_next(lexer).kind == TOKEN_END;
            int third = lexer_next(lexer).kind == TOKEN_END;
            printf("%d %d %d\n", first, second, third);
            lexer_free(lexer);
            return 0;
        }
        """
        out, code = run(build(body, [LAB / "lexer.c"]))
        self.assertEqual((out.strip(), code), ("1 1 1", 0), out)

    def test_given_lexer_tests_compile(self):
        build(None, [LAB / "lexer_tests.c", LAB / "lexer.c"], ["-Werror"])


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