"""Pruebas del peering por el IXP en ixp/ixp.clab.yml."""
import os
import re
import subprocess
import time
import unittest

from netlab import HERE, Lab, wait_until

LAB = Lab("ixp", "w07-ixp", "redes-w07-ixp")
H2, H3 = "100.64.72.10", "100.64.73.10"
PEERING = os.path.join(HERE, "ixp", "peering.sh")


def setUpModule():
    unittest.addModuleCleanup(LAB.destroy)
    LAB.deploy()
    if not wait_until(lambda: LAB.sh("h1", "ping", "-c", "1", "-W", "2", H2).returncode == 0
                      and LAB.sh("h1", "ping", "-c", "1", "-W", "2", H3).returncode == 0, seconds=60):
        raise RuntimeError("la topología no quedó lista")


def peering(state):
    done = subprocess.run(["sh", PEERING, state], capture_output=True, text=True)
    if done.returncode != 0:
        raise RuntimeError(done.stdout + done.stderr)


def rtt(node, address):
    output = LAB.out(node, "ping", "-c", "5", "-i", "0.2", address)
    return float(re.search(r"= [\d.]+/([\d.]+)/", output).group(1))


def path_to(node, address):
    return LAB.out(node, "traceroute", "-n", "-w", "1", address)


def transit_bytes():
    text = LAB.out("t", "cat", "/sys/class/net/eth1/statistics/rx_bytes")
    return int(text.strip())


def send_mbytes(megabytes=5):
    LAB.daemon("h2", "iperf3 -s -1 >/dev/null 2>&1")
    time.sleep(0.5)
    before = transit_bytes()
    done = LAB.sh("h1", "iperf3", "-c", H2, "-n", f"{megabytes}M", timeout=120)
    if done.returncode != 0:
        raise RuntimeError(done.stdout + done.stderr)
    time.sleep(1)  # lo último que se envió todavía viene en camino por el enlace con retardo
    return transit_bytes() - before


def peer_routes():
    return "100.64.72.0/24" in LAB.out("isp1", "ip", "route", "get", H2) \
        or "10.208.100.2" in LAB.out("isp1", "ip", "route", "get", H2)


class Peering(unittest.TestCase):
    def setUp(self):
        peering("off")
        self.addCleanup(peering, "off")
        self.assertTrue(wait_until(lambda: not peer_routes(), seconds=20))
        self.assertTrue(wait_until(lambda: LAB.sh("h1", "ping", "-c", "1", "-W", "2", H2).returncode == 0))

    def test_without_peering_local_traffic_goes_through_the_far_provider(self):
        self.assertIn("10.208.1.2", path_to("h1", H2))
        self.assertGreater(rtt("h1", H2), 120)

    def test_peering_shortens_the_path_and_keeps_the_rest(self):
        peering("on")
        self.assertTrue(wait_until(peer_routes, seconds=30))
        path = path_to("h1", H2)
        self.assertNotIn("10.208.1.2", path)
        self.assertIn("10.208.100.2", path)
        self.assertLess(rtt("h1", H2), 40)
        # isp3 no hizo peering: con él sigue todo igual.
        self.assertIn("10.208.1.2", path_to("h1", H3))
        self.assertGreater(rtt("h1", H3), 120)

    def test_traffic_leaves_the_paid_link(self):
        before = send_mbytes()
        self.assertGreater(before, 4_000_000)
        peering("on")
        self.assertTrue(wait_until(peer_routes, seconds=30))
        self.assertLess(send_mbytes(), 500_000)

    def test_peer_does_not_get_free_transit(self):
        peering("on")
        self.assertTrue(wait_until(peer_routes, seconds=30))
        offered = LAB.out("isp1", "vtysh", "-c",
                          "show bgp ipv4 unicast neighbors 10.208.100.2 advertised-routes")
        self.assertIn("100.64.71.0/24", offered)
        self.assertNotIn("100.64.73.0/24", offered)

    def test_if_the_ixp_fails_traffic_returns_to_transit(self):
        peering("on")
        self.assertTrue(wait_until(peer_routes, seconds=30))
        self.addCleanup(LAB.out, "ix", "ip", "link", "set", "eth2", "up")
        LAB.out("ix", "ip", "link", "set", "eth2", "down")
        self.assertTrue(wait_until(lambda: not peer_routes(), seconds=25))
        self.assertTrue(wait_until(lambda: LAB.sh("h1", "ping", "-c", "1", "-W", "2", H2).returncode == 0))
        self.assertGreater(rtt("h1", H2), 120)
        LAB.out("ix", "ip", "link", "set", "eth2", "up")
        self.assertTrue(wait_until(peer_routes, seconds=40))
        self.assertLess(rtt("h1", H2), 40)


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