/*
 * 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.commons.configuration2.builder;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotSame;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertTrue;

import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;

import org.apache.commons.configuration2.BaseConfiguration;
import org.apache.commons.configuration2.BaseHierarchicalConfiguration;
import org.apache.commons.configuration2.Configuration;
import org.apache.commons.configuration2.Initializable;
import org.apache.commons.configuration2.PropertiesConfiguration;
import org.apache.commons.configuration2.XMLConfiguration;
import org.apache.commons.configuration2.beanutils.BeanCreationContext;
import org.apache.commons.configuration2.beanutils.BeanDeclaration;
import org.apache.commons.configuration2.beanutils.BeanFactory;
import org.apache.commons.configuration2.beanutils.BeanHelper;
import org.apache.commons.configuration2.beanutils.DefaultBeanFactory;
import org.apache.commons.configuration2.beanutils.XMLBeanDeclaration;
import org.apache.commons.configuration2.convert.DefaultListDelimiterHandler;
import org.apache.commons.configuration2.convert.ListDelimiterHandler;
import org.apache.commons.configuration2.event.ConfigurationErrorEvent;
import org.apache.commons.configuration2.event.ConfigurationEvent;
import org.apache.commons.configuration2.event.ErrorListenerTestImpl;
import org.apache.commons.configuration2.event.EventListener;
import org.apache.commons.configuration2.event.EventListenerRegistrationData;
import org.apache.commons.configuration2.event.EventListenerTestImpl;
import org.apache.commons.configuration2.ex.ConfigurationException;
import org.apache.commons.configuration2.ex.ConfigurationRuntimeException;
import org.apache.commons.configuration2.reloading.ReloadingController;
import org.apache.commons.configuration2.reloading.ReloadingDetector;
import org.easymock.EasyMock;
import org.junit.BeforeClass;
import org.junit.Test;

/**
 * Test class for {@code BasicConfigurationBuilder}.
 *
 */
public class TestBasicConfigurationBuilder
{
    /** A test list delimiter handler. */
    private static ListDelimiterHandler listHandler;

    @BeforeClass
    public static void setUpBeforeClass() throws Exception
    {
        listHandler = new DefaultListDelimiterHandler(';');
    }

    /**
     * Tries to create an instance without a result class.
     */
    @Test(expected = IllegalArgumentException.class)
    public void testInitNoClass()
    {
        new BasicConfigurationBuilder<Configuration>(null);
    }

    /**
     * Creates a map with test initialization parameters.
     *
     * @return the map with parameters
     */
    private static Map<String, Object> createTestParameters()
    {
        final Map<String, Object> params = new HashMap<>();
        params.put("throwExceptionOnMissing", Boolean.TRUE);
        params.put("listDelimiterHandler", listHandler);
        return params;
    }

    /**
     * Tests whether initialization parameters can be passed to the constructor.
     */
    @Test
    public void testInitWithParameters()
    {
        final Map<String, Object> params = createTestParameters();
        final BasicConfigurationBuilder<Configuration> builder =
                new BasicConfigurationBuilder<>(
                        PropertiesConfiguration.class, params);
        final Map<String, Object> params2 =
                new HashMap<>(builder.getParameters());
        assertEquals("Wrong parameters", createTestParameters(), params2);
    }

    /**
     * Tests whether a copy of the passed in parameters is created.
     */
    @Test
    public void testInitWithParametersDefensiveCopy()
    {
        final Map<String, Object> params = createTestParameters();
        final BasicConfigurationBuilder<PropertiesConfiguration> builder =
                new BasicConfigurationBuilder<>(
                        PropertiesConfiguration.class, params);
        params.put("anotherParameter", "value");
        final Map<String, Object> params2 =
                new HashMap<>(builder.getParameters());
        assertEquals("Wrong parameters", createTestParameters(), params2);
    }

    /**
     * Tests whether null parameters are handled correctly.
     */
    @Test
    public void testInitWithParametersNull()
    {
        final BasicConfigurationBuilder<PropertiesConfiguration> builder =
                new BasicConfigurationBuilder<>(
                        PropertiesConfiguration.class, null);
        assertTrue("Got parameters", builder.getParameters().isEmpty());
    }

    /**
     * Tests that the map with parameters cannot be modified.
     */
    @Test(expected = UnsupportedOperationException.class)
    public void testGetParametersModify()
    {
        final BasicConfigurationBuilder<PropertiesConfiguration> builder =
                new BasicConfigurationBuilder<>(
                        PropertiesConfiguration.class, createTestParameters());
        builder.getParameters().clear();
    }

    /**
     * Tests whether parameters can be set using the configure() method.
     */
    @Test
    public void testConfigure()
    {
        final BasicConfigurationBuilder<PropertiesConfiguration> builder =
                new BasicConfigurationBuilder<>(
                        PropertiesConfiguration.class)
                        .configure(new BasicBuilderParameters()
                                .setListDelimiterHandler(listHandler)
                                .setThrowExceptionOnMissing(true));
        final Map<String, Object> params2 =
                new HashMap<>(builder.getParameters());
        assertEquals("Wrong parameters", createTestParameters(), params2);
    }

    /**
     * Tests whether new parameters can be set to replace existing ones.
     */
    @Test
    public void testSetParameters()
    {
        final Map<String, Object> params1 = new HashMap<>();
        params1.put("someParameter", "value");
        final BasicConfigurationBuilder<PropertiesConfiguration> builder =
                new BasicConfigurationBuilder<>(
                        PropertiesConfiguration.class, params1);
        assertSame("Wrong result", builder,
                builder.setParameters(createTestParameters()));
        final Map<String, Object> params2 =
                new HashMap<>(builder.getParameters());
        assertEquals("Wrong parameters", createTestParameters(), params2);
    }

    /**
     * Tests whether additional parameters can be added.
     */
    @Test
    public void testAddParameters()
    {
        final BasicConfigurationBuilder<PropertiesConfiguration> builder =
                new BasicConfigurationBuilder<>(
                        PropertiesConfiguration.class, createTestParameters());
        final Map<String, Object> params = createTestParameters();
        params.put("anotherParameter", "value");
        assertSame("Wrong result", builder, builder.addParameters(params));
        final Map<String, Object> params2 = builder.getParameters();
        assertTrue("No original parameters",
                params2.keySet().containsAll(createTestParameters().keySet()));
        assertEquals("Additional parameter not found", "value",
                params2.get("anotherParameter"));
    }

    /**
     * Tests whether null parameters are handled correctly by addParameters().
     */
    @Test
    public void testAddParametersNull()
    {
        final BasicConfigurationBuilder<PropertiesConfiguration> builder =
                new BasicConfigurationBuilder<>(
                        PropertiesConfiguration.class, createTestParameters());
        final Map<String, Object> params = builder.getParameters();
        builder.addParameters(null);
        assertEquals("Parameters changed", params, builder.getParameters());
    }

    /**
     * Tests whether all parameters can be reset.
     */
    @Test
    public void testResetParameters()
    {
        final BasicConfigurationBuilder<PropertiesConfiguration> builder =
                new BasicConfigurationBuilder<>(
                        PropertiesConfiguration.class, createTestParameters());
        builder.resetParameters();
        assertTrue("Still got parameters", builder.getParameters().isEmpty());
    }

    /**
     * Tests whether the builder can create a correctly initialized
     * configuration object.
     */
    @Test
    public void testGetConfiguration() throws ConfigurationException
    {
        final PropertiesConfiguration config =
                new BasicConfigurationBuilder<>(
                        PropertiesConfiguration.class).configure(
                        new BasicBuilderParameters().setListDelimiterHandler(
                                listHandler).setThrowExceptionOnMissing(true))
                        .getConfiguration();
        assertTrue("Wrong exception flag", config.isThrowExceptionOnMissing());
        assertEquals("Wrong list delimiter handler", listHandler,
                config.getListDelimiterHandler());
    }

    /**
     * Tests whether the builder can be accessed by multiple threads and that
     * only a single result object is produced.
     */
    @Test
    public void testGetConfigurationConcurrently() throws Exception
    {
        final int threadCount = 32;
        final CountDownLatch startLatch = new CountDownLatch(1);
        final CountDownLatch endLatch = new CountDownLatch(threadCount);
        final ConfigurationBuilder<?> builder =
                new BasicConfigurationBuilder<>(
                        PropertiesConfiguration.class);
        final AccessBuilderThread[] threads = new AccessBuilderThread[threadCount];
        for (int i = 0; i < threadCount; i++)
        {
            threads[i] = new AccessBuilderThread(startLatch, endLatch, builder);
            threads[i].start();
        }
        startLatch.countDown();
        assertTrue("Timeout", endLatch.await(5, TimeUnit.SECONDS));
        final Set<Object> results = new HashSet<>();
        for (final AccessBuilderThread t : threads)
        {
            results.add(t.result);
        }
        assertEquals("Wrong number of result objects", 1, results.size());
    }

    /**
     * Tests whether a reset of the result object can be performed.
     */
    @Test
    public void testResetResult() throws ConfigurationException
    {
        final BasicConfigurationBuilder<PropertiesConfiguration> builder =
                new BasicConfigurationBuilder<>(
                        PropertiesConfiguration.class, createTestParameters());
        final PropertiesConfiguration config = builder.getConfiguration();
        builder.resetResult();
        final PropertiesConfiguration config2 = builder.getConfiguration();
        assertNotSame("No new result", config, config2);
        assertTrue("Wrong property", config2.isThrowExceptionOnMissing());
    }

    /**
     * Tests a full reset of the builder.
     */
    @Test
    public void testReset() throws ConfigurationException
    {
        final BasicConfigurationBuilder<PropertiesConfiguration> builder =
                new BasicConfigurationBuilder<>(
                        PropertiesConfiguration.class, createTestParameters());
        final PropertiesConfiguration config = builder.getConfiguration();
        builder.reset();
        final PropertiesConfiguration config2 = builder.getConfiguration();
        assertNotSame("No new result", config, config2);
        assertFalse("Parameters not reset", config2.isThrowExceptionOnMissing());
    }

    /**
     * Tests whether a check for the correct bean class is made.
     */
    @Test(expected = ConfigurationRuntimeException.class)
    public void testGetResultDeclarationInvalidBeanClass()
            throws ConfigurationException
    {
        final BasicConfigurationBuilder<PropertiesConfiguration> builder =
                new BasicConfigurationBuilder<PropertiesConfiguration>(
                        PropertiesConfiguration.class, createTestParameters())
                {
                    @Override
                    protected BeanDeclaration createResultDeclaration(
                            final Map<String, Object> params)
                    {
                        return new XMLBeanDeclaration(
                                new BaseHierarchicalConfiguration(), "bean",
                                true, Object.class.getName());
                    }
                };
        builder.getConfiguration();
    }

    /**
     * Creates a mock for an event listener.
     *
     * @return the event listener mock
     */
    private static EventListener<ConfigurationEvent> createEventListener()
    {
        @SuppressWarnings("unchecked")
        final
        EventListener<ConfigurationEvent> listener =
                EasyMock.createMock(EventListener.class);
        return listener;
    }

    /**
     * Tests whether configuration listeners can be added.
     */
    @Test
    public void testAddConfigurationListener() throws ConfigurationException
    {
        final EventListener<ConfigurationEvent> l1 = createEventListener();
        final EventListener<ConfigurationEvent> l2 = createEventListener();
        EasyMock.replay(l1, l2);
        final BasicConfigurationBuilder<PropertiesConfiguration> builder =
                new BasicConfigurationBuilder<>(
                        PropertiesConfiguration.class);
        builder.addEventListener(ConfigurationEvent.ANY, l1);
        final PropertiesConfiguration config = builder.getConfiguration();
        builder.addEventListener(ConfigurationEvent.ANY, l2);
        final Collection<EventListener<? super ConfigurationEvent>> listeners =
                config.getEventListeners(ConfigurationEvent.ANY);
        assertTrue("Listener 1 not registered", listeners.contains(l1));
        assertTrue("Listener 2 not registered", listeners.contains(l2));
    }

    /**
     * Tests whether configuration listeners can be removed.
     */
    @Test
    public void testRemoveConfigurationListener() throws ConfigurationException
    {
        final EventListener<ConfigurationEvent> l1 = createEventListener();
        final EventListener<ConfigurationEvent> l2 = createEventListener();
        EasyMock.replay(l1, l2);
        final BasicConfigurationBuilder<PropertiesConfiguration> builder =
                new BasicConfigurationBuilder<>(
                        PropertiesConfiguration.class);
        builder.addEventListener(ConfigurationEvent.ANY_HIERARCHICAL,
                l1);
        builder.addEventListener(ConfigurationEvent.ANY, l2);
        assertTrue("Wrong result",
                builder.removeEventListener(ConfigurationEvent.ANY, l2));
        final PropertiesConfiguration config = builder.getConfiguration();
        assertFalse("Removed listener was registered", config
                .getEventListeners(ConfigurationEvent.ANY).contains(l2));
        assertTrue("Listener not registered",
                config.getEventListeners(ConfigurationEvent.ANY_HIERARCHICAL)
                        .contains(l1));
        builder.removeEventListener(
                ConfigurationEvent.ANY_HIERARCHICAL, l1);
        assertFalse("Listener still registered",
                config.getEventListeners(ConfigurationEvent.ANY_HIERARCHICAL)
                        .contains(l1));
    }

    /**
     * Tests whether event listeners can be copied to another builder.
     */
    @Test
    public void testCopyEventListeners() throws ConfigurationException
    {
        final EventListener<ConfigurationEvent> l1 = createEventListener();
        final EventListener<ConfigurationEvent> l2 = createEventListener();
        final EventListener<ConfigurationErrorEvent> l3 = new ErrorListenerTestImpl(null);
        final BasicConfigurationBuilder<PropertiesConfiguration> builder =
                new BasicConfigurationBuilder<>(
                        PropertiesConfiguration.class);
        builder.addEventListener(ConfigurationEvent.ANY, l1);
        builder.addEventListener(ConfigurationEvent.ANY_HIERARCHICAL, l2);
        builder.addEventListener(ConfigurationErrorEvent.ANY, l3);
        final BasicConfigurationBuilder<XMLConfiguration> builder2 =
                new BasicConfigurationBuilder<>(
                        XMLConfiguration.class);
        builder.copyEventListeners(builder2);
        final XMLConfiguration config = builder2.getConfiguration();
        Collection<EventListener<? super ConfigurationEvent>> listeners =
                config.getEventListeners(ConfigurationEvent.ANY);
        assertEquals("Wrong number of listeners", 1, listeners.size());
        assertTrue("Wrong listener", listeners.contains(l1));
        listeners =
                config.getEventListeners(ConfigurationEvent.ANY_HIERARCHICAL);
        assertEquals("Wrong number of listeners for hierarchical", 2,
                listeners.size());
        assertTrue("Listener 1 not found", listeners.contains(l1));
        assertTrue("Listener 2 not found", listeners.contains(l2));
        final Collection<EventListener<? super ConfigurationErrorEvent>> errListeners =
                config.getEventListeners(ConfigurationErrorEvent.ANY);
        assertEquals("Wrong number of error listeners", 1, errListeners.size());
        assertTrue("Wrong error listener", errListeners.contains(l3));
    }

    /**
     * Tests whether configuration listeners can be defined via the configure()
     * method.
     */
    @Test
    public void testEventListenerConfiguration() throws ConfigurationException
    {
        final EventListenerTestImpl listener1 = new EventListenerTestImpl(null);
        final EventListenerRegistrationData<ConfigurationErrorEvent> regData =
                new EventListenerRegistrationData<>(
                        ConfigurationErrorEvent.WRITE,
                        new ErrorListenerTestImpl(null));
        final BasicConfigurationBuilder<PropertiesConfiguration> builder =
                new BasicConfigurationBuilder<>(
                        PropertiesConfiguration.class)
                        .configure(new EventListenerParameters()
                                .addEventListener(ConfigurationEvent.ANY,
                                        listener1).addEventListener(regData));
        final PropertiesConfiguration config = builder.getConfiguration();
        assertTrue("Configuration listener not found", config
                .getEventListeners(ConfigurationEvent.ANY).contains(listener1));
        assertTrue(
                "Error listener not found",
                config.getEventListeners(regData.getEventType()).contains(
                        regData.getListener()));
    }

    /**
     * Tests whether configuration listeners are removed from the managed
     * configuration when the builder's result object is reset.
     */
    @Test
    public void testRemoveConfigurationListenersOnReset()
            throws ConfigurationException
    {
        final EventListenerTestImpl listener = new EventListenerTestImpl(null);
        final BasicConfigurationBuilder<PropertiesConfiguration> builder =
                new BasicConfigurationBuilder<>(
                        PropertiesConfiguration.class)
                        .configure(new EventListenerParameters()
                                .addEventListener(ConfigurationEvent.ANY,
                                        listener));
        final PropertiesConfiguration config = builder.getConfiguration();
        builder.resetResult();
        config.addProperty("foo", "bar");
        listener.done();
    }

    /**
     * Tests whether parameters starting with a reserved prefix are filtered out
     * before result objects are initialized.
     */
    @Test
    public void testReservedParameter() throws ConfigurationException
    {
        final Map<String, Object> params = new HashMap<>();
        params.put("throwExceptionOnMissing", Boolean.TRUE);
        params.put("config-test", "a test");
        final BasicConfigurationBuilder<PropertiesConfiguration> builder =
                new BasicConfigurationBuilder<>(
                        PropertiesConfiguration.class, params);
        final PropertiesConfiguration config = builder.getConfiguration();
        assertTrue("Flag not set", config.isThrowExceptionOnMissing());
    }

    /**
     * Tests an exception during configuration initialization if the
     * allowFailOnInit flag is false.
     */
    @Test(expected = ConfigurationException.class)
    public void testInitializationErrorNotAllowed()
            throws ConfigurationException
    {
        final BasicConfigurationBuilderInitFailImpl builder =
                new BasicConfigurationBuilderInitFailImpl(false);
        builder.getConfiguration();
    }

    /**
     * Tests an exception during configuration initialization if the
     * allowFailOnInit flag is true.
     */
    @Test
    public void testInitializationErrorAllowed() throws ConfigurationException
    {
        final BasicConfigurationBuilderInitFailImpl builder =
                new BasicConfigurationBuilderInitFailImpl(true);
        final PropertiesConfiguration config = builder.getConfiguration();
        assertTrue("Got data", config.isEmpty());
    }

    /**
     * Tests whether a configuration implementing {@code Initializable} is
     * correctly handled.
     */
    @Test
    public void testInitializableCalled() throws ConfigurationException
    {
        final BasicConfigurationBuilder<InitializableConfiguration> builder =
                new BasicConfigurationBuilder<>(
                        InitializableConfiguration.class);
        builder.configure(new BasicBuilderParameters()
                .setThrowExceptionOnMissing(true));
        final InitializableConfiguration config = builder.getConfiguration();
        assertEquals("Property not correctly initialized",
                "Initialized with flag true", config.getInitProperty());
    }

    /**
     * Tests whether a configured BeanHelper is used for result creation.
     */
    @Test
    public void testBeanHelperInConfiguration() throws ConfigurationException
    {
        final Set<Class<?>> classesPassedToFactory = new HashSet<>();
        final BeanFactory factory = new DefaultBeanFactory()
        {
            @Override
            public Object createBean(final BeanCreationContext bcc) throws Exception
            {
                classesPassedToFactory.add(bcc.getBeanClass());
                return super.createBean(bcc);
            }
        };
        final BeanHelper helper = new BeanHelper(factory);
        final BasicConfigurationBuilder<PropertiesConfiguration> builder =
                new BasicConfigurationBuilder<>(
                        PropertiesConfiguration.class);
        builder.configure(new BasicBuilderParameters().setBeanHelper(helper));
        final PropertiesConfiguration config = builder.getConfiguration();
        assertTrue("BeanFactory was not used correctly",
                classesPassedToFactory.contains(config.getClass()));
    }

    /**
     * Tests whether a builder can be connected to a reloading controller.
     */
    @Test
    public void testConnectToReloadingController()
            throws ConfigurationException
    {
        final ReloadingDetector detector =
                EasyMock.createNiceMock(ReloadingDetector.class);
        EasyMock.expect(detector.isReloadingRequired()).andReturn(Boolean.TRUE);
        EasyMock.replay(detector);
        final ReloadingController controller = new ReloadingController(detector);
        final BasicConfigurationBuilder<Configuration> builder =
                new BasicConfigurationBuilder<>(
                        PropertiesConfiguration.class);
        final Configuration configuration = builder.getConfiguration();

        builder.connectToReloadingController(controller);
        controller.checkForReloading(null);
        assertTrue("Not in reloading state", controller.isInReloadingState());
        assertNotSame("No new configuration created", configuration,
                builder.getConfiguration());
        assertFalse("Still in reloading state", controller.isInReloadingState());
    }

    /**
     * Tries to connect to a null reloading controller.
     */
    @Test(expected = IllegalArgumentException.class)
    public void testConnectToReloadingControllerNull()
    {
        final BasicConfigurationBuilder<Configuration> builder =
                new BasicConfigurationBuilder<>(
                        PropertiesConfiguration.class);
        builder.connectToReloadingController(null);
    }

    /**
     * A test thread class for testing whether the builder's result object can
     * be requested concurrently.
     */
    private static class AccessBuilderThread extends Thread
    {
        /** A latch for controlling the start of the thread. */
        private final CountDownLatch startLatch;

        /** A latch for controlling the end of the thread. */
        private final CountDownLatch endLatch;

        /** The builder to be accessed. */
        private final ConfigurationBuilder<?> builder;

        /** The result object obtained from the builder. */
        private volatile Object result;

        /**
         * Creates a new instance of {@code AccessBuilderThread}.
         *
         * @param lstart the latch for controlling the thread start
         * @param lend the latch for controlling the thread end
         * @param bldr the builder to be tested
         */
        public AccessBuilderThread(final CountDownLatch lstart, final CountDownLatch lend,
                final ConfigurationBuilder<?> bldr)
        {
            startLatch = lstart;
            endLatch = lend;
            builder = bldr;
        }

        @Override
        public void run()
        {
            try
            {
                startLatch.await();
                result = builder.getConfiguration();
            }
            catch (final Exception ex)
            {
                result = ex;
            }
            finally
            {
                endLatch.countDown();
            }
        }
    }

    /**
     * A builder test implementation which allows checking exception handling
     * when creating new configuration objects.
     */
    private static class BasicConfigurationBuilderInitFailImpl extends
            BasicConfigurationBuilder<PropertiesConfiguration>
    {
        public BasicConfigurationBuilderInitFailImpl(final boolean allowFailOnInit)
        {
            super(PropertiesConfiguration.class, null, allowFailOnInit);
        }

        /**
         * {@inheritDoc} This implementation only throws an exception.
         */
        @Override
        protected void initResultInstance(final PropertiesConfiguration obj)
                throws ConfigurationException
        {
            throw new ConfigurationException("Initialization test exception!");
        }
    }

    /**
     * A test configuration implementation which also implements Initializable.
     */
    public static class InitializableConfiguration extends BaseConfiguration
            implements Initializable
    {
        /** A property which is initialized if the builder works as expected. */
        private String initProperty;

        /**
         * Sets the value of the initProperty member based on other flag values.
         * This tests whether the method is called after other properties have
         * been set.
         */
        @Override
        public void initialize()
        {
            initProperty =
                    "Initialized with flag " + isThrowExceptionOnMissing();
        }

        public String getInitProperty()
        {
            return initProperty;
        }
    }
}