"""Paso 4 del laboratorio: dos switches, trunks y un router con subinterfaces."""
import re
import subprocess
import time
import unittest

import labutil

H1, H2, H3 = (f"02:00:00:00:00:0{i}" for i in range(1, 4))
R1 = "02:00:00:00:00:fe"


class BeforeVlans(unittest.TestCase):
    """Sin VLANs, todo es un solo dominio de broadcast. Después, ya no."""

    @classmethod
    def setUpClass(cls):
        cls.addClassCleanup(labutil.destroy, "campus")
        labutil.deploy("campus")
        cls.lab = labutil.Lab("campus")
        time.sleep(6)

    def flush(self):
        for node in ("h1", "h2", "h3", "h4", "r1"):
            self.lab.sh(node, "ip neigh flush all")
        for switch in ("sw1", "sw2"):
            self.lab.sh(switch, "ip link set br0 type bridge fdb_flush")

    def test_1_without_vlans_a_broadcast_reaches_everyone(self):
        self.flush()
        for host in ("h2", "h4"):
            lines = self.lab.capture(host, "eth1",
                                     lambda: self.lab.ping("h1", "10.203.110.3"), "arp")
            self.assertTrue(any("Request who-has 10.203.110.3" in l for l in lines), lines)
            self.flush()

    def test_2_same_port_destination_is_filtered(self):
        # h1 y h2 están en sw1. Si sw1 no conoce a h2, hace flooding y el frame llega a sw2. Si
        # sw2 ya vio a h2 por ese mismo puerto, descarta el frame; si no, también hace flooding.
        lab = self.lab
        lab.sh("h2", "ip addr add 10.203.110.2/24 dev eth1", check=True)
        try:
            self.flush()
            lab.ping("h1", "10.203.110.2")
            lab.ping("h3", "10.203.110.2")
            self.assertRegex(lab.out("sw2", "bridge fdb show br br0"), rf"{H2} dev eth3")
            lab.sh("sw1", "ip link set br0 type bridge fdb_flush")
            lines = lab.capture("h4", "eth1", lambda: lab.ping("h1", "10.203.110.2"), "icmp")
            self.assertEqual(lines, [])
            lab.sh("sw1", "ip link set br0 type bridge fdb_flush")
            lab.sh("sw2", "ip link set br0 type bridge fdb_flush")
            lines = lab.capture("h4", "eth1", lambda: lab.ping("h1", "10.203.110.2"), "icmp")
            self.assertEqual(len(lines), 1, lines)
        finally:
            lab.sh("h2", "ip addr del 10.203.110.2/24 dev eth1")

    def test_3_two_subnets_without_router_do_not_talk(self):
        self.assertFalse(self.lab.ping("h1", "10.203.120.2"))

    def test_4_vlans_and_trunks(self):
        lab = self.lab
        for node in ("sw1", "sw2", "r1"):
            lab.apply_script(node, f"vlan/campus_{node}.sh")
        self.flush()

        # Un broadcast de la VLAN 110 ya no llega a la 120, ni siquiera por el trunk.
        lines = lab.capture("h4", "eth1", lambda: lab.ping("h1", "10.203.110.3"), "arp")
        self.assertEqual(lines, [])

        # Misma VLAN entre switches: el frame va etiquetado por el trunk.
        self.flush()
        lines = lab.capture("sw1", "eth3", lambda: lab.ping("h1", "10.203.110.3"), "icmp")
        self.assertTrue(lines)
        self.assertTrue(all("vlan 110" in l for l in lines), lines)

        # En el puerto de acceso el mismo tráfico va sin etiqueta.
        lines = lab.capture("h1", "eth1", lambda: lab.ping("h1", "10.203.110.3"), "icmp")
        self.assertTrue(lines)
        self.assertFalse(any("802.1Q" in l for l in lines), lines)

        # Entre VLANs: el paquete pasa dos veces por el trunk, con etiquetas distintas.
        self.flush()
        lines = lab.capture("sw1", "eth3", lambda: lab.ping("h1", "10.203.120.2"), "vlan and icmp")
        request = [l for l in lines if "echo request" in l]
        self.assertEqual(len(request), 2, lines)
        self.assertRegex(request[0], rf"{H1} > {R1}.*vlan 110")
        self.assertRegex(request[1], rf"{R1} > {H2}.*vlan 120")

        # El router ve la etiqueta en eth1 y no la ve en la subinterfaz.
        lines = lab.capture("r1", "eth1", lambda: lab.ping("h1", "10.203.120.2"), "vlan and icmp")
        self.assertTrue(any("vlan 110" in l for l in lines), lines)
        lines = lab.capture("r1", "eth1.110", lambda: lab.ping("h1", "10.203.120.2"), "icmp")
        self.assertTrue(lines)
        self.assertFalse(any("802.1Q" in l for l in lines), lines)

        # En otra subred el host busca con ARP la MAC del gateway, no la del destino.
        self.assertRegex(lab.out("h1", "ip neigh show 10.203.110.254"), R1)
        self.assertEqual(lab.out("h1", "ip neigh show 10.203.120.2").strip(), "")

        # Las direcciones IP no cambian por el camino; las MAC y el TTL, sí.
        self.flush()
        seen_h1 = lab.capture("h1", "eth1", lambda: lab.ping("h1", "10.203.120.2"), "icmp")
        self.assertRegex(seen_h1[0], rf"{H1} > {R1}.*10.203.110.1 > 10.203.120.2")
        seen_h2 = lab.capture("h2", "eth1", lambda: lab.ping("h1", "10.203.120.2"), "icmp")
        self.assertRegex(seen_h2[0], rf"{R1} > {H2}.*10.203.110.1 > 10.203.120.2")

        trace = lab.out("h1", "traceroute -n -w 1 -q 1 10.203.120.4")
        self.assertRegex(trace, r"1\s+10.203.110.254")

        # El mismo MAC puede estar en dos VLANs: la tabla del switch es por (VLAN, MAC).
        fdb = lab.out("sw1", "bridge fdb show br br0 | grep -v permanent")
        self.assertRegex(fdb, rf"{R1} dev eth3 vlan 110")
        self.assertRegex(fdb, rf"{R1} dev eth3 vlan 120")

        # El script de comprobación del laboratorio queda en verde.
        done = subprocess.run(["python3", "check_lab.py"], text=True, capture_output=True,
                              check=False, cwd=labutil.HERE)
        self.assertEqual(done.returncode, 0, done.stdout)
        self.assertNotIn("FALLA", done.stdout)

    def test_5_vlan_missing_from_a_trunk_breaks_only_that_vlan(self):
        lab = self.lab
        lab.sh("sw1", "bridge vlan del dev eth3 vid 120", check=True)
        try:
            self.assertFalse(lab.ping("h2", "10.203.120.254"))
            self.assertTrue(lab.ping("h1", "10.203.110.254"))
            done = subprocess.run(["python3", "check_lab.py"], text=True, capture_output=True,
                                  check=False, cwd=labutil.HERE)
            self.assertEqual(done.returncode, 1)
            self.assertTrue(re.search(r"FALLA\s+el trunk de sw1", done.stdout), done.stdout)
        finally:
            lab.sh("sw1", "bridge vlan add dev eth3 vid 120", check=True)

    def test_6_untagged_frame_on_a_trunk_is_dropped(self):
        # El trunk no tiene VLAN por defecto (sin pvid): un frame sin etiqueta no pasa.
        lab = self.lab
        lab.sh("r1", "ip addr add 10.203.9.1/24 dev eth1", check=True)
        try:
            lines = lab.capture("sw1", "eth3", lambda: lab.sh("r1", "ping -c 1 -W 1 10.203.9.2"),
                                "arp")
            self.assertEqual(lines, [])
        finally:
            lab.sh("r1", "ip addr del 10.203.9.1/24 dev eth1")


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