import sys
import unittest
from nose_helper.config import Config
from nose_helper.util import resolve_name, try_run
import importlib.util


def find_module_path(module_name):
    spec = importlib.util.find_spec(module_name)
    if spec is None:
        raise ImportError

    return spec.origin


class Test(unittest.TestCase):
    """The universal test case wrapper.
    """
    __test__ = False  # do not collect

    def __init__(self, test, config=None):
        if not hasattr(test, '__call__'):
            raise TypeError("Test called with argument %r that "
                            "is not callable. A callable is required."
                            % test)
        self.test = test
        if config is None:
            config = Config()
        self.config = config
        unittest.TestCase.__init__(self)

    def __call__(self, *arg, **kwarg):
        return self.run(*arg, **kwarg)

    def __str__(self):
        return str(self.test)

    def _context(self):
        try:
            return self.test.context
        except AttributeError:
            pass
        try:
            return self.test.__class__
        except AttributeError:
            pass
        try:
            return resolve_name(self.test.__module__)
        except AttributeError:
            pass
        return None

    context = property(_context, None, None,
                       """Get the context object of this test.""")

    def run(self, result):
        try:
            self.runTest(result)
        except KeyboardInterrupt:
            raise
        except:
            err = sys.exc_info()
            result.addError(self, err)

    def runTest(self, result):
        test = self.test
        test(result)


class TestBase(unittest.TestCase):
    """Common functionality for FunctionTestCase and MethodTestCase.
    """
    __test__ = False  # do not collect

    class Suite:
        pass

    def runTest(self):
        self.test(*self.arg)


class FunctionTestCase(TestBase):
    """TestCase wrapper for test functions.
    """
    __test__ = False  # do not collect

    def __init__(self, test, setUp=None, tearDown=None, arg=tuple(),
                 descriptor=None):
        self.test = test
        self.setUpFunc = setUp
        self.tearDownFunc = tearDown
        self.arg = arg
        self.descriptor = descriptor
        TestBase.__init__(self)

        self.suite = TestBase.Suite()
        self.suite.__module__ = self.__get_module()
        self.suite.__name__ = ""
        has_module = True
        try:
            find_module_path(self.suite.__module__)
        except ImportError:
            has_module = False
        if sys.version.find("IronPython") != -1 or not has_module:
            # Iron Python doesn't fully support imp
            self.suite.abs_location = ""
            self.suite.location = ""
        else:
            self.suite.abs_location = "file://" + find_module_path(self.suite.__module__)
            self.suite.location = "file://" + find_module_path(self.suite.__module__)

    def _context(self):
        return resolve_name(self.test.__module__)

    context = property(_context, None, None,
                       """Get context (module) of this test""")

    def setUp(self):
        """Run any setup function attached to the test function
        """
        if self.setUpFunc:
            self.setUpFunc()
        else:
            names = ('setup', 'setUp', 'setUpFunc')
            try_run(self.test, names)

    def tearDown(self):
        """Run any teardown function attached to the test function
        """
        if self.tearDownFunc:
            self.tearDownFunc()
        else:
            names = ('teardown', 'tearDown', 'tearDownFunc')
            try_run(self.test, names)

    def __str__(self):
        func, arg = self._descriptors()
        if hasattr(func, 'compat_func_name'):
            name = func.compat_func_name
        else:
            name = func.__name__
        if arg:
            name = "%s%s" % (name, arg)
        return name

    __repr__ = __str__

    def __get_module(self):
        func, arg = self._descriptors()
        if hasattr(func, "__module__"):
            return func.__module__
        else:
            # TODO[kate]: get module of function in jython < 2.2
            return "Unknown module."

    def _descriptors(self):
        """In most cases, this is the function itself and no arguments. For
        tests generated by generator functions, the original
        (generator) function and args passed to the generated function
        are returned.
        """
        if self.descriptor:
            return self.descriptor, self.arg
        else:
            return self.test, self.arg


class MethodTestCase(TestBase):
    """Test case wrapper for test methods.
    """
    __test__ = False  # do not collect

    def __init__(self, method, test=None, arg=tuple(), descriptor=None):
        """Initialize the MethodTestCase.
        """
        self.method = method
        self.test = test
        self.arg = arg
        self.descriptor = descriptor
        self.cls = method.im_class
        self.inst = self.cls()
        if self.test is None:
            method_name = self.method.__name__
            self.test = getattr(self.inst, method_name)
        TestBase.__init__(self)

        self.suite = TestBase.Suite()
        self.suite.__module__, self.suite.__name__ = self.__get_module()

        has_module = True
        try:
            find_module_path(self.suite.__module__)
        except ImportError:
            has_module = False
        if sys.version.find("IronPython") != -1 or not has_module:
            # Iron Python doesn't fully support imp
            self.suite.abs_location = ""
        else:
            self.suite.abs_location = "file://" + find_module_path(self.suite.__module__)
        self.suite.location = "python_uttestid://" + self.suite.__module__ + "." + self.suite.__name__

    def __get_module(self):
        def get_class_that_defined_method(meth):
            import inspect
            obj = meth.im_self
            for cls in inspect.getmro(meth.im_class):
                if meth.__name__ in cls.__dict__: return (cls.__module__, cls.__name__)
            return ("Unknown module", "")

        func, arg = self._descriptors()
        return get_class_that_defined_method(func)

    def __str__(self):
        func, arg = self._descriptors()
        if hasattr(func, 'compat_func_name'):
            name = func.compat_func_name
        else:
            name = func.__name__
        if arg:
            name = "%s%s" % (name, arg)
        return name

    __repr__ = __str__

    def _context(self):
        return self.cls

    context = property(_context, None, None,
                       """Get context (class) of this test""")

    def setUp(self):
        try_run(self.inst, ('setup', 'setUp'))

    def tearDown(self):
        try_run(self.inst, ('teardown', 'tearDown'))

    def _descriptors(self):
        """in most cases, this is the method itself and no arguments. For
        tests generated by generator methods, the original
        (generator) method and args passed to the generated method 
        or function are returned.
        """
        if self.descriptor:
            return self.descriptor, self.arg
        else:
            return self.method, self.arg
