"""Comprueba los programas de la clase y lo que los apuntes dicen de sus errores."""
import pathlib
import re
import subprocess
import tempfile
import unittest

HERE = pathlib.Path(__file__).parent
BUILD = HERE / "build"
CXX = ["g++", "-Wall", "-Wextra", "-std=c++20", "-g", "-fsanitize=address,undefined"]
C_MODULE = HERE / "lab" / "c"

# Programa de errors/ que no compila, y parte del mensaje de g++.
COMPILE_ERRORS = {
    "malloc_no_cast": "invalid conversion from 'void*' to 'int*'",
    "keyword_name": "expected primary-expression before 'int'",
    "using_std_collision": "reference to 'count' is ambiguous",
    "ref_uninitialized": "'alias' declared as reference but not initialized",
    "ref_to_temporary": "cannot bind non-const lvalue reference",
    "const_ref_write": "assignment of read-only reference 'value'",
    "private_access": "is private within this context",
    "const_method_writes": "increment of member 'Counter::value_' in read-only object",
    "const_object_call": "discards qualifiers",
    "no_default_ctor": "no matching function for call to 'Range::Range()'",
    "vexing_parse": "which is of non-class type 'Tally()'",
}
# Programa de errors/ que compila con una advertencia, y su opción.
WARNINGS = {
    "return_local_ref": "-Wreturn-local-addr",
    "init_order": "-Wreorder",
    "sign_compare": "-Wsign-compare",
}
# Programa de errors/ que falla al correr, y lo que debe decir.
RUNTIME_ERRORS = {
    "vector_index": "heap-buffer-overflow",
    "vector_at": "std::out_of_range",
}


def compile_error(name):
    done = subprocess.run(CXX + ["-I.", "-c", f"errors/{name}.cpp", "-o", "/dev/null"], cwd=HERE, text=True,
                          stdout=subprocess.PIPE, stderr=subprocess.STDOUT, timeout=120)
    return done.returncode, done.stdout.replace("‘", "'").replace("’", "'")


class Programs(unittest.TestCase):
    def output(self, name, *args):
        done = subprocess.run([str(BUILD / name), *args], cwd=HERE, text=True, capture_output=True, timeout=60)
        self.assertEqual(done.returncode, 0, done.stderr)
        return done.stdout

    def test_every_program_builds_without_warnings(self):
        with tempfile.TemporaryDirectory() as temp:
            for source in sorted(HERE.glob("*.cpp")):
                done = subprocess.run(CXX + ["-c", str(source), "-o", f"{temp}/program.o"], cwd=HERE, text=True,
                                      capture_output=True, timeout=120)
                with self.subTest(source=source.name):
                    self.assertEqual((done.returncode, done.stderr), (0, ""))

    def test_tally_tests_pass(self):
        self.assertIn("todas las pruebas pasaron", self.output("tally_tests"))

    def test_both_tallies_say_the_same(self):
        self.assertEqual(self.output("tally_demo"), self.output("tally_c_demo"))

    def test_lookup_counts(self):
        # Lineal: 1 + 2 + ... + n exactamente.
        for n in (1, 10, 1000):
            text = self.output("lookup", str(n), "linear")
            self.assertIn(f"comparaciones = {n * (n + 1) // 2}", text)
        # Binaria: como mucho floor(log2(n)) + 1 por búsqueda.
        text = self.output("lookup", "1000", "binary")
        count = int(re.search(r"comparaciones = (\d+)", text).group(1))
        self.assertLessEqual(count, 1000 * 10)

    def test_copy_counts(self):
        self.assertIn("elementos copiados = 1000000", self.output("copy_in_loop", "1000", "copy"))
        self.assertIn("elementos copiados = 0", self.output("copy_in_loop", "1000", "ref"))

    def test_only_by_value_makes_another_block(self):
        lines = self.output("copy_or_not").splitlines()
        self.assertIn("mismo bloque = 0", lines[0])
        self.assertIn("mismo bloque = 1", lines[2])
        self.assertIn("mismo bloque = 1", lines[4])


class DeliberateErrors(unittest.TestCase):
    def test_compile_errors(self):
        for name, message in COMPILE_ERRORS.items():
            with self.subTest(program=name):
                code, text = compile_error(name)
                self.assertNotEqual(code, 0)
                self.assertIn(message, text)

    def test_warnings(self):
        for name, option in WARNINGS.items():
            with self.subTest(program=name):
                code, text = compile_error(name)
                self.assertEqual(code, 0)
                self.assertIn(option, text)

    def test_runtime_errors(self):
        with tempfile.TemporaryDirectory() as temp:
            for name, message in RUNTIME_ERRORS.items():
                with self.subTest(program=name):
                    program = f"{temp}/{name}"
                    subprocess.run(CXX + [f"errors/{name}.cpp", "-o", program], cwd=HERE, check=True, timeout=120)
                    done = subprocess.run([program], text=True, capture_output=True, timeout=60)
                    self.assertNotEqual(done.returncode, 0)
                    self.assertIn(message, done.stderr)


class GivenCModule(unittest.TestCase):
    """El módulo en C del laboratorio es de verdad: compila limpio, pasa sus pruebas y no tiene fugas."""

    def test_compiles_and_passes_with_sanitizers(self):
        with tempfile.TemporaryDirectory() as temp:
            program = f"{temp}/tests"
            done = subprocess.run(["gcc", "-Wall", "-Wextra", "-std=c17", "-g", "-fsanitize=address,undefined",
                                   str(C_MODULE / "test_grade_book.c"), str(C_MODULE / "grade_book.c"), "-o", program],
                                  text=True, capture_output=True, timeout=120)
            self.assertEqual((done.returncode, done.stderr), (0, ""))
            run = subprocess.run([program], text=True, capture_output=True, timeout=60)
            self.assertEqual(run.returncode, 0, run.stderr)

    def test_clean_under_valgrind(self):
        with tempfile.TemporaryDirectory() as temp:
            program = f"{temp}/tests"
            subprocess.run(["gcc", "-Wall", "-Wextra", "-std=c17", "-g", "-O0", str(C_MODULE / "test_grade_book.c"),
                            str(C_MODULE / "grade_book.c"), "-o", program], check=True, timeout=120)
            run = subprocess.run(["valgrind", "--leak-check=full", "--error-exitcode=9", program], text=True,
                                 capture_output=True, timeout=120)
            self.assertEqual(run.returncode, 0, run.stderr)
            self.assertIn("All heap blocks were freed", run.stderr)


class SolutionComparison(unittest.TestCase):
    @unittest.skipUnless((HERE / "solution").exists(), "falta la solución")
    def test_numbers_match_the_files(self):
        def lines(path):
            return len((HERE / path).read_text(encoding="utf-8").splitlines())

        text = (HERE / "solution" / "comparison.txt").read_text(encoding="utf-8")
        h_c, c_c = lines("lab/c/grade_book.h"), lines("lab/c/grade_book.c")
        h_cpp, c_cpp = lines("solution/grade_book.h"), lines("solution/grade_book.cpp")
        self.assertIn(f"{h_c + c_c} ({h_c} + {c_c})", text)
        self.assertIn(f"{h_cpp + c_cpp} ({h_cpp} + {c_cpp})", text)


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