/*
 * 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.util.ArrayList;
import java.util.List;
import java.util.concurrent.CyclicBarrier;
import java.util.concurrent.atomic.AtomicInteger;
import javax.cache.expiry.CreatedExpiryPolicy;
import javax.cache.expiry.Duration;
import javax.cache.expiry.ExpiryPolicy;
import org.apache.ignite.Ignite;
import org.apache.ignite.IgniteCache;
import org.apache.ignite.IgniteException;
import org.apache.ignite.cache.CacheMode;
import org.apache.ignite.configuration.CacheConfiguration;
import org.apache.ignite.configuration.IgniteConfiguration;
import org.apache.ignite.events.Event;
import org.apache.ignite.events.EventType;
import org.apache.ignite.lang.IgnitePredicate;
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;
import org.eclipse.jetty.util.BlockingArrayQueue;

import static java.util.concurrent.TimeUnit.MILLISECONDS;
import static java.util.concurrent.TimeUnit.SECONDS;

/**
 *
 */
public class GridCacheTtlManagerNotificationTest extends GridCommonAbstractTest {
    /** Count of caches in multi caches test. */
    private static final int CACHES_CNT = 10;

    /** Prefix for cache name fir multi caches test. */
    private static final String CACHE_PREFIX = "cache-";

    /** IP finder. */
    private static final TcpDiscoveryIpFinder IP_FINDER = new TcpDiscoveryVmIpFinder(true);

    /** Test cache mode. */
    protected CacheMode cacheMode;

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

        TcpDiscoverySpi discoSpi = new TcpDiscoverySpi();

        discoSpi.setIpFinder(IP_FINDER);

        cfg.setDiscoverySpi(discoSpi);

        CacheConfiguration[] ccfgs = new CacheConfiguration[CACHES_CNT + 1];

        ccfgs[0] = createCacheConfiguration(DEFAULT_CACHE_NAME);

        for (int i = 0; i < CACHES_CNT; i++)
            ccfgs[i + 1] = createCacheConfiguration(CACHE_PREFIX + i);

        cfg.setCacheConfiguration(ccfgs);

        return cfg;
    }

    /**
     * @param name Cache name.
     * @return Cache configuration.
     */
    private CacheConfiguration createCacheConfiguration(String name) {
        CacheConfiguration ccfg = new CacheConfiguration(DEFAULT_CACHE_NAME);

        ccfg.setCacheMode(cacheMode);
        ccfg.setEagerTtl(true);
        ccfg.setName(name);

        return ccfg;
    }

    /**
     * @throws Exception If failed.
     */
    public void testThatNotificationWorkAsExpected() throws Exception {
        try (final Ignite g = startGrid(0)) {
            final BlockingArrayQueue<Event> queue = new BlockingArrayQueue<>();

            g.events().localListen(new IgnitePredicate<Event>() {
                @Override public boolean apply(Event evt) {
                    queue.add(evt);

                    return true;
                }
            }, EventType.EVT_CACHE_OBJECT_EXPIRED);

            final String key = "key";

            IgniteCache<Object, Object> cache = g.cache(DEFAULT_CACHE_NAME);

            ExpiryPolicy plc1 = new CreatedExpiryPolicy(new Duration(MILLISECONDS, 100_000));

            cache.withExpiryPolicy(plc1).put(key + 1, 1);

            Thread.sleep(1_000); // Cleaner should see entry.

            ExpiryPolicy plc2 = new CreatedExpiryPolicy(new Duration(MILLISECONDS, 1000));

            cache.withExpiryPolicy(plc2).put(key + 2, 1);

            assertNotNull(queue.poll(5, SECONDS)); // We should receive event about second entry expiration.
        }
    }

    /**
     * Adds in several threads value to cache with different expiration policy.
     * Waits for expiration of keys with small expiration duration.
     *
     * @throws Exception If failed.
     */
    public void testThatNotificationWorkAsExpectedInMultithreadedMode() throws Exception {
        final CyclicBarrier barrier = new CyclicBarrier(21);
        final AtomicInteger keysRangeGen = new AtomicInteger();
        final AtomicInteger evtCnt = new AtomicInteger();
        final int cnt = 1_000;

        try (final Ignite g = startGrid(0)) {
            final IgniteCache<Object, Object> cache = g.cache(DEFAULT_CACHE_NAME);

            g.events().localListen(new IgnitePredicate<Event>() {
                @Override public boolean apply(Event evt) {
                    evtCnt.incrementAndGet();

                    return true;
                }
            }, EventType.EVT_CACHE_OBJECT_EXPIRED);


            int smallDuration = 2000;

            int threadCnt = 10;

            GridTestUtils.runMultiThreadedAsync(
                new CacheFiller(cache, 100_000, barrier, keysRangeGen, cnt),
                threadCnt, "");

            GridTestUtils.runMultiThreadedAsync(
                new CacheFiller(cache, smallDuration, barrier, keysRangeGen, cnt),
                threadCnt, "");

            barrier.await();

            Thread.sleep(1_000); // Cleaner should see at least one entry.

            barrier.await();

            assertEquals(2 * threadCnt * cnt, cache.size());

            Thread.sleep(2 * smallDuration);

            assertEquals(threadCnt * cnt, cache.size());
            assertEquals(threadCnt * cnt, evtCnt.get());
        }
    }

    /**
     * Adds in several threads value to several caches with different expiration policy.
     * Waits for expiration of keys with small expiration duration.
     *
     * @throws Exception If failed.
     */
    public void testThatNotificationWorkAsExpectedManyCaches() throws Exception {
        final int smallDuration = 4_000;

        final int cnt = 1_000;
        final int cacheCnt = CACHES_CNT;
        final int threadCnt = 2;

        final CyclicBarrier barrier = new CyclicBarrier(2 * threadCnt * cacheCnt + 1);
        final AtomicInteger keysRangeGen = new AtomicInteger();
        final AtomicInteger evtCnt = new AtomicInteger(0);
        final List<IgniteCache<Object, Object>> caches = new ArrayList<>(cacheCnt);

        try (final Ignite g = startGrid(0)) {
            for (int i = 0; i < cacheCnt; i++) {
                IgniteCache<Object, Object> cache = g.cache("cache-" + i);

                caches.add(cache);
            }

            g.events().localListen(new IgnitePredicate<Event>() {
                @Override public boolean apply(Event evt) {
                    evtCnt.incrementAndGet();

                    return true;
                }
            }, EventType.EVT_CACHE_OBJECT_EXPIRED);

            for (int i = 0; i < cacheCnt; i++) {
                GridTestUtils.runMultiThreadedAsync(
                    new CacheFiller(caches.get(i), 100_000, barrier, keysRangeGen, cnt),
                    threadCnt,
                    "put-large-duration");

                GridTestUtils.runMultiThreadedAsync(
                    new CacheFiller(caches.get(i), smallDuration, barrier, keysRangeGen, cnt),
                    threadCnt,
                    "put-small-duration");
            }

            barrier.await();

            Thread.sleep(1_000);

            barrier.await();

            for (int i = 0; i < cacheCnt; i++)
                assertEquals("Unexpected size of " + CACHE_PREFIX + i, 2 * threadCnt * cnt, caches.get(i).size());

            Thread.sleep(2 * smallDuration);

            for (int i = 0; i < cacheCnt; i++)
                assertEquals("Unexpected size of " + CACHE_PREFIX + i, threadCnt * cnt, caches.get(i).size());

            assertEquals("Unexpected count of expired entries", threadCnt * CACHES_CNT * cnt, evtCnt.get());
        }
    }

    /** */
    private static class CacheFiller implements Runnable {
        /** Barrier. */
        private final CyclicBarrier barrier;

        /** Keys range generator. */
        private final AtomicInteger keysRangeGenerator;

        /** Count. */
        private final int cnt;

        /** Cache. */
        private final IgniteCache<Object, Object> cache;

        /** Expiration duration. */
        private final int expirationDuration;

        /**
         * @param cache Cache.
         * @param expirationDuration Expiration duration.
         * @param barrier Barrier.
         * @param keysRangeGenerator Keys.
         * @param cnt Count.
         */
        CacheFiller(IgniteCache<Object, Object> cache, int expirationDuration, CyclicBarrier barrier,
            AtomicInteger keysRangeGenerator, int cnt) {
            this.expirationDuration = expirationDuration;
            this.barrier = barrier;
            this.keysRangeGenerator = keysRangeGenerator;
            this.cnt = cnt;
            this.cache = cache;
        }

        /** {@inheritDoc} */
        @Override public void run() {
            try {
                barrier.await();

                ExpiryPolicy plc1 = new CreatedExpiryPolicy(new Duration(MILLISECONDS, expirationDuration));

                int keyStart = keysRangeGenerator.getAndIncrement() * cnt;

                for (int i = keyStart; i < keyStart + cnt; i++)
                    cache.withExpiryPolicy(plc1).put("key" + i, 1);

                barrier.await();
            }
            catch (Exception e) {
                throw new IgniteException(e);
            }
        }
    }
}