"""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 apply(call, width, height, max_value, values):
    """Devuelve (ancho, alto, max, píxeles) de lo que da call sobre la imagen img."""
    numbers = ", ".join(map(str, values))
    source = (
        '#include "filters.h"\nint main(void) {\n'
        f"    const int values[] = {{{numbers}}};\n"
        f"    struct image img = image_from_values({width}, {height}, {max_value}, values);\n"
        f"    image_print({call});\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}")
    numbers = [int(n) for n in re.findall(r"\d+", out)]
    return numbers[0], numbers[1], numbers[2], numbers[3:]


def reference_blur(values, width, height, radius):
    result = []
    for y in range(height):
        for x in range(width):
            window = [values[ny * width + nx]
                      for ny in range(max(0, y - radius), min(height, y + radius + 1))
                      for nx in range(max(0, x - radius), min(width, x + radius + 1))]
            result.append(sum(window) // len(window))
    return result


@unittest.skipUnless((SOURCE / "filters.c").exists(), f"no está {LAB_DIR}/filters.c")
class Filters(unittest.TestCase):
    def test_invert(self):
        values = [0, 10, 255, 30, 40, 100]
        self.assertEqual(apply("invert(img)", 3, 2, 255, values),
                         (3, 2, 255, [255, 245, 0, 225, 215, 155]))

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

    def test_threshold_with_the_exact_level(self):
        self.assertEqual(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.assertEqual(apply("threshold(img, 8)", 2, 1, 15, [7, 8]), (2, 1, 15, [0, 15]))

    def test_flip_with_odd_width(self):
        self.assertEqual(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.assertEqual(apply("flip_horizontal(img)", 4, 1, 255, [1, 2, 3, 4]),
                         (4, 1, 255, [4, 3, 2, 1]))

    def test_blur_center_and_borders(self):
        # La esquina que tiene el 90 separa las reglas de borde: 22 solo sale si se
        # promedian únicamente los píxeles que existen.
        values = [90, 0, 0, 0, 0, 0, 0, 0, 0]
        self.assertEqual(apply("box_blur(img, 1)", 3, 3, 255, values),
                         (3, 3, 255, [22, 15, 0, 15, 10, 0, 0, 0, 0]))

    def test_blur_reads_the_original_values(self):
        # Si se escribe sobre la misma imagen, los últimos píxeles usan valores ya promediados.
        values = [0, 0, 0, 0, 90, 0, 0, 0, 0]
        self.assertEqual(apply("box_blur(img, 1)", 3, 3, 255, values),
                         (3, 3, 255, [22, 15, 22, 15, 10, 15, 22, 15, 22]))

    def test_blur_of_a_uniform_image_does_not_darken_the_border(self):
        self.assertEqual(apply("box_blur(img, 1)", 3, 2, 255, [80] * 6)[3], [80] * 6)
        self.assertEqual(apply("box_blur(img, 1)", 3, 2, 255, [80, 80, 80, 80, 80, 86])[3],
                         reference_blur([80, 80, 80, 80, 80, 86], 3, 2, 1))

    def test_blur_with_other_radius_and_not_square(self):
        values = [3 * i % 200 for i in range(35)]
        for radius in (0, 2, 9):
            with self.subTest(radius=radius):
                self.assertEqual(apply(f"box_blur(img, {radius})", 7, 5, 255, values),
                                 (7, 5, 255, reference_blur(values, 7, 5, radius)))

    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("box_blur(flip_horizontal(invert(threshold(img, 128))), 1)", 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)]
        self.assertEqual(out[3], reference_blur(flipped, 64, 64, 1))


@unittest.skipUnless((SOURCE / "filter_tests.c").exists(), f"no está {LAB_DIR}/filter_tests.c")
class YourTests(unittest.TestCase):
    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", "box_blur"):
            with self.subTest(filter=name):
                self.assertRegex(text, rf"\b{name}\(", 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"])
        self.assertEqual(code, 0, out)


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