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

HERE = pathlib.Path(__file__).parent
DATA = os.environ.get("DATA", "../../data")
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", stdin=subprocess.DEVNULL,
        stdout=subprocess.PIPE, stderr=subprocess.STDOUT, timeout=120,
    )


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

    def test_hashes_with_signed_char(self):
        """El mismo resultado aunque char tenga signo, como en x86-64."""
        done = run(*SAN, "-fsigned-char", "test_structures.cpp", "-o", "build/test_structures_signed")
        self.assertEqual(done.returncode, 0, done.stdout)
        done = run("build/test_structures_signed")
        self.assertEqual(done.returncode, 0, done.stdout)

    def test_sum_hash_uses_few_values(self):
        lines = run("build/hash_compare", f"{DATA}/terminos.tsv").stdout.splitlines()
        rows = [line.split() for line in lines[2:]]
        by_m = {}
        for row in rows:
            by_m.setdefault(row[1], {})[row[0]] = row
        # Más casillas no ayudan a la suma: se queda con los mismos valores.
        self.assertEqual(by_m["32768"]["suma"][3], by_m["1048576"]["suma"][3])
        self.assertGreater(int(by_m["1048576"]["suma"][4]), 20 * int(by_m["1048576"]["fnv1a"][4]))

    def test_load_experiment_follows_the_formula(self):
        rows = [line.split() for line in run("build/load_experiment", f"{DATA}/palabras.txt").stdout.splitlines()[1:]]
        self.assertEqual(rows[0][:3], ["100", "300", "3.00"])
        for row in rows:
            alpha, hit, hit_formula, miss = float(row[2]), float(row[3]), float(row[4]), float(row[5])
            with self.subTest(alpha=alpha):
                self.assertLess(abs(hit - hit_formula), 0.25 + 0.05 * alpha)
                self.assertLess(abs(miss - alpha), 0.25 + 0.1 * alpha)

    def test_naive_copy_loses_about_half(self):
        lines = run("build/naive_copy", f"{DATA}/terminos.tsv").stdout.splitlines()
        counts = [line.split(":")[1].split() for line in lines if "encontradas" in line]
        total = int(counts[0][2])
        self.assertEqual(int(counts[0][0]), total)
        self.assertEqual(int(counts[2][0]), total)
        self.assertLess(abs(int(counts[1][0]) / total - 0.5), 0.05)

    def test_flood_puts_every_key_in_one_slot(self):
        lines = run("build/flood").stdout.splitlines()
        self.assertTrue(any("elegidas" in line and "m=4096" in line and "cadena  4000" in line for line in lines))
        self.assertTrue(any("elegidas" in line and "m=8192" in line and "cadena  4000" in line for line in lines))
        self.assertTrue(any("elegidas" in line and "tiene  4000" in line for line in lines))

    def test_hash_is_stable_within_this_library(self):
        lines = [run("build/library_demo").stdout.splitlines()[-1] for _ in range(2)]
        self.assertEqual(lines[0], lines[1])

    def test_programs_exit_cleanly(self):
        for name in ["hash_demo", "fnv_trace", "chain_trace", "growth_count", "library_demo"]:
            with self.subTest(program=name):
                self.assertEqual(run(f"build/{name}").returncode, 0)


class DeliberateErrors(unittest.TestCase):
    def test_mutated_key(self):
        run(*SAN, "errors/mutated_key.cpp", "-o", "build/mutated_key")
        done = run("build/mutated_key")
        self.assertEqual(done.returncode, 0, done.stdout)
        self.assertIn('get("lima"): no está', done.stdout)
        self.assertIn("invariantes: 0", done.stdout)

    def test_modulo_zero(self):
        run(*SAN, "errors/modulo_zero.cpp", "-o", "build/modulo_zero")
        done = run("build/modulo_zero")
        self.assertIn("division by zero", done.stdout)

    def test_const_key_does_not_compile(self):
        done = run("g++", "-std=c++20", "-c", "errors/const_key.cpp", "-o", "build/const_key.o")
        self.assertNotEqual(done.returncode, 0)
        self.assertIn("no match for 'operator='", done.stdout.replace("‘", "'").replace("’", "'"))


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