# -*- coding: utf-8; mode: django -*-
import os
import traceback
from datetime import datetime
from itertools import groupby
from xml.sax.saxutils import XMLGenerator
from xml.sax.xmlreader import AttributesImpl
from django.conf import settings
from django.test.simple import DjangoTestSuiteRunner
from django.test.testcases import TestCase
from django.utils.unittest import TestSuite, TextTestResult, TextTestRunner
from django_jenkins import signals
from django_jenkins.functions import total_seconds

try:
    from django.test.simple import reorder_suite
except ImportError:
    from django.test.runner import reorder_suite

try:
    from django.utils.encoding import smart_text
except ImportError:
    from django.utils.encoding import smart_unicode as smart_text


STDOUT_LINE = '\nStdout:\n%s'
STDERR_LINE = '\nStderr:\n%s'


class TestInfo(object):
    class RESULT(object):
        SUCCESS = 0
        ERROR = 1
        FAILURE = 2
        EXPECTED_FAILURE = 3
        UNEXPECTED_SUCCESS = 4
        SKIPPED = 5

    __slots__ = ('test_method', 'start_time', 'end_time',
                 'err', 'stdout', 'stderr', 'result', 'reason')

    def __init__(self, **kwargs):
        for slot_name in self.__slots__:
            setattr(self, slot_name, None)
        for key, value in kwargs.items():
            setattr(self, key, value)


class XMLTestResult(TextTestResult):
    """
    Dumps xml junit output as well as text
    """
    def __init__(self, *args, **kwargs):
        super(XMLTestResult, self).__init__(*args, **kwargs)
        self.testInfos = []
        self.currentTestInfo = None

    def startTestRun(self):
        """
        Called once before any tests are executed.
        """
        super(XMLTestResult, self).startTestRun()

    def startTest(self, test):
        """
        Called when the given test is about to be run
        """
        self.currentTestInfo = TestInfo(test_method=test,
                                        start_time=datetime.now())
        super(XMLTestResult, self).startTest(test)

    def stopTest(self, test):
        """
        Called when the given test has been run
        """
        self.currentTestInfo.end_time = datetime.now()
        self.currentTestInfo.stdout = self._stdout_buffer.getvalue()
        self.currentTestInfo.stderr = self._stderr_buffer.getvalue()
        self.testInfos.append(self.currentTestInfo)
        super(XMLTestResult, self).stopTest(test)

    def addError(self, test, err):
        """
        Called when an error has occurred. 'err' is a tuple of values as
        returned by sys.exc_info()
        """
        self.currentTestInfo.result = TestInfo.RESULT.ERROR
        self.currentTestInfo.err = err
        super(XMLTestResult, self).addError(test, err)
        signals.test_error.send(sender=self, test=test, err=err)

    def addFailure(self, test, err):
        """
        Called when an error has occurred. 'err' is a tuple of values as
        returned by sys.exc_info().
        """
        self.currentTestInfo.result = TestInfo.RESULT.FAILURE
        self.currentTestInfo.err = err
        super(XMLTestResult, self).addFailure(test, err)
        signals.test_failure.send(sender=self, test=test, err=err)

    def addSuccess(self, test):
        """
        Called when a test has completed successfully
        """
        self.currentTestInfo.result = TestInfo.RESULT.SUCCESS
        super(XMLTestResult, self).addSuccess(test)
        signals.test_success.send(sender=self, test=test)

    def addSkip(self, test, reason):
        """
        Called when a test is skipped.
        """
        self.currentTestInfo.result = TestInfo.RESULT.SKIPPED
        self.currentTestInfo.reason = reason
        super(XMLTestResult, self).addSkip(test, reason)

    def addExpectedFailure(self, test, err):
        """
        Called when an expected failure/error occured.
        """
        self.currentTestInfo.result = TestInfo.RESULT.EXPECTED_FAILURE
        self.currentTestInfo.err = err
        super(XMLTestResult, self).addExpectedFailure(test, err)
        signals.test_expected_failure.send(sender=self, test=test, err=err)

    def addUnexpectedSuccess(self, test):
        """
        Called when a test was expected to fail, but succeed.
        """
        self.currentTestInfo.result = TestInfo.RESULT.UNEXPECTED_SUCCESS
        super(XMLTestResult, self).addUnexpectedSuccess(test)
        signals.test_unexpected_success.send(sender=self, test=test)

    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
        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)

        if self.buffer:
            if self._stdout_buffer:
                output = self._stdout_buffer.getvalue()
                if output:
                    if not output.endswith('\n'):
                        output += '\n'
                    msgLines.append(STDOUT_LINE % output)
            if self._stderr_buffer:
                error = self._stderr_buffer.getvalue()
                if error:
                    if not error.endswith('\n'):
                        error += '\n'
                    msgLines.append(STDERR_LINE % error)
        return ''.join(msgLines)

    def test_method_name(self, test):
        """
        Returns the test method name.
        """
        test_id = test.id()
        return test_id.split('.')[-1]

    def test_case_name(self, test):
        """
        Returns test case name
        """
        testcase = type(test)
        module = testcase.__module__ + '.'
        if module == '__main__.':
            module = ''
        return module + testcase.__name__

    def dump_xml(self, output_dir):
        """
        Dumps test result to xml
        """
        self.buffer = False

        if not os.path.exists(output_dir):
            os.makedirs(output_dir)

        with open(os.path.join(output_dir, 'junit.xml'), 'w') as output:
            document = XMLGenerator(output, 'utf-8')
            document.startDocument()
            document.startElement('testsuites', AttributesImpl({}))

            suites = groupby(self.testInfos,
                             key=lambda test_info: self.test_case_name(
                                                        test_info.test_method))
            for suite_name, suite in suites:
                document.startElement('testsuite',
                                      AttributesImpl({'name': suite_name}))

                for test_info in suite:
                    document.startElement('testcase', AttributesImpl({
                        'classname': suite_name,
                        'name': self.test_method_name(test_info.test_method),
                        'time': '%3f' % total_seconds(
                                    test_info.end_time - test_info.start_time)
                    }))

                    if test_info.result == TestInfo.RESULT.ERROR:
                        document.startElement('error', AttributesImpl({
                            'message': smart_text(test_info.err[1])
                        }))
                        document.characters(self._exc_info_to_string(
                                        test_info.err, test_info.test_method))
                        document.endElement('error')
                    elif test_info.result == TestInfo.RESULT.FAILURE:
                        document.startElement('failure', AttributesImpl({
                            'message': smart_text(test_info.err[1])
                        }))
                        document.characters(self._exc_info_to_string(
                                        test_info.err, test_info.test_method))
                        document.endElement('failure')
                    elif test_info.result == \
                                    TestInfo.RESULT.UNEXPECTED_SUCCESS:
                        document.startElement('error', AttributesImpl({
                            'message': 'Unexpected success'
                        }))
                        document.endElement('error')
                    elif test_info.result == TestInfo.RESULT.SKIPPED:
                        document.startElement('skipped', AttributesImpl({}))
                        document.characters(test_info.reason)
                        document.endElement('skipped')

                    if test_info.stdout:
                        document.startElement('system-out', AttributesImpl({}))
                        document.characters(test_info.stdout)
                        document.endElement('system-out')

                    if test_info.stderr:
                        document.startElement('system-err', AttributesImpl({}))
                        document.characters(test_info.stderr)
                        document.endElement('system-err')
                    document.endElement('testcase')

                document.endElement('testsuite')

            document.endElement('testsuites')
            document.endDocument()


class CITestSuiteRunner(DjangoTestSuiteRunner):
    """
    Continuous integration test runner
    """
    def __init__(self, output_dir, with_reports=True, **kwargs):
        super(CITestSuiteRunner, self).__init__(**kwargs)
        self.with_reports = with_reports
        self.output_dir = output_dir

    def setup_test_environment(self, **kwargs):
        super(CITestSuiteRunner, self).setup_test_environment()
        signals.setup_test_environment.send(sender=self)

    def teardown_test_environment(self, **kwargs):
        super(CITestSuiteRunner, self).teardown_test_environment()
        signals.teardown_test_environment.send(sender=self)

    def setup_databases(self):
        if 'south' in settings.INSTALLED_APPS:
            from south.management.commands import (
                    patch_for_test_db_setup  # pylint: disable=F0401
                    )
            patch_for_test_db_setup()
        return super(CITestSuiteRunner, self).setup_databases()

    def build_suite(self, test_labels, extra_tests=None, **kwargs):
        suite = TestSuite()
        signals.build_suite.send(sender=self, suite=suite)
        return reorder_suite(suite, (TestCase,))

    def run_suite(self, suite, **kwargs):
        signals.before_suite_run.send(sender=self)
        result = TextTestRunner(buffer=True,
                resultclass=XMLTestResult,
                verbosity=self.verbosity
                ).run(suite)
        if self.with_reports:
            result.dump_xml(self.output_dir)
        signals.after_suite_run.send(sender=self)
        return result
