"""Comprueba el ejemplo de clase, los módulos ya terminados y lo que los apuntes dicen del intérprete."""
import pathlib
import shutil
import subprocess
import tempfile
import unittest

HERE = pathlib.Path(__file__).parent
BUILD = HERE / "build"
LAB = HERE / "lab"
SOLUTION = HERE / "solution"
BASE = ["-Wall", "-Wextra", "-std=c17"]
FLAGS = [*BASE, "-g", "-fsanitize=address,undefined"]
FINISHED = ["env.c", "expr.c", "lexer.c", "stmt.c"]
REPORTS = ("AddressSanitizer", "LeakSanitizer", "runtime error")


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


def run_c(body, sources=FINISHED):
    """Compila y corre un main con los módulos dados, y devuelve (salida, código)."""
    with tempfile.TemporaryDirectory() as name:
        folder = pathlib.Path(name)
        driver = folder / "driver.c"
        driver.write_text(body, encoding="utf-8")
        binary = folder / "driver"
        files = [str(driver)] + [str(LAB / s) for s in sources]
        done = subprocess.run(["gcc", *FLAGS, "-Werror", f"-I{LAB}", *files, "-o", str(binary)],
                              text=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
        if done.returncode != 0:
            raise AssertionError(f"No compila:\n{done.stdout}")
        return run(binary)


def solution_copy(folder, change=None):
    """Copia lab/ con la solución encima. change recibe el texto de interp.c y lo modifica."""
    for path in list(LAB.glob("*.[ch]")) + list(SOLUTION.glob("*.c")):
        shutil.copy(path, folder)
    if change is not None:
        interp = pathlib.Path(folder) / "interp.c"
        interp.write_text(change(interp.read_text(encoding="utf-8")), encoding="utf-8")


def compile_all(folder, flags, name):
    binary = pathlib.Path(folder) / name
    sources = [str(path) for path in pathlib.Path(folder).glob("*.c")]
    subprocess.run(["gcc", *flags, *sources, "-o", str(binary)], check=True,
                   stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
    return binary


class Frames(unittest.TestCase):
    def test_frames_program(self):
        out, code = run(BUILD / "frames")
        self.assertEqual(code, 0, out)
        self.assertIn("scale      rate  = 2", out)
        self.assertIn("global     n     no existe", out)
        self.assertIn("wrong      n     = 4", out)


class Compiled(unittest.TestCase):
    def test_lab_compiles_without_warnings(self):
        for path in sorted(LAB.glob("*.c")):
            with self.subTest(file=path.name):
                subprocess.run(["gcc", *FLAGS, "-Werror", f"-I{LAB}", "-c", str(path), "-o", "/dev/null"],
                               check=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)

    def test_finished_modules_also_compile_with_o2(self):
        for name in FINISHED + ["mini.c"]:
            with self.subTest(file=name):
                subprocess.run(["gcc", *BASE, "-O2", "-Werror", f"-I{LAB}", "-c", str(LAB / name), "-o", "/dev/null"],
                               check=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)

    def test_env_is_a_copy_of_the_contract(self):
        # Env se usa solo por su encabezado: nadie entra a sus campos.
        for name in ("parser.c", "interp.c", "expr.c", "mini.c"):
            self.assertNotIn("->first", (LAB / name).read_text(encoding="utf-8"), name)


class FinishedExpr(unittest.TestCase):
    def test_print_keeps_the_tree(self):
        body = r"""
        #include <stdio.h>
        #include "expr.h"
        static Expr *n(double v) { return expr_number(v); }
        static Expr *v(const char *s) { return expr_variable(s); }
        static void show(Expr *e) { expr_print(e, stdout); printf("\n"); expr_free(e); }
        int main(void) {
            show(expr_binary(OP_SUB, n(1), expr_binary(OP_SUB, n(2), n(3))));
            show(expr_binary(OP_SUB, expr_binary(OP_SUB, n(1), n(2)), n(3)));
            show(expr_binary(OP_MUL, expr_binary(OP_ADD, v("a"), n(1)), v("b")));
            show(expr_binary(OP_ADD, v("a"), expr_binary(OP_MUL, n(1), v("b"))));
            show(expr_binary(OP_LESS, expr_binary(OP_LESS, n(1), n(2)), n(3)));
            show(expr_binary(OP_ADD, expr_if(v("c"), n(1), n(2)), n(3)));
            show(expr_if(expr_if(v("a"), n(1), n(0)), n(2), expr_if(v("b"), n(3), n(4))));
            show(expr_call("f", NULL, 0));
            return 0;
        }
        """
        out, code = run_c(body)
        self.assertEqual(code, 0, out)
        self.assertEqual(out.splitlines(), [
            "1 - (2 - 3)", "1 - 2 - 3", "(a + 1) * b", "a + 1 * b", "(1 < 2) < 3", "(if c then 1 else 2) + 3",
            "if if a then 1 else 0 then 2 else if b then 3 else 4", "f()",
        ])

    def test_a_failed_eval_does_not_touch_the_result(self):
        body = r"""
        #include <stdio.h>
        #include "expr.h"
        int main(void) {
            Env *env = env_new(NULL);
            if (env == NULL) { return 1; }
            Expr *trees[3] = {
                expr_variable("missing"),
                expr_binary(OP_DIV, expr_number(1), expr_number(0)),
                expr_if(expr_number(1), expr_call("f", NULL, 0), expr_number(2)),
            };
            for (int i = 0; i < 3; i++) {
                double r = 42;
                int ok = expr_eval(trees[i], env, &r);
                printf("%d %g\n", ok, r);
                expr_free(trees[i]);
            }
            env_free(env);
            return 0;
        }
        """
        out, code = run_c(body, ["env.c", "expr.c"])
        self.assertEqual(code, 0, out)
        self.assertEqual(out.splitlines(), ["0 42", "0 42", "0 42"])

    def test_simplify_follows_the_rules_of_week_13(self):
        body = r"""
        #include <stdio.h>
        #include "expr.h"
        int main(void) {
            // (0 - 1) * 0 es 0, no -0: la regla de x * 0 va antes que calcular.
            Expr *e = expr_simplify(expr_binary(OP_MUL, expr_binary(OP_SUB, expr_number(0), expr_number(1)), expr_number(0)));
            expr_print(e, stdout);
            printf("\n%d\n", expr_simplify(NULL) == NULL);
            expr_free(e);
            return 0;
        }
        """
        out, code = run_c(body, ["env.c", "expr.c"])
        self.assertEqual(code, 0, out)
        self.assertEqual(out.splitlines(), ["0", "1"])

    def test_constructors_take_ownership_when_they_fail(self):
        body = r"""
        #include <stdio.h>
        #include <stdlib.h>
        #include "expr.h"
        int main(void) {
            printf("%d", expr_binary(OP_ADD, expr_variable("a"), NULL) == NULL);
            printf("%d", expr_binary(OP_ADD, NULL, expr_variable("a")) == NULL);
            printf("%d", expr_if(expr_variable("a"), NULL, expr_number(1)) == NULL);
            Expr **args = malloc(2 * sizeof *args);
            if (args == NULL) { return 1; }
            args[0] = expr_number(1);
            args[1] = NULL;
            printf("%d\n", expr_call("f", args, 2) == NULL);
            return 0;
        }
        """
        out, code = run_c(body)
        self.assertEqual((out.strip(), code), ("1111", 0), out)

    def test_copy_equal_simplify_derive_eval(self):
        body = r"""
        #include <stdio.h>
        #include <stdlib.h>
        #include "expr.h"
        static Expr *n(double v) { return expr_number(v); }
        static Expr *v(const char *s) { return expr_variable(s); }
        static void show(Expr *e) { expr_print(e, stdout); printf("\n"); expr_free(e); }
        int main(void) {
            Expr **args = malloc(2 * sizeof *args);
            if (args == NULL) { return 1; }
            args[0] = expr_binary(OP_ADD, v("x"), n(0));
            args[1] = expr_if(v("a"), n(1), n(2));
            Expr *call = expr_call("f", args, 2);
            Expr *copy = expr_copy(call);
            printf("%d\n", expr_equal(call, copy));
            expr_free(copy);
            show(expr_simplify(call));
            show(expr_simplify(expr_binary(OP_MUL, expr_binary(OP_ADD, v("x"), n(0)), n(1))));
            show(expr_simplify(expr_binary(OP_MUL, v("x"), n(0))));
            show(expr_simplify(expr_binary(OP_ADD, n(2), expr_binary(OP_MUL, n(3), n(4)))));
            show(expr_simplify(expr_binary(OP_DIV, n(1), n(0))));
            Expr *e = expr_binary(OP_MUL, v("x"), v("x"));
            show(expr_simplify(expr_derive(e, "x")));
            expr_free(e);
            e = expr_binary(OP_LESS, v("x"), n(1));
            printf("%d\n", expr_derive(e, "x") == NULL);
            Env *env = env_new(NULL);
            double r = 0;
            if (env == NULL || !env_set(env, "x", 3)) { return 1; }
            int ok = expr_eval(e, env, &r);
            printf("%d %g\n", ok, r);
            expr_free(e);
            e = expr_if(v("x"), n(7), expr_binary(OP_DIV, n(1), n(0)));
            ok = expr_eval(e, env, &r);
            printf("%d %g\n", ok, r);
            expr_free(e);
            e = expr_call("f", NULL, 0);
            ok = expr_eval(e, env, &r);
            printf("%d\n", ok);
            expr_free(e);
            env_free(env);
            return 0;
        }
        """
        out, code = run_c(body)
        self.assertEqual(code, 0, out)
        self.assertEqual(out.splitlines(), [
            "1", "f(x, if a then 1 else 2)", "x", "0", "14", "1 / 0", "x + x", "1", "1 0", "1 7", "0",
        ])


class FinishedLexer(unittest.TestCase):
    def tokens(self, text):
        literal = text.replace("\\", "\\\\").replace('"', '\\"').replace("\n", "\\n")
        body = r"""
        #include <stdio.h>
        #include "lexer.h"
        int main(void) {
            Lexer *lexer = lexer_new("%s");
            if (lexer == NULL) { return 1; }
            Token t;
            do {
                t = lexer_next(lexer);
                printf("%%d:%%d:%%d:%%s\n", t.kind, t.line, t.column, t.text);
            } while (t.kind != TOKEN_END && t.kind != TOKEN_ERROR);
            lexer_free(lexer);
            return 0;
        }
        """ % literal
        out, code = run_c(body, ["lexer.c"])
        self.assertEqual(code, 0, out)
        return [line.split(":", 3) for line in out.splitlines()]

    def test_positions_and_comments(self):
        got = self.tokens("let x1 = 3.5 # nada\n  print <= != ==")
        self.assertEqual([t[3] for t in got], ["let", "x1", "=", "3.5", "print", "<=", "!=", "==", ""])
        self.assertEqual(got[4][1:3], ["2", "3"])
        self.assertEqual(got[0][0], "2")    # TOKEN_LET
        self.assertEqual(got[1][0], "1")    # TOKEN_NAME

    def test_errors(self):
        self.assertIn("inesperado", self.tokens("1 @")[-1][3])
        self.assertIn("!", self.tokens("!")[-1][3])
        self.assertIn("largo", self.tokens("a" * 64)[-1][3])
        self.assertEqual(self.tokens("a" * 63)[0][3], "a" * 63)
        self.assertIn("largo", self.tokens("1" * 70)[-1][3])

    def test_a_point_needs_digits(self):
        self.assertIn("inesperado", self.tokens("3.")[-1][3])
        self.assertEqual(self.tokens("3.25")[0][3], "3.25")


class FinishedExpressionParser(unittest.TestCase):
    """parse_expression, sin if ni llamadas: esa parte está en lab/parser.c antes de que la toquen."""

    def show(self, text):
        literal = text.replace('"', '\\"')
        body = r"""
        #include <stdio.h>
        #include "parser.h"
        int main(void) {
            ParseError error;
            Expr *e = parse_expression("%s", &error);
            if (e == NULL) {
                printf("%%d:%%d\n", error.line, error.column);
                return 0;
            }
            expr_print(e, stdout);
            printf("\n");
            expr_free(e);
            return 0;
        }
        """ % literal
        out, code = run_c(body, FINISHED + ["parser.c"])
        self.assertEqual(code, 0, out)
        return out.strip()

    def test_precedence_and_association(self):
        self.assertEqual(self.show("1 + 2 * 3"), "1 + 2 * 3")
        self.assertEqual(self.show("(1 + 2) * 3"), "(1 + 2) * 3")
        self.assertEqual(self.show("1 - 2 - 3"), "1 - 2 - 3")
        self.assertEqual(self.show("-x"), "0 - x")
        self.assertEqual(self.show("a + 1 < b * 2"), "a + 1 < b * 2")

    def test_errors_without_leaks(self):
        self.assertEqual(self.show("1 +"), "1:4")
        self.assertEqual(self.show("(1 + 2"), "1:7")
        self.assertEqual(self.show("1 < 2 < 3"), "1:7")
        self.assertEqual(self.show("a * (b + * c)"), "1:10")


@unittest.skipUnless(SOLUTION.exists(), "falta la solución")
class WhatTheNotesSayOfTheInterpreter(unittest.TestCase):
    PROGRAM = "lab/programs/{}.mini"

    def test_runaway_recursion_overflows_the_c_stack(self):
        # En x86-64 emulado, ASan a veces muere sin informe: también vale una señal.
        out, code = run(BUILD / "solution" / "mini", self.PROGRAM.format("runaway"))
        self.assertNotEqual(code, 0)
        if "AddressSanitizer" in out:
            self.assertIn("stack-overflow", out)
            self.assertIn("eval_call", out)
            self.assertNotIn("forever", out)

    def test_my_if_never_reaches_its_base_case(self):
        out, code = run(BUILD / "solution" / "mini", self.PROGRAM.format("my_if"))
        self.assertNotEqual(code, 0)
        self.assertNotIn("6\n", out.split("AddressSanitizer")[0])

    def test_without_sanitizers_it_is_a_signal(self):
        with tempfile.TemporaryDirectory() as folder:
            solution_copy(folder)
            binary = compile_all(folder, [*BASE, "-g", "-O0"], "plain")
            _, code = run(binary, self.PROGRAM.format("runaway"))
        self.assertIn(code, (-11, 139))

    def test_a_forgotten_env_free_leaks_three_blocks_per_frame(self):
        def forget(text):
            old = "    env_free(callee);\n    return ok;"
            self.assertIn(old, text)
            return text.replace(old, "    return ok;")

        with tempfile.TemporaryDirectory() as folder:
            solution_copy(folder, forget)
            binary = compile_all(folder, FLAGS, "leaky")
            out, code = run(binary, self.PROGRAM.format("fact"))
        self.assertNotEqual(code, 0)
        self.assertIn("in 15 allocation(s)", out)

    def test_the_frame_of_the_caller_changes_the_result(self):
        def wrong_parent(text):
            old = "env_new(in->global);\n    if (callee"
            self.assertIn(old, text)
            return text.replace(old, "env_new((Env *) frame);\n    if (callee")

        with tempfile.TemporaryDirectory() as folder:
            solution_copy(folder, wrong_parent)
            binary = compile_all(folder, FLAGS, "dynamic")
            out, _ = run(binary, SOLUTION / "mine" / "frames.mini")
            self.assertEqual(out.strip(), "100")
            out, _ = run(binary, self.PROGRAM.format("fact"))
            self.assertEqual(out.strip(), "120")
            out, _ = run(binary, self.PROGRAM.format("fib"))
            self.assertEqual(out.split(), ["1", "55", "6765"])
        out, _ = run(BUILD / "solution" / "mini", SOLUTION / "mine" / "frames.mini")
        self.assertEqual(out.strip(), "20")

    def test_argument_in_the_new_frame_gives_the_wrong_result(self):
        def args_in_new_frame(text):
            old = "        if (!env_set(callee, function->as.def.params[i], values[i])) {"
            self.assertIn(old, text)
            return text.replace(old, "        if (!eval(in, e->as.call.args[i], callee, &values[i])) { return 0; }\n" + old)

        program = "def sub(a, b) = a - b\nlet a = 10\nlet b = 3\nprint sub(b, a)\n"
        with tempfile.TemporaryDirectory() as folder:
            solution_copy(folder, args_in_new_frame)
            binary = compile_all(folder, FLAGS, "wrong_args")
            path = pathlib.Path(folder) / "p.mini"
            path.write_text(program, encoding="utf-8")
            self.assertEqual(run(binary, path)[0].strip(), "0")
        with tempfile.TemporaryDirectory() as folder:
            path = pathlib.Path(folder) / "p.mini"
            path.write_text(program, encoding="utf-8")
            self.assertEqual(run(BUILD / "solution" / "mini", path)[0].strip(), "-7")

    def test_two_stacks_in_gdb(self):
        if shutil.which("gdb") is None:
            self.skipTest("no hay gdb")
        with tempfile.TemporaryDirectory() as folder:
            solution_copy(folder)
            binary = compile_all(folder, [*BASE, "-g", "-O0"], "with_gdb")
            out, _ = run("gdb", "-q", "-batch", "-ex", "set print frame-arguments none",
                         "-ex", "break eval if e->kind == EXPR_NUMBER && e->as.number == 1", "-ex", "ignore 1 2",
                         "-ex", f"run {self.PROGRAM.format('fact3')}", "-ex", "backtrace", binary)
        if " main " not in out:
            self.skipTest("gdb no puede correr programas aquí")
        frames = [line for line in out.splitlines() if line.startswith("#")]
        self.assertEqual(sum(" eval_call " in line for line in frames), 3)
        self.assertEqual(len(frames), 20)


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