/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */
package org.apache.jackrabbit.vault.validation;

import javax.jcr.NamespaceException;
import javax.xml.parsers.ParserConfigurationException;

import java.io.IOException;
import java.io.InputStream;
import java.net.URISyntaxException;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;

import org.apache.jackrabbit.spi.Name;
import org.apache.jackrabbit.spi.NameFactory;
import org.apache.jackrabbit.spi.commons.name.NameConstants;
import org.apache.jackrabbit.spi.commons.name.NameFactoryImpl;
import org.apache.jackrabbit.vault.fs.io.DocViewParser;
import org.apache.jackrabbit.vault.fs.io.DocViewParser.XmlParseException;
import org.apache.jackrabbit.vault.util.DocViewNode2;
import org.apache.jackrabbit.vault.util.DocViewProperty2;
import org.apache.jackrabbit.vault.util.JcrConstants;
import org.apache.jackrabbit.vault.validation.impl.util.ValidatorDocViewParserHandler;
import org.apache.jackrabbit.vault.validation.spi.DocumentViewXmlValidator;
import org.apache.jackrabbit.vault.validation.spi.NodeContext;
import org.apache.jackrabbit.vault.validation.spi.ValidationMessage;
import org.apache.jackrabbit.vault.validation.spi.ValidationMessageSeverity;
import org.apache.jackrabbit.vault.validation.spi.impl.DocumentViewParserValidator;
import org.apache.jackrabbit.vault.validation.spi.impl.DocumentViewParserValidatorFactory;
import org.apache.jackrabbit.vault.validation.spi.util.NodeContextImpl;
import org.apache.sling.api.SlingConstants;
import org.apache.sling.jcr.resource.JcrResourceConstants;
import org.hamcrest.MatcherAssert;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.InOrder;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.junit.MockitoJUnitRunner;
import org.xml.sax.SAXException;

@RunWith(MockitoJUnitRunner.class)
public class DocumentViewParserValidatorTest {

    private DocumentViewParserValidator validator;
    private Map<String, Integer> nodePathsAndLineNumbers;

    private static final Name NAME_SLING_RESOURCE_TYPE = NameFactoryImpl.getInstance()
            .create(JcrResourceConstants.SLING_NAMESPACE_URI, SlingConstants.PROPERTY_RESOURCE_TYPE);
    private static final Name NAME_SLING_TARGET =
            NameFactoryImpl.getInstance().create(JcrResourceConstants.SLING_NAMESPACE_URI, "target");

    @Mock
    private DocumentViewXmlValidator docViewXmlValidator;

    @Before
    public void setUp() throws ParserConfigurationException, SAXException, IOException {
        validator = new DocumentViewParserValidator(
                ValidationMessageSeverity.ERROR, false, ValidationMessageSeverity.ERROR);
        nodePathsAndLineNumbers = new HashMap<>();
        validator.setDocumentViewXmlValidators(Collections.singletonMap("docviewid", docViewXmlValidator));
    }

    @Test
    public void testShouldValidate() {
        Assert.assertTrue(validator.shouldValidateJcrData(Paths.get("some", "file.xml"), Paths.get("")));
        Assert.assertFalse(validator.shouldValidateJcrData(Paths.get("some", "other", "file.json"), Paths.get("")));
    }

    @Test
    public void testDocViewDotContentXml()
            throws ParserConfigurationException, SAXException, URISyntaxException, IOException, NamespaceException {
        Mockito.when(docViewXmlValidator.validate(Mockito.any(DocViewNode2.class), Mockito.any(), Mockito.anyBoolean()))
                .thenReturn(
                        Collections.singleton(new ValidationMessage(ValidationMessageSeverity.ERROR, "startDocView")));
        try (InputStream input = this.getClass().getResourceAsStream("/simple-package/jcr_root/apps/.content.xml")) {
            Collection<ValidationMessage> messages = validator.validateJcrData(
                    input, Paths.get("apps", ".content.xml"), Paths.get(""), nodePathsAndLineNumbers);
            // filter
            ValidationExecutorTest.assertViolation(
                    messages,
                    new ValidationViolation(
                            "docviewid",
                            ValidationMessageSeverity.ERROR,
                            "startDocView",
                            Paths.get("apps/.content.xml"),
                            Paths.get(""),
                            "/apps",
                            19,
                            35,
                            null),
                    new ValidationViolation(
                            "docviewid",
                            ValidationMessageSeverity.ERROR,
                            "startDocView",
                            Paths.get("apps/.content.xml"),
                            Paths.get(""),
                            "/apps/somepath",
                            21,
                            29,
                            null),
                    new ValidationViolation(
                            "docviewid",
                            ValidationMessageSeverity.ERROR,
                            "startDocView",
                            Paths.get("apps/.content.xml"),
                            Paths.get(""),
                            "/apps/somepath/jc:content",
                            22,
                            54,
                            null),
                    new ValidationViolation(
                            "docviewid",
                            ValidationMessageSeverity.ERROR,
                            "startDocView",
                            Paths.get("apps/.content.xml"),
                            Paths.get(""),
                            "/apps/0123_sample.jpg",
                            25,
                            29,
                            null),
                    new ValidationViolation(
                            "docviewid",
                            ValidationMessageSeverity.ERROR,
                            "startDocView",
                            Paths.get("apps/.content.xml"),
                            Paths.get(""),
                            "/apps/01234_sample.jpg",
                            26,
                            55,
                            null));

            // verify node names
            InOrder orderVerifier = Mockito.inOrder(docViewXmlValidator);
            Map<String, Integer> expectedNodePathsAndLineNumber = new HashMap<>();
            expectedNodePathsAndLineNumber.put("/apps", 19);
            expectedNodePathsAndLineNumber.put("/apps/somepath", 21);
            expectedNodePathsAndLineNumber.put("/apps/somepath/jc:content", 22);
            expectedNodePathsAndLineNumber.put("/apps/01234_sample.jpg", 26);
            Assert.assertEquals(expectedNodePathsAndLineNumber, nodePathsAndLineNumbers);
            Collection<DocViewProperty2> properties = new ArrayList<>();
            properties.add(new DocViewProperty2(NameConstants.JCR_PRIMARYTYPE, "sling:Folder"));
            NameFactory nameFactory = NameFactoryImpl.getInstance();
            DocViewNode2 node = new DocViewNode2(nameFactory.create("{}apps"), properties);
            NodeContextImpl contextRootNode =
                    new NodeContextImpl("/apps", Paths.get("apps", ".content.xml"), Paths.get(""), 19, 35);
            orderVerifier.verify(docViewXmlValidator).validate(node, contextRootNode, true);

            properties = new ArrayList<>();
            properties.add(new DocViewProperty2(NameConstants.JCR_PRIMARYTYPE, JcrConstants.NT_UNSTRUCTURED));
            properties.add(new DocViewProperty2(nameFactory.create("{}attribute1"), "value1"));
            DocViewNode2 childNode = new DocViewNode2(nameFactory.create("{}somepath"), properties);
            NodeContextImpl contextChildNode =
                    new NodeContextImpl("/apps/somepath", Paths.get("apps", ".content.xml"), Paths.get(""), 21, 29);
            orderVerifier.verify(docViewXmlValidator).validate(childNode, contextChildNode, false);

            properties = new ArrayList<>();
            properties.add(new DocViewProperty2(NameConstants.JCR_PRIMARYTYPE, JcrConstants.NT_UNSTRUCTURED));
            DocViewNode2 grandChildNode = new DocViewNode2(NameConstants.JCR_CONTENT, properties);
            NodeContextImpl contextGrandChildNode = new NodeContextImpl(
                    "/apps/somepath/jc:content", Paths.get("apps", ".content.xml"), Paths.get(""), 22, 54);
            orderVerifier.verify(docViewXmlValidator).validate(grandChildNode, contextGrandChildNode, false);

            orderVerifier
                    .verify(docViewXmlValidator)
                    .validateEnd(
                            grandChildNode,
                            duplicateNodeContextWithDifferentLocation(contextGrandChildNode, 23, 22),
                            false);
            orderVerifier
                    .verify(docViewXmlValidator)
                    .validateEnd(childNode, duplicateNodeContextWithDifferentLocation(contextChildNode, 24, 16), false);
            orderVerifier
                    .verify(docViewXmlValidator)
                    .validateEnd(node, duplicateNodeContextWithDifferentLocation(contextRootNode, 27, 11), true);
        }
    }

    private static final NodeContextImpl duplicateNodeContextWithDifferentLocation(
            NodeContext nodeContext, int line, int column) {
        return new NodeContextImpl(
                nodeContext.getNodePath(), nodeContext.getFilePath(), nodeContext.getBasePath(), line, column);
    }

    @Test
    public void testDocViewDotContentXmlOnRootLevel()
            throws ParserConfigurationException, SAXException, URISyntaxException, IOException, NamespaceException {
        Mockito.when(docViewXmlValidator.validate(Mockito.any(DocViewNode2.class), Mockito.any(), Mockito.anyBoolean()))
                .thenReturn(
                        Collections.singleton(new ValidationMessage(ValidationMessageSeverity.ERROR, "startDocView")));
        try (InputStream input = this.getClass().getResourceAsStream("/simple-package/jcr_root/.content.xml")) {
            Collection<ValidationMessage> messages =
                    validator.validateJcrData(input, Paths.get(".content.xml"), Paths.get(""), nodePathsAndLineNumbers);
            // filter
            ValidationExecutorTest.assertViolation(
                    messages,
                    new ValidationViolation(
                            "docviewid",
                            ValidationMessageSeverity.ERROR,
                            "startDocView",
                            Paths.get(".content.xml"),
                            Paths.get(""),
                            "/",
                            6,
                            32,
                            null));

            // verify node names
            Map<String, Integer> expectedNodePathsAndLineNumber = new HashMap<>();
            expectedNodePathsAndLineNumber.put("/", 6);
            Assert.assertEquals(expectedNodePathsAndLineNumber, nodePathsAndLineNumbers);
            Collection<DocViewProperty2> properties = new ArrayList<>();
            properties.add(new DocViewProperty2(NameConstants.JCR_PRIMARYTYPE, "rep:root"));
            properties.add(new DocViewProperty2(
                    NameConstants.JCR_MIXINTYPES,
                    Arrays.asList("rep:AccessControllable", "rep:RepoAccessControllable")));
            properties.add(new DocViewProperty2(NAME_SLING_RESOURCE_TYPE, "sling:redirect"));
            properties.add(new DocViewProperty2(NAME_SLING_TARGET, "/index.html"));

            DocViewNode2 node = new DocViewNode2(NameConstants.ROOT, properties);
            Mockito.verify(docViewXmlValidator)
                    .validate(node, new NodeContextImpl("/", Paths.get(".content.xml"), Paths.get(""), 6, 32), true);
        }
    }

    @Test
    public void testDocViewWithNamespacedFilename()
            throws ParserConfigurationException, SAXException, URISyntaxException, IOException, NamespaceException {
        Path filePath = Paths.get("apps", "_cq_content.xml");
        String nodePath = "/apps/cq:content";
        String message = "Unknown namespace prefix used in file name 'cq:content'";
        // fail during parsing due to unknown namespace in filename
        try (InputStream input = this.getClass().getResourceAsStream("/simple-package/jcr_root/apps/_cq_content.xml")) {
            Collection<ValidationMessage> messages =
                    validator.validateJcrData(input, filePath, Paths.get(""), nodePathsAndLineNumbers);
            ValidationExecutorTest.assertViolation(
                    messages,
                    new ValidationViolation(
                            DocumentViewParserValidatorFactory.ID,
                            ValidationMessageSeverity.ERROR,
                            "Could not parse FileVault Document View XML: " + message,
                            filePath,
                            Paths.get(""),
                            nodePath,
                            19,
                            36,
                            new DocViewParser.XmlParseException(message, nodePath, 19, 36)));
        }
        validator =
                new DocumentViewParserValidator(ValidationMessageSeverity.ERROR, true, ValidationMessageSeverity.ERROR);
        try (InputStream input = this.getClass().getResourceAsStream("/simple-package/jcr_root/apps/_cq_content.xml")) {
            Collection<ValidationMessage> messages =
                    validator.validateJcrData(input, filePath, Paths.get(""), nodePathsAndLineNumbers);
            // filter
            MatcherAssert.assertThat(
                    messages, AnyValidationViolationMessageMatcher.noValidationViolationMessageInCollection());
        }
    }

    @Test
    public void testDocViewWithEmptyElements() throws IOException {
        try (InputStream input =
                this.getClass().getResourceAsStream("/simple-package/jcr_root/apps/emptyelements/.content.xml")) {
            Collection<ValidationMessage> messages = validator.validateJcrData(
                    input, Paths.get("apps", "emptyelements", ".content.xml"), Paths.get(""), nodePathsAndLineNumbers);
            MatcherAssert.assertThat(
                    messages, AnyValidationViolationMessageMatcher.noValidationViolationMessageInCollection());

            // verify node names
            Map<String, Integer> expectedNodePathsAndLineNumber = new HashMap<>();
            expectedNodePathsAndLineNumber.put("/apps/emptyelements", 20);
            expectedNodePathsAndLineNumber.put("/apps/emptyelements/nonemptyelement", 23);
            Assert.assertEquals(expectedNodePathsAndLineNumber, nodePathsAndLineNumbers);
        }
    }

    @Test
    public void testDocViewWithRegularFileName()
            throws ParserConfigurationException, SAXException, URISyntaxException, IOException, NamespaceException {

        try (InputStream input = this.getClass().getResourceAsStream("/simple-package/jcr_root/apps/child1.xml")) {
            Collection<ValidationMessage> messages = validator.validateJcrData(
                    input, Paths.get("apps", "child1.xml"), Paths.get(""), nodePathsAndLineNumbers);
            MatcherAssert.assertThat(
                    messages, AnyValidationViolationMessageMatcher.noValidationViolationMessageInCollection());

            NameFactory nameFactory = NameFactoryImpl.getInstance();
            Collection<DocViewProperty2> properties = new ArrayList<>();
            properties.add(new DocViewProperty2(NameConstants.JCR_PRIMARYTYPE, "sling:Folder"));
            DocViewNode2 node = new DocViewNode2(nameFactory.create("{}child1"), properties);
            Mockito.verify(docViewXmlValidator)
                    .validate(
                            node,
                            new NodeContextImpl("/apps/child1", Paths.get("apps", "child1.xml"), Paths.get(""), 20, 36),
                            true);

            properties = new ArrayList<>();
            properties.add(new DocViewProperty2(NameConstants.JCR_PRIMARYTYPE, JcrConstants.NT_UNSTRUCTURED));
            properties.add(new DocViewProperty2(nameFactory.create("{}attribute1"), "value1"));
            node = new DocViewNode2(nameFactory.create("{}somepath"), properties);
            Mockito.verify(docViewXmlValidator)
                    .validate(
                            node,
                            new NodeContextImpl(
                                    "/apps/child1/somepath", Paths.get("apps", "child1.xml"), Paths.get(""), 23, 6),
                            false);

            // verify node names
            Map<String, Integer> expectedNodePathsAndLineNumber = new HashMap<>();
            expectedNodePathsAndLineNumber.put("/apps/child1", 20);
            expectedNodePathsAndLineNumber.put("/apps/child1/somepath", 23);
            Assert.assertEquals(expectedNodePathsAndLineNumber, nodePathsAndLineNumbers);
        }
    }

    @Test
    public void testDocViewWithRegularFileNameAndUndeclaredNamespacePrefixInFilename()
            throws ParserConfigurationException, SAXException, URISyntaxException, IOException, NamespaceException {

        try (InputStream input = this.getClass().getResourceAsStream("/simple-package/jcr_root/apps/child1.xml")) {
            Collection<ValidationMessage> messages = validator.validateJcrData(
                    input, Paths.get("apps", "_cq_child1.xml"), Paths.get(""), nodePathsAndLineNumbers);

            ValidationExecutorTest.assertViolation(
                    messages,
                    new ValidationViolation(
                            DocumentViewParserValidatorFactory.ID,
                            ValidationMessageSeverity.ERROR,
                            "Could not parse FileVault Document View XML: Unknown namespace prefix used in file name 'cq:child1'",
                            Paths.get("apps", "_cq_child1.xml"),
                            Paths.get(""),
                            "/apps/cq:child1",
                            20,
                            36,
                            new XmlParseException(
                                    "Unknown namespace prefix used in file name 'cq:child1'",
                                    "/apps/cq:child1",
                                    20,
                                    36)));
        }
    }

    @Test
    public void testDocViewDotContentXmlWithRootElementDifferentThanJcrRoot()
            throws ParserConfigurationException, SAXException, URISyntaxException, IOException, NamespaceException {
        try (InputStream input =
                this.getClass().getResourceAsStream("/simple-package/jcr_root/apps/child2/.content.xml")) {
            Collection<ValidationMessage> messages = validator.validateJcrData(
                    input, Paths.get("apps", "child2", ".content.xml"), Paths.get(""), nodePathsAndLineNumbers);
            MatcherAssert.assertThat(
                    messages, AnyValidationViolationMessageMatcher.noValidationViolationMessageInCollection());

            NameFactory nameFactory = NameFactoryImpl.getInstance();
            Collection<DocViewProperty2> properties = new ArrayList<>();
            properties.add(new DocViewProperty2(NameConstants.JCR_PRIMARYTYPE, "sling:Folder"));
            DocViewNode2 node = new DocViewNode2(nameFactory.create("{}child3"), properties);
            Mockito.verify(docViewXmlValidator)
                    .validate(
                            node,
                            new NodeContextImpl(
                                    "/apps/child3", Paths.get("apps", "child2", ".content.xml"), Paths.get(""), 20, 36),
                            true);

            properties.clear();
            properties.add(new DocViewProperty2(NameConstants.JCR_PRIMARYTYPE, JcrConstants.NT_UNSTRUCTURED));
            properties.add(new DocViewProperty2(nameFactory.create("{}attribute1"), "value1"));
            node = new DocViewNode2(nameFactory.create("{}somepath"), properties);
            Mockito.verify(docViewXmlValidator)
                    .validate(
                            node,
                            new NodeContextImpl(
                                    "/apps/child3/somepath",
                                    Paths.get("apps", "child2", ".content.xml"),
                                    Paths.get(""),
                                    23,
                                    6),
                            false);

            // verify node names
            Map<String, Integer> expectedNodePathsAndLineNumber = new HashMap<>();
            expectedNodePathsAndLineNumber.put("/apps/child3", 20);
            expectedNodePathsAndLineNumber.put("/apps/child3/somepath", 23);
            Assert.assertEquals(expectedNodePathsAndLineNumber, nodePathsAndLineNumbers);
        }
    }

    @Test
    public void testDocViewWithRegularFileNameWithRootElementDifferentThanJcrRoot() throws IOException {
        // https://issues.apache.org/jira/browse/JCRVLT-358 and https://issues.apache.org/jira/browse/JCRVLT-637
        try (InputStream input =
                this.getClass().getResourceAsStream("/simple-package/jcr_root/apps/child2/child1.xml")) {
            Collection<ValidationMessage> messages = validator.validateJcrData(
                    input, Paths.get("apps", "child2", "child1.xml"), Paths.get(""), nodePathsAndLineNumbers);
            MatcherAssert.assertThat(
                    messages, AnyValidationViolationMessageMatcher.noValidationViolationMessageInCollection());

            Mockito.verifyNoMoreInteractions(docViewXmlValidator);

            // verify node names in case this is no docview xml
            Assert.assertEquals(new HashMap<>(), nodePathsAndLineNumbers);
        }
    }

    @Test
    public void testDocViewWithUnknownType()
            throws ParserConfigurationException, SAXException, URISyntaxException, IOException {
        try (InputStream input =
                this.getClass().getResourceAsStream("/simple-package/jcr_root/apps/invalid/wrongtype.xml")) {
            Collection<ValidationMessage> messages = validator.validateJcrData(
                    input, Paths.get("apps", "invalid", "wrongtype.xml"), Paths.get(""), nodePathsAndLineNumbers);

            ValidationExecutorTest.assertViolation(
                    messages,
                    new ValidationViolation(
                            DocumentViewParserValidatorFactory.ID,
                            ValidationMessageSeverity.ERROR,
                            "Could not parse FileVault Document View XML: unknown type: Invalid",
                            Paths.get("apps/invalid/wrongtype.xml"),
                            Paths.get(""),
                            "/apps/invalid/wrongtype/somepath",
                            24,
                            6,
                            new XmlParseException(
                                    new IllegalArgumentException("unknown type: Invalid"),
                                    "/apps/invalid/wrongtype/somepath",
                                    24,
                                    6)));
        }
    }

    @Test
    public void testDocViewWithInvalidStringSerializationForType()
            throws ParserConfigurationException, SAXException, URISyntaxException, IOException {
        try (InputStream input =
                this.getClass().getResourceAsStream("/simple-package/jcr_root/apps/invalid/inconvertibletypes.xml")) {
            Collection<ValidationMessage> messages = validator.validateJcrData(
                    input,
                    Paths.get("apps", "invalid", "inconvertibletypes.xml"),
                    Paths.get(""),
                    nodePathsAndLineNumbers);

            ValidationExecutorTest.assertViolation(
                    messages,
                    new ValidationViolation(
                            DocumentViewParserValidatorFactory.ID,
                            ValidationMessageSeverity.ERROR,
                            String.format(
                                    ValidatorDocViewParserHandler.MESSAGE_INVALID_STRING_SERIALIZATION,
                                    "Long",
                                    "attribute2",
                                    "1.0"),
                            Paths.get("apps/invalid/inconvertibletypes.xml"),
                            Paths.get(""),
                            "/apps/invalid/inconvertibletypes/somepath",
                            28,
                            6,
                            null),
                    new ValidationViolation(
                            DocumentViewParserValidatorFactory.ID,
                            ValidationMessageSeverity.ERROR,
                            String.format(
                                    ValidatorDocViewParserHandler.MESSAGE_INVALID_STRING_SERIALIZATION,
                                    "Date",
                                    "attribute1",
                                    "somedate"),
                            Paths.get("apps/invalid/inconvertibletypes.xml"),
                            Paths.get(""),
                            "/apps/invalid/inconvertibletypes/somepath",
                            28,
                            6,
                            null));
        }
    }

    @Test
    public void testDocViewWithUnusedCharacterData()
            throws ParserConfigurationException, SAXException, URISyntaxException, IOException {
        try (InputStream input =
                this.getClass().getResourceAsStream("/simple-package/jcr_root/apps/invalid/unusedcharacterdata.xml")) {
            Collection<ValidationMessage> messages = validator.validateJcrData(
                    input,
                    Paths.get("apps", "invalid", "unusedcharacterdata.xml"),
                    Paths.get(""),
                    nodePathsAndLineNumbers);

            ValidationExecutorTest.assertViolation(
                    messages,
                    new ValidationViolation(
                            DocumentViewParserValidatorFactory.ID,
                            ValidationMessageSeverity.ERROR,
                            String.format(
                                    ValidatorDocViewParserHandler.MESSAGE_UNUSED_CHARACTER_DATA,
                                    "This is some character data"),
                            Paths.get("apps/invalid/unusedcharacterdata.xml"),
                            Paths.get(""),
                            "/apps/invalid/unusedcharacterdata/somepath",
                            21,
                            78,
                            null));
        }
    }
}
