'''
Plugin
~~~~~~~~~~~~~~~~~~~

.. autoclass:: Plugin
   :members:
   :member-order: bysource
   

Test Runner
~~~~~~~~~~~~~~~~~~~

.. autoclass:: TestRunner
   :members:
   :member-order: bysource

   
'''
import sys
import traceback
import unittest
import logging
from inspect import istraceback
from copy import deepcopy

from pulsar.utils.structures import AttributeDictionary

from .utils import TestFunction

__all__ = ['Plugin',
           'TestStream',
           'TestRunner',
           'TestResult',
           'Plugin',
           'LOGGER']


LOGGER = logging.getLogger('TestRunner')
STDOUT_LINE = '\nStdout:\n%s'
STDERR_LINE = '\nStderr:\n%s'


class Plugin(object):
    '''Interface for all classes which are part of of the :class:`TestRunner`,
including :class:`TestRunner` itself, :class:`TestResult`
and :class:`pulsar.apps.test.TestPlugin`.'''
    descriptions = None

    def configure(self, cfg):
        '''Called once just after construction of
a :class:`TestRunner` and therefore **before any test class is loaded**.
This is a chance to configure the :class:`Plugin` or global variables
which may affect the way tests are run.
If it returns something other than ``None`` (for example an abort message)
it will stop the configuration of all subsequent plugins and quit the test.

:parameter cfg: a :class:`pulsar.utils.config.Config`.
:return: ``None`` unless the tests runner must be stopped.
'''
        pass
    
    @property
    def name(self):
        return self.__class__.__name__.lower()

    def on_start(self):
        '''Called once by :class:`TestSuite` after all tests are loaded but
but before it starts running them.'''
        pass

    def on_end(self):
        '''Called once by :class:`TestSuite` just before it stops.'''
        pass

    def loadTestsFromTestCase(self, test_class):
        '''Called just before a ``test_class`` executes all its test
methods. This is run just before ``setUpClass`` method.'''
        pass
    
    def startTest(self, test):
        '''Called just before a ``test`` function is executed. This is run
just before ``_pre_setup`` method.'''
        pass

    def stopTest(self, test):
        '''Called just after a ``test`` function has finished. This is run just
after the ``_post_teardown`` method.'''
        pass
    
    def before_test_function_run(self, test, local):
        '''This function can be used by plugins to manipulate the *test*
behaviour in the process domain where the test run.'''
        return test
    
    def after_test_function_run(self, test, local, result, async):
        '''Given a test-function instance return a, possibly, modified test
instance. This function can be used by plugins to modify the behaviour of test
cases. By default it returns *test*.'''
        pass

    def addSuccess(self, test):
        pass

    def addFailure(self, test, err):
        pass

    def addError(self, test, err):
        pass

    def addSkip(self, test, reason):
        pass

    def printErrors(self):
        pass

    def printSummary(self, timeTaken):
        pass

    def import_module(self, mod, parent=None):
        return mod

    def getDescription(self, test):
        doc_first_line = test.shortDescription()
        teststr = '%s.%s' % (test.tag, test)
        if self.descriptions and doc_first_line:
            return '\n'.join((teststr, doc_first_line))
        else:
            return teststr


class TestResultProxy(Plugin):
    result = None
    stream = None

    @property
    def count(self):
        return self.result.count if self.result else 0

    @property
    def testsRun(self):
        return self.result.testsRun if self.result else 0


class TestStream(TestResultProxy):
    '''handle the writing of test results'''
    separator1 = '=' * 70
    separator2 = '-' * 70

    def __init__(self, stream, result, descriptions=True):
        self._handlers = {}
        self.stream = stream
        self.result = result
        self.descriptions = descriptions
        self.showAll = False
        self.dots = True

    def configure(self, cfg):
        verbosity = cfg.verbosity
        self.showAll = verbosity > 1
        self.dots = verbosity == 1

    def handler(self, name):
        return self._handlers.get(name, self.stream)

    def startTest(self, test):
        if self.showAll:
            self.head(test, 'Started')
            
    #def after_test_function_run(self, test, local, result, async):
    #    if async:
    #        self.startTest(test)

    def head(self, test, v):
        v = self.getDescription(test) + ' ... %s\n' % v
        self.stream.write(v)
        self.stream.flush()

    def addSuccess(self, test):
        if self.showAll:
            self.head(test, 'OK')
        elif self.dots:
            self.stream.write('.')
            self.stream.flush()

    def addError(self, test, err):
        if self.showAll:
            self.head(test, 'ERROR')
        elif self.dots:
            self.stream.write('E')
            self.stream.flush()

    def addFailure(self, test, err):
        if self.showAll:
            self.head(test, "FAIL")
        elif self.dots:
            self.stream.write('F')
            self.stream.flush()

    def addSkip(self, test, reason):
        if self.showAll:
            self.head(test,"skipped {0!r}".format(reason))
        elif self.dots:
            self.stream.write("s")
            self.stream.flush()

    def addExpectedFailure(self, test, err):
        if self.showAll:
            self.head(test, "expected failure")
        elif self.dots:
            self.stream.write("x")
            self.stream.flush()

    def addUnexpectedSuccess(self, test):
        if self.showAll:
            self.head(test, "unexpected success")
        elif self.dots:
            self.stream.write("u")
            self.stream.flush()

    def printErrors(self):
        if self.dots or self.showAll:
            self.stream.writeln()
        self.printErrorList('ERROR', self.result.errors)
        self.printErrorList('FAIL', self.result.failures)
        return True

    def printErrorList(self, flavour, errors):
        for test, err in errors:
            self.stream.writeln(self.separator1)
            self.stream.writeln("%s: %s" % (flavour,test))
            self.stream.writeln(self.separator2)
            self.stream.writeln("%s" % err)

    def printSummary(self, timeTaken):
        '''Write the summuray of tests results.'''
        stream = self.stream
        result = self.result
        self.printErrors()
        run = result.testsRun
        stream.writeln("Ran %d test%s in %.3fs" %
                            (run, run != 1 and "s" or "", timeTaken))
        stream.writeln()

        expectedFails = unexpectedSuccesses = skipped = 0
        results = map(len, (result.expectedFailures,
                            result.unexpectedSuccesses,
                            result.skipped))
        expectedFails, unexpectedSuccesses, skipped = results

        infos = []
        if not result.wasSuccessful():
            stream.write("FAILED")
            failed, errored = map(len, (result.failures, result.errors))
            if failed:
                infos.append("failures=%d" % failed)
            if errored:
                infos.append("errors=%d" % errored)
        else:
            stream.write("OK")
        if skipped:
            infos.append("skipped=%d" % skipped)
        if expectedFails:
            infos.append("expected failures=%d" % expectedFails)
        if unexpectedSuccesses:
            infos.append("unexpected successes=%d" % unexpectedSuccesses)
        if infos:
            stream.writeln(" (%s)" % (", ".join(infos),))
        else:
            stream.write("\n")

        return True


class TestResult(Plugin):
    '''A result of a test run.'''
    def __init__(self, descriptions=True):
        self.descriptions = descriptions
        self.testsRun = 0
        self._count = 0
        self.failures = []
        self.errors = []
        self.skipped = []
        self.expectedFailures = []
        self.unexpectedSuccesses = []

    @property
    def count(self):
        return self._count

    def startTest(self, test):
        self.testsRun += 1

    def addError(self, test, err):
        """Called when an error has occurred. 'err' is a tuple of values as
        returned by sys.exc_info().
        """
        self._add_error(test, err, self.errors)

    def addFailure(self, test, err):
        self._add_error(test, err, self.failures)

    def addSkip(self, test, reason):
        """Called when a test is skipped."""
        self.skipped.append((self.getDescription(test), reason))

    def addExpectedFailure(self, test, err):
        """Called when an expected failure/error occured."""
        self._add_error(test, err, self.expectedFailures)

    def addUnexpectedSuccess(self, test):
        """Called when a test was expected to fail, but succeed."""
        self.unexpectedSuccesses.append(self.getDescription(test))

    def _add_error(self, test, err, container):
        err = self._exc_info_to_string(err, test)
        test = self.getDescription(test)
        container.append((test, err))

    def add(self, result):
        self._count += 1
        self.testsRun += result.testsRun
        self.failures.extend(result.failures)
        self.errors.extend(result.errors)
        self.skipped.extend(result.skipped)
        self.expectedFailures.extend(result.expectedFailures)
        self.unexpectedSuccesses.extend(result.unexpectedSuccesses)

    def wasSuccessful(self):
        "Tells whether or not this result was a success"
        return len(self.failures) == len(self.errors) == 0

    def _exc_info_to_string(self, err, test):
        """Converts a sys.exc_info()-style tuple of values into a string."""
        exctype, value, tb = err
        # Skip test runner traceback levels
        if istraceback(tb):
            while tb and self._is_relevant_tb_level(tb):
                tb = tb.tb_next

            if exctype is test.failureException:
                # Skip assert*() traceback levels
                length = self._count_relevant_tb_levels(tb)
                msgLines = traceback.format_exception(exctype, value, tb, length)
            else:
                msgLines = traceback.format_exception(exctype, value, tb)
        else:
            msgLines = tb

        if not msgLines:
            if value:
                msgLines = [repr(value)]
            else:
                msgLines = ()

        return ''.join(msgLines)


    def _is_relevant_tb_level(self, tb):
        return '__unittest' in tb.tb_frame.f_globals

    def _count_relevant_tb_levels(self, tb):
        length = 0
        while tb and not self._is_relevant_tb_level(tb):
            length += 1
            tb = tb.tb_next
        return length


def testsafe(name, return_val=None):
    if not return_val:
        return_val = lambda c: None
    def _(self, *args):
        for p in self.plugins:
            try:
                c = getattr(p, name)(*args)
                if c:
                    return return_val(c)
            except Exception:
                LOGGER.critical('Unhadled error in %s.%s' % (p, name),
                                exc_info=True)
    return _                
            
    
class TestRunner(TestResultProxy):
    '''An asynchronous test runner'''
    def __init__(self, plugins, stream, writercls=None, descriptions=True,
                 logger=None):
        self.descriptions = descriptions
        self.plugins = []
        writercls = writercls or TestStream
        result = TestResult(descriptions=self.descriptions)
        stream = writercls(stream, result, descriptions=self.descriptions)
        for p in plugins:
            p = deepcopy(p)
            p.descriptions = self.descriptions
            p.result = result
            p.stream = stream
            self.plugins.append(p)
        self.plugins.append(result)
        self.plugins.append(stream)
        self.stream = stream
        self.result = result
        self.loader = unittest.TestLoader()

    configure = testsafe('configure', lambda c: c)        
    on_start = testsafe('on_start')
    on_end = testsafe('on_end')
    startTest = testsafe('startTest')    
    stopTest = testsafe('stopTest')
    addSuccess = testsafe('addSuccess')
    addFailure = testsafe('addFailure')
    addError = testsafe('addError')
    addSkip = testsafe('addSkip')
    printErrors = testsafe('printErrors')
    printSummary = testsafe('printSummary')

    def loadTestsFromTestCase(self, test_cls):
        '''Load all *test* functions for the test class *cls*.'''
        c = testsafe('loadTestsFromTestCase', lambda v: v)(self, test_cls)
        if c is None:
            return self.loader.loadTestsFromTestCase(test_cls)
        else:
            return c

    def add(self, result):
        self.result.add(result)

    def import_module(self, mod, parent=None):
        for p in self.plugins:
            mod = p.import_module(mod, parent)
            if not mod:
                return
        return mod

    def before_test_function_run(self, test):
        '''Called before the test run, in the test process domain.'''
        test.plugins = plugins = {}
        for p in self.plugins:
            local = AttributeDictionary()
            plugins[p.name] = local
            test = p.before_test_function_run(test, local) or test
        return test
    
    def after_test_function_run(self, test, result, async):
        '''Called before the test starts.'''
        for p in self.plugins:
            local = test.plugins.get(p.name)
            if local is not None:
                p.after_test_function_run(test, local, result, async)
        return result
            
    def run_test_function(self, test, func, timeout=None):
        '''Run function *func* which belong to *test*.
    
:parameter test: test instance or class
:parameter func: test function belonging to *test*
:parameter timeout: An optional timeout for asynchronous tests.
:return: an asynchronous result
'''
        if func is None:
            return func
        tfunc = getattr(func, 'testfunction', None)
        if tfunc is None:
            tfunc = TestFunction(func.__name__)
        return tfunc(test, timeout)