"""Pruebas de iBGP, NEXT_HOP, hot potato y MED (ibgp/ibgp.clab.yml)."""
import os
import unittest

from netlab import HERE, Lab, wait_until

LAB = Lab("ibgp", "w06-ibgp", "redes-w06-ibgp")
PREFIX = "100.64.120.0/24"
BORDER_B1 = "100.64.100.1"
BORDER_B4 = "100.64.100.4"


def conf(name):
    return os.path.join(HERE, "ibgp", name)


def setUpModule():
    unittest.addModuleCleanup(LAB.destroy)
    LAB.deploy()
    ready = lambda: all(LAB.sessions_up(f"b{n}", 4 if n in (1, 4) else 3) for n in range(1, 5))
    if not wait_until(ready, seconds=120, step=2):
        raise RuntimeError("la topología no quedó lista")


def show(node):
    return LAB.vtysh(node, f"show bgp ipv4 {PREFIX}")


class A_NextHop(unittest.TestCase):
    def test_ibgp_keeps_the_next_hop_and_the_route_is_useless(self):
        ok = wait_until(lambda: show("b2").count("inaccessible") == 2, seconds=30)
        self.assertTrue(ok, show("b2"))
        self.assertIn("10.207.1.2 (inaccessible", show("b2"))
        self.assertIn("no best path", show("b2"))
        self.assertNotIn("100.64.120.0/24", LAB.out("b2", "ip", "route"))

    def test_bgp_routes_do_not_go_into_ospf(self):
        self.assertNotIn("100.64.120", LAB.vtysh("b2", "show ip ospf route"))
        self.assertIn("100.64.100.1/32", LAB.vtysh("b2", "show ip ospf route"))

    def test_ebgp_border_sees_the_route_fine(self):
        self.assertIn("best", show("b1"))


class B_HotPotato(unittest.TestCase):
    @classmethod
    def setUpClass(cls):
        LAB.load_conf("b1", conf("nhs_b1.conf"))
        LAB.load_conf("b4", conf("nhs_b4.conf"))
        wait_until(lambda: BORDER_B1 in show("b2") and BORDER_B4 in show("b2")
                   and BORDER_B1 in show("b3") and BORDER_B4 in show("b3"), seconds=30)

    def test_next_hop_self_puts_the_border_loopback(self):
        self.assertNotIn("inaccessible", show("b2"))

    def test_each_router_goes_to_its_closest_exit(self):
        for node, border, first_hop in (("b2", BORDER_B1, "10.207.12.1"),
                                        ("b3", BORDER_B4, "10.207.34.2")):
            text = show(node)
            best = [block for block in text.split("\n  65200") if "best (IGP Metric)" in block]
            self.assertEqual(len(best), 1, text)
            self.assertIn(border, best[0])
            source = "100.64.100." + node[1]
            LAB.assert_hops(self, node, "100.64.120.1", [first_hop], source=source)

    def test_border_prefers_its_own_ebgp_route(self):
        self.assertIn("best (Peer Type)", show("b1"))


class C_Mesh(unittest.TestCase):
    """iBGP no reenvía a iBGP: si a b3 solo le queda la sesión con b2, se queda sin la ruta."""

    def test_route_learned_by_ibgp_is_not_passed_to_ibgp(self):
        peers = ("100.64.100.1", "100.64.100.4")
        off = ["configure terminal", "router bgp 65100"] + [f"neighbor {p} shutdown" for p in peers]
        on = ["configure terminal", "router bgp 65100"] + [f"no neighbor {p} shutdown" for p in peers]
        self.addCleanup(lambda: wait_until(lambda: LAB.sessions_up("b3", 3), seconds=60))
        self.addCleanup(LAB.vtysh, "b3", *on)
        LAB.vtysh("b3", *off)
        self.assertTrue(wait_until(lambda: "Network not in table" in show("b3"), seconds=15))
        self.assertIn("best", show("b2"))
        self.assertNotIn(PREFIX, LAB.advertised("b2", "100.64.100.3"))


class D_Med(unittest.TestCase):
    """x pide entrar por b4 con un MED menor. MED se compara antes que la distancia."""

    def test_med_beats_hot_potato_and_ebgp_preference(self):
        wait_until(lambda: LAB.sessions_up("b3", 3), seconds=60)
        wait_until(lambda: "best (IGP Metric)" in show("b2"), seconds=30)
        LAB.load_conf("x", conf("med_x.conf"))
        # b1 ya compara el MED: su mejor camino pasa a ser el de b4, aunque tenga un enlace directo.
        self.assertTrue(wait_until(lambda: "best (MED)" in show("b1"), seconds=30), show("b1"))
        LAB.assert_hops(self, "b1", "100.64.120.1", ["10.207.12.2"], source="100.64.100.1")
        # Y como b1 solo pasa por iBGP su mejor camino si es externo, b2 se queda con uno solo.
        self.assertTrue(wait_until(lambda: BORDER_B1 not in show("b2"), seconds=30), show("b2"))
        self.assertIn(BORDER_B4, show("b2"))
        LAB.assert_hops(self, "b2", "100.64.120.1", ["10.207.23.2", "10.207.34.2"],
                        source="100.64.100.2")


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