/*
 * 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.internal.processors.query;

import java.util.List;
import java.util.concurrent.Callable;
import javax.cache.CacheException;
import org.apache.ignite.IgniteCache;
import org.apache.ignite.IgniteException;
import org.apache.ignite.IgniteLogger;
import org.apache.ignite.Ignition;
import org.apache.ignite.cache.CacheAtomicityMode;
import org.apache.ignite.cache.CacheMode;
import org.apache.ignite.cache.query.SqlFieldsQuery;
import org.apache.ignite.cache.query.annotations.QuerySqlField;
import org.apache.ignite.configuration.CacheConfiguration;
import org.apache.ignite.configuration.IgniteConfiguration;
import org.apache.ignite.internal.processors.query.h2.IgniteH2Indexing;
import org.apache.ignite.spi.discovery.tcp.TcpDiscoverySpi;
import org.apache.ignite.spi.discovery.tcp.ipfinder.TcpDiscoveryIpFinder;
import org.apache.ignite.spi.discovery.tcp.ipfinder.vm.TcpDiscoveryVmIpFinder;
import org.apache.ignite.testframework.GridTestUtils;
import org.apache.ignite.testframework.junits.common.GridCommonAbstractTest;

/**
 * Tests {@link IgniteH2Indexing} support {@link CacheConfiguration#setSqlSchema(String)} configuration.
 */
@SuppressWarnings("unchecked")
public class IgniteSqlSchemaIndexingTest extends GridCommonAbstractTest {
    /** */
    private static final TcpDiscoveryIpFinder ipFinder = new TcpDiscoveryVmIpFinder(true);

    /** {@inheritDoc} */
    @Override protected IgniteConfiguration getConfiguration() throws Exception {
        IgniteConfiguration cfg = super.getConfiguration();

        cfg.setPeerClassLoadingEnabled(false);

        TcpDiscoverySpi disco = new TcpDiscoverySpi();

        disco.setIpFinder(ipFinder);

        cfg.setDiscoverySpi(disco);

        return cfg;
    }

    /**
     * @param name Cache name.
     * @param partitioned Partition or replicated cache.
     * @param idxTypes Indexed types.
     * @return Cache configuration.
     */
    private static CacheConfiguration cacheConfig(String name, boolean partitioned, Class<?>... idxTypes) {
        return new CacheConfiguration(DEFAULT_CACHE_NAME)
            .setName(name)
            .setCacheMode(partitioned ? CacheMode.PARTITIONED : CacheMode.REPLICATED)
            .setAtomicityMode(CacheAtomicityMode.ATOMIC)
            .setBackups(1)
            .setIndexedTypes(idxTypes);
    }

    /** {@inheritDoc} */
    @Override protected void afterTest() throws Exception {
        stopAllGrids();
    }

    /**
     * Tests collision for case insensitive sqlScheme.
     *
     * @throws Exception If failed.
     */
    public void testCaseSensitive() throws Exception {
        //TODO rewrite with dynamic cache creation, and GRID start in #beforeTest after resolve of
        //https://issues.apache.org/jira/browse/IGNITE-1094

        GridTestUtils.assertThrows(log, new Callable<Object>() {
            @Override public Object call() throws Exception {
                final CacheConfiguration cfg = cacheConfig("InSensitiveCache", true, Integer.class, Integer.class)
                    .setSqlSchema("InsensitiveCache");
                final CacheConfiguration collisionCfg = cacheConfig("InsensitiveCache", true, Integer.class, Integer.class)
                    .setSqlSchema("Insensitivecache");
                IgniteConfiguration icfg = new IgniteConfiguration()
                    .setLocalHost("127.0.0.1")
                    .setCacheConfiguration(cfg, collisionCfg);

                Ignition.start(icfg);

                return null;
            }
        }, IgniteException.class, "Cache already registered: ");
    }

    /**
     * Tests unregistration of previous scheme.
     *
     * @throws Exception If failed.
     */
    public void testCacheUnregistration() throws Exception {
        startGridsMultiThreaded(3, true);

        final CacheConfiguration<Integer, Fact> cfg = cacheConfig("Insensitive_Cache", true, Integer.class, Fact.class)
            .setSqlSchema("Insensitive_Cache");
        final CacheConfiguration<Integer, Fact> collisionCfg = cacheConfig("InsensitiveCache", true, Integer.class, Fact.class)
            .setSqlSchema("Insensitive_Cache");

        IgniteCache<Integer, Fact> cache = ignite(0).createCache(cfg);

        SqlFieldsQuery qry = new SqlFieldsQuery("select f.id, f.name " +
            "from InSENSitive_Cache.Fact f");

        cache.put(1, new Fact(1, "cacheInsensitive"));

        for (List<?> row : cache.query(qry)) {
            assertEquals(2, row.size());
            assertEquals(1, row.get(0));
            assertEquals("cacheInsensitive", row.get(1));
        }

        ignite(0).destroyCache(cache.getName());

        cache = ignite(0).createCache(collisionCfg); // Previous collision should be removed by now.

        cache.put(1, new Fact(1, "cacheInsensitive"));
        cache.put(2, new Fact(2, "ThisIsANewCache"));
        cache.put(3, new Fact(3, "With3RecordsAndAnotherName"));

        assertEquals(3, cache.query(qry).getAll().size());

        ignite(0).destroyCache(cache.getName());
    }

    /**
     * Tests escapeAll and sqlSchema apposition.
     *
     * @throws Exception If failed.
     */
    public void testSchemaEscapeAll() throws Exception {
        startGridsMultiThreaded(3, true);

        final CacheConfiguration<Integer, Fact> cfg = cacheConfig("simpleSchema", true, Integer.class, Fact.class)
            .setSqlSchema("SchemaName1")
            .setSqlEscapeAll(true);

        final CacheConfiguration<Integer, Fact> cfgEsc = cacheConfig("escapedSchema", true, Integer.class, Fact.class)
            .setSqlSchema("\"SchemaName2\"")
            .setSqlEscapeAll(true);

        escapeCheckSchemaName(ignite(0).createCache(cfg), log, cfg.getSqlSchema());

        escapeCheckSchemaName(ignite(0).createCache(cfgEsc), log, "SchemaName2");

        ignite(0).destroyCache(cfg.getName());
        ignite(0).destroyCache(cfgEsc.getName());
    }

    /**
     * Executes query with and without escaped schema name.
     * @param cache cache for querying
     * @param log logger for assertThrows
     * @param schemaName - schema name without quotes for testing
     */
    private static void escapeCheckSchemaName(final IgniteCache<Integer, Fact> cache, IgniteLogger log, String schemaName) {
        final SqlFieldsQuery qryWrong = new SqlFieldsQuery("select f.id, f.name " +
            "from " + schemaName.toUpperCase() + ".Fact f");

        cache.put(1, new Fact(1, "cacheInsensitive"));

        GridTestUtils.assertThrows(log, new Callable<Object>() {
            @Override public Object call() throws Exception {
                cache.query(qryWrong);
                return null;
            }
        }, CacheException.class, "Failed to parse query");

        SqlFieldsQuery qryCorrect = new SqlFieldsQuery("select f.\"id\", f.\"name\" " +
            "from \""+schemaName+"\".\"Fact\" f");

        for ( List<?> row : cache.query(qryCorrect)) {
            assertEquals(2, row.size());
            assertEquals(1, row.get(0));
            assertEquals("cacheInsensitive", row.get(1));
        }
    }

    // TODO add tests with dynamic cache unregistration, after resolve of https://issues.apache.org/jira/browse/IGNITE-1094

    /** Test class as query entity */
    private static class Fact {
        /** Primary key. */
        @QuerySqlField
        private int id;

        @QuerySqlField
        private String name;

        /**
         * Constructs a fact.
         *
         * @param id fact ID.
         * @param name fact name.
         */
        Fact(int id, String name) {
            this.id = id;
            this.name = name;
        }

        /**
         * Gets fact ID.
         *
         * @return fact ID.
         */
        public int getId() {
            return id;
        }

        /**
         * Gets fact name.
         *
         * @return Fact name.
         */
        public String getName() {
            return name;
        }
    }
}
