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

Cada prueba compila un main propio con value.c y table.c de esa carpeta, por separado
de las pruebas de ustedes. Corren con ASan, así que una fuga también hace fallar la prueba.
"""
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 build_and_run(main_source, sources, files=None):
    """Devuelve (salida, código de salida) de un main propio, 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")
        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", errors="replace",
            stdout=subprocess.PIPE, stderr=subprocess.STDOUT, timeout=30,
        )
        return done.stdout, done.returncode


def run_value(body):
    source = ('#include <stdio.h>\n#include <string.h>\n#include "value.h"\n'
              "int main(void) {\n" + body + "\nreturn 0;\n}\n")
    return build_and_run(source, [SOURCE / "value.c"])


def run_table(body, csv=None):
    source = ('#include <stdio.h>\n#include "table.h"\n'
              "int main(void) {\n" + body + "\nreturn 0;\n}\n")
    files = {"in.csv": csv} if csv is not None else None
    return build_and_run(source, [SOURCE / "value.c", SOURCE / "table.c"], files)


def summary_body(column):
    return (
        'Table *t = table_load("in.csv");\n'
        'if (t == NULL) { printf("NULL\\n"); return 0; }\n'
        f"ColumnSummary s = table_summary(t, {column});\n"
        'printf("%d %d %d %d %.2f %.2f %.2f\\n", s.integers, s.decimals, s.strings, s.nones,'
        " s.sum, s.min, s.max);\n"
        "table_free(t);"
    )


class LabCase(unittest.TestCase):
    longMessage = False

    def check(self, result, wanted, what):
        out, code = result
        if code != 0:
            self.fail(f"{what}: el programa terminó con error.\n{out}")
        if out.strip() != wanted:
            self.fail(f"{what}\n  se esperaba: {wanted}\n  se obtuvo:   {out.strip()}")


@unittest.skipUnless((SOURCE / "value.c").exists(), f"no está {LAB_DIR}/value.c")
class ValueModule(LabCase):
    def test_constructors_set_the_tag_and_the_variant(self):
        body = """
        Value a = value_none(); Value b = value_int(-3); Value c = value_double(2.5);
        Value d = value_string("Ate");
        printf("%d %d %d %d\\n", a.kind == VALUE_NONE, b.kind == VALUE_INT,
               c.kind == VALUE_DOUBLE, d.kind == VALUE_STRING);
        if (c.kind == VALUE_DOUBLE) { printf("%.1f\\n", c.as.number); }
        if (d.kind == VALUE_STRING) { printf("%s\\n", d.as.text); }
        value_free(&d);
        """
        self.check(run_value(body), "1 1 1 1\n2.5\nAte", "Los constructores no dejan la etiqueta o el dato esperado.")

    def test_string_is_a_copy(self):
        body = """
        char name[] = "Ate";
        Value v = value_string(name);
        name[0] = 'X';
        if (v.kind == VALUE_STRING) { printf("%s %d\\n", v.as.text, v.as.text != name); }
        value_free(&v);
        """
        self.check(run_value(body), "Ate 1", "value_string debe guardar una copia en memoria nueva.")

    def test_string_can_be_empty(self):
        body = """
        Value v = value_string("");
        printf("%d\\n", v.kind == VALUE_STRING);
        if (v.kind == VALUE_STRING) { printf("[%s]\\n", v.as.text); }
        value_free(&v);
        """
        self.check(run_value(body), "1\n[]", "Una cadena vacía también es una cadena.")

    def test_print_each_variant(self):
        body = """
        Value values[5] = {value_none(), value_int(-42), value_double(2.5), value_double(3.0),
                           value_string("hora punta")};
        for (int i = 0; i < 5; i++) { value_print(&values[i]); printf(";"); }
        for (int i = 0; i < 5; i++) { value_free(&values[i]); }
        """
        self.check(run_value(body), '(nada);-42;2.50;3.00;"hora punta";', "value_print no escribe lo que dice value.h.")

    def test_print_does_not_add_a_newline(self):
        body = 'Value v = value_int(1); value_print(&v); value_print(&v); value_free(&v);'
        self.check(run_value(body), "11", "value_print no debe escribir un salto de línea.")

    def test_equal(self):
        body = """
        Value none = value_none(), none2 = value_none();
        Value i = value_int(3), i2 = value_int(3), i3 = value_int(4);
        Value d = value_double(3.0), d2 = value_double(3.0), d3 = value_double(3.5);
        Value s = value_string("Ate"), s2 = value_string("Ate"), s3 = value_string("Lima");
        printf("%d%d%d", value_equal(&none, &none2), value_equal(&i, &i2), value_equal(&d, &d2));
        printf("%d ", value_equal(&s, &s2));
        printf("%d%d%d", value_equal(&i, &i3), value_equal(&d, &d3), value_equal(&s, &s3));
        printf("%d%d%d", value_equal(&i, &d), value_equal(&none, &i), value_equal(&s, &none));
        value_free(&s); value_free(&s2); value_free(&s3);
        """
        self.check(run_value(body), "1111 000000", "value_equal no cumple lo que dice value.h.")

    def test_equal_does_not_read_another_variant(self):
        # Los bytes del 0 y del 0.0 coinciden en lo que se compara: solo la etiqueta los separa.
        body = """
        Value a = value_int(0); Value b = value_double(0.0);
        printf("%d\\n", value_equal(&a, &b));
        """
        self.check(run_value(body), "0", "value_equal debe mirar primero la etiqueta.")

    def test_free_leaves_nothing_and_can_be_repeated(self):
        body = """
        Value v = value_string("Ate");
        value_free(&v);
        printf("%d\\n", v.kind == VALUE_NONE);
        value_free(&v);
        Value n = value_int(3);
        value_free(&n);
        printf("%d\\n", n.kind == VALUE_NONE);
        """
        self.check(run_value(body), "1\n1", "value_free debe liberar y dejar el valor como nada.")

    def test_a_string_that_is_not_freed_is_reported(self):
        # Comprueba que aquí sí se detectan las fugas: si no, las otras pruebas no dirían nada.
        out, code = build_and_run(
            '#include <stdlib.h>\nint main(void) { char *p = malloc(8); p = NULL; return p != NULL; }\n', [])
        self.assertNotEqual(code, 0, "Este entorno no detecta fugas, y estas pruebas no sirven aquí.")
        self.assertIn("LeakSanitizer", out)

    def test_parse(self):
        cases = {
            "": "(nada)", "42": "42", "-7": "-7", "0": "0", "2.50": "2.50", "-4.5": "-4.50",
            "Callao": '"Callao"', "hora punta": '"hora punta"', "-": '"-"', "3x": '"3x"',
            ".5": '".5"', "5.": '"5."', "1.2.3": '"1.2.3"', "1e5": '"1e5"', "nan": '"nan"',
            "inf": '"inf"', "2147483647": "2147483647", "2147483648": '"2147483648"',
            "99999999999999999999": '"99999999999999999999"',
            "-2147483647": "-2147483647", "-2147483648": '"-2147483648"',
            "1" * 70 + ".5": '"' + "1" * 70 + '.5"',
        }
        for field, wanted in cases.items():
            with self.subTest(field=field):
                body = f'Value v = value_parse("{field}"); value_print(&v); value_free(&v);'
                self.check(run_value(body), wanted, f"value_parse con el campo [{field}]")


@unittest.skipUnless((SOURCE / "table.c").exists(), f"no está {LAB_DIR}/table.c")
class TableModule(LabCase):
    CSV = "a,b,c\n1,2.5,x\n,-3,y\n4,,\n"

    def test_size_and_names(self):
        body = """
        Table *t = table_load("in.csv");
        if (t == NULL) { printf("NULL\\n"); return 0; }
        printf("%d %d %s %s\\n", table_rows(t), table_columns(t), table_column_name(t, 0),
               table_column_name(t, 2));
        table_free(t);
        """
        self.check(run_table(body, self.CSV), "3 3 a c", "table_load debe leer todas las filas.")

    def test_cells_have_the_right_variant(self):
        body = """
        Table *t = table_load("in.csv");
        if (t == NULL) { printf("NULL\\n"); return 0; }
        for (int r = 0; r < table_rows(t); r++) {
            for (int c = 0; c < table_columns(t); c++) {
                value_print(table_cell(t, r, c));
                printf(" ");
            }
        }
        table_free(t);
        """
        self.check(run_table(body, self.CSV), '1 2.50 "x" (nada) -3 "y" 4 (nada) (nada)',
                   "Las celdas no tienen lo que dice el archivo.")

    def test_only_the_header_gives_an_empty_table(self):
        body = """
        Table *t = table_load("in.csv");
        if (t == NULL) { printf("NULL\\n"); return 0; }
        printf("%d %d\\n", table_rows(t), table_columns(t));
        table_free(t);
        """
        self.check(run_table(body, "a,b\n"), "0 2", "Una tabla sin filas también es válida.")

    def test_many_rows_need_to_grow_and_do_not_leak(self):
        csv = "name,n\n" + "".join(f"fila{i},{i}\n" for i in range(2000))
        body = """
        Table *t = table_load("in.csv");
        if (t == NULL) { printf("NULL\\n"); return 0; }
        printf("%d ", table_rows(t));
        value_print(table_cell(t, 1999, 0));
        table_free(t);
        """
        self.check(run_table(body, csv), '2000 "fila1999"', "Con 2000 filas algo falla.")

    def test_bad_files_give_null_without_leaks(self):
        bad = {
            "falta un campo": "a,b\n1,2\n3\n",
            "sobra un campo": "a,b\n1,2\n3,4,5\n",
            "falla tras cadenas": "a,b\nx,y\nz,w\n1\n",
            "vacío": "",
        }
        body = 'Table *t = table_load("in.csv"); printf("%d\\n", t == NULL); table_free(t);'
        for why, csv in bad.items():
            with self.subTest(why=why):
                self.check(run_table(body, csv), "1", f"table_load debe devolver NULL: {why}.")

    def test_missing_file_and_free_of_null(self):
        body = 'Table *t = table_load("nada.csv"); printf("%d\\n", t == NULL); table_free(NULL);'
        self.check(run_table(body), "1", "Un archivo que no existe da NULL, y table_free acepta NULL.")

    def test_summary_counts_each_variant(self):
        csv = "c\n1\n2.5\nx\n\n3\n"
        # La línea vacía se salta: en una tabla de una columna no hay forma de escribir nada.
        self.check(run_table(summary_body(0), csv), "2 1 1 0 6.50 1.00 3.00",
                   "El resumen no cuenta bien, o no suma enteros con decimales.")

    def test_summary_with_nothing_in_a_cell(self):
        self.check(run_table(summary_body(1), "a,b\n1,\n2,5\n"), "1 0 0 1 5.00 5.00 5.00",
                   "Una celda con nada se cuenta, y no entra en el mínimo.")

    def test_summary_min_and_max_with_negatives(self):
        # Si el mínimo o el máximo empiezan en 0, estas dos columnas lo descubren.
        self.check(run_table(summary_body(0), "a,b\n-4.5,3\n-1,7\n"), "1 1 0 0 -5.50 -4.50 -1.00",
                   "El máximo de números negativos no puede ser 0.")
        self.check(run_table(summary_body(1), "a,b\n-4.5,3\n-1,7\n"), "2 0 0 0 10.00 3.00 7.00",
                   "El mínimo de números positivos no puede ser 0.")

    def test_summary_without_numbers_gives_zero(self):
        self.check(run_table(summary_body(0), "a\nx\ny\n"), "0 0 2 0 0.00 0.00 0.00",
                   "Sin números, la suma, el mínimo y el máximo valen 0.")

    def test_report_on_the_data_file(self):
        with tempfile.TemporaryDirectory() as name:
            binary = pathlib.Path(name) / "report"
            built = subprocess.run(
                ["gcc", *FLAGS, f"-I{HERE / 'lab'}", str(HERE / "lab" / "report.c"),
                 str(SOURCE / "value.c"), str(SOURCE / "table.c"), "-o", str(binary)],
                text=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
            )
            self.assertEqual(built.returncode, 0, built.stdout)
            done = subprocess.run(
                [str(binary), str(HERE / "lab" / "data" / "viajes.csv")], text=True, encoding="utf-8",
                stdout=subprocess.PIPE, stderr=subprocess.STDOUT, timeout=30,
            )
        if done.returncode != 0:
            self.fail(f"report terminó con error:\n{done.stdout}")
        lines = [re.sub(r"\s+", " ", line).strip() for line in done.stdout.splitlines()]
        for wanted in ("7 filas y 4 columnas", "pasajeros 5 0 1 1 105.00 21.00 9.00 31.00",
                       "tarifa 2 5 0 0 17.80 2.54 1.80 3.50", '"Ate" | (nada) | 1.80 | "sin conteo"'):
            with self.subTest(line=wanted):
                self.assertTrue(wanted in lines, f"report no escribió esta línea: {wanted}")


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

    def test_each_function_has_tests(self):
        text = re.sub(r"//[^\n]*", "", (SOURCE / "value_tests.c").read_text(encoding="utf-8"))
        for name in ("value_double", "value_string", "value_equal", "value_free"):
            with self.subTest(function=name):
                self.assertRegex(text, rf"\b{name}\(", f"value_tests.c no prueba {name}")

    def test_your_tests_pass(self):
        source = (SOURCE / "value_tests.c").read_text(encoding="utf-8")
        out, code = build_and_run(source, [SOURCE / "value.c"])
        self.assertEqual(code, 0, out)


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