import inspect, types, warnings

from twisted.trial import runner, unittest

from nevow import athena, loaders, tags, page


class TestCase(athena.LiveFragment, unittest.TestCase):
    """
    Server-side component of a B{N}evow B{I}nteractive B{T}est.

    TestCases are L{athena.LiveFragment}s which correspond to one or more test
    methods which will be invoked on the client.  They are responsible for
    specifying a JavaScript widget class which defines the client portion of
    this set of tests, specifying a document which will be rendered for that
    JavaScript widget, and defining any methods which the client may need to
    invoke during the test.
    """
    docFactory = loaders.stan(tags.div(render=tags.directive('testWidget')))

    def render_liveTest(self, ctx, data):
        warnings.warn(
            "liveTest renderer is deprecated, use the default "
            "docFactory and override getWidgetTag, getTestContainer, "
            "and/or getWidgetDocument instead",
            category=DeprecationWarning)
        return self.render_liveFragment(ctx, data)


    def render_testWidget(self, ctx, data):
        container = self.getTestContainer()
        widget = self.getWidgetTag()(render=tags.directive('liveFragment'))
        container.fillSlots('widget', widget[self.getWidgetDocument()])
        return ctx.tag[container]


    def getWidgetTag(self):
        """
        Return a Nevow tag object which will be used as the top-level widget
        node for this test case.

        Subclasses may want to override this.
        """
        return tags.div


    def getTestContainer(self):
        """
        Return a Nevow DOM object (generally a tag) with a C{widget} slot in
        it.  This will be used as the top-level DOM for this test case, and the
        actual widget will be used to fill the C{widget} slot.

        Subclasses may want to override this.
        """
        return tags.invisible[tags.slot('widget')]


    def getWidgetDocument(self):
        """
        Retrieve a Nevow DOM object (generally a tag) which will be placed
        inside this TestCase's widget node.

        Subclasses may want to override this.
        """
        return ''


    def head(self):
        """
        Return objects that should be rendered inside the <head> tag of the
        document for the test suite that this test case belongs to.  Typically
        instances of tags from L{nevow.tags}.

        Subclasses may want to override this
        """


    def __repr__(self):
        return object.__repr__(self)



class TestError(athena.LiveElement):
    """
    An element rendering an error that occurred during test collection.
    """
    docFactory = loaders.stan(tags.div(render=tags.directive('error')))

    def __init__(self, holder):
        holder.run(self)

    def addError(self, holder, error):
        self._error = error

    def head(self):
        """
        We have nothing to render in <head>.
        """

    def error(self, req, tag):
        return tag(_class='test-suite')[
            tags.pre[self._error.getTraceback()]]
    page.renderer(error)


class TestSuite(object):
    """
    A collection of test cases.
    """

    holderType = runner.ErrorHolder

    def __init__(self, name="Live Tests"):
        self.tests = []
        self.name = name


    def addTest(self, test):
        self.tests.append(test)


    def gatherInstances(self):
        l = []
        for test in self.tests:
            if isinstance(test, TestSuite):
                l.extend(test.gatherInstances())
            elif isinstance(test, self.holderType):
                l.append(TestError(test))
            else:
                test.name = '%s.%s' % (self.name, test.__name__)
                l.append(test())
        return l



class TestLoader(runner.TestLoader):
    """
    An object which discovers test cases and collects them into a suite.
    """
    modulePrefix = 'livetest_'

    def __init__(self):
        runner.TestLoader.__init__(self)
        self.suiteFactory = TestSuite


    def loadByName(self, name, recurse=False):
        thing = self.findByName(name)
        return self.loadAnything(thing, recurse)


    def loadMethod(self, method):
        raise NotImplementedError('livetests must be classes')


    def loadClass(self, klass):
        if not (isinstance(klass, type) or isinstance(klass, type)):
            raise TypeError("%r is not a class" % (klass,))
        if not self.isTestCase(klass):
            raise ValueError("%r is not a test case" % (klass,))
        return klass


    def loadModule(self, module):
        if not isinstance(module, types.ModuleType):
            raise TypeError("%r is not a module" % (module,))
        suite = self.suiteFactory(module.__name__)
        for testClass in self._findTestClasses(module):
            suite.addTest(self.loadClass(testClass))
        return suite


    def isTestCase(self, obj):
        return isinstance(obj, type) and issubclass(obj, TestCase) and obj is not TestCase


    def _findTestClasses(self, module):
        classes = []
        for name, val in inspect.getmembers(module):
            if self.isTestCase(val):
                classes.append(val)
        return self.sort(classes)
