/**
 * 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;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
import static org.ow2.petals.binding.rest.RESTConstants.ProvidesParameter.HTTP_BODY_TYPE;
import static org.ow2.petals.binding.rest.RESTConstants.ProvidesParameter.HTTP_METHOD;
import static org.ow2.petals.binding.rest.RESTConstants.ProvidesParameter.OPERATION_NAME;
import static org.ow2.petals.binding.rest.RESTConstants.ProvidesParameter.URI;
import static org.ow2.petals.binding.rest.RESTConstants.SUParameter.MAPPING;
import static org.ow2.petals.binding.rest.RESTConstants.SUParameter.NAMESPACE_SU_PARAM;

import java.net.URL;

import javax.management.MalformedObjectNameException;
import javax.xml.namespace.QName;

import org.apache.http.nio.client.HttpAsyncClient;
import org.apache.mina.util.AvailablePortFinder;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.ServerConnector;
import org.junit.ClassRule;
import org.junit.Test;
import org.ow2.petals.binding.rest.RESTConstants.ComponentParameter;
import org.ow2.petals.binding.rest.RESTConstants.HTTPServer;
import org.ow2.petals.binding.rest.config.HTTPBodyType;
import org.ow2.petals.binding.rest.config.incoming.OperationBuilder;
import org.ow2.petals.binding.rest.exchange.incoming.RESTServer;
import org.ow2.petals.binding.rest.junit.AbstractServiceConfigurationFactory;
import org.ow2.petals.binding.rest.junit.RestBaseProvidesServiceConfiguration;
import org.ow2.petals.component.framework.AbstractComponent;
import org.ow2.petals.component.framework.exception.NegativeValueException;
import org.ow2.petals.component.framework.exception.StrictPositiveValueException;
import org.ow2.petals.component.framework.junit.impl.ServiceConfiguration;
import org.ow2.petals.component.framework.junit.mbean.AbstractBootstrapTest;
import org.ow2.petals.component.framework.junit.rule.ComponentUnderTest;
import org.ow2.petals.junit.rules.log.handler.InMemoryLogHandler;
import org.w3c.dom.Document;
import org.w3c.dom.Element;

import com.ebmwebsourcing.easycommons.lang.reflect.ReflectionHelper;

/**
 * Unit tests of {@link RESTBootstrap}
 * 
 * @author Christophe DENEUX - Linagora
 * 
 */
public class RESTBootstrapTest extends AbstractBootstrapTest {

    @ClassRule
    public static final InMemoryLogHandler IN_MEMORY_LOG_HANDLER = new InMemoryLogHandler();

    public RESTBootstrapTest() throws MalformedObjectNameException {
        super();
    }

    /**
     * Check that the component embeds the right default configuration in its JBI descriptor (values set to their
     * default value in jbi.xml)
     */
    @Test
    public void defaultConfiguration_definedInJbiDescriptor() throws Exception {

        this.embeddedJmxSrvCon.registerConfigurationInstallerMBean(this.initBootstrap(new RESTBootstrap()));

        this.assertDefaultConfigurationValues();
    }

    /**
     * Check to set invalid values
     */
    @Test
    public void setInvalidValues() throws Exception {

        this.embeddedJmxSrvCon.registerConfigurationInstallerMBean(this.initBootstrap(new RESTBootstrap()));

        // -------------------------------
        // About the HTTP listening port
        // -------------------------------
        // 1 - Negative values are invalid
        try {
            this.jmxClient.setBootstrapAttribute(RESTBootstrap.ATTR_NAME_CONNECTION_TIMEOUT, -1);
        } catch (final NegativeValueException e) {
            // NOP, it's the expected exception
        }
        // 2 - Zero value is invalid
        try {
            this.jmxClient.setBootstrapAttribute(RESTBootstrap.ATTR_NAME_CONNECTION_TIMEOUT, 0);
        } catch (final StrictPositiveValueException e) {
            // NOP, it's the expected exception
        }

        // -------------------------------
        // About the connection timeout
        // -------------------------------
        // 1 - Negative values are invalid
        try {
            this.jmxClient.setBootstrapAttribute(RESTBootstrap.ATTR_NAME_CONNECTION_TIMEOUT, Long.valueOf(-1));
        } catch (final NegativeValueException e) {
            // NOP, it's the expected exception
        }

        // -------------------------------
        // About the read timeout
        // -------------------------------
        // 1 - Negative values are invalid
        try {
            this.jmxClient.setBootstrapAttribute(RESTBootstrap.ATTR_NAME_READ_TIMEOUT, Long.valueOf(-1));
        } catch (final NegativeValueException e) {
            // NOP, it's the expected exception
        }
    }

    /**
     * Check that valid values are correctly set
     */
    @Test
    public void setValues() throws Exception {

        this.embeddedJmxSrvCon.registerConfigurationInstallerMBean(this.initBootstrap(new RESTBootstrap()));

        // --------------------------
        // About the HTTP webserver
        // --------------------------
        final int httpPort = 8080;
        this.jmxClient.setBootstrapAttribute(RESTBootstrap.ATTR_NAME_HTTP_PORT, Integer.valueOf(httpPort));
        assertEquals(httpPort, this.jmxClient.getBootstrapAttributeAsInt(RESTBootstrap.ATTR_NAME_HTTP_PORT));

        // -------------------------------
        // About the connection timeout
        // -------------------------------
        this.jmxClient.setBootstrapAttribute(RESTBootstrap.ATTR_NAME_CONNECTION_TIMEOUT, Long.valueOf(0));
        final long connectionTimeout = 123456;
        this.jmxClient.setBootstrapAttribute(RESTBootstrap.ATTR_NAME_CONNECTION_TIMEOUT,
                Long.valueOf(connectionTimeout));
        assertEquals(connectionTimeout,
                this.jmxClient.getBootstrapAttributeAsLong(RESTBootstrap.ATTR_NAME_CONNECTION_TIMEOUT));

        // -------------------------------
        // About the read timeout
        // -------------------------------
        this.jmxClient.setBootstrapAttribute(RESTBootstrap.ATTR_NAME_READ_TIMEOUT, Long.valueOf(0));
        final long readTimeout = 123456;
        this.jmxClient.setBootstrapAttribute(RESTBootstrap.ATTR_NAME_READ_TIMEOUT, Long.valueOf(readTimeout));
        assertEquals(readTimeout, this.jmxClient.getBootstrapAttributeAsLong(RESTBootstrap.ATTR_NAME_READ_TIMEOUT));
    }

    private void assertDefaultConfigurationValues() throws Exception {

        assertEquals(RESTConstants.HTTPServer.DEFAULT_HTTP_SERVER_HOSTNAME,
                this.jmxClient.getBootstrapAttributeAsString(RESTBootstrap.ATTR_NAME_HTTP_HOST));
        assertEquals(RESTConstants.HTTPServer.DEFAULT_HTTP_SERVER_PORT,
                this.jmxClient.getBootstrapAttributeAsInt(RESTBootstrap.ATTR_NAME_HTTP_PORT));
        assertEquals(RESTConstants.ComponentParameter.DEFAULT_CONNECTION_TIMEOUT,
                this.jmxClient.getBootstrapAttributeAsLong(RESTBootstrap.ATTR_NAME_CONNECTION_TIMEOUT));
        assertEquals(RESTConstants.ComponentParameter.DEFAULT_READ_TIMEOUT,
                this.jmxClient.getBootstrapAttributeAsLong(RESTBootstrap.ATTR_NAME_READ_TIMEOUT));
    }

    /**
     * Check that valid values set through the component bootstrap through JMX are correctly used by component.
     */
    @Test
    public void setValuesAreUsed() throws Throwable {

        final String operationName = "dummy";

        // To access the component bootstrap through JMX, the component has not to be installed, only loaded
        final ComponentUnderTest componentUnderTest = new ComponentUnderTest(false, false)
                .addLogHandler(IN_MEMORY_LOG_HANDLER.getHandler()).addEmbeddedJmxSrv(this.embeddedJmxSrvCon)
                .setParameter(new QName(NAMESPACE_SU_PARAM, ComponentParameter.HTTP_PORT),
                        String.valueOf(AvailablePortFinder.getNextAvailable(HTTPServer.DEFAULT_HTTP_SERVER_PORT)))
                .registerServiceToDeploy("VALID_SU", new AbstractServiceConfigurationFactory() {
                    @Override
                    public ServiceConfiguration createServiceConfiguration(final URL wsdlUrl) {

                        return new RestBaseProvidesServiceConfiguration(wsdlUrl) {

                            @Override
                            protected void extraServiceConfiguration(final Document jbiDocument,
                                    final Element service) {

                                assert jbiDocument != null;
                                final Element services = getOrCreateServicesElement(jbiDocument);
                                final Element provides = (Element) services
                                        .getElementsByTagNameNS(JBI_NAMESPACE_URI, "provides").item(0);

                                final Element mappingElt = addElement(jbiDocument, provides,
                                        new QName(NAMESPACE_SU_PARAM, MAPPING));

                                final Element operationElt = addElement(jbiDocument, mappingElt,
                                        new QName(NAMESPACE_SU_PARAM, OperationBuilder.XML_TAG_NAME));
                                operationElt.setAttribute(OPERATION_NAME,
                                        provides.lookupPrefix(GED_NAMESPACE) + ":" + operationName);
                                final Element httpMethodElt = addElement(jbiDocument, operationElt,
                                        new QName(NAMESPACE_SU_PARAM, HTTP_METHOD));
                                httpMethodElt.setTextContent("GET");
                                final Element httpBodyTypeElt = addElement(jbiDocument, operationElt,
                                        new QName(NAMESPACE_SU_PARAM, HTTP_BODY_TYPE));
                                httpBodyTypeElt.setTextContent(HTTPBodyType.NO_BODY.toString());
                                final Element uriElt = addElement(jbiDocument, operationElt,
                                        new QName(NAMESPACE_SU_PARAM, URI));
                                uriElt.setTextContent("http://localhost/dummy");
                            }
                        };
                    }
                });
        componentUnderTest.create();

        try {
            final AbstractComponent component = componentUnderTest.getComponentObject();
            assertNotNull(component);
            assertTrue(component instanceof RESTComponent);
            final RESTComponent restComponent = (RESTComponent) component;

            // Set values using the component bootstrap through JMX
            final String httpHost = "127.0.0.1";
            this.jmxClient.setBootstrapAttribute(RESTBootstrap.ATTR_NAME_HTTP_HOST, httpHost);
            final int httpPort = 8099;
            this.jmxClient.setBootstrapAttribute(RESTBootstrap.ATTR_NAME_HTTP_PORT, Integer.valueOf(httpPort));
            final long connectionTimeout = 123456;
            this.jmxClient.setBootstrapAttribute(RESTBootstrap.ATTR_NAME_CONNECTION_TIMEOUT,
                    Long.valueOf(connectionTimeout));
            final long readTimeout = 65897;
            this.jmxClient.setBootstrapAttribute(RESTBootstrap.ATTR_NAME_READ_TIMEOUT, Long.valueOf(readTimeout));

            // We continue component lifecycle to be able to check are correctly used
            componentUnderTest.install();
            componentUnderTest.deployServiceConfigurations();
            componentUnderTest.start();

            // Some parameter are set in the embedded HTTP server
            final RESTServer restServer = (RESTServer) ReflectionHelper.getFieldValue(RESTComponent.class,
                    restComponent, "server", false);
            final Server httpServer = (Server) ReflectionHelper.getFieldValue(RESTServer.class, restServer, "server",
                    false);
            assertNotNull(httpServer.getConnectors());
            assertEquals(1, httpServer.getConnectors().length);
            assertTrue(httpServer.getConnectors()[0] instanceof ServerConnector);

            final HttpAsyncClient httpClient = (HttpAsyncClient) ReflectionHelper.getFieldValue(RESTComponent.class,
                    restComponent, "httpClient", false);
            assertNotNull(httpClient);

            assertEquals(1, restComponent.getProvidesConfigs().values().size());

            // Configuration assertions
            assertEquals(httpHost, ((ServerConnector) httpServer.getConnectors()[0]).getHost());
            assertEquals(httpPort, ((ServerConnector) httpServer.getConnectors()[0]).getPort());
            // TODO these are not accessible...
            // assertEquals(connectionTimeout, httpClient.getConnectTimeout());
            // assertEquals(readTimeout, httpClient.getReadTimeout());
        } finally {
            componentUnderTest.delete();
        }
    }
}
