"""
exception classes and constants handling test outcomes
as well as functions creating them
"""
from __future__ import absolute_import, division, print_function
import py
import sys


class OutcomeException(BaseException):
    """ OutcomeException and its subclass instances indicate and
        contain info about test and collection outcomes.
    """

    def __init__(self, msg=None, pytrace=True):
        BaseException.__init__(self, msg)
        self.msg = msg
        self.pytrace = pytrace

    def __repr__(self):
        if self.msg:
            val = self.msg
            if isinstance(val, bytes):
                val = py._builtin._totext(val, errors="replace")
            return val
        return "<%s instance>" % (self.__class__.__name__,)

    __str__ = __repr__


TEST_OUTCOME = (OutcomeException, Exception)


class Skipped(OutcomeException):
    # XXX hackish: on 3k we fake to live in the builtins
    # in order to have Skipped exception printing shorter/nicer
    __module__ = "builtins"

    def __init__(self, msg=None, pytrace=True, allow_module_level=False):
        OutcomeException.__init__(self, msg=msg, pytrace=pytrace)
        self.allow_module_level = allow_module_level


class Failed(OutcomeException):
    """ raised from an explicit call to pytest.fail() """

    __module__ = "builtins"


class Exit(KeyboardInterrupt):
    """ raised for immediate program exits (no tracebacks/summaries)"""

    def __init__(self, msg="unknown reason"):
        self.msg = msg
        KeyboardInterrupt.__init__(self, msg)


# exposed helper methods


def exit(msg):
    """ exit testing process as if KeyboardInterrupt was triggered. """
    __tracebackhide__ = True
    raise Exit(msg)


exit.Exception = Exit


def skip(msg="", **kwargs):
    """ skip an executing test with the given message.  Note: it's usually
    better to use the pytest.mark.skipif marker to declare a test to be
    skipped under certain conditions like mismatching platforms or
    dependencies.  See the pytest_skipping plugin for details.

    :kwarg bool allow_module_level: allows this function to be called at
        module level, skipping the rest of the module. Default to False.
    """
    __tracebackhide__ = True
    allow_module_level = kwargs.pop("allow_module_level", False)
    if kwargs:
        keys = [k for k in kwargs.keys()]
        raise TypeError("unexpected keyword arguments: {}".format(keys))
    raise Skipped(msg=msg, allow_module_level=allow_module_level)


skip.Exception = Skipped


def fail(msg="", pytrace=True):
    """ explicitly fail a currently-executing test with the given Message.

    :arg pytrace: if false the msg represents the full failure information
                  and no python traceback will be reported.
    """
    __tracebackhide__ = True
    raise Failed(msg=msg, pytrace=pytrace)


fail.Exception = Failed


class XFailed(fail.Exception):
    """ raised from an explicit call to pytest.xfail() """


def xfail(reason=""):
    """ xfail an executing test or setup functions with the given reason."""
    __tracebackhide__ = True
    raise XFailed(reason)


xfail.Exception = XFailed


def importorskip(modname, minversion=None):
    """ return imported module if it has at least "minversion" as its
    __version__ attribute.  If no minversion is specified the a skip
    is only triggered if the module can not be imported.
    """
    import warnings

    __tracebackhide__ = True
    compile(modname, "", "eval")  # to catch syntaxerrors
    should_skip = False

    with warnings.catch_warnings():
        # make sure to ignore ImportWarnings that might happen because
        # of existing directories with the same name we're trying to
        # import but without a __init__.py file
        warnings.simplefilter("ignore")
        try:
            __import__(modname)
        except ImportError:
            # Do not raise chained exception here(#1485)
            should_skip = True
    if should_skip:
        raise Skipped("could not import %r" % (modname,), allow_module_level=True)
    mod = sys.modules[modname]
    if minversion is None:
        return mod
    verattr = getattr(mod, "__version__", None)
    if minversion is not None:
        try:
            from pkg_resources import parse_version as pv
        except ImportError:
            raise Skipped(
                "we have a required version for %r but can not import "
                "pkg_resources to parse version strings." % (modname,),
                allow_module_level=True,
            )
        if verattr is None or pv(verattr) < pv(minversion):
            raise Skipped(
                "module %r has __version__ %r, required is: %r"
                % (modname, verattr, minversion),
                allow_module_level=True,
            )
    return mod
