"""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 = ["typedef_units", "trip_defines", "trip_enum", "enum_values", "union_bytes", "payment_sizes",
         "sign_rule", "promotion", "narrowing"]


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


def build(source, flags, extra=()):
    """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, "-I.", source, *extra, "-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 + ["payment", "payment_demo"]:
            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_header_can_be_included_twice(self):
        with tempfile.TemporaryDirectory() as name:
            source = pathlib.Path(name) / "twice.c"
            source.write_text('#include "payment.h"\n#include "payment.h"\nint main(void) { return 0; }\n')
            subprocess.run(["gcc", *BASE, "-Werror", f"-I{HERE}", str(source), "-o", "/dev/null"], check=True)

    def test_typedef_does_not_protect_the_order(self):
        out, _ = run(BUILD / "typedef_units")
        self.assertEqual(out.splitlines(), ["bien: 20.00 m/s", "al revés: 0.05 m/s"])

    def test_union_is_as_big_as_its_largest_field(self):
        out, _ = run(BUILD / "union_bytes")
        self.assertIn("sizeof(union either) = 8", out)

    def test_payment_demo_has_no_leaks(self):
        out, code = run(BUILD / "payment_demo")
        self.assertEqual(code, 0, out)
        self.assertEqual(len(out.splitlines()), 3)


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

    def test_struct_units_do_not_compile(self):
        text, code = self.messages("errors/struct_units.c")
        self.assertNotEqual(code, 0)
        self.assertIn("incompatible type", text)

    def test_missing_case_warns_and_default_hides_it(self):
        self.assertIn("-Wswitch", self.messages("errors/missing_case.c")[0])
        self.assertEqual(self.messages("errors/default_hides.c")[0], "")

    def test_mixing_enums_warns_but_an_int_does_not(self):
        text, _ = self.messages("errors/enum_mix.c")
        self.assertEqual(text.count("-Wenum-conversion"), 1)

    def test_minus_one_warns_and_prints_mayor(self):
        binary, folder, text, _ = build("errors/minus_one.c", BASE)
        self.assertIn("-Wsign-compare", text)
        self.assertEqual(run(binary)[0].strip(), "mayor")
        folder.cleanup()

    def test_narrowing_needs_wconversion(self):
        self.assertEqual(self.messages("narrowing.c")[0], "")
        self.assertIn("-Wconversion", self.messages("narrowing.c", [*BASE, "-Wconversion"])[0])

    def test_these_errors_give_no_warning(self):
        for source in ("errors/overflow_compare.c", "errors/index_out.c", "errors/double_too_big.c",
                       "errors/wrong_variant.c", "errors/wrong_text.c"):
            with self.subTest(source=source):
                self.assertEqual(self.messages(source)[0], "")
        _, folder, text, _ = build("errors/shared_owner.c", BASE, ["payment.c"])
        folder.cleanup()
        self.assertEqual(text, "")

    def test_uninitialized_warns_only_with_optimization(self):
        self.assertEqual(self.messages("errors/uninitialized.c", [*BASE, "-O0"])[0], "")
        self.assertIn("-Wmaybe-uninitialized", self.messages("errors/uninitialized.c", [*BASE, "-O2"])[0])


class SanitizerClaims(unittest.TestCase):
    def test_wrong_variant_is_not_detected(self):
        binary, folder, _, _ = build("errors/wrong_variant.c", FLAGS)
        out, code = run(binary)
        self.assertEqual(code, 0, out)
        self.assertNotIn("runtime error", out)
        folder.cleanup()

    def test_wrong_text_crashes(self):
        binary, folder, _, _ = build("errors/wrong_text.c", FLAGS)
        out, code = run(binary)
        self.assertNotEqual(code, 0)
        self.assertIn("SEGV", out)
        folder.cleanup()

    def test_shared_owner_is_a_double_free(self):
        binary, folder, _, _ = build("errors/shared_owner.c", FLAGS, ["payment.c"])
        out, code = run(binary)
        self.assertNotEqual(code, 0)
        self.assertIn("double-free", out)
        folder.cleanup()

    def test_ubsan_finds_the_overflow(self):
        binary, folder, _, _ = build("errors/overflow_compare.c", [*BASE, "-g", "-fsanitize=undefined"])
        self.assertIn("signed integer overflow", run(binary, "2147483647\n")[0])
        self.assertNotIn("runtime error", run(binary, "5\n")[0])
        folder.cleanup()

    def test_sanitizers_find_the_index_only_if_that_path_runs(self):
        binary, folder, _, _ = build("errors/index_out.c", FLAGS)
        self.assertIn("out of bounds", run(binary, "4\n")[0])
        self.assertEqual(run(binary, "2\n"), ("grades[2] = 17\n", 0))
        folder.cleanup()

    def test_sanitizers_do_not_find_the_uninitialized_read(self):
        binary, folder, _, _ = build("errors/uninitialized.c", FLAGS)
        out, code = run(binary, "30\n")
        self.assertEqual(code, 0, out)
        self.assertNotIn("runtime error", out)
        self.assertNotIn("AddressSanitizer", out)
        folder.cleanup()

    def test_valgrind_finds_the_uninitialized_read(self):
        binary, folder, _, _ = build("errors/uninitialized.c", [*BASE, "-g", "-O0"])
        done = subprocess.run(
            ["valgrind", str(binary)], input="30\n", text=True, encoding="utf-8", errors="replace",
            stdout=subprocess.PIPE, stderr=subprocess.STDOUT, timeout=60,
        )
        self.assertIn("uninitialised", done.stdout)
        folder.cleanup()

    def valgrind_output(self, source, stdin="", extra=()):
        binary, folder, _, _ = build(source, [*BASE, "-g", "-O0"], extra)
        done = subprocess.run(
            ["valgrind", str(binary)], input=stdin, text=True, encoding="utf-8", errors="replace",
            stdout=subprocess.PIPE, stderr=subprocess.STDOUT, timeout=60,
        )
        folder.cleanup()
        return done.stdout

    def test_valgrind_does_not_see_the_overflow(self):
        self.assertIn("ERROR SUMMARY: 0 errors", self.valgrind_output("errors/overflow_compare.c", "2147483647\n"))

    def test_valgrind_does_not_see_the_index(self):
        out = self.valgrind_output("errors/index_out.c", "4\n")
        self.assertNotIn("Invalid read", out)

    def test_valgrind_does_not_see_the_wrong_variant(self):
        self.assertIn("ERROR SUMMARY: 0 errors", self.valgrind_output("errors/wrong_variant.c"))

    def test_valgrind_finds_the_double_free(self):
        self.assertIn("Invalid free", self.valgrind_output("errors/shared_owner.c", extra=["payment.c"]))

    def test_valgrind_does_not_see_the_double_to_int(self):
        self.assertIn("ERROR SUMMARY: 0 errors", self.valgrind_output("errors/double_too_big.c", "3e10\n"))

    def test_double_to_int_needs_its_own_sanitizer(self):
        binary, folder, _, _ = build("errors/double_too_big.c", FLAGS)
        self.assertNotIn("runtime error", run(binary, "3e10\n")[0])
        folder.cleanup()
        binary, folder, _, _ = build("errors/double_too_big.c",
                                     [*BASE, "-g", "-fsanitize=undefined,float-cast-overflow"])
        self.assertIn("outside the range", run(binary, "3e10\n")[0])
        folder.cleanup()


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