'''
See COPYRIGHT.md for copyright information.
'''
from __future__ import annotations

import os, io, logging
from collections import defaultdict

from arelle import XmlUtil, XbrlConst, ModelValue
from arelle.ModelObject import ModelObject
from arelle.PluginManager import pluginClassMethods

TXMY_PKG_SRC_ELTS = ("metadata", "catalog", "taxonomy")


def testcaseVariationsByTarget(testcaseVariations):
    for modelTestcaseVariation in testcaseVariations:
        modelTestcaseVariation.errors = None # Errors accumulate over multiple ixdsTargets for same variation
        ixdsTargets = [instElt.get("target")
                      for resultElt in modelTestcaseVariation.iterdescendants("{*}result")
                      for instElt in resultElt.iterdescendants("{*}instance")]
        if ixdsTargets:
            # track status and actual (error codes, counts) across all targets
            allTargetsActual = []
            allTargetsStatus = ""
            for ixdsTarget in ixdsTargets:
                modelTestcaseVariation.ixdsTarget = ixdsTarget
                yield modelTestcaseVariation
                allTargetsActual.extend(modelTestcaseVariation.actual)
                if allTargetsStatus not in ("fail", "fail (count)"):
                    # update status unless fail were noted by a prior target of this variation
                    allTargetsStatus = modelTestcaseVariation.status
            modelTestcaseVariation.status = allTargetsStatus
        else: # probably an expected error situation
            modelTestcaseVariation.ixdsTarget = None
            yield modelTestcaseVariation

class ModelTestcaseVariation(ModelObject):
    def init(self, modelDocument):
        super(ModelTestcaseVariation, self).init(modelDocument)
        self.status = ""
        self.duration: float | None = None
        self.actual = []
        self.assertions = None
        self.ixdsTarget = None

    @property
    def id(self):
        # if there is a real ID, use it
        id = super(ModelTestcaseVariation, self).id
        if id is not None:
            return id
        # no ID, use the object ID so it isn't None
        return self.objectId()

    @property
    def name(self):
        try:
            return self._name
        except AttributeError:
            if self.get("name"):
                self._name = self.get("name")
            else:
                nameElement = XmlUtil.descendant(self, None, "name" if self.localName != "testcase" else "number")
                if nameElement is not None:
                    self._name = XmlUtil.innerText(nameElement)
                else:
                    self._name = None
            return self._name

    @property
    def description(self):
        nameElement = XmlUtil.descendant(self, None, ("description", "documentation"))
        if nameElement is not None:
            return XmlUtil.innerText(nameElement)
        return None

    @property
    def reference(self):
        efmNameElts = XmlUtil.children(self.getparent(), None, "name")
        for efmNameElt in efmNameElts:
            if efmNameElt is not None and efmNameElt.text.startswith("EDGAR"):
                return efmNameElt.text
        referenceElement = XmlUtil.descendant(self, None, "reference")
        if referenceElement is not None: # formula test suite
            return "{0}#{1}".format(referenceElement.get("specification"), referenceElement.get("id"))
        referenceElement = XmlUtil.descendant(self, None, "documentationReference")
        if referenceElement is not None: # w3c test suite
            return referenceElement.get("{http://www.w3.org/1999/xlink}href")
        descriptionElement = XmlUtil.descendant(self, None, "description")
        if descriptionElement is not None and descriptionElement.get("reference"):
            return descriptionElement.get("reference")  # xdt test suite
        if self.getparent().get("description"):
            return self.getparent().get("description")  # base spec 2.1 test suite
        functRegistryRefElt = XmlUtil.descendant(self.getparent(), None, "reference")
        if functRegistryRefElt is not None: # function registry
            return functRegistryRefElt.get("{http://www.w3.org/1999/xlink}href")
        return None

    @property
    def readMeFirstUris(self):
        try:
            return self._readMeFirstUris
        except AttributeError:
            self._readMeFirstUris = []
            self.readMeFirstElements = []
            # first look if any plugin method to get readme first URIs
            if not any(pluginXbrlMethod(self)
                       for pluginXbrlMethod in pluginClassMethods("ModelTestcaseVariation.ReadMeFirstUris")):
                if self.localName == "testGroup":  #w3c testcase
                    instanceTestElement = XmlUtil.descendant(self, None, "instanceTest")
                    if instanceTestElement is not None: # take instance first
                        self._readMeFirstUris.append(XmlUtil.descendantAttr(instanceTestElement, None,
                                                                            "instanceDocument",
                                                                            "{http://www.w3.org/1999/xlink}href"))
                        self.readMeFirstElements.append(instanceTestElement)
                    else:
                        schemaTestElement = XmlUtil.descendant(self, None, "schemaTest")
                        if schemaTestElement is not None:
                            self._readMeFirstUris.append(XmlUtil.descendantAttr(schemaTestElement, None,
                                                                                "schemaDocument",
                                                                                "{http://www.w3.org/1999/xlink}href"))
                            self.readMeFirstElements.append(schemaTestElement)
                elif self.localName == "test-case":  #xpath testcase
                    inputFileElement = XmlUtil.descendant(self, None, "input-file")
                    if inputFileElement is not None: # take instance first
                        self._readMeFirstUris.append("TestSources/" + inputFileElement.text + ".xml")
                        self.readMeFirstElements.append(inputFileElement)
                elif self.resultIsTaxonomyPackage:
                    self._readMeFirstUris.append( os.path.join(self.modelDocument.filepathdir, "tests", self.get("name") + ".zip") )
                    self.readMeFirstElements.append(self)
                else:
                    # default built-in method for readme first uris
                    for anElement in self.iterdescendants():
                        if isinstance(anElement,ModelObject) and anElement.get("readMeFirst") == "true":
                            if anElement.get("{http://www.w3.org/1999/xlink}href"):
                                uri = anElement.get("{http://www.w3.org/1999/xlink}href")
                            else:
                                uri = XmlUtil.innerText(anElement)
                            if anElement.get("name"):
                                self._readMeFirstUris.append( (ModelValue.qname(anElement, anElement.get("name")), uri) )
                                self.readMeFirstElements.append(anElement)
                            elif anElement.get("dts"):
                                self._readMeFirstUris.append( (anElement.get("dts"), uri) )
                                self.readMeFirstElements.append(anElement)
                            else:
                                self._readMeFirstUris.append(uri)
                                self.readMeFirstElements.append(anElement)
            if not self._readMeFirstUris:  # provide a dummy empty instance document
                self._readMeFirstUris.append(os.path.join(self.modelXbrl.modelManager.cntlr.configDir, "empty-instance.xml"))
                self.readMeFirstElements.append(None)
            return self._readMeFirstUris

    @property
    def dataUris(self):
        try:
            return self._dataUris
        except AttributeError:
            self._dataUris = defaultdict(list) # may contain instances, schemas, linkbases
            for dataElement in XmlUtil.descendants(self, None, ("data", "input")):
                for elt in XmlUtil.descendants(dataElement, None, ("xsd", "schema", "linkbase", "instance")):
                    self._dataUris["schema" if elt.localName == "xsd" else elt.localName].append(elt.textValue.strip())
            return self._dataUris

    @property
    def parameters(self):
        try:
            return self._parameters
        except AttributeError:
            self._parameters = dict([
                (ModelValue.qname(paramElt, paramElt.get("name")), # prefix-less parameter names take default namespace of element
                 (ModelValue.qname(paramElt, paramElt.get("datatype")),paramElt.get("value")))
                for paramElt in XmlUtil.descendants(self, self.namespaceURI, "parameter")])
            return self._parameters

    @property
    def resultIsVersioningReport(self):
        return XmlUtil.descendant(XmlUtil.descendant(self, None, "result"), None, "versioningReport") is not None

    @property
    def versioningReportUri(self):
        return XmlUtil.text(XmlUtil.descendant(self, None, "versioningReport"))

    @property
    def resultIsXbrlInstance(self):
        return XmlUtil.descendant(XmlUtil.descendant(self, None, "result"), None, "instance") is not None

    @property
    def resultXbrlInstanceUri(self):
        for pluginXbrlMethod in pluginClassMethods("ModelTestcaseVariation.ResultXbrlInstanceUri"):
            resultInstanceUri = pluginXbrlMethod(self)
            if resultInstanceUri is not None:
                return resultInstanceUri or None # (empty string returns None)

        for resultElt in self.iterdescendants("{*}result"):
            for instElt in resultElt.iterdescendants("{*}instance"):
                if (instElt.get("target") or "") == (self.ixdsTarget or ""): # match null and emptyString
                    return XmlUtil.text(instElt)
        return None

    @property
    def resultIsInfoset(self):
        if self.modelDocument.outpath:
            result = XmlUtil.descendant(self, None, "result")
            if result is not None:
                return XmlUtil.child(result, None, "file") is not None or XmlUtil.text(result).endswith(".xml")
        return False

    @property
    def resultInfosetUri(self):
        result = XmlUtil.descendant(self, None, "result")
        if result is not None:
            child = XmlUtil.child(result, None, "file")
            return os.path.join(self.modelDocument.outpath, XmlUtil.text(child if child is not None else result))
        return None

    @property
    def resultIsTable(self):
        result = XmlUtil.descendant(self, None, "result")
        if result is not None :
            child = XmlUtil.child(result, None, "table")
            if child is not None and XmlUtil.text(child).endswith(".xml"):
                return True
        return False

    @property
    def resultTableUri(self):
        result = XmlUtil.descendant(self, None, "result")
        if result is not None:
            child = XmlUtil.child(result, None, "table")
            if child is not None:
                return os.path.join(self.modelDocument.outpath, XmlUtil.text(child))
        return None

    @property
    def resultIsTaxonomyPackage(self):
        return any(e.localName for e in XmlUtil.descendants(self,None,TXMY_PKG_SRC_ELTS))

    @property
    def variationDiscoversDTS(self):
        return any(e.localName != "taxonomyPackage" # find any nonTP element (instance, schema, linkbase, etc)
                   for e in self.iterdescendants()
                   if isinstance(e,ModelObject) and e.get("readMeFirst") == "true")

    @property
    def cfcnCall(self):
        # tuple of (expression, element holding the expression)
        try:
            return self._cfcnCall
        except AttributeError:
            self._cfcnCall = None
            if self.localName == "test-case":  #xpath testcase
                queryElement = XmlUtil.descendant(self, None, "query")
                if queryElement is not None:
                    filepath = (self.modelDocument.filepathdir + "/" + "Queries/XQuery/" +
                                self.get("FilePath") + queryElement.get("name") + '.xq')
                    if os.sep != "/": filepath = filepath.replace("/", os.sep)
                    with io.open(filepath, 'rt', encoding='utf-8') as f:
                        self._cfcnCall = (f.read(), self)
            else:
                for callElement in XmlUtil.descendants(self, XbrlConst.cfcn, "call"):
                    self._cfcnCall = (XmlUtil.innerText(callElement), callElement)
                    break
            if self._cfcnCall is None and self.namespaceURI == "http://xbrl.org/2011/conformance-rendering/transforms":
                name = self.getparent().get("name")
                input = self.get("input")
                if name and input:
                    self._cfcnCall =  ("{0}('{1}')".format(name, input.replace("'","''")), self)
            return self._cfcnCall

    @property
    def cfcnTest(self):
        # tuple of (expression, element holding the expression)
        try:
            return self._cfcnTest
        except AttributeError:
            self._cfcnTest = None
            if self.localName == "test-case":  #xpath testcase
                outputFileElement = XmlUtil.descendant(self, None, "output-file")
                if outputFileElement is not None and outputFileElement.get("compare") == "Text":
                    filepath = (self.modelDocument.filepathdir + "/" + "ExpectedTestResults/" +
                                self.get("FilePath") + outputFileElement.text)
                    if os.sep != "/": filepath = filepath.replace("/", os.sep)
                    with io.open(filepath, 'rt', encoding='utf-8') as f:
                        self._cfcnTest = ("xs:string($result) eq '{0}'".format(f.read()), self)
            else:
                testElement = XmlUtil.descendant(self, XbrlConst.cfcn, "test")
                if testElement is not None:
                    self._cfcnTest = (XmlUtil.innerText(testElement), testElement)
                elif self.namespaceURI == "http://xbrl.org/2011/conformance-rendering/transforms":
                    output = self.get("output")
                    if output:
                        self._cfcnTest =  ("$result eq '{0}'".format(output.replace("'","''")), self)
            return self._cfcnTest

    @property
    def expected(self):
        for pluginXbrlMethod in pluginClassMethods("ModelTestcaseVariation.ExpectedResult"):
            expected = pluginXbrlMethod(self)
            if expected:
                return expected
        # default behavior without plugins
        if self.localName == "testcase":
            return self.document.basename[:4]   #starts with PASS or FAIL
        elif self.localName == "testGroup":  #w3c testcase
            instanceTestElement = XmlUtil.descendant(self, None, "instanceTest")
            if instanceTestElement is not None: # take instance first
                return XmlUtil.descendantAttr(instanceTestElement, None, "expected", "validity")
            else:
                schemaTestElement = XmlUtil.descendant(self, None, "schemaTest")
                if schemaTestElement is not None:
                    return XmlUtil.descendantAttr(schemaTestElement, None, "expected", "validity")
        resultElement = XmlUtil.descendant(self, None, "result")
        if resultElement is not None:
            expected = resultElement.get("expected")
            if expected and resultElement.get("nonStandardErrorCodes") == "true":
                # if @expected and @nonStandardErrorCodes then use expected instead of error codes
                return expected
        errorElements = XmlUtil.descendants(self, None, "error")
        resultElement = XmlUtil.descendant(self, None, "result")
        if isinstance(errorElements,list) and len(errorElements) > 0:
            return [ModelValue.qname(e, e.stringValue) if not e.get("nonStandardErrorCodes")
                    else e.stringValue
                    for e in errorElements]
        #else:
        #    errorElement = errorElements[1]
        #    if errorElement is not None and not errorElement.get("nonStandardErrorCodes"):
        #        _errorText = XmlUtil.text(errorElement)
        #        if ' ' in _errorText: # list of tokens
        #            return _errorText
        #        return ModelValue.qname(errorElement, _errorText)  # turn into a QName
        if resultElement is not None:
            if expected:
                return expected
            for assertElement in XmlUtil.children(resultElement, None, "assert"):
                num = assertElement.get("num")
                if num == "99999": # inline test, use name as expected
                    return assertElement.get("name")
                if len(num) == 5:
                    return "EFM.{0}.{1}.{2}".format(num[0],num[1:3],num[3:6])
            asserTests = {}
            for atElt in XmlUtil.children(resultElement, None, "assertionTests"):
                try:
                    asserTests[atElt.get("assertionID")] = (int(atElt.get("countSatisfied")),int(atElt.get("countNotSatisfied")))
                except ValueError:
                    pass
            if asserTests:
                return asserTests
        elif self.get("result"):
            return self.get("result")

        return None

    @property
    def match(self) -> str | None:
        resultElement = XmlUtil.descendant(self, None, "result")
        if resultElement is None:
            return None
        return resultElement.get('match')

    @property
    def expectedCount(self):
        for pluginXbrlMethod in pluginClassMethods("ModelTestcaseVariation.ExpectedCount"):
            _count = pluginXbrlMethod(self)
            if _count is not None: # ignore plug in if not a plug-in-recognized test case
                return _count
        return None

    @property
    def expectedReportCount(self):
        resultElement = XmlUtil.descendant(self, None, "result")
        if resultElement is None:
            return None
        report_count = resultElement.get('report_count')
        return int(report_count) if report_count is not None else None

    @property
    def severityLevel(self):
        for pluginXbrlMethod in pluginClassMethods("ModelTestcaseVariation.ExpectedSeverity"):
            severityLevelName = pluginXbrlMethod(self)
            if severityLevelName: # ignore plug in if not a plug-in-recognized test case
                return logging._checkLevel(severityLevelName)
        # default behavior without plugins
        # SEC error cases have <assert severity={err|wrn}>...
        if (XmlUtil.descendant(self, None, "assert", attrName="severity", attrValue="wrn") is not None or
            XmlUtil.descendant(self, None, "result", attrName="severity", attrValue="warning") is not None):
            return logging._checkLevel("WARNING")
        return logging._checkLevel("INCONSISTENCY")

    @property
    def blockedMessageCodes(self):
        blockedCodesRegex = XmlUtil.descendantAttr(self, None, ("results","result"), "blockedMessageCodes") # DQC 4/5 test suite
        if not blockedCodesRegex:
            ignoredCodes = XmlUtil.descendants(self, None, "ignore-error") # ESEF test suite
            if ignoredCodes:
                blockedCodesRegex = "|".join(".*" + c.stringValue for c in ignoredCodes)
        return blockedCodesRegex

    @property
    def expectedVersioningReport(self):
        XmlUtil.text(XmlUtil.text(XmlUtil.descendant(XmlUtil.descendant(self, None, "result"), None, "versioningReport")))

    @property
    def propertyView(self):
        assertions = []
        for assertionElement in XmlUtil.descendants(self, None, "assertionTests"):
            assertions.append(("assertion",assertionElement.get("assertionID")))
            assertions.append(("   satisfied", assertionElement.get("countSatisfied")))
            assertions.append(("   not sat.", assertionElement.get("countNotSatisfied")))
        '''
        for assertionElement in XmlUtil.descendants(self, None, "assert"):
            efmNum = assertionElement.get("num")
            assertions.append(("assertion",
                               "EFM.{0}.{1}.{2}".format(efmNum[0], efmNum[1:2], efmNum[3:4])))
            assertions.append(("   not sat.", "1"))
        '''
        readMeFirsts = [("readFirst", readMeFirstUri) for readMeFirstUri in self.readMeFirstUris]
        parameters = []
        if len(self.parameters) > 0: parameters.append(("parameters", None))
        for pName, pTypeValue in self.parameters.items():
            parameters.append((pName,pTypeValue[1]))
        return [("id", self.id),
                ("name", self.name),
                ("description", self.description)] + \
                readMeFirsts + \
                parameters + \
               [("status", self.status),
                ("call", self.cfcnCall[0]) if self.cfcnCall else (),
                ("test", self.cfcnTest[0]) if self.cfcnTest else (),
                ("infoset", self.resultInfosetUri) if self.resultIsInfoset else (),
                ("expected", self.expected) if self.expected else (),
                ("actual", " ".join(str(i) for i in self.actual) if len(self.actual) > 0 else ())] + \
                assertions

    def __repr__(self):
        return ("modelTestcaseVariation[{0}]{1})".format(self.objectId(),self.propertyView))
