"""Comprueba en la topología qué estado guarda cada nodo y qué sobrevive."""
import re
import subprocess
import time
import unittest

from netlab import HERE

TOPOLOGY = "fate.clab.yml"
PREFIX = "clab-w02-fate-"
H2_ADDR = "10.2.4.10"


def sh(*cmd, timeout=120, check=True):
    return subprocess.run(cmd, capture_output=True, text=True, timeout=timeout, check=check,
                          cwd=HERE)


def node(name, *cmd, timeout=60, check=True):
    return sh("docker", "exec", PREFIX + name, *cmd, timeout=timeout, check=check)


def start(name, *cmd):
    return subprocess.Popen(["docker", "exec", PREFIX + name, *cmd],
                            stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True)


def wait_for_route(via, timeout=90):
    deadline = time.monotonic() + timeout
    while time.monotonic() < deadline:
        out = node("r1", "ip", "route", "show", "10.2.4.0/24", check=False).stdout
        if via in out:
            return
        time.sleep(1)
    raise AssertionError(f"r1 no aprendió la ruta por {via}")


def hops():
    out = node("h1", "traceroute", "-n", "-q", "1", "-w", "2", H2_ADDR).stdout
    return re.findall(r"^\s*\d+\s+(\S+)", out, re.M)


class Fate(unittest.TestCase):
    @classmethod
    def setUpClass(cls):
        sh("containerlab", "deploy", "--reconfigure", "-t", TOPOLOGY, timeout=240)
        cls.addClassCleanup(cls.destroy)
        wait_for_route("10.2.12.2")

    @staticmethod
    def destroy():
        sh("containerlab", "destroy", "-t", TOPOLOGY, "--cleanup", timeout=120, check=False)
        sh("docker", "network", "rm", "redes-w02", check=False)

    @staticmethod
    def stop(process):
        process.kill()
        process.communicate()

    def tcp_transfer(self, port, size):
        receiver = start("h2", "/lab/chunk_recv", str(port))
        time.sleep(0.5)
        sent = node("h1", "/lab/chunk_send", H2_ADDR, str(port), str(size), "1")
        self.assertIn(f"{size} bytes", sent.stdout)
        out, _ = receiver.communicate(timeout=60)
        return out

    def test_1_path_goes_through_r2(self):
        self.assertEqual(hops(), ["10.2.1.1", "10.2.12.2", "10.2.24.4", H2_ADDR])

    def test_2_mss_follows_the_link_mtu(self):
        capture = start("r1", "timeout", "8", "tcpdump", "-ni", "eth1", "-c", "1",
                        f"tcp[tcpflags] & tcp-syn != 0 and dst host {H2_ADDR}")
        time.sleep(1.5)
        self.tcp_transfer(9010, 1000)
        out, _ = capture.communicate(timeout=30)
        self.assertIn("mss 1460", out)

    def test_3_tcp_is_complete_and_udp_is_not_on_a_lossy_link(self):
        node("r1", "tc", "qdisc", "add", "dev", "eth2", "root", "netem",
             "delay", "5ms", "3ms", "loss", "10%")
        self.addCleanup(node, "r1", "tc", "qdisc", "del", "dev", "eth2", "root", check=False)
        out = self.tcp_transfer(9011, 102400)
        self.assertRegex(out, r"recv: \d+ llamadas, 102400 bytes")
        self.assertIn("fuera de lugar: 0", out)

        receiver = start("h2", "/lab/seq_recv", "9012", "1000", "2000")
        time.sleep(0.5)
        node("h1", "/lab/seq_send", H2_ADDR, "9012", "1000", "200", "1000")
        out, _ = receiver.communicate(timeout=60)
        lost = int(re.search(r"perdidos: (\d+)", out).group(1))
        reordered = int(re.search(r"fuera de orden: (\d+)", out).group(1))
        self.assertGreater(lost, 30, out)
        self.assertLess(lost, 250, out)
        self.assertGreater(reordered, 0, out)
        self.assertIn("200 bytes, 0 distintos", out)

    def test_4_router_has_no_socket_of_the_connection(self):
        receiver = start("h2", "/lab/chunk_recv", "9013")
        time.sleep(0.5)
        sender = start("h1", "/lab/chunk_send", "-g", "50", H2_ADDR, "9013", "1000", "60")
        time.sleep(1)
        at_host = node("h1", "ss", "-tn").stdout
        at_router = node("r1", "ss", "-tn").stdout
        sender.communicate(timeout=30)
        receiver.communicate(timeout=30)
        self.assertIn(f"{H2_ADDR}:9013", at_host)
        self.assertNotIn(H2_ADDR, at_router)

    def test_5_connection_survives_a_dead_router(self):
        # Después de esta prueba r2 sigue caído: por eso las pruebas van numeradas.
        receiver = start("h2", "/lab/chunk_recv", "9014")
        time.sleep(0.5)
        sender = start("h1", "/lab/chunk_send", "-g", "50", H2_ADDR, "9014", "1000", "120")
        time.sleep(2)
        sh("docker", "kill", PREFIX + "r2")
        out_send, _ = sender.communicate(timeout=60)
        out_recv, _ = receiver.communicate(timeout=60)
        self.assertEqual(sender.returncode, 0, out_send)
        self.assertIn("120000 bytes", out_recv)
        self.assertIn("fuera de lugar: 0", out_recv)
        wait_for_route("10.2.13.3", timeout=20)
        self.assertIn("10.2.13.3", hops())

    def test_6_connection_dies_with_its_end_process(self):
        receiver = start("h2", "/lab/chunk_recv", "9015")
        time.sleep(0.5)
        sender = start("h1", "/lab/chunk_send", "-g", "50", H2_ADDR, "9015", "1000", "200")
        time.sleep(1)
        node("h2", "sh", "-c", "kill -9 $(pidof chunk_recv)")
        receiver.communicate(timeout=10)
        out, _ = sender.communicate(timeout=30)
        self.assertNotEqual(sender.returncode, 0)
        self.assertRegex(out, "Broken pipe|Connection reset")

    def test_7_silent_end_host_leaves_the_sender_retransmitting(self):
        receiver = start("h2", "/lab/chunk_recv", "9016")
        time.sleep(0.5)
        sender = start("h1", "/lab/chunk_send", "-g", "50", H2_ADDR, "9016", "1000", "400")
        self.addCleanup(self.stop, sender)
        self.addCleanup(self.stop, receiver)
        time.sleep(1)
        # Imita una máquina colgada: h2 deja de responder, sin avisar a nadie.
        node("h2", "nft", "add table inet dead")
        node("h2", "nft", "add chain inet dead in { type filter hook input priority 0; policy drop; }")
        time.sleep(4)
        out = node("h1", "ss", "-tno", f"dst {H2_ADDR}").stdout
        self.assertIsNone(sender.poll(), "send no debe fallar todavía")
        self.assertIn("timer:(on", out)


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