"""Pruebas de los simuladores: no necesitan red."""
import os
import re
import subprocess
import unittest

import tail_rounds
from netlab import HERE


def tool(*args):
    done = subprocess.run([os.path.join(HERE, "bin", args[0]), *args[1:]], capture_output=True,
                          text=True, timeout=120)
    return done.returncode, done.stdout + done.stderr


class EcmpSim(unittest.TestCase):
    def elephant(self, paths, flows):
        _, out = tool("ecmp_sim", "elephant", str(paths), str(flows), "200000")
        found = re.search(r"sin choques en ([\d.]+) %.*promedio ([\d.]+); throughput total (\d+) %", out)
        return float(found.group(1)), float(found.group(2)), int(found.group(3))

    def test_two_elephants_on_two_paths_match_the_closed_form(self):
        # P(sin choque) = 1/2; caminos ocupados en promedio = 1,5; o sea 75 % del ideal.
        clean, used, share = self.elephant(2, 2)
        self.assertAlmostEqual(clean, 50.0, delta=1.0)
        self.assertAlmostEqual(used, 1.5, delta=0.02)
        self.assertEqual(share, 75)

    def test_four_elephants_on_four_paths_match_the_closed_form(self):
        # P(sin choque) = 4!/4^4 = 9,375 %; ocupados = 4 × (1 - (3/4)^4) ≈ 2,73.
        clean, used, _ = self.elephant(4, 4)
        self.assertAlmostEqual(clean, 9.375, delta=0.5)
        self.assertAlmostEqual(used, 2.734, delta=0.03)

    def test_more_flows_balance_better(self):
        ratio = {}
        for flows in (8, 64, 6400):
            _, out = tool("ecmp_sim", "balance", "2", str(flows), "5000")
            ratio[flows] = float(re.search(r"lleva ([\d.]+) veces", out).group(1))
        self.assertGreater(ratio[8], ratio[64])
        self.assertGreater(ratio[64], ratio[6400])
        self.assertLess(ratio[6400], 1.05)

    def test_rehash_moves_less_with_better_schemes(self):
        _, out = tool("ecmp_sim", "rehash", "8", "1000000")
        dead = float(re.search(r"en él: ([\d.]+) %", out).group(1))
        modulo = float(re.search(r"módulo: +se mueven ([\d.]+)", out).group(1))
        threshold = float(re.search(r"hash-threshold: +se mueven ([\d.]+)", out).group(1))
        rendezvous = float(re.search(r"rendezvous: +se mueven ([\d.]+)", out).group(1))
        self.assertAlmostEqual(dead, 12.5, delta=0.5)
        self.assertGreater(modulo, threshold)
        self.assertGreater(threshold, rendezvous)
        self.assertAlmostEqual(rendezvous, dead, delta=0.5)


class DcfSim(unittest.TestCase):
    def useful(self, *args):
        _, out = tool("dcf_sim", *args)
        found = re.search(r"tiempo útil\s+([\d.]+) %; intentos que chocan\s+([\d.]+) %", out)
        return float(found.group(1)), float(found.group(2))

    def test_lone_station_never_collides(self):
        useful, collisions = self.useful("shared", "1", "1000000")
        self.assertEqual(collisions, 0.0)
        # Un backoff promedio de 7,5 ranuras por cada trama de 150: 150 / 157,5.
        self.assertAlmostEqual(useful, 95.2, delta=0.5)

    def test_more_stations_collide_more(self):
        few = self.useful("shared", "2", "5000000")
        many = self.useful("shared", "50", "5000000")
        self.assertGreater(many[1], few[1])
        self.assertLess(many[0], few[0])

    def test_rts_rescues_hidden_stations(self):
        without = self.useful("hidden", "5000000")
        with_rts = self.useful("hidden_rts", "5000000")
        self.assertGreater(with_rts[0], without[0] + 20)
        self.assertLess(with_rts[1], without[1])


class TailRounds(unittest.TestCase):
    def test_simulation_matches_the_exact_probability(self):
        _, _, fraction = tail_rounds.simulate(10, 0.01, 1, 100, 50000)
        self.assertAlmostEqual(fraction, tail_rounds.expected_slow_fraction(10, 0.01), delta=0.01)

    def test_the_round_is_worse_than_the_average_worker(self):
        per_worker, per_round, _ = tail_rounds.simulate(1000, 0.01, 1, 100, 2000)
        self.assertLess(per_worker, 3)
        self.assertGreater(per_round, 95)
