/*
 * 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.xml.parsers.ParserConfigurationException;

import java.io.IOException;
import java.io.InputStream;
import java.net.URISyntaxException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

import org.apache.jackrabbit.vault.fs.config.ConfigurationException;
import org.apache.jackrabbit.vault.validation.spi.DocumentViewXmlValidator;
import org.apache.jackrabbit.vault.validation.spi.GenericJcrDataValidator;
import org.apache.jackrabbit.vault.validation.spi.GenericMetaInfDataValidator;
import org.apache.jackrabbit.vault.validation.spi.JcrPathValidator;
import org.apache.jackrabbit.vault.validation.spi.MetaInfPathValidator;
import org.apache.jackrabbit.vault.validation.spi.NodeContext;
import org.apache.jackrabbit.vault.validation.spi.NodePathValidator;
import org.apache.jackrabbit.vault.validation.spi.OsgiConfigurationValidator;
import org.apache.jackrabbit.vault.validation.spi.PropertiesValidator;
import org.apache.jackrabbit.vault.validation.spi.ValidationContext;
import org.apache.jackrabbit.vault.validation.spi.ValidationMessage;
import org.apache.jackrabbit.vault.validation.spi.ValidationMessageSeverity;
import org.apache.jackrabbit.vault.validation.spi.Validator;
import org.apache.jackrabbit.vault.validation.spi.impl.DocumentViewParserValidatorFactory;
import org.apache.jackrabbit.vault.validation.spi.impl.OsgiConfigurationParserValidatorFactory;
import org.apache.jackrabbit.vault.validation.spi.util.NodeContextImpl;
import org.hamcrest.MatcherAssert;
import org.hamcrest.Matchers;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.junit.MockitoJUnitRunner;
import org.xml.sax.SAXException;

import static org.junit.Assert.assertEquals;

@RunWith(MockitoJUnitRunner.class)
public class ValidationExecutorTest {

    @Mock
    private DocumentViewXmlValidator docViewXmlValidator;

    @Mock
    private PropertiesValidator propertiesValidator;

    @Mock
    private NodePathValidator nodePathValidator;

    @Mock
    private GenericJcrDataValidator genericJcrDataValidator;

    @Mock
    private GenericJcrDataValidator genericJcrDataValidator2;

    @Mock
    private GenericMetaInfDataValidator genericMetaInfDataValidator;

    @Mock
    private GenericMetaInfDataValidator genericMetaInfDataValidator2;

    @Mock
    private GenericMetaInfDataValidator osgiConfigurationParserValidator;

    @Mock
    private JcrPathValidator jcrPathValidator;

    @Mock
    private MetaInfPathValidator metaInfPathValidator;

    @Mock
    private OsgiConfigurationValidator osgiConfigurationValidator;

    @Mock
    private Validator unusedValidator;

    @Mock
    private ValidationContext context;

    private ValidationExecutor executor;

    private Map<String, Validator> validators;

    @Before
    public void setUp() throws ParserConfigurationException, SAXException {
        validators = new LinkedHashMap<>();
        validators.put("docviewid", docViewXmlValidator);
        validators.put("propertiesid", propertiesValidator);
        validators.put("genericmetadataid", genericMetaInfDataValidator);
        validators.put("genericmetadataid2", genericMetaInfDataValidator2);
        validators.put(DocumentViewParserValidatorFactory.ID, genericJcrDataValidator);
        validators.put("genericjcrdataid2", genericJcrDataValidator2);
        validators.put("jcrpathid", jcrPathValidator);
        validators.put("metainfpathid", metaInfPathValidator);
        validators.put("nodepathid", nodePathValidator);
        validators.put(OsgiConfigurationParserValidatorFactory.ID, osgiConfigurationParserValidator);
        validators.put("osgiConfigurationValidator", osgiConfigurationValidator);
        validators.put("unusedid", unusedValidator);
        executor = new ValidationExecutor(validators);
    }

    @Test
    public void testUnusedValidators() throws ParserConfigurationException, SAXException {
        MatcherAssert.assertThat(executor.getUnusedValidatorsById(), Matchers.hasEntry("unusedid", unusedValidator));
        MatcherAssert.assertThat(executor.getUnusedValidatorsById(), Matchers.aMapWithSize(1));
    }

    @Test
    public void testWithoutOsgiValidator() throws ParserConfigurationException {
        assertEquals(12, executor.getAllValidatorsById().size());
        validators.remove("osgiConfigurationValidator");
        // this must implicitly remove the jackrabbit-osgiconfigparser as well
        executor = new ValidationExecutor(validators);
        assertEquals(10, executor.getAllValidatorsById().size());
    }

    @Test
    public void testValidateNodePath()
            throws ParserConfigurationException, SAXException, IOException, URISyntaxException {
        Mockito.when(nodePathValidator.validate(
                        Mockito.argThat(new NodeContextNodePathMatcher("/apps/invalid/wrongtype.xml"))))
                .thenReturn(Collections.singletonList(
                        new ValidationMessage(ValidationMessageSeverity.ERROR, "Invalid node path")));
        try (InputStream input =
                this.getClass().getResourceAsStream("/simple-package/jcr_root/apps/invalid/wrongtype.xml")) {
            Collection<ValidationViolation> messages =
                    validate(input, executor, Paths.get(""), "apps/invalid/wrongtype.xml", false);

            ValidationExecutorTest.assertViolation(
                    messages,
                    new ValidationViolation(
                            "nodepathid",
                            ValidationMessageSeverity.ERROR,
                            "Invalid node path",
                            Paths.get("apps/invalid/wrongtype.xml"),
                            Paths.get(""),
                            "/apps/invalid/wrongtype.xml",
                            0,
                            0,
                            null));
        }
    }

    @Test
    public void testGenericMetaInfData()
            throws URISyntaxException, IOException, SAXException, ParserConfigurationException, ConfigurationException {
        Mockito.when(genericMetaInfDataValidator.shouldValidateMetaInfData(Mockito.any(), Mockito.any()))
                .thenReturn(true);
        Mockito.when(genericMetaInfDataValidator2.shouldValidateMetaInfData(Mockito.any(), Mockito.any()))
                .thenReturn(true);
        CapturingInputStreamFromArgumentAnswer<Void> answer =
                new CapturingInputStreamFromArgumentAnswer<>(StandardCharsets.US_ASCII, 0, null);
        Mockito.when(genericMetaInfDataValidator.validateMetaInfData(Mockito.any(), Mockito.any(), Mockito.any()))
                .thenAnswer(answer);
        CapturingInputStreamFromArgumentAnswer<Collection<ValidationMessage>> answer2 =
                new CapturingInputStreamFromArgumentAnswer<>(
                        StandardCharsets.US_ASCII,
                        0,
                        Collections.singleton(new ValidationMessage(ValidationMessageSeverity.WARN, "error1")));
        Mockito.when(genericMetaInfDataValidator2.validateMetaInfData(Mockito.any(), Mockito.any(), Mockito.any()))
                .thenAnswer(answer2);
        Mockito.when(metaInfPathValidator.validateMetaInfPath(Mockito.any(), Mockito.any(), Mockito.anyBoolean()))
                .thenReturn(Collections.singleton(new ValidationMessage(ValidationMessageSeverity.ERROR, "patherror")));
        try (InputStream input =
                this.getClass().getResourceAsStream("/simple-package/META-INF/vault/genericfile.txt")) {
            Collection<ValidationViolation> messages =
                    validate(input, executor, Paths.get(""), "vault/genericfile.txt", true);
            assertViolation(
                    messages,
                    new ValidationViolation(
                            "metainfpathid",
                            ValidationMessageSeverity.ERROR,
                            "patherror",
                            Paths.get("vault", "genericfile.txt"),
                            Paths.get(""),
                            null,
                            0,
                            0,
                            null),
                    new ValidationViolation(
                            "genericmetadataid2",
                            ValidationMessageSeverity.WARN,
                            "error1",
                            Paths.get("vault", "genericfile.txt"),
                            Paths.get(""),
                            null,
                            0,
                            0,
                            null));
            Assert.assertEquals("Test", answer.getValue());
            Assert.assertEquals("Test", answer2.getValue());
            Path expectedPath = Paths.get("vault/genericfile.txt");
            Mockito.verify(metaInfPathValidator).validateMetaInfPath(expectedPath, Paths.get(""), false);
            Mockito.verify(genericMetaInfDataValidator, Mockito.atLeastOnce())
                    .shouldValidateMetaInfData(expectedPath, Paths.get(""));
            Mockito.verify(genericMetaInfDataValidator)
                    .validateMetaInfData(Mockito.any(), Mockito.eq(expectedPath), Mockito.eq(Paths.get("")));
            Mockito.verify(genericMetaInfDataValidator2, Mockito.atLeastOnce())
                    .shouldValidateMetaInfData(expectedPath, Paths.get(""));
            Mockito.verify(genericMetaInfDataValidator2)
                    .validateMetaInfData(Mockito.any(), Mockito.eq(expectedPath), Mockito.eq(Paths.get("")));
        }
    }

    @Test
    public void testGenericMetaInfDataWithNotInterestedValidator()
            throws URISyntaxException, IOException, SAXException, ParserConfigurationException, ConfigurationException {
        try (InputStream input =
                this.getClass().getResourceAsStream("/simple-package/META-INF/vault/genericfile.txt")) {
            Collection<ValidationViolation> messages =
                    validate(input, executor, Paths.get(""), "vault/genericfile.txt", true);
            MatcherAssert.assertThat(
                    messages, AnyValidationViolationMessageMatcher.noValidationViolationMessageInCollection());
            Mockito.verify(genericMetaInfDataValidator, Mockito.never())
                    .validateMetaInfData(Mockito.any(), Mockito.any(), Mockito.any());
        }
    }

    @Test
    public void testMetaInfFolder() throws URISyntaxException, IOException, SAXException {
        Collection<ValidationViolation> messages =
                validateFolder(executor, Paths.get(""), "vault/genericfile.txt", true);
        MatcherAssert.assertThat(
                messages, AnyValidationViolationMessageMatcher.noValidationViolationMessageInCollection());
        Mockito.verify(metaInfPathValidator)
                .validateMetaInfPath(Paths.get("vault", "genericfile.txt"), Paths.get(""), true);
    }

    @Test
    public void testGenericJcrData()
            throws URISyntaxException, IOException, SAXException, ParserConfigurationException, ConfigurationException {
        Mockito.when(genericJcrDataValidator.shouldValidateJcrData(Mockito.any(), Mockito.any()))
                .thenReturn(true);
        CapturingInputStreamFromArgumentAnswer<Collection<ValidationMessage>> answer =
                new CapturingInputStreamFromArgumentAnswer<>(
                        StandardCharsets.US_ASCII,
                        0,
                        Collections.singleton(new ValidationMessage(ValidationMessageSeverity.WARN, "error1")));
        Mockito.when(genericJcrDataValidator.validateJcrData(
                        Mockito.any(), Mockito.any(), Mockito.any(), Mockito.any()))
                .thenAnswer(answer);
        CapturingInputStreamFromArgumentAnswer<Void> answer2 =
                new CapturingInputStreamFromArgumentAnswer<>(StandardCharsets.US_ASCII, 0, null);
        Mockito.when(genericJcrDataValidator2.shouldValidateJcrData(Mockito.any(), Mockito.any()))
                .thenReturn(true);
        Mockito.when(genericJcrDataValidator2.validateJcrData(
                        Mockito.any(), Mockito.any(), Mockito.any(), Mockito.any()))
                .thenAnswer(answer2);
        Mockito.when(jcrPathValidator.validateJcrPath(Mockito.any(), Mockito.anyBoolean(), Mockito.anyBoolean()))
                .thenReturn(Collections.singleton(new ValidationMessage(ValidationMessageSeverity.ERROR, "patherror")));
        try (InputStream input = this.getClass().getResourceAsStream("/simple-package/jcr_root/apps/genericfile.xml")) {
            Collection<ValidationViolation> messages =
                    validate(input, executor, Paths.get(""), "apps/genericfile.xml", false);
            assertViolation(
                    messages,
                    new ValidationViolation(
                            DocumentViewParserValidatorFactory.ID,
                            ValidationMessageSeverity.WARN,
                            "error1",
                            Paths.get("apps", "genericfile.xml"),
                            Paths.get(""),
                            null,
                            0,
                            0,
                            null),
                    new ValidationViolation(
                            "jcrpathid",
                            ValidationMessageSeverity.ERROR,
                            "patherror",
                            Paths.get("apps", "genericfile.xml"),
                            Paths.get(""),
                            null,
                            0,
                            0,
                            null));
            Assert.assertEquals("Test", answer.getValue());
            Assert.assertEquals("Test", answer2.getValue());
            Path expectedPath = Paths.get("apps/genericfile.xml");
            NodeContext expectedNodeContext = new NodeContextImpl("/apps/genericfile.xml", expectedPath, Paths.get(""));
            Mockito.verify(jcrPathValidator).validateJcrPath(expectedNodeContext, false, false);
            Mockito.verify(genericJcrDataValidator, Mockito.atLeastOnce())
                    .shouldValidateJcrData(expectedPath, Paths.get(""));
            Mockito.verify(genericJcrDataValidator)
                    .validateJcrData(Mockito.any(), Mockito.eq(expectedPath), Mockito.eq(Paths.get("")), Mockito.any());
            Mockito.verify(genericJcrDataValidator2, Mockito.atLeastOnce())
                    .shouldValidateJcrData(expectedPath, Paths.get(""));
            Mockito.verify(genericJcrDataValidator2)
                    .validateJcrData(Mockito.any(), Mockito.eq(expectedPath), Mockito.eq(Paths.get("")), Mockito.any());
        }
    }

    @Test
    public void testGenericJcrDataWithNotInterestedValidator()
            throws URISyntaxException, IOException, SAXException, ParserConfigurationException, ConfigurationException {
        try (InputStream input = this.getClass().getResourceAsStream("/simple-package/jcr_root/apps/genericfile.xml")) {
            Collection<ValidationViolation> messages =
                    validate(input, executor, Paths.get(""), "apps/genericfile.xml", false);
            MatcherAssert.assertThat(
                    messages, AnyValidationViolationMessageMatcher.noValidationViolationMessageInCollection());
            Mockito.verify(genericJcrDataValidator, Mockito.never())
                    .validateJcrData(Mockito.any(), Mockito.any(), Mockito.any(), Mockito.any());
        }
    }

    @Test
    public void testGenericJcrDataWithNoGenericJcrDataValidator()
            throws URISyntaxException, IOException, SAXException, ParserConfigurationException, ConfigurationException {
        Map<String, Validator> validators = new LinkedHashMap<>();
        validators.put("docviewid", docViewXmlValidator);
        validators.put("propertiesid", propertiesValidator);
        validators.put("genericmetadataid", genericMetaInfDataValidator);
        validators.put("genericmetadataid2", genericMetaInfDataValidator2);
        validators.put("jcrpathid", jcrPathValidator);
        validators.put("metainfpathid", metaInfPathValidator);
        validators.put("nodepathid", nodePathValidator);
        validators.put("unusedid", unusedValidator);
        Assert.assertThrows(IllegalStateException.class, () -> new ValidationExecutor(validators));
    }

    @Test
    public void testGenericJcrDataWithNoGenericJcrDataAndNoPathValidator()
            throws URISyntaxException, IOException, SAXException, ParserConfigurationException, ConfigurationException {
        Map<String, Validator> validators = new LinkedHashMap<>();
        validators.put("docviewid", docViewXmlValidator);
        validators.put("propertiesid", propertiesValidator);
        validators.put("genericmetadataid", genericMetaInfDataValidator);
        validators.put("genericmetadataid2", genericMetaInfDataValidator2);
        validators.put("metainfpathid", metaInfPathValidator);
        validators.put("unusedid", unusedValidator);
        executor = new ValidationExecutor(validators);
        try (InputStream input = this.getClass().getResourceAsStream("/simple-package/jcr_root/apps/genericfile.xml")) {
            Collection<ValidationViolation> messages =
                    validate(input, executor, Paths.get(""), "apps/genericfile.xml", false);
            MatcherAssert.assertThat(
                    messages, AnyValidationViolationMessageMatcher.noValidationViolationMessageInCollection());
            Mockito.verify(genericJcrDataValidator, Mockito.never())
                    .validateJcrData(Mockito.any(), Mockito.any(), Mockito.any(), Mockito.any());
        }
    }

    @Test
    public void testJcrRootFolder() throws URISyntaxException, IOException, SAXException {
        Collection<ValidationViolation> messages = validateFolder(executor, Paths.get(""), "apps.dir", false);
        MatcherAssert.assertThat(
                messages, AnyValidationViolationMessageMatcher.noValidationViolationMessageInCollection());
        NodeContext expectedNodeContext = new NodeContextImpl("/apps", Paths.get("apps.dir"), Paths.get(""));
        Mockito.verify(jcrPathValidator).validateJcrPath(expectedNodeContext, true, true);
        Mockito.verify(nodePathValidator).validate(expectedNodeContext);
    }

    @Test
    public void testDone() {
        Mockito.when(genericJcrDataValidator.done())
                .thenReturn(Collections.singleton(new ValidationMessage(ValidationMessageSeverity.ERROR, "test1")));
        Mockito.when(genericJcrDataValidator2.done())
                .thenReturn(Collections.singleton(new ValidationMessage(ValidationMessageSeverity.WARN, "test2")));

        assertViolation(
                executor.done(),
                new ValidationViolation(
                        DocumentViewParserValidatorFactory.ID, ValidationMessageSeverity.ERROR, "test1"),
                new ValidationViolation("genericjcrdataid2", ValidationMessageSeverity.WARN, "test2"));
    }

    private Collection<ValidationViolation> validate(
            InputStream input, ValidationExecutor executor, Path basePath, String resourcePath, boolean isMetaInf)
            throws URISyntaxException, IOException, SAXException {
        if (input == null) {
            throw new IllegalArgumentException("Invalid input stream given");
        }
        final Collection<ValidationViolation> messages;
        if (isMetaInf) {
            messages = executor.validateMetaInf(input, Paths.get(resourcePath), basePath);
        } else {
            messages = executor.validateJcrRoot(input, Paths.get(resourcePath), basePath);
        }
        return messages;
    }

    private Collection<ValidationViolation> validateFolder(
            ValidationExecutor executor, Path basePath, String resourcePath, boolean isMetaInf)
            throws URISyntaxException, IOException, SAXException {
        final Collection<ValidationViolation> messages;
        if (isMetaInf) {
            messages = executor.validateMetaInf(null, Paths.get(resourcePath), basePath);
        } else {
            messages = executor.validateJcrRoot(null, Paths.get(resourcePath), basePath);
        }
        return messages;
    }

    @Test
    public void testFilePathToNodePath() {
        Assert.assertEquals("/apps/test", ValidationExecutor.filePathToNodePath(Paths.get("apps", "test")));
        Assert.assertEquals(
                "/some/other/path", ValidationExecutor.filePathToNodePath(Paths.get("some", "other", "path")));
        Assert.assertEquals(
                "/apps/test", ValidationExecutor.filePathToNodePath(Paths.get("apps", "test", "property.binary")));
        Assert.assertEquals("/", ValidationExecutor.filePathToNodePath(Paths.get("")));
        // don't strip xml extension
        Assert.assertEquals(
                "/apps/test/child.xml", ValidationExecutor.filePathToNodePath(Paths.get("apps", "test", "child.xml")));
        // filename escaping must be unescaped
        Assert.assertEquals(
                "/apps/test:node/cq:content",
                ValidationExecutor.filePathToNodePath(Paths.get("apps", "test%3anode", "_cq_content")));
    }

    public static void assertViolation(
            Collection<? extends ValidationMessage> messages,
            ValidationMessageSeverity thresholdSeverity,
            ValidationMessage... violations) {
        if (messages == null) {
            Assert.fail("No violations found at all!");
        } else {
            List<ValidationMessage> filteredMessages = messages.stream()
                    .filter(m -> m.getSeverity().ordinal() >= thresholdSeverity.ordinal())
                    .collect(Collectors.toList());
            MatcherAssert.assertThat(filteredMessages, Matchers.contains(violations));
        }
    }

    /**
     * Assert that all messages (after ones with level < WARN are filtered out) are equal to the given violations
     * @param messages the actual messages (to be filtered)
     * @param violations the expected violations (in the right order)
     */
    public static void assertViolation(
            Collection<? extends ValidationMessage> messages, ValidationMessage... violations) {
        ValidationExecutorTest.assertViolation(messages, ValidationMessageSeverity.WARN, violations);
    }
}
