/**
 * 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
 * <p>
 * http://www.apache.org/licenses/LICENSE-2.0
 * <p>
 * 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;

import org.apache.commons.io.FileUtils;
import org.apache.http.HttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.conn.ssl.TrustAllStrategy;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.ssl.SSLContexts;
import org.jboss.installer.auto.InstallationDataSerializer;
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.secdom.CertSecurity;
import org.jboss.installer.test.utils.CertificateUtils;
import org.jboss.installer.test.utils.MockLanguageUtils;
import org.jboss.installer.test.utils.ServerUtils;
import org.jboss.installer.test.utils.TestServer;
import org.jboss.shrinkwrap.api.ShrinkWrap;
import org.jboss.shrinkwrap.api.asset.StringAsset;
import org.jboss.shrinkwrap.api.spec.WebArchive;
import org.junit.After;
import org.junit.Before;
import org.junit.ClassRule;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
import org.w3c.dom.Document;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.wildfly.security.x500.cert.SelfSignedX509CertificateAndSigningKey;

import javax.net.ssl.SSLContext;
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.io.File;
import java.io.FileInputStream;
import java.nio.file.Path;
import java.security.KeyStore;
import java.util.Optional;

import static org.jboss.installer.postinstall.task.secdom.CertSecurity.CertificateConfig.TRUST_STORE_PASSWORD_KEY;
import static org.jboss.installer.test.utils.TestServer.TARGET_PATH;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;

public class AddCertSecurityDomainTest {

    public static final String A_PASSWORD = "secret";
    private StandaloneServer standaloneServer;
    private DomainServer domainServer;
    @ClassRule
    public static TestServer testServer = new TestServer();
    @Rule
    public TemporaryFolder tempFolder = new TemporaryFolder();
    private SecurityDomainTask task;
    private InstallationData idata;
    private DocumentBuilder builder;
    private XPath xPath =  XPathFactory.newInstance().newXPath();

    private File keyStore;
    private File trustStore;
    private TaskPrinter printer = new NoopPrinter();

    @Before
    public void setup() throws Exception {
        final DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
        builder = factory.newDocumentBuilder();

        task = new SecurityDomainTask();
        idata = new InstallationData();
        standaloneServer = new StandaloneServer(TARGET_PATH);
        domainServer = new DomainServer(TARGET_PATH);
        idata.setTargetFolder(TARGET_PATH);

        keyStore = tempFolder.newFile("test-key-store.jks");
        trustStore = tempFolder.newFile("test-trust-store.jks");

        final SelfSignedX509CertificateAndSigningKey selfSignedCertificateAndSigningKey = CertificateUtils.generateSelfSignedCertificate();
        CertificateUtils.generateKeyStore(selfSignedCertificateAndSigningKey, keyStore, "jks");
        CertificateUtils.generateTrustStore(selfSignedCertificateAndSigningKey.getSelfSignedCertificate(), trustStore, "jks");
    }

    @After
    public void tearDown() throws Exception {
        standaloneServer.close();
        domainServer.close();

        testServer.restoreConfigs();
    }

    @Test
    public void testInstallCertSecurityDomainInStandalone() throws Exception {
        defaultConfig();

        standaloneServer.start("standalone.xml");
        try {
            assertTrue(task.applyToStandalone(idata, standaloneServer, printer));
        } finally {
            standaloneServer.shutdown();
        }

        ServerUtils.testOnDeployment(standaloneServer, createTestArchive(), ()-> assertConnection(keyStore.getAbsolutePath()));
        verifyConfig("standalone", "standalone.xml");
    }

    @Test
    public void testInstallCertSecurityWithAttrNameDomainInStandalone() throws Exception {
        CertSecurity.CertificateConfig cfg = defaultConfig();
        cfg.setUseOid(false);
        cfg.setFilterExpression("CN");

        standaloneServer.start("standalone.xml");
        try {
            assertTrue(task.applyToStandalone(idata, standaloneServer, printer));
        } finally {
            standaloneServer.shutdown();
        }

        ServerUtils.testOnDeployment(standaloneServer, createTestArchive(), ()-> assertConnection(keyStore.getAbsolutePath()));
    }

    @Test
    public void testInstallCertSecurityDomainWithCredStoreInStandalone() throws Exception {
        defaultConfig();
        final CredentialStoreInstallTask.Config config = new CredentialStoreInstallTask.Config("test-store", "jboss.server.config.dir", "test", "test");
        idata.putConfig(config);

        standaloneServer.start("standalone.xml");
        try {
            assertTrue(new CredentialStoreInstallTask().applyToStandalone(idata, standaloneServer, printer));
            assertTrue(task.applyToStandalone(idata, standaloneServer, printer));
        } finally {
            standaloneServer.shutdown();
        }

        ServerUtils.testOnDeployment(standaloneServer, createTestArchive(), ()-> assertConnection(keyStore.getAbsolutePath()));
    }

    @Test
    public void testInstallCertSecurityDomainInDomain() throws Exception {
        defaultConfig();

        domainServer.start("host.xml");
        try {
            assertTrue(task.applyToDomain(idata, domainServer, printer));
        } finally {
            domainServer.shutdown();
        }
        verifyConfig("domain", "domain.xml");

        domainServer.start("host-primary.xml");
        try {
            assertTrue(task.applyToDomain(idata, domainServer, printer));
        } finally {
            domainServer.shutdown();
        }

        domainServer.start("host-secondary.xml");
        try {
            assertTrue(task.applyToDomain(idata, domainServer, printer));
        } finally {
            domainServer.shutdown();
        }

        // TODO: verify with deployment... - need to change how the archive is deployed in domain
//        ServerUtils.testOnDeployment(domainServer, createTestArchive(), ()-> assertConnection(keyStore.getAbsolutePath()));
    }

    @Test
    public void serializationTest() throws Exception {
        final CertSecurity.CertificateConfig certificateConfig = defaultConfig();
        final InstallationDataSerializer serializer = new InstallationDataSerializer(new MockLanguageUtils());
        final Path outputFile = tempFolder.newFile().toPath();

        serializer.serialize(idata, outputFile);
        final File variableFile = outputFile.getParent().resolve(outputFile.getFileName().toString() + ".variables").toFile();
        FileUtils.writeStringToFile(variableFile, TRUST_STORE_PASSWORD_KEY + "=" + A_PASSWORD, "UTF-8");
        final SecurityDomainTask.Config deserializedConfig = serializer.deserialize(outputFile, Optional.empty()).getConfig(SecurityDomainTask.Config.class);

        assertEquals(certificateConfig, deserializedConfig.getCertConfig());
    }

    private CertSecurity.CertificateConfig defaultConfig() {
        final SecurityDomainTask.Config config = new SecurityDomainTask.Config();
        idata.putConfig(config);
        config.setDomainName("TestSD");
        final CertSecurity.CertificateConfig certConfig = new CertSecurity.CertificateConfig();
        config.setCertConfig(certConfig);

        certConfig.setApplicationDomainName("TestSD");
        certConfig.setFilterExpression("2.5.4.3");
        certConfig.setMaximumSegments(1);
        certConfig.setStartSegments(0);
        certConfig.setRoles(new String[]{"Admin", "Guest"});
        certConfig.setTrustStorePath(trustStore.toPath());
        certConfig.setTrustStorePassword(A_PASSWORD);
        certConfig.setUseOid(true);
        return certConfig;
    }

    private void verifyConfig(String mode, String config) throws Exception {
        final Document doc = builder.parse(TARGET_PATH.resolve(mode).resolve("configuration").resolve(config).toFile());

        assertEquals("TestSD", getAttribute(doc, "//http-authentication-factory[@name=\"TestSD-certHttpAuth\"]", "security-domain"));
        assertEquals("CLIENT_CERT", getAttribute(doc, "//http-authentication-factory[@name=\"TestSD-certHttpAuth\"]/mechanism-configuration/mechanism", "mechanism-name"));

        assertEquals("TestSD-certHttpAuth", getAttribute(doc, "//application-security-domain[@name=\"TestSD\"]", "http-authentication-factory"));
    }

    private String getAttribute(Document doc, String expression, String attrName) throws XPathExpressionException {
        NodeList nodes = (NodeList) xPath.compile(expression).evaluate(
                doc, XPathConstants.NODESET);
        final Node attr = nodes.item(0).getAttributes().getNamedItem(attrName);
        return attr == null?null:attr.getNodeValue();
    }

    private void assertConnection(String keyStorePath) throws Exception {

        KeyStore keyStore = KeyStore.getInstance("JKS");
        keyStore.load(new FileInputStream(keyStorePath), A_PASSWORD.toCharArray());


        SSLContext sslContext = SSLContexts.custom()
                .loadKeyMaterial(keyStore, A_PASSWORD.toCharArray())
                .loadTrustMaterial(TrustAllStrategy.INSTANCE)
                .build();

        try (CloseableHttpClient httpClient = HttpClients.custom()
                .setSSLContext(sslContext)
                .build()) {
        HttpResponse response = httpClient.execute(new HttpGet("https://localhost:8443/test/secure/index.html"));
        assertEquals(200, response.getStatusLine().getStatusCode());
        }
    }

    private WebArchive createTestArchive() throws Exception {
        final WebArchive webArchive = ShrinkWrap.create(WebArchive.class, "test.war");
        webArchive.addAsWebInfResource(new StringAsset(
                "<jboss-web>\n" +
                "  <security-domain>exampleApplicationDomain</security-domain>\n" +
                "</jboss-web>"), "jboss-web.xml");

        webArchive.addAsWebInfResource(new StringAsset(
                "<web-app>\n" +
                        "  <security-constraint>\n" +
                        "    <web-resource-collection>\n" +
                        "      <web-resource-name>secure</web-resource-name>\n" +
                        "      <url-pattern>/secure/*</url-pattern>\n" +
                        "    </web-resource-collection>\n" +
                        "    <auth-constraint>\n" +
                        "      <role-name>Admin</role-name>\n" +
                        "    </auth-constraint>\n" +
                        "  </security-constraint>\n" +
                        "  <security-role>\n" +
                        "    <role-name>Admin</role-name>\n" +
                        "  </security-role>\n" +
                        "  <login-config>\n" +
                        "    <auth-method>CLIENT-CERT</auth-method>\n" +
                        "    <realm-name>exampleCertSD</realm-name>\n" +
                        "  </login-config>\n" +
                        "</web-app>"), "web.xml");
        webArchive.addAsWebResource(new StringAsset("HELLO"), "index.html");
        webArchive.addAsWebResource(new StringAsset("HELLO"), "secure/index.html");

        return webArchive;
    }
}
