# -*- coding: utf-8 -*-

"""
Created on 2019-05-11
Updated on 2021-03-04

:author: Philipp Schneider <philipp.schneider@tracetronic.de>
:copyright: tracetronic GmbH
"""


import os
import itertools
from openpyxl import load_workbook
import xml.etree.ElementTree as ET
import requests


class Converter(object):
    """
    Converts the passed Excel file into a TestCaseCoverage filter definition of type filterTreeV2.
    """

    # Name of the Excel sheets which contains the data
    __excelSheet = "Tabelle1"

    def __init__(self, inputFile, outputFile="filterTree.xml"):
        """
        Constructor.
        :param inputFile: Path to Excel file
        :type inputFile: str
        :param outputFile: Path to the coverage XML file to be generated
        :type outputFile: str
        """
        self.__inputFile = inputFile
        self.__outputFile = outputFile

    def CreateFilterTree(self):
        """
        Creates the filter definition.
        :return: True, if the filter definition XML for test.guide could be generated,
                 otherwise False.
        :rtype: boolean
        """
        try:
            requirements = self.__ParseInput()
            rootNode = self.__ComputeCoverageStruct(requirements)

            # Debug print of the filter tree structure
            print(rootNode)

            print('Creating test case coverage filter definition...')
            tree = rootNode.CreateXmlRepresentation(None)
            tree.write(self.__outputFile)
            print('\tFilter definition XML written to \'{0}\'.'.format(self.__outputFile))
        except BaseException as err:
            print(err)
            return False

        return True

    def __ParseInput(self):
        """
        Reads the Excel file and returns the data from each row as a requirement object.
        :return: List of requirements
        :rtype: list[type -> Requirement]
        """
        result = []
        wb = load_workbook(filename=self.__inputFile, read_only=True, data_only=True)
        eachSheet = wb[self.__excelSheet]
        for eachRow in eachSheet.rows:
            req = ""
            reqDesc = ""
            platform = ""
            sop = ""

            for eachCell in eachRow:
                # Skip header line
                if not eachCell.value or eachCell.row <= 1:
                    continue

                if eachCell.column == 1:
                    req = str(eachCell.value).strip()
                elif eachCell.column == 2:
                    reqDesc = str(eachCell.value).strip()
                elif eachCell.column == 3:
                    platform = str(eachCell.value).strip()
                elif eachCell.column == 4:
                    sop = str(eachCell.value).strip()

            # If data has been collected, save it as a requirement
            if req and platform and sop:
                result.append(Requirement(req, reqDesc, platform, sop))

        return result

    def __ComputeCoverageStruct(self, requirements):
        """
        Calculates the coverage filter definition based on the requirements.
        :param requirements: Requirements that are to be mapped as coverage
        :type requirements: list[type -> Requirement]
        :return: the root node of the calculated coverage with all coverage nodes
        :rtype: RootNode
        """
        result = RootNode()
        for each in requirements:
            result.AddRequirement(each)
        return result


class Node(object):
    """
    Abstract base class of each coverage filter node.
    """

    def __init__(self, depth, requirement):
        """
        Constructor.
        :param depth: The depth of the element in the tree. Is needed for debug printing.
        :type depth: integer
        :param requirement: Requirement to be processed in the element
        :type requirement: Requirement
        """
        self.requirement = requirement
        self._depth = depth
        self._childs = []
        self.AddRequirement(requirement)

    def GetLabel(self):
        """
        :return: The label of the node for display and sorting in the coverage filter.
        :return: str
        """
        raise NotImplementedError("To be implemented")

    def _SortChildByLabel(self):
        """
        Sorts all children of the node by their label name.
        """
        self._childs = sorted(self._childs, key=lambda node: node.GetLabel().lower())

    def AddRequirement(self, requirement):
        """
        Adds a requirement to be processed to the node.
        :param requirement: Requirement that is to be processed in the coverage.
        :type requirement: Requirement
        """
        raise NotImplementedError("To be implemented")

    def CreateXmlRepresentation(self, parentXmlElement):
        """
        Creates the XML representation of the current node and its children.
        :param parentXmlElement: XML parent node for the current node to which it is assigned.
        :type parentXmlElement: xml.etree.Element*
        :return: Generated XML element for the node with all its children
        :rtype: xml.etree.Element*
        """
        raise NotImplementedError("To be implemented")

    def __repr__(self):
        result = "{0}\n".format(self.GetLabel())
        for each in self._childs:
            result = "{0}{1}{2}".format(result, self._depth * '\t', each)
        return result


class RootNode(Node):
    """
    Root node of the coverage filter, which holds all elements and is not displayed itself.
    """

    def __init__(self):
        """
        Constructor.
        """
        super(RootNode, self).__init__(1, None)

    def GetLabel(self):
        """
        :Override
        """
        return "Root"

    def AddRequirement(self, requirement):
        """
        :Override
        """
        # Special case for the root node
        if requirement is None:
            return

        for each in self._childs:
            if each.requirement.GetSop() == requirement.GetSop():
                each.AddRequirement(requirement)
                return

        self._childs.append(SOPNode(self._depth + 1, requirement))
        self._SortChildByLabel()

    def CreateXmlRepresentation(self, parentXmlElement):
        """
        :Override
        """
        root = ET.Element("filterTreeV2")
        for each in self._childs:
            each.CreateXmlRepresentation(root)
        return ET.ElementTree(root)


class SOPNode(Node):
    """
    Node of the coverage filter which lists all start of productions (SOP).
    """

    def __init__(self, depth, requirement):
        """
        Constructor.
        :param depth: The depth of the element in the tree, is needed to create a better debug print
        :type depth: integer
        :param requirement: Requirement to be processed in the element
        :type requirement: Requirement
        """
        super(SOPNode, self).__init__(depth, requirement)

    def GetLabel(self):
        """
        :Override
        """
        return self.requirement.GetSop()

    def AddRequirement(self, requirement):
        """
        :Override
        """

        for each in self._childs:
            if each.requirement.GetPlatform() == requirement.GetPlatform():
                each.AddRequirement(requirement)
                return

        self._childs.append(PlatformNode(self._depth + 1, requirement))
        self._SortChildByLabel()

    def CreateXmlRepresentation(self, parentXmlElement):
        """
        :Override
        """
        node = ET.SubElement(parentXmlElement, "node")
        ET.SubElement(node, "label").text = self.GetLabel()
        ET.SubElement(node, "scope", name="SOP")
        filters = ET.SubElement(node, "filters")
        constant = ET.SubElement(filters, "constant", name="SOP")
        ET.SubElement(constant, "value").text = u"*{0}*".format(self.requirement.GetSop())
        for each in self._childs:
            each.CreateXmlRepresentation(node)
        return node


class PlatformNode(Node):
    """
    Node of the coverage filter, which lists all platforms.
    """

    def __init__(self, depth, requirement):
        """
        Constructor.
        :param depth: The depth of the element in the tree. Is needed for debug printing.
        :type depth: integer
        :param requirement: Requirement to be processed in the element
        :type requirement: Requirement
        """
        super(PlatformNode, self).__init__(depth, requirement)

    def GetLabel(self):
        """
        :Override
        """
        return self.requirement.GetPlatform()

    def AddRequirement(self, requirement):
        """
        :Override
        """
        self._childs.append(ReqLeaf(self._depth + 1, requirement))
        self._SortChildByLabel()

    def CreateXmlRepresentation(self, parentXmlElement):
        """
        :Override
        """
        node = ET.SubElement(parentXmlElement, "node")
        ET.SubElement(node, "label").text = self.GetLabel()
        ET.SubElement(node, "scope", name="Platform")
        filters = ET.SubElement(node, "filters")
        constant = ET.SubElement(filters, "constant", name="PLATFORM")
        ET.SubElement(constant, "value").text = u"*{0}*".format(self.requirement.GetPlatform())
        for each in self._childs:
            each.CreateXmlRepresentation(node)
        return node


class ReqLeaf(Node):
    """
    Leaf, which provides the display and filtering of the actual requirement.
    """

    def __init__(self, depth, requirement):
        """
        Constructor.
        :param depth: The depth of the element in the tree. Is needed for debug printing.
        :type depth: integer
        :param requirement: Requirement to be processed in the element
        :type requirement: Requirement
        """
        super(ReqLeaf, self).__init__(depth, requirement)

    def GetLabel(self):
        """
        :Override
        """
        return "Req-{0}".format(self.requirement.GetReq())

    def AddRequirement(self, requirement):
        """
        :Override
        """
        pass

    def CreateXmlRepresentation(self, parentXmlElement):
        """
        :Override
        """
        leaf = ET.SubElement(parentXmlElement, "leaf")
        ET.SubElement(leaf, "label").text = self.GetLabel()
        ET.SubElement(leaf, "url").text = "https://tracetronic.de"
        #### Optional ####
        properties = ET.SubElement(leaf, "properties")
        property = ET.SubElement(properties, "property")
        ET.SubElement(property, "key").text = "Desc"
        ET.SubElement(property, "value").text = self.requirement.GetReqDesc()
        ### ä Filter ####
        filters = ET.SubElement(leaf, "filters")
        attribute = ET.SubElement(filters, "attribute", name="ReqId")
        ET.SubElement(attribute, "value").text = self.requirement.GetReq()
        return leaf


class Requirement(object):
    """
    Represents a requirement that consists of an ID, a description, a target
    platform and an intended Start of Production (SOP).
    """

    # Encoding in which the Excel cell data is available
    __EXCEL_CELL_ENCODING = "latin-1"

    def __init__(self, req, reqDesc, platform, sop):
        """
        Constructor.
        :type reqId: str
        :type reqDesc: str
        :type platform: str
        :type sop: str
        """
        self.__req = self.__EncodeDecodeStr(req)
        self.__reqDesc = self.__EncodeDecodeStr(reqDesc)
        self.__platform = self.__EncodeDecodeStr(platform)
        self.__sop = self.__EncodeDecodeStr(sop)

    def __EncodeDecodeStr(self, toEnDecode):
        """
        Protection method that converts the transferred Excel content to UTF-8 so that the umlaut
        can be displayed correctly.
        :param toEnDecode: String, which is to be encoded
        :type toEnDecode: str
        :return: encoded UTF-8 string
        :rtype: str
        """
        return toEnDecode.encode(self.__EXCEL_CELL_ENCODING, 'replace').decode("utf-8", 'replace')

    def GetReq(self):
        """
        :return: str
        """
        return self.__req

    def GetReqDesc(self):
        """
        :return: str
        """
        return self.__reqDesc

    def GetPlatform(self):
        """
        :rtype: str
        """
        return self.__platform

    def GetSop(self):
        """
        :rtype: str
        """
        return self.__sop

    def __repr__(self):
        return "{0}:{1}".format(self.__req, self.__reqDesc)


class TGUploader(object):
    """
    Service for uploading coverage configurations to test.guide.
    """

    def __init__(self, url, authKey, projectId=1):
        """
        Constructor.
        :param url: URL der test.guide Instanz
        :type url: str
        :param authKey: Key zum Authentifizieren bei test.guide
        :type authKey: str
        :param projectId: test.guide Project-ID, welche angibt für welches Projekt die Coverage
                          gedacht ist.
        :type projectId: integer
        """
        self.url = url
        self.authKey = authKey
        self.projectId = projectId

    def __CreateParameterPayload(self, configName, category=None, publicUse=False, description=""):
        """
        Compose parameter payload for the REST call.
        :param path: Path of the coverage filter XML to be uploaded to test.guide.
        :type path: str
        :param configName: Name of the filter under which it should be saved.
        :type configName: str
        :param category: Optional category in which the filter should be saved.
        :type category: str
        :param publicUse: True, if the filter should be public, otherwise False.
        :type publicUse: boolean
        :param description: Optional description of the filter.
        :type description: str
        :return: Parameter payload
        :rtype: dict
        """
        payload = {'name': configName}

        payload['publicUse'] = publicUse

        if (self.authKey):
            payload['authKey'] = self.authKey

        if category:
            payload['category'] = category

        if self.projectId:
            payload['projectId'] = self.projectId

        if description:
            payload['description'] = description

        return payload

    def __OverrideXmlConfig(self, filterDefinitionUuid, content, configName, category, publicUse,
                            description):
        """
        Uploads and override the coverage to test.guide.
        :param filterDefinitionUuid: The coverage uuid to which the data should be overwritten.
        :type filterDefinitionUuid: str
        :param content: Name of the filter under which it should be saved.
        :type content: str
        :param configName: Name of the filter under which it should be saved.
        :type configName: str
        :param category: Optional category in which the filter should be saved.
        :type category: str
        :param publicUse: True, if the filter should be public, otherwise False.
        :type publicUse: boolean
        :param description: Optional description of the filter.
        :type description: str
        :return: True, if the upload was successful, otherwise false.
        :rtype: boolean
        """
        uploadUrl = self.url + '/api/coverage/filterdefinitions/' + filterDefinitionUuid
        payload = self.__CreateParameterPayload(configName, category, publicUse, description)

        print('Overriding upload to test.guide:')
        print('\tOverriding filterdefinition "{0}" to {1} with project-id: {2}'.format(configName,
                                                                                       self.url,
                                                                                       self.projectId))

        headers = {'Content-type': 'application/xml', 'Accept': 'application/json'}
        r = requests.put(uploadUrl, params=payload, data=content, verify=False,
                         headers=headers)

        print("\tOverriding upload test.guide status code: {0}".format(r.status_code))

    def UploadXmlConfig(self, path, configName, category=None, publicUse=False, description="",
                        overrideExistingConfig=False):
        """
        Uploads the coverage file to test.guide under the specified path.
        :param path: Path of the coverage filter XML to be uploaded to test.guide.
        :type path: str
        :param configName: Name of the filter under which it should be saved.
        :type configName: str
        :param category: Optional category in which the filter should be saved.
        :type category: str
        :param publicUse: True, if the filter should be public, otherwise False.
        :type publicUse: boolean
        :param description: Optional description of the filter.
        :type description: str
        :param overrideExistingConfig: True if an existing filter at the specified location is
                                       to be overwritten, false otherwise.
        :type overrideExistingConfig: boolean
        :return: True, if the upload was successful, otherwise false.
        :rtype: boolean
        """
        uploadUrl = self.url + '/api/coverage/filterdefinitions'

        payload = self.__CreateParameterPayload(configName, category, publicUse, description)

        content = open(path, 'r').read()
        print('Upload to test.guide:')
        print('\tUploading filterdefinition "{0}" to {1} with project-id: {2}'.format(configName,
                                                                                      self.url,
                                                                                      self.projectId))
        headers = {'Content-type': 'application/xml', 'Accept': 'application/json'}
        r = requests.post(uploadUrl, params=payload, data=content, verify=False,
                          headers=headers)
        print("\tUpload test.guide status code: {0}".format(r.status_code))

        if overrideExistingConfig and r.status_code == 409:
            filterDefinitionUuid = r.json()["existingConfig"]["filterDefinitionUUID"]
            self.__OverrideXmlConfig(filterDefinitionUuid, content, configName, category, publicUse,
                                     description)
            print('\tSuccess - coverage filterdefinition was overwritten.')
            return True
        elif r.status_code == 409:
            print("\t{0}".format(r.json()["messsage"]))
            print("\tHint: Use the parameter 'overrideExistingConfig' to override the filterdefinition")
            print('\tFAILED!')
            return False
        elif r.status_code > 400:
            print("\tUpload to test.guide response: {0}".format(r.content))
            print('\tFAILED!')
            return False
        else:
            print('\tSuccess')
            return True


if __name__ == '__main__':
    rootPath = os.path.dirname(os.path.abspath(__file__))

    # Excel file converter specifications
    INPUT = os.path.join(rootPath, "Requirements.xlsx")  # <1>
    OUTPUT = os.path.join(rootPath, "filterTree2.xml")  # <2>

    # test.guide upload parameters
    AUTHKEY = "nOPVJ3T6JwqUAR8skQ~yourAuthKey~"  # <3>
    URL = "https://your.test-guide.instance:8443"  # <4>
    PROJECT_ID = 1  # <5>
    FILTER_CATEGORY = "Requirement/Excel"  # <6>
    FILTERNAME = "Excel-Export"  # <7>
    IS_FILTER_PUBLIC = True  # <8>
    FILTER_DESC = "List of all requirements"  # <9>

    CON = Converter(INPUT, OUTPUT)
    if CON.CreateFilterTree():
        UPLOADER = TGUploader(URL, AUTHKEY, PROJECT_ID)
        UPLOADER.UploadXmlConfig(OUTPUT, FILTERNAME, FILTER_CATEGORY,
                                 IS_FILTER_PUBLIC, FILTER_DESC, True)
