"""Test the TCP implementation of UCSPI."""

from __future__ import annotations

import argparse
import dataclasses
import pathlib
import socket
import tempfile
import typing

import utf8_locale

import ucspi_test


if typing.TYPE_CHECKING:
    import os
    from typing import Any, Final


@dataclasses.dataclass
class NotAUnixSocketError(ucspi_test.RunnerError):
    """Could not create a Unix-domain socket."""

    sockpath: str
    sbuf: os.stat_result

    def __str__(self) -> str:
        """Provide a human-readable error message."""
        return f"Expected a Unix-domain socket at {self.sockpath!r}, got {self.sbuf!r}"


@dataclasses.dataclass
class FileRemoveError(ucspi_test.RunnerError):
    """Could not create a Unix-domain socket."""

    sockpath: str
    err: Exception

    def __str__(self) -> str:
        """Provide a human-readable error message."""
        return f"Could not examine and clean {self.sockpath!r} up: {self.err}"


@dataclasses.dataclass
class SocketCreateError(ucspi_test.RunnerError):
    """Could not create a Unix-domain socket."""

    err: Exception

    def __str__(self) -> str:
        """Provide a human-readable error message."""
        return f"Could not create a Unix-domain socket: {self.err}"


@dataclasses.dataclass
class SocketBindError(ucspi_test.RunnerError):
    """Could not bind a Unix-domain socket."""

    sockpath: str
    err: Exception

    def __str__(self) -> str:
        """Provide a human-readable error message."""
        return f"Could not bind to {self.sockpath!r}: {self.err}"


@dataclasses.dataclass
class SocketListenError(ucspi_test.RunnerError):
    """Could not listen on a Unix-domain socket."""

    sockpath: str
    err: Exception

    def __str__(self) -> str:
        """Provide a human-readable error message."""
        return f"Could not listen on {self.sockpath!r}: {self.err}"


@dataclasses.dataclass
class SocketConnectError(ucspi_test.RunnerError):
    """Could not connect to a Unix-domain socket."""

    sockpath: str
    err: Exception

    def __str__(self) -> str:
        """Provide a human-readable error message."""
        return f"Could not connect a Unix-domain socket to {self.sockpath!r}: {self.err}"


@dataclasses.dataclass(frozen=True)
class Config(ucspi_test.Config):
    """Runtime configuration for the TCP test runner."""

    tempd: pathlib.Path
    tempd_obj: tempfile.TemporaryDirectory[str]


class UnixRunner(ucspi_test.Runner):
    """Run ucspi-unix tests."""

    def find_listening_address(self) -> list[str]:
        """Find a local address/port combination."""
        print(f"{self.proto}.find_listening_address() starting")
        assert isinstance(self.cfg, Config), repr(self.cfg)  # noqa: S101  # mypy needs this

        sockpath: Final = self.cfg.tempd / "listen.sock"
        if sockpath.exists() or sockpath.is_symlink():
            raise RuntimeError(repr(sockpath))
        return [str(sockpath)]

    @property
    def supports_remote_info(self) -> bool:
        """Whether the client and server support the -R command-line option."""
        return False

    @property
    def logs_to_stdout(self) -> bool:
        """Whether verbose output goes to the standard output stream."""
        return True

    def get_listening_socket(self, addr: list[str]) -> socket.socket:  # noqa: PLR6301
        """Start listening on the specified address."""
        if len(addr) != 1:
            raise RuntimeError(repr(addr))
        sockpath: Final = addr[0]

        try:
            spath = pathlib.Path(sockpath)
            if spath.exists():
                if not spath.is_socket():
                    raise NotAUnixSocketError(sockpath, spath.stat())
                print(f"- removing the {sockpath} socket")
                spath.unlink()
        except OSError as err:
            raise FileRemoveError(sockpath, err) from err

        try:
            sock: Final = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM, 0)
        except OSError as err:
            raise SocketCreateError(err) from err
        try:
            sock.bind(sockpath)
        except OSError as err:
            raise SocketBindError(sockpath, err) from err
        try:
            sock.listen(5)
        except OSError as err:
            raise SocketListenError(sockpath, err) from err

        return sock

    def get_connected_socket(self, addr: list[str]) -> socket.socket:
        """Connect to the specified address."""
        if len(addr) != 1:
            raise ucspi_test.SocketAddressLengthError(self.proto, addr)
        sockpath: Final = addr[0]

        try:
            sock: Final = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM, 0)
        except OSError as err:
            raise SocketCreateError(err) from err
        try:
            sock.connect(sockpath)
        except OSError as err:
            raise SocketConnectError(sockpath, err) from err

        return sock

    def format_local_addr(self, addr: list[str]) -> str:  # noqa: PLR6301
        """Format an address returned by accept(), etc."""
        if len(addr) != 1:
            raise RuntimeError(repr(addr))
        return addr[0]

    def format_remote_addr(self, addr: Any) -> str:  # noqa: ANN401,PLR6301
        """Format an address returned by accept(), etc."""
        if not isinstance(addr, str):
            raise TypeError(repr(addr))
        return addr


def parse_args() -> Config | None:
    """Parse the command-line arguments."""
    parser = argparse.ArgumentParser(prog="uctest")

    parser.add_argument(
        "-d",
        "--bindir",
        type=pathlib.Path,
        required=True,
        help="the path to the UCSPI utilities",
    )
    parser.add_argument(
        "-p",
        "--proto",
        type=str,
        required=True,
        help="the UCSPI protocol ('tcp', 'unix', etc)",
    )
    args: Final = parser.parse_args()

    tempd_obj: Final = tempfile.TemporaryDirectory(prefix="ucspi-unix-test.")
    tempd: Final = pathlib.Path(tempd_obj.name)

    return Config(
        bindir=args.bindir.absolute(),
        proto=args.proto,
        tempd=tempd,
        tempd_obj=tempd_obj,
        utf8_env=utf8_locale.UTF8Detect().detect().env,
    )


def main() -> None:
    """Parse command-line arguments, run the tests."""
    cfg: Final = parse_args()
    if cfg is None:
        print("No loopback interface addresses for the requested family")
        return

    ucspi_test.add_handler("unix", UnixRunner)
    ucspi_test.run_test_handler(cfg)


if __name__ == "__main__":
    main()
