"""
This plugin randomizes the order of tests within a unittest.TestCase class also
can exclude test_a or test_z

"""
__test__ = False

import logging
from nose.plugins import Plugin
import random
import sys
import time
from nose.suite import ContextList

log = logging.getLogger(__name__)
CLASS_SPECIFIC_RANDOMIZE_TESTS_FIELD_NAME = 'randomize_tests_seed'

try:
    from factory.fuzzy import set_random_state as factory_set_random_state

    have_factory_boy = True
except ImportError:
    have_factory_boy = False

# fake-factory
try:
    from faker.generator import random as faker_random

    have_faker = True
except ImportError:
    have_faker = False

try:
    from numpy import random as np_random

    have_numpy = True
except ImportError:
    have_numpy = False

# Compat
if sys.version_info[0] == 2:  # Python 2
    map_return_type = list
else:
    map_return_type = map


class Randomize(Plugin):
    """
    Randomize the order of the tests within a unittest.TestCase class exclude test_a and test_z
    """
    name = 'disorder'
    # Generate a seed for deterministic behaviour
    seed = random.getrandbits(32)
    class_specific = False

    def options(self, parser, env):
        """Register commandline options.
        """
        Plugin.options(self, parser, env)
        parser.add_option('--seed', action='store', dest='seed', default=int(time.time()),
                          type=int,
                          help="Initialize the seed "
                               "for deterministic behavior in "
                               "reproducing failed tests")

    def configure(self, options, conf):
        """
        Configure plugin.
        """
        Plugin.configure(self, options, conf)
        if not self.enabled:
            return
        self.options = options

    def setOutputStream(self, stream):
        if not self.enabled:
            return
        self.output_stream = stream
        dd = "Using --randomly-seed={seed}".format(seed=self.options.seed)
        self.output_stream.writeln(dd)

    def startContext(self, context):
        self.reset_random_seed()

    def startTest(self, test):
        self.reset_random_seed()

    def reset_random_seed(self):
        if not self.enabled:
            return

        if have_factory_boy:
            factory_set_random_state(self.random_state)

        if have_faker:
            faker_random.setstate(self.random_state)

        if have_numpy:
            np_random.set_state(self.random_state_numpy)

    @property
    def random_state(self):
        if not hasattr(self, '_random_state'):
            random.seed(self.options.seed)
            self._random_state = random.getstate()
        return self._random_state

    @property
    def random_state_numpy(self):
        # numpy uses its own random state implementation.
        if not have_numpy:
            raise RuntimeError('numpy not installed')
        if not hasattr(self, '_random_state_numpy'):
            np_random.seed(self.options.seed)
            self._random_state_numpy = np_random.get_state()
        return self._random_state_numpy

    def prepareTestLoader(self, loader):
        """
        Randomize the order of tests loaded from modules and from classes.

        This is a hack. We take the class of the existing, passed in loader
        (normally nose.loader.Loader) and subclass it to monkey-patch in
        shuffle calls for module and case loading when they requested, and then
        mutate the existing test loader's class to this new subclass.

        This is somewhat horrible, but nose's plugin infrastructure isn't so
        flexible - there is no way to wrap just the loader without risking
        interfering with other plugins (if you return anything, no other plugin
        may do anything to the loader).
        """
        if not self.enabled:
            return

        options = self.options

        class ShuffledLoader(loader.__class__):
            def loadTestsFromModule(self, *args, **kwargs):
                """
                Temporarily wrap self.suiteClass with a function that shuffles
                any ContextList instances that the super() call will pass it.
                """
                orig_suiteClass = self.suiteClass

                def hackSuiteClass(tests, **kwargs):
                    if isinstance(tests, ContextList):
                        random.seed(options.seed)
                        random.shuffle(tests.tests)
                    return orig_suiteClass(tests, **kwargs)

                self.suiteClass = hackSuiteClass
                suite = super(ShuffledLoader, self).loadTestsFromModule(
                    *args, **kwargs)

                self.suiteClass = orig_suiteClass

                return suite

            def loadTestsFromTestCase(self, testCaseClass):
                """
                Temporarily wrap self.suiteClass with a function that shuffles
                any list of tests that the super() call will pass it.
                """
                orig_suiteClass = self.suiteClass

                def hackSuiteClass(tests, **kwargs):
                    if isinstance(tests, map_return_type):
                        randomized_tests = []
                        test_dict = {}
                        tests = list(tests)
                        random.seed(options.seed)
                        for t in range(len(tests)-1, -1, -1):
                            if tests[t]._testMethodName.startswith('test_a') \
                                    or tests[t]._testMethodName.\
                                    startswith('test_z'):
                                test_index = t
                                test_dict[test_index] = tests[t]
                                tests.pop(test_index)
                            else:
                                randomized_tests.append(tests[t])
                        random.shuffle(randomized_tests)
                        for key, value in test_dict.items():
                            randomized_tests.insert(key, value)
                        tests = (t for t in randomized_tests)
                    return orig_suiteClass(tests, **kwargs)

                self.suiteClass = hackSuiteClass

                suite = super(ShuffledLoader, self).loadTestsFromTestCase(
                    testCaseClass)

                self.suiteClass = orig_suiteClass

                return suite

        # Directly mutate the class of loader... eww
        loader.__class__ = ShuffledLoader

        # Tell the plugin infrastructure we did nothing so 'loader', as mutated
        # above, continues to be used
        return None


