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

Además de correr sus pruebas en C, llaman a las funciones directamente: cada prueba
compila grades.c junto con un main propio, sin correr el main 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")
FOLDER = HERE / LAB_DIR
PROGRAM = HERE / "build" / LAB_DIR / "grades"
FLAGS = ["-Wall", "-Wextra", "-std=c17", "-g", "-fsanitize=address,undefined"]

# Lo que debe salir para cada estudiante válido de notas.txt, sin la última columna.
EXPECTED_ROWS = {
    "Ana": ("A   Ana          14  9 17 12  13.00  aprobado", 14.33),
    "Diego": ("B   Diego        12 10 11 11  11.00  aprobado", 11.33),
    "Gisela": ("C   Gisela       10 11 12 10  10.75  desaprobado", 11.00),
    "Bruno": ("A   Bruno         8 10 11  9   9.50  desaprobado", 10.00),
    "Elena": ("B   Elena        20 18 19 17  18.50  aprobado", 19.00),
    "Julio": ("C   Julio        15 13 13 16  14.25  aprobado", 14.67),
    "Fabio": ("B   Fabio         5  7  0  6   4.50  desaprobado", 6.00),
    "Carla": ("A   Carla        11 11 11 11  11.00  aprobado", 11.00),
    "Katy": ("C   Katy         16 18 20 15  17.25  aprobado", 18.00),
    "Luis": ("C   Luis          9 13 14 11  11.75  aprobado", 12.67),
}


def run_program(stdin):
    done = subprocess.run(
        [str(PROGRAM)], input=stdin, text=True, cwd=FOLDER,
        stdout=subprocess.PIPE, stderr=subprocess.PIPE, timeout=10,
    )
    return done.stdout, done.stderr, done.returncode


def call_with_driver(body):
    """Compila grades.c con otro main y devuelve lo que escribe."""
    folder = pathlib.Path(tempfile.mkdtemp())
    driver = folder / "driver.c"
    driver.write_text(
        f'#define main lab_main\n#include "{FOLDER}/grades.c"\n#undef main\n'
        f"int main(void) {{\n{body}\n    return 0;\n}}\n",
        encoding="utf-8",
    )
    binary = folder / "driver"
    built = subprocess.run(
        ["gcc", *FLAGS, str(driver), "-o", str(binary)],
        text=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
    )
    if built.returncode != 0:
        raise AssertionError(f"No compila con las funciones pedidas:\n{built.stdout}")
    done = subprocess.run(
        [str(binary)], text=True, cwd=FOLDER,
        stdout=subprocess.PIPE, stderr=subprocess.PIPE, timeout=20,
    )
    return done.stdout


def student(grades):
    values = ", ".join(map(str, grades))
    return f"(struct student) {{'A', \"X\", {{{values}}}}}"


def rows(output):
    return [line for line in output.splitlines() if re.match(r"^[A-Z]   \S", line)]


@unittest.skipUnless(FOLDER.is_dir(), f"no existe {LAB_DIR}/")
class Session1(unittest.TestCase):
    def test_no_warnings(self):
        for name in ["grades.c", "test_grades.c"]:
            with self.subTest(name=name):
                done = subprocess.run(
                    ["gcc", "-Wall", "-Wextra", "-std=c17", "-Werror", "-fsyntax-only", name],
                    cwd=FOLDER, text=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
                )
                self.assertEqual(done.returncode, 0, done.stdout)

    def test_their_c_tests_pass(self):
        done = subprocess.run(
            [str(HERE / "build" / LAB_DIR / "test_grades")], cwd=FOLDER, text=True,
            stdout=subprocess.PIPE, stderr=subprocess.STDOUT, timeout=20,
        )
        self.assertEqual(done.returncode, 0, done.stdout)

    def test_average_keeps_decimals(self):
        output = call_with_driver(
            f'printf("%.2f %.2f\\n", average({student([14, 9, 17, 13])}), average({student([1, 0, 0, 0])}));'
        )
        self.assertEqual(output.strip(), "13.25 0.25")

    def test_each_grade_is_checked(self):
        lines = ["A X 21 9 17 12", "A X 14 21 17 12", "A X 14 9 21 12", "A X 14 9 17 21",
                 "A X -1 9 17 12", "A X 14 -1 17 12", "A X 14 9 -1 12", "A X 14 9 17 -1"]
        body = ["struct student list[1];"]
        body += [f'printf("%d\\n", parse_student("{line}", list, 0));' for line in lines]
        output = call_with_driver("\n".join(body))
        self.assertEqual(output.split(), ["0"] * len(lines))

    def test_bad_lines_are_reported(self):
        _, errors, status = run_program("notas.txt\n")
        self.assertEqual(status, 0)
        for number in [6, 10, 12]:
            self.assertIn(f"Línea {number}: mal formada", errors)

    def test_report_values(self):
        output, _, _ = run_program("notas.txt\n")
        found = rows(output)
        self.assertEqual(len(found), len(EXPECTED_ROWS), output)
        for line, (name, (prefix, _)) in zip(found, EXPECTED_ROWS.items()):
            with self.subTest(name=name):
                self.assertTrue(line.startswith(prefix), line)

    def test_summary(self):
        output, _, _ = run_program("notas.txt\n")
        self.assertIn("Estudiantes: 10\n", output)
        self.assertIn("Aprobados: 7\n", output)
        self.assertIn("Promedio del curso: 12.15\n", output)
        self.assertIn("Mejor promedio: Elena (18.50)\n", output)


@unittest.skipUnless(FOLDER.is_dir(), f"no existe {LAB_DIR}/")
class WhatAlreadyWorked(unittest.TestCase):
    """Lo que ya funcionaba en el programa dado debe seguir funcionando."""

    def test_missing_file(self):
        output, errors, status = run_program("no_existe.txt\n")
        self.assertEqual(status, 1)
        self.assertEqual(output, "")
        self.assertIn("no_existe.txt", errors)

    def test_no_input(self):
        _, errors, status = run_program("")
        self.assertEqual(status, 1)
        self.assertIn("Falta el nombre", errors)

    def test_empty_file(self):
        with tempfile.NamedTemporaryFile("w", suffix=".txt", delete=False) as empty:
            pass
        output, _, status = run_program(f"{empty.name}\n")
        self.assertEqual(status, 0)
        self.assertEqual(output, "No hay estudiantes\n")

    def test_more_students_than_capacity(self):
        with tempfile.NamedTemporaryFile("w", suffix=".txt", delete=False) as many:
            for i in range(60):
                many.write(f"A X{i} 11 11 11 11\n")
        output, errors, status = run_program(f"{many.name}\n")
        self.assertEqual(status, 0)
        self.assertIn("Estudiantes: 50\n", output)
        self.assertIn("Demasiados estudiantes", errors)

    def test_long_line_does_not_shift_the_rest(self):
        with tempfile.NamedTemporaryFile("w", suffix=".txt", delete=False) as long_line:
            long_line.write("A Ana 14 9 17 12\n" + "B " + "x" * 100 + " 1 2 3 4\nC Luis 9 13 14 11\n")
        output, errors, _ = run_program(f"{long_line.name}\n")
        self.assertIn("Estudiantes: 2\n", output)
        self.assertIn("Línea 2: mal formada", errors)

    def test_functions_keep_their_parameters(self):
        output = call_with_driver(
            """
    struct student list[MAX_STUDENTS];
    int n = read_students("casos.txt", list, MAX_STUDENTS);
    print_report(list, n);
    print_summary(list, n);
    print_student(list[0]);
    printf("%d %d %d\\n", count_passed(list, n), best_index(list, n), is_passed(list[0]));
    printf("%.2f %d\\n", class_average(list, n), total_points(list[0]));
"""
        )
        self.assertIn("Estudiantes:", output)


@unittest.skipUnless(FOLDER.is_dir(), f"no existe {LAB_DIR}/")
class Session2(unittest.TestCase):
    def check_without_lowest(self, grades, expected):
        output = call_with_driver(
            f'printf("%.3f\\n", average_without_lowest({student(grades)}));'
        )
        self.assertEqual(output.strip(), f"{expected:.3f}")

    def test_lowest_in_any_position(self):
        self.check_without_lowest([3, 12, 12, 13], 37 / 3)
        self.check_without_lowest([12, 3, 12, 13], 37 / 3)
        self.check_without_lowest([12, 12, 3, 13], 37 / 3)
        self.check_without_lowest([12, 12, 13, 3], 37 / 3)

    def test_keeps_decimals(self):
        self.check_without_lowest([14, 9, 17, 12], 43 / 3)

    def test_tie_removes_only_one(self):
        self.check_without_lowest([5, 5, 11, 11], 9.0)
        self.check_without_lowest([11, 5, 11, 5], 9.0)

    def test_all_equal(self):
        self.check_without_lowest([8, 8, 8, 8], 8.0)
        self.check_without_lowest([0, 0, 0, 0], 0.0)

    def test_they_wrote_tests(self):
        text = (FOLDER / "test_grades.c").read_text(encoding="utf-8")
        calls = re.findall(r"(?<!test_)average_without_lowest\(", text)
        self.assertGreaterEqual(len(calls), 4, "faltan pruebas de average_without_lowest en test_grades.c")
        self.assertIn("failed += test_average_without_lowest();", text)

    def test_report_has_new_column(self):
        output, _, _ = run_program("notas.txt\n")
        found = rows(output)
        self.assertEqual(len(found), len(EXPECTED_ROWS), output)
        for line, (name, (prefix, value)) in zip(found, EXPECTED_ROWS.items()):
            with self.subTest(name=name):
                self.assertTrue(line.startswith(prefix), line)
                self.assertEqual(line.split()[-1], f"{value:.2f}")

    def test_filter_by_section(self):
        output, _, status = run_program("notas.txt\nB\n")
        self.assertEqual(status, 0)
        self.assertEqual([line.split()[1] for line in rows(output)], ["Diego", "Elena", "Fabio"])
        self.assertIn("Estudiantes: 3\n", output)
        self.assertIn("Aprobados: 2\n", output)
        self.assertIn("Promedio del curso: 11.33\n", output)
        self.assertIn("Mejor promedio: Elena (18.50)\n", output)

    def test_filter_another_section(self):
        output, _, _ = run_program("notas.txt\nC\n")
        self.assertEqual([line.split()[1] for line in rows(output)], ["Gisela", "Julio", "Katy", "Luis"])
        self.assertIn("Mejor promedio: Katy (17.25)\n", output)

    def test_section_without_students(self):
        output, _, status = run_program("notas.txt\nZ\n")
        self.assertEqual(status, 0)
        self.assertEqual(output, "No hay estudiantes\n")

    def test_empty_option_shows_everyone(self):
        output, _, status = run_program("notas.txt\n\n")
        self.assertEqual(status, 0)
        self.assertIn("Estudiantes: 10\n", output)


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