"""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"
BASE = ["-Wall", "-Wextra", "-std=c17"]
FLAGS = [*BASE, "-g", "-fsanitize=address,undefined"]
PROGRAMS = ["logic_demo", "logic_free_order", "logic_simplify_demo", "node_sizes", "env_demo"]
SOURCES = ["logic.c", "logic_demo.c", "logic_free_order.c", "logic_simplify_demo.c", "node_sizes.c", "env_demo.c"]


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


def build(sources, flags=FLAGS):
    """Compila y devuelve (ruta del ejecutable, carpeta temporal, mensajes de gcc, código)."""
    folder = tempfile.TemporaryDirectory()
    binary = pathlib.Path(folder.name) / "program"
    done = subprocess.run(
        ["gcc", *flags, "-I.", "-Ilab", *sources, "-o", str(binary)], cwd=HERE, text=True,
        stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
    )
    return binary, folder, done.stdout, done.returncode


def report(source):
    binary, folder, _, code = build([source, "logic.c"])
    assert code == 0
    out, code = run(binary)
    folder.cleanup()
    return out, code


class Programs(unittest.TestCase):
    def test_every_source_compiles_without_warnings(self):
        for name in SOURCES:
            with self.subTest(source=name):
                subprocess.run(
                    ["gcc", *FLAGS, "-Werror", "-I.", "-Ilab", "-c", name, "-o", "/dev/null"],
                    cwd=HERE, check=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
                )

    def test_every_program_ends_without_reports(self):
        for name in PROGRAMS:
            with self.subTest(program=name):
                out, code = run(BUILD / name)
                self.assertEqual(code, 0, out)
                self.assertNotIn("runtime error", out)
                self.assertNotIn("AddressSanitizer", out)

    def test_the_tree_of_the_notes(self):
        lines = run(BUILD / "logic_demo")[0].splitlines()
        self.assertEqual(lines[0], "or(and(a, not(b)), c)")
        self.assertEqual(lines[1], "nodos: 6")
        self.assertEqual(lines[2], "profundidad: 4")
        # La tabla de verdad coincide con (a and not b) or c, escrita a mano.
        for line in lines[4:]:
            a, b, c, _, result = line.split()
            wanted = (a == "1" and b == "0") or c == "1"
            self.assertEqual(result == "1", wanted, line)

    def test_children_are_freed_before_their_parent(self):
        lines = run(BUILD / "logic_free_order")[0].splitlines()
        self.assertLess(lines.index("libero b"), lines.index("libero un not"))
        self.assertLess(lines.index("libero un not"), lines.index("libero un and"))
        self.assertEqual(lines[-1], "libero un or")
        self.assertEqual(len(lines), 6)

    def test_simplifying_does_not_change_the_kept_formula(self):
        lines = run(BUILD / "logic_simplify_demo")[0].splitlines()
        self.assertEqual(lines[-1], "and(a, or(b, c))  ->  and(a, or(b, c))  (nodos: de 5 a 5)")

    def test_env_is_used_only_through_its_header(self):
        self.assertNotIn("struct binding", (HERE / "env_demo.c").read_text(encoding="utf-8"))
        self.assertNotIn("env.c", (HERE / "env_demo.c").read_text(encoding="utf-8"))


class CompilerClaims(unittest.TestCase):
    def test_a_node_cannot_contain_itself(self):
        _, folder, text, code = build(["errors/self_by_value.c"], BASE)
        folder.cleanup()
        self.assertNotEqual(code, 0)
        self.assertIn("has incomplete type", text)

    def test_a_missing_case_gives_a_warning(self):
        _, folder, text, code = build(["errors/missing_case.c", "logic.c"], BASE)
        folder.cleanup()
        self.assertEqual(code, 0)
        self.assertIn("LOGIC_OR", text)
        self.assertIn("-Wswitch", text)

    def test_a_switch_needs_the_final_return(self):
        with tempfile.TemporaryDirectory() as name:
            path = pathlib.Path(name) / "no_return.c"
            text = (HERE / "logic.c").read_text(encoding="utf-8")
            cut = text.index("int logic_count(")
            end = text.index("int logic_depth(")
            path.write_text('#include "logic.h"\n' + text[cut:end].replace("    return 0;\n", ""), encoding="utf-8")
            done = subprocess.run(["gcc", *BASE, "-I.", "-c", str(path), "-o", "/dev/null"], cwd=HERE, text=True,
                                  stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
        self.assertIn("-Wreturn-type", done.stdout)

    def test_strdup_is_not_declared_in_c17(self):
        with tempfile.TemporaryDirectory() as name:
            path = pathlib.Path(name) / "dup.c"
            path.write_text('#include <string.h>\nchar *copy(const char *s) { return strdup(s); }\n', encoding="utf-8")
            done = subprocess.run(["gcc", *BASE, "-c", str(path), "-o", "/dev/null"], text=True,
                                  stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
        self.assertIn("strdup", done.stdout)

    def test_negative_zero_is_written_with_its_sign(self):
        with tempfile.TemporaryDirectory() as name:
            path = pathlib.Path(name) / "zero.c"
            binary = pathlib.Path(name) / "zero"
            path.write_text('#include <stdio.h>\nint main(void) { volatile double a = -3.0; '
                            'printf("%g\\n", a * 0.0); return 0; }\n', encoding="utf-8")
            subprocess.run(["gcc", *BASE, str(path), "-o", str(binary)], check=True)
            self.assertEqual(run(binary)[0].strip(), "-0")

    def test_gcc_warns_about_freeing_the_parent_first(self):
        _, folder, text, code = build(["errors/free_parent_first.c", "logic.c"])
        folder.cleanup()
        self.assertEqual(code, 0)
        self.assertIn("-Wuse-after-free", text)

    def test_the_other_errors_give_no_warning(self):
        for source in ("errors/free_only_root.c", "errors/shared_child.c", "errors/simplify_leak.c",
                       "errors/simplify_dangling.c"):
            with self.subTest(source=source):
                _, folder, text, code = build([source, "logic.c"])
                folder.cleanup()
                self.assertEqual(code, 0)
                self.assertEqual(text, "")


class RunClaims(unittest.TestCase):
    def test_freeing_the_parent_first_is_use_after_free(self):
        out, code = report("errors/free_parent_first.c")
        self.assertNotEqual(code, 0)
        self.assertIn("heap-use-after-free", out)

    def test_freeing_only_the_root_leaks(self):
        out, code = report("errors/free_only_root.c")
        self.assertNotEqual(code, 0)
        self.assertIn("detected memory leaks", out)

    def test_a_shared_child_is_freed_twice(self):
        out, code = report("errors/shared_child.c")
        self.assertNotEqual(code, 0)
        self.assertIn("heap-use-after-free", out)

    def test_dropping_a_subtree_leaks(self):
        out, code = report("errors/simplify_leak.c")
        self.assertNotEqual(code, 0)
        self.assertIn("in 3 allocation(s)", out)

    def test_freeing_the_kept_child_is_use_after_free(self):
        out, code = report("errors/simplify_dangling.c")
        self.assertNotEqual(code, 0)
        self.assertIn("heap-use-after-free", out)


class LabIsReady(unittest.TestCase):
    def test_env_files_are_there(self):
        for name in ("env.h", "env.c", "expr.h", "expr.c", "expr_tests.c", "algebra.c", "README.md"):
            self.assertTrue((HERE / "lab" / name).exists(), name)

    def test_lab_compiles_without_warnings(self):
        for name in ("env.c", "expr.c", "expr_tests.c", "algebra.c"):
            with self.subTest(source=name):
                subprocess.run(
                    ["gcc", *FLAGS, "-Werror", "-Ilab", "-c", f"lab/{name}", "-o", "/dev/null"],
                    cwd=HERE, check=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
                )


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