import contextlib
import functools
import socket
import ssl
import tempfile
import time
from typing import (
    Any,
    Callable,
    Container,
    Dict,
    Generic,
    Hashable,
    Iterable,
    Iterator,
    List,
    Optional,
    Set,
    Tuple,
    Type,
    TypeVar,
    Union,
    cast,
)

import pytest

from . import basecontrollers, client_mock, patma, runner, tls
from .authentication import Authentication
from .basecontrollers import TestCaseControllerConfig
from .exceptions import ConnectionClosed
from .irc_utils import capabilities, message_parser
from .irc_utils.message_parser import Message
from .irc_utils.sasl import sasl_plain_blob
from .numerics import (
    ERR_BADCHANNELKEY,
    ERR_BANNEDFROMCHAN,
    ERR_INVITEONLYCHAN,
    ERR_NEEDREGGEDNICK,
    ERR_NOSUCHCHANNEL,
    ERR_TOOMANYCHANNELS,
    RPL_HELLO,
)
from .specifications import Capabilities, IsupportTokens, Specifications

__tracebackhide__ = True  # Hide from pytest tracebacks on test failure.

CHANNEL_JOIN_FAIL_NUMERICS = frozenset(
    [
        ERR_NOSUCHCHANNEL,
        ERR_TOOMANYCHANNELS,
        ERR_BADCHANNELKEY,
        ERR_INVITEONLYCHAN,
        ERR_BANNEDFROMCHAN,
        ERR_NEEDREGGEDNICK,
    ]
)

# typevar for decorators
TCallable = TypeVar("TCallable", bound=Callable)
TClass = TypeVar("TClass", bound=Type)

# typevar for the client name used by tests (usually int or str)
TClientName = TypeVar("TClientName", bound=Union[Hashable, int])

TController = TypeVar("TController", bound=basecontrollers._BaseController)

# general-purpose typevar
T = TypeVar("T")


class ChannelJoinException(Exception):
    def __init__(self, code: str, params: List[str]):
        super().__init__(f"Failed to join channel ({code}): {params}")
        self.code = code
        self.params = params


class _IrcTestCase(Generic[TController]):
    """Base class for test cases.

    It implements various `assert*` method that look like unittest's,
    but is actually based on the `assert` statement so derived classes are
    pytest-style rather than unittest-style.

    It also calls setUp() and tearDown() like unittest would."""

    # Will be set by __main__.py
    controllerClass: Type[TController]
    show_io: bool

    controller: TController

    __new__ = object.__new__  # pytest won't collect Generic subclasses otherwise

    @staticmethod
    def config() -> TestCaseControllerConfig:
        """Some configuration to pass to the controllers.
        For example
, Oragono only enables its MySQL support if
        config()["chathistory"]=True.
        """
        return TestCaseControllerConfig()

    def setUp(self) -> None:
        if self.controllerClass is not None:
            self.controller = self.controllerClass(self.config())
        if
 self.show_io:
            print("---- new test ----")

    def tearDown(self) -> None:
        pass

    def setup_method(self, method: Callable) -> None:
        self.setUp()

    def teardown_method(self, method: Callable) -> None:
        self.tearDown()

    def assertMessageMatch(self, msg: Message, **kwargs: Any) -> None:
        """Helper for partially comparing a message.

        Takes the message as first arguments, and comparisons to be made
        as keyword arguments.

        Uses patma.match_list on the params argument.
        """
        error = self.messageDiffers(msg, **kwargs)
        if error:
            raise AssertionError(error)

    def messageEqual(self, msg: Message, **kwargs: Any) -> bool:
        """Boolean negation of `messageDiffers` (returns a boolean,
        not an optional string)."""
        return not self.messageDiffers(msg, **kwargs)

    def messageDiffers(
        self,
        msg: Message,
        params: Optional[List[Union[str, None, patma.Operator]]] = None,
        target: Optional[str] = None,
        tags: Optional[
            Dict[Union[str, patma.Operator], Union[str, patma.Operator, None]]
        ] = None,
        nick: Optional[str] = None,
        fail_msg: Optional[str] = None,
        extra_format: Tuple = (),
        **kwargs: Any,
    ) -> Optional[str]:
        """Returns an error message if the message doesn't match the given arguments,
        or None if it matches."""
        for (key, value) in kwargs.items():
            if getattr(msg, key) != value:
                fail_msg = (
                    fail_msg or "expected {param} to be {expects}, got {got}: {msg}"
                )
                return fail_msg.format(
                    *extra_format,
                    got=getattr(msg, key),
                    expects=value,
                    param=key,
                    msg=msg,
                )

        if params and not patma.match_list(list(msg.params), params):
            fail_msg = (
                fail_msg or "expected params to match {expects}, got {got}: {msg}"
            )
            return fail_msg.format(
                *extra_format, got=msg.params, expects=params, msg=msg
            )

        if tags and not patma.match_dict(msg.tags, tags):
            fail_msg = fail_msg or "expected tags to match {expects}, got {got}: {msg}"
            return fail_msg.format(*extra_format, got=msg.tags, expects=tags, msg=msg)

        if nick:
            got_nick = msg.prefix.split("!")[0] if msg.prefix else None
            if nick != got_nick:
                fail_msg = (
                    fail_msg
                    or "expected nick to be {expects}, got {got} instead: {msg}"
                )
                return fail_msg.format(
                    *extra_format, got=got_nick, expects=nick, param=key, msg=msg
                )

        return None

    def assertIn(
        self,
        member: Any,
        container: Union[Iterable[Any], Container[Any]],
        msg: Optional[str] = None,
        fail_msg: Optional[str] = None,
        extra_format: Tuple = (),
    ) -> None:
        if fail_msg:
            msg = fail_msg.format(*extra_format, item=member, list=container, msg=msg)
        assert member in container, msg  # type: ignore

    def assertNotIn(
        self,
        member: Any,
        container: Union[Iterable[Any], Container[Any]],
        msg: Optional[str] = None,
        fail_msg: Optional[str] = None,
        extra_format: Tuple = (),
    ) -> None:
        if fail_msg:
            msg = fail_msg.format(*extra_format, item=member, list=container, msg=msg)
        assert member not in container, msg  # type: ignore

    def assertEqual(
        self,
        got: T,
        expects: T,
        msg: Any = None,
        fail_msg: Optional[str] = None,
        extra_format: Tuple = (),
    ) -> None:
        if fail_msg:
            msg = fail_msg.format(*extra_format, got=got, expects=expects, msg=msg)
        assert got == expects, msg

    def assertNotEqual(
        self,
        got: T,
        expects: T,
        msg: Any = None,
        fail_msg: Optional[str] = None,
        extra_format: Tuple = (),
    ) -> None:
        if fail_msg:
            msg = fail_msg.format(*extra_format, got=got, expects=expects, msg=msg)
        assert got != expects, msg

    def assertGreater(
        self,
        got: T,
        expects: T,
        msg: Any = None,
        fail_msg: Optional[str] = None,
        extra_format: Tuple = (),
    ) -> None:
        if fail_msg:
            msg = fail_msg.format(*extra_format, got=got, expects=expects, msg=msg)
        assert got >= expects, msg  # type: ignore

    def assertGreaterEqual(
        self,
        got: T,
        expects: T,
        msg: Any = None,
        fail_msg: Optional[str] = None,
        extra_format: Tuple = (),
    ) -> None:
        if fail_msg:
            msg = fail_msg.format(*extra_format, got=got, expects=expects, msg=msg)
        assert got >= expects, msg  # type: ignore

    def assertLess(
        self,
        got: T,
        expects: T,
        msg: Any = None,
        fail_msg: Optional[str] = None,
        extra_format: Tuple = (),
    ) -> None:
        if fail_msg:
            msg = fail_msg.format(*extra_format, got=got, expects=expects, msg=msg)
        assert got < expects, msg  # type: ignore

    def assertLessEqua