/*
 * JBoss, Home of Professional Open Source.
 * Copyright 2012, Red Hat, Inc., and individual contributors
 * as indicated by the @author tags. See the copyright.txt file in the
 * distribution for a full listing of individual contributors.
 *
 * This is free software; you can redistribute it and/or modify it
 * under the terms of the GNU Lesser General Public License as
 * published by the Free Software Foundation; either version 2.1 of
 * the License, or (at your option) any later version.
 *
 * This software is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this software; if not, write to the Free
 * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
 * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
 */
package org.jboss.as.test.integration.security.loginmodules;

import static org.junit.Assert.assertEquals;

import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URISyntaxException;
import java.net.URL;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.HashMap;
import java.util.Map;

import org.apache.http.client.ClientProtocolException;
import org.h2.tools.Server;
import org.jboss.arquillian.container.test.api.Deployment;
import org.jboss.arquillian.container.test.api.OperateOnDeployment;
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.test.categories.CommonCriteria;
import org.jboss.as.test.integration.security.common.AbstractDataSourceServerSetupTask;
import org.jboss.as.test.integration.security.common.AbstractSecurityDomainsServerSetupTask;
import org.jboss.as.test.integration.security.common.Coding;
import org.jboss.as.test.integration.security.common.Utils;
import org.jboss.as.test.integration.security.common.config.DataSource;
import org.jboss.as.test.integration.security.common.config.SecurityDomain;
import org.jboss.as.test.integration.security.common.config.SecurityModule;
import org.jboss.as.test.integration.security.common.servlets.SimpleSecuredServlet;
import org.jboss.as.test.integration.security.common.servlets.SimpleServlet;
import org.jboss.logging.Logger;
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.experimental.categories.Category;
import org.junit.runner.RunWith;

/**
 * Tests for {@link org.jboss.security.auth.spi.DatabaseServerLoginModule}. It uses embedded H2 database as a datastore for
 * users and roles.
 *
 * @author Jan Lanik
 * @author Josef Cacek
 */
@RunWith(Arquillian.class)
@ServerSetup({ DatabaseLoginModuleTestCase.DBSetup.class, //
        DatabaseLoginModuleTestCase.DataSourcesSetup.class, //
        DatabaseLoginModuleTestCase.SecurityDomainsSetup.class //
})
@RunAsClient
@Category(CommonCriteria.class)
public class DatabaseLoginModuleTestCase {

    private static Logger LOGGER = Logger.getLogger(DatabaseLoginModuleTestCase.class);

    private static final String MARCUS = "marcus";
    private static final String ANIL = "anil";

    private static final String DATASOURCE_NAME = "DBLMTest";

    private static final String DEP1 = "DEP1"; //"DatabaseLogin-defaultSetting";
    private static final String DEP2 = "DEP2"; //"DatabaseLogin-hashMD5";
    private static final String DEP3 = "DEP3"; //"DatabaseLogin-hashMD5-base64";
    private static final String DEP4 = "DEP4"; //"DatabaseLogin-hashMD5-hex";

    private static final String MD5 = "MD5";

    /**
     * Creates WAR for test login module with default settings.
     *
     * @return
     */
    @Deployment(name = DEP1)
    public static WebArchive appDeployment1() {
        return createWar(DEP1);
    }

    /**
     * Creates WAR for test login module with MD5 hashing enabled.
     *
     * @return
     */
    @Deployment(name = DEP2)
    public static WebArchive appDeployment2() {
        return createWar(DEP2);
    }

    /**
     * Creates WAR for test login module with MD5 hashing enabled - Base64 coding used.
     *
     * @return
     */
    @Deployment(name = DEP3)
    public static WebArchive appDeployment3() {
        return createWar(DEP3);
    }

    /**
     * Creates WAR for test login module with MD5 hashing enabled - HEX coding used.
     *
     * @return
     */
    @Deployment(name = DEP4)
    public static WebArchive appDeployment4() {
        return createWar(DEP4);
    }

    /**
     * Test default login module settings.
     */
    @OperateOnDeployment(DEP1)
    @Test
    public void testDefault(@ArquillianResource URL url) throws Exception {
        testAccess(url);
    }

    /**
     * Test login module setting with MD5 hashing enabled.
     */
    @OperateOnDeployment(DEP2)
    @Test
    public void testHashed(@ArquillianResource URL url) throws Exception {
        testAccess(url);
    }

    /**
     * Test login module setting with MD5 hashing enabled - Base64 coding used.
     */
    @OperateOnDeployment(DEP3)
    @Test
    public void testHashedBase64(@ArquillianResource URL url) throws Exception {
        testAccess(url);
    }

    /**
     * Test login module setting with MD5 hashing enabled - HEX coding used.
     */
    @OperateOnDeployment(DEP4)
    @Test
    public void testHashedHex(@ArquillianResource URL url) throws Exception {
        testAccess(url);
    }

    // Private methods -------------------------------------------------------

    /**
     * Tests access to a protected servlet.
     *
     * @param url
     * @throws MalformedURLException
     * @throws ClientProtocolException
     * @throws IOException
     * @throws URISyntaxException
     */
    private void testAccess(URL url) throws MalformedURLException, ClientProtocolException, IOException, URISyntaxException {
        final URL servletUrl = new URL(url.toExternalForm() + SimpleSecuredServlet.SERVLET_PATH.substring(1));
        //successful authentication and authorization
        assertEquals("Response body is not correct.", SimpleSecuredServlet.RESPONSE_BODY,
                Utils.makeCallWithBasicAuthn(servletUrl, ANIL, ANIL, 200));
        //successful authentication and unsuccessful authorization
        Utils.makeCallWithBasicAuthn(servletUrl, MARCUS, MARCUS, 403);
        //unsuccessful authentication
        Utils.makeCallWithBasicAuthn(servletUrl, ANIL, MARCUS, 401);
        Utils.makeCallWithBasicAuthn(servletUrl, ANIL, Utils.hash(ANIL, MD5, Coding.BASE_64), 401);
        Utils.makeCallWithBasicAuthn(servletUrl, ANIL, Utils.hash(ANIL, MD5, Coding.HEX), 401);
    }

    /**
     * Creates {@link WebArchive} (WAR) for given deployment name.
     *
     * @param deployment
     * @return
     */
    private static WebArchive createWar(final String deployment) {
        LOGGER.info("Starting deployment " + deployment);

        final WebArchive war = ShrinkWrap.create(WebArchive.class, deployment + ".war");
        war.addClasses(SimpleSecuredServlet.class, SimpleServlet.class);
        war.addAsWebInfResource(DatabaseLoginModuleTestCase.class.getPackage(), "web-basic-authn.xml", "web.xml");
        war.addAsWebInfResource(new StringAsset("<jboss-web>" + //
                "<security-domain>" + deployment + "</security-domain>" + //
                "</jboss-web>"), "jboss-web.xml");

        if (LOGGER.isDebugEnabled()) {
            LOGGER.debug(war.toString(true));
        }
        return war;
    }

    // Embedded classes ------------------------------------------------------

    /**
     * A {@link ServerSetupTask} instance which creates security domains for this test case.
     *
     * @author Josef Cacek
     */
    static class SecurityDomainsSetup extends AbstractSecurityDomainsServerSetupTask {

        /**
         * Returns SecurityDomains configuration for this testcase.
         *
         * @see org.jboss.as.test.integration.security.common.AbstractSecurityDomainsServerSetupTask#getSecurityDomains()
         */
        @Override
        protected SecurityDomain[] getSecurityDomains() {
            final SecurityModule.Builder loginModuleBuilder = new SecurityModule.Builder().name("Database").options(
                    getLoginModuleOptions(DEP1));
            final SecurityDomain sd1 = new SecurityDomain.Builder().name(DEP1).loginModules(loginModuleBuilder.build()).build();
            loginModuleBuilder.options(getLoginModuleOptions(DEP2)).putOption("hashAlgorithm", MD5);
            final SecurityDomain sd2 = new SecurityDomain.Builder().name(DEP2).loginModules(loginModuleBuilder.build()).build();
            loginModuleBuilder.options(getLoginModuleOptions(DEP3)).putOption("hashAlgorithm", MD5)
                    .putOption("hashEncoding", "base64");
            final SecurityDomain sd3 = new SecurityDomain.Builder().name(DEP3).loginModules(loginModuleBuilder.build()).build();
            loginModuleBuilder.options(getLoginModuleOptions(DEP4)).putOption("hashAlgorithm", MD5)
                    .putOption("hashEncoding", "hex");
            final SecurityDomain sd4 = new SecurityDomain.Builder().name(DEP4).loginModules(loginModuleBuilder.build()).build();
            return new SecurityDomain[] { sd1, sd2, sd3, sd4 };
        }

        /**
         * Generates common login module options.
         *
         * @param deployment
         * @return
         */
        private Map<String, String> getLoginModuleOptions(String deployment) {
            final Map<String, String> options = new HashMap<String, String>();
            options.put("dsJndiName", "java:jboss/datasources/" + DATASOURCE_NAME);
            options.put("principalsQuery", "select Password from Principals" + deployment + " where PrincipalID=?");
            options.put("rolesQuery", "select Role, RoleGroup from Roles where PrincipalID=?");
            return options;
        }
    }

    /**
     * Datasource setup task for H2 DB.
     */
    static class DataSourcesSetup extends AbstractDataSourceServerSetupTask {

        @Override
        protected DataSource[] getDataSourceConfigurations(ManagementClient managementClient, String containerId) {
            return new DataSource[] { new DataSource.Builder()
                    .name(DATASOURCE_NAME)
                    .connectionUrl(
                            "jdbc:h2:tcp://" + Utils.getSecondaryTestAddress(managementClient) + "/mem:" + DATASOURCE_NAME)
                    .driver("h2").username("sa").password("sa").build() };
        }
    }

    /**
     * H2 DB configuration setup task.
     */
    static class DBSetup implements ServerSetupTask {

        private Server server;

        public void setup(ManagementClient managementClient, String containerId) throws Exception {
            server = Server.createTcpServer("-tcpAllowOthers").start();
            final String dbUrl = "jdbc:h2:mem:" + DATASOURCE_NAME + ";DB_CLOSE_DELAY=-1";
            LOGGER.info("Creating database " + dbUrl);

            final Connection conn = DriverManager.getConnection(dbUrl, "sa", "sa");
            executeUpdate(conn, "CREATE TABLE Roles(PrincipalID Varchar(50), Role Varchar(50), RoleGroup Varchar(50))");
            executeUpdate(conn, "INSERT INTO Roles VALUES ('anil','" + SimpleSecuredServlet.ALLOWED_ROLE + "','Roles')");
            executeUpdate(conn, "INSERT INTO Roles VALUES ('marcus','superuser','Roles')");
            createPrincipalsTab(conn, DEP1, null);
            createPrincipalsTab(conn, DEP2, Coding.BASE_64);
            createPrincipalsTab(conn, DEP3, Coding.BASE_64);
            createPrincipalsTab(conn, DEP4, Coding.HEX);
            conn.close();
        }

        public void tearDown(ManagementClient managementClient, String containerId) throws Exception {
            server.shutdown();
            server = null;
        }

        private void createPrincipalsTab(Connection conn, String dep, Coding coding) throws SQLException {
            executeUpdate(conn, "CREATE TABLE Principals" + dep + "(PrincipalID Varchar(50), Password Varchar(50))");
            executeUpdate(conn, "INSERT INTO Principals" + dep + " VALUES ('anil','" + Utils.hashMD5(ANIL, coding) + "')");
            executeUpdate(conn, "INSERT INTO Principals" + dep + " VALUES ('marcus','" + Utils.hashMD5(MARCUS, coding) + "')");
        }

        private void executeUpdate(Connection connection, String query) throws SQLException {
            final Statement statement = connection.createStatement();
            final int updateResult = statement.executeUpdate(query);
            LOGGER.info("Result: " + updateResult + ".  SQL statement: " + query);
            statement.close();
        }
    }

}
