/**
 * Copyright (c) 2017-2024 Linagora
 * 
 * This program/library 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 program/library 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 program/library; If not, see http://www.gnu.org/licenses/
 * for the GNU Lesser General Public License version 2.1.
 */
package org.ow2.petals.binding.rest.junit;

import static org.junit.Assert.assertNotNull;

import java.io.File;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.util.HashMap;
import java.util.Map;

import javax.activation.DataHandler;

import org.glassfish.grizzly.http.server.HttpServer;
import org.glassfish.grizzly.ssl.SSLContextConfigurator;
import org.glassfish.grizzly.ssl.SSLEngineConfigurator;
import org.glassfish.jersey.grizzly2.httpserver.GrizzlyHttpServerFactory;
import org.glassfish.jersey.media.multipart.MultiPartFeature;
import org.glassfish.jersey.server.ResourceConfig;
import org.glassfish.jersey.server.filter.RolesAllowedDynamicFeature;
import org.junit.rules.ExternalResource;
import org.ow2.petals.binding.rest.junit.auth.BasicAuthenticationFilter;
import org.ow2.petals.binding.rest.junit.data.Metadata;
import org.ow2.petals.binding.rest.junit.data.Metadatas;
import org.ow2.petals.binding.rest.junit.data.Repository;
import org.ow2.petals.binding.rest.junit.resource.DocumentMetadataResource;
import org.ow2.petals.binding.rest.junit.resource.DocumentResource;
import org.ow2.petals.binding.rest.junit.resource.MultipartDocumentMetadataResource;
import org.ow2.petals.binding.rest.junit.resource.MultipartDocumentResource;
import org.ow2.petals.binding.rest.junit.resource.TextResource;

/**
 * A Rest server based on Jersey for unit tests.
 *
 */
public class RestServer extends ExternalResource {

    /**
     * The default HTTP port
     */
    public static final int DEFAULT_HTTP_PORT = 10080;

    // Base URI of the HTTP(S) server will listen on
    private static final String BASE_URI_TEMPLATE = "%s://localhost:%d/unit-tests";

    /**
     * The port on which HTTP requests are listen
     */
    private final int httpPort;

    /**
     * A flag to secure or not the REST server with a basic authentication
     */
    private final boolean isBasicSecured;

    /**
     * Resource name of the keystore containing the SSL certificate to use. {@code null} to disable SSL support.
     */
    private final String sslKeystoreResourceName;

    /**
     * Pass-phrase of the SSL keystore.
     */
    private final String sslKeystorePass;

    /**
     * Pass-phrase of the SSL certificate.
     */
    private final String sslKeyPass;

    /**
     * The Rest server
     */
    private HttpServer server;

    /**
     * Document repositories used as document database
     */
    private final Map<String, Repository> docRepositories = new HashMap<>();

    /**
     * Create HTTP server exposing JAX-RS resources defined in this Jersey application.
     * 
     * @param httpPort
     *            The HTTP port on which requests will be listen
     * @param isBasicSecured
     *            A flag to secure or not the REST server with a basic authentication
     * @param sslKeystoreResourceName
     *            Resource name of the keystore containing the SSL certificate to use. {@code null} to disable SSL
     *            support
     * @param sslKeystorePass
     *            Pass phrase of the SSL keystore
     * 
     * @return HTTP server.
     */
    private static HttpServer createServer(final int httpPort, final boolean isBasicSecured,
            final String sslKeystoreResourceName, final String sslKeystorePass, final String sslKeyPass,
            final Map<String, Repository> docRepositories) throws URISyntaxException {
        // create a resource config that scans for JAX-RS resources and providers in the given package
        final ResourceConfig rc = new ResourceConfig();

        rc.register(MultiPartFeature.class);

        rc.registerInstances(new DocumentResource(docRepositories));
        rc.registerInstances(new DocumentMetadataResource(docRepositories));
        rc.registerInstances(new TextResource());
        rc.registerInstances(new MultipartDocumentResource(docRepositories));
        rc.registerInstances(new MultipartDocumentMetadataResource(docRepositories));

        if (isBasicSecured) {
            rc.register(BasicAuthenticationFilter.class);
            rc.register(RolesAllowedDynamicFeature.class);
        }

        // create and start a new instance of http server exposing the Jersey application at BASE_URI
        final URI srvUri = URI.create(RestServer.getUriBase(httpPort, sslKeystoreResourceName));
        if (sslKeystoreResourceName == null) {
            return GrizzlyHttpServerFactory.createHttpServer(srvUri, rc, false);
        } else {
            final URL keystoreUrl = Thread.currentThread().getContextClassLoader().getResource(sslKeystoreResourceName);
            assertNotNull(keystoreUrl);

            final SSLContextConfigurator sslCtxCfg = new SSLContextConfigurator();
            sslCtxCfg.setKeyStoreFile(new File(keystoreUrl.toURI()).getAbsolutePath());
            sslCtxCfg.setKeyStorePass(sslKeystorePass);
            sslCtxCfg.setKeyPass(sslKeyPass);

            return GrizzlyHttpServerFactory.createHttpServer(srvUri, rc, true,
                    new SSLEngineConfigurator(sslCtxCfg, false, false, false), true);
        }
    }

    private static String getUriBase(final int httpPort, final String sslKeystoreResourceName) {
        return String.format(BASE_URI_TEMPLATE, sslKeystoreResourceName == null ? "http" : "https", httpPort);
    }

    /**
     * <p>
     * Creates a temporary unprotected REST-server listening HTTP requests on the default port
     * ({@value #DEFAULT_HTTP_PORT})
     * </p>
     * <p>
     * Note: SSL is not activated.
     * </p>
     */
    public RestServer() {
        this(DEFAULT_HTTP_PORT);
    }

    /**
     * <p>
     * Creates a temporary unprotected REST-server listening HTTP requests on the given port.
     * </p>
     * <p>
     * Note: SSL is not activated.
     * </p>
     * 
     * @param httpPort
     *            The HTTP port on which requests will be listen
     */
    public RestServer(final int httpPort) {
        this(httpPort, false);
    }

    /**
     * <p>
     * Creates a temporary REST-server listening HTTP requests on the given port.
     * </p>
     * <p>
     * Note: SSL is not activated.
     * </p>
     * 
     * @param httpPort
     *            The HTTP port on which requests will be listen
     * @param isBasicSecured
     *            A flag to secure or not the REST server with a basic authentication
     */
    public RestServer(final int httpPort, final boolean isBasicSecured) {
        this(httpPort, isBasicSecured, null, null, null);
    }

    /**
     * Creates a temporary REST-server listening HTTP or HTTPS requests on the given port.
     * 
     * @param httpPort
     *            The HTTP or HTTPS port on which requests will be listen
     * @param isBasicSecured
     *            A flag to secure or not the REST server with a basic authentication
     * @param sslKeystoreResourceName
     *            Resource name of the keystore containing the SSL certificate to use. {@code null} to disable SSL
     *            support
     * @param sslKeystorePass
     *            Pass phrase of the SSL keystore. Can be {@code null}
     * @param sslKeyPass
     *            Pass phrase of the SSL certificate. Can be {@code null}
     * 
     */
    public RestServer(final int httpPort, final boolean isBasicSecured, final String sslKeystoreResourceName,
            final String sslKeystorePass, final String sslKeyPass) {
        this.httpPort = httpPort;
        this.isBasicSecured = isBasicSecured;
        this.sslKeystoreResourceName = sslKeystoreResourceName;
        this.sslKeystorePass = sslKeystorePass;
        this.sslKeyPass = sslKeyPass;
    }

    @Override
    protected void before() throws Throwable {
        this.initializeRestServer();
    }

    @Override
    protected void after() {
        try {
            this.destroyRestServer();
        } catch (final Exception e) {
            e.printStackTrace();
        }
    }

    /**
     * Initialize the Rest server
     */
    private void initializeRestServer() throws Exception {

        this.cleanDocRepositories();
        this.start();
    }

    /**
     * Free the Rest server
     */
    private void destroyRestServer() throws Exception {
        this.stop();
        this.cleanDocRepositories();
    }

    /**
     * Start the REST server
     */
    public void start() throws Exception {
        this.server = RestServer.createServer(this.httpPort, this.isBasicSecured, this.sslKeystoreResourceName,
                this.sslKeystorePass, this.sslKeyPass, this.docRepositories);
        this.server.start();
        System.out.println(
                String.format("Jersey app started with WADL available at %s/application.wadl.", this.getUriBase()));
    }

    /**
     * Stop the REST server
     */
    public void stop() throws Exception {
        this.server.shutdownNow();
        System.out.println("Jersey app stopped.");
    }

    public String getUriBase() {
        return RestServer.getUriBase(this.httpPort, this.sslKeystoreResourceName);
    }

    public void cleanDocRepositories() {
        this.docRepositories.clear();
    }

    public Map<String, Repository> getDocRepositories() {
        return this.docRepositories;
    }

    /**
     * @param library
     *            The reference of the library in which a document will be created
     * @return the reference of the document added
     */
    public String addDocument(final String library) {
        final Repository repository = this.docRepositories.get(library);
        if (repository != null) {
            return repository.addDocument(new DataHandler("dummy doc content", "text/plain"), new Metadatas());
        } else {
            final Repository newRepository = new Repository();
            this.docRepositories.put(library, newRepository);
            return newRepository.addDocument(new DataHandler("dummy doc content", "text/plain"), new Metadatas());
        }
    }

    /**
     * @param library
     *            The reference of the library in which a document will be created
     * @param reference
     *            The document reference to use
     */
    public void addDocument(final String library, final String reference) {
        final Repository repository = this.docRepositories.get(library);
        if (repository != null) {
            repository.addDocument(reference, new DataHandler("dummy doc content", "text/plain"), new Metadatas());
        } else {
            final Repository newRepository = new Repository();
            this.docRepositories.put(library, newRepository);
            newRepository.addDocument(reference, new DataHandler("dummy doc content", "text/plain"), new Metadatas());
        }
    }

    /**
     * @param library
     *            The reference of the library in which a document will be created
     * @param reference
     *            The document reference to use
     */
    public DataHandler getDocument(final String library, final String reference) {
        final Repository repository = this.docRepositories.get(library);
        if (repository != null) {
            return repository.getDocument(reference);
        } else {
            return null;
        }
    }

    /**
     * @param library
     *            The reference of the library in which the document is
     * @param reference
     *            The document reference to use
     */
    public Metadatas getMetadatas(final String library, final String reference) {
        final Repository repository = this.docRepositories.get(library);
        if (repository != null) {
            return repository.getMetadatas(reference);
        } else {
            return null;
        }
    }

    public void setMetadata(final String library, final String docReference, final String metadataName,
            final String metadataValue) {
        final Repository repository = this.docRepositories.get(library);
        repository.getMetadatas(docReference).getMetadata().add(new Metadata(metadataName, metadataValue));
    }
}
