# -*- coding: utf-8 -*-
#
# Adds Parameterized tests for Python's unittest module
#
# Code from: parameterizedtestcase, version: 0.1.0
# Homepage: https://github.com/msabramo/python_unittest_parameterized_test_case
# Author: Marc Abramowitz, email: marc@marc-abramowitz.com
# License: MIT
#
# Fixed for to work in Python 2 & 3 with "add_metaclass" decorator from six
# https://pypi.python.org/pypi/six
# Author: Benjamin Peterson
# License: MIT
#
# Use like this:
#
#    from parameterizedtestcase import ParameterizedTestCase
#
#    class MyTests(ParameterizedTestCase):
#        @ParameterizedTestCase.parameterize(
#            ("input", "expected_output"),
#            [
#                ("2+4", 6),
#                ("3+5", 8),
#                ("6*9", 54),
#            ]
#        )
#        def test_eval(self, input, expected_output):
#            self.assertEqual(eval(input), expected_output)

try:
    import unittest2 as unittest
except ImportError:  # pragma: no cover
    import unittest

from functools import wraps
import collections


def add_metaclass(metaclass):
    """Class decorator for creating a class with a metaclass."""
    def wrapper(cls):
        orig_vars = cls.__dict__.copy()
        orig_vars.pop('__dict__', None)
        orig_vars.pop('__weakref__', None)
        slots = orig_vars.get('__slots__')
        if slots is not None:
            if isinstance(slots, str):
                slots = [slots]
            for slots_var in slots:
                orig_vars.pop(slots_var)
        return metaclass(cls.__name__, cls.__bases__, orig_vars)
    return wrapper


def augment_method_docstring(method, new_class_dict, classname,
                             param_names, param_values, new_method):
    param_assignments_str = '; '.join(
        ['%s = %s' % (k, v) for (k, v) in zip(param_names, param_values)])
    extra_doc = "%s (%s.%s) [with %s] " % (
        method.__name__, new_class_dict.get('__module__', '<module>'),
        classname, param_assignments_str)

    try:
        new_method.__doc__ = extra_doc + new_method.__doc__
    except TypeError:  # Catches when new_method.__doc__ is None
        new_method.__doc__ = extra_doc


class ParameterizedTestCaseMetaClass(type):
    method_counter = {}

    def __new__(meta, classname, bases, class_dict):
        new_class_dict = {}

        for attr_name, attr_value in list(class_dict.items()):
            if isinstance(attr_value, collections.Callable) and hasattr(attr_value, 'param_names'):
                # print("Processing attr_name = %r; attr_value = %r" % (
                #     attr_name, attr_value))

                method = attr_value
                param_names = attr_value.param_names
                data = attr_value.data
                func_name_format = attr_value.func_name_format

                meta.process_method(
                    classname, method, param_names, data, new_class_dict,
                    func_name_format)
            else:
                new_class_dict[attr_name] = attr_value

        return type.__new__(meta, classname, bases, new_class_dict)

    @classmethod
    def process_method(
            cls, classname, method, param_names, data, new_class_dict,
            func_name_format):
        method_counter = cls.method_counter

        for param_values in data:
            new_method = cls.new_method(method, param_values)
            method_counter[method.__name__] = \
                method_counter.get(method.__name__, 0) + 1
            case_data = dict(list(zip(param_names, param_values)))
            case_data['func_name'] = method.__name__
            case_data['case_num'] = method_counter[method.__name__]

            new_method.__name__ = func_name_format.format(**case_data)

            augment_method_docstring(
                method, new_class_dict, classname,
                param_names, param_values, new_method)
            new_class_dict[new_method.__name__] = new_method

    @classmethod
    def new_method(cls, method, param_values):
        @wraps(method)
        def new_method(self):
            return method(self, *param_values)

        return new_method

@add_metaclass(ParameterizedTestCaseMetaClass)
class ParameterizedTestMixin(object):
    @classmethod
    def parameterize(cls, param_names, data,
                     func_name_format='{func_name}_{case_num:05d}'):
        """Decorator for parameterizing a test method - example:

        @ParameterizedTestCase.parameterize(
            ("isbn", "expected_title"), [
                ("0262033844", "Introduction to Algorithms"),
                ("0321558146", "Campbell Essential Biology")])

        """

        def decorator(func):
            @wraps(func)
            def newfunc(*arg, **kwargs):
                return func(*arg, **kwargs)

            newfunc.param_names = param_names
            newfunc.data = data
            newfunc.func_name_format = func_name_format

            return newfunc

        return decorator

@add_metaclass(ParameterizedTestCaseMetaClass)
class ParameterizedTestCase(unittest.TestCase, ParameterizedTestMixin):
    pass
