/*
 * 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.qpid.jms.jndi;

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

import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.Hashtable;
import java.util.Properties;

import javax.jms.ConnectionFactory;
import javax.naming.Context;
import javax.naming.InitialContext;
import javax.naming.NameNotFoundException;
import javax.naming.NamingException;
import javax.naming.OperationNotSupportedException;

import org.apache.qpid.jms.JmsConnectionFactory;
import org.apache.qpid.jms.JmsQueue;
import org.apache.qpid.jms.JmsTopic;
import org.apache.qpid.jms.test.QpidJmsTestCase;
import org.junit.Test;

public class JmsInitialContextFactoryTest extends QpidJmsTestCase {

    private JmsInitialContextFactory factory;
    private Context context;

    private Context createInitialContext(final Hashtable<Object, Object> environment) throws NamingException {
        factory = new JmsInitialContextFactory();
        context = factory.getInitialContext(environment);
        assertNotNull("No context created", context);

        return context;
    }

    @Test
    public void testDefaultConnectionFactoriesPresentWithEmptyEnvironment() throws Exception {
        Hashtable<Object, Object> env = new Hashtable<Object, Object>();
        Context ctx = createInitialContext(env);

        for (String factoryName : JmsInitialContextFactory.DEFAULT_CONNECTION_FACTORY_NAMES) {
            Object o = ctx.lookup(factoryName);

            assertNotNull("No object returned", o);
            assertEquals("Unexpected class type for returned object", JmsConnectionFactory.class, o.getClass());
            assertEquals("Unexpected URI for returned factory", JmsConnectionFactory.getDefaultRemoteAddress(), ((JmsConnectionFactory) o).getRemoteURI());
        }
    }

    @Test
    public void testDefaultConnectionFactoriesSeeDefaultURIUpdate() throws Exception {
        String updatedDefaultURI = "amqp://example.com:1234";

        Hashtable<Object, Object> env = new Hashtable<Object, Object>();
        env.put(JmsInitialContextFactory.CONNECTION_FACTORY_DEFAULT_KEY_PREFIX + JmsConnectionFactory.REMOTE_URI_PROP, updatedDefaultURI);
        Context ctx = createInitialContext(env);

        for (String factoryName : JmsInitialContextFactory.DEFAULT_CONNECTION_FACTORY_NAMES) {
            Object o = ctx.lookup(factoryName);

            assertNotNull("No object returned", o);
            assertEquals("Unexpected class type for returned object", JmsConnectionFactory.class, o.getClass());
            assertEquals("Unexpected URI for returned factory", updatedDefaultURI, ((JmsConnectionFactory) o).getRemoteURI());
        }
    }

    @Test
    public void testDefaultConnectionFactorySeesFactorySpecificProperty() throws Exception {
        String factoryName = JmsInitialContextFactory.DEFAULT_CONNECTION_FACTORY_NAMES[0];

        // lower case prefix
        doDefaultConnectionFactorySeesFactorySpecificPropertyTestImpl("property.connectionfactory.", factoryName);
        // camelCase prefix
        doDefaultConnectionFactorySeesFactorySpecificPropertyTestImpl("property.connectionFactory.", factoryName);
    }

    private void doDefaultConnectionFactorySeesFactorySpecificPropertyTestImpl(String propertyPrefix, String factoryName) throws Exception {
        String updatedClientID = _testName.getMethodName();

        Hashtable<Object, Object> env = new Hashtable<Object, Object>();
        env.put(propertyPrefix + factoryName + "." + "clientID", updatedClientID);
        Context ctx = createInitialContext(env);

        Object o = ctx.lookup(factoryName);

        assertNotNull("No object returned", o);
        assertEquals("Unexpected class type for returned object", JmsConnectionFactory.class, o.getClass());
        assertEquals("Unexpected ClientID for returned factory", updatedClientID, ((JmsConnectionFactory) o).getClientID());
    }

    @Test
    public void testDefaultConnectionFactoriesNotPresentWhenOneIsExplicitlyDefined() throws Exception {
        Hashtable<Object, Object> env = new Hashtable<Object, Object>();
        env.put(JmsInitialContextFactory.CONNECTION_FACTORY_KEY_PREFIX + "myNewFactory", "amqp://example.com:1234");
        Context ctx = createInitialContext(env);

        for (String factoryName : JmsInitialContextFactory.DEFAULT_CONNECTION_FACTORY_NAMES) {
            try {
                ctx.lookup(factoryName);
                fail("should have thrown exception due to name not being found");
            } catch (NameNotFoundException nnfe) {
                // //expected
            }
        }
    }


    @Test
    public void testDefaultConnectionFactoriesSeeDefaultPropertyUpdate() throws Exception {
        String factoryName = JmsInitialContextFactory.DEFAULT_CONNECTION_FACTORY_NAMES[0];

        // lower case prefix
        doDefaultConnectionFactorySeesDefaultPropertyUpdatePropertyTestImpl("default.connectionfactory.", factoryName);
        // camelCase prefix
        doDefaultConnectionFactorySeesDefaultPropertyUpdatePropertyTestImpl("default.connectionFactory.", factoryName);
    }

    private void doDefaultConnectionFactorySeesDefaultPropertyUpdatePropertyTestImpl(String propertyPrefix, String factoryName) throws Exception {
        String updatedClientID = _testName.getMethodName();

        Hashtable<Object, Object> env = new Hashtable<Object, Object>();
        env.put(propertyPrefix + "clientID", updatedClientID);
        Context ctx = createInitialContext(env);

        Object o = ctx.lookup(factoryName);

        assertNotNull("No object returned", o);
        assertEquals("Unexpected class type for returned object", JmsConnectionFactory.class, o.getClass());
        assertEquals("Unexpected ClientID for returned factory", updatedClientID, ((JmsConnectionFactory) o).getClientID());
    }

    @Test
    public void testConnectionFactoryBinding() throws Exception {
        String factoryName = "myNewFactory";
        // lower case prefix
        doConnectionFactoryBindingTestImpl("connectionfactory." + factoryName, factoryName);
        // camelCase prefix
        doConnectionFactoryBindingTestImpl("connectionFactory." + factoryName, factoryName);
    }

    private void doConnectionFactoryBindingTestImpl(String environmentProperty, String factoryName) throws NamingException {
        String uri = "amqp://example.com:1234";

        Hashtable<Object, Object> env = new Hashtable<Object, Object>();
        env.put(environmentProperty, uri);
        Context ctx = createInitialContext(env);

        Object o = ctx.lookup(factoryName);

        assertNotNull("No object returned", o);
        assertEquals("Unexpected class type for returned object", JmsConnectionFactory.class, o.getClass());
        assertEquals("Unexpected URI for returned factory", uri, ((JmsConnectionFactory) o).getRemoteURI());
    }

    @Test
    public void testConnectionFactoryBindingWithInvalidFactorySpecificProperty() throws Exception {
        String factoryName = "myNewFactory";
        String uri = "amqp://example.com:1234";

        String propertyPrefix = JmsInitialContextFactory.CONNECTION_FACTORY_PROPERTY_KEY_PREFIX;

        Hashtable<Object, Object> env = new Hashtable<Object, Object>();
        env.put(JmsInitialContextFactory.CONNECTION_FACTORY_KEY_PREFIX + factoryName, uri);
        env.put(propertyPrefix + factoryName + "." + "invalidProperty", "value");

        try {
            createInitialContext(env);
            fail("Should have thrown exception");
        } catch (NamingException ne) {
            // Expected
            assertTrue("Should have had a cause", ne.getCause() != null);
        }
    }

    @Test
    public void testConnectionFactoryBindingWithInvalidDefaultProperty() throws Exception {
        String factoryName = "myNewFactory";
        String uri = "amqp://example.com:1234";

        String defaultPrefix = JmsInitialContextFactory.CONNECTION_FACTORY_DEFAULT_KEY_PREFIX;

        Hashtable<Object, Object> env = new Hashtable<Object, Object>();
        env.put(JmsInitialContextFactory.CONNECTION_FACTORY_KEY_PREFIX + factoryName, uri);
        env.put(defaultPrefix + "invalidDefaultProperty", "value");

        try {
            createInitialContext(env);
            fail("Should have thrown exception");
        } catch (NamingException ne) {
            // Expected
            assertTrue("Should have had a cause", ne.getCause() != null);
        }
    }

    @Test
    public void testConnectionFactoryBindingUsesDefaultURIWhenEmpty() throws Exception {
        doConnectionFactoryBindingUsesDefaultURITestImpl("");
    }

    @Test
    public void testConnectionFactoryBindingUsesDefaultURIWhenNull() throws Exception {
        doConnectionFactoryBindingUsesDefaultURITestImpl("");
    }

    private void doConnectionFactoryBindingUsesDefaultURITestImpl(String uriPropertyValue) throws NamingException {
        String factoryName = "myNewFactory";

        Hashtable<Object, Object> env = new Hashtable<Object, Object>();
        env.put(JmsInitialContextFactory.CONNECTION_FACTORY_KEY_PREFIX + factoryName, uriPropertyValue);
        Context ctx = createInitialContext(env);

        Object o = ctx.lookup(factoryName);

        assertNotNull("No object returned", o);
        assertEquals("Unexpected class type for returned object", JmsConnectionFactory.class, o.getClass());
        assertEquals("Unexpected URI for returned factory", JmsConnectionFactory.getDefaultRemoteAddress(), ((JmsConnectionFactory) o).getRemoteURI());
    }

    @Test
    public void testQueueBinding() throws Exception {
        String lookupName = "myLookupName";
        String actualName = "myQueueName";

        Hashtable<Object, Object> env = new Hashtable<Object, Object>();
        env.put(JmsInitialContextFactory.QUEUE_KEY_PREFIX + lookupName, actualName);

        Context ctx = createInitialContext(env);
        Object o = ctx.lookup(lookupName);

        assertNotNull("No object returned", o);
        assertEquals("Unexpected class type for returned object", JmsQueue.class, o.getClass());
        assertEquals("Unexpected name for returned object", actualName, ((JmsQueue) o).getQueueName());
    }

    @Test
    public void testDynamicQueueLookup() throws Exception {
        String actualName = "myQueueName";
        String lookupName = "dynamicQueues/" + actualName;

        Context ctx = createInitialContext(new Hashtable<Object, Object>());
        Object o = ctx.lookup(lookupName);

        assertNotNull("No object returned", o);
        assertEquals("Unexpected class type for returned object", JmsQueue.class, o.getClass());
        assertEquals("Unexpected name for returned object", actualName, ((JmsQueue) o).getQueueName());
    }

    @Test
    public void testTopicBinding() throws Exception {
        String lookupName = "myLookupName";
        String actualName = "myTopicName";

        Hashtable<Object, Object> env = new Hashtable<Object, Object>();
        env.put(JmsInitialContextFactory.TOPIC_KEY_PREFIX + lookupName, actualName);

        Context ctx = createInitialContext(env);
        Object o = ctx.lookup(lookupName);

        assertNotNull("No object returned", o);
        assertEquals("Unexpected class type for returned object", JmsTopic.class, o.getClass());
        assertEquals("Unexpected name for returned object", actualName, ((JmsTopic) o).getTopicName());
    }

    @Test
    public void testDynamicTopicLookup() throws Exception {
        String actualName = "myTopicName";
        String lookupName = "dynamicTopics/" + actualName;

        Context ctx = createInitialContext(new Hashtable<Object, Object>());
        Object o = ctx.lookup(lookupName);

        assertNotNull("No object returned", o);
        assertEquals("Unexpected class type for returned object", JmsTopic.class, o.getClass());
        assertEquals("Unexpected name for returned object", actualName, ((JmsTopic) o).getTopicName());
    }

    @Test
    public void testQueueBindingWithSlashInLookupName() throws Exception {
        String lookupName = "myLookup/Name";
        String actualName = "myQueueName";

        Hashtable<Object, Object> env = new Hashtable<Object, Object>();
        env.put(JmsInitialContextFactory.QUEUE_KEY_PREFIX + lookupName, actualName);

        Context ctx = createInitialContext(env);
        Object o = ctx.lookup(lookupName);

        assertNotNull("No object returned", o);
        assertEquals("Unexpected class type for returned object", JmsQueue.class, o.getClass());
        assertEquals("Unexpected name for returned object", actualName, ((JmsQueue) o).getQueueName());
    }

    @Test
    public void testTopicBindingWithMulupleSlashesInLookupName() throws Exception {
        String lookupName = "my/Lookup/Name";
        String actualName = "myTopicName";

        Hashtable<Object, Object> env = new Hashtable<Object, Object>();
        env.put(JmsInitialContextFactory.TOPIC_KEY_PREFIX + lookupName, actualName);

        Context ctx = createInitialContext(env);
        Object o = ctx.lookup(lookupName);

        assertNotNull("No object returned", o);
        assertEquals("Unexpected class type for returned object", JmsTopic.class, o.getClass());
        assertEquals("Unexpected name for returned object", actualName, ((JmsTopic) o).getTopicName());
    }

    @Test(expected = OperationNotSupportedException.class)
    public void testContextPreventsUnbind() throws Exception {
        Hashtable<Object, Object> env = new Hashtable<Object, Object>();
        Context ctx = createInitialContext(env);

        ctx.unbind("lookupName");
    }

    @Test(expected = OperationNotSupportedException.class)
    public void testContextPreventsRebind() throws Exception {
        Hashtable<Object, Object> env = new Hashtable<Object, Object>();
        Context ctx = createInitialContext(env);

        ctx.rebind("lookupName", new Object());
    }

    @Test(expected = OperationNotSupportedException.class)
    public void testContextPreventsRename() throws Exception {
        Hashtable<Object, Object> env = new Hashtable<Object, Object>();
        Context ctx = createInitialContext(env);

        ctx.rename("lookupName", "");
    }

    @Test
    public void testContextFromProviderUrlNotFoundThrowsNamingException() throws Exception {
        Hashtable<Object, Object> env = new Hashtable<Object, Object>();

        env.put(Context.PROVIDER_URL, "/does/not/exist/1234");
        try {
            createInitialContext(env);
            fail("Should have thrown exception");
        } catch (NamingException ne) {
            // Expected
            assertTrue("Should have had a cause", ne.getCause() != null);
        }
    }

    @Test
    public void testContextFromProviderUrlInEnvironmentMap() throws Exception {
        doContextFromProviderUrlInEnvironmentMapTestImpl(false);
    }

    @Test
    public void testContextFromProviderUrlInEnvironmentMapWithBareFilePath() throws Exception {
        doContextFromProviderUrlInEnvironmentMapTestImpl(true);
    }

    private void doContextFromProviderUrlInEnvironmentMapTestImpl(boolean useBareFilePath) throws IOException, FileNotFoundException, NamingException {
        String myFactory = "myFactory";
        String myURI = "amqp://example.com:2765";

        Properties properties = new Properties();
        properties.put("connectionfactory." + myFactory, myURI);

        File f = File.createTempFile(getTestName(), ".properties");
        try {
            FileOutputStream fos = new FileOutputStream(f);
            try {
                properties.store(fos, null);
            } finally {
                fos.close();
            }

            Hashtable<Object, Object> env = new Hashtable<Object, Object>();
            env.put(Context.INITIAL_CONTEXT_FACTORY, JmsInitialContextFactory.class.getName());
            if (useBareFilePath) {
                env.put(Context.PROVIDER_URL, f.getAbsolutePath());
            } else if(QpidJmsTestCase.IS_WINDOWS) {
                env.put(Context.PROVIDER_URL, "file:///" + f.getAbsolutePath());
            } else {
                env.put(Context.PROVIDER_URL, "file://" + f.getAbsolutePath());
            }

            InitialContext context = new InitialContext(env);

            ConnectionFactory factory = (ConnectionFactory) context.lookup(myFactory);
            assertEquals("Unexpected type of object", JmsConnectionFactory.class, factory.getClass());
            assertEquals("Unexpected URI value", myURI, ((JmsConnectionFactory) factory).getRemoteURI());

            context.close();
        } finally {
            f.delete();
        }
    }

    @Test
    public void testContextFromProviderUrlInSystemProperty() throws Exception
    {
        String myFactory = "myFactory";
        String myURI = "amqp://example.com:2765";

        Properties properties = new Properties();
        properties.put("connectionfactory." + myFactory, myURI);

        File f = File.createTempFile(getTestName(), ".properties");
        try {
            FileOutputStream fos = new FileOutputStream(f);
            try {
                properties.store(fos, null);
            } finally {
                fos.close();
            }

            setTestSystemProperty(Context.INITIAL_CONTEXT_FACTORY, JmsInitialContextFactory.class.getName());
            setTestSystemProperty(Context.PROVIDER_URL, "file://" + f.getAbsolutePath());
            if(QpidJmsTestCase.IS_WINDOWS) {
                setTestSystemProperty(Context.PROVIDER_URL, "file:///" + f.getAbsolutePath());
            } else {
                setTestSystemProperty(Context.PROVIDER_URL, "file://" + f.getAbsolutePath());
            }

            InitialContext context = new InitialContext();

            ConnectionFactory factory = (ConnectionFactory) context.lookup(myFactory);
            assertEquals("Unexpected type of object", JmsConnectionFactory.class, factory.getClass());
            assertEquals("Unexpected URI value", myURI, ((JmsConnectionFactory) factory).getRemoteURI());

            context.close();
        } finally {
            f.delete();
        }
    }
}
