/*
 * 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.ignite.jdbc.thin;

import java.sql.Connection;
import java.sql.SQLException;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Hashtable;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.Callable;
import javax.naming.Binding;
import javax.naming.Context;
import javax.naming.InitialContext;
import javax.naming.Name;
import javax.naming.NameClassPair;
import javax.naming.NameParser;
import javax.naming.NamingEnumeration;
import javax.naming.NamingException;
import javax.naming.spi.InitialContextFactory;
import org.apache.ignite.IgniteJdbcThinDataSource;
import org.apache.ignite.configuration.CacheConfiguration;
import org.apache.ignite.configuration.ClientConnectorConfiguration;
import org.apache.ignite.configuration.IgniteConfiguration;
import org.apache.ignite.internal.binary.BinaryMarshaller;
import org.apache.ignite.internal.jdbc.thin.JdbcThinConnection;
import org.apache.ignite.internal.jdbc.thin.JdbcThinTcpIo;
import org.apache.ignite.testframework.GridTestUtils;
import org.junit.Test;
import static org.apache.ignite.cache.query.SqlFieldsQuery.DFLT_LAZY;

/**
 * DataSource test.
 */
@SuppressWarnings("ThrowableNotThrown")
public class JdbcThinDataSourceSelfTest extends JdbcThinAbstractSelfTest {
    /** {@inheritDoc} */
    @SuppressWarnings("deprecation")
    @Override protected IgniteConfiguration getConfiguration(String igniteInstanceName) throws Exception {
        IgniteConfiguration cfg = super.getConfiguration(igniteInstanceName);

        cfg.setCacheConfiguration(cacheConfiguration(DEFAULT_CACHE_NAME));

        cfg.setMarshaller(new BinaryMarshaller());

        return cfg;
    }

    /**
     * @param name Cache name.
     * @return Cache configuration.
     * @throws Exception In case of error.
     */
    private CacheConfiguration cacheConfiguration(String name) throws Exception {
        CacheConfiguration cfg = defaultCacheConfiguration();

        cfg.setName(name);

        return cfg;
    }

    /** {@inheritDoc} */
    @Override protected void beforeTestsStarted() throws Exception {
        super.beforeTestsStarted();

        startGridsMultiThreaded(2);
    }

    /**
     * @throws Exception If failed.
     */
    @Test
    public void testJndi() throws Exception {
        IgniteJdbcThinDataSource ids = new IgniteJdbcThinDataSource();

        ids.setUrl("jdbc:ignite:thin://127.0.0.1");

        InitialContext ic = getInitialContext();

        ic.bind("ds/test", ids);

        IgniteJdbcThinDataSource ds = (IgniteJdbcThinDataSource)ic.lookup("ds/test");

        assertTrue("Cannot looking up DataSource from JNDI", ds != null);

        assertEquals(ids.getUrl(), ds.getUrl());
    }

    /**
     * @throws Exception If failed.
     */
    @Test
    public void testUrlCompose() throws Exception {
        IgniteJdbcThinDataSource ids = new IgniteJdbcThinDataSource();

        ids.setAddresses("127.0.0.1:" + ClientConnectorConfiguration.DFLT_PORT);

        assertEquals("jdbc:ignite:thin://127.0.0.1:10800/PUBLIC", ids.getUrl());

        ids.setSchema("test");

        assertEquals("jdbc:ignite:thin://127.0.0.1:10800/test", ids.getUrl());
        assertEquals("jdbc:ignite:thin://127.0.0.1:10800/test", ids.getURL());

        ids.setAddresses("127.0.0.1:" + ClientConnectorConfiguration.DFLT_PORT + ".."
            + (ClientConnectorConfiguration.DFLT_PORT + 10), "myhost:10801..10802");

        ids.setSchema("test2");
        assertEquals("jdbc:ignite:thin://127.0.0.1:10800..10810,myhost:10801..10802/test2", ids.getURL());
    }

    /**
     * @throws Exception If failed.
     */
    @Test
    public void testResetUrl() throws Exception {
        IgniteJdbcThinDataSource ids = new IgniteJdbcThinDataSource();

        ids.setUrl("jdbc:ignite:thin://127.0.0.1:10800/test?lazy=" + (!DFLT_LAZY));

        assertEquals("test", ids.getSchema());
        assertEquals(!DFLT_LAZY, ids.isLazy());

        ids.setUrl("jdbc:ignite:thin://mydomain.org,localhost?collocated=true");

        assertNull(ids.getSchema());
        assertEquals(DFLT_LAZY, ids.isLazy());
        assertTrue(ids.isCollocated());
    }

    /**
     * @throws Exception If failed.
     */
    @Test
    public void testSqlHints() throws Exception {
        IgniteJdbcThinDataSource ids = new IgniteJdbcThinDataSource();

        ids.setUrl("jdbc:ignite:thin://127.0.0.1");

        try (Connection conn = ids.getConnection()) {

            for (JdbcThinTcpIo io: ios(conn)) {
                assertFalse(io.connectionProperties().isAutoCloseServerCursor());
                assertFalse(io.connectionProperties().isCollocated());
                assertFalse(io.connectionProperties().isEnforceJoinOrder());
                assertEquals(DFLT_LAZY, io.connectionProperties().isLazy());
                assertFalse(io.connectionProperties().isDistributedJoins());
                assertFalse(io.connectionProperties().isReplicatedOnly());
            }
        }

        ids.setAutoCloseServerCursor(true);
        ids.setCollocated(true);
        ids.setEnforceJoinOrder(true);
        ids.setLazy(!DFLT_LAZY);
        ids.setDistributedJoins(true);
        ids.setReplicatedOnly(true);

        try (Connection conn = ids.getConnection()) {

            for (JdbcThinTcpIo io: ios(conn)) {
                assertTrue(io.connectionProperties().isAutoCloseServerCursor());
                assertTrue(io.connectionProperties().isCollocated());
                assertTrue(io.connectionProperties().isEnforceJoinOrder());
                assertEquals(!DFLT_LAZY, io.connectionProperties().isLazy());
                assertTrue(io.connectionProperties().isDistributedJoins());
                assertTrue(io.connectionProperties().isReplicatedOnly());
            }
        }
    }

    /**
     * @throws Exception If failed.
     */
    @Test
    public void testTcpNoDelay() throws Exception {
        IgniteJdbcThinDataSource ids = new IgniteJdbcThinDataSource();

        ids.setUrl("jdbc:ignite:thin://127.0.0.1");

        try (Connection conn = ids.getConnection()) {

            for (JdbcThinTcpIo io: ios(conn))
                assertTrue(io.connectionProperties().isTcpNoDelay());
        }

        ids.setTcpNoDelay(false);

        try (Connection conn = ids.getConnection()) {

            for (JdbcThinTcpIo io: ios(conn))
                assertFalse(io.connectionProperties().isTcpNoDelay());
        }
    }

    /**
     * @throws Exception If failed.
     */
    @Test
    public void testSocketBuffers() throws Exception {
        final IgniteJdbcThinDataSource ids = new IgniteJdbcThinDataSource();

        ids.setUrl("jdbc:ignite:thin://127.0.0.1");
        ids.setSocketReceiveBuffer(111);
        ids.setSocketSendBuffer(111);

        try (Connection conn = ids.getConnection()) {

            for (JdbcThinTcpIo io: ios(conn)) {
                assertEquals(111, io.connectionProperties().getSocketReceiveBuffer());
                assertEquals(111, io.connectionProperties().getSocketSendBuffer());
            }
        }

        GridTestUtils.assertThrows(log, new Callable<Object>() {
            @Override public Object call() throws Exception {
                ids.setSocketReceiveBuffer(-1);

                ids.getConnection();

                return null;
            }
        }, SQLException.class, "Property cannot be lower than 0 [name=socketReceiveBuffer, value=-1]");

        GridTestUtils.assertThrows(log, new Callable<Object>() {
            @Override public Object call() throws Exception {
                ids.setSocketReceiveBuffer(1024);
                ids.setSocketSendBuffer(-1);

                ids.getConnection();

                return null;
            }
        }, SQLException.class, "Property cannot be lower than 0 [name=socketSendBuffer, value=-1]");
    }

    /**
     * Initial context creation testing purposes
     * @return Initial context.
     * @throws Exception On error.
     */
    private InitialContext getInitialContext() throws Exception {
        Hashtable<String, String> env = new Hashtable<>();

        env.put(Context.INITIAL_CONTEXT_FACTORY, JndiContextMockFactory.class.getName());

        return new InitialContext(env);
    }

    /**
     *
     */
    public static class JndiContextMockFactory implements InitialContextFactory {
        /** {@inheritDoc} */
        @Override public Context getInitialContext(Hashtable<?, ?> environment) throws NamingException {
            return new JndiMockContext();
        }
    }

    /**
     * Get client endpoints for connection.
     *
     * @param conn Connection.
     * @return Collection of endpoints.
     * @throws Exception If failed.
     */
    private static Collection<JdbcThinTcpIo> ios(Connection conn) throws Exception {
        JdbcThinConnection conn0 = conn.unwrap(JdbcThinConnection.class);

        Collection<JdbcThinTcpIo> ios = partitionAwareness ? ((Map<UUID, JdbcThinTcpIo>)
            GridTestUtils.getFieldValue(conn0, JdbcThinConnection.class, "ios")).values() :
            Collections.singleton(GridTestUtils.getFieldValue(conn0, JdbcThinConnection.class, "singleIo"));

        assert !ios.isEmpty();

        return ios;
    }

    /**
     *
     */
    public static class JndiMockContext implements Context {
        /** Objects map. */
        private Map<String, Object> map = new HashMap<String, Object>();

        /** {@inheritDoc} */
        @Override public Object lookup(Name name) throws NamingException {
            return lookup(name.get(0));
        }

        /** {@inheritDoc} */
        @Override public Object lookup(String name) throws NamingException {
            return map.get(name);
        }

        /** {@inheritDoc} */
        @Override public void bind(Name name, Object obj) throws NamingException {
            rebind(name.get(0), obj);
        }

        /** {@inheritDoc} */
        @Override public void bind(String name, Object obj) throws NamingException {
            rebind(name, obj);
        }

        /** {@inheritDoc} */
        @Override public void rebind(Name name, Object obj) throws NamingException {
            rebind(name.get(0), obj);
        }

        /** {@inheritDoc} */
        @Override public void rebind(String name, Object obj) throws NamingException {
            map.put(name, obj);
        }

        /** {@inheritDoc} */
        @Override public void unbind(Name name) throws NamingException {
            unbind(name.get(0));
        }

        /** {@inheritDoc} */
        @Override public void unbind(String name) throws NamingException {
            map.remove(name);
        }

        /** {@inheritDoc} */
        @Override public void rename(Name oldName, Name newName) throws NamingException {
            rename(oldName.get(0), newName.get(0));
        }

        /** {@inheritDoc} */
        @Override public void rename(String oldName, String newName) throws NamingException {
            map.put(newName, map.remove(oldName));
        }

        /** {@inheritDoc} */
        @Override public NamingEnumeration<NameClassPair> list(Name name) throws NamingException {
            return null;
        }

        /** {@inheritDoc} */
        @Override public NamingEnumeration<NameClassPair> list(String name) throws NamingException {
            return null;
        }

        /** {@inheritDoc} */
        @Override public NamingEnumeration<Binding> listBindings(Name name) throws NamingException {
            return null;
        }

        /** {@inheritDoc} */
        @Override public NamingEnumeration<Binding> listBindings(String name) throws NamingException {
            return null;
        }

        /** {@inheritDoc} */
        @Override public void destroySubcontext(Name name) throws NamingException {
        }

        /** {@inheritDoc} */
        @Override public void destroySubcontext(String name) throws NamingException {
        }

        /** {@inheritDoc} */
        @Override public Context createSubcontext(Name name) throws NamingException {
            return null;
        }

        /** {@inheritDoc} */
        @Override public Context createSubcontext(String name) throws NamingException {
            return null;
        }

        /** {@inheritDoc} */
        @Override public Object lookupLink(Name name) throws NamingException {
            return null;
        }

        /** {@inheritDoc} */
        @Override public Object lookupLink(String name) throws NamingException {
            return null;
        }

        /** {@inheritDoc} */
        @Override public NameParser getNameParser(Name name) throws NamingException {
            return null;
        }

        /** {@inheritDoc} */
        @Override public NameParser getNameParser(String name) throws NamingException {
            return null;
        }

        /** {@inheritDoc} */
        @Override public Name composeName(Name name, Name prefix) throws NamingException {
            return null;
        }

        /** {@inheritDoc} */
        @Override public String composeName(String name, String prefix) throws NamingException {
            return null;
        }

        /** {@inheritDoc} */
        @Override public Object addToEnvironment(String propName, Object propVal) throws NamingException {
            return null;
        }

        /** {@inheritDoc} */
        @Override public Object removeFromEnvironment(String propName) throws NamingException {
            return null;
        }

        /** {@inheritDoc} */
        @Override public Hashtable<?, ?> getEnvironment() throws NamingException {
            return null;
        }

        /** {@inheritDoc} */
        @Override public void close() throws NamingException {
        }

        /** {@inheritDoc} */
        @Override public String getNameInNamespace() throws NamingException {
            return null;
        }
    }
}
