"""Pruebas del laboratorio. LAB_DIR dice dónde está el código: lab o solution.

Cada prueba compila un main propio con filters.c de esa carpeta y con el módulo de
lab/, sin correr las pruebas de ustedes.
"""
import os
import pathlib
import re
import subprocess
import tempfile
import unittest

HERE = pathlib.Path(__file__).parent
LAB_DIR = os.environ.get("LAB_DIR", "solution")
SOURCE = HERE / LAB_DIR
FLAGS = ["-Wall", "-Wextra", "-std=c17", "-g", "-fsanitize=address,undefined"]


def compile_and_run(main_source, sources):
    with tempfile.TemporaryDirectory() as name:
        folder = pathlib.Path(name)
        driver = folder / "driver.c"
        driver.write_text(main_source, encoding="utf-8")
        binary = folder / "driver"
        built = subprocess.run(
            ["gcc", *FLAGS, f"-I{HERE / 'lab'}", str(driver), *map(str, sources), "-o", str(binary)],
            text=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
        )
        if built.returncode != 0:
            raise AssertionError(f"No compila:\n{built.stdout}")
        done = subprocess.run(
            [str(binary)], cwd=folder, text=True, encoding="utf-8",
            stdout=subprocess.PIPE, stderr=subprocess.STDOUT, timeout=30,
        )
        return done.stdout, done.returncode


def with_image(body, width, height, max_value, values):
    """Corre body con la imagen img ya creada, y devuelve lo que escribe."""
    numbers = ", ".join(map(str, values))
    source = (
        '#include <stdio.h>\n#include "filters.h"\nint main(void) {\n'
        f"    const int values[] = {{{numbers}}};\n"
        "    struct image img;\n"
        f"    if (!image_from_values(&img, {width}, {height}, {max_value}, values)) {{ return 2; }}\n"
        f"    {body}\n    return 0;\n}}\n"
    )
    out, code = compile_and_run(source, [SOURCE / "filters.c", HERE / "lab" / "image.c"])
    if code != 0:
        raise AssertionError(f"El programa falló:\n{out}")
    return out


def apply(call, width, height, max_value, values):
    """Devuelve (ancho, alto, max, píxeles) después de correr call sobre img."""
    out = with_image(f"{call}; image_print(&img);", width, height, max_value, values)
    numbers = [int(n) for n in re.findall(r"\d+", out)]
    return numbers[0], numbers[1], numbers[2], numbers[3:]


def stats(width, height, max_value, values):
    """Devuelve (mínimo, máximo, promedio). Los valores de partida delatan si no se escribió."""
    body = (
        "int min = -1; int max = -1; double mean = -1;\n"
        "    image_stats(&img, &min, &max, &mean);\n"
        '    printf("%d %d %.4f\\n", min, max, mean);'
    )
    low, high, mean = with_image(body, width, height, max_value, values).split()
    return int(low), int(high), float(mean)


@unittest.skipUnless((SOURCE / "filters.c").exists(), f"no está {LAB_DIR}/filters.c")
class Filters(unittest.TestCase):
    longMessage = False

    def check(self, what, actual, wanted):
        # Un mensaje de una línea: lo que se pidió, lo que se esperaba y lo que salió.
        if actual != wanted:
            self.fail(f"{what}: se esperaba {wanted} y salió {actual}")

    def test_invert(self):
        self.check("invert(&img)", apply("invert(&img)", 3, 2, 255, [0, 10, 255, 30, 40, 100]),
                   (3, 2, 255, [255, 245, 0, 225, 215, 155]))

    def test_invert_uses_the_max_value_of_the_image(self):
        self.check("invert(&img) con max 15", apply("invert(&img)", 2, 1, 15, [0, 5]),
                   (2, 1, 15, [15, 10]))

    def test_threshold_with_the_exact_level(self):
        self.check("threshold(&img, 100)",
                   apply("threshold(&img, 100)", 3, 2, 255, [99, 100, 101, 0, 255, 7]),
                   (3, 2, 255, [0, 255, 255, 0, 255, 0]))

    def test_threshold_uses_the_max_value_of_the_image(self):
        self.check("threshold(&img, 8) con max 15", apply("threshold(&img, 8)", 2, 1, 15, [7, 8]),
                   (2, 1, 15, [0, 15]))

    def test_flip_with_odd_width(self):
        self.check("flip_horizontal con ancho impar",
                   apply("flip_horizontal(&img)", 3, 2, 255, [1, 2, 3, 4, 5, 6]),
                   (3, 2, 255, [3, 2, 1, 6, 5, 4]))

    def test_flip_with_even_width(self):
        self.check("flip_horizontal con ancho par",
                   apply("flip_horizontal(&img)", 4, 1, 255, [1, 2, 3, 4]),
                   (4, 1, 255, [4, 3, 2, 1]))

    def test_filters_on_the_largest_image(self):
        values = [(x * 7 + y * 11) % 256 for y in range(64) for x in range(64)]
        out = apply("threshold(&img, 128); invert(&img); flip_horizontal(&img)", 64, 64, 255, values)
        cut = [255 if v >= 128 else 0 for v in values]
        inverted = [255 - v for v in cut]
        flipped = [inverted[y * 64 + 63 - x] for y in range(64) for x in range(64)]
        if out[3] != flipped:
            self.fail("threshold, invert y flip_horizontal sobre una imagen de 64 x 64: "
                      "algún píxel no es el esperado")

    def test_stats(self):
        self.check("image_stats", stats(3, 2, 255, [7, 2, 3, 4, 9, 5]), (2, 9, 5.0))

    def test_stats_mean_keeps_the_decimals(self):
        self.check("image_stats de 0 y 1", stats(2, 1, 255, [0, 1]), (0, 1, 0.5))

    def test_stats_when_the_extremes_are_at_the_borders(self):
        self.check("image_stats, mínimo primero y máximo último",
                   stats(3, 1, 255, [1, 2, 3]), (1, 3, 2.0))
        self.check("image_stats, máximo primero y mínimo último",
                   stats(3, 1, 255, [3, 2, 1]), (1, 3, 2.0))

    def test_stats_when_every_pixel_is_the_same(self):
        self.check("image_stats, todo en 0", stats(2, 2, 255, [0] * 4), (0, 0, 0.0))
        self.check("image_stats, todo en 255", stats(2, 2, 255, [255] * 4), (255, 255, 255.0))

    def test_stats_ignores_unused_cells(self):
        # Una imagen que no es cuadrada: el 99 de relleno no es parte de ella.
        body = (
            "img.pixels[0][3] = 99; img.pixels[1][0] = 99;\n"
            "    int min = -1; int max = -1; double mean = -1;\n"
            "    image_stats(&img, &min, &max, &mean);\n"
            '    printf("%d %d %.4f\\n", min, max, mean);'
        )
        low, high, mean = with_image(body, 3, 1, 255, [1, 2, 3]).split()
        self.check("image_stats en una imagen de 3 x 1 con basura fuera de ella",
                   (int(low), int(high), float(mean)), (1, 3, 2.0))

    def test_stats_on_the_largest_image(self):
        self.check("image_stats de 64 x 64, todo en 255",
                   stats(64, 64, 255, [255] * (64 * 64)), (255, 255, 255.0))

    def test_stats_does_not_change_the_image(self):
        out = apply("int a; int b; double c; image_stats(&img, &a, &b, &c)", 2, 1, 255, [4, 6])
        self.check("la imagen después de image_stats", out, (2, 1, 255, [4, 6]))

    def test_filters_compile_without_warnings(self):
        built = subprocess.run(
            ["gcc", *FLAGS, "-Werror", f"-I{HERE / 'lab'}", "-c", str(SOURCE / "filters.c"),
             "-o", "/dev/null"],
            text=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
        )
        if built.returncode != 0:
            self.fail(f"filters.c da advertencias:\n{built.stdout}")


@unittest.skipUnless((SOURCE / "filter_tests.c").exists(), f"no está {LAB_DIR}/filter_tests.c")
class YourTests(unittest.TestCase):
    longMessage = False

    def source_without_comments(self):
        text = (SOURCE / "filter_tests.c").read_text(encoding="utf-8")
        return re.sub(r"//[^\n]*", "", text)

    def test_each_filter_has_tests(self):
        text = self.source_without_comments()
        for name in ("invert", "threshold", "flip_horizontal", "image_stats"):
            with self.subTest(filter=name):
                if not re.search(rf"\b{name}\(", text):
                    self.fail(f"filter_tests.c no prueba {name}")

    def test_your_tests_pass(self):
        source = (SOURCE / "filter_tests.c").read_text(encoding="utf-8")
        out, code = compile_and_run(source, [SOURCE / "filters.c", HERE / "lab" / "image.c"])
        if code != 0:
            self.fail(f"filter_tests falló:\n{out}")


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