# -*- coding: utf-8 -*-

"""This module provides the
:py:class:`~tdda.referencetest.referencetestcase.ReferenceTestCase` class,
which extends the
standard :py:class:`unittest.TestCase` test-case class, augmenting it
with methods for checking correctness of files against reference data.

It also provides a main() function, which can be used to run (and
regenerate) reference tests which have been implemented using subclasses
of ReferenceTestCase.

For example::

    from tdda.referencetest.referencetestcase import ReferenceTestCase
    import my_module

    class MyTest(ReferenceTestCase):
        def test_my_csv_function(self):
            result = my_module.my_csv_function(self.tmp_dir)
            self.assertCSVFileCorrect(result, 'result.csv')

        def test_my_pandas_dataframe_function(self):
            result = my_module.my_dataframe_function()
            self.assertDataFrameCorrect(result, 'result.csv')

        def test_my_table_function(self):
            result = my_module.my_table_function()
            self.assertStringCorrect(result, 'table.txt', kind='table')

        def test_my_graph_function(self):
            result = my_module.my_graph_function()
            self.assertStringCorrect(result, 'graph.txt', kind='graph')

    MyTest.set_default_data_location('testdata')

    if __name__ == '__main__':
        ReferenceTestCase.main()

Tagged Tests
~~~~~~~~~~~~

If the tests are run with the ``--tagged`` or ``-1`` (the digit one)
command-line option, then only tests that have been decorated with
``referencetestcase.tag``, are run. This is a mechanism for allowing
only a chosen subset of tests to be run, which is useful during
development. The ``@tag`` decorator can be applied to either test
classes or test methods.

If the tests are run with the ``--istagged`` or ``-0`` (the digit
zero) command-line option, then no tests are run; instead, the
framework reports the full module names of any test classes that have
been decorated with ``@tag``, or which contain any tests that have been
decorated with ``@tag``.

For example::

    from tdda.referencetest.referencetestcase import ReferenceTestCase, tag
    import my_module

    class MyTest1(ReferenceTestCase):
        @tag
        def test_a(self):
            ...

        def test_b(self):
            ...

    @tag
    class MyTest2(ReferenceTestCase):
        def test_x(self):
            ...

        def test_y(self):
            ...

If run with ``python mytests.py --tagged``, only the tagged tests are
run (``MyTest1.test_a``, ``MyTest2.test_x`` and ``MyTest2.test_y``).

Regeneration of Results
~~~~~~~~~~~~~~~~~~~~~~~

When its main is run with ``--write-all`` or ``--write`` (or ``-W`` or ``-w``
respectively), it causes the framework to regenerate reference data
files. Different kinds of reference results can be regenerated by
passing in a comma-separated list of *kind* names immediately after
the ``--write`` option. If no list of *kind* names is provided, then all
test results will be regenerated.

To regenerate all reference results (or generate them for the first time)::

    python my_tests.py --write-all

To regenerate just a particular kind of reference (e.g. table results)::

    python my_tests.py --write table

To regenerate a number of different kinds of reference (e.g. both table
and graph results)::

    python my_tests.py --write table graph

``unittest`` Integration Details
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

"""

from __future__ import absolute_import
from __future__ import print_function
from __future__ import division

import sys
import unittest

from tdda.referencetest.referencetest import ReferenceTest


def tag(test):
    """
    Decorator for tests, so that you can specify you only want to
    run a tagged subset of tests, with the -1 or --tagged option.
    """
    test._tagged = True
    return test


class ReferenceTestCase(unittest.TestCase, ReferenceTest):
    """
    Wrapper around the
    :py:class:`~tdda.referencetest.referencetest.ReferenceTest`
    class to allow it to operate as a test-case class using the
    ``unittest`` testing framework.

    The ``ReferenceTestCase`` class is a mix-in of
    :py:class:`unittest.TestCase`
    and :py:class:`~tdda.referencetest.referencetest.ReferenceTest`,
    so it can be used as the base class for unit tests, allowing the
    tests to use any of the standard ``unittest`` *assert* methods,
    and also use any of the ``referencetest`` *assert* extensions.
    """
    tag = tag

    def __init__(self, *args, **kwargs):
        # Initializer for a ReferenceTestCase instance.
        # This is called automatically by the ``unittest`` runtime framework.
        unittest.TestCase.__init__(self, *args, **kwargs)
        ReferenceTest.__init__(self, self.assertTrue)

    @staticmethod
    def main():
        """
        Wrapper around the :py:func:`unittest.main()` entry point.

        This is the same as the :py:func:`~tdda.referencetestcase.main()`
        function, and is provided just as a convenience, as it means that
        tests using the ``ReferenceTestCase`` class only need to import
        that single class on its own.
        """
        _run_tests(_set_write_from_argv())


def _run_tests(argv):
    """
    If the -1 or --tagged option is set, then only run tagged tests.
    """
    if argv is None:
        argv = sys.argv
    tagged = False
    check = False
    for option in ('-1', '--tagged', '-0', '--istagged'):
        if option in argv:
            idx = argv.index(option)
            argv = argv[:idx] + argv[idx+1:]
            if option in ('-0', '--istagged'):
                check = True
            else:
                tagged = True
    loader = (TaggedTestLoader(check) if tagged or check
              else unittest.defaultTestLoader)
    unittest.main(argv=argv, testLoader=loader)


def _set_write_from_argv(argv=None):
    """
    This is used to set the class's write flag if a **-write** or
    **--write-all** (or **-W** or **-w**) option is passed on the
    command line, either using the argv provided,
    or :py:data:`sys.argv` otherwise.

    The **-write-all** (or **-W**) option takes no parameters, and turns on
    reference-regeneration for all kinds of results.

    The **-write** (or **-w**) option takes a list of parameter, consisting
    of names of result kinds to be regenerated. The names can be separate
    parameters, or they can be a single comma-separated parameter.

    The framework reports on each file being regenerated, by default. Use
    The **-wquiet** option to make it rewrite files quietly.

    The **--wquiet** option causes files to be rewritten silently.

    ``argv`` or :py:data:`sys.argv` is returned, with any of the *rewrite*
    options removed.
    """
    if argv is None:
        argv = sys.argv
    for quietflag in ('-wquiet', '--wquiet'):
        if quietflag in argv:
            idx = argv.index(quietflag)
            ReferenceTestCase.set_defaults(verbose=False)
            argv = argv[:idx] + argv[idx+1:]
    if any(writeflag in argv for writeflag in ('-W', '--W', '--write-all')):
        ReferenceTestCase.set_regeneration()
    for writeflag in ('-W', '--W', '--write-all'):
        if writeflag in argv:
            idx = argv.index(writeflag)
            if idx:
                return argv[:idx] + argv[idx+1:]
    for writeflag in ('-w', '--w', '--write'):
        if writeflag in argv:
            idx = argv.index(writeflag)
            if idx:
                if idx < len(argv) - 1:
                    for r in argv[idx+1:]:
                        for kind in r.split(','):
                            ReferenceTestCase.set_regeneration(kind)
                else:
                    raise Exception('--write option requires parameters; '
                                    'use --write-all to regenerate all '
                                    'reference results')
            return argv[:idx]
    else:
        return argv


class TaggedTestLoader(unittest.TestLoader):
    """
    Subclass of TestLoader, which strips out any non-tagged tests.
    """
    def __init__(self, check):
        unittest.TestLoader.__init__(self)
        self.check = check

    def loadTestsFromTestCase(self, *args, **kwargs):
        suite = unittest.TestLoader.loadTestsFromTestCase(self, *args,
                                                          **kwargs)
        return self._tagged_tests_only(suite)

    def loadTestsFromModule(self, *args, **kwargs):
        suite = unittest.TestLoader.loadTestsFromModule(self, *args, **kwargs)
        return self._tagged_tests_only(suite)

    def loadTestsFromName(self, *args, **kwargs):
        suite = unittest.TestLoader.loadTestsFromName(self, *args, **kwargs)
        return self._tagged_tests_only(suite)

    def loadTestsFromNames(self, *args, **kwargs):
        suite = unittest.TestLoader.loadTestsFromNames(self, *args, **kwargs)
        return self._tagged_tests_only(suite)

    def _tagged_tests_only(self, suite):
        newsuite = unittest.TestSuite()
        cases = set()
        for test in suite:
            if isinstance(test, unittest.suite.TestSuite):
                test = self._tagged_tests_only(test)
            if self.check and not isinstance(test, unittest.suite.TestSuite):
                cases.add('%s.%s' % (test.__class__.__module__,
                                     test.__class__.__name__))
            else:
                newsuite.addTest(test)
        if self.check:
            for module in cases:
                print(module)
        return newsuite

    def getTestCaseNames(self, testCaseClass):
        names = unittest.TestLoader.getTestCaseNames(self, testCaseClass)
        if hasattr(testCaseClass, '_tagged'):
            return names
        else:
            return [name for name in names
                         if hasattr(getattr(testCaseClass, name), '_tagged')]


def main():
    """
    Wrapper around the :py:func:`unittest.main()` entry point.
    """
    ReferenceTestCase.main()

