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

HERE = pathlib.Path(__file__).parent
DATA = os.environ.get("DATA", "../../data")
TERMS = f"{DATA}/terminos.tsv"
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=120,
    )


def shell(command):
    return run("bash", "-c", command)


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

    def test_programs_exit_cleanly(self):
        for name in ["trace_merge", "trace_partition", "trace_search", "comparators"]:
            with self.subTest(program=name):
                self.assertEqual(run(f"build/{name}").returncode, 0)

    def test_merge_tree_for_eight(self):
        """Cada nivel de mezclas copia exactamente 8 elementos."""
        output = run("build/trace_merge").stdout
        self.assertEqual(re.findall(r"(\d+) elementos copiados", output), ["8", "8", "8"])
        self.assertEqual(re.findall(r"(\d+) comparaciones", output), ["4", "6", "7"])

    def test_comparison_counts(self):
        lines = run("build/count_sorts").stdout.splitlines()
        rows = {line.split(maxsplit=1)[0]: line.split() for line in lines[2:7]}
        self.assertEqual(rows["ordenada"][2], str(4096 * 4095 // 2))
        self.assertEqual(rows["iguales"][2], str(4096 * 4095 // 2))
        self.assertEqual(rows["iguales"][3], str(4096 * 4095 // 2))
        self.assertEqual(rows["iguales"][4], str(2 * 4096))
        # En todas las entradas, mergesort hace a lo sumo n log2 n comparaciones.
        for name in ["ordenada", "inversa", "iguales"]:
            self.assertLessEqual(int(rows[name][1]), 4096 * 12)

    def test_stability_rebuilds_the_file(self):
        lines = run("build/stability", TERMS).stdout.splitlines()
        self.assertIn("partido:  1d 2a 3c 3b", lines)
        self.assertIn("líneas: 28039", lines)
        self.assertIn("std::sort con 16 elementos, empates invertidos: 0", lines)
        self.assertNotIn("std::sort con 17 elementos, empates invertidos: 0", lines)
        self.assertIn("std::stable_sort, iguales al archivo: 28039", lines)
        kept = next(line for line in lines if line.startswith("std::sort,"))
        self.assertLess(int(kept.split()[-1]), 28039)

    def test_byte_order_does_not_depend_on_the_sign_of_char(self):
        signed = run("build/byte_order_signed", TERMS).stdout.splitlines()
        unsigned = run("build/byte_order_unsigned", TERMS).stdout.splitlines()
        by_string = "con std::string: cama cazar caña cámara zorro árbol ñandú"
        self.assertIn(by_string, signed)
        self.assertIn(by_string, unsigned)
        self.assertIn("char con signo: sí", signed)
        self.assertIn("char con signo: no", unsigned)
        self.assertIn("char por char : árbol ñandú cámara caña cama cazar zorro", signed)
        self.assertIn("char por char : cama cazar caña cámara zorro árbol ñandú", unsigned)

    def test_binary_search_on_the_dictionary(self):
        lines = run("build/word_search", f"{DATA}/palabras.txt", "casa", "rodillas").stdout.splitlines()
        self.assertEqual(lines[0], "1045757 palabras; ordenado por bytes: sí")
        for line in lines[1:]:
            self.assertLessEqual(int(line.split()[1]), 21)


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)
        return binary

    def test_midpoint_overflow(self):
        binary = self.build("midpoint_overflow")
        done = run("timeout", "2", binary)
        self.assertIn("signed integer overflow", done.stdout)

    def test_stuck_search(self):
        binary = self.build("stuck_search")
        self.assertEqual(run("timeout", "2", binary).returncode, 124)

    def test_bad_comparator(self):
        done = run(self.build("bad_comparator"))
        self.assertNotEqual(done.returncode, 0)
        self.assertIn("heap-buffer-overflow", done.stdout)

    def test_deep_recursion(self):
        binary = self.build("deep_recursion")
        done = shell(f"ulimit -s 1024; {binary} 20000")
        self.assertNotEqual(done.returncode, 0)
        self.assertIn("stack-overflow", done.stdout)
        self.assertEqual(run(binary, "2000").returncode, 0)


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