import logging
import re

import six

from nose2 import events, util
from nose2.suite import LayerSuite
from nose2.compat import unittest, OrderedDict

BRIGHT = r'\033[1m'
RESET = r'\033[0m'

__unittest = True

log = logging.getLogger(__name__)


class Layers(events.Plugin):
    alwaysOn = True

    def startTestRun(self, event):
        event.suite = self._makeLayerSuite(event)

    def _makeLayerSuite(self, event):
        return self._sortByLayers(
            event.suite, self.session.testLoader.suiteClass)

    def _sortByLayers(self, suite, suiteClass):
        top = suiteClass()
        # first find all of the layers mentioned
        layers = OrderedDict()
        for test in self._flatten(suite):
            # split tests up into buckets by layer
            layer = getattr(test, 'layer', None)
            if layer:
                layers.setdefault(layer, LayerSuite(layer=layer)).addTest(test)
            else:
                top.addTest(test)

        # then organize layers into a tree
        remaining = list(layers.keys())
        seen = set()
        tree = {}
        while remaining:
            ly = remaining.pop()
            if ly in seen:
                continue
            seen.add(ly)
            # superclasses of this layer
            if ly is None:
                deps = []
            else:
                deps = [cls for cls in util.bases_and_mixins(ly)
                        if cls is not object]
                deps.reverse()
            if not deps:
                # layer is top-level
                self._addToTree(tree, ly, None)
            else:
                outer = ly
                while deps:
                    inner, outer = outer, deps.pop()
                    self._addToTree(tree, inner, outer)
                    if outer not in layers:
                        remaining.append(outer)
                        layers[outer] = LayerSuite(layer=outer)

        # finally build the top-level suite
        self._treeToSuite(tree, None, top, layers)
        # printtree(top)
        return top

    def _addToTree(self, tree, inner, outer):
        found = False
        for k, v in tree.items():
            if inner in v:
                found = True
                if outer is not None:
                    v.remove(inner)
                break
        if outer is not None or not found:
            tree.setdefault(outer, []).append(inner)

    def _treeToSuite(self, tree, key, suite, layers):
        mysuite = layers.get(key, None)
        if mysuite:
            suite.addTest(mysuite)
            suite = mysuite
        sublayers = tree.get(key, [])
        # ensure that layers with a set order are in order
        sublayers.sort(key=self._sortKey)
        log.debug('sorted sublayers of %s (%s): %s', mysuite,
                  getattr(mysuite, 'layer', 'no layer'), sublayers)
        for layer in sublayers:
            self._treeToSuite(tree, layer, suite, layers)

    def _flatten(self, suite):
        out = []
        for test in suite:
            try:
                out.extend(self._flatten(test))
            except TypeError:
                out.append(test)
        return out

    def _sortKey(self, layer):
        pos = getattr(layer, 'position', None)
        # ... lame
        if pos is not None:
            key = six.u("%04d") % pos
        else:
            key = layer.__name__
        return key


class LayerReporter(events.Plugin):
    commandLineSwitch = (
        None, 'layer-reporter', 'Add layer information to test reports')
    configSection = 'layer-reporter'

    def __init__(self):
        self.indent = self.config.as_str('indent', '  ')
        self.colors = self.config.as_bool('colors', False)
        self.highlight_words = self.config.as_list('highlight-words',
                                                   ['A', 'having', 'should'])
        self.highlight_re = re.compile(
            r'\b(%s)\b' % '|'.join(self.highlight_words))
        self.layersReported = set()

    def reportStartTest(self, event):
        if self.session.verbosity < 2:
            return
        test = event.testEvent.test
        layer = getattr(test, 'layer', None)
        if not layer:
            return
        for ix, lys in enumerate(util.ancestry(layer)):
            for layer in lys:
                if layer not in self.layersReported:
                    desc = self.describeLayer(layer)
                    event.stream.writeln('%s%s' % (self.indent * ix, desc))
                    self.layersReported.add(layer)
        event.stream.write(self.indent * (ix + 1))

    def describeLayer(self, layer):
        return self.format(getattr(layer, 'description', layer.__name__))

    def format(self, st):
        if self.colors:
            return self.highlight_re.sub(r'%s\1%s' % (BRIGHT, RESET), st)
        return st

    def describeTest(self, event):
        if hasattr(event.test, 'methodDescription'):
            event.description = self.format(event.test.methodDescription())
        if event.errorList and hasattr(event.test, 'layer'):
            # walk back layers to build full description
            self.describeLayers(event)

    def describeLayers(self, event):
        desc = [event.description]
        base = event.test.layer
        for layer in (base.__mro__ + getattr(base, 'mixins', ())):
            if layer is object:
                continue
            desc.append(self.describeLayer(layer))
        desc.reverse()
        event.description = ' '.join(desc)


# for debugging
def printtree(suite, indent=''):
    six.print_('%s%s ->' % (indent, getattr(suite, 'layer', 'no layer')))
    for test in suite:
        if isinstance(test, unittest.BaseTestSuite):
            printtree(test, indent + '  ')
        else:
            six.print_('%s %s' % (indent, test))
    six.print_('%s<- %s' % (indent, getattr(suite, 'layer', 'no layer')))