/*
 * JBoss, Home of Professional Open Source.
 * Copyright 2021 Red Hat, Inc., and individual contributors
 * as indicated by the @author tags.
 *
 * Licensed 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.jboss.installer.postinstall.task.impl;

import org.jboss.installer.core.DatabaseDriver;
import org.jboss.installer.core.InstallationData;
import org.jboss.installer.postinstall.TaskPrinter;
import org.jboss.installer.postinstall.server.DomainServer;
import org.jboss.installer.postinstall.server.StandaloneServer;
import org.jboss.installer.postinstall.task.JDBCDriverConfig;
import org.jboss.installer.postinstall.task.CredentialStoreConfig;
import org.jboss.installer.postinstall.task.DatasourceConfig;
import org.jboss.installer.postinstall.task.NoopPrinter;
import org.jboss.installer.test.utils.TestServer;
import org.junit.Assert;
import org.junit.Before;
import org.junit.ClassRule;
import org.junit.Test;
import org.w3c.dom.Document;
import org.w3c.dom.NodeList;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.xpath.XPath;
import javax.xml.xpath.XPathConstants;
import javax.xml.xpath.XPathExpressionException;
import javax.xml.xpath.XPathFactory;

import java.util.Arrays;
import java.util.Collections;
import java.util.List;

import static org.jboss.installer.postinstall.task.impl.DatasourceTask.DATASOURCE_PASSWORD_ALIAS;
import static org.jboss.installer.postinstall.task.impl.DatasourceTask.XA_RECOVERY_PASSWORD_ALIAS;
import static org.junit.Assert.assertTrue;

public class DatasourceTaskTest {

    public static final String XA_TYPE = "xa-";
    public static final String NON_XA_TYPE = "";
    public static final String IBM_JNDI_NAME = "java:/DB2DS";
    public static final String MIN_POOL = "0";
    public static final String MAX_POOL = "20";
    public static final String SECURITY_DOMAIN_NAME = "securityDomainName";
    public static final String IBM_CONNECTION_URL = "jdbc:db2://SERVER_NAME:PORT/DATABASE_NAME";
    public static final String DS_USERNAME = "user";
    public static final String DS_PASSWORD = "password";
    public static final String XA_PASSWORD = "xa-password";
    public static final String XA_USER = "xa-user";
    public static final String TEST_STORE = "test-store";
    public static final boolean WITH_CRED_STORE = true;
    public static final boolean WITHOUT_CRED_STORE = false;

    @ClassRule
    public static TestServer testServer = new TestServer();
    private StandaloneServer standaloneServer;
    private DomainServer domainServer;
    private InstallationData iData;
    private XPath xPath =  XPathFactory.newInstance().newXPath();
    private DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
    private DocumentBuilder builder;

    private final CredentialStoreConfig credStoreConfig = new CredentialStoreConfig(TEST_STORE, null, TestServer.TARGET_PATH.resolve("test.store").toString(), "abcd1234");
    private final JDBCDriverConfig jdbcConfig = new JDBCDriverConfig(DatabaseDriver.IBM_DB2, Collections.emptyList(), Collections.emptyList());

    private TaskPrinter printer = new NoopPrinter();

    @Before
    public void setUp() throws Exception {
        standaloneServer = new StandaloneServer(TestServer.TARGET_PATH);
        domainServer = new DomainServer(TestServer.TARGET_PATH);
        iData = new InstallationData();
        iData.setTargetFolder(TestServer.TARGET_PATH);
        builder = factory.newDocumentBuilder();
    }

    @Test
    public void dataSourceAddedWithSecurityDomainInDomainModeTest() throws Exception{
        DatasourceConfig dsConfig = new DatasourceConfig("ds1", IBM_JNDI_NAME, MIN_POOL, MAX_POOL);
        dsConfig.setSecurityDomain(SECURITY_DOMAIN_NAME);
        dsConfig.setConnectionUrl(IBM_CONNECTION_URL);

        iData.putConfig(jdbcConfig);
        iData.putConfig(dsConfig);
        domainServer.start("host.xml");

        try {
            Assert.assertTrue(new DatasourceTask().applyToDomain(iData, domainServer, printer));
            Document doc = builder.parse(TestServer.TARGET_PATH.resolve("domain").resolve("configuration").resolve("domain.xml").toFile());
            for (String profile : DomainServer.PROFILES) {
                String prefix = String.format("//profile[@name=\"%s\"]/subsystem/datasources/", profile);
                assertDatasourceConfiguration(doc, dsConfig, prefix, NON_XA_TYPE, WITHOUT_CRED_STORE);
            }
        } finally {
            domainServer.shutdown();
        }
    }

    @Test
    public void dataSourceAddedWithUserNameInStandaloneModeTest() throws Exception{
        DatasourceConfig dsConfig = new DatasourceConfig("ds2", IBM_JNDI_NAME, MIN_POOL, MAX_POOL);
        dsConfig.setDsUsername(DS_USERNAME);
        dsConfig.setDsPassword(DS_PASSWORD);
        dsConfig.setConnectionUrl(IBM_CONNECTION_URL);

        iData.putConfig(jdbcConfig);
        iData.putConfig(dsConfig);
        String serverConfig = "standalone.xml";
        standaloneServer.start(serverConfig);

        try {
            Assert.assertTrue(new DatasourceTask().applyToStandalone(iData, standaloneServer, printer));
            Document doc = builder.parse(TestServer.TARGET_PATH.resolve("standalone").resolve("configuration").resolve(serverConfig).toFile());
            String prefix = "/" ;
            assertDatasourceConfiguration(doc, dsConfig, prefix, NON_XA_TYPE, WITHOUT_CRED_STORE);

        } finally {
            standaloneServer.shutdown();
        }
    }

    @Test
    public void xaDataSourceAddedWithUserNameInDomainModeTest() throws Exception{
        DatasourceConfig xaDsConfig = new DatasourceConfig("ds3", IBM_JNDI_NAME, MIN_POOL, MAX_POOL);
        xaDsConfig.setDsUsername(DS_USERNAME);
        xaDsConfig.setDsPassword(DS_PASSWORD);
        xaDsConfig.setXaUsername(XA_USER);
        xaDsConfig.setXaPassword(XA_PASSWORD);
        xaDsConfig.setXaProperties(Arrays.asList(
                new DatasourceConfig.XaProperty("key", "value"),
                new DatasourceConfig.XaProperty("key2", "value2")
        ));

        iData.putConfig(jdbcConfig);
        iData.putConfig(xaDsConfig);
        domainServer.start("host.xml");

        try {
            Assert.assertTrue(new DatasourceTask().applyToDomain(iData, domainServer, printer));
            Document doc = builder.parse(TestServer.TARGET_PATH.resolve("domain").resolve("configuration").resolve("domain.xml").toFile());
            for (String profile : DomainServer.PROFILES) {
                String prefix = String.format("//profile[@name=\"%s\"]/subsystem/datasources/", profile);
                assertDatasourceConfiguration(doc, xaDsConfig, prefix, XA_TYPE, WITHOUT_CRED_STORE);
            }
        } finally {
            domainServer.shutdown();
        }
    }

    @Test
    public void xaDataSourceWithCredentialStoreTest() throws Exception{
        DatasourceConfig xaDsConfig = new DatasourceConfig("ds3", IBM_JNDI_NAME, MIN_POOL, MAX_POOL);
        xaDsConfig.setDsUsername(DS_USERNAME);
        xaDsConfig.setDsPassword(DS_PASSWORD);
        xaDsConfig.setXaUsername(XA_USER);
        xaDsConfig.setXaPassword(XA_PASSWORD);
        xaDsConfig.setXaProperties(Arrays.asList(
                new DatasourceConfig.XaProperty("key", "value"),
                new DatasourceConfig.XaProperty("key2", "value2")
        ));

        iData.putConfig(jdbcConfig);
        iData.putConfig(credStoreConfig);
        iData.putConfig(xaDsConfig);
        String serverConfig = "standalone.xml";
        standaloneServer.start(serverConfig);

        try {
            assertTrue(new CredentialStoreInstallTask().applyToStandalone(iData, standaloneServer, printer));
            Assert.assertTrue(new DatasourceTask().applyToStandalone(iData, standaloneServer, printer));
            Document doc = builder.parse(TestServer.TARGET_PATH.resolve("standalone").resolve("configuration").resolve(serverConfig).toFile());
            String prefix = "/";
            assertDatasourceConfiguration(doc, xaDsConfig, prefix, XA_TYPE, WITH_CRED_STORE);

        } finally {
            standaloneServer.shutdown();
        }
    }

    private boolean containsTestValue(Document doc, String expression, String testValue) throws XPathExpressionException {
        NodeList datasource = (NodeList) xPath.compile(expression).evaluate(
                doc, XPathConstants.NODESET);
        for (int i = 0; i < datasource.getLength(); i++) {
            if (datasource.item(i).getTextContent().equals(testValue)) {
                return true;
            }
        }
        return false;
    }

    private boolean containsNamedAttribute(Document doc, String expression, String attribute, String testedAttribute) throws XPathExpressionException {
        NodeList datasource = (NodeList) xPath.compile(expression).evaluate(
                doc, XPathConstants.NODESET);
        for (int i = 0; i < datasource.getLength(); i++) {
            if (datasource.item(i).getAttributes().getNamedItem(attribute).getNodeValue().equals(testedAttribute)) {
                return true;
            }
        }
        return false;
    }

    private boolean containsXaProperty(Document doc, String expression, DatasourceConfig.XaProperty property) throws XPathExpressionException {
        NodeList datasource = (NodeList) xPath.compile(expression).evaluate(
                doc, XPathConstants.NODESET);
        for (int i = 0; i < datasource.getLength(); i++) {
            if (datasource.item(i).getAttributes().getNamedItem("name").getNodeValue().equals(property.getKey())
                    && datasource.item(i).getTextContent().contains(property.getValue())) {
                return true;
            }
        }
        return false;
    }

    private void assertXaProperties(Document doc, String prefix, List<DatasourceConfig.XaProperty> properties) throws XPathExpressionException {
        for (DatasourceConfig.XaProperty property : properties) {
            Assert.assertTrue(containsXaProperty(doc, prefix + "/xa-datasource/xa-datasource-property", property));
        }
    }

    private void assertDatasourceConfiguration(Document doc, DatasourceConfig dsConfig, String prefix, String dsType, boolean credStore) throws XPathExpressionException {
        Assert.assertTrue(containsNamedAttribute(doc, prefix + "/" + dsType + "datasource", "pool-name", dsConfig.getDsName()));
        Assert.assertTrue(containsNamedAttribute(doc, prefix + "/" + dsType + "datasource", "jndi-name", dsConfig.getJndiName()));
        Assert.assertTrue(containsTestValue(doc, prefix + "/" + dsType + "datasource/" + dsType + "pool/min-pool-size", dsConfig.getMinPool()));
        Assert.assertTrue(containsTestValue(doc, prefix + "/" + dsType + "datasource/" + dsType + "pool/max-pool-size", dsConfig.getMaxPool()));

        if (dsConfig.getSecurityDomain() != null) {
            Assert.assertTrue(containsTestValue(doc, prefix + "/" + dsType + "datasource/security/security-domain", dsConfig.getSecurityDomain()));
        } else {
            Assert.assertTrue(containsNamedAttribute(doc, prefix + "/" + dsType + "datasource/security", "user-name", dsConfig.getDsUsername()));
            if (credStore) {
                Assert.assertTrue(containsNamedAttribute(doc, prefix + "/" + dsType + "datasource/security/credential-reference", "store", TEST_STORE));
                Assert.assertTrue(containsNamedAttribute(doc, prefix + "/" + dsType + "datasource/security/credential-reference", "alias", DATASOURCE_PASSWORD_ALIAS));
            } else {
                Assert.assertTrue(containsNamedAttribute(doc, prefix + "/" + dsType + "datasource/security/credential-reference", "clear-text", dsConfig.getDsPassword()));
            }
        }
        if (dsConfig.getConnectionUrl() != null) {
            Assert.assertTrue(containsTestValue(doc, prefix + "/datasource/connection-url", dsConfig.getConnectionUrl()));
        } else {
            assertXaProperties(doc, prefix, dsConfig.getXaProperties());
            Assert.assertTrue(containsNamedAttribute(doc, prefix + "/xa-datasource/recovery/recover-credential", "user-name", dsConfig.getXaUsername()));
            if (credStore) {
                Assert.assertTrue(containsNamedAttribute(doc, prefix + "/" + dsType + "datasource/recovery/recover-credential/credential-reference", "store", TEST_STORE));
                Assert.assertTrue(containsNamedAttribute(doc, prefix + "/" + dsType + "datasource/recovery/recover-credential/credential-reference", "alias", XA_RECOVERY_PASSWORD_ALIAS));
            } else {
                Assert.assertTrue(containsNamedAttribute(doc, prefix + "/" + dsType + "datasource/recovery/recover-credential/credential-reference", "clear-text", dsConfig.getXaPassword()));
            }
        }
    }
}
