# ------------------------------------------------------------------------------
# Appy is a framework for building applications in the Python language.
# Copyright (C) 2007 Gaetan Delannay

# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.

# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.

# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301,USA.

# ------------------------------------------------------------------------------
import os, os.path, sys, zipfile, re, shutil
import appy.shared.test
from appy.shared.test import TesterError
from appy.shared.utils import FolderDeleter
from appy.shared.xml_parser import escapeXml
from appy.pod.odf_parser import OdfEnvironment, OdfParser
from appy.pod.renderer import Renderer

# TesterError-related constants ------------------------------------------------
TEMPLATE_NOT_FOUND = 'Template file "%s" was not found.'
CONTEXT_NOT_FOUND = 'Context file "%s" was not found.'
EXPECTED_RESULT_NOT_FOUND = 'Expected result "%s" was not found.'

# ------------------------------------------------------------------------------
class AnnotationsRemover(OdfParser):
    '''This parser is used to remove from content.xml and styles.xml the
       Python tracebacks that may be dumped into OpenDocument annotations by
       pod when generating errors. Indeed, those tracebacks contain lot of
       machine-specific info, like absolute paths to the python files, etc.'''
    def __init__(self, env, caller):
        OdfParser.__init__(self, env, caller)
        self.res = u''
        self.inAnnotation = False # Are we parsing an annotation ?
        self.textEncountered = False # Within an annotation, have we already
        # met a text ?
        self.ignore = False # Must we avoid dumping the current tag/content
        # into the result ?
    def startElement(self, elem, attrs):
        e = OdfParser.startElement(self, elem, attrs)
        # Do we enter into an annotation ?
        if elem == '%s:annotation' % e.ns(e.NS_OFFICE):
            self.inAnnotation = True
            self.textEncountered = False
        elif elem == '%s:p' % e.ns(e.NS_TEXT):
            if self.inAnnotation:
                if not self.textEncountered:
                    self.textEncountered = True
                else:
                    self.ignore = True
        if not self.ignore:
            self.res += '<%s' % elem
            for attrName, attrValue in attrs.items():
                self.res += ' %s="%s"' % (attrName, attrValue)
            self.res += '>'
    def endElement(self, elem):
        e = OdfParser.endElement(self, elem)
        if elem == '%s:annotation' % e.ns(e.NS_OFFICE):
            self.inAnnotation = False
            self.ignore = False
        if not self.ignore:
            self.res += '</%s>' % elem
    def characters(self, content):
        e = OdfParser.characters(self, content)
        if not self.ignore: self.res += escapeXml(content)
    def getResult(self):
        return self.res

# ------------------------------------------------------------------------------
class Test(appy.shared.test.Test):
    '''Abstract test class.'''
    interestingOdtContent = ('content.xml', 'styles.xml')

    def __init__(self, testData, testDescription, testFolder, config, flavour):
        appy.shared.test.Test.__init__(self, testData, testDescription,
                                       testFolder, config, flavour)
        self.templatesFolder = os.path.join(self.testFolder, 'templates')
        self.contextsFolder = os.path.join(self.testFolder, 'contexts')
        self.resultsFolder = os.path.join(self.testFolder, 'results')
        self.result = None

    def getContext(self, contextName):
        '''Gets the objects that are in the context.'''
        contextPy = os.path.join(self.contextsFolder, contextName + '.py')
        if not os.path.exists(contextPy):
            raise TesterError(CONTEXT_NOT_FOUND % contextPy)
        contextPkg = 'appy.pod.test.contexts.%s' % contextName
        exec 'import %s' % contextPkg
        exec 'context = dir(%s)' % contextPkg
        res = {}
        for elem in context:
            if not elem.startswith('__'):
                exec 'res[elem] = %s.%s' % (contextPkg, elem)
        return res

    def do(self):
        self.result = os.path.join(
            self.tempFolder, '%s.%s' % (
                self.data['Name'], self.data['Result']))
        # Get the path to the template to use for this test
        if self.data['Template'].endswith('.ods'):
            suffix = ''
        else:
            # For ODT, which is the most frequent case, no need to specify the
            # file extension.
            suffix = '.odt'
        template = os.path.join(self.templatesFolder,
                                self.data['Template'] + suffix)
        if not os.path.exists(template):
            raise TesterError(TEMPLATE_NOT_FOUND % template)
        # Get the context
        context = self.getContext(self.data['Context'])
        # Get the OpenOffice port
        ooPort = self.data['OpenOfficePort']
        pythonWithUno = self.config['pythonWithUnoPath']
        # Get the styles mapping
        stylesMapping = eval('{' + self.data['StylesMapping'] + '}')
        # Mmh, dicts are not yet managed by RtfTablesParser
        # Call the renderer.
        Renderer(template, context, self.result, ooPort=ooPort,
                 pythonWithUnoPath=pythonWithUno,
                 stylesMapping=stylesMapping).run()
        # Store all result files
        # I should allow to do this from an option given to Tester.py: this code
        # keeps in a separate folder the odt results of all ran tests.
        #tempFolder2 = '%s/sevResults' % self.testFolder
        #if not os.path.exists(tempFolder2):
        #    os.mkdir(tempFolder2)
        #print('Result is %s, temp folder 2 is %s.' % (self.result,tempFolder2))
        #shutil.copy(self.result, tempFolder2)

    def getOdtContent(self, odtFile):
        '''Creates in the temp folder content.xml and styles.xml extracted
           from p_odtFile.'''
        contentXml = None
        stylesXml = None
        if odtFile == self.result:
            filePrefix = 'actual'
        else:
            filePrefix = 'expected'
        zipFile = zipfile.ZipFile(odtFile)
        for zippedFile in zipFile.namelist():
            if zippedFile in self.interestingOdtContent:
                f = file(os.path.join(self.tempFolder,
                                      '%s.%s' % (filePrefix, zippedFile)), 'wb')
                fileContent = zipFile.read(zippedFile)
                if zippedFile == 'content.xml':
                    # Sometimes, in annotations, there are Python tracebacks.
                    # Those tracebacks include the full path to the Python
                    # files, which of course may be different from one machine
                    # to the other. So we remove those paths.
                    annotationsRemover = AnnotationsRemover(
                       OdfEnvironment(), self)
                    annotationsRemover.parse(fileContent)
                    fileContent = annotationsRemover.getResult()
                try:
                    f.write(fileContent.encode('utf-8'))
                except UnicodeDecodeError:
                    f.write(fileContent)
                f.close()
        zipFile.close()

    def checkResult(self):
        '''r_ is False if the test succeeded.'''
        # Get styles.xml and content.xml from the actual result
        res = False
        self.getOdtContent(self.result)
        # Get styles.xml and content.xml from the expected result
        expectedResult = os.path.join(self.resultsFolder,
                                  self.data['Name'] + '.' + self.data['Result'])
        if not os.path.exists(expectedResult):
            raise TesterError(EXPECTED_RESULT_NOT_FOUND % expectedResult)
        self.getOdtContent(expectedResult)
        for fileName in self.interestingOdtContent:
            diffOccurred = self.compareFiles(
                os.path.join(self.tempFolder, 'actual.%s' % fileName),
                os.path.join(self.tempFolder, 'expected.%s' % fileName),
                areXml=True, xmlTagsToIgnore=(
                   (OdfEnvironment.NS_DC, 'date'),
                   (OdfEnvironment.NS_STYLE, 'style')),
                xmlAttrsToIgnore=('draw:name','text:name','text:bullet-char',
                                  'table:name', 'table:style-name'),
                encoding='utf-8')
            if diffOccurred:
                res = True
                break
        return res

# Concrete test classes --------------------------------------------------------
class NominalTest(Test):
    '''Tests an application model.'''
    def __init__(self, testData, testDescription, testFolder, config, flavour):
        Test.__init__(self, testData, testDescription, testFolder, config,
                      flavour)

class ErrorTest(Test):
    '''Tests an application model.'''
    def __init__(self, testData, testDescription, testFolder, config, flavour):
        Test.__init__(self, testData, testDescription, testFolder, config,
                      flavour)
    def onError(self):
        '''Compares the error that occurred with the expected error.'''
        Test.onError(self)
        return not self.isExpectedError(self.data['Message'])

# ------------------------------------------------------------------------------
class PodTestFactory(appy.shared.test.TestFactory):
    def createTest(testData, testDescription, testFolder, config, flavour):
        if testData.table.instanceOf('ErrorTest'):
            test = ErrorTest(testData, testDescription, testFolder, config,
                              flavour)
        else:
            test = NominalTest(testData, testDescription, testFolder, config,
                               flavour)
        return test
    createTest = staticmethod(createTest)

# ------------------------------------------------------------------------------
class PodTester(appy.shared.test.Tester):
    def __init__(self, testPlan):
        appy.shared.test.Tester.__init__(self, testPlan, [], PodTestFactory)

# ------------------------------------------------------------------------------
if __name__ == '__main__':
    PodTester('Tests.rtf').run()
# ------------------------------------------------------------------------------