"""Ayudas de las pruebas de la semana 5: levantar topologías y correr comandos."""
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=240, stdin=None):
    return subprocess.run(args, capture_output=True, text=True, timeout=timeout, stdin=stdin)


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, **variables):
        done = subprocess.run(
            ["containerlab", "deploy", "-t", self.topology, "--reconfigure"],
            cwd=self.folder, capture_output=True, text=True, timeout=300,
            env={**os.environ, **variables})
        if done.returncode != 0:
            raise RuntimeError(done.stdout + done.stderr)

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

    def sh(self, node, *command, timeout=60):
        """Corre un comando en un nodo y devuelve el resultado completo."""
        return run(["docker", "exec", self.prefix + node, *command], timeout=timeout)

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

    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 en un nodo y devuelve su salida."""
        arguments = [part for command in commands for part in ("-c", command)]
        return self.out(node, "vtysh", *arguments)

    def configure(self, node, *commands):
        """Aplica líneas de configuración, como en conf t."""
        return self.vtysh(node, "conf t", *commands)

    def route(self, node, prefix):
        """Devuelve la entrada de la tabla de routing de un prefijo, en JSON de FRRouting."""
        output = self.vtysh(node, f"show ip route {prefix} json")
        return json.loads(output or "{}").get(prefix, [])

    def neighbor_states(self, node):
        """Devuelve el estado de cada vecino OSPF: {router id: estado}."""
        data = json.loads(self.vtysh(node, "show ip ospf neighbor json") or "{}")
        states = {}
        for router_id, entries in data.get("neighbors", {}).items():
            for entry in entries:
                states[router_id] = entry.get("nbrState", "")
        return states

    def full_with(self, node, router_id):
        return self.neighbor_states(node).get(router_id, "").startswith("Full")


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()
