import functools


MAGIC_METHODS = frozenset(
    [
        "__iter__",
        "__next__",
        "__len__",
        "__contains__",
        "__getitem__",
        "__setitem__",
        "__delitem__",
        "__enter__",
        "__exit__",
        "__lt__",
        "__le__",
        "__eq__",
        "__ne__",
        "__gt__",
        "__ge__",
        "__add__",
        "__and__",
        "__divmod__",
        "__floordiv__",
        "__lshift__",
        "__mod__",
        "__mul__",
        "__or__",
        "__pow__",
        "__rshift__",
        "__sub__",
        "__truediv__",
        "__xor__",
        "__bool__",
        "__call__",
        "__repr__",
    ]
)


def _build_magic_dispatcher(method):
    def inner(self, *args, **kwargs):
        return self.__dict__[method](*args, **kwargs)

    inner.__name__ = method
    return inner


class stub(object):
    _classes_cache = {}

    def __new__(cls, **kwargs):
        magic_methods_present = MAGIC_METHODS.intersection(kwargs)
        if magic_methods_present not in cls._classes_cache:
            attrs = {
                method: _build_magic_dispatcher(method)
                for method in magic_methods_present
            }
            attrs["__module__"] = cls.__module__
            cls._classes_cache[magic_methods_present] = type("stub", (cls,), attrs)
        new_cls = cls._classes_cache[magic_methods_present]
        return super(stub, new_cls).__new__(new_cls)

    def __init__(self, **kwargs):
        self.__dict__.update(kwargs)

    def __repr__(self):
        return "<stub(%s)>" % ", ".join(
            "%s=%r" % (key, val) for key, val in self.__dict__.items()
        )


def raiser(exc):
    if not (
        isinstance(exc, BaseException)
        or isinstance(exc, type)
        and issubclass(exc, BaseException)
    ):
        raise TypeError("exc must be either an exception instance or class.")

    def inner(*args, **kwargs):
        raise exc

    return inner


class call(object):
    def __init__(self, *args, **kwargs):
        self.args = args
        self.kwargs = kwargs

    def __eq__(self, other):
        if not isinstance(other, call):
            return NotImplemented
        return self.args == other.args and self.kwargs == other.kwargs

    def __ne__(self, other):
        return not (self == other)

    def __hash__(self):
        return hash((self.args, frozenset(self.kwargs.items())))

    def __repr__(self):
        args = ", ".join(map(repr, self.args))
        kwargs = ", ".join("%s=%r" % (k, v) for k, v in self.kwargs.items())
        comma = ", " if args and kwargs else ""
        return "<call(%s%s%s)>" % (args, comma, kwargs)


def call_recorder(func):
    @functools.wraps(func)
    def inner(*args, **kwargs):
        inner.calls.append(call(*args, **kwargs))
        return func(*args, **kwargs)

    inner.calls = []
    return inner
