"""
Load tests from generators.

This plugin implements :func:`loadTestFromTestCase`,
:func:`loadTestsFromName` and :func:`loadTestFromModule` to enable
loading tests from generators.

Generators may be functions or methods in test cases. In either case,
they must yield a callable and arguments for that callable once for
each test they generate. The callable and arguments may all be in one
tuple, or the arguments may be grouped into a separate tuple::

  def test_gen():
      yield check, 1, 2
      yield check, (1, 2)

To address a particular generated test via a command-line test name,
append a colon (':') followed by the index (*starting from 1*) of the
generated case you want to execute.

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

import functools
import logging
import sys
import types
import unittest

from nose2 import exceptions, util
from nose2.events import Plugin


log = logging.getLogger(__name__)
__unittest = True


class Generators(Plugin):

    """Loader plugin that loads generator tests"""
    alwaysOn = True
    configSection = 'generators'

    def registerInSubprocess(self, event):
        event.pluginClasses.append(self.__class__)

    def unpack(self, generator):
        for index, func_args in enumerate(generator):
            try:
                func, args = func_args
                if not isinstance(args, tuple):
                    args = (args,)
                yield index, (func, args)
            except ValueError:
                func, args = func_args[0], func_args[1:]
                yield index, (func, args)

    def loadTestsFromTestCase(self, event):
        """Load generator tests from test case"""
        log.debug('loadTestsFromTestCase %s', event.testCase)
        testCaseClass = event.testCase
        for name in dir(testCaseClass):
            method = getattr(testCaseClass, name)
            if (name.startswith(self.session.testMethodPrefix) and
                hasattr(getattr(testCaseClass, name), '__call__') and
                util.isgenerator(method)):
                instance = testCaseClass(name)
                event.extraTests.extend(
                    self._testsFromGenerator(
                        event, name, method(instance), testCaseClass)
                )

    def loadTestsFromTestClass(self, event):
        testCaseClass = event.testCase
        for name in dir(testCaseClass):
            method = getattr(testCaseClass, name)
            if (name.startswith(self.session.testMethodPrefix) and
                hasattr(getattr(testCaseClass, name), '__call__') and
                util.isgenerator(method)):
                instance = testCaseClass()
                event.extraTests.extend(
                    self._testsFromGeneratorMethod(
                        event, name, method, instance)
                )

    def getTestCaseNames(self, event):
        """Get generator test case names from test case class"""
        log.debug('getTestCaseNames %s', event.testCase)
        names = filter(event.isTestMethod, dir(event.testCase))
        klass = event.testCase
        for name in names:
            method = getattr(klass, name)
            if util.isgenerator(method):
                event.excludedNames.append(name)

    def getTestMethodNames(self, event):
        return self.getTestCaseNames(event)

    def loadTestsFromName(self, event):
        """Load tests from generator named on command line"""
        original_name = name = event.name
        module = event.module
        try:
            result = util.test_from_name(name, module)
        except (AttributeError, ImportError):
            event.handled = True
            return event.loader.failedLoadTests(name, sys.exc_info())
        if result is None:
            # we can't find it - let the default case handle it
            return

        parent, obj, name, index = result
        if not util.isgenerator(obj):
            return

        if (index is None and not
            isinstance(parent, type) and not
            isinstance(obj, types.FunctionType)):
            log.debug("Don't know how to load generator tests from %s", obj)
            return

        if (parent and
            isinstance(parent, type) and
            issubclass(parent, unittest.TestCase)):
            # generator method in test case
            instance = parent(obj.__name__)
            tests = list(
                self._testsFromGenerator(
                    event, obj.__name__, obj(instance), parent)
            )
        elif (parent and
              isinstance(parent, type)):
              # generator method in test class
            method = obj
            instance = parent()
            tests = list(
                self._testsFromGeneratorMethod(event, name, method, instance)
            )
        else:
            # generator func
            tests = list(self._testsFromGeneratorFunc(event, obj))

        if index is not None:
            try:
                tests = [tests[index - 1]]
            except IndexError:
                raise exceptions.TestNotFoundError(original_name)

        suite = event.loader.suiteClass()
        suite.addTests(tests)
        event.handled = True
        return suite

    def loadTestsFromModule(self, event):
        """Load tests from generator functions in a module"""
        module = event.module

        def is_test(obj):
            return (obj.__name__.startswith(self.session.testMethodPrefix)
                    and util.isgenerator(obj))
        tests = []
        for name in dir(module):
            obj = getattr(module, name)
            if isinstance(obj, types.FunctionType) and is_test(obj):
                tests.extend(self._testsFromGeneratorFunc(event, obj))
        event.extraTests.extend(tests)

    def _testsFromGenerator(self, event, name, generator, testCaseClass):
        try:
            for index, (func, args) in self.unpack(generator):
                method_name = util.name_from_args(name, index, args)
                setattr(testCaseClass, method_name, None)
                instance = testCaseClass(method_name)
                delattr(testCaseClass, method_name)

                def method(func=func, args=args):
                    return func(*args)
                method = functools.update_wrapper(method, func)
                setattr(instance, method_name, method)
                yield instance
        except Exception as e:
            test_name = '%s.%s.%s' % (testCaseClass.__module__,
                                      testCaseClass.__name__,
                                      name)
            yield event.loader.failedLoadTests(test_name, e)

    def _testsFromGeneratorFunc(self, event, obj):
        extras = list(obj())
        name = '%s.%s' % (obj.__module__, obj.__name__)
        args = {}
        setUp = getattr(obj, 'setUp', None)
        tearDown = getattr(obj, 'tearDown', None)
        if setUp is not None:
            args['setUp'] = setUp
        if tearDown is not None:
            args['tearDown'] = tearDown

        def createTest(name):
            return util.transplant_class(
                GeneratorFunctionCase, obj.__module__)(name, **args)
        for test in self._testsFromGenerator(event, name, extras, createTest):
            yield test

    def _testsFromGeneratorMethod(self, event, name, method, instance):
        extras = list(method(instance))
        name = "%s.%s.%s" % (instance.__class__.__module__,
                             instance.__class__.__name__,
                             method.__name__)
        args = {}
        setUp = getattr(instance, 'setUp', None)
        tearDown = getattr(instance, 'tearDown', None)
        if setUp is not None:
            args['setUp'] = setUp
        if tearDown is not None:
            args['tearDown'] = tearDown

        def createTest(name):
            return util.transplant_class(
                GeneratorMethodCase(instance.__class__),
                instance.__class__.__module__)(name, **args)
        for test in self._testsFromGenerator(event, name, extras, createTest):
            yield test


class GeneratorFunctionCase(unittest.FunctionTestCase):

    def __init__(self, name, **args):
        self._funcName = name
        unittest.FunctionTestCase.__init__(self, None, **args)

    _testFunc = property(lambda self: getattr(self, self._funcName),
                         lambda self, func: None)

    def __repr__(self):
        return self._funcName

    id = __str__ = __repr__


def GeneratorMethodCase(cls):
    class _GeneratorMethodCase(GeneratorFunctionCase):

        @classmethod
        def setUpClass(klass):
            if hasattr(cls, 'setUpClass'):
                cls.setUpClass()

        @classmethod
        def tearDownClass(klass):
            if hasattr(cls, 'tearDownClass'):
                cls.tearDownClass()
    return _GeneratorMethodCase
