/*
 * 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.concurrent.TimeUnit;
import javax.cache.configuration.Factory;
import javax.cache.expiry.AccessedExpiryPolicy;
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.IgniteCheckedException;
import org.apache.ignite.cache.CachePeekMode;
import org.apache.ignite.cache.eviction.fifo.FifoEvictionPolicy;
import org.apache.ignite.configuration.CacheConfiguration;
import org.apache.ignite.internal.IgniteEx;
import org.apache.ignite.internal.util.typedef.G;
import org.apache.ignite.internal.util.typedef.PA;
import org.apache.ignite.internal.util.typedef.internal.U;
import org.apache.ignite.testframework.GridTestUtils;
import org.apache.ignite.testframework.MvccFeatureChecker;
import org.apache.ignite.testframework.junits.IgniteCacheConfigVariationsAbstractTest;
import org.junit.Test;

/**
 *
 */
@SuppressWarnings("unchecked")
public class IgniteCacheReadThroughEvictionSelfTest extends IgniteCacheConfigVariationsAbstractTest {
    /** */
    private static final int TIMEOUT = 400;

    /** */
    private static final int KEYS = 100;

    /** {@inheritDoc} */
    @Override public void setUp() throws Exception {
        MvccFeatureChecker.skipIfNotSupported(MvccFeatureChecker.Feature.CACHE_STORE);

        super.setUp();
    }

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

    /**
     * @throws Exception if failed.
     */
    @Test
    public void testReadThroughWithExpirePolicy() throws Exception {
        MvccFeatureChecker.skipIfNotSupported(MvccFeatureChecker.Feature.EXPIRATION);

        Ignite ig = testedGrid();

        CacheConfiguration<Object, Object> cc = variationConfig("expire");

        IgniteCache<Object, Object> cache = ig.createCache(cc);

        try {
            ExpiryPolicy exp = new CreatedExpiryPolicy(new Duration(TimeUnit.MILLISECONDS, TIMEOUT));

            for (int i = 0; i < KEYS; i++)
                cache.withExpiryPolicy(exp).put(key(i), value(i));

            U.sleep(TIMEOUT);

            waitEmpty(cc.getName());

            exp = new AccessedExpiryPolicy(new Duration(TimeUnit.MILLISECONDS, TIMEOUT));

            for (int i = 0; i < KEYS; i++) {
                assertEquals(value(i), cache.get(key(i)));

                cache.withExpiryPolicy(exp).get(key(i));
            }

            U.sleep(TIMEOUT);

            waitEmpty(cc.getName());

            for (int i = 0; i < KEYS; i++)
                assertEquals(value(i), cache.get(key(i)));
        }
        finally {
            destroyCacheSafe(ig, cc.getName());
        }
    }

    /**
     * @throws Exception if failed.
     */
    @Test
    public void testReadThroughExpirePolicyConfigured() throws Exception {
        MvccFeatureChecker.skipIfNotSupported(MvccFeatureChecker.Feature.EXPIRATION);

        Ignite ig = testedGrid();

        CacheConfiguration<Object, Object> cc = variationConfig("expireConfig");

        cc.setExpiryPolicyFactory(new ExpirePolicyFactory());

        IgniteCache<Object, Object> cache = ig.createCache(cc);

        try {
            for (int i = 0; i < KEYS; i++)
                cache.put(key(i), value(i));

            U.sleep(TIMEOUT);

            waitEmpty(cc.getName());

            for (int i = 0; i < KEYS; i++) {
                assertEquals(value(i), cache.get(key(i)));

                // Access expiry.
                cache.get(key(i));
            }

            U.sleep(TIMEOUT);

            waitEmpty(cc.getName());

            for (int i = 0; i < KEYS; i++)
                assertEquals(value(i), cache.get(key(i)));

            for (int i = 0; i < KEYS; i++) {
                assertEquals(value(i), cache.get(key(i)));

                // Update expiry.
                cache.put(key(i), value(i));
            }

            U.sleep(TIMEOUT);

            waitEmpty(cc.getName());

            for (int i = 0; i < KEYS; i++)
                assertEquals(value(i), cache.get(key(i)));
        }
        finally {
            destroyCacheSafe(ig, cc.getName());
        }
    }

    /**
     * @throws Exception if failed.
     */
    @Test
    public void testReadThroughEvictionPolicy() throws Exception {
        MvccFeatureChecker.skipIfNotSupported(MvccFeatureChecker.Feature.EVICTION);

        Ignite ig = testedGrid();

        CacheConfiguration<Object, Object> cc = variationConfig("eviction");

        cc.setEvictionPolicy(new FifoEvictionPolicy(1));
        cc.setOnheapCacheEnabled(true);

        final IgniteCache<Object, Object> cache = ig.createCache(cc);

        try {
            for (int i = 0; i < KEYS; i++)
                cache.put(key(i), value(i));

            assertTrue(GridTestUtils.waitForCondition(new PA() {
                @Override public boolean apply() {
                    int size = cache.size(CachePeekMode.ONHEAP);
                    int offheapSize = cache.size(CachePeekMode.OFFHEAP);

                    System.out.println("Cache [onHeap=" + size + ", offHeap=" + offheapSize + ']');

                    return size <= testsCfg.gridCount();
                }
            }, 30_000));

            for (int i = 0; i < KEYS; i++)
                assertEquals(value(i), cache.get(key(i)));
        }
        finally {
            destroyCacheSafe(ig, cc.getName());
        }
    }

    /**
     * @throws Exception if failed.
     */
    @Test
    public void testReadThroughSkipStore() throws Exception {
        Ignite ig = testedGrid();

        CacheConfiguration<Object, Object> cc = variationConfig("skipStore");

        final IgniteCache<Object, Object> cache = ig.createCache(cc);

        try {
            for (int i = 0; i < KEYS; i++) {
                cache.put(key(i), value(i));

                cache.withSkipStore().remove(key(i));
            }

            waitEmpty(cc.getName());

            for (int i = 0; i < KEYS; i++)
                assertEquals(value(i), cache.get(key(i)));
        }
        finally {
            destroyCacheSafe(ig, cc.getName());
        }
    }

    /**
     * @return Variation test configuration.
     */
    private CacheConfiguration<Object, Object> variationConfig(String suffix) {
        CacheConfiguration ccfg = testsCfg.configurationFactory().cacheConfiguration(getTestIgniteInstanceName(testedNodeIdx));

        ccfg.setName(cacheName() + "_" + suffix);

        return ccfg;
    }

    /**
     * @throws Exception if failed.
     */
    private void waitEmpty(final String name) throws Exception {
        boolean success = GridTestUtils.waitForCondition(new PA() {
            @Override public boolean apply() {
                for (Ignite g : G.allGrids()) {
                    GridCacheAdapter<Object, Object> cache = ((IgniteEx)g).context().cache().internalCache(name);

                    if (cache == null)
                        return true;

                    if (!cache.isEmpty())
                        return false;
                }

                return true;
            }
        }, getTestTimeout());

        assertTrue("Failed to wait for the cache to be empty", success);
    }

    /**
     * @param ig Ignite.
     * @param cacheName Cache name to destroy.
     * @throws IgniteCheckedException If failed.
     */
    private void destroyCacheSafe(Ignite ig, final String cacheName) throws IgniteCheckedException {
        ig.destroyCache(cacheName);

        GridTestUtils.waitForCondition(new PA() {
            @Override public boolean apply() {
                for (Ignite g : G.allGrids()) {
                    IgniteInternalCache<Object, Object> cache = ((IgniteEx)g).context().cache().cache(cacheName);

                    if (cache != null)
                        return false;
                }

                return true;
            }
        }, getTestTimeout());
    }

    /**
     *
     */
    private static class ExpirePolicyFactory implements Factory<ExpiryPolicy> {
        /** {@inheritDoc} */
        @Override public ExpiryPolicy create() {
            return new ExpiryPolicy() {
                @Override public Duration getExpiryForCreation() {
                    return new Duration(TimeUnit.MILLISECONDS, TIMEOUT);
                }

                /** {@inheritDoc} */
                @Override public Duration getExpiryForAccess() {
                    return new Duration(TimeUnit.MILLISECONDS, TIMEOUT);
                }

                /** {@inheritDoc} */
                @Override public Duration getExpiryForUpdate() {
                    return new Duration(TimeUnit.MILLISECONDS, TIMEOUT);
                }
            };
        }
    }
}
