/*
 * 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.cache;

import java.io.Externalizable;
import java.util.Collection;
import java.util.UUID;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicLong;
import javax.cache.Cache;
import org.apache.ignite.IgniteCache;
import org.apache.ignite.IgniteCheckedException;
import org.apache.ignite.cache.CachePeekMode;
import org.apache.ignite.cache.CacheRebalanceMode;
import org.apache.ignite.cache.CacheWriteSynchronizationMode;
import org.apache.ignite.cache.query.QueryCursor;
import org.apache.ignite.cache.query.SqlQuery;
import org.apache.ignite.cache.query.TextQuery;
import org.apache.ignite.cache.query.annotations.QuerySqlField;
import org.apache.ignite.cache.query.annotations.QueryTextField;
import org.apache.ignite.configuration.CacheConfiguration;
import org.apache.ignite.configuration.IgniteConfiguration;
import org.apache.ignite.internal.IgniteInternalFuture;
import org.apache.ignite.internal.util.tostring.GridToStringExclude;
import org.apache.ignite.internal.util.typedef.CAX;
import org.apache.ignite.internal.util.typedef.F;
import org.apache.ignite.internal.util.typedef.internal.S;
import org.apache.ignite.testframework.GridTestUtils;
import org.apache.ignite.testframework.junits.common.GridCommonAbstractTest;
import org.junit.Test;

import static org.apache.ignite.cache.CacheAtomicityMode.TRANSACTIONAL;
import static org.apache.ignite.cache.CacheMode.PARTITIONED;

/**
 * Tests for partitioned cache queries.
 */
public class IgniteCachePartitionedQueryMultiThreadedSelfTest extends GridCommonAbstractTest {
    /** */
    private static final boolean TEST_INFO = true;

    /** Number of test grids (nodes). Should not be less than 2. */
    private static final int GRID_CNT = 3;

    /** Don't start grid by default. */
    public IgniteCachePartitionedQueryMultiThreadedSelfTest() {
        super(false);
    }

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

        CacheConfiguration cc = defaultCacheConfiguration();

        cc.setCacheMode(PARTITIONED);

        // Query should be executed without ongoing transactions.
        cc.setWriteSynchronizationMode(CacheWriteSynchronizationMode.FULL_SYNC);
        cc.setBackups(0);
        cc.setRebalanceMode(CacheRebalanceMode.SYNC);
        cc.setAtomicityMode(TRANSACTIONAL);
        cc.setIndexedTypes(
            UUID.class, PersonObj.class
        );

        c.setCacheConfiguration(cc);

        return c;
    }

    /** {@inheritDoc} */
    @Override protected void beforeTestsStarted() throws Exception {
        assert GRID_CNT >= 2 : "Constant GRID_CNT must be greater than or equal to 2.";

        startGridsMultiThreaded(GRID_CNT);
    }

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

        // Clean up all caches.
        for (int i = 0; i < GRID_CNT; i++)
            grid(i).cache(DEFAULT_CACHE_NAME).removeAll();
    }

    /** {@inheritDoc} */
    @Override protected void info(String msg) {
        if (TEST_INFO)
            super.info(msg);
    }

    /**
     * JUnit.
     *
     * @throws Exception If failed.
     */
    @SuppressWarnings({"TooBroadScope"})
    @Test
    public void testLuceneAndSqlMultithreaded() throws Exception {
        // ---------- Test parameters ---------- //
        int luceneThreads = 10;
        int sqlThreads = 10;
        long duration = 10 * 1000;
        final int logMod = 100;

        final PersonObj p1 = new PersonObj("Jon", 1500, "Master");
        final PersonObj p2 = new PersonObj("Jane", 2000, "Master");
        final PersonObj p3 = new PersonObj("Mike", 1800, "Bachelor");
        final PersonObj p4 = new PersonObj("Bob", 1900, "Bachelor");

        final IgniteCache<UUID, PersonObj> cache0 = grid(0).cache(DEFAULT_CACHE_NAME);

        cache0.put(p1.id(), p1);
        cache0.put(p2.id(), p2);
        cache0.put(p3.id(), p3);
        cache0.put(p4.id(), p4);

        assertEquals(4, cache0.localSize(CachePeekMode.ALL));

        assert grid(0).cluster().nodes().size() == GRID_CNT;

        final AtomicBoolean done = new AtomicBoolean();

        final AtomicLong luceneCnt = new AtomicLong();

        // Start lucene query threads.
        IgniteInternalFuture<?> futLucene = GridTestUtils.runMultiThreadedAsync(new CAX() {
            @Override public void applyx() throws IgniteCheckedException {
                while (!done.get()) {
                    QueryCursor<Cache.Entry<UUID, PersonObj>> master =
                        cache0.query(new TextQuery(PersonObj.class, "Master"));

                    Collection<Cache.Entry<UUID, PersonObj>> entries = master.getAll();

                    checkResult(entries, p1, p2);

                    long cnt = luceneCnt.incrementAndGet();

                    if (cnt % logMod == 0)
                        info("Executed LUCENE queries: " + cnt);
                }
            }
        }, luceneThreads, "LUCENE-THREAD");

        final AtomicLong sqlCnt = new AtomicLong();

        // Start sql query threads.
        IgniteInternalFuture<?> futSql = GridTestUtils.runMultiThreadedAsync(new CAX() {
            @Override public void applyx() throws IgniteCheckedException {
                while (!done.get()) {
                    QueryCursor<Cache.Entry<UUID, PersonObj>> bachelors =
                            cache0.query(new SqlQuery(PersonObj.class, "degree = 'Bachelor'"));

                    Collection<Cache.Entry<UUID, PersonObj>> entries = bachelors.getAll();

                    checkResult(entries, p3, p4);

                    long cnt = sqlCnt.incrementAndGet();

                    if (cnt % logMod == 0)
                        info("Executed SQL queries: " + cnt);
                }
            }
        }, sqlThreads, "SQL-THREAD");

        Thread.sleep(duration);

        done.set(true);

        futLucene.get();
        futSql.get();
    }

    /**
     * @param entries Queried result.
     * @param persons Persons that should be in the result.
     */
    private void checkResult(Iterable<Cache.Entry<UUID, PersonObj>> entries, PersonObj... persons) {
        for (Cache.Entry<UUID, PersonObj> entry : entries) {
            assertEquals(entry.getKey(), entry.getValue().id());

            assert F.asList(persons).contains(entry.getValue());
        }
    }

    /** Test class. */
    private static class PersonObj {
        /** */
        @GridToStringExclude
        private UUID id = UUID.randomUUID();

        /** */
        @QuerySqlField
        private String name;

        /** */
        @QuerySqlField
        private int salary;

        /** */
        @QuerySqlField
        @QueryTextField
        private String degree;

        /** Required by {@link Externalizable}. */
        public PersonObj() {
            // No-op.
        }

        /**
         * @param name Name.
         * @param salary Salary.
         * @param degree Degree.
         */
        PersonObj(String name, int salary, String degree) {
            assert name != null;
            assert salary > 0;
            assert degree != null;

            this.name = name;
            this.salary = salary;
            this.degree = degree;
        }

        /** @return Id. */
        UUID id() {
            return id;
        }

        /** @return Name. */
        String name() {
            return name;
        }

        /** @return Salary. */
        double salary() {
            return salary;
        }

        /** @return Degree. */
        String degree() {
            return degree;
        }

        /** {@inheritDoc} */
        @Override public int hashCode() {
            return id.hashCode() + 31 * name.hashCode() + 31 * 31 * salary;
        }

        /** {@inheritDoc} */
        @Override public boolean equals(Object obj) {
            if (obj == this)
                return true;

            if (!(obj instanceof PersonObj))
                return false;

            PersonObj that = (PersonObj)obj;

            return that.id.equals(id) && that.name.equals(name) && that.salary == salary && that.degree.equals(degree);
        }

        /** {@inheritDoc} */
        @Override public String toString() {
            return S.toString(PersonObj.class, this);
        }
    }
}
