# Adapted from unittest2/loader.py from the unittest2 plugins branch.
# This module contains some code copied from unittest2/loader.py and other
# code developed in reference to that module and others within unittest2.
# unittest2 is Copyright (c) 2001-2010 Python Software Foundation; All
# Rights Reserved. See: http://docs.python.org/license.html

import logging
import traceback
import unittest

from nose2 import events

log = logging.getLogger(__name__)
__unittest = True


class PluggableTestLoader:
    """Test loader that defers all loading to plugins

    :param session: Test run session.

    .. attribute :: suiteClass

       Suite class to use. Default: :class:`unittest.TestSuite`.

    """

    suiteClass = unittest.TestSuite

    def __init__(self, session):
        self.session = session

    def loadTestsFromModule(self, module):
        """Load tests from module.

        Fires :func:`loadTestsFromModule` hook.

        """
        evt = events.LoadFromModuleEvent(self, module)
        result = self.session.hooks.loadTestsFromModule(evt)
        if evt.handled:
            suite = result or self.suiteClass()
        else:
            suite = self.suiteClass(evt.extraTests)
        filterevt = events.ModuleSuiteEvent(self, module, suite)
        result = self.session.hooks.moduleLoadedSuite(filterevt)
        if result:
            return result or self.suiteClass()
        return filterevt.suite

    def loadTestsFromNames(self, testNames, module=None):
        """Load tests from test names.

        Fires :func:`loadTestsFromNames` hook.

        """
        event = events.LoadFromNamesEvent(self, testNames, module)
        result = self.session.hooks.loadTestsFromNames(event)
        log.debug("loadTestsFromNames event %s result %s", event, result)
        if event.handled:
            suites = result or []
        else:
            if event.names:
                suites = [self.loadTestsFromName(name, module) for name in event.names]
            elif module:
                suites = self.loadTestsFromModule(module)
        if event.extraTests:
            suites.extend(event.extraTests)
        return self.suiteClass(suites)

    def loadTestsFromName(self, name, module=None):
        """Load tests from test name.

        Fires :func:`loadTestsFromName` hook.

        """
        log.debug("loadTestsFromName %s/%s", name, module)
        event = events.LoadFromNameEvent(self, name, module)
        result = self.session.hooks.loadTestsFromName(event)
        if event.handled:
            suite = result or self.suiteClass()
            return suite
        return self.suiteClass(event.extraTests)

    def failedImport(self, name):
        """Make test case representing a failed import."""
        message = f"Failed to import test module: {name}\n{traceback.format_exc()}"
        return self._makeFailedTest("ModuleImportFailure", name, ImportError(message))

    def failedLoadTests(self, name, exception):
        """Make test case representing a failed test load."""
        return self._makeFailedTest("LoadTestsFailure", name, exception)

    def sortTestMethodsUsing(self, name):
        """Sort key for test case test methods."""
        return name.lower()

    def discover(self, start_dir=None, pattern=None):
        """Compatibility shim for ``load_tests`` protocol."""
        try:
            oldsd = self.session.startDir
            self.session.startDir = start_dir
            return self.loadTestsFromNames([])
        finally:
            self.session.startDir = oldsd

    def _makeFailedTest(self, classname, methodname, exception):
        if isinstance(exception, Exception):

            def testFailure(self):
                raise exception

        elif isinstance(exception, tuple) and len(exception) == 3:
            _, value, tb = exception

            def testFailure(self):
                raise value.with_traceback(tb)

        else:  # unreachable
            raise NotImplementedError

        attrs = {methodname: testFailure}
        TestClass = type(classname, (unittest.TestCase,), attrs)
        return self.suiteClass((TestClass(methodname),))

    def __repr__(self):
        return "<%s>" % self.__class__.__name__
