package eu.dnetlib.validator2.validation.guideline

import eu.dnetlib.validator2.engine.Predicates
import eu.dnetlib.validator2.engine.Rule
import spock.lang.Specification
import spock.lang.Unroll

import java.util.function.Predicate

class ElementSpecBuilderSpecification extends Specification {

    def "Builder generates expected ElementSpec from valid inputs"() {
        given: "Initial values"
        def baseElementName = "baseElement"
        def baseElementRequirementLevel = RequirementLevel.OPTIONAL
        def baseElementCardinality = Cardinality.ONE
        def baseElementPredicate = Mock(Predicate) // alternatively allowed values below
        def baseElementOccurrence = ElementPosition.FIRST
        def baseElementParents = ["parent1", "parent2"] as String[]

        def baseElementAttr1Name = "baseElementAttr1"
        def baseElementAttr1RequirementLevel = RequirementLevel.OPTIONAL

        def baseElementAttr2Name = "baseElementAttr2" // also has allowed values
        def baseElementAttr2RequirementLevel = RequirementLevel.MANDATORY_IF_APPLICABLE
        def baseElementAttr2ApplicabilityRule = Mock(Rule)
        def baseElementAttr2Predicate = Mock(Predicate)

        def subElement1Name = "subElement1"
        def subElement1RequirementLevel = RequirementLevel.MANDATORY_IF_APPLICABLE
        def subElement1Cardinality = Cardinality.ONE
        def subElement1Predicate = Mock(Predicate)
        def subElement1ApplicabilityRule = Mock(Rule)

        def subElement2Name = "subElement2"
        def subElement2RequirementLevel = RequirementLevel.RECOMMENDED
        def subElement2Cardinality = Cardinality.ONE_TO_N

        def subElement2Attr1Name = "baseElementAttr2"
        def subElement2Attr1RequirementLevel = RequirementLevel.MANDATORY

        // common values whenever needed
        def allowedVals = ["val1", "val2"] as String[]
        def prefix = "test-prefix"

        when: "building a basic ElementSpec with a predicate of allowed values and parents"
        def elementSpec1 = Builders.
                forOptionalElement(baseElementName).
                inContext(baseElementParents).
                allowedValues(baseElementPredicate).
                build()

        then: "an ElementSpec is created with expected fields and default values"
        assertElementSpecIsOf(elementSpec1, baseElementParents, baseElementName, baseElementRequirementLevel, baseElementCardinality, null, baseElementPredicate, null, null, 0, 0)

        when: "building a more complex ElementSpec with more possible fields"
        def elementSpec2 = Builders.
                forOptionalElement(baseElementName).
                allowedValues(allowedVals).
                atPosition(baseElementOccurrence).
                valueMustStartWith(prefix).
                withOptionalAttribute(baseElementAttr1Name).
                withMandatoryIfApplicableAttribute(baseElementAttr2Name, baseElementAttr2ApplicabilityRule, baseElementAttr2Predicate).
                withSubElement(Builders.
                        forMandatoryIfApplicableElement(subElement1Name, subElement1Cardinality, subElement1ApplicabilityRule).allowedValues(subElement1Predicate)).
                withSubElement(Builders.
                        forRecommendedRepeatableElement(subElement2Name).
                        withMandatoryAttribute(subElement2Attr1Name)).
                build()

        then: "an ElementSpec is returned with expected fields"
        assertElementSpecIsOf(elementSpec2, null, baseElementName, baseElementRequirementLevel, baseElementCardinality, null, allowedVals, baseElementOccurrence, prefix, 2, 2)

        // Check attributes
        // Convert attributes set to list to check each attribute more thoroughly
        def attributeSpecsList = elementSpec2.attributeSpecs() as List

        def attr1Spec = getNodeSpecFromListOfName(attributeSpecsList, baseElementAttr1Name)
        assertAttributeSpecIsOf(attr1Spec, baseElementAttr1Name, baseElementAttr1RequirementLevel)

        def attr2Spec = getNodeSpecFromListOfName(attributeSpecsList, baseElementAttr2Name)
        assertAttributeSpecIsOf(attr2Spec, baseElementAttr2Name, baseElementAttr2RequirementLevel, baseElementAttr2Predicate)

        // Check sub-elements
        def subElementsSpecList = elementSpec2.subElementSpecs() as List
        def subElement1Spec = getNodeSpecFromListOfName(subElementsSpecList, subElement1Name)
        assertElementSpecIsOf(subElement1Spec, null, subElement1Name, subElement1RequirementLevel, subElement1Cardinality, subElement1ApplicabilityRule, null, null, null, 0, 0)

        ElementSpec subElement2Spec = getNodeSpecFromListOfName(subElementsSpecList, subElement2Name)
        assertElementSpecIsOf(subElement2Spec, null, subElement2Name, subElement2RequirementLevel, subElement2Cardinality, null, null, null, null, 1, 0)
        def subElement2AttrSpecs = subElement2Spec.attributeSpecs() as List
        def subElement2Attr1Spec = getNodeSpecFromListOfName(subElement2AttrSpecs, subElement2Attr1Name)
        assertAttributeSpecIsOf(subElement2Attr1Spec, subElement2Attr1Name, subElement2Attr1RequirementLevel)

        //TODO: add more sub-elements, attributes?
    }

    @Unroll
    def "Builder for Mandatory & MandatoryIfApplicable element validates invalid inputs name: #elementName, cardinality: #cardinality"() {
        when: "building a basic ElementSpec"
        Builders.
                forMandatoryElement(elementName, cardinality).
                build()

        then:
        def error1 = thrown(expectedException)
        error1.message == expectedMessage

        when: "building a basic 'Mandatory if Applicable' ElementSpec"
        Builders.
                forMandatoryIfApplicableElement(elementName, cardinality, Mock(Rule)).
                build()

        then:
        def error2 = thrown(expectedException)
        error2.message == expectedMessage

        where:
        elementName | cardinality     || expectedException     | expectedMessage
        ""          | Cardinality.ONE || IllegalStateException | "Element name cannot be empty"
        null        | Cardinality.ONE || IllegalStateException | "Element name cannot be empty"
        "valid"     | null            || IllegalStateException | "Element:$elementName cardinality cannot be empty"
    }

    @Unroll
    def "Builder validates invalid element inputs name: #elementName, cardinality: #cardinality, allowedValues: #allowedValues"() {
        when: "building a basic OptionalRepeatable ElementSpec with allowed values"
        Builders.
                forOptionalRepeatableElement(elementName).
                allowedValues(allowedValues).
                build()

        then:
        def error1 = thrown(expectedException)
        error1.message == expectedMessage

        when: "building a basic RecommendedRepeatable ElementSpec with allowed values"
        Builders.
                forRecommendedRepeatableElement(elementName).
                allowedValues(allowedValues).
                build()

        then:
        def error2 = thrown(expectedException)
        error2.message == expectedMessage

        when: "building a basic Mandatory ElementSpec with allowed values"
        Builders.
                forMandatoryElement(elementName, Cardinality.ONE_TO_N).
                allowedValues(allowedValues).
                build()

        then:
        def error3 = thrown(expectedException)
        error3.message == expectedMessage

        where:
        elementName | allowedValues            || expectedException        | expectedMessage
        ""          | ["", "2"] as String[]    || IllegalStateException    | "Element name cannot be empty"
        null        | ["1", null] as String[]  || IllegalStateException    | "Element name cannot be empty"
        "valid"     | ["", ""] as String[]     || IllegalArgumentException | "Element allowed values cannot be empty"
        "valid"     | ["", null] as String[]   || IllegalArgumentException | "Element allowed values cannot be empty"
        "valid"     | [null, ""] as String[]   || IllegalArgumentException | "Element allowed values cannot be empty"
        "valid"     | [null, null] as String[] || IllegalArgumentException | "Element allowed values cannot be empty"
        "valid"     | null                     || IllegalStateException    | "Element:$elementName allowed values predicate cannot be empty"
    }

    @Unroll
    def "Builder validates invalid attribute inputs name: #attrName"() {
        when: "building a basic ElementSpec with an attribute"
        Builders.
                forOptionalElement("valid").
                withOptionalAttribute(attrName).
                build()

        then:
        def error = thrown(expectedException)
        error.message == expectedMessage

        where:
        attrName || expectedException        | expectedMessage
        ""       || IllegalStateException    | "Attribute name cannot be empty"
        null     || IllegalStateException    | "Attribute name cannot be empty"
    }

    @Unroll
    def "Builder validates invalid attribute inputs name: #attrName, allowedValues: #allowedValues"() {
        when: "building a basic ElementSpec with an attribute"
        Builders.
                forOptionalElement("valid").
                withOptionalAttribute(attrName, allowedValues).
                build()

        then:
        def error = thrown(expectedException)
        error.message == expectedMessage

        where:
        attrName | allowedValues            || expectedException        | expectedMessage
        ""       | ["", "2"] as String[]    || IllegalStateException    | "Attribute name cannot be empty"
        null     | ["1", null] as String[]  || IllegalStateException    | "Attribute name cannot be empty"
        "valid"  | ["", " "] as String[]    || IllegalArgumentException | "Attribute allowed values cannot be empty"
        "valid"  | ["", null] as String[]   || IllegalArgumentException | "Attribute allowed values cannot be empty"
        "valid"  | [null, ""] as String[]   || IllegalArgumentException | "Attribute allowed values cannot be empty"
        "valid"  | [null, null] as String[] || IllegalArgumentException | "Attribute allowed values cannot be empty"
        "valid"  | null                     || IllegalStateException    | "Attribute:$attrName allowed values predicate cannot be empty"
    }

    @Unroll
    def "Builder validates invalid attribute inputs name: #attrName, allowedValuesPredicate: #allowedValuesPredicate"() {
        when: "building a basic ElementSpec with attribute"
        Builders.
                forMandatoryElement("valid", Cardinality.ONE).
                withMandatoryAttribute(attrName, allowedValuesPredicate).
                build()

        then:
        def error = thrown(expectedException)
        error.message == expectedMessage

        where:
        attrName | allowedValuesPredicate    || expectedException     | expectedMessage
        ""       | Builders.ALLOW_ALL_VALUES || IllegalStateException | "Attribute name cannot be empty"
        null     | Builders.ALLOW_ALL_VALUES || IllegalStateException | "Attribute name cannot be empty"
        "valid"  | null                      || IllegalStateException | "Attribute:$attrName allowed values predicate cannot be empty"
    }

    def getNodeSpecFromListOfName = { list, name ->
        list.get(list.findIndexOf { it.nodeName() == name })
    }

    void assertElementSpecIsOf(elementSpec, paters, name, reqLevel, inpCardinality, applRule, predicateOrAllowedValuesList, inpOccurrence, inpValuePrefix, attributesNumber, subElementsNumber) {
        verifyAll(elementSpec as ElementSpec) {
            parents() == ((paters == null) ? ([] as Set) : (paters as Set))
            nodeName() == name
            requirementLevel() == reqLevel
            cardinality() == inpCardinality
            applicabilityRule() == applRule
            if (predicateOrAllowedValuesList instanceof Predicate) {
                allowedValuesPredicate() == predicateOrAllowedValuesList
            } else if (predicateOrAllowedValuesList instanceof List<String>) {
                allowedValuesPredicate() instanceof Predicates.SetOfCaseInsensitiveAllowedValues
                allowedValuesPredicate().allowedValues == predicateOrAllowedValuesList as Set
            }
            position() == (inpOccurrence != null) ? inpOccurrence : ElementPosition.ALL
            valuePrefix() == inpValuePrefix
            attributeSpecs().size() == attributesNumber
            subElementSpecs().size() == subElementsNumber
        }
    }

    void assertAttributeSpecIsOf(attributeSpec, name, requirementLevel) {
        assertAttributeSpecIsOf(attributeSpec, name, requirementLevel, Builders.ALLOW_ALL_VALUES)
    }

    void assertAttributeSpecIsOf(attributeSpec, name, reqLevel, predicate) {
        verifyAll(attributeSpec as AttributeSpec) {
            nodeName() == name
            requirementLevel() == reqLevel
            cardinality() == Cardinality.ONE
            allowedValuesPredicate() == predicate
        }
    }

}
