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

import java.util.Map;
import java.util.SortedMap;

import org.apache.camel.CamelContext;
import org.apache.camel.Component;
import org.apache.camel.ComponentConfiguration;
import org.apache.camel.Consumer;
import org.apache.camel.Endpoint;
import org.apache.camel.InvalidPropertyException;
import org.apache.camel.Processor;
import org.apache.camel.Producer;
import org.apache.camel.ResolveEndpointFailedException;
import org.apache.camel.TestSupport;
import org.apache.camel.component.seda.SedaComponent;
import org.apache.camel.component.seda.SedaEndpoint;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;

/**
 * Tests the use of {@link ComponentConfiguration} both from the URI string approach or for creating and mutating Endpoint instances
 * with both regular Components/Endpoints and {@link UriEndpointComponent} instances which have Endpoints annotated
 * with {@link org.apache.camel.spi.UriEndpoint}, {@link org.apache.camel.spi.UriParam} and {@link org.apache.camel.spi.UriParams}
 */
public class ComponentConfigurationTest {
    private static final Logger LOG = LoggerFactory.getLogger(ComponentConfigurationTest.class);

    private CamelContext context;

    @Before
    public void createContext() throws Exception {
        context = new DefaultCamelContext();
        context.addComponent("cheese", new NonUriComponent());
        context.start(); // so that TypeConverters are available
    }

    @After
    public void destroyContext() throws Exception {
        context.stop();
        context = null;
    }

    /**
     * Show we can create a URI from the base URI and the underlying query parameter values
     */
    @Test
    public void testCreateUriStringFromParameters() throws Exception {
        Component component = context.getComponent("seda");
        ComponentConfiguration configuration = component.createComponentConfiguration();
        assertNotNull("Should have created a ComponentConfiguration for component " + component,
                configuration);

        // configure the base URI properties
        configuration.setBaseUri("foo");

        // lets try set and get a valid parameter
        configuration.setParameter("concurrentConsumers", 5);
        configuration.setParameter("size", 1000);

        String uriString = configuration.getUriString();
        assertEquals("uriString", "foo?concurrentConsumers=5&size=1000", uriString);
    }

    /**
     * Show we can create a URI from the base URI and the underlying query parameter values
     * on any endpoint (even if its not a {@link UriEndpointComponent})
     */
    @Test
    public void testCreateUriStringFromParametersOnDefaultComponent() throws Exception {
        Component component = context.getComponent("cheese");
        ComponentConfiguration configuration = component.createComponentConfiguration();
        assertNotNull("Should have created a ComponentConfiguration for component " + component,
                configuration);

        // configure the base URI properties
        configuration.setBaseUri("somePath");

        // lets try set and get a valid parameter
        configuration.setParameter("foo", "something");
        configuration.setParameter("bar", 123);

        String uriString = configuration.getUriString();
        assertEquals("uriString", "somePath?bar=123&foo=something", uriString);
    }

    /**
     * Test that parameters are strictly typed on {@link UriEndpointComponent}s
     */
    @Test
    public void testSetParametersFromUriString() throws Exception {
        Component component = context.getComponent("seda");
        ComponentConfiguration configuration = component.createComponentConfiguration();
        assertNotNull("Should have created a ComponentConfiguration for component " + component,
                configuration);

        // configure the uri and query parameters
        configuration.setUriString("foo?concurrentConsumers=5&size=1000");

        // notice the parameters are all correctly typed due to the use of a UriEndpointComponent
        // and the associated @UriEndpoint / @UriParam annotations
        assertEquals("concurrentConsumers", 5, configuration.getParameter("concurrentConsumers"));
        assertEquals("size", 1000, configuration.getParameter("size"));

        configuration.setUriString("foo?concurrentConsumers=9&size=2000");

        assertEquals("concurrentConsumers", 9, configuration.getParameter("concurrentConsumers"));
        assertEquals("size", 2000, configuration.getParameter("size"));
    }

    /**
     * Tests that parameters can be used on non-{@link UriEndpointComponent} implementations
     * but that their types tend to be String until we try to create an Endpoint
     */
    @Test
    public void testSetParametersFromUriStringOnDefaultComponent() throws Exception {
        Component component = context.getComponent("cheese");
        ComponentConfiguration configuration = component.createComponentConfiguration();
        assertNotNull("Should have created a ComponentConfiguration for component " + component,
                configuration);

        // configure the uri and query parameters
        configuration.setUriString("somePath?foo=something&bar=123");

        // notice the parameters are all Strings since we don't use UriEndpointComponent
        assertEquals("foo", "something", configuration.getParameter("foo"));
        assertEquals("bar", "123", configuration.getParameter("bar"));

        configuration.setUriString("somePath?foo=another&bar=456");

        assertEquals("foo", "another", configuration.getParameter("foo"));
        assertEquals("bar", "456", configuration.getParameter("bar"));
    }

    /**
     * Use the {@link ComponentConfiguration}, set some parameters then lets turn it into an endpoint
     */
    @Test
    public void testCreateNewSedaUriEndpoint() throws Exception {
        Component component = context.getComponent("seda");
        ComponentConfiguration configuration = component.createComponentConfiguration();
        assertNotNull("Should have created a ComponentConfiguration for component " + component,
                configuration);

        // configure the base URI properties
        configuration.setBaseUri("foo");

        // lets try set and get a valid parameter
        configuration.setParameter("concurrentConsumers", 5);
        configuration.setParameter("size", 1000);

        // lets try set an invalid parameter
        try {
            configuration.setParameter("doesNotExist", 1000);
            fail("Should have got InvalidPropertyException thrown!");
        } catch (InvalidPropertyException e) {
            LOG.info("Got expected exception: " + e);
        }

        SedaEndpoint endpoint = TestSupport
                .assertIsInstanceOf(SedaEndpoint.class, configuration.createEndpoint());
        assertEquals("concurrentConsumers", 5, endpoint.getConcurrentConsumers());
        assertEquals("size", 1000, endpoint.getSize());

        assertEquals("endpoint uri", "foo?concurrentConsumers=5&size=1000", endpoint.getEndpointUri());

        // lets try configure a parameter
        configuration.setEndpointParameter(endpoint, "concurrentConsumers", 6);
        assertEquals("concurrentConsumers", 6, endpoint.getConcurrentConsumers());

        // lets try set an invalid parameter
        try {
            configuration.setEndpointParameter(endpoint, "doesNotExist", 1000);
            fail("Should have got InvalidPropertyException thrown!");
        } catch (InvalidPropertyException e) {
            LOG.info("Got expected exception: " + e);
        }
    }

    @Test
    public void testCreateNewDefaultComponentEndpoint() throws Exception {
        Component component = context.getComponent("cheese");
        ComponentConfiguration configuration = component.createComponentConfiguration();
        assertNotNull("Should have created a ComponentConfiguration for component " + component,
                configuration);

        // configure the base URI properties
        configuration.setBaseUri("something");

        // lets try set and get a valid parameter
        configuration.setParameter("foo", "xyz");
        configuration.setParameter("bar", 5);

        NonUriEndpoint endpoint = TestSupport
                .assertIsInstanceOf(NonUriEndpoint.class, configuration.createEndpoint());
        assertEquals("foo", "xyz", endpoint.getFoo());
        assertEquals("bar", 5, endpoint.getBar());

        LOG.info("Created endpoint " + endpoint + " on URI " + endpoint.getEndpointUri());

        // lets try configure a parameter
        configuration.setEndpointParameter(endpoint, "bar", 6);
        assertEquals("bar", 6, endpoint.getBar());

        // lets try configure an invalid parameter
        try {
            configuration.setEndpointParameter(endpoint, "doesNotExist", 1000);
            fail("Should have got InvalidPropertyException thrown!");
        } catch (InvalidPropertyException e) {
            LOG.info("Got expected exception: " + e);
        }

        ComponentConfiguration badConfiguration = component.createComponentConfiguration();
        badConfiguration.setBaseUri(configuration.getBaseUri());
        badConfiguration.setParameters(configuration.getParameters());

        // lets try set an invalid parameter on a configuration
        // there is no way to validate on non UriEndpoint unless the endpoint
        // creates its own configuration object so this always works...
        badConfiguration.setParameter("doesNotExist", 1000);

        // however it fails if we now try create an
        try {
            badConfiguration.createEndpoint();
            fail("Should have got ResolveEndpointFailedException thrown!");
        } catch (ResolveEndpointFailedException e) {
            LOG.info("Got expected exception: " + e);
        }
    }

    /**
     * Shows we can introspect a {@link UriEndpointComponent} and find all the available parameters
     * along with their types and {@link ParameterConfiguration}
     */
    @Test
    public void testIntrospectSedaEndpointParameters() throws Exception {
        Component component = context.getComponent("seda");
        ComponentConfiguration configuration = component.createComponentConfiguration();
        assertNotNull("Should have created a ComponentConfiguration for component " + component,
                configuration);

        SortedMap<String, ParameterConfiguration> parameterMap = configuration.getParameterConfigurationMap();
        assertTrue("getParameterConfigurationMap() should not be empty!", !parameterMap.isEmpty());

        ParameterConfiguration concurrentConsumersConfig = parameterMap.get("concurrentConsumers");
        assertNotNull("parameterMap[concurrentConsumers] should not be null!", concurrentConsumersConfig);
        assertEquals("concurrentConsumersConfig.getName()", "concurrentConsumers",
                concurrentConsumersConfig.getName());
        assertEquals("concurrentConsumersConfig.getParameterType()", int.class,
                concurrentConsumersConfig.getParameterType());

        LOG.info("{} has has configuration properties {}", component, parameterMap.keySet());
    }

    /**
     * Shows we can introspect the parameters of a DefaultComponent (i.e. a non {@link UriEndpointComponent})
     * though we only get to introspect the parameter values from teh current configuration
     */
    @Test
    public void testIntrospectDefaultComponentParameters() throws Exception {
        Component component = context.getComponent("cheese");
        ComponentConfiguration configuration = component.createComponentConfiguration();
        assertNotNull("Should have created a ComponentConfiguration for component " + component,
                configuration);

        SortedMap<String, ParameterConfiguration> parameterMap = configuration.getParameterConfigurationMap();
        assertTrue("getParameterConfigurationMap() should be empty as we have no parameters yet",
                parameterMap.isEmpty());

        // configure the uri and query parameters
        configuration.setUriString("somePath?foo=something&bar=123");

        parameterMap = configuration.getParameterConfigurationMap();
        assertEquals("getParameterConfigurationMap() size", 2, parameterMap.size());
        ParameterConfiguration barConfiguration = configuration.getParameterConfiguration("bar");
        assertNotNull("should hav a configuration for 'bar'", barConfiguration);
        assertEquals("barConfiguration.getName()", "bar", barConfiguration.getName());
        assertEquals("barConfiguration.getParameterType()", String.class,
                barConfiguration.getParameterType());
    }

    /**
     * Shows how we can use the configuration to get and set parameters directly on the endpoint
     * for a {@link UriEndpointComponent}
     */
    @Test
    public void testConfigureAnExistingSedaEndpoint() throws Exception {
        SedaEndpoint endpoint = context.getEndpoint("seda:cheese?concurrentConsumers=5", SedaEndpoint.class);
        SedaComponent component = endpoint.getComponent();
        ComponentConfiguration configuration = component.createComponentConfiguration();

        assertEquals("concurrentConsumers", 5, endpoint.getConcurrentConsumers());
        assertEquals("concurrentConsumers", 5,
                configuration.getEndpointParameter(endpoint, "concurrentConsumers"));

        // lets try set and get some valid parameters
        configuration.setEndpointParameter(endpoint, "concurrentConsumers", 10);
        Object concurrentConsumers = configuration.getEndpointParameter(endpoint, "concurrentConsumers");
        assertEquals("endpoint.concurrentConsumers", 10, concurrentConsumers);

        configuration.setEndpointParameter(endpoint, "size", 1000);
        Object size = configuration.getEndpointParameter(endpoint, "size");
        assertEquals("endpoint.size", 1000, size);

        // lets try set an invalid parameter
        try {
            configuration.setEndpointParameter(endpoint, "doesNotExist", 1000);
            fail("Should have got InvalidPropertyException thrown!");
        } catch (InvalidPropertyException e) {
            LOG.info("Got expected exception: " + e);
        }
    }


    /**
     * Shows how we can use the configuration to get and set parameters directly on the endpoint
     * which is typesafe and performs validation even if the component is not a {@link UriEndpointComponent}
     */
    @Test
    public void testConfigureAnExistingDefaultEndpoint() throws Exception {
        NonUriEndpoint endpoint = context
                .getEndpoint("cheese:somePath?bar=123&foo=something", NonUriEndpoint.class);
        Component component = endpoint.getComponent();
        ComponentConfiguration configuration = component.createComponentConfiguration();

        assertEquals("bar", 123, endpoint.getBar());
        assertEquals("bar", 123, configuration.getEndpointParameter(endpoint, "bar"));

        // lets try set and get some valid parameters
        configuration.setEndpointParameter(endpoint, "bar", 10);
        Object bar = configuration.getEndpointParameter(endpoint, "bar");
        assertEquals("endpoint.bar", 10, bar);

        configuration.setEndpointParameter(endpoint, "foo", "anotherThing");
        Object foo = configuration.getEndpointParameter(endpoint, "foo");
        assertEquals("endpoint.foo", "anotherThing", foo);

        // lets try set an invalid parameter
        try {
            configuration.setEndpointParameter(endpoint, "doesNotExist", 1000);
            fail("Should have got InvalidPropertyException thrown!");
        } catch (InvalidPropertyException e) {
            LOG.info("Got expected exception: " + e);
        }
    }

    public static class NonUriComponent extends DefaultComponent {

        @Override
        protected Endpoint createEndpoint(String uri, String remaining, Map<String, Object> parameters) throws Exception {
            return new NonUriEndpoint(uri, this);
        }

    }

    public static class NonUriEndpoint extends DefaultEndpoint {
        private String foo;
        private int bar;

        public NonUriEndpoint(String uri, NonUriComponent component) {
            super(uri, component);
        }

        public int getBar() {
            return bar;
        }

        public void setBar(int bar) {
            this.bar = bar;
        }

        public String getFoo() {
            return foo;
        }

        public void setFoo(String foo) {
            this.foo = foo;
        }

        @Override
        public Producer createProducer() throws Exception {
            return null;
        }

        @Override
        public Consumer createConsumer(Processor processor) throws Exception {
            return null;
        }

        @Override
        public boolean isSingleton() {
            return false;
        }
    }
}
