"""Ayudas de las pruebas de la semana 6: levantar topologías y preguntarle a FRRouting."""
import json
import os
import subprocess
import time
import unittest

HERE = os.path.dirname(os.path.abspath(__file__))
# La solución no se publica. Sin ella, las pruebas usan lo que ustedes escriben en lab/.
ANSWERS = os.path.join(HERE, "solution")
if not os.path.isdir(ANSWERS):
    ANSWERS = os.path.join(HERE, "lab")


def answer_path(name):
    """Devuelve la ruta de un archivo del laboratorio, o salta si sigue con el TODO."""
    path = os.path.join(ANSWERS, name)
    with open(path, encoding="utf-8") as file:
        if "TODO" in file.read():
            raise unittest.SkipTest(f"falta completar lab/{name}")
    return path


def run(args, timeout=120, **options):
    return subprocess.run(args, capture_output=True, text=True, timeout=timeout, **options)


class Lab:
    """Una topología de Containerlab que se levanta y se destruye desde las pruebas."""

    def __init__(self, folder, name, network):
        self.folder = os.path.join(HERE, folder)
        self.topology = f"{folder}.clab.yml"
        self.prefix = f"clab-{name}-"
        self.network = network

    def deploy(self):
        done = run(["containerlab", "deploy", "-t", self.topology, "--reconfigure"],
                   timeout=300, cwd=self.folder)
        if done.returncode != 0:
            raise RuntimeError(done.stdout + done.stderr)

    def destroy(self):
        run(["containerlab", "destroy", "-t", self.topology, "--cleanup"],
            timeout=300, cwd=self.folder)
        run(["docker", "network", "rm", self.network])

    def out(self, node, *command, timeout=60):
        """Corre un comando en un nodo y devuelve stdout y stderr juntos."""
        done = run(["docker", "exec", self.prefix + node, *command], timeout=timeout)
        return done.stdout + done.stderr

    def ok(self, node, *command, timeout=60):
        """Corre un comando en un nodo y dice si terminó bien."""
        done = run(["docker", "exec", self.prefix + node, *command], timeout=timeout)
        return done.returncode == 0

    def daemon(self, node, command):
        """Deja un comando corriendo en segundo plano dentro de un nodo."""
        run(["docker", "exec", "-d", self.prefix + node, "sh", "-c", command])

    def vtysh(self, node, *commands):
        """Corre comandos de vtysh; varios seguidos sirven para configurar."""
        arguments = []
        for command in commands:
            arguments += ["-c", command]
        return self.out(node, "vtysh", *arguments)

    def load_conf(self, node, path):
        """Carga un archivo de configuración en un router que ya está corriendo."""
        done = run(["docker", "cp", path, f"{self.prefix}{node}:/tmp/loaded.conf"])
        if done.returncode != 0:
            raise RuntimeError(done.stderr)
        done = run(["docker", "exec", self.prefix + node, "vtysh", "-f", "/tmp/loaded.conf"])
        if done.returncode != 0:
            raise RuntimeError(done.stdout + done.stderr)

    def bgp(self, node):
        """Devuelve la tabla BGP de un router: prefijo -> lista de caminos."""
        text = self.vtysh(node, "show bgp ipv4 json")
        return json.loads(text).get("routes", {})

    def best_path(self, node, prefix):
        """Devuelve el AS_PATH del mejor camino como lista de enteros, o None."""
        for path in self.bgp(node).get(prefix, []):
            if path.get("bestpath"):
                return [int(asn) for asn in path.get("path", "").split()]
        return None

    def advertised(self, node, neighbor):
        """Devuelve los prefijos que un router le anuncia a un vecino."""
        text = self.vtysh(node, f"show bgp ipv4 neighbors {neighbor} advertised-routes json")
        return set(json.loads(text).get("advertisedRoutes", {}))

    def sessions_up(self, node, count):
        """Dice si un router tiene al menos count sesiones en Established."""
        text = self.vtysh(node, "show bgp ipv4 summary json")
        peers = json.loads(text).get("ipv4Unicast", {}).get("peers", {})
        return sum(peer.get("state") == "Established" for peer in peers.values()) >= count

    def hops(self, node, target, source=None):
        """Devuelve las direcciones de un traceroute, salto por salto."""
        command = ["traceroute", "-n", "-w", "1", "-q", "1"]
        if source:
            command += ["-s", source]
        output = self.out(node, *command, target)
        hops = []
        for line in output.splitlines()[1:]:
            parts = line.split()
            if parts and parts[0].isdigit():
                hops.append(parts[1])
        return hops

    def assert_hops(self, test, node, target, expected, source=None, seconds=20):
        """Exige que el traceroute empiece con expected: el kernel tarda un poco tras BGP."""
        def matches():
            return self.hops(node, target, source=source)[:len(expected)] == list(expected)
        if not wait_until(matches, seconds=seconds):
            test.fail(f"{node} -> {target}: {self.hops(node, target, source=source)}, "
                      f"se esperaba {expected}")

    def wait_for_table(self, nodes, prefixes):
        """Espera a que cada nodo tenga una ruta a cada prefijo."""
        def ready():
            return all(self.best_path(node, prefix) is not None
                       for node in nodes for prefix in prefixes)
        return wait_until(ready, seconds=90, step=1)


def wait_until(predicate, seconds=30, step=0.5):
    """Espera a que una condición se cumpla; devuelve si se cumplió."""
    deadline = time.monotonic() + seconds
    while time.monotonic() < deadline:
        if predicate():
            return True
        time.sleep(step)
    return predicate()
