"""Corre los ejemplos de la semana y comprueba lo que se puede contar: nunca tiempos."""
import os
import pathlib
import subprocess
import unittest

HERE = pathlib.Path(__file__).parent
DATA = os.environ.get("DATA", "../../data")
MIRAFLORES = f"{DATA}/mapas/miraflores.txt"
MAPS = [MIRAFLORES, f"{DATA}/mapas/lima-sur.txt", f"{DATA}/mapas/lima.txt"]
SAN = ["g++", "-Wall", "-Wextra", "-std=c++20", "-g", "-fsanitize=address,undefined"]


def run(*command):
    return subprocess.run(
        list(command), cwd=HERE, text=True, encoding="utf-8", errors="replace", stdin=subprocess.DEVNULL,
        stdout=subprocess.PIPE, stderr=subprocess.STDOUT, timeout=300,
    )


class Examples(unittest.TestCase):
    def test_structure_tests_pass(self):
        done = run("build/test_structures")
        self.assertEqual(done.returncode, 0, done.stdout)

    def test_programs_exit_cleanly(self):
        for name in ["bfs_trace", "dfs_demo", "representations", "uf_demo", "uf_costs"]:
            with self.subTest(program=name):
                done = run(f"build/{name}")
                self.assertEqual(done.returncode, 0, done.stdout)

    def test_bfs_reads_each_list_once_on_real_maps(self):
        """Cuenta V nodos sacados y 2E entradas leídas. Los tiempos no se comparan."""
        lines = run("build/bfs_scaling", *MAPS).stdout.splitlines()
        for row in lines[1:4]:
            nodes, edges, pulled, reads, *_ = row.split()
            self.assertEqual(pulled, nodes)
            self.assertEqual(int(reads), 2 * int(edges))
        self.assertIn("matriz en el primer mapa: 5026564 celdas leidas", lines[4])

    def test_review_question_memory(self):
        lines = run("build/representations").stdout.splitlines()
        self.assertIn("matriz, bytes: 1000032", lines)
        self.assertTrue(any(line.startswith("listas, entradas: 6000,") for line in lines))

    def test_sequence_without_improvements_is_quadratic(self):
        """n(n - 1)/2 saltos exactos en la cadena: la fórmula de los apuntes."""
        lines = run("build/uf_costs").stdout.splitlines()
        rows = [line.split() for line in lines if line.startswith("sin mejoras")]
        for row in rows:
            n, hops = int(row[2]), int(row[4])
            self.assertEqual(hops, n * (n - 1) // 2)

    def test_single_find_can_cost_log_n(self):
        lines = run("build/uf_costs").stdout.splitlines()
        start = next(i for i, line in enumerate(lines) if line.startswith("Un solo find"))
        rows = [line.split() for line in lines[start + 2:]]
        self.assertEqual(len(rows), 4)
        for n, log_n, first, second in rows:
            self.assertEqual(int(n), 2 ** int(log_n))
            self.assertEqual(first, log_n)
            self.assertEqual(second, "1")

    def test_online_connectivity_answers_agree(self):
        done = run("build/online_connectivity", MIRAFLORES)
        self.assertEqual(done.returncode, 0, done.stdout)
        self.assertNotIn("no coinciden", done.stdout)

    def test_map_is_one_component_and_bfs_is_not_longer(self):
        lines = run("build/map_tour", MIRAFLORES, "0", "2000").stdout.splitlines()
        self.assertIn("nodos: 2242, aristas: 3474", lines)
        self.assertIn("componentes: 1", lines)
        by_bfs = next(line for line in lines if line.startswith("BFS:"))
        by_dfs = next(line for line in lines if line.startswith("DFS:"))
        self.assertLess(int(by_bfs.split()[1]), int(by_dfs.split()[1]))


class DeliberateErrors(unittest.TestCase):
    def build(self, name):
        binary = f"build/{name}"
        done = run(*SAN, f"errors/{name}.cpp", "-o", binary)
        self.assertEqual(done.returncode, 0, done.stdout)
        self.assertNotIn("warning", done.stdout)
        return binary

    def test_marking_on_pop_breaks_distance_and_queue(self):
        lines = run(self.build("bfs_mark_on_pop")).stdout.splitlines()
        self.assertEqual(lines[1], "  marcando al sacar: 2")
        self.assertEqual(lines[2], "  e ignorando los ya marcados: 2")
        for row in lines[4:]:
            k, _, edges, entries, skipping = (int(field) for field in row.split())
            self.assertEqual(entries, 2 ** (k + 2) - 3)
            self.assertLessEqual(skipping, 2 * edges + 1)

    def test_no_marks_never_empties_the_queue(self):
        done = run(self.build("no_marks"))
        self.assertIn("cortado tras 20 pasos", done.stdout)

    def test_recursive_dfs_overflows(self):
        binary = self.build("recursive_dfs_overflow")
        self.assertEqual(run(binary, "1000").returncode, 0)
        done = run(binary, "1000000")
        self.assertNotEqual(done.returncode, 0)
        self.assertIn("con pila propia: 1000000 nodos", done.stdout)
        self.assertIn("stack-overflow", done.stdout)

    def test_undirected_check_lies_on_directed_graph(self):
        done = run(self.build("directed_false_cycle"))
        self.assertIn("revision no dirigida: hay ciclo", done.stdout)
        self.assertIn("revision con tres colores: sin ciclo", done.stdout)

    def test_unite_without_find_loses_a_connection(self):
        done = run(self.build("unite_without_find"))
        self.assertIn("0 y 1: separados", done.stdout)


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