/*
 * 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.catalina.servlets;

import java.io.ByteArrayInputStream;
import java.io.PrintWriter;
import java.nio.charset.StandardCharsets;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.sql.SQLFeatureNotSupportedException;
import java.sql.Statement;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.logging.Logger;

import javax.sql.DataSource;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;

import org.junit.Assert;
import org.junit.Assume;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
import org.junit.runners.Parameterized.Parameter;

import org.apache.catalina.servlets.WebdavServlet.PropertyStore;
import org.apache.catalina.servlets.WebdavServlet.PropertyUpdateType;
import org.apache.catalina.servlets.WebdavServlet.ProppatchOperation;
import org.apache.catalina.startup.LoggingBaseTest;
import org.apache.catalina.util.XMLWriter;
import org.apache.tomcat.util.compat.JreCompat;
import org.w3c.dom.Document;
import org.w3c.dom.Node;
import org.xml.sax.InputSource;

@RunWith(Parameterized.class)
public class TestWebdavPropertyStore extends LoggingBaseTest {

    private static final String PROPERTY1 =
            "<?xml version=\"1.0\" encoding=\"utf-8\" ?>\n" +
            "<T:customprop xmlns:T=\"http://tomcat.apache.org/testsuite\">\n" +
            "  <T:myvalue/>\n" +
            "</T:customprop>";

    private static final String PROPERTY2 =
            "<?xml version=\"1.0\" encoding=\"utf-8\" ?>\n" +
            "<V:someprop xmlns:V=\"http://tomcat.apache.org/other\">\n" +
            "  <V:myvalue>bla</V:myvalue>\n" +
            "</V:someprop>";

    private static final String PROPERTY3 =
            "<?xml version=\"1.0\" encoding=\"utf-8\" ?>\n" +
            "<V:someprop xmlns:V=\"http://tomcat.apache.org/other\">\n" +
            "  <V:othervalue>foooooooo</V:othervalue>\n" +
            "</V:someprop>";

    public static final String SIMPLE_SCHEMA =
            "CREATE TABLE webdavproperties (\n" +
            "  path         VARCHAR(1024) NOT NULL,\n" +
            "  namespace    VARCHAR(64) NOT NULL,\n" +
            "  name         VARCHAR(64) NOT NULL,\n" +
            "  node         VARCHAR(2048) NOT NULL,\n" +
            "  PRIMARY KEY (path, namespace, name)\n" +
            ")";

    public static class CustomDataSourcePropertyStore extends DataSourcePropertyStore {
        public void setDataSource(DataSource dataSource) {
            this.dataSource = dataSource;
        }
    }

    private class DerbyDataSource implements DataSource {

        Connection connection = null;

        DerbyDataSource() {
            try {
                Class.forName("org.apache.derby.jdbc.EmbeddedDriver");
                connection = DriverManager.getConnection("jdbc:derby:" + getTemporaryDirectory().getAbsolutePath()
                        + "/webdavproperties;create=true");
                try (Statement statement = connection.createStatement()) {
                    statement.execute(SIMPLE_SCHEMA);
                }
            } catch (Exception e) {
                throw new IllegalStateException(e);
            }
        }

        @Override
        public Logger getParentLogger() throws SQLFeatureNotSupportedException {
            return null;
        }

        @Override
        public <T> T unwrap(Class<T> iface) throws SQLException {
            return null;
        }

        @Override
        public boolean isWrapperFor(Class<?> iface) throws SQLException {
            return false;
        }

        @Override
        public Connection getConnection() throws SQLException {
            if (connection.isClosed()) {
                connection = DriverManager.getConnection("jdbc:derby:" + getTemporaryDirectory().getAbsolutePath()
                        + "/webdavproperties");
            }
            return connection;
        }

        @Override
        public Connection getConnection(String username, String password) throws SQLException {
            return getConnection();
        }

        @Override
        public PrintWriter getLogWriter() throws SQLException {
            return null;
        }

        @Override
        public void setLogWriter(PrintWriter out) throws SQLException {
        }

        @Override
        public void setLoginTimeout(int seconds) throws SQLException {
        }

        @Override
        public int getLoginTimeout() throws SQLException {
            return 0;
        }

    }

    @Parameterized.Parameters(name = "{0}")
    public static Collection<Object[]> parameters() {
        List<Object[]> parameterSets = new ArrayList<>();
        parameterSets.add(new Object[] { "org.apache.catalina.servlets.WebdavServlet$MemoryPropertyStore" });
        parameterSets.add(new Object[] { "org.apache.catalina.servlets.TestWebdavPropertyStore$CustomDataSourcePropertyStore" });
        return parameterSets;
    }

    @Parameter(0)
    public String storeName;

    @Test
    public void testStore() throws Exception {
        if (storeName.contains("DataSource")) {
            Assume.assumeTrue(JreCompat.isJre16Available());
        }

        DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance();
        documentBuilderFactory.setNamespaceAware(true);
        documentBuilderFactory.setExpandEntityReferences(false);
        DocumentBuilder documentBuilder = documentBuilderFactory.newDocumentBuilder();
        Document document1 = documentBuilder.parse(new InputSource(new ByteArrayInputStream(PROPERTY1.getBytes(StandardCharsets.UTF_8))));
        Node node1 = document1.getDocumentElement();
        Document document2 = documentBuilder.parse(new InputSource(new ByteArrayInputStream(PROPERTY2.getBytes(StandardCharsets.UTF_8))));
        Node node2 = document2.getDocumentElement();
        Document document3 = documentBuilder.parse(new InputSource(new ByteArrayInputStream(PROPERTY3.getBytes(StandardCharsets.UTF_8))));
        Node node3 = document3.getDocumentElement();

        PropertyStore propertyStore = (PropertyStore) Class.forName(storeName).getDeclaredConstructor().newInstance();
        if (propertyStore instanceof CustomDataSourcePropertyStore) {
            ((CustomDataSourcePropertyStore) propertyStore).setDataSource(new DerbyDataSource());
            ((CustomDataSourcePropertyStore) propertyStore).setTableName("webdavproperties");
        }
        propertyStore.init();

        // Add properties
        ArrayList<ProppatchOperation> operations = new ArrayList<>();
        operations.add(new ProppatchOperation(PropertyUpdateType.SET, node1));
        operations.add(new ProppatchOperation(PropertyUpdateType.SET, node2));
        propertyStore.proppatch("/some/path1", operations);

        // Add properties
        operations = new ArrayList<>();
        operations.add(new ProppatchOperation(PropertyUpdateType.SET, node1));
        propertyStore.proppatch("/other/path2", operations);

        // Get single property
        XMLWriter xmlWriter1 = new XMLWriter();
        Assert.assertTrue(propertyStore.propfind("/some/path1", node1, false, xmlWriter1));
        Assert.assertTrue(xmlWriter1.toString().contains("<myvalue "));

        // Get property names
        XMLWriter xmlWriter2 = new XMLWriter();
        Assert.assertFalse(propertyStore.propfind("/some/path1", null, true, xmlWriter2));
        Assert.assertTrue(xmlWriter2.toString().contains("<someprop"));

        // Get all properties
        XMLWriter xmlWriter3 = new XMLWriter();
        Assert.assertFalse(propertyStore.propfind("/some/path1", null, false, xmlWriter3));
        Assert.assertTrue(xmlWriter3.toString().contains(">bla</myvalue>"));

        propertyStore.copy("/some/path1", "/some/path2");
        XMLWriter xmlWriter4 = new XMLWriter();
        Assert.assertFalse(propertyStore.propfind("/some/path2", null, true, xmlWriter4));
        Assert.assertTrue(xmlWriter4.toString().contains("<someprop"));

        propertyStore.delete("/some/path1");
        XMLWriter xmlWriter5 = new XMLWriter();
        Assert.assertFalse(propertyStore.propfind("/some/path1", null, true, xmlWriter5));
        Assert.assertTrue(xmlWriter5.toString().isEmpty());

        propertyStore.copy("/some/path2", "/other/path2");
        XMLWriter xmlWriter6 = new XMLWriter();
        Assert.assertFalse(propertyStore.propfind("/other/path2", null, true, xmlWriter6));
        Assert.assertTrue(xmlWriter6.toString().contains("<customprop"));

        operations = new ArrayList<>();
        operations.add(new ProppatchOperation(PropertyUpdateType.REMOVE, node1));
        propertyStore.proppatch("/other/path2", operations);

        XMLWriter xmlWriter7 = new XMLWriter();
        Assert.assertFalse(propertyStore.propfind("/other/path2", null, false, xmlWriter7));
        Assert.assertFalse(xmlWriter7.toString().contains("<customprop"));
        Assert.assertTrue(xmlWriter7.toString().contains(">bla</myvalue>"));

        operations = new ArrayList<>();
        operations.add(new ProppatchOperation(PropertyUpdateType.SET, node3));
        propertyStore.proppatch("/other/path2", operations);

        XMLWriter xmlWriter8 = new XMLWriter();
        Assert.assertFalse(propertyStore.propfind("/other/path2", null, false, xmlWriter8));
        Assert.assertFalse(xmlWriter8.toString().contains("<customprop"));
        Assert.assertTrue(xmlWriter8.toString().contains(">foooooooo</othervalue>"));

        XMLWriter xmlWriter9 = new XMLWriter();
        Assert.assertFalse(propertyStore.propfind("/other/path2", node1, false, xmlWriter9));
        Assert.assertTrue(xmlWriter9.toString().isEmpty());

        propertyStore.destroy();

    }
}
