"""Comprueba lo que los apuntes afirman sobre los programas de la semana 1."""
import pathlib
import subprocess
import tempfile
import unittest

HERE = pathlib.Path(__file__).parent
BUILD = HERE / "build"
FLAGS = ["-Wall", "-Wextra", "-std=c17"]


def run(program, stdin=""):
    """Ejecuta un programa de build/; devuelve su salida (con errores) y su estado."""
    done = subprocess.run(
        [str(BUILD / program)], input=stdin, text=True,
        stdout=subprocess.PIPE, stderr=subprocess.STDOUT, timeout=10,
    )
    return done.stdout, done.returncode


def compile_messages(source, *extra):
    """Compila un programa de errors/; devuelve los mensajes y el estado."""
    out = pathlib.Path(tempfile.mkdtemp()) / "demo"
    done = subprocess.run(
        ["gcc", *FLAGS, *extra, source, "-o", str(out)], cwd=HERE,
        text=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
    )
    return done.stdout, done.returncode


def fewest_coins(cents):
    """Otra forma de contar las monedas, para comparar: probar todas las combinaciones."""
    best = [0] + [None] * cents
    for amount in range(10, cents + 1, 10):
        best[amount] = 1 + min(best[amount - coin] for coin in (10, 20, 50, 100, 200, 500)
                               if coin <= amount)
    return best[cents]


class CleanCompile(unittest.TestCase):
    def test_examples_have_no_warnings(self):
        for source in sorted(HERE.glob("*.c")):
            with self.subTest(source=source.name):
                done = subprocess.run(
                    ["gcc", *FLAGS, "-Werror", "-fsyntax-only", str(source)],
                    text=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
                )
                self.assertEqual(done.returncode, 0, done.stdout)


class TypesAndOperators(unittest.TestCase):
    def test_division(self):
        self.assertEqual(run("division"), ("3 3.5\n0.0 0.5\n3\n", 0))

    def test_types(self):
        output, status = run("types")
        self.assertEqual(status, 0)
        self.assertIn("7 tiene el código 55 y vale 7\n", output)
        self.assertIn("0.30000000000000004\nx no es igual a 0.3\n", output)

    def test_overflow_is_reported(self):
        self.assertIn("signed integer overflow", run("overflow")[0])

    def test_operators(self):
        self.assertEqual(run("operators"), ("3 -3\n1 -1\n14 20\nSin datos o promedio bajo\n", 0))


class Control(unittest.TestCase):
    def test_loops(self):
        self.assertEqual(run("loops"), ("3\n6\n4 1 2\n", 0))

    def test_off_by_one(self):
        self.assertEqual(run("off_by_one"), ("2\n3\n", 0))

    def test_grades_average(self):
        self.assertEqual(run("grades_average", "12 8 15\n"), ("3 notas, promedio 11.67\n", 0))
        self.assertEqual(run("grades_average", ""), ("No hay notas\n", 1))
        self.assertEqual(run("grades_average", "12 abc 15\n"), ("1 notas, promedio 12.00\n", 0))


class Functions(unittest.TestCase):
    def test_by_value(self):
        self.assertEqual(run("by_value"), (
            "En add_one, x vale 6\nEn main, x vale 5\n"
            "En try_swap, a = 2 y b = 1\nEn main, first = 1 y second = 2\n", 0))

    def test_double_argument_has_no_warning(self):
        self.assertEqual(compile_messages("errors/double_argument.c"), ("", 0))


class Change(unittest.TestCase):
    def test_both_versions_agree_with_the_fewest_coins(self):
        for cents in range(0, 2010, 10):
            expected = f"Para {cents} céntimos hacen falta {fewest_coins(cents)} monedas\n"
            for program in ["change", "change_print"]:
                with self.subTest(program=program, cents=cents):
                    self.assertEqual(run(program, str(cents)), (expected, 0))

    def test_amounts_that_cannot_be_given(self):
        for cents in [15, -10, 7, -5]:
            expected = f"No se puede dar {cents} céntimos en monedas\n"
            for program in ["change", "change_print"]:
                with self.subTest(program=program, cents=cents):
                    self.assertEqual(run(program, str(cents)), (expected, 1))

    def test_bad_input(self):
        for program in ["change", "change_print"]:
            self.assertEqual(run(program, "abc"), ("Entrada inválida\n", 1))

    def test_change_tests_pass(self):
        self.assertEqual(run("change_tests"), ("Todas las pruebas pasaron\n", 0))

    def test_change_bug_passes_the_normal_case_and_fails_the_edge(self):
        output, status = run("change_bug")
        self.assertNotEqual(status, 0)
        self.assertIn("Assertion `coins_needed(500) == 1' failed", output)


class Exercises(unittest.TestCase):
    def test_exercise_mutation_is_not_caught(self):
        # Con > 100 todas las pruebas de change_tests.c pasan, y aun así la función falla con 100.
        text = (HERE / "change_tests.c").read_text(encoding="utf-8")
        self.assertIn("cents >= 100", text)
        with tempfile.TemporaryDirectory() as folder:
            mutated = pathlib.Path(folder) / "mutated.c"
            mutated.write_text(text.replace("cents >= 100", "cents > 100"), encoding="utf-8")
            binary = pathlib.Path(folder) / "mutated"
            subprocess.run(["gcc", *FLAGS, str(mutated), "-o", str(binary)], check=True)
            done = subprocess.run([str(binary)], text=True, stdout=subprocess.PIPE,
                                  stderr=subprocess.STDOUT)
            self.assertEqual((done.stdout, done.returncode), ("Todas las pruebas pasaron\n", 0))
            mutated.write_text(text.replace("cents >= 100", "cents > 100").replace(
                "assert(coins_needed(380) == 5);", "assert(coins_needed(100) == 1);"), encoding="utf-8")
            subprocess.run(["gcc", *FLAGS, str(mutated), "-o", str(binary)], check=True)
            done = subprocess.run([str(binary)], text=True, stdout=subprocess.PIPE,
                                  stderr=subprocess.STDOUT)
            self.assertNotEqual(done.returncode, 0)

    def test_collatz_numbers_of_the_exercises(self):
        def peak(n):
            highest = n
            while n != 1:
                n = n // 2 if n % 2 == 0 else 3 * n + 1
                highest = max(highest, n)
            return highest

        self.assertLessEqual(max(peak(n) for n in range(1, 10000)), 2147483647)
        self.assertLessEqual(max(peak(n) for n in range(1, 113383)), 2147483647)
        self.assertGreater(peak(113383), 2147483647)
        self.assertEqual(peak(6), 16)


class Warnings(unittest.TestCase):
    def test_flags_report_the_mistakes(self):
        cases = {
            "errors/missing_return.c": "-Wreturn-type",
            "errors/assign_in_if.c": "-Wparentheses",
            "errors/chained_compare.c": "-Wparentheses",
            "errors/wrong_format.c": "-Wformat",
            "errors/unused_parameter.c": "-Wunused-parameter",
        }
        for source, flag in cases.items():
            with self.subTest(source=source):
                messages, status = compile_messages(source)
                self.assertIn(flag, messages)
                self.assertEqual(status, 0, "una advertencia no detiene al compilador")

    def test_uninitialized_needs_optimization_to_warn(self):
        self.assertEqual(compile_messages("errors/uninitialized.c"), ("", 0))
        messages, status = compile_messages("errors/uninitialized.c", "-O2")
        self.assertIn("-Wuninitialized", messages)
        self.assertEqual(status, 0)

    def test_werror_stops_the_build(self):
        messages, status = compile_messages("errors/missing_return.c", "-Werror")
        self.assertNotEqual(status, 0)
        self.assertIn("all warnings being treated as errors", messages)

    def test_wall_alone_misses_the_unused_parameter(self):
        done = subprocess.run(
            ["gcc", "-Wall", "-std=c17", "-fsyntax-only", "errors/unused_parameter.c"],
            cwd=HERE, text=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
        )
        self.assertEqual(done.stdout, "")


class GitSession(unittest.TestCase):
    def test_commands_of_the_notes_work(self):
        def git(*arguments, cwd):
            return subprocess.run(["git", *arguments], cwd=cwd, text=True, check=True,
                                  stdout=subprocess.PIPE, stderr=subprocess.STDOUT).stdout

        with tempfile.TemporaryDirectory() as folder:
            base = pathlib.Path(folder)
            original = base / "original"
            original.mkdir()
            git("init", "-q", "-b", "main", cwd=original)
            git("config", "user.name", "Ana", cwd=original)
            git("config", "user.email", "ana@example.com", cwd=original)
            (original / "notas.txt").write_text("12\n", encoding="utf-8")
            git("add", "notas.txt", cwd=original)
            git("commit", "-q", "-m", "Primera nota", cwd=original)
            git("clone", "-q", str(original), "copia", cwd=base)
            copy = base / "copia"
            self.assertEqual((copy / "notas.txt").read_text(encoding="utf-8"), "12\n")
            git("config", "user.name", "Ana", cwd=copy)
            git("config", "user.email", "ana@example.com", cwd=copy)
            (copy / "notas.txt").write_text("12\n15\n", encoding="utf-8")
            git("add", "notas.txt", cwd=copy)
            git("commit", "-q", "-m", "Agrega una nota", cwd=copy)
            self.assertEqual(len(git("log", "--oneline", cwd=copy).splitlines()), 2)
            self.assertEqual(len(git("log", "--oneline", cwd=original).splitlines()), 1)


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