"""Pruebas de las áreas de OSPF en areas/areas.clab.yml: qué cruza de un área a otra."""
import time
import unittest

from netlab import Lab, wait_until

LAB = Lab("areas", "w05-areas", "redes-w05-areas")
X1_ID = "10.205.0.11"
ROUTERS = {"x1": 1, "abr1": 2, "core": 2, "abr2": 2, "y1": 1}


def setUpModule():
    unittest.addModuleCleanup(LAB.destroy)


def converged(prefix):
    return all(len(LAB.neighbor_states(node)) == count
               and all(state.startswith("Full") for state in LAB.neighbor_states(node).values())
               for node, count in ROUTERS.items()) and bool(LAB.route("y1", prefix))


def spf_count(node):
    """Cuántas veces corrió SPF en cada área del router, sumadas."""
    text = LAB.vtysh(node, "show ip ospf")
    return sum(int(line.split()[3]) for line in text.splitlines() if "SPF algorithm executed" in line)


def x1_router_lsa_seq(node):
    text = LAB.vtysh(node, f"show ip ospf database router {X1_ID}")
    return [line.split()[-1] for line in text.splitlines() if "LS Seq Number" in line]


class A_WithoutSummary(unittest.TestCase):
    @classmethod
    def setUpClass(cls):
        LAB.deploy()
        if not wait_until(lambda: converged("10.205.17.0/24"), 60):
            raise RuntimeError("OSPF no convergió")

    def setUp(self):
        # Una prueba apaga lan1 y la vuelve a prender: hay que esperar a que reaparezca.
        self.assertTrue(wait_until(lambda: LAB.route("y1", "10.205.17.0/24"), 30))

    def test_each_area_keeps_its_own_router_lsas(self):
        database = LAB.vtysh("y1", "show ip ospf database")
        self.assertIn("10.205.0.15", database)
        self.assertIn("Summary Link States", database)
        self.assertEqual(x1_router_lsa_seq("y1"), [])
        self.assertNotEqual(x1_router_lsa_seq("abr1"), [])

    def test_summaries_carry_each_prefix_of_the_other_area(self):
        database = LAB.vtysh("y1", "show ip ospf database summary")
        for prefix in ("10.205.16.0", "10.205.17.0", "10.205.18.0"):
            self.assertIn(prefix, database)
        self.assertTrue(LAB.route("y1", "10.205.17.0/24"))

    def test_abr_keeps_one_database_per_area(self):
        database = LAB.vtysh("abr1", "show ip ospf database")
        self.assertIn("Router Link States (Area 0.0.0.0)", database)
        self.assertIn("Router Link States (Area 0.0.0.1)", database)

    def test_a_change_in_area_1_reaches_area_2_only_as_a_summary(self):
        before = x1_router_lsa_seq("abr1")
        LAB.sh("x1", "ip", "link", "set", "lan1", "down")
        self.addCleanup(LAB.sh, "x1", "ip", "link", "set", "lan1", "up")
        self.assertTrue(wait_until(lambda: not LAB.route("y1", "10.205.17.0/24"), 30))
        self.assertNotEqual(x1_router_lsa_seq("abr1"), before)
        self.assertEqual(x1_router_lsa_seq("y1"), [])


class B_WithSummary(unittest.TestCase):
    @classmethod
    def setUpClass(cls):
        LAB.destroy()
        LAB.deploy(ABR1_CONF="frr-range.conf")
        if not wait_until(lambda: converged("10.205.16.0/20"), 60):
            raise RuntimeError("OSPF no convergió con el resumen")

    def test_one_prefix_replaces_the_detail(self):
        database = LAB.vtysh("y1", "show ip ospf database summary")
        self.assertIn("10.205.16.0/20", LAB.vtysh("y1", "show ip route ospf"))
        self.assertNotIn("10.205.17.0", database)
        self.assertNotIn("10.205.18.0", database)

    def test_a_change_inside_does_not_leave_the_area(self):
        # La red recién convergió: que termine de calcular antes de tomar la línea base.
        before = spf_count("y1")
        for _ in range(10):
            time.sleep(8)
            current = spf_count("y1")
            if current == before:
                break
            before = current
        database = LAB.vtysh("y1", "show ip ospf database summary")
        LAB.sh("x1", "ip", "link", "set", "lan1", "down")
        self.addCleanup(LAB.sh, "x1", "ip", "link", "set", "lan1", "up")
        self.assertTrue(wait_until(lambda: not LAB.route("abr1", "10.205.17.0/24"), 30))
        time.sleep(4)  # si algo fuera a salir del área, daría tiempo a que llegue
        self.assertTrue(LAB.route("y1", "10.205.16.0/20"))
        self.assertEqual(spf_count("y1"), before)
        self.assertEqual(self._seqs(LAB.vtysh("y1", "show ip ospf database summary")),
                         self._seqs(database))
        # El precio: y1 sigue mandando hacia allá, y el paquete se pierde adentro.
        self.assertNotEqual(LAB.sh("y1", "ping", "-c", "1", "-W", "1", "10.205.17.1").returncode, 0)
        self.assertEqual(LAB.sh("y1", "ping", "-c", "2", "-W", "2", "10.205.18.1").returncode, 0)

    @staticmethod
    def _seqs(text):
        return [line.split()[-1] for line in text.splitlines() if "LS Seq Number" in line]


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