/*
 * Copyright The WildFly Authors
 * SPDX-License-Identifier: Apache-2.0
 */

package org.jboss.as.test.integration.security.credentialreference;

import static org.jboss.as.controller.security.CredentialReference.CREDENTIAL_REFERENCE;
import static org.junit.Assert.fail;

import java.io.IOException;
import java.sql.Connection;
import java.sql.DriverManager;
import java.util.Objects;

import org.h2.tools.Server;
import org.jboss.arquillian.container.test.api.Deployment;
import org.jboss.arquillian.container.test.api.RunAsClient;
import org.jboss.arquillian.junit.Arquillian;
import org.jboss.arquillian.test.api.ArquillianResource;
import org.jboss.as.arquillian.api.ServerSetup;
import org.jboss.as.arquillian.api.ServerSetupTask;
import org.jboss.as.arquillian.container.ManagementClient;
import org.jboss.as.controller.PathAddress;
import org.jboss.as.controller.client.helpers.Operations;
import org.jboss.as.controller.descriptions.ModelDescriptionConstants;
import org.jboss.as.test.integration.management.util.MgmtOperationException;
import org.jboss.as.test.integration.security.common.Utils;
import org.jboss.dmr.ModelNode;
import org.jboss.shrinkwrap.api.ShrinkWrap;
import org.jboss.shrinkwrap.api.asset.StringAsset;
import org.jboss.shrinkwrap.api.spec.WebArchive;
import org.junit.Test;
import org.junit.runner.RunWith;

/**
 * tests for credential-reference in datasource subsystem
 */
@RunWith(Arquillian.class)
@ServerSetup({CredentialStoreServerSetupTask.class, CredentialReferenceDatasourceTestCase.DatasourceServerSetupTask.class})
@RunAsClient
public class CredentialReferenceDatasourceTestCase {
    private static final String DATABASE_PASSWORD = "chucknorris";
    private static final PathAddress DATASOURCES_SUBSYSTEM_ADDRESS = PathAddress.pathAddress(ModelDescriptionConstants.SUBSYSTEM, "datasources");
    private static final String CLEAR_TEXT_CREDENTIAL_REF_DS_NAME = "ClearTextCredentialReferenceDatasource";
    private static final String STORE_ALIAS_CREDENTIAL_REF_DS_NAME = "StoreAliasCredentialReferenceDatasource";
    private static final String PASSWORD_AND_CREDENTIAL_REF_DS_NAME = "PasswordAndClearTextCredentialReferenceDatasource";

    enum Scenario {
        // <credential-reference clear-text="chucknorris"/>
        CREDENTIAL_REFERENCE_CLEAR_TEXT(CLEAR_TEXT_CREDENTIAL_REF_DS_NAME),

        // <credential-reference store="store001" alias="alias001"/>
        CREDENTIAL_REFERENCE_STORE_ALIAS(STORE_ALIAS_CREDENTIAL_REF_DS_NAME),

        // password and credential-reference should be mutually exclusive
        // test that it is not possible to create datasource with both defined
        PASSWORD_AND_CREDENTIAL_REFERENCE_PREFERENCE(PASSWORD_AND_CREDENTIAL_REF_DS_NAME);

        private final String datasourceName;

        Scenario(String datasourceName) {
            Objects.requireNonNull(datasourceName);
            this.datasourceName = datasourceName;
        }

        private PathAddress getDatasourceAddress() {
            return DATASOURCES_SUBSYSTEM_ADDRESS
                    .append("data-source", datasourceName);
        }

        private PathAddress getXADatasourceAddress() {
            return DATASOURCES_SUBSYSTEM_ADDRESS
                    .append("xa-data-source", datasourceName);
        }

        public String getDatasourceJndiName() {
            return "java:jboss/datasources/" + datasourceName;
        }
    }

    @ArquillianResource
    private ManagementClient client;


    @Deployment
    public static WebArchive deployment() {
        final WebArchive war = ShrinkWrap.create(WebArchive.class, "test.war");
        war.addAsManifestResource(new StringAsset("Dependencies: org.jboss.dmr, org.jboss.as.controller-client, org.jboss.as.controller\n"), "MANIFEST.MF");
        war.addClass(CredentialStoreServerSetupTask.class);
        war.addClass(CredentialReferenceDatasourceTestCase.class);
        return war;
    }

    @Test
    public void testDatasourceClearTextCredentialReference() throws IOException, MgmtOperationException {
        final Scenario scenario = Scenario.CREDENTIAL_REFERENCE_CLEAR_TEXT;
        addDatasource(scenario);
        try {
            testConnectionInPool(scenario.getDatasourceAddress());
        } finally {
            removeDatasourceSilently(scenario);
        }
    }

    @Test
    public void testDatasourceStoreAliasCredentialReference() throws IOException, MgmtOperationException {
        final Scenario scenario = Scenario.CREDENTIAL_REFERENCE_STORE_ALIAS;
        addDatasource(scenario);
        try {
            testConnectionInPool(scenario.getDatasourceAddress());
        } finally {
            removeDatasourceSilently(scenario);
        }
    }

    @Test
    public void testDatasourceCredentialReferenceOverPasswordPreference() throws IOException {
        final Scenario scenario = Scenario.PASSWORD_AND_CREDENTIAL_REFERENCE_PREFERENCE;
        try {
            addDatasource(scenario);
            fail("It shouldn't be possible to add datasource with both, credential-reference and password, defined");
        } catch (MgmtOperationException moe) {
            // expected
        } finally {
            removeDatasourceSilently(scenario);
        }
    }

    @Test
    public void testXADatasourceClearTextCredentialReference() throws IOException, MgmtOperationException {
        final Scenario scenario = Scenario.CREDENTIAL_REFERENCE_CLEAR_TEXT;
        addXADatasource(scenario);
        try {
            testConnectionInPool(scenario.getXADatasourceAddress());
        } finally {
            removeXADatasourceSilently(scenario);
        }
    }

    @Test
    public void testXADatasourceStoreAliasCredentialReference() throws IOException, MgmtOperationException {
        final Scenario scenario = Scenario.CREDENTIAL_REFERENCE_STORE_ALIAS;
        addXADatasource(scenario);
        try {
            testConnectionInPool(scenario.getXADatasourceAddress());
        } finally {
            removeXADatasourceSilently(scenario);
        }
    }

    @Test
    public void testXADatasourceCredentialReferenceOverPasswordPreference() throws IOException {
        final Scenario scenario = Scenario.PASSWORD_AND_CREDENTIAL_REFERENCE_PREFERENCE;
        try {
            addXADatasource(scenario);
            fail("It shouldn't be possible to add datasource with both, credential-reference and password, defined");
        } catch (MgmtOperationException moe) {
            // expected
        } finally {
            removeXADatasourceSilently(scenario);
        }
    }

    private void testConnectionInPool(PathAddress datasourceAddress) throws IOException, MgmtOperationException {
        final ModelNode operation = new ModelNode();
        operation.get(ModelDescriptionConstants.OP).set("test-connection-in-pool");
        operation.get(ModelDescriptionConstants.OP_ADDR).set(datasourceAddress.toModelNode());
        execute(operation);
    }

    private void addDatasource(final Scenario scenario) throws IOException, MgmtOperationException {
        final ModelNode addOperation = Operations.createAddOperation(scenario.getDatasourceAddress().toModelNode());
        addOperation.get("jndi-name").set(scenario.getDatasourceJndiName());
        addOperation.get("driver-name").set("h2");
        addOperation.get("user-name").set("sa");
        addOperation.get("connection-url").set("jdbc:h2:tcp://" + Utils.getSecondaryTestAddress(client) + "/mem:" + CredentialReferenceDatasourceTestCase.class.getName());
        addOperation.get(ModelDescriptionConstants.OPERATION_HEADERS).get("allow-resource-service-restart").set(true);

        ModelNode credentialReference = null;
        switch (scenario) {
            case PASSWORD_AND_CREDENTIAL_REFERENCE_PREFERENCE:
                addOperation.get("password").set("wrong-password");
                credentialReference = addOperation.get(CREDENTIAL_REFERENCE).setEmptyObject();
                credentialReference.get("clear-text").set(DATABASE_PASSWORD);
                break;
            case CREDENTIAL_REFERENCE_CLEAR_TEXT:
                credentialReference = addOperation.get(CREDENTIAL_REFERENCE).setEmptyObject();
                credentialReference.get("clear-text").set(DATABASE_PASSWORD);
                break;
            case CREDENTIAL_REFERENCE_STORE_ALIAS:
                credentialReference = addOperation.get(CREDENTIAL_REFERENCE).setEmptyObject();
                credentialReference.get("store").set("store001");
                credentialReference.get("alias").set("alias001");
                break;
        }
        execute(addOperation);
    }

    private void removeDatasourceSilently(final Scenario scenario) throws IOException {
        final ModelNode removeOperation = Operations.createRemoveOperation(scenario.getDatasourceAddress().toModelNode());
        removeOperation.get(ModelDescriptionConstants.OPERATION_HEADERS).get("allow-resource-service-restart").set(true);
        try {
            execute(removeOperation);
        } catch (MgmtOperationException moe) {
            // ignore
        }
    }

    private void addXADatasource(final Scenario scenario) throws IOException, MgmtOperationException {
        final ModelNode addOperation = Operations.createAddOperation(scenario.getXADatasourceAddress().toModelNode());
        addOperation.get("jndi-name").set(scenario.getDatasourceJndiName());
        addOperation.get("driver-name").set("h2");
        addOperation.get("user-name").set("sa");
        addOperation.get(ModelDescriptionConstants.OPERATION_HEADERS).get("allow-resource-service-restart").set(true);

        ModelNode credentialReference = null;
        switch (scenario) {
            case PASSWORD_AND_CREDENTIAL_REFERENCE_PREFERENCE:
                addOperation.get("password").set("wrong-password");
                credentialReference = addOperation.get(CREDENTIAL_REFERENCE).setEmptyObject();
                credentialReference.get("clear-text").set(DATABASE_PASSWORD);
                break;
            case CREDENTIAL_REFERENCE_CLEAR_TEXT:
                credentialReference = addOperation.get(CREDENTIAL_REFERENCE).setEmptyObject();
                credentialReference.get("clear-text").set(DATABASE_PASSWORD);
                break;
            case CREDENTIAL_REFERENCE_STORE_ALIAS:
                credentialReference = addOperation.get(CREDENTIAL_REFERENCE).setEmptyObject();
                credentialReference.get("store").set("store001");
                credentialReference.get("alias").set("alias001");
                break;
        }

        final PathAddress urlXADatasourcePropertyAddress = scenario.getXADatasourceAddress().append("xa-datasource-properties", "URL");
        final ModelNode addURLDatasourcePropertyOperation = Operations.createAddOperation(urlXADatasourcePropertyAddress.toModelNode());
        addURLDatasourcePropertyOperation.get("value").set("jdbc:h2:tcp://" + Utils.getSecondaryTestAddress(client) + "/mem:" + CredentialReferenceDatasourceTestCase.class.getName());

        ModelNode compositeOperation = Operations.createCompositeOperation();
        compositeOperation.get(ModelDescriptionConstants.STEPS).add(addOperation);
        compositeOperation.get(ModelDescriptionConstants.STEPS).add(addURLDatasourcePropertyOperation);

        execute(compositeOperation);
    }

    private void removeXADatasourceSilently(final Scenario scenario) throws IOException {
        final ModelNode removeOperation = Operations.createRemoveOperation(scenario.getXADatasourceAddress().toModelNode());
        removeOperation.get(ModelDescriptionConstants.OPERATION_HEADERS).get("allow-resource-service-restart").set(true);
        try {
            execute(removeOperation);
        } catch (MgmtOperationException moe) {
            // ignore
        }
    }

    private ModelNode execute(final ModelNode op) throws IOException, MgmtOperationException {
        final ModelNode result = client.getControllerClient().execute(op);
        if (!Operations.isSuccessfulOutcome(result)) {
            throw new MgmtOperationException(Operations.getFailureDescription(result).asString());
        }
        return result;
    }

    public static class DatasourceServerSetupTask implements ServerSetupTask {

        private Server h2Server;
        private Connection connection;

        @Override
        public void setup(ManagementClient managementClient, String s) throws Exception {
            h2Server = Server.createTcpServer("-tcpAllowOthers").start();
            // open connection to database, because that's only (easy) way to set password for user sa
            connection = DriverManager.getConnection("jdbc:h2:mem:" + CredentialReferenceDatasourceTestCase.class.getName() + ";DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE", "sa", DATABASE_PASSWORD);
        }

        @Override
        public void tearDown(ManagementClient managementClient, String s) throws Exception {
            connection.close();
            h2Server.shutdown();
        }
    }
}
