"""
Utility functions for testing server components.

This module provides functions for managing test server instances,
including port allocation and server readiness checking.
"""

import errno
import logging
import socket
import time
import typing as t
from copy import copy
from functools import lru_cache
from urllib.request import urlopen

logger = logging.getLogger(__name__)


def random_port() -> int:
    """
    Return a random available port by binding to port 0.

    Returns:
        int: An available port number that can be used for testing.
    """
    sock = socket.socket()
    try:
        sock.bind(("", 0))
        return sock.getsockname()[1]
    finally:
        sock.close()


@lru_cache(maxsize=None)
def transient_socket_error_numbers() -> t.List[int]:
    """
    A list of TCP socket error numbers to ignore in `wait_server_tcp`.

    On Windows, Winsock error codes are the Unix error code + 10000.

    Returns:
        List[int]: A list containing both Unix and Windows-specific error codes.
        For each Unix error code 'x', includes both 'x' and 'x + 10000'.
    """
    error_numbers = [
        errno.EAGAIN,
        errno.ECONNABORTED,
        errno.ECONNREFUSED,
        errno.ETIMEDOUT,
        errno.EWOULDBLOCK,
    ]
    error_numbers_effective = copy(error_numbers)
    error_numbers_effective.extend(error_number + 10000 for error_number in error_numbers)
    return error_numbers_effective


def wait_server_tcp(
    port: int,
    host: str = "127.0.0.1",
    timeout: int = 10,
    delay: float = 0.1,
) -> None:
    """
    Wait for server to be ready by attempting TCP connections.

    Args:
        port: The port number to connect to
        host: The host to connect to (default: "127.0.0.1")
        timeout: Maximum time to wait in seconds (default: 10)
        delay: Delay between attempts in seconds (default: 0.1)

    Raises:
        RuntimeError: If server is not ready within timeout period
    """
    endpoint = f"tcp://{host}:{port}/"
    logger.debug(f"Waiting for endpoint: {endpoint}")
    start_time = time.time()
    while time.time() - start_time < timeout:
        with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
            sock.settimeout(delay / 2)  # Set socket timeout
            error_number = sock.connect_ex((host, port))
            if error_number == 0:
                break

            # Expected errors when server is not ready.
            if error_number in transient_socket_error_numbers():
                pass

            # Unexpected error.
            else:
                raise RuntimeError(
                    f"Unexpected error while connecting to {endpoint}: {error_number}"
                )
        time.sleep(delay)
    else:
        raise RuntimeError(
            f"Server at {endpoint} failed to start within {timeout} seconds"
        )


def wait_server_http(
    port: int,
    host: str = "127.0.0.1",
    protocol: str = "http",
    attempts: int = 20,
    delay: float = 0.1,
) -> None:
    """
    Wait for server to be ready by attempting to connect to it.

    Args:
        port: The port number to connect to
        host: The host to connect to (default: "127.0.0.1")
        protocol: The protocol to use (default: "http")
        attempts: Number of connection attempts (default: 20)
        delay: Delay per attempt in seconds (default: 0.1)

    Raises:
        RuntimeError: If server is not ready after all attempts
    """
    url = f"{protocol}://{host}:{port}/"
    for attempt in range(1, attempts + 1):
        try:
            urlopen(url, timeout=delay / 2)  # noqa: S310
            break
        except OSError:
            if attempt < attempts:  # Don't sleep on last attempt
                time.sleep(delay)
    else:
        raise RuntimeError(
            f"Server at {url} failed to respond after {attempts} attempts "
            f"(total wait time: {attempts * delay:.1f}s)"
        )
