"""Pruebas de los programas en C: las cifras son las que aparecen en los apuntes."""
import re
import struct
import subprocess
import tempfile
import unittest
from pathlib import Path

HERE = Path(__file__).resolve().parent


def run(*args):
    return subprocess.run([str(HERE / args[0]), *map(str, args[1:])], capture_output=True, text=True, check=False)


def number_after(label, text):
    match = re.search(re.escape(label) + r"\s*=?\s*([\d.e+-]+)", text)
    return float(match.group(1))


class DelayTest(unittest.TestCase):
    def test_lima_miami(self):
        out = run("delay", 1500, 100, 4200).stdout
        self.assertAlmostEqual(number_after("transmision L/R", out), 0.120, places=3)
        self.assertAlmostEqual(number_after("propagacion d/s", out), 21.0, places=3)
        self.assertIn("domina la propagacion", out)

    def test_review_question(self):
        out = run("delay", 1000, 10, 2000).stdout
        self.assertAlmostEqual(number_after("transmision L/R", out), 0.8, places=3)
        self.assertAlmostEqual(number_after("propagacion d/s", out), 10.0, places=3)

    def test_short_slow_link_transmission_dominates(self):
        self.assertIn("domina la transmision", run("delay", 1500, 1, 1).stdout)

    def test_custom_speed(self):
        out = run("delay", 1000, 10, 3000, "3e8").stdout
        self.assertAlmostEqual(number_after("propagacion d/s", out), 10.0, places=3)

    def test_rejects_bad_input(self):
        self.assertNotEqual(run("delay", 1500, 0, 10).returncode, 0)
        self.assertNotEqual(run("delay", "mucho", 10, 10).returncode, 0)
        self.assertNotEqual(run("delay").returncode, 0)


class BottleneckTest(unittest.TestCase):
    def test_slowest_link_sets_time(self):
        out = run("bottleneck", 1000, 1000, 50, 200).stdout
        self.assertIn("enlace 2, a 50 Mbps", out)
        self.assertIn("160.00 s", out)

    def test_order_does_not_matter(self):
        for rates in [(50, 200, 1000), (1000, 200, 50)]:
            self.assertIn("160.00 s", run("bottleneck", 1000, *rates).stdout)

    def test_rejects_bad_input(self):
        self.assertNotEqual(run("bottleneck", 1000).returncode, 0)
        self.assertNotEqual(run("bottleneck", 1000, -5).returncode, 0)


class MultiplexTest(unittest.TestCase):
    def tail(self, users, p=0.1):
        return number_after("activos a la vez)", run("multiplex", 1000, 100, p, users).stdout)

    def test_ten_users_fit_with_circuits(self):
        self.assertIn("caben: 10 usuarios", run("multiplex", 1000, 100, 0.1, 40).stdout)

    def test_ten_users_never_overflow(self):
        self.assertEqual(self.tail(10), 0)

    def test_known_tails(self):
        # Calculadas aparte, sumando la binomial con enteros exactos.
        self.assertAlmostEqual(self.tail(20) / 7.089e-07, 1, places=2)
        self.assertAlmostEqual(self.tail(50) / 9.355e-03, 1, places=2)

    def test_more_users_more_risk(self):
        tails = [self.tail(n) for n in (20, 30, 40, 50, 100)]
        self.assertEqual(tails, sorted(tails))

    def test_rejects_bad_input(self):
        self.assertNotEqual(run("multiplex", 1000, 100, 1.5, 10).returncode, 0)
        self.assertNotEqual(run("multiplex", 1000, 100, 0, 10).returncode, 0)


class QueueSimTest(unittest.TestCase):
    def sim(self, *args):
        out = run("queue_sim", *args).stdout
        return (number_after("retardo_medio", out), number_after("retardo_max", out),
                number_after("perdida", out))

    def test_random_arrivals_match_theory(self):
        # Fórmula de la cola M/D/1: espera media = rho / (2 (1 - rho)), en unidades de L/R.
        for rho in (0.3, 0.5, 0.7, 0.8):
            mean, _, _ = self.sim("poisson", rho)
            self.assertAlmostEqual(mean / (rho / (2 * (1 - rho))), 1, delta=0.1)

    def test_delay_grows_with_intensity(self):
        means = [self.sim("poisson", rho)[0] for rho in (0.1, 0.5, 0.8, 0.9, 0.95)]
        self.assertEqual(means, sorted(means))

    def test_periodic_arrivals_do_not_queue(self):
        for rho in (0.5, 0.9, 0.99):
            self.assertEqual(self.sim("periodic", rho)[0], 0)

    def test_bursts_queue_at_low_intensity(self):
        mean, largest, _ = self.sim("burst:10", 0.5)
        self.assertAlmostEqual(mean, 4.5)  # los 10 de la ráfaga esperan 0, 1, ..., 9
        self.assertAlmostEqual(largest, 9)

    def test_above_one_without_limit_delay_keeps_growing(self):
        short = self.sim("periodic", 1.2, 0, 100000)[0]
        long = self.sim("periodic", 1.2, 0, 1000000)[0]
        self.assertAlmostEqual(long / short, 10, delta=0.5)

    def test_above_one_with_buffer_loses_the_excess(self):
        mean, largest, loss = self.sim("poisson", 1.2, 100)
        self.assertAlmostEqual(loss, 100 * (1 - 1 / 1.2), delta=1.5)
        self.assertLess(largest, 100)
        self.assertGreater(mean, 80)

    def test_same_seed_same_result(self):
        self.assertEqual(run("queue_sim", "poisson", 0.7).stdout, run("queue_sim", "poisson", 0.7).stdout)

    def test_rejects_bad_input(self):
        self.assertNotEqual(run("queue_sim", "poisson", 0).returncode, 0)
        self.assertNotEqual(run("queue_sim", "rara", 0.5).returncode, 0)


def build_ipv4_tcp(payload=b"hola", options=b""):
    tcp = struct.pack("!HHIIBBHHH", 40000, 80, 1, 2, (5 + len(options) // 4) << 4, 0x18, 1000, 0, 0) + options
    total = 20 + len(tcp) + len(payload)
    ip = struct.pack("!BBHHHBBH4B4B", 0x45, 0, total, 1, 0, 64, 6, 0, 10, 1, 0, 2, 10, 2, 0, 2)
    return ip + tcp + payload


def build_ipv4_udp(payload=b""):
    udp = struct.pack("!HHHH", 5000, 53, 8 + len(payload), 0)
    ip = struct.pack("!BBHHHBBH4B4B", 0x45, 0, 28 + len(payload), 1, 0, 9, 17, 0, 10, 0, 0, 1, 10, 0, 0, 2)
    return ip + udp + payload


def pcap_bytes(linktype, frames, endian="<"):
    data = struct.pack(endian + "IHHIIII", 0xa1b2c3d4, 2, 4, 0, 0, 65535, linktype)
    for frame in frames:
        data += struct.pack(endian + "IIII", 0, 0, len(frame), len(frame)) + frame
    return data


class LayersTest(unittest.TestCase):
    def layers(self, data):
        with tempfile.NamedTemporaryFile(suffix=".pcap") as file:
            file.write(data)
            file.flush()
            return run("layers", file.name)

    def test_ethernet(self):
        frame = bytes(12) + b"\x08\x00" + build_ipv4_tcp()
        line = self.layers(pcap_bytes(1, [frame])).stdout
        self.assertRegex(line, r"total=58\s+enlace=14 ip=20 tcp=20 app=4 ttl=64 10\.1\.0\.2 > 10\.2\.0\.2 \[PA\]")

    def test_tcp_options_count_in_the_header(self):
        frame = bytes(12) + b"\x08\x00" + build_ipv4_tcp(options=bytes(12))
        self.assertIn("tcp=32 app=4", self.layers(pcap_bytes(1, [frame])).stdout)

    def test_cooked_capture_v1(self):
        frame = bytes(14) + b"\x08\x00" + build_ipv4_tcp()
        self.assertIn("enlace=16 ip=20 tcp=20", self.layers(pcap_bytes(113, [frame])).stdout)

    def test_cooked_capture_v2(self):
        frame = b"\x08\x00" + bytes(18) + build_ipv4_tcp()
        self.assertIn("enlace=20 ip=20 tcp=20", self.layers(pcap_bytes(276, [frame])).stdout)

    def test_short_udp_packet(self):
        frame = bytes(12) + b"\x08\x00" + build_ipv4_udp()
        self.assertIn("udp=8 app=0 ttl=9", self.layers(pcap_bytes(1, [frame])).stdout)

    def test_ethernet_padding_is_not_application_data(self):
        frame = bytes(12) + b"\x08\x00" + build_ipv4_udp() + bytes(18)
        self.assertIn("udp=8 app=0", self.layers(pcap_bytes(1, [frame])).stdout)

    def test_big_endian_capture(self):
        frame = bytes(12) + b"\x08\x00" + build_ipv4_tcp()
        self.assertIn("tcp=20 app=4", self.layers(pcap_bytes(1, [frame], endian=">")).stdout)

    def test_not_ipv4(self):
        frame = bytes(12) + b"\x08\x06" + bytes(28)
        self.assertIn("0x0806", self.layers(pcap_bytes(1, [frame])).stdout)

    def test_truncated_ipv4(self):
        frame = bytes(12) + b"\x08\x00" + bytes(5)
        result = self.layers(pcap_bytes(1, [frame]))
        self.assertEqual(result.returncode, 0)
        self.assertIn("recortado", result.stdout)

    def test_rejects_other_files(self):
        self.assertNotEqual(self.layers(b"esto no es una captura, aunque sea largo").returncode, 0)
        self.assertNotEqual(self.layers(b"").returncode, 0)
        self.assertNotEqual(run("layers", "/no/existe.pcap").returncode, 0)

    def test_rejects_record_cut_in_half(self):
        frame = bytes(12) + b"\x08\x00" + build_ipv4_tcp()
        self.assertNotEqual(self.layers(pcap_bytes(1, [frame])[:-5]).returncode, 0)


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