package org.redisson;

import static org.assertj.core.api.Assertions.assertThat;
import static org.awaitility.Awaitility.await;

import java.util.AbstractMap;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Map.Entry;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Consumer;

import org.awaitility.Duration;
import org.joor.Reflect;
import org.junit.Assert;
import org.junit.Assume;
import org.junit.Test;
import org.redisson.api.MapOptions;
import org.redisson.api.MapOptions.WriteMode;
import org.redisson.api.RMap;
import org.redisson.api.RMapCache;
import org.redisson.api.RedissonClient;
import org.redisson.api.map.event.EntryCreatedListener;
import org.redisson.api.map.event.EntryEvent;
import org.redisson.api.map.event.EntryExpiredListener;
import org.redisson.api.map.event.EntryRemovedListener;
import org.redisson.api.map.event.EntryUpdatedListener;
import org.redisson.client.codec.Codec;
import org.redisson.client.codec.DoubleCodec;
import org.redisson.client.codec.IntegerCodec;
import org.redisson.client.codec.LongCodec;
import org.redisson.client.codec.StringCodec;
import org.redisson.codec.CompositeCodec;
import org.redisson.config.Config;
import org.redisson.eviction.EvictionScheduler;

public class RedissonMapCacheTest extends BaseMapTest {

    @Test
    public void testDestroy() {
        RMapCache<String, String> cache = redisson.getMapCache("test");
        
        EvictionScheduler evictionScheduler = ((Redisson)redisson).getEvictionScheduler();
        Map<?, ?> map = Reflect.on(evictionScheduler).get("tasks");
        assertThat(map.isEmpty()).isFalse();
        cache.destroy();
        assertThat(map.isEmpty()).isTrue();
    }
    
    @Override
    protected <K, V> RMap<K, V> getMap(String name) {
        return redisson.getMapCache(name);        
    }
    
    @Override
    protected <K, V> RMap<K, V> getMap(String name, Codec codec) {
        return redisson.getMapCache(name, codec);
    }
    
    @Override
    protected <K, V> RMap<K, V> getWriterTestMap(String name, Map<K, V> map) {
        MapOptions<K, V> options = MapOptions.<K, V>defaults().writer(createMapWriter(map));
        return redisson.getMapCache("test", options);        
    }
    
    @Override
    protected <K, V> RMap<K, V> getWriteBehindTestMap(String name, Map<K, V> map) {
        MapOptions<K, V> options = MapOptions.<K, V>defaults()
                                    .writer(createMapWriter(map))
                                    .writeMode(WriteMode.WRITE_BEHIND);
        return redisson.getMapCache("test", options);        
    }
    
    @Override
    protected <K, V> RMap<K, V> getLoaderTestMap(String name, Map<K, V> map) {
        MapOptions<K, V> options = MapOptions.<K, V>defaults().loader(createMapLoader(map));
        return redisson.getMapCache("test", options);        
    }
    
    @Test
    public void testSizeInMemory() {
        Assume.assumeTrue(RedisRunner.getDefaultRedisServerInstance().getRedisVersion().compareTo("4.0.0") > 0);
        
        RMapCache<Integer, Integer> map = redisson.getMapCache("test");
        for (int i = 0; i < 10; i++) {
            map.put(i, i, 5, TimeUnit.SECONDS);
        }
        
        assertThat(map.sizeInMemory()).isGreaterThanOrEqualTo(466);
    }
    
    @Test
    public void testRemainTimeToLive() {
        RMapCache<String, String> map = redisson.getMapCache("test");
        map.put("1", "2", 2, TimeUnit.SECONDS);
        assertThat(map.remainTimeToLive("1")).isBetween(1900L, 2000L);
        map.put("3", "4");
        assertThat(map.remainTimeToLive("3")).isEqualTo(-1);
        assertThat(map.remainTimeToLive("0")).isEqualTo(-2);

        map.put("5", "6", 20, TimeUnit.SECONDS, 10, TimeUnit.SECONDS);
        assertThat(map.remainTimeToLive("1")).isLessThan(9900);
        map.destroy();
    }
    
    @Test
    public void testFastPutTTL() throws InterruptedException {
        RMapCache<SimpleKey, SimpleValue> map = redisson.getMapCache("getAll");
        map.trySetMaxSize(1);
        map.fastPut(new SimpleKey("1"), new SimpleValue("3"), 5, TimeUnit.SECONDS, 0, TimeUnit.SECONDS);
        Thread.sleep(5000);
        assertThat(map.get(new SimpleKey("1"))).isNull();

        map.fastPut(new SimpleKey("1"), new SimpleValue("4"), 5, TimeUnit.SECONDS, 0, TimeUnit.SECONDS);
        Thread.sleep(10000);
        assertThat(map.get(new SimpleKey("1"))).isNull();
    }
    
    @Test
    public void testWriterPutIfAbsentTTL() {
        Map<String, String> store = new HashMap<>();
        RMapCache<String, String> map = (RMapCache<String, String>) getWriterTestMap("test", store);

        map.putIfAbsent("1", "11", 10, TimeUnit.SECONDS);
        map.putIfAbsent("1", "00", 10, TimeUnit.SECONDS);
        map.putIfAbsent("2", "22", 10, TimeUnit.SECONDS);
        
        Map<String, String> expected = new HashMap<>();
        expected.put("1", "11");
        expected.put("2", "22");
        assertThat(store).isEqualTo(expected);
        map.destroy();
    }
    
    @Test
    public void testWriterPutTTL() {
        Map<String, String> store = new HashMap<>();
        RMapCache<String, String> map = (RMapCache<String, String>) getWriterTestMap("test", store);
        
        map.put("1", "11", 10, TimeUnit.SECONDS);
        map.put("2", "22", 10, TimeUnit.SECONDS);
        map.put("3", "33", 10, TimeUnit.SECONDS);
        
        Map<String, String> expected = new HashMap<>();
        expected.put("1", "11");
        expected.put("2", "22");
        expected.put("3", "33");
        assertThat(store).isEqualTo(expected);
        map.destroy();
    }
    
    @Test
    public void testWriterFastPutIfAbsentTTL() {
        Map<String, String> store = new HashMap<>();
        RMapCache<String, String> map = (RMapCache<String, String>) getWriterTestMap("test", store);

        map.fastPutIfAbsent("1", "11", 10, TimeUnit.SECONDS);
        map.fastPutIfAbsent("1", "00", 10, TimeUnit.SECONDS);
        map.fastPutIfAbsent("2", "22", 10, TimeUnit.SECONDS);
        
        Map<String, String> expected = new HashMap<>();
        expected.put("1", "11");
        expected.put("2", "22");
        assertThat(store).isEqualTo(expected);
        map.destroy();
    }
    
    @Test
    public void testWriterFastPutTTL() {
        Map<String, String> store = new HashMap<>();
        RMapCache<String, String> map = (RMapCache<String, String>) getWriterTestMap("test", store);

        map.fastPut("1", "11", 10, TimeUnit.SECONDS);
        map.fastPut("2", "22", 10, TimeUnit.SECONDS);
        map.fastPut("3", "33", 10, TimeUnit.SECONDS);
        
        Map<String, String> expected = new HashMap<>();
        expected.put("1", "11");
        expected.put("2", "22");
        expected.put("3", "33");
        assertThat(store).isEqualTo(expected);
        map.destroy();
    }

    @Test
    public void testExpirationWithMaxSize() throws InterruptedException {
        Config config = new Config();
        config.useSingleServer().setAddress(RedisRunner.getDefaultRedisServerBindAddressAndPort());
        config.setMaxCleanUpDelay(2);
        config.setMinCleanUpDelay(1);
        RedissonClient redisson = Redisson.create(config);

        RMapCache<String, String> map = redisson.getMapCache("test", StringCodec.INSTANCE);
        assertThat(map.trySetMaxSize(2)).isTrue();

        map.put("1", "1", 3, TimeUnit.SECONDS);
        map.put("2", "2", 0, TimeUnit.SECONDS, 3, TimeUnit.SECONDS);
        map.put("3", "3", 3, TimeUnit.SECONDS);
        map.put("4", "4", 0, TimeUnit.SECONDS, 3, TimeUnit.SECONDS);

        Thread.sleep(5000);

        assertThat(map.size()).isZero();

        assertThat(redisson.getKeys().count()).isEqualTo(2);
        redisson.shutdown();
    }

    @Test
    public void testMaxSize() {
        final AtomicInteger maxSize = new AtomicInteger(2);
        Map<String, String> store = new LinkedHashMap<String, String>() {
            @Override
            protected boolean removeEldestEntry(Entry<String, String> eldest) {
                return size() > maxSize.get();
            }
        };
        MapOptions<String, String> options = MapOptions.<String, String>defaults().writer(createMapWriter(store));
        RMapCache<String, String> map = redisson.getMapCache("test", options);
        assertThat(map.trySetMaxSize(maxSize.get())).isTrue();
        assertThat(map.trySetMaxSize(1)).isFalse();

        assertThat(map.fastPutIfAbsent("01", "00")).isTrue();
        assertThat(map.fastPutIfAbsent("02", "00")).isTrue();
        assertThat(map.put("03", "00")).isNull();
        assertThat(map.fastPutIfAbsent("04", "00", 60, TimeUnit.SECONDS)).isTrue();
        assertThat(map.fastPut("1", "11", 60, TimeUnit.SECONDS)).isTrue();
        assertThat(map.size()).isEqualTo(2);
        assertThat(map.fastPut("2", "22", 60, TimeUnit.SECONDS)).isTrue();
        assertThat(map.fastPut("3", "33", 60, TimeUnit.SECONDS)).isTrue();

        assertThat(map.size()).isEqualTo(maxSize.get());

        Map<String, String> expected = new HashMap<>();
        expected.put("2", "22");
        expected.put("3", "33");
        assertThat(store).isEqualTo(expected);
        
        assertThat(map.get("2")).isEqualTo("22");
        assertThat(map.get("0")).isNull();
        assertThat(map.putIfAbsent("2", "3")).isEqualTo("22");
        assertThat(map.putIfAbsent("3", "4", 60, TimeUnit.SECONDS, 60, TimeUnit.SECONDS)).isEqualTo("33");
        assertThat(map.containsKey("2")).isTrue();
        assertThat(map.containsKey("0")).isFalse();
        assertThat(map.containsValue("22")).isTrue();
        assertThat(map.containsValue("00")).isFalse();
        assertThat(map.getAll(new HashSet<String>(Arrays.asList("2", "3")))).isEqualTo(expected);
        assertThat(map.remove("2", "33")).isFalse();
        assertThat(map.remove("2", "22")).isTrue();
        assertThat(map.remove("0")).isNull();
        assertThat(map.remove("3")).isEqualTo("33");

        maxSize.set(6);
        map.setMaxSize(maxSize.get());
        assertThat(map.fastPut("01", "00")).isTrue();
        assertThat(map.fastPut("02", "00")).isTrue();
        assertThat(map.fastPut("03", "00")).isTrue();
        assertThat(map.fastPut("04", "00")).isTrue();
        assertThat(map.fastPut("05", "00")).isTrue();
        assertThat(map.fastPut("06", "00")).isTrue();
        assertThat(map.fastPut("07", "00")).isTrue();

        assertThat(map.size()).isEqualTo(maxSize.get());
        assertThat(map.keySet()).containsExactly("02", "03", "04", "05", "06", "07");
        
        map.put("08", "00");
        map.put("09", "00");
        map.put("10", "00");
        map.put("11", "00");
        map.put("12", "00");
        map.put("13", "00");
        map.put("14", "00");
        
        assertThat(map.size()).isEqualTo(maxSize.get());
        assertThat(map.keySet()).containsExactly("09", "10", "11", "12", "13", "14");
        
        map.putIfAbsent("15", "00", 1, TimeUnit.SECONDS);
        map.putIfAbsent("16", "00", 1, TimeUnit.SECONDS);
        map.putIfAbsent("17", "00", 1, TimeUnit.SECONDS);
        map.putIfAbsent("18", "00", 1, TimeUnit.SECONDS);
        map.putIfAbsent("19", "00", 1, TimeUnit.SECONDS);
        map.putIfAbsent("20", "00", 1, TimeUnit.SECONDS);
        map.putIfAbsent("21", "00", 1, TimeUnit.SECONDS);
        
        assertThat(map.size()).isEqualTo(maxSize.get());
        assertThat(map.keySet()).containsExactly("16", "17", "18", "19", "20", "21");

        map.putIfAbsent("22", "00");
        map.putIfAbsent("23", "00");
        map.putIfAbsent("24", "00");
        map.putIfAbsent("25", "00");
        map.putIfAbsent("26", "00");
        map.putIfAbsent("27", "00");
        map.putIfAbsent("28", "00");
        
        assertThat(map.size()).isEqualTo(maxSize.get());
        assertThat(map.keySet()).containsExactly("23", "24", "25", "26", "27", "28");

        map.fastPut("29", "00", 1, TimeUnit.SECONDS);
        map.fastPut("30", "00", 1, TimeUnit.SECONDS);
        map.fastPut("31", "00", 1, TimeUnit.SECONDS);
        map.fastPut("32", "00", 1, TimeUnit.SECONDS);
        map.fastPut("33", "00", 1, TimeUnit.SECONDS);
        map.fastPut("34", "00", 1, TimeUnit.SECONDS);
        map.fastPut("35", "00", 1, TimeUnit.SECONDS);
        
        assertThat(map.size()).isEqualTo(maxSize.get());
        assertThat(map.keySet()).containsExactly("30", "31", "32", "33", "34", "35");

        map.put("36", "00", 1, TimeUnit.SECONDS);
        map.put("37", "00", 1, TimeUnit.SECONDS);
        map.put("38", "00", 1, TimeUnit.SECONDS);
        map.put("39", "00", 1, TimeUnit.SECONDS);
        map.put("40", "00", 1, TimeUnit.SECONDS);
        map.put("41", "00", 1, TimeUnit.SECONDS);
        map.put("42", "00", 1, TimeUnit.SECONDS);
        
        assertThat(map.size()).isEqualTo(maxSize.get());
        assertThat(map.keySet()).containsExactly("37", "38", "39", "40", "41", "42");

        map.fastPutIfAbsent("43", "00");
        map.fastPutIfAbsent("44", "00");
        map.fastPutIfAbsent("45", "00");
        map.fastPutIfAbsent("46", "00");
        map.fastPutIfAbsent("47", "00");
        map.fastPutIfAbsent("48", "00");
        map.fastPutIfAbsent("49", "00");
        
        assertThat(map.size()).isEqualTo(maxSize.get());
        assertThat(map.keySet()).containsExactly("44", "45", "46", "47", "48", "49");

        map.fastPutIfAbsent("50", "00", 1, TimeUnit.SECONDS);
        map.fastPutIfAbsent("51", "00", 1, TimeUnit.SECONDS);
        map.fastPutIfAbsent("52", "00", 1, TimeUnit.SECONDS);
        map.fastPutIfAbsent("53", "00", 1, TimeUnit.SECONDS);
        map.fastPutIfAbsent("54", "00", 1, TimeUnit.SECONDS);
        map.fastPutIfAbsent("55", "00", 1, TimeUnit.SECONDS);
        map.fastPutIfAbsent("56", "00", 1, TimeUnit.SECONDS);
        
        assertThat(map.size()).isEqualTo(maxSize.get());
        assertThat(map.keySet()).containsExactly("51", "52", "53", "54", "55", "56");

        Map<String, String> newMap = new LinkedHashMap<>();
        newMap.put("57", "00");
        newMap.put("58", "00");
        newMap.put("59", "00");
        newMap.put("60", "00");
        newMap.put("61", "00");
        newMap.put("62", "00");
        newMap.put("63", "00");
        map.putAll(newMap);
        
        assertThat(map.size()).isEqualTo(maxSize.get());
        assertThat(map.keySet()).containsExactly("58", "59", "60", "61", "62", "63");
        map.destroy();

    }    
    
    @Test
    public void testCacheValues() {
        final RMapCache<String, String> map = redisson.getMapCache("testRMapCacheValues");
        map.put("1234", "5678", 0, TimeUnit.MINUTES, 60, TimeUnit.MINUTES);
        assertThat(map.values()).containsOnly("5678");
        map.destroy();
    }    

    @Test
    public void testGetAllTTL() throws InterruptedException {
        RMapCache<Integer, Integer> map = redisson.getMapCache("getAll");
        map.put(1, 100);
        map.put(2, 200, 1, TimeUnit.SECONDS);
        map.put(3, 300, 1, TimeUnit.SECONDS, 1, TimeUnit.SECONDS);
        map.put(4, 400);

        Map<Integer, Integer> filtered = map.getAll(new HashSet<Integer>(Arrays.asList(2, 3, 5)));

        Map<Integer, Integer> expectedMap = new HashMap<Integer, Integer>();
        expectedMap.put(2, 200);
        expectedMap.put(3, 300);
        Assert.assertEquals(expectedMap, filtered);

        Thread.sleep(1000);

        Map<Integer, Integer> filteredAgain = map.getAll(new HashSet<Integer>(Arrays.asList(2, 3, 5)));
        Assert.assertTrue(filteredAgain.isEmpty());
        map.destroy();
    }

    @Test
    public void testGetAllWithStringKeys() {
        RMapCache<String, Integer> map = redisson.getMapCache("getAllStrings");
        map.put("A", 100);
        map.put("B", 200);
        map.put("C", 300);
        map.put("D", 400);

        Map<String, Integer> filtered = map.getAll(new HashSet<String>(Arrays.asList("B", "C", "E")));

        Map<String, Integer> expectedMap = new HashMap<String, Integer>();
        expectedMap.put("B", 200);
        expectedMap.put("C", 300);
        Assert.assertEquals(expectedMap, filtered);
        map.destroy();
    }

    @Test
    public void testExpiredIterator() throws InterruptedException {
        RMapCache<String, String> cache = redisson.getMapCache("simple");
        cache.put("0", "8");
        cache.put("1", "6", 1, TimeUnit.SECONDS);
        cache.put("2", "4", 3, TimeUnit.SECONDS);
        cache.put("3", "2", 4, TimeUnit.SECONDS);
        cache.put("4", "4", 1, TimeUnit.SECONDS);

        Thread.sleep(1000);

        assertThat(cache.keySet()).containsOnly("0", "2", "3");
        cache.destroy();
    }

    @Test
    public void testExpire() throws InterruptedException {
        RMapCache<String, String> cache = redisson.getMapCache("simple");
        cache.put("0", "8", 1, TimeUnit.SECONDS);

        cache.expire(100, TimeUnit.MILLISECONDS);

        Thread.sleep(500);

        Assert.assertEquals(0, cache.size());
        cache.destroy();
    }

    @Test
    public void testExpireAt() throws InterruptedException {
        RMapCache<String, String> cache = redisson.getMapCache("simple");
        cache.put("0", "8", 1, TimeUnit.SECONDS);

        cache.expireAt(System.currentTimeMillis() + 100);

        Thread.sleep(500);

        Assert.assertEquals(0, cache.size());
        cache.destroy();
    }
    
    @Test
    public void testClearExpire() throws InterruptedException {
        RMapCache<String, String> cache = redisson.getMapCache("simple");
        cache.put("0", "8", 1, TimeUnit.SECONDS);

        cache.expireAt(System.currentTimeMillis() + 100);

        cache.clearExpire();

        Thread.sleep(500);

        Assert.assertEquals(1, cache.size());
        cache.destroy();
    }

    @Test
    public void testEntrySet() throws InterruptedException {
        RMapCache<Integer, String> map = redisson.getMapCache("simple12");
        map.put(1, "12");
        map.put(2, "33", 1, TimeUnit.SECONDS);
        map.put(3, "43");

        Map<Integer, String> expected = new HashMap<>();
        map.put(1, "12");
        map.put(3, "43");
        
        assertThat(map.entrySet()).containsAll(expected.entrySet());
        assertThat(map).hasSize(3);
        map.destroy();
    }
    
    @Test
    public void testKeySet() throws InterruptedException {
        RMapCache<SimpleKey, SimpleValue> map = redisson.getMapCache("simple03");
        map.put(new SimpleKey("33"), new SimpleValue("44"), 1, TimeUnit.SECONDS);
        map.put(new SimpleKey("1"), new SimpleValue("2"));

        Assert.assertTrue(map.keySet().contains(new SimpleKey("33")));
        Assert.assertFalse(map.keySet().contains(new SimpleKey("44")));
        Assert.assertTrue(map.keySet().contains(new SimpleKey("1")));

        Thread.sleep(1000);

        Assert.assertFalse(map.keySet().contains(new SimpleKey("33")));
        Assert.assertFalse(map.keySet().contains(new SimpleKey("44")));
        Assert.assertTrue(map.keySet().contains(new SimpleKey("1")));
        map.destroy();
    }

    @Test
    public void testValues() throws InterruptedException {
        RMapCache<SimpleKey, SimpleValue> map = redisson.getMapCache("simple05");
        map.put(new SimpleKey("33"), new SimpleValue("44"), 1, TimeUnit.SECONDS);
        map.put(new SimpleKey("1"), new SimpleValue("2"));

        Assert.assertTrue(map.values().contains(new SimpleValue("44")));
        Assert.assertFalse(map.values().contains(new SimpleValue("33")));
        Assert.assertTrue(map.values().contains(new SimpleValue("2")));

        Thread.sleep(1000);

        Assert.assertFalse(map.values().contains(new SimpleValue("44")));
        Assert.assertFalse(map.values().contains(new SimpleValue("33")));
        Assert.assertTrue(map.values().contains(new SimpleValue("2")));
        map.destroy();
    }

    @Test
    public void testKeySetByPatternTTL() {
        RMapCache<String, String> map = redisson.getMapCache("simple", StringCodec.INSTANCE);
        map.put("10", "100");
        map.put("20", "200", 1, TimeUnit.MINUTES);
        map.put("30", "300");

        assertThat(map.keySet("?0")).containsExactly("10", "20", "30");
        assertThat(map.keySet("1")).isEmpty();
        assertThat(map.keySet("10")).containsExactly("10");
        map.destroy();
    }

    @Test
    public void testValuesByPatternTTL() {
        RMapCache<String, String> map = redisson.getMapCache("simple", StringCodec.INSTANCE);
        map.put("10", "100");
        map.put("20", "200", 1, TimeUnit.MINUTES);
        map.put("30", "300");

        assertThat(map.values("?0")).containsExactly("100", "200", "300");
        assertThat(map.values("1")).isEmpty();
        assertThat(map.values("10")).containsExactly("100");
        map.destroy();
    }

    @Test
    public void testEntrySetByPatternTTL() {
        RMapCache<String, String> map = redisson.getMapCache("simple", StringCodec.INSTANCE);
        map.put("10", "100");
        map.put("20", "200", 1, TimeUnit.MINUTES);
        map.put("30", "300");

        assertThat(map.entrySet("?0")).containsExactly(new AbstractMap.SimpleEntry("10", "100"), new AbstractMap.SimpleEntry("20", "200"), new AbstractMap.SimpleEntry("30", "300"));
        assertThat(map.entrySet("1")).isEmpty();
        assertThat(map.entrySet("10")).containsExactly(new AbstractMap.SimpleEntry("10", "100"));
        map.destroy();
    }

    
    @Test
    public void testContainsValueTTL() throws InterruptedException {
        RMapCache<SimpleKey, SimpleValue> map = redisson.getMapCache("simple01");
        Assert.assertFalse(map.containsValue(new SimpleValue("34")));
        map.put(new SimpleKey("33"), new SimpleValue("44"), 1, TimeUnit.SECONDS);

        Assert.assertTrue(map.containsValue(new SimpleValue("44")));
        Assert.assertFalse(map.containsValue(new SimpleValue("34")));

        Thread.sleep(1000);

        Assert.assertFalse(map.containsValue(new SimpleValue("44")));
        map.destroy();
    }

    @Test
    public void testContainsKeyTTL() throws InterruptedException {
        RMapCache<SimpleKey, SimpleValue> map = redisson.getMapCache("simple30");
        map.put(new SimpleKey("33"), new SimpleValue("44"), 1, TimeUnit.SECONDS);

        Assert.assertTrue(map.containsKey(new SimpleKey("33")));
        Assert.assertFalse(map.containsKey(new SimpleKey("34")));

        Thread.sleep(1000);

        Assert.assertFalse(map.containsKey(new SimpleKey("33")));
        map.destroy();
    }

    @Test
    public void testRemoveValueTTL() throws InterruptedException {
        RMapCache<SimpleKey, SimpleValue> map = redisson.getMapCache("simple");
        map.put(new SimpleKey("1"), new SimpleValue("2"), 1, TimeUnit.SECONDS);

        boolean res = map.remove(new SimpleKey("1"), new SimpleValue("2"));
        Assert.assertTrue(res);

        SimpleValue val1 = map.get(new SimpleKey("1"));
        Assert.assertNull(val1);

        Assert.assertEquals(0, map.size());
        
        map.put(new SimpleKey("3"), new SimpleValue("4"), 1, TimeUnit.SECONDS);

        Thread.sleep(1000);
        
        assertThat(map.remove(new SimpleKey("3"), new SimpleValue("4"))).isFalse();

        assertThat(map.get(new SimpleKey("3"))).isNull();
        map.destroy();
    }
    
    @Test
    public void testRemoveValueFail() {
        RMapCache<SimpleKey, SimpleValue> map = redisson.getMapCache("simple");
        map.put(new SimpleKey("1"), new SimpleValue("2"));

        boolean res = map.remove(new SimpleKey("2"), new SimpleValue("1"));
        Assert.assertFalse(res);

        boolean res1 = map.remove(new SimpleKey("1"), new SimpleValue("3"));
        Assert.assertFalse(res1);

        SimpleValue val1 = map.get(new SimpleKey("1"));
        Assert.assertEquals("2", val1.getValue());
        map.destroy();
    }


    @Test
    public void testReplaceOldValueFail() {
        RMapCache<SimpleKey, SimpleValue> map = redisson.getMapCache("simple");
        map.put(new SimpleKey("1"), new SimpleValue("2"));

        boolean res = map.replace(new SimpleKey("1"), new SimpleValue("43"), new SimpleValue("31"));
        Assert.assertFalse(res);

        SimpleValue val1 = map.get(new SimpleKey("1"));
        Assert.assertEquals("2", val1.getValue());
        map.destroy();
    }

    @Test
    public void testReplaceOldValueSuccess() {
        RMapCache<SimpleKey, SimpleValue> map = redisson.getMapCache("simple");
        map.put(new SimpleKey("1"), new SimpleValue("2"));

        boolean res = map.replace(new SimpleKey("1"), new SimpleValue("2"), new SimpleValue("3"));
        Assert.assertTrue(res);

        boolean res1 = map.replace(new SimpleKey("1"), new SimpleValue("2"), new SimpleValue("3"));
        Assert.assertFalse(res1);

        SimpleValue val1 = map.get(new SimpleKey("1"));
        Assert.assertEquals("3", val1.getValue());
        map.destroy();
    }

    @Test
    public void testReplaceValueTTL() throws InterruptedException {
        RMapCache<SimpleKey, SimpleValue> map = redisson.getMapCache("simple");
        map.put(new SimpleKey("1"), new SimpleValue("2"), 1, TimeUnit.SECONDS);

        Thread.sleep(1000);
        
        SimpleValue res = map.replace(new SimpleKey("1"), new SimpleValue("3"));
        assertThat(res).isNull();

        SimpleValue val1 = map.get(new SimpleKey("1"));
        assertThat(val1).isNull();
        map.destroy();
    }

    @Test
    public void testReplaceValueTTLIdleUpdate() throws InterruptedException {
        RMapCache<SimpleKey, SimpleValue> map = null;
		SimpleValue val1;
		try {
			map = redisson.getMapCache("simple");
			map.put(new SimpleKey("1"), new SimpleValue("2"), 2, TimeUnit.SECONDS, 1, TimeUnit.SECONDS);

			Thread.sleep(750);
		
			// update value, would like idle timeout to be refreshed
			SimpleValue res = map.replace(new SimpleKey("1"), new SimpleValue("3"));
			assertThat(res).isNotNull();

			Thread.sleep(750);

			// if idle timeout has been updated val1 will be not be null, else it will be null
			val1 = map.get(new SimpleKey("1"));
			assertThat(val1).isNotNull(); 

			Thread.sleep(750);
			
			// val1 will have expired due to TTL
			val1 = map.get(new SimpleKey("1"));
			assertThat(val1).isNull();

		} catch (Exception e) {
			e.printStackTrace();
		} finally {
	        map.remove(new SimpleKey("1"));
		}
    }

    @Test
    public void testScheduler() throws InterruptedException {
        RMapCache<SimpleKey, SimpleValue> map = redisson.getMapCache("simple3");
        Assert.assertNull(map.get(new SimpleKey("33")));

        map.put(new SimpleKey("33"), new SimpleValue("44"), 5, TimeUnit.SECONDS);
        map.put(new SimpleKey("10"), new SimpleValue("32"), 5, TimeUnit.SECONDS, 2, TimeUnit.SECONDS);
        map.put(new SimpleKey("01"), new SimpleValue("92"), 0, null, 2, TimeUnit.SECONDS);

        Assert.assertEquals(3, map.size());

        Thread.sleep(11000);

        Assert.assertEquals(0, map.size());
        map.destroy();

    }

    @Test
    public void testPutGetTTL() throws InterruptedException {
        RMapCache<SimpleKey, SimpleValue> map = redisson.getMapCache("simple04");
        Assert.assertNull(map.get(new SimpleKey("33")));

        map.put(new SimpleKey("33"), new SimpleValue("44"), 2, TimeUnit.SECONDS);

        SimpleValue val1 = map.get(new SimpleKey("33"));
        Assert.assertEquals("44", val1.getValue());

        Thread.sleep(1000);

        Assert.assertEquals(1, map.size());
        SimpleValue val2 = map.get(new SimpleKey("33"));
        Assert.assertEquals("44", val2.getValue());
        Assert.assertEquals(1, map.size());

        Thread.sleep(1000);

        Assert.assertNull(map.get(new SimpleKey("33")));
        map.destroy();
    }

    @Test
    public void testPutAllGetTTL() throws InterruptedException {
        RMapCache<SimpleKey, SimpleValue> map = redisson.getMapCache("simple06");
        Assert.assertNull(map.get(new SimpleKey("33")));
        Assert.assertNull(map.get(new SimpleKey("55")));

        Map<SimpleKey, SimpleValue> entries = new HashMap<>();
        entries.put(new SimpleKey("33"), new SimpleValue("44"));
        entries.put(new SimpleKey("55"), new SimpleValue("66"));
        map.putAll(entries, 2, TimeUnit.SECONDS);

        SimpleValue val1 = map.get(new SimpleKey("33"));
        Assert.assertEquals("44", val1.getValue());
        SimpleValue val2 = map.get(new SimpleKey("55"));
        Assert.assertEquals("66", val2.getValue());

        Thread.sleep(1000);

        Assert.assertEquals(2, map.size());
        SimpleValue val3 = map.get(new SimpleKey("33"));
        Assert.assertEquals("44", val3.getValue());
        SimpleValue val4 = map.get(new SimpleKey("55"));
        Assert.assertEquals("66", val4.getValue());
        Assert.assertEquals(2, map.size());

        Thread.sleep(1000);

        Assert.assertNull(map.get(new SimpleKey("33")));
        Assert.assertNull(map.get(new SimpleKey("55")));
        map.destroy();
    }
    
    @Test
    public void testPutIfAbsentTTL() throws Exception {
        RMapCache<SimpleKey, SimpleValue> map = redisson.getMapCache("simple");
        SimpleKey key = new SimpleKey("1");
        SimpleValue value = new SimpleValue("2");
        map.put(key, value);
        Assert.assertEquals(value, map.putIfAbsent(key, new SimpleValue("3"), 1, TimeUnit.SECONDS));
        Assert.assertEquals(value, map.get(key));

        map.putIfAbsent(new SimpleKey("4"), new SimpleValue("4"), 1, TimeUnit.SECONDS);
        Assert.assertEquals(new SimpleValue("4"), map.get(new SimpleKey("4")));
        
        Thread.sleep(1000);

        Assert.assertNull(map.get(new SimpleKey("4")));
        
        // this should be passed
        map.putIfAbsent(new SimpleKey("4"), new SimpleValue("4"), 1, TimeUnit.SECONDS);
        Assert.assertEquals(new SimpleValue("4"), map.get(new SimpleKey("4")));
        

        SimpleKey key1 = new SimpleKey("2");
        SimpleValue value1 = new SimpleValue("4");
        Assert.assertNull(map.putIfAbsent(key1, value1, 2, TimeUnit.SECONDS));
        Assert.assertEquals(value1, map.get(key1));
        map.destroy();
    }

    @Test
    public void testFastPutIfAbsentTTL() throws Exception {
        RMapCache<SimpleKey, SimpleValue> map = redisson.getMapCache("simple");
        SimpleKey key = new SimpleKey("1");
        SimpleValue value = new SimpleValue("2");
        map.put(key, value);
        assertThat(map.fastPutIfAbsent(key, new SimpleValue("3"))).isFalse();
        assertThat(map.get(key)).isEqualTo(value);

        SimpleKey key1 = new SimpleKey("2");
        SimpleValue value1 = new SimpleValue("4");
        assertThat(map.fastPutIfAbsent(key1, value1)).isTrue();
        assertThat(map.get(key1)).isEqualTo(value1);
        
        SimpleKey key2 = new SimpleKey("3");
        map.put(key2, new SimpleValue("31"), 500, TimeUnit.MILLISECONDS);
        assertThat(map.fastPutIfAbsent(key2, new SimpleValue("32"))).isFalse();
        
        Thread.sleep(500);
        assertThat(map.fastPutIfAbsent(key2, new SimpleValue("32"))).isTrue();
        assertThat(map.get(key2)).isEqualTo(new SimpleValue("32"));
        map.destroy();

    }

    @Test
    public void testCreatedListener() {
        RMapCache<Integer, Integer> map = redisson.getMapCache("simple");
        
        checkCreatedListener(map, 1, 2, () -> map.put(1, 2));
        checkCreatedListener(map, 10, 2, () -> map.put(10, 2, 2, TimeUnit.SECONDS));
        checkCreatedListener(map, 2, 5, () -> map.fastPut(2, 5));
        checkCreatedListener(map, 13, 2, () -> map.fastPut(13, 2, 2, TimeUnit.SECONDS));
        checkCreatedListener(map, 3, 2, () -> map.putIfAbsent(3, 2));
        checkCreatedListener(map, 14, 2, () -> map.putIfAbsent(14, 2, 2, TimeUnit.SECONDS));
        checkCreatedListener(map, 4, 1, () -> map.fastPutIfAbsent(4, 1));
        checkCreatedListener(map, 15, 2, () -> map.fastPutIfAbsent(15, 2, 2, TimeUnit.SECONDS));
        map.destroy();
        
        RMapCache<Integer, Integer> map2 = redisson.getMapCache("simple3", new CompositeCodec(redisson.getConfig().getCodec(), IntegerCodec.INSTANCE));
        checkCreatedListener(map2, 5, 10, () -> map2.addAndGet(5, 10));
        map2.destroy();
    }

    private void checkCreatedListener(RMapCache<Integer, Integer> map, Integer key, Integer value, Runnable runnable) {
        AtomicBoolean ref = new AtomicBoolean();
        int createListener1 = map.addListener(new EntryCreatedListener<Integer, Integer>() {

            @Override
            public void onCreated(EntryEvent<Integer, Integer> event) {
                try {
                    assertThat(event.getKey()).isEqualTo(key);
                    assertThat(event.getValue()).isEqualTo(value);
                    
                    if (!ref.compareAndSet(false, true)) {
                        Assert.fail();
                    }
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
            
        });
        runnable.run();

        await().atMost(Duration.ONE_SECOND).untilTrue(ref);
        map.removeListener(createListener1);
        map.destroy();
    }
    
    @Test
    public void testUpdatedListener() {
        RMapCache<Integer, Integer> map = redisson.getMapCache("simple");

        map.put(1, 1);
        checkUpdatedListener(map, 1, 3, 1, () -> map.put(1, 3));
        
        map.put(10, 1);
        checkUpdatedListener(map, 10, 2, 1, () -> map.put(10, 2, 2, TimeUnit.SECONDS));
        
        map.put(2, 1);
        checkUpdatedListener(map, 2, 5, 1, () -> map.fastPut(2, 5));
        
        map.put(13, 1);
        checkUpdatedListener(map, 13, 2, 1, () -> map.fastPut(13, 2, 2, TimeUnit.SECONDS));
        
        map.put(14, 1);
        checkUpdatedListener(map, 14, 2, 1, () -> map.replace(14, 2));
        checkUpdatedListener(map, 14, 3, 2, () -> map.replace(14, 2, 3));
        map.destroy();
        
        RMapCache<Integer, Integer> map2 = redisson.getMapCache("simple2", new CompositeCodec(redisson.getConfig().getCodec(), IntegerCodec.INSTANCE));
        map2.put(5, 1);
        checkUpdatedListener(map2, 5, 4, 1, () -> map2.addAndGet(5, 3));
        map2.destroy();

    }
    
    @Test
    public void testExpiredListener() {
        RMapCache<Integer, Integer> map = redisson.getMapCache("simple");

        checkExpiredListener(map, 10, 2, () -> map.put(10, 2, 2, TimeUnit.SECONDS));
        checkExpiredListener(map, 13, 2, () -> map.fastPut(13, 2, 2, TimeUnit.SECONDS));
        checkExpiredListener(map, 14, 2, () -> map.putIfAbsent(14, 2, 2, TimeUnit.SECONDS));
        checkExpiredListener(map, 15, 2, () -> map.fastPutIfAbsent(15, 2, 2, TimeUnit.SECONDS));
        map.destroy();
    }

    private void checkExpiredListener(RMapCache<Integer, Integer> map, Integer key, Integer value, Runnable runnable) {
        AtomicBoolean ref = new AtomicBoolean();
        int createListener1 = map.addListener(new EntryExpiredListener<Integer, Integer>() {

            @Override
            public void onExpired(EntryEvent<Integer, Integer> event) {
                assertThat(event.getKey()).isEqualTo(key);
                assertThat(event.getValue()).isEqualTo(value);
                
                if (!ref.compareAndSet(false, true)) {
                    Assert.fail();
                }
            }
            
        });
        runnable.run();

        await().atMost(Duration.ONE_MINUTE).untilTrue(ref);
        map.removeListener(createListener1);
    }

    
    private void checkUpdatedListener(RMapCache<Integer, Integer> map, Integer key, Integer value, Integer oldValue, Runnable runnable) {
        AtomicBoolean ref = new AtomicBoolean();
        int createListener1 = map.addListener(new EntryUpdatedListener<Integer, Integer>() {

            @Override
            public void onUpdated(EntryEvent<Integer, Integer> event) {
                assertThat(event.getKey()).isEqualTo(key);
                assertThat(event.getValue()).isEqualTo(value);
                assertThat(event.getOldValue()).isEqualTo(oldValue);
                
                if (!ref.compareAndSet(false, true)) {
                    Assert.fail();
                }
            }
            
        });
        runnable.run();

        await().atMost(Duration.ONE_SECOND).untilTrue(ref);
        map.removeListener(createListener1);
    }

    @Test
    public void testRemovedListener() {
        RMapCache<Integer, Integer> map = redisson.getMapCache("simple");

        map.put(1, 1);
        checkRemovedListener(map, 1, 1, () -> map.remove(1, 1));
        
        map.put(10, 1);
        checkRemovedListener(map, 10, 1, () -> map.remove(10));
        
        map.put(2, 1);
        checkRemovedListener(map, 2, 1, () -> map.fastRemove(2));
        map.destroy();
    }
    
    private void checkRemovedListener(RMapCache<Integer, Integer> map, Integer key, Integer value, Runnable runnable) {
        AtomicBoolean ref = new AtomicBoolean();
        int createListener1 = map.addListener(new EntryRemovedListener<Integer, Integer>() {

            @Override
            public void onRemoved(EntryEvent<Integer, Integer> event) {
                assertThat(event.getKey()).isEqualTo(key);
                assertThat(event.getValue()).isEqualTo(value);
                
                if (!ref.compareAndSet(false, true)) {
                    Assert.fail();
                }
            }
            
        });
        runnable.run();

        await().atMost(Duration.ONE_SECOND).untilTrue(ref);
        map.removeListener(createListener1);
    }

    @Test
    public void testIdle() throws InterruptedException {
        testIdleExpiration(map -> {
            map.put("12", 1, 0, null, 1, TimeUnit.SECONDS);
            map.put("14", 2, 0, null, 2, TimeUnit.SECONDS);
            map.put("15", 3, 0, null, 3, TimeUnit.SECONDS);
        });

        testIdleExpiration(map -> {
            map.fastPut("12", 1, 0, null, 1, TimeUnit.SECONDS);
            map.fastPut("14", 2, 0, null, 2, TimeUnit.SECONDS);
            map.fastPut("15", 3, 0, null, 3, TimeUnit.SECONDS);
        });

        testIdleExpiration(map -> {
            map.putIfAbsent("12", 1, 0, null, 1, TimeUnit.SECONDS);
            map.putIfAbsent("14", 2, 0, null, 2, TimeUnit.SECONDS);
            map.putIfAbsent("15", 3, 0, null, 3, TimeUnit.SECONDS);
        });

        testIdleExpiration(map -> {
            map.fastPutIfAbsent("12", 1, 0, null, 1, TimeUnit.SECONDS);
            map.fastPutIfAbsent("14", 2, 0, null, 2, TimeUnit.SECONDS);
            map.fastPutIfAbsent("15", 3, 0, null, 3, TimeUnit.SECONDS);
        });
    }
    
    @Test
    public void testTTL() throws InterruptedException {
        testTTLExpiration(map -> {
            map.put("12", 1, 1, TimeUnit.SECONDS);
            map.put("14", 2, 2, TimeUnit.SECONDS);
            map.put("15", 3, 3, TimeUnit.SECONDS);
        });

        testTTLExpiration(map -> {
            map.fastPut("12", 1, 1, TimeUnit.SECONDS);
            map.fastPut("14", 2, 2, TimeUnit.SECONDS);
            map.fastPut("15", 3, 3, TimeUnit.SECONDS);
        });

        testTTLExpiration(map -> {
            map.putIfAbsent("12", 1, 1, TimeUnit.SECONDS);
            map.putIfAbsent("14", 2, 2, TimeUnit.SECONDS);
            map.putIfAbsent("15", 3, 3, TimeUnit.SECONDS);
        });

        testTTLExpiration(map -> {
            map.fastPutIfAbsent("12", 1, 1, TimeUnit.SECONDS);
            map.fastPutIfAbsent("14", 2, 2, TimeUnit.SECONDS);
            map.fastPutIfAbsent("15", 3, 3, TimeUnit.SECONDS);
        });
    }

    protected void testIdleExpiration(Consumer<RMapCache<String, Integer>> callback) throws InterruptedException {
        RMapCache<String, Integer> map = redisson.getMapCache("simple");

        callback.accept(map);

        Thread.sleep(1000);
        
        assertThat(map.get("12")).isNull();
        assertThat(map.get("14")).isEqualTo(2);
        assertThat(map.get("15")).isEqualTo(3);
        
        Thread.sleep(2000);
        
        assertThat(map.get("12")).isNull();
        assertThat(map.get("14")).isNull();
        assertThat(map.get("15")).isEqualTo(3);
        
        Thread.sleep(3000);

        assertThat(map.get("12")).isNull();
        assertThat(map.get("14")).isNull();
        assertThat(map.get("15")).isNull();

        
        map.clear();
        map.destroy();
    }
    
    protected void testTTLExpiration(Consumer<RMapCache<String, Integer>> callback) throws InterruptedException {
        RMapCache<String, Integer> map = redisson.getMapCache("simple");

        callback.accept(map);

        Thread.sleep(1000);
        
        assertThat(map.get("12")).isNull();
        assertThat(map.get("14")).isEqualTo(2);
        assertThat(map.get("15")).isEqualTo(3);
        
        Thread.sleep(1000);
        
        assertThat(map.get("12")).isNull();
        assertThat(map.get("14")).isNull();
        assertThat(map.get("15")).isEqualTo(3);
        
        Thread.sleep(1000);

        assertThat(map.get("12")).isNull();
        assertThat(map.get("14")).isNull();
        assertThat(map.get("15")).isNull();

        
        map.clear();
        map.destroy();
    }

    @Test
    public void testExpireOverwrite() throws InterruptedException, ExecutionException {
        RMapCache<String, Integer> map = redisson.getMapCache("simple");
        map.put("123", 3, 1, TimeUnit.SECONDS);

        Thread.sleep(800);

        map.put("123", 3, 1, TimeUnit.SECONDS);

        Thread.sleep(800);
        Assert.assertEquals(3, (int)map.get("123"));

        Thread.sleep(200);

        Assert.assertFalse(map.containsKey("123"));
        map.destroy();
    }

    @Test
    public void testRMapCacheValues() {
        final RMapCache<String, String> map = redisson.getMapCache("testRMapCacheValues");
        map.put("1234", "5678", 1, TimeUnit.MINUTES, 60, TimeUnit.MINUTES);
        assertThat(map.values()).containsOnly("5678");
        map.destroy();
    }

    @Test
    public void testReadAllEntrySet() throws InterruptedException {
        RMapCache<Integer, String> map = redisson.getMapCache("simple12");
        map.put(1, "12");
        map.put(2, "33", 10, TimeUnit.MINUTES, 60, TimeUnit.MINUTES);
        map.put(3, "43");
        
        assertThat(map.readAllEntrySet()).isEqualTo(map.entrySet());
        map.destroy();
    }

    @Test
    public void testReadAllValuesTTL() {
        final RMapCache<String, String> map = redisson.getMapCache("testRMapCacheAllValues");
        map.put("1234", "5678", 1, TimeUnit.MINUTES, 60, TimeUnit.MINUTES);
        assertThat(map.readAllValues()).containsOnly("5678");
        map.destroy();
    }

    @Test
    public void testAddAndGetTTL() {
        RMapCache<String, Object> mapCache = redisson.getMapCache("test_put_if_absent", LongCodec.INSTANCE);
        assertThat(mapCache.putIfAbsent("4", 0L, 10000L, TimeUnit.SECONDS)).isNull();
        assertThat(mapCache.addAndGet("4", 1L)).isEqualTo(1L);
        assertThat(mapCache.putIfAbsent("4", 0L)).isEqualTo(1L);
        Assert.assertEquals(1L, mapCache.get("4"));
        mapCache.destroy();
        mapCache = redisson.getMapCache("test_put_if_absent_1", LongCodec.INSTANCE);
        mapCache.putIfAbsent("4", 0L);
        mapCache.addAndGet("4", 1L);
        mapCache.putIfAbsent("4", 0L);
        Assert.assertEquals(1L, mapCache.get("4"));
        RMap map = redisson.getMap("test_put_if_absent_2", LongCodec.INSTANCE);
        map.putIfAbsent("4", 0L);
        map.addAndGet("4", 1L);
        map.putIfAbsent("4", 0L);
        Assert.assertEquals(1L, map.get("4"));
        RMapCache<String, Object> mapCache1 = redisson.getMapCache("test_put_if_absent_3", DoubleCodec.INSTANCE);
        mapCache1.putIfAbsent("4", 1.23, 10000L, TimeUnit.SECONDS);
        mapCache1.addAndGet("4", 1D);
        Assert.assertEquals(2.23, mapCache1.get("4"));
        mapCache.destroy();
        mapCache1.destroy();
    }

    
    @Test
    public void testFastPutIfAbsentWithTTL() throws Exception {
        RMapCache<SimpleKey, SimpleValue> map = redisson.getMapCache("simpleTTL");
        SimpleKey key = new SimpleKey("1");
        SimpleValue value = new SimpleValue("2");
        map.fastPutIfAbsent(key, value, 1, TimeUnit.SECONDS);
        assertThat(map.fastPutIfAbsent(key, new SimpleValue("3"), 1, TimeUnit.SECONDS)).isFalse();
        assertThat(map.get(key)).isEqualTo(value);
        
        Thread.sleep(1100);
        
        assertThat(map.fastPutIfAbsent(key, new SimpleValue("3"), 1, TimeUnit.SECONDS)).isTrue();
        assertThat(map.get(key)).isEqualTo(new SimpleValue("3"));
        
        assertThat(map.fastPutIfAbsent(key, new SimpleValue("4"), 1, TimeUnit.SECONDS)).isFalse();
        assertThat(map.get(key)).isEqualTo(new SimpleValue("3"));
        
        Thread.sleep(1100);
        assertThat(map.fastPutIfAbsent(key, new SimpleValue("4"), 1, TimeUnit.SECONDS, 500, TimeUnit.MILLISECONDS)).isTrue();
        
        Thread.sleep(550);
        assertThat(map.fastPutIfAbsent(key, new SimpleValue("5"), 1, TimeUnit.SECONDS, 500, TimeUnit.MILLISECONDS)).isTrue();
        map.destroy();

    }
}