/**
 * Created on  13-09-23 17:35
 */
package com.alicp.jetcache.anno.method;

import com.alicp.jetcache.CacheManager;
import com.alicp.jetcache.anno.CacheInvalidate;
import com.alicp.jetcache.anno.CachePenetrationProtect;
import com.alicp.jetcache.anno.CacheRefresh;
import com.alicp.jetcache.anno.CacheType;
import com.alicp.jetcache.anno.CacheUpdate;
import com.alicp.jetcache.anno.Cached;
import com.alicp.jetcache.anno.EnableCache;
import com.alicp.jetcache.anno.support.CacheContext;
import com.alicp.jetcache.anno.support.ConfigProvider;
import com.alicp.jetcache.anno.support.JetCacheBaseBeans;
import com.alicp.jetcache.test.anno.TestUtil;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

import java.util.HashMap;
import java.util.Map;
import java.util.Random;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;

/**
 * @author <a href="mailto:areyouok@gmail.com">huangli</a>
 */
public class ProxyUtilTest {

    private ConfigProvider configProvider;
    private CacheManager cacheManager;

    @BeforeEach
    public void setup() {
        configProvider = TestUtil.createConfigProvider();
        configProvider.init();
        cacheManager = new JetCacheBaseBeans().cacheManager(configProvider);
    }

    @AfterEach
    public void stop() {
        configProvider.shutdown();
    }

    public interface I1 {
        int count();

        int countWithoutCache();
    }

    public class C1 implements I1 {
        int count;

        @Cached
        public int count() {
            return count++;
        }

        public int countWithoutCache() {
            return count++;
        }
    }

    @Test
    //annotation on class
    public void testGetProxyByAnnotation1() {
        I1 c1 = new C1();
        I1 c2 = ProxyUtil.getProxyByAnnotation(c1, configProvider, cacheManager);
        assertNotEquals(c1.count(), c1.count());
        assertNotEquals(c1.countWithoutCache(), c1.countWithoutCache());
        assertEquals(c2.count(), c2.count());
        assertNotEquals(c2.countWithoutCache(), c2.countWithoutCache());
    }

    public interface I2 {
        @Cached
        int count();

        int countWithoutCache();
    }

    public class C2 implements I2 {
        int count;

        public int count() {
            return count++;
        }

        public int countWithoutCache() {
            return count++;
        }
    }

    public class C22 implements I2 {
        int count;

        public int count() {
            return count++;
        }

        public int countWithoutCache() {
            return count++;
        }
    }

    @Test
    //annotation on intface
    public void testGetProxyByAnnotation2() {
        I2 c1 = new C2();
        I2 c2 = ProxyUtil.getProxyByAnnotation(c1, configProvider, cacheManager);

        assertNotEquals(c1.count(), c1.count());
        assertNotEquals(c1.countWithoutCache(), c1.countWithoutCache());
        assertEquals(c2.count(), c2.count());
        assertNotEquals(c2.countWithoutCache(), c2.countWithoutCache());

        I2 c3 = new C22();
        I2 c4 = ProxyUtil.getProxyByAnnotation(c3, configProvider, cacheManager);
        assertEquals(c2.count(), c4.count());
    }

    public interface I3_1 {
        @Cached
        int count();
    }

    public interface I3_2 extends I3_1 {
        int count();

        int countWithoutCache();
    }

    public class C3 implements I3_2 {
        int count;

        public int count() {
            return count++;
        }

        public int countWithoutCache() {
            return count++;
        }

    }

    @Test
    //annotation on super interface
    public void testGetProxyByAnnotation3() {
        I3_2 c1 = new C3();
        I3_2 c2 = ProxyUtil.getProxyByAnnotation(c1, configProvider, cacheManager);
        assertNotEquals(c1.count(), c1.count());
        assertNotEquals(c1.countWithoutCache(), c1.countWithoutCache());
        assertEquals(c2.count(), c2.count());
        assertNotEquals(c2.countWithoutCache(), c2.countWithoutCache());
    }

    public interface I4_1 {
        int count();

        int countWithoutCache();
    }

    public interface I4_2 extends I4_1 {
        @Cached
        int count();
    }

    public class C4 implements I4_2 {
        int count;

        public int count() {
            return count++;
        }

        public int countWithoutCache() {
            return count++;
        }
    }

    @Test
    //with super interface
    public void testGetProxyByAnnotation4() {
        I4_1 c1 = new C4();
        I4_1 c2 = ProxyUtil.getProxyByAnnotation(c1, configProvider, cacheManager);
        assertNotEquals(c1.count(), c1.count());
        assertNotEquals(c1.countWithoutCache(), c1.countWithoutCache());
        assertEquals(c2.count(), c2.count());
        assertNotEquals(c2.countWithoutCache(), c2.countWithoutCache());
    }

    public interface I5 {
        int count();

        int countWithoutCache();
    }

    public class C5 implements I5 {
        int count;

        @Cached(enabled = false)
        public int count() {
            return count++;
        }

        public int countWithoutCache() {
            return count++;
        }
    }

    @Test
    //enabled=false
    public void testGetProxyByAnnotation5() {
        I5 c1 = new C5();
        I5 c2 = ProxyUtil.getProxyByAnnotation(c1, configProvider, cacheManager);
        assertNotEquals(c1.count(), c1.count());
        assertNotEquals(c1.countWithoutCache(), c1.countWithoutCache());
        assertNotEquals(c2.count(), c2.count());
        assertNotEquals(c2.countWithoutCache(), c2.countWithoutCache());
        CacheContext.enableCache(() -> {
            assertNotEquals(c1.count(), c1.count());
            assertNotEquals(c1.countWithoutCache(), c1.countWithoutCache());
            assertEquals(c2.count(), c2.count());
            assertNotEquals(c2.countWithoutCache(), c2.countWithoutCache());
            return null;
        });
    }

    public interface I6 {
        int count();

        int countWithoutCache();
    }

    public class C6 implements I6 {
        int count;

        @EnableCache
        @Cached(enabled = false)
        public int count() {
            return count++;
        }

        public int countWithoutCache() {
            return count++;
        }
    }

    @Test
    //enabled=false+EnableCache
    public void testGetProxyByAnnotation6() {
        I6 c1 = new C6();
        I6 c2 = ProxyUtil.getProxyByAnnotation(c1, configProvider, cacheManager);
        assertNotEquals(c1.count(), c1.count());
        assertNotEquals(c1.countWithoutCache(), c1.countWithoutCache());
        assertEquals(c2.count(), c2.count());
        assertNotEquals(c2.countWithoutCache(), c2.countWithoutCache());
    }

    public interface I7_1 {
        int count();

        int countWithoutCache();
    }

    public class C7_1 implements I7_1 {
        int count;

        @Cached(enabled = false)
        public int count() {
            return count++;
        }

        @Override
        public int countWithoutCache() {
            return count++;
        }
    }

    public interface I7_2 {
        int count();

        int countWithoutCache();
    }

    public class C7_2 implements I7_2 {
        I7_1 service;

        @EnableCache
        public int count() {
            return service.count();
        }

        @EnableCache
        public int countWithoutCache() {
            return service.countWithoutCache();
        }
    }

    @Test
    //enabled=false+EnableCache（enable in caller）
    public void testGetProxyByAnnotation7() {
        I7_1 c1_1 = new C7_1();
        I7_1 c1_2 = ProxyUtil.getProxyByAnnotation(c1_1, configProvider, cacheManager);

        C7_2 c2_1 = new C7_2();
        c2_1.service = c1_2;
        I7_2 c2_2 = ProxyUtil.getProxyByAnnotation(c2_1, configProvider, cacheManager);
        assertNotEquals(c2_1.count(), c2_1.count());
        assertNotEquals(c2_2.countWithoutCache(), c2_2.countWithoutCache());
        assertEquals(c2_2.count(), c2_2.count());
    }

    public interface I8 {
        @Cached(name = "c1", key = "args[0]")
        int count(String id);

        @CacheUpdate(name = "c1", key = "#id", value = "args[1]")
        void update(String id, int value);

        @CacheUpdate(name = "c2", key = "args[0]", value = "args[1]")
        void update2(String id, int value);

        @CacheInvalidate(name = "c1", key = "#id")
        void delete(String id);

        @CacheInvalidate(name = "c2", key = "args[0]")
        void delete2(String id);

        @CacheUpdate(name = "c1", key = "#id", value="#result")
        int randomUpdate(String id);

        @CacheUpdate(name = "c1", key = "#id", value="result")
        int randomUpdate2(String id);
    }

    public class C8 implements I8 {
        int count;
        Map<String, Integer> m = new HashMap<>();

        @Override
        public int count(String id) {
            Integer v = m.get(id);
            if (v == null) {
                v = count++;
            }
            v++;
            m.put(id, v);
            return v;
        }

        @Override
        public void update(String theId, int value) {
            m.put(theId, value);
        }

        @Override
        public void delete(String theId) {
            m.remove(theId);
        }

        @Override
        public void update2(String theId, int value) {
            m.put(theId, value);
        }

        @Override
        public void delete2(String theId) {
            m.remove(theId);
        }

        @Override
        public int randomUpdate(String id) {
            return new Random().nextInt();
        }

        @Override
        public int randomUpdate2(String id) {
            return new Random().nextInt();
        }
    }


    @Test
    // @CacheUpdate and @CacheInvalidate test
    public void testGetProxyByAnnotation8() {
        I8 i8 = new C8();
        I8 i8_proxy = ProxyUtil.getProxyByAnnotation(i8, configProvider, cacheManager);

        int v1 = i8_proxy.count("K1");
        assertEquals(v1, i8_proxy.count("K1"));

        i8_proxy.delete("K1");
        int v2 = i8_proxy.count("K1");
        assertNotEquals(v1, v2);
        i8_proxy.delete2("K1");
        assertEquals(v2, i8_proxy.count("K1"));

        i8_proxy.update("K1", 200);
        assertEquals(200, i8_proxy.count("K1"));
        i8_proxy.update2("K1", 300);
        assertEquals(200, i8_proxy.count("K1"));

        assertEquals(i8_proxy.count("K1"), i8_proxy.count("K1"));
        assertNotEquals(i8_proxy.count("K1"), i8_proxy.count("K2"));

        assertEquals(i8_proxy.randomUpdate("K1"), i8_proxy.count("K1"));
        assertEquals(i8_proxy.randomUpdate2("K1"), i8_proxy.count("K1"));
    }

    public interface I9 {
        @Cached
        @CacheRefresh(refresh = 100, timeUnit = TimeUnit.MILLISECONDS)
        int count();

        @Cached(key = "#a", cacheType = CacheType.BOTH)
        @CacheRefresh(refresh = 100, timeUnit = TimeUnit.MILLISECONDS)
        int count(int a, int b);
    }

    public class C9 implements I9 {
        int count1;
        int count2;

        public int count() {
            return count1++;
        }

        @Override
        public int count(int a, int b) {
            return a + b + count2++;
        }
    }

    @Test
    // refresh test
    public void testGetProxyByAnnotation9() throws Exception {
        I9 beanProxy = ProxyUtil.getProxyByAnnotation(new C9(), configProvider, cacheManager);
        {
            int x1 = beanProxy.count();
            int x2 = beanProxy.count();
            assertEquals(x1, x2);
            int i = 0;
            while (true) { //auto refreshment may take some time to init
                assertTrue(i < 10);
                Thread.sleep(150);
                if (x2 == beanProxy.count()) {
                    i++;
                    continue;
                } else {
                    break;
                }
            }
        }
        {
            int x1 = beanProxy.count(1, 2);
            int x2 = beanProxy.count(1, 200);
            assertEquals(x1, x2);
            Thread.sleep(150);
            assertEquals(x1 + 1, beanProxy.count(1, 400));
        }
    }

    public interface I10 {
        @Cached
        int count1(int p);

        @Cached
        @CachePenetrationProtect
        int count2(int p);
    }

    public class C10 implements I10 {
        AtomicInteger count1 = new AtomicInteger(0);
        AtomicInteger count2 = new AtomicInteger(0);;

        @Override
        public int count1(int p) {
            try {
                Thread.sleep(50);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            return count1.incrementAndGet();
        }

        @Override
        public int count2(int p) {
            try {
                Thread.sleep(50);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            return count2.incrementAndGet();
        }
    }

    @Test
    // protect test
    public void testGetProxyByAnnotation10() throws Exception {
        I10 beanProxy = ProxyUtil.getProxyByAnnotation(new C10(), configProvider, cacheManager);

        // preheat
        beanProxy.count1(1);
        beanProxy.count2(1);

        {
            int[] x = new int[1];
            int[] y = new int[1];
            CountDownLatch countDownLatch = new CountDownLatch(2);
            new Thread(() -> {
                x[0] = beanProxy.count1(2);
                countDownLatch.countDown();
            }).start();
            new Thread(() -> {
                y[0] = beanProxy.count1(2);
                countDownLatch.countDown();
            }).start();
            countDownLatch.await();
            assertNotEquals(x[0], y[0]);
        }
        {
            int[] x = new int[1];
            int[] y = new int[1];
            CountDownLatch countDownLatch = new CountDownLatch(2);
            new Thread(() -> {
                x[0] = beanProxy.count2(2);
                countDownLatch.countDown();
            }).start();
            new Thread(() -> {
                y[0] = beanProxy.count2(2);
                countDownLatch.countDown();
            }).start();
            countDownLatch.await();
            assertEquals(x[0], y[0]);
        }
    }
}
