"""Comprueba los ejemplos de los apuntes, los errores a propósito y que el laboratorio
de partida compile sin advertencias."""
import os
import pathlib
import shutil
import subprocess
import tempfile
import unittest

HERE = pathlib.Path(__file__).parent
FLAGS = ["-Wall", "-Wextra", "-std=c17", "-g", "-fsanitize=address,undefined"]
CLEAN_ENV = {k: v for k, v in os.environ.items() if k not in ("MAKELEVEL", "MAKEFLAGS", "MFLAGS")}


def shell(args, cwd=HERE):
    done = subprocess.run(
        args, cwd=cwd, env=CLEAN_ENV, text=True, encoding="utf-8",
        stdout=subprocess.PIPE, stderr=subprocess.STDOUT, timeout=60,
    )
    return done.stdout.replace("‘", "'").replace("’", "'").replace("`", "'"), done.returncode


def build_and_run(sources, folder=HERE, cwd=None):
    """Compila con sanitizers y sin advertencias, y corre. ASan avisa si queda una fuga."""
    with tempfile.TemporaryDirectory() as name:
        binary = pathlib.Path(name) / "program"
        out, code = shell(["gcc", *FLAGS, "-Werror", *sources, "-o", str(binary)], folder)
        if code != 0:
            raise AssertionError(f"No compila:\n{out}")
        return shell([str(binary)], cwd or folder)


def compile_only(source, folder=HERE, *flags):
    with tempfile.TemporaryDirectory() as name:
        target = str(pathlib.Path(name) / "out.o")
        return shell(["gcc", "-Wall", "-Wextra", "-std=c17", *flags, "-c", source, "-o", target], folder)


class Examples(unittest.TestCase):
    def test_receipt(self):
        sources = ["receipt.c", "ticket.c", "item.c", "money.c"]
        out, code = build_and_run(sources, HERE / "receipt")
        self.assertEqual(code, 0, out)
        self.assertEqual(out.splitlines()[-2:], ["Total:         S/ 29.10", "Con 10%:       S/ 26.19"])

    def test_tally_passes_with_both_implementations_and_no_leaks(self):
        for name in ("tally_array.c", "tally_summary.c"):
            with self.subTest(implementation=name):
                out, code = build_and_run(["test_tally.c", name], HERE / "tally")
                self.assertEqual(code, 0, out)
                self.assertEqual(out.strip(), "Todas las pruebas pasaron")

    def test_private_function_stays_private(self):
        with tempfile.TemporaryDirectory() as name:
            target = str(pathlib.Path(name) / "money.o")
            shell(["gcc", "-std=c17", "-c", "money.c", "-o", target], HERE / "receipt")
            out, _ = shell(["nm", target])
        self.assertRegex(out, r"\st percent_of$|\st percent_of\n")
        self.assertRegex(out, r"\sT money_print")

    def test_no_test_file_includes_a_c_file(self):
        for source in list(HERE.rglob("test_*.c")):
            if "build" not in source.parts:
                with self.subTest(file=str(source.relative_to(HERE))):
                    self.assertNotRegex(source.read_text(encoding="utf-8"), r'#include\s+"[^"]*\.c"')

    def test_every_real_header_has_a_guard(self):
        for source in HERE.rglob("*.h"):
            parts = source.relative_to(HERE).parts
            if "no_guard" in parts or "build" in parts:
                continue
            if parts[:2] == ("lab", "split"):
                continue
            with self.subTest(file=str(source.relative_to(HERE))):
                self.assertRegex(source.read_text(encoding="utf-8"), r"#ifndef \w+_H\n#define \w+_H")


class DeliberateErrors(unittest.TestCase):
    def assert_compile_error(self, source, message, *flags):
        out, code = compile_only(source, HERE, *flags)
        self.assertNotEqual(code, 0, f"{source} debería fallar")
        self.assertIn(message, out)

    def test_no_prototype(self):
        self.assert_compile_error("errors/no_prototype.c", "implicit declaration of function 'money_print'")

    def test_no_guard(self):
        self.assert_compile_error("errors/no_guard/main.c", "redefinition of 'struct point'")

    def test_drift(self):
        self.assert_compile_error("errors/drift.c", "conflicting types for 'area'")

    def test_peek_at_opaque_type(self):
        out, code = compile_only("errors/peek_tally.c", HERE, "-Itally")
        self.assertNotEqual(code, 0)
        self.assertIn("storage size of 'local' isn't known", out)
        self.assertIn("invalid use of incomplete typedef 'Tally'", out)
        self.assertIn("invalid application of 'sizeof' to incomplete type", out)

    def link_error(self, sources, flags=()):
        with tempfile.TemporaryDirectory() as name:
            return shell(["gcc", "-std=c17", *flags, *sources, "-o", str(pathlib.Path(name) / "program")])

    def test_same_function_in_two_files(self):
        out, code = self.link_error(["receipt/receipt.c", "receipt/ticket.c", "receipt/item.c",
                                     "receipt/money.c", "errors/money_twin.c"])
        self.assertNotEqual(code, 0)
        self.assertIn("multiple definition of 'money_print'", out)

    def test_call_private(self):
        out, code = self.link_error(["errors/call_private.c", "receipt/money.c"])
        self.assertNotEqual(code, 0)
        self.assertIn("undefined reference to 'percent_of'", out)

    def test_body_in_header(self):
        out, code = self.link_error(["errors/header_body/first.c", "errors/header_body/second.c"])
        self.assertNotEqual(code, 0)
        self.assertIn("multiple definition of 'twice'", out)


class Starter(unittest.TestCase):
    def test_split_program_works_in_one_file(self):
        out, code = build_and_run(["market.c"], HERE / "lab" / "split")
        self.assertEqual(code, 0, out)
        self.assertIn("Total: S/ 118.60", out)
        self.assertIn("La mejor venta: papa", out)

    def test_stubs_compile_without_warnings(self):
        for folder, source in (("fraction", "fraction.c"), ("fraction", "test_fraction.c"),
                               ("stack", "stack_array.c"), ("stack", "stack_blocks.c"),
                               ("stack", "test_stack.c"), ("image", "image.c"),
                               ("image", "filters.c"), ("image", "filter_demo.c")):
            with self.subTest(file=f"{folder}/{source}"):
                out, code = compile_only(source, HERE / "lab" / folder, "-Werror")
                self.assertEqual(code, 0, out)

    def test_transparent_image_module_runs(self):
        with tempfile.TemporaryDirectory() as name:
            copy = pathlib.Path(name) / "image"
            shutil.copytree(HERE / "lab" / "image", copy, ignore=shutil.ignore_patterns("build"))
            out, code = shell(["make"], copy)
            self.assertEqual(code, 0, out)
            self.assertNotIn("warning:", out)
            out, code = shell(["build/filter_demo"], copy)
            self.assertEqual(code, 0, out)
            self.assertIn("Tamaño: 32 x 16", out)


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