"""Apoyo para las pruebas: lanza servidores y recuerda sus procesos.

El contenedor del curso comparte la red y los procesos con la máquina. Por eso
las pruebas piden el puerto 0, leen el que eligió el sistema y solo matan a los
procesos que ellas mismas lanzaron.
"""
import re
import socket
import subprocess
import threading
import time
import unittest
from pathlib import Path

HERE = Path(__file__).resolve().parent
BUILD = HERE / "build"


def free_port(kind=socket.SOCK_STREAM):
    """Un puerto libre en este momento, para los programas que no aceptan 0."""
    with socket.socket(socket.AF_INET, kind) as s:
        s.bind(("127.0.0.1", 0))
        return s.getsockname()[1]


def connect(port, timeout=5.0):
    s = socket.create_connection(("127.0.0.1", port), timeout=timeout)
    s.settimeout(timeout)
    return s


def recv_line(sock):
    data = b""
    while not data.endswith(b"\n"):
        chunk = sock.recv(1)
        if not chunk:
            break
        data += chunk
    return data.decode()


def recv_until_close(sock):
    data = b""
    while True:
        chunk = sock.recv(65536)
        if not chunk:
            return data
        data += chunk


class Server:
    """Un servidor en segundo plano. Reúne su salida para poder revisarla."""

    def __init__(self, path, *args):
        self.process = subprocess.Popen(
            [str(path), *map(str, args)],
            stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True,
        )
        line = self.process.stdout.readline()
        match = re.search(r"puerto (\d+)", line)
        if match is None:
            self.process.kill()
            self.process.wait()
            self.process.stdout.close()
            raise AssertionError(f"{path} no dijo su puerto: {line!r}")
        self.port = int(match.group(1))
        self.lines = []
        self.reader = threading.Thread(target=self._read, daemon=True)
        self.reader.start()

    def _read(self):
        for line in self.process.stdout:
            self.lines.append(line.rstrip("\n"))

    def alive(self):
        return self.process.poll() is None

    def wait_output(self, pattern, timeout=5.0):
        """Espera una línea de salida que cumpla pattern y devuelve su match."""
        deadline = time.monotonic() + timeout
        while time.monotonic() < deadline:
            for line in list(self.lines):
                match = re.search(pattern, line)
                if match:
                    return match
            time.sleep(0.02)
        raise AssertionError(f"no apareció {pattern!r}; salida: {self.lines}")

    def wait(self, timeout=10.0):
        code = self.process.wait(timeout=timeout)
        self.reader.join(timeout=2)
        return code

    def stop(self):
        if self.process.poll() is None:
            self.process.kill()
        self.process.wait()
        self.reader.join(timeout=2)
        self.process.stdout.close()


class NetTestCase(unittest.TestCase):
    """Detiene al terminar cada prueba a los servidores que esta lanzó."""

    def server(self, name, *args):
        path = name if isinstance(name, Path) else BUILD / name
        server = Server(path, *args)
        self.addCleanup(server.stop)
        return server

    def connect(self, port):
        sock = connect(port)
        self.addCleanup(sock.close)
        return sock
