"""Comprueba los programas de la clase, los errores a propósito y el módulo de imágenes."""
import pathlib
import subprocess
import tempfile
import unittest

HERE = pathlib.Path(__file__).parent
BUILD = HERE / "build"
FLAGS = ["-Wall", "-Wextra", "-std=c17", "-g", "-fsanitize=address,undefined"]
WITH_MODULE = {"image_demo", "show_sun"}
PLAIN = ["pointer_basics", "swap", "out_params", "array_pointer", "struct_pointer",
         "const_pointers", "text_pointers", "file_pointer"]


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


def gcc_errors(name, extra=()):
    """Compila un programa de errors/ sin sanitizers y devuelve (mensajes, estado)."""
    with tempfile.TemporaryDirectory() as folder:
        done = subprocess.run(
            ["gcc", "-Wall", "-Wextra", "-std=c17", f"errors/{name}.c", *extra,
             "-o", str(pathlib.Path(folder) / name)],
            cwd=HERE, text=True, encoding="utf-8", stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
        )
    return done.stdout.replace("‘", "'").replace("’", "'"), done.returncode


def asan_run(name):
    """Compila un programa de errors/ con sanitizers, y devuelve lo que escribe al correr."""
    with tempfile.TemporaryDirectory() as folder:
        binary = pathlib.Path(folder) / name
        subprocess.run(
            ["gcc", *FLAGS, f"errors/{name}.c", "-o", str(binary)],
            cwd=HERE, check=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
        )
        done = subprocess.run(
            [str(binary)], cwd=HERE, text=True, encoding="utf-8",
            stdout=subprocess.PIPE, stderr=subprocess.STDOUT, timeout=20,
        )
    return done.stdout, done.returncode


def run_driver(body, files=None):
    """Compila un main propio con el módulo y lo corre en una carpeta temporal."""
    with tempfile.TemporaryDirectory() as name:
        folder = pathlib.Path(name)
        for file_name, content in (files or {}).items():
            (folder / file_name).write_text(content, encoding="utf-8")
        source = folder / "driver.c"
        source.write_text(
            '#include <stdio.h>\n#include "image.h"\nint main(void) {\n' + body + "\nreturn 0;\n}\n",
            encoding="utf-8",
        )
        binary = folder / "driver"
        subprocess.run(
            ["gcc", *FLAGS, "-Werror", f"-I{HERE / 'lab'}", str(source),
             str(HERE / "lab" / "image.c"), "-o", str(binary)],
            check=True, text=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
        )
        done = subprocess.run(
            [str(binary)], cwd=folder, text=True, encoding="utf-8",
            stdout=subprocess.PIPE, stderr=subprocess.STDOUT, timeout=20,
        )
        return done.stdout, done.returncode


def load_status(content):
    body = 'struct image img; printf("%d\\n", image_load(&img, "in.pgm"));'
    out, code = run_driver(body, {"in.pgm": content})
    return out.strip(), code


class Programs(unittest.TestCase):
    def test_every_program_compiles_without_warnings(self):
        for name in [*PLAIN, *WITH_MODULE]:
            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_star_writes_through_the_pointer(self):
        lines = run("pointer_basics")[0].splitlines()
        self.assertEqual(lines[:3], ["x = 5, *p = 5", "x = 7, *p = 7", "x = 7, y = 21, *p = 21"])
        self.assertEqual(lines[4].split()[-1], lines[5].split()[-1], "p debe ser &y")

    def test_swap_needs_addresses(self):
        self.assertEqual(run("swap")[0].splitlines(),
                         ["después de swap_values: x = 1, y = 2", "después de swap: x = 2, y = 1"])

    def test_out_params_are_untouched_on_failure(self):
        out = run("out_params", "8\n")[0]
        self.assertIn("cociente 3, resto 2", out)
        self.assertIn("q sigue en -1", out)
        self.assertIn("leí 8", out)

    def test_out_params_checks_scanf(self):
        out, code = run("out_params", "abc\n")
        self.assertIn("no era un número", out)
        self.assertEqual(code, 1)

    def test_pointer_steps_are_elements(self):
        lines = run("array_pointer")[0].splitlines()
        first = int(lines[1].split()[-1], 16)
        second = int(lines[2].split()[-1], 16)
        third = int(lines[3].split()[-1], 16)
        self.assertEqual((second - first, third - first), (4, 8))
        self.assertIn("q - p = 2 elementos", lines)
        self.assertIn("sumas: 100 y 100", lines)
        self.assertIn("desde a[2]: 70", lines)

    def test_lab_pointer_programs_give_what_the_answers_say(self):
        wanted = {
            "pointers_basic": "a = 29, b = 20, *p = 20, *q = 29\n",
            "pointers_params": "x = 4, y = 9, z = 99\n",
            "pointers_array": "1 7 7 12 5 \ncount = 2, last = 1\n",
        }
        for name, text in wanted.items():
            with self.subTest(program=name), tempfile.TemporaryDirectory() as folder:
                binary = pathlib.Path(folder) / name
                subprocess.run(
                    ["gcc", *FLAGS, "-Werror", str(HERE / "lab" / f"{name}.c"), "-o", str(binary)],
                    check=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
                )
                done = subprocess.run([str(binary)], text=True, stdout=subprocess.PIPE, timeout=10)
                self.assertEqual((done.stdout, done.returncode), (text, 0))


class Mistakes(unittest.TestCase):
    def test_return_local_gives_a_warning_and_a_null(self):
        out, code = gcc_errors("return_local")
        self.assertEqual(code, 0)
        self.assertIn("function returns address of local variable", out)
        out, code = asan_run("return_local")
        self.assertIn("null pointer", out)
        self.assertNotEqual(code, 0)

    def test_dangling_is_silent_for_gcc_and_caught_by_asan(self):
        self.assertEqual(gcc_errors("dangling"), ("", 0))
        out, code = asan_run("dangling")
        self.assertIn("stack-use-after-return", out)
        self.assertIn("'value'", out)
        self.assertNotEqual(code, 0)

    def test_null_deref_is_silent_for_gcc_and_caught_at_run_time(self):
        self.assertEqual(gcc_errors("null_deref"), ("", 0))
        out, code = asan_run("null_deref")
        self.assertIn("store to null pointer", out)
        self.assertNotEqual(code, 0)

    def test_programs_that_must_not_compile(self):
        wanted = {
            "missing_ampersand": "makes pointer from integer",
            "star_dot": "did you mean to use '->'",
            "const_location": "read-only location '*p'",
            "const_variable": "read-only variable 'p'",
            "wrong_type": "incompatible pointer type",
        }
        for name, message in wanted.items():
            with self.subTest(program=name):
                out, code = gcc_errors(name)
                self.assertNotEqual(code, 0)
                self.assertIn(message, out)
        out, code = gcc_errors("const_image", ["lab/image.c"])
        self.assertNotEqual(code, 0)
        self.assertIn("read-only object", out)

    def test_programs_that_only_warn(self):
        self.assertIn("value computed is not used", gcc_errors("star_increment")[0])
        self.assertIn("'p' is used uninitialized", gcc_errors("uninit_pointer")[0])
        self.assertIn("'img' is used uninitialized", gcc_errors("uninit_image", ["lab/image.c"])[0])


class ImageModule(unittest.TestCase):
    def test_reads_comments_anywhere_between_numbers(self):
        out, code = run_driver(
            'struct image img; if (!image_load(&img, "in.pgm")) { return 1; } image_print(&img);',
            {"in.pgm": "P2\n# uno\n2 # dos\n1\n# tres\n9\n3#cuatro\n4\n"},
        )
        self.assertEqual(code, 0, out)
        self.assertEqual(out.split(), ["2", "x", "1,", "max", "9", "3", "4"])

    def test_good_file_loads(self):
        self.assertEqual(load_status("P2\n1 1\n255\n7\n"), ("1", 0))

    def test_largest_size_loads(self):
        pixels = " ".join(["1"] * (64 * 64))
        self.assertEqual(load_status(f"P2\n64 64\n255\n{pixels}\n"), ("1", 0))

    BAD_FILES = {
        "formato binario": "P5\n1 1\n255\n7\n",
        "vacío": "",
        "ancho cero": "P2\n0 1\n255\n",
        "demasiado ancho": "P2\n65 1\n255\n" + "0 " * 65,
        "demasiado alto": "P2\n1 65\n255\n" + "0 " * 65,
        "max_value cero": "P2\n1 1\n0\n0\n",
        "max_value grande": "P2\n1 1\n256\n0\n",
        "faltan píxeles": "P2\n2 2\n255\n1 2 3\n",
        "píxel mayor que max": "P2\n1 1\n100\n101\n",
        "píxel negativo": "P2\n1 1\n255\n-1\n",
        "letra": "P2\n1 1\n255\nabc\n",
        "número enorme": "P2\n99999999999999999999 1\n255\n0\n",
    }

    def test_bad_files_return_zero(self):
        for why, content in self.BAD_FILES.items():
            with self.subTest(why=why):
                self.assertEqual(load_status(content), ("0", 0))

    def test_missing_file_returns_zero(self):
        out, code = run_driver('struct image img; printf("%d\\n", image_load(&img, "nada.pgm"));')
        self.assertEqual((out.strip(), code), ("0", 0))

    def test_failed_load_does_not_change_the_image(self):
        body = """
        struct image img;
        if (!image_new(&img, 2, 1, 200, 7)) { return 1; }
        printf("%d ", image_load(&img, "in.pgm"));
        printf("%d %d %d %d %d", img.width, img.height, img.max_value, img.pixels[0][0], img.pixels[0][1]);
        """
        # Tiene un encabezado correcto, y falla a medio camino.
        out, code = run_driver(body, {"in.pgm": "P2\n3 3\n255\n1 2 3\n"})
        self.assertEqual((out.split(), code), (["0", "2", "1", "200", "7", "7"], 0))

    def test_save_and_load_give_the_same_image(self):
        body = """
        int values[64 * 3];
        for (int i = 0; i < 64 * 3; i++) { values[i] = i % 256; }
        struct image img;
        struct image back;
        printf("%d\\n", image_from_values(&img, 64, 3, 255, values));
        printf("%d\\n", image_save(&img, "out.pgm"));
        printf("%d\\n", image_load(&back, "out.pgm"));
        printf("%d\\n", image_equal(&back, &img));
        """
        out, code = run_driver(body)
        self.assertEqual((out.split(), code), (["1", "1", "1", "1"], 0))

    def test_saved_lines_are_at_most_70_characters(self):
        body = """
        struct image img;
        printf("%d\\n", image_new(&img, 64, 2, 255, 255));
        printf("%d\\n", image_save(&img, "out.pgm"));
        FILE *file = fopen("out.pgm", "r");
        if (file == NULL) { return 1; }
        char line[200];
        int longest = 0;
        while (fgets(line, sizeof(line), file) != NULL) {
            int length = 0;
            while (line[length] != '\\0') { length++; }
            if (length > longest) { longest = length; }
        }
        fclose(file);
        printf("%d\\n", longest <= 71);
        """
        out, code = run_driver(body)
        self.assertEqual((out.split(), code), (["1", "1", "1"], 0))

    def test_save_to_a_bad_path_returns_zero(self):
        body = """
        struct image img;
        if (!image_new(&img, 1, 1, 255, 0)) { return 1; }
        printf("%d\\n", image_save(&img, "no_existe/out.pgm"));
        """
        out, code = run_driver(body)
        self.assertEqual((out.strip(), code), ("0", 0))

    def test_new_checks_its_arguments(self):
        body = """
        struct image img;
        printf("%d", image_new(&img, 1, 1, 255, 0));
        printf("%d", image_new(&img, 0, 1, 255, 0));
        printf("%d", image_new(&img, 1, 65, 255, 0));
        printf("%d", image_new(&img, 1, 1, 256, 0));
        printf("%d", image_new(&img, 1, 1, 255, 256));
        printf("%d", image_new(&img, 1, 1, 255, -1));
        """
        out, code = run_driver(body)
        self.assertEqual((out.strip(), code), ("100000", 0))

    def test_from_values_checks_and_leaves_the_image_alone(self):
        body = """
        struct image img;
        const int bad[] = {1, 300};
        if (!image_new(&img, 2, 1, 255, 7)) { return 1; }
        printf("%d ", image_from_values(&img, 2, 1, 255, bad));
        printf("%d %d", img.pixels[0][0], img.pixels[0][1]);
        """
        out, code = run_driver(body)
        self.assertEqual((out.split(), code), (["0", "7", "7"], 0))

    def test_equal_ignores_unused_cells(self):
        body = """
        struct image a;
        if (!image_new(&a, 2, 2, 255, 7)) { return 1; }
        struct image b = a;
        b.pixels[5][5] = 1;
        printf("%d", image_equal(&a, &b));
        b.pixels[1][1] = 1;
        printf("%d", image_equal(&a, &b));
        """
        out, code = run_driver(body)
        self.assertEqual((out.strip(), code), ("10", 0))


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