import contextlib
import re
import sys


def eq_(a, b, msg=None):
    """Assert a == b, with repr messaging on failure."""
    assert a == b, msg or "%r != %r" % (a, b)


def ne_(a, b, msg=None):
    """Assert a != b, with repr messaging on failure."""
    assert a != b, msg or "%r == %r" % (a, b)


def in_(a, b, msg=None):
    """Assert a in b, with repr messaging on failure."""
    assert a in b, msg or "%r not in %r" % (a, b)


def not_in(a, b, msg=None):
    """Assert a in not b, with repr messaging on failure."""
    assert a not in b, msg or "%r is in %r" % (a, b)


def _assert_proper_exception_context(exception):
    """assert that any exception we're catching does not have a __context__
    without a __cause__, and that __suppress_context__ is never set.

    Python 3 will report nested as exceptions as "during the handling of
    error X, error Y occurred". That's not what we want to do. We want
    these exceptions in a cause chain.

    """

    if (
        exception.__context__ is not exception.__cause__
        and not exception.__suppress_context__
    ):
        assert False, (
            "Exception %r was correctly raised but did not set a cause, "
            "within context %r as its cause."
            % (exception, exception.__context__)
        )


def _assert_proper_cause_cls(exception, cause_cls):
    """assert that any exception we're catching does not have a __context__
    without a __cause__, and that __suppress_context__ is never set.

    Python 3 will report nested as exceptions as "during the handling of
    error X, error Y occurred". That's not what we want to do. We want
    these exceptions in a cause chain.

    """
    assert isinstance(exception.__cause__, cause_cls), (
        "Exception %r was correctly raised but has cause %r, which does not "
        "have the expected cause type %r."
        % (exception, exception.__cause__, cause_cls)
    )


def assert_raises(except_cls, callable_, *args, **kw):
    return _assert_raises(except_cls, callable_, args, kw)


def assert_raises_with_proper_context(except_cls, callable_, *args, **kw):
    return _assert_raises(except_cls, callable_, args, kw, check_context=True)


def assert_raises_with_given_cause(
    except_cls, cause_cls, callable_, *args, **kw
):
    return _assert_raises(except_cls, callable_, args, kw, cause_cls=cause_cls)


def assert_raises_message(except_cls, msg, callable_, *args, **kwargs):
    return _assert_raises(except_cls, callable_, args, kwargs, msg=msg)


def assert_raises_message_with_proper_context(
    except_cls, msg, callable_, *args, **kwargs
):
    return _assert_raises(
        except_cls, callable_, args, kwargs, msg=msg, check_context=True
    )


def assert_raises_message_with_given_cause(
    except_cls, msg, cause_cls, callable_, *args, **kwargs
):
    return _assert_raises(
        except_cls, callable_, args, kwargs, msg=msg, cause_cls=cause_cls
    )


def _assert_raises(
    except_cls,
    callable_,
    args,
    kwargs,
    msg=None,
    check_context=False,
    cause_cls=None,
):

    with _expect_raises(except_cls, msg, check_context, cause_cls) as ec:
        callable_(*args, **kwargs)
    return ec.error


class _ErrorContainer:
    error = None


@contextlib.contextmanager
def _expect_raises(except_cls, msg=None, check_context=False, cause_cls=None):
    ec = _ErrorContainer()
    if check_context:
        are_we_already_in_a_traceback = sys.exc_info()[0]
    try:
        yield ec
        success = False
    except except_cls as err:
        ec.error = err
        success = True
        if msg is not None:
            # I'm often pdbing here, and "err" above isn't
            # in scope, so assign the string explicitly
            error_as_string = str(err)
            assert re.search(msg, error_as_string, re.UNICODE), "%r !~ %s" % (
                msg,
                error_as_string,
            )
        if cause_cls is not None:
            _assert_proper_cause_cls(err, cause_cls)
        if check_context and not are_we_already_in_a_traceback:
            _assert_proper_exception_context(err)
        print(str(err).encode("utf-8"))

    # it's generally a good idea to not carry traceback objects outside
    # of the except: block, but in this case especially we seem to have
    # hit some bug in either python 3.10.0b2 or greenlet or both which
    # this seems to fix:
    # https://github.com/python-greenlet/greenlet/issues/242
    del ec

    # assert outside the block so it works for AssertionError too !
    assert success, "Callable did not raise an exception"


def expect_raises(except_cls, check_context=False):
    return _expect_raises(except_cls, check_context=check_context)


def expect_raises_message(except_cls, msg, check_context=False):
    return _expect_raises(except_cls, msg=msg, check_context=check_context)


def expect_raises_with_proper_context(except_cls, check_context=True):
    return _expect_raises(except_cls, check_context=check_context)


def expect_raises_message_with_proper_context(
    except_cls, msg, check_context=True
):
    return _expect_raises(except_cls, msg=msg, check_context=check_context)
