"""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"]
PLAIN = ["function_pointer", "repeated_loops", "prices", "sort_students", "sort_names", "any_sort",
         "search_students", "for_each", "calculator", "permutations"]


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, 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, source, "-o", str(binary)], cwd=HERE, text=True,
        stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
    )
    return binary, folder, done.stdout, done.returncode


class Programs(unittest.TestCase):
    def test_every_program_compiles_without_warnings(self):
        for name in PLAIN:
            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,
                )

    def test_every_program_ends_without_reports(self):
        for name in PLAIN:
            args = ["suma"] if name == "calculator" else []
            with self.subTest(program=name):
                out, code = run(BUILD / name, *args)
                self.assertEqual(code, 0, out)
                self.assertNotIn("runtime error", out)

    def test_loops_and_callback_agree(self):
        repeated = run(BUILD / "repeated_loops")[0].splitlines()
        with_callback = run(BUILD / "prices")[0].splitlines()
        self.assertEqual(repeated, with_callback[2:])

    def test_our_search_agrees_with_bsearch(self):
        lines = run(BUILD / "search_students")[0].splitlines()
        self.assertEqual(lines[0], lines[1])
        self.assertEqual(lines[2], lines[3])

    def test_table_rejects_unknown_operation(self):
        out, code = run(BUILD / "calculator", "potencia")
        self.assertEqual(code, 1)
        self.assertIn("no conozco", out)


class CompilerClaims(unittest.TestCase):
    def messages(self, source, flags=BASE):
        _, folder, text, code = build(source, flags)
        folder.cleanup()
        return text, code

    def test_these_do_not_compile(self):
        for source, phrase in (("errors/missing_parens.c", "initialized like a variable"),
                               ("errors/wrong_signature.c", "incompatible pointer type"),
                               ("errors/typed_compare.c", "incompatible pointer type")):
            with self.subTest(source=source):
                text, code = self.messages(source)
                self.assertNotEqual(code, 0)
                self.assertIn(phrase, text)

    def test_these_errors_give_no_warning(self):
        for source in ("errors/void_arithmetic.c", "errors/subtract_compare.c", "errors/subtract_sort.c",
                       "errors/double_compare.c", "errors/wrong_level.c", "errors/wrong_context.c",
                       "errors/wrong_context_silent.c", "errors/nested_function.c", "errors/table_gap.c",
                       "errors/typed_compare_cast.c"):
            with self.subTest(source=source):
                self.assertEqual(self.messages(source)[0], "")

    def test_extra_flags_show_the_extensions(self):
        self.assertIn("-Wpointer-arith", self.messages("errors/void_arithmetic.c", [*BASE, "-Wpointer-arith"])[0])
        self.assertIn("ISO C forbids nested functions", self.messages("errors/nested_function.c", [*BASE, "-pedantic"])[0])


class RunClaims(unittest.TestCase):
    def output(self, source):
        binary, folder, _, _ = build(source, FLAGS)
        out, code = run(binary)
        folder.cleanup()
        return out, code

    def test_subtracting_gets_the_sign_wrong(self):
        out, _ = self.output("errors/subtract_compare.c")
        self.assertIn("signed integer overflow", out)
        self.assertIn("sin resta: -1", out)

    def test_sort_by_subtracting_overflows(self):
        self.assertIn("signed integer overflow", self.output("errors/subtract_sort.c")[0])

    def test_double_compare_does_not_sort(self):
        out, code = self.output("errors/double_compare.c")
        self.assertEqual(code, 0, out)
        values = [float(x) for x in out.split()]
        self.assertNotEqual(values, sorted(values))
        self.assertNotIn("runtime error", out)

    def test_wrong_level_is_silent_and_wrong(self):
        out, code = self.output("errors/wrong_level.c")
        self.assertEqual(code, 0, out)
        self.assertNotIn("runtime error", out)

    def test_wrong_context_is_caught_when_it_goes_outside(self):
        out, code = self.output("errors/wrong_context.c")
        self.assertNotEqual(code, 0)
        self.assertIn("stack-buffer-overflow", out)
        self.assertIn("add_if_above", out)

    def test_wrong_context_can_be_silent(self):
        out, code = self.output("errors/wrong_context_silent.c")
        self.assertEqual(code, 0, out)
        self.assertNotIn("runtime error", out)
        self.assertNotIn("AddressSanitizer", out)
        self.assertIn("fin", out)

    def test_table_gap_jumps_to_address_zero(self):
        out, code = self.output("errors/table_gap.c")
        self.assertNotEqual(code, 0)
        self.assertIn("SEGV", out)

    def test_gdb_knows_the_type_of_a_function(self):
        with tempfile.TemporaryDirectory() as name:
            binary = pathlib.Path(name) / "program"
            subprocess.run(["gcc", *BASE, "-g", "-O0", "function_pointer.c", "-o", str(binary)], cwd=HERE, check=True)
            done = subprocess.run(
                ["gdb", "-q", "-batch", "-ex", "print add", "-ex", "whatis add", str(binary)],
                text=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
            )
        self.assertIn("{int (int, int)}", done.stdout)
        self.assertIn("<add>", done.stdout)


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