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

import com.alicp.jetcache.Cache;
import com.alicp.jetcache.anno.CacheConsts;
import com.alicp.jetcache.anno.CacheType;
import com.alicp.jetcache.anno.KeyConvertor;
import com.alicp.jetcache.anno.support.CacheAnnoConfig;
import com.alicp.jetcache.anno.support.CacheContext;
import com.alicp.jetcache.anno.support.GlobalCacheConfig;
import com.alicp.jetcache.embedded.LinkedHashMapCacheBuilder;
import com.alicp.jetcache.support.FastjsonKeyConvertor;
import com.alicp.jetcache.test.support.DynamicQuery;
import com.alicp.jetcache.testsupport.CountClass;
import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;

import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.concurrent.TimeUnit;

/**
 * @author <a href="mailto:[email protected]">huangli</a>
 */
public class CacheHandlerTest {

    private GlobalCacheConfig globalCacheConfig;
    private CacheAnnoConfig cacheAnnoConfig;
    private CacheInvokeConfig cacheInvokeConfig;
    private CountClass count;
    private Cache cache;

    @Before
    public void setup() {
        globalCacheConfig = new GlobalCacheConfig();
        globalCacheConfig.setLocalCacheBuilders(new HashMap<>());
        globalCacheConfig.setRemoteCacheBuilders(new HashMap<>());
        globalCacheConfig.init();
        cache = LinkedHashMapCacheBuilder.createLinkedHashMapCacheBuilder()
                .keyConvertor(FastjsonKeyConvertor.INSTANCE)
                .buildCache();

        cacheAnnoConfig = new CacheAnnoConfig();
        cacheAnnoConfig.setArea(CacheConsts.DEFAULT_AREA);
        cacheAnnoConfig.setName(CacheConsts.UNDEFINED_STRING);
        cacheAnnoConfig.setEnabled(CacheConsts.DEFAULT_ENABLED);
        cacheAnnoConfig.setTimeUnit(TimeUnit.SECONDS);
        cacheAnnoConfig.setExpire(100);
        cacheAnnoConfig.setCacheType(CacheType.REMOTE);
        cacheAnnoConfig.setLocalLimit(CacheConsts.DEFAULT_LOCAL_LIMIT);
        cacheAnnoConfig.setCacheNullValue(CacheConsts.DEFAULT_CACHE_NULL_VALUE);
        cacheAnnoConfig.setCondition(CacheConsts.UNDEFINED_STRING);
        cacheAnnoConfig.setUnless(CacheConsts.UNDEFINED_STRING);
        cacheAnnoConfig.setSerialPolicy(CacheConsts.DEFAULT_SERIAL_POLICY);
        cacheAnnoConfig.setKeyConvertor(KeyConvertor.FASTJSON);


        cacheInvokeConfig = new CacheInvokeConfig();
        cacheInvokeConfig.setCacheAnnoConfig(cacheAnnoConfig);
        count = new CountClass();
    }

    @After
    public void stop() {
        globalCacheConfig.shutdown();
    }

    private CacheInvokeContext createContext(Invoker invoker, Method method, Object[] args) {
        CacheInvokeContext c = globalCacheConfig.getCacheContext().createCacheInvokeContext();
        c.cacheInvokeConfig = cacheInvokeConfig;
        cacheInvokeConfig.setCacheAnnoConfig(cacheAnnoConfig);
        c.invoker = invoker;
        c.method = method;
        c.args = args;
        c.setCacheFunction((n) -> cache);
        return c;
    }

    private Integer invoke(Method method, Object[] params) throws Throwable {
        return (Integer) CacheHandler.invoke(createContext(() -> method.invoke(count, params), method, params));
    }

    // basic test
    @Test
    public void testStaticInvoke1() throws Throwable {
        Method method = CountClass.class.getMethod("count");
        int x1, x2, x3;
        method.invoke(count);

        x1 = invoke(method, null);
        x2 = invoke(method, null);
        x3 = invoke(method, null);
        Assert.assertEquals(x1, x2);
        Assert.assertEquals(x1, x3);

        method = CountClass.class.getMethod("count", int.class);
        int X1, X2, X3, X4;

        X1 = invoke(method, new Object[]{1000});
        X2 = invoke(method, new Object[]{2000});
        X3 = invoke(method, new Object[]{1000});
        X4 = invoke(method, new Object[]{2000});
        Assert.assertEquals(X1, X3);
        Assert.assertEquals(X2, X4);

    }

    // basic test
    @Test
    public void testStaticInvoke2() throws Throwable {
        Method method = CountClass.class.getMethod("count", String.class, int.class);
        int x1, x2, x3, x4, x5, x6;

        x1 = invoke(method, new Object[]{"aaa", 10});
        x2 = invoke(method, new Object[]{"bbb", 100});
        x3 = invoke(method, new Object[]{"ccc", 10});
        x4 = invoke(method, new Object[]{"aaa", 10});
        x5 = invoke(method, new Object[]{"bbb", 100});
        x6 = invoke(method, new Object[]{"ccc", 10});
        Assert.assertEquals(x1, x4);
        Assert.assertEquals(x2, x5);
        Assert.assertEquals(x3, x6);
    }

    // basic test
    @Test
    public void testStaticInvoke3() throws Throwable {
        DynamicQuery q1 = new DynamicQuery();
        DynamicQuery q2 = new DynamicQuery();
        q2.setId(1000);
        DynamicQuery q3 = new DynamicQuery();
        q3.setId(1000);
        q3.setName("N1");
        DynamicQuery q4 = new DynamicQuery();
        q4.setId(1000);
        q4.setName("N2");
        DynamicQuery q5 = new DynamicQuery();
        q5.setId(1000);
        q5.setName("N2");
        q5.setEmail("");
        DynamicQuery q6 = new DynamicQuery();//q6=q4
        q6.setId(1000);
        q6.setName("N2");

        DynamicQuery[] querys = new DynamicQuery[]{q1, q2, q3, q4, q5, q6};
        int[] ps = new int[]{10, 9000000, 10};

        for (DynamicQuery Q1 : querys) {
            for (DynamicQuery Q2 : querys) {
                for (int P1 : ps) {
                    for (int P2 : ps) {
                        if (Q1 == Q2 && P1 == P2) {
                            assertEquals(Q1, P1, Q2, P2);
                        } else if (P1 == P2 && (Q1 == q4 || Q1 == q6) && (Q2 == q4 || Q2 == q6)) {
                            assertEquals(Q1, P1, Q2, P2);
                        } else {
                            assertNotEquals(Q1, P1, Q2, P2);
                        }
                    }
                }
            }
        }
    }

    @Test
    public void testStaticInvokeNull() throws Throwable {
        Method method = CountClass.class.getMethod("countNull");
        Integer x1, x2, x3;

        cacheAnnoConfig.setCacheNullValue(false);
        x1 = invoke(method, null);//null, not cached
        x2 = invoke(method, null);//not null, so cached
        x3 = invoke(method, null);//hit cache
        Assert.assertNull(x1);
        Assert.assertNotNull(x2);
        Assert.assertNotNull(x3);
        Assert.assertEquals(x2, x3);

        setup();
        cacheAnnoConfig.setCacheNullValue(true);
        x1 = invoke(method, null); //null,cached
        x2 = invoke(method, null);
        x3 = invoke(method, null);
        Assert.assertNull(x1);
        Assert.assertNull(x2);
        Assert.assertNull(x3);

        cacheAnnoConfig.setCacheNullValue(false);
        x1 = invoke(method, null);//cached value is null, invoke, cached
        x2 = invoke(method, null);
        x3 = invoke(method, null);
        Assert.assertNotNull(x1);
        Assert.assertNotNull(x2);
        Assert.assertNotNull(x3);
        Assert.assertEquals(x1, x2);
        Assert.assertEquals(x2, x3);
    }

    @Test
    public void testStaticInvokeCondition() throws Throwable {
        Method method = CountClass.class.getMethod("count", int.class);
        int x1, x2;
        cacheAnnoConfig.setCondition("mvel{args[0]>10}");
        cacheInvokeConfig.init();
        x1 = invoke(method, new Object[]{10});
        x2 = invoke(method, new Object[]{10});
        Assert.assertNotEquals(x1, x2);
        x1 = invoke(method, new Object[]{11});
        x2 = invoke(method, new Object[]{11});
        Assert.assertEquals(x1, x2);
    }

    @Test
    public void testStaticInvokeUnless() throws Throwable {
        Method method = CountClass.class.getMethod("count");
        int x1, x2, x3, x4;
        cacheAnnoConfig.setUnless("mvel{result%2==1}");
        cacheInvokeConfig.init();
        x1 = invoke(method, null);//return 0, unless=false, so cached
        x2 = invoke(method, null);//cache hit
        Assert.assertEquals(x1, x2);
        cacheAnnoConfig.setUnless("mvel{result%2==0}");
        cacheInvokeConfig.init();
        x3 = invoke(method, null);//cache hit(0),unless=true,invoke and return 1, and then cached
        x4 = invoke(method, null);//cache hit(1)
        Assert.assertEquals(x3, x4);

        Assert.assertNotEquals(x3, x1);
    }

    @Test
    public void testStaticInvokeUnlessAndNull() throws Throwable {
        Method method = CountClass.class.getMethod("countNull");

        cacheAnnoConfig.setCacheNullValue(false);
        cacheAnnoConfig.setUnless("mvel{result==0}");
        cacheInvokeConfig.init();
        Assert.assertNull(invoke(method, null));//null, not cached
        Assert.assertEquals(0, invoke(method, null).longValue());//0, not cached
        Assert.assertEquals(1, invoke(method, null).longValue());//1, cache
        Assert.assertEquals(1, invoke(method, null).longValue());//cache hit

        cacheAnnoConfig.setUnless("mvel{result==1}");
        cacheInvokeConfig.init();
        Assert.assertEquals(2, invoke(method, null).longValue());
        Assert.assertEquals(2, invoke(method, null).longValue());

        count = new CountClass();
        cacheAnnoConfig.setUnless("mvel{result==2}");
        cacheInvokeConfig.init();
        Assert.assertNull(invoke(method, null));
        Assert.assertEquals(0, invoke(method, null).longValue());//0, cached
        Assert.assertEquals(0, invoke(method, null).longValue());//cache hit

        cacheAnnoConfig.setCacheNullValue(true);
        count = new CountClass();
        cacheAnnoConfig.setUnless("mvel{result==0}");
        cacheInvokeConfig.init();
        Assert.assertNull(invoke(method, null));
        Assert.assertNull(invoke(method, null));
    }

    private void assertEquals(DynamicQuery q1, int p1, DynamicQuery q2, int p2) throws Throwable {
        int x1, x2;
        Method method = CountClass.class.getMethod("count", DynamicQuery.class, int.class);
        x1 = invoke(method, new Object[]{q1, p1});
        x2 = invoke(method, new Object[]{q2, p2});
        Assert.assertEquals(x1, x2);
    }

    private void assertNotEquals(DynamicQuery q1, int p1, DynamicQuery q2, int p2) throws Throwable {
        int x1, x2;
        Method method = CountClass.class.getMethod("count", DynamicQuery.class, int.class);
        x1 = invoke(method, new Object[]{q1, p1});
        x2 = invoke(method, new Object[]{q2, p2});
        Assert.assertNotEquals(x1, x2);
    }

    // test enableCache
    @Test
    public void testStaticInvoke_CacheContext() throws Throwable {
        final Method method = CountClass.class.getMethod("count");
        int x1, x2, x3;
        Invoker invoker = () -> method.invoke(count);

        CacheInvokeContext context = createContext(invoker, method, null);
        cacheAnnoConfig.setEnabled(false);
        x1 = (Integer) CacheHandler.invoke(context);
        context = createContext(invoker, method, null);
        context.cacheInvokeConfig.setCacheAnnoConfig(null);
        x2 = (Integer) CacheHandler.invoke(context);
        context = createContext(invoker, method, null);
        context.cacheInvokeConfig.setCacheAnnoConfig(null);
        x3 = (Integer) CacheHandler.invoke(context);
        Assert.assertTrue(x1 != x2 && x1 != x3 && x2 != x3);

        cacheAnnoConfig.setEnabled(false);
        x1 = invoke(method, null);
        x2 = invoke(method, null);
        x3 = invoke(method, null);
        Assert.assertTrue(x1 != x2 && x1 != x3 && x2 != x3);

        cacheAnnoConfig.setEnabled(false);
        CacheContext.enableCache(() -> {
            try {
                int xx1 = invoke(method, null);
                int xx2 = invoke(method, null);
                int xx3 = invoke(method, null);
                Assert.assertEquals(xx1, xx2);
                Assert.assertEquals(xx1, xx3);
            } catch (Throwable e) {
                Assert.fail();
            }
            return null;
        });

        cacheAnnoConfig.setEnabled(false);
        cacheInvokeConfig.setEnableCacheContext(true);
        x1 = invoke(method, null);
        x2 = invoke(method, null);
        x3 = invoke(method, null);
        Assert.assertEquals(x1, x2);
        Assert.assertEquals(x1, x3);
    }

    @Test
    public void testInvoke1() throws Throwable {
        Method method = CountClass.class.getMethod("count");
        Invoker invoker = () -> method.invoke(count);
        CacheHandler ch = new CacheHandler(count, cacheInvokeConfig, () -> createContext(invoker, method, null), null);
        int x1 = (Integer) ch.invoke(null, method, null);
        int x2 = (Integer) ch.invoke(null, method, null);
        Assert.assertEquals(x1, x2);
    }

    @Test
    public void testInvoke2() throws Throwable {
        Method method = CountClass.class.getMethod("count");
        final CacheInvokeConfig cac = new CacheInvokeConfig();
        cac.setCacheAnnoConfig(cacheAnnoConfig);
        HashMap<String, CacheInvokeConfig> configMap = new HashMap<String, CacheInvokeConfig>() {
            @Override
            public CacheInvokeConfig get(Object key) {
                return cac;
            }
        };
        Invoker invoker = () -> method.invoke(count);
        CacheHandler ch = new CacheHandler(count, configMap, () -> createContext(invoker, method, null), null);

        int x1 = (Integer) ch.invoke(null, method, null);
        int x2 = (Integer) ch.invoke(null, method, null);
        Assert.assertEquals(x1, x2);

        cacheAnnoConfig.setEnabled(false);
        x1 = (Integer) ch.invoke(null, method, null);
        x2 = (Integer) ch.invoke(null, method, null);
        Assert.assertNotEquals(x1, x2);

        cac.setEnableCacheContext(true);
        x1 = (Integer) ch.invoke(null, method, null);
        x2 = (Integer) ch.invoke(null, method, null);
        Assert.assertEquals(x1, x2);
    }

}