/*
 * Copyright (C) 2010 eXo Platform SAS.
 *
 * This is free software; you can redistribute it and/or modify it
 * under the terms of the GNU Lesser General Public License as
 * published by the Free Software Foundation; either version 2.1 of
 * the License, or (at your option) any later version.
 *
 * This software is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this software; if not, write to the Free
 * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
 * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
 */
package org.exoplatform.services.cache.impl.infinispan;

import junit.framework.TestCase;

import org.exoplatform.container.ExoContainerContext;
import org.exoplatform.container.PortalContainer;
import org.exoplatform.container.configuration.ConfigurationManager;
import org.exoplatform.services.cache.CacheListener;
import org.exoplatform.services.cache.CacheListenerContext;
import org.exoplatform.services.cache.CacheService;
import org.exoplatform.services.cache.CachedObjectSelector;
import org.exoplatform.services.cache.ExoCache;
import org.exoplatform.services.cache.ExoCacheConfig;
import org.exoplatform.services.cache.ExoCacheFactory;
import org.exoplatform.services.cache.ExoCacheInitException;
import org.exoplatform.services.cache.ObjectCacheInfo;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicInteger;

/**
 * @author <a href="mailto:nicolas.filotto@exoplatform.com">Nicolas Filotto</a>
 * @version $Id$
 *
 */
public class TestAbstractExoCache extends TestCase
{

   CacheService service;

   AbstractExoCache<Serializable, Object> cache;

   public TestAbstractExoCache(String name)
   {
      super(name);
   }

   public void setUp() throws Exception
   {
      this.service = (CacheService)PortalContainer.getInstance().getComponentInstanceOfType(CacheService.class);
      this.cache = (AbstractExoCache<Serializable, Object>)service.getCacheInstance("myCache");
   }

   public void testGet() throws Exception
   {
      cache.put(new MyKey("a"), "a");
      assertEquals("a", cache.get(new MyKey("a")));
      cache.put(new MyKey("a"), "c");
      assertEquals("c", cache.get(new MyKey("a")));
      cache.remove(new MyKey("a"));
      assertEquals(null, cache.get(new MyKey("a")));
      assertEquals(null, cache.get(new MyKey("x")));

      cache.clearCache();
   }

   public void testRemove() throws Exception
   {
      cache.put(new MyKey("a"), 1);
      cache.put(new MyKey("b"), 2);
      cache.put(new MyKey("c"), 3);
      assertEquals(3, cache.getCacheSize());
      assertEquals(1, cache.remove(new MyKey("a")));
      assertEquals(2, cache.getCacheSize());
      assertEquals(2, cache.remove(new MyKey("b")));
      assertEquals(1, cache.getCacheSize());
      assertEquals(null, cache.remove(new MyKey("x")));
      assertEquals(1, cache.getCacheSize());

      cache.clearCache();
   }

   public void testPutMap() throws Exception
   {
      Map<Serializable, Object> values = new HashMap<Serializable, Object>();
      values.put(new MyKey("a"), "a");
      values.put(new MyKey("b"), "b");
      assertEquals(0, cache.getCacheSize());
      cache.putMap(values);
      assertEquals(2, cache.getCacheSize());
      values = new HashMap<Serializable, Object>()
      {
         private static final long serialVersionUID = 1L;

         public Set<Entry<Serializable, Object>> entrySet()
         {
            Set<Entry<Serializable, Object>> set = new LinkedHashSet<Entry<Serializable, Object>>(super.entrySet());
            set.add(new Entry<Serializable, Object>()
            {

               public Object setValue(Object paramV)
               {
                  return null;
               }

               public Object getValue()
               {
                  throw new RuntimeException("An exception");
               }

               public Serializable getKey()
               {
                  return "c";
               }
            });
            return set;
         }
      };
      values.put(new MyKey("e"), "e");
      values.put(new MyKey("d"), "d");
      cache.putMap(values);
      assertEquals(2, cache.getCacheSize());

      cache.clearCache();
   }

   public void testGetCachedObjects() throws Exception
   {
      cache.put(new MyKey("a"), "a");
      cache.put(new MyKey("b"), "b");
      cache.put(new MyKey("c"), "c");
      cache.put(new MyKey("d"), null);
      assertEquals(3, cache.getCacheSize());
      List<Object> values = cache.getCachedObjects();
      assertEquals(3, values.size());
      assertTrue(values.contains("a"));
      assertTrue(values.contains("b"));
      assertTrue(values.contains("c"));

      cache.clearCache();
   }

   public void testRemoveCachedObjects() throws Exception
   {
      cache.put(new MyKey("a"), "a");
      cache.put(new MyKey("b"), "b");
      cache.put(new MyKey("c"), "c");
      cache.put(new MyKey("d"), null);
      assertEquals(3, cache.getCacheSize());
      List<Object> values = cache.removeCachedObjects();
      assertEquals(3, values.size());
      assertTrue(values.contains("a"));
      assertTrue(values.contains("b"));
      assertTrue(values.contains("c"));
      assertEquals(0, cache.getCacheSize());

      cache.clearCache();
   }

   public void testSelect() throws Exception
   {
      cache.put(new MyKey("a"), 1);
      cache.put(new MyKey("b"), 2);
      cache.put(new MyKey("c"), 3);
      final AtomicInteger count = new AtomicInteger();
      CachedObjectSelector<Serializable, Object> selector = new CachedObjectSelector<Serializable, Object>()
      {

         public void onSelect(ExoCache<? extends Serializable, ? extends Object> cache, Serializable key,
            ObjectCacheInfo<? extends Object> ocinfo) throws Exception
         {
            assertTrue(key.equals(new MyKey("a")) || key.equals(new MyKey("b")) || key.equals(new MyKey("c")));
            assertTrue(ocinfo.get().equals(1) || ocinfo.get().equals(2) || ocinfo.get().equals(3));
            count.incrementAndGet();
         }

         public boolean select(Serializable key, ObjectCacheInfo<? extends Object> ocinfo)
         {
            return true;
         }
      };
      cache.select(selector);
      assertEquals(3, count.intValue());

      cache.clearCache();
   }

   public void testGetHitsNMisses() throws Exception
   {
      int hits = cache.getCacheHit();
      int misses = cache.getCacheMiss();
      cache.put(new MyKey("a"), "a");
      cache.get(new MyKey("a"));
      cache.remove(new MyKey("a"));
      cache.get(new MyKey("a"));
      cache.get(new MyKey("z"));
      assertEquals(1, cache.getCacheHit() - hits);
      assertEquals(2, cache.getCacheMiss() - misses);

      cache.clearCache();
   }

   private ExoCacheFactory getExoCacheFactoryInstance() throws ExoCacheInitException
   {
      PortalContainer pc = PortalContainer.getInstance();
      return new ExoCacheFactoryImpl((ExoContainerContext)pc.getComponentInstanceOfType(ExoContainerContext.class),
         "jar:/conf/portal/cache-configuration-template.xml", (ConfigurationManager)pc
            .getComponentInstanceOfType(ConfigurationManager.class));
   }

   public void testMultiThreading() throws Exception
   {
      final ExoCache<Serializable, Object> cache = service.getCacheInstance("test-multi-threading");
      final int totalElement = 100;
      final int totalTimes = 20;
      int reader = 20;
      int writer = 10;
      int remover = 5;
      int cleaner = 1;
      final CountDownLatch startSignalWriter = new CountDownLatch(1);
      final CountDownLatch startSignalOthers = new CountDownLatch(1);
      final CountDownLatch doneSignal = new CountDownLatch(reader + writer + remover);
      final List<Exception> errors = Collections.synchronizedList(new ArrayList<Exception>());
      for (int i = 0; i < writer; i++)
      {
         final int index = i;
         Thread thread = new Thread()
         {
            public void run()
            {
               try
               {
                  startSignalWriter.await();
                  for (int j = 0; j < totalTimes; j++)
                  {
                     for (int i = 0; i < totalElement; i++)
                     {
                        cache.put(new MyKey("key" + i), "value" + i);
                     }
                     if (index == 0 && j == 0)
                     {
                        // The cache is full, we can launch the others
                        startSignalOthers.countDown();
                     }
                     sleep(50);
                  }
               }
               catch (Exception e)
               {
                  errors.add(e);
               }
               finally
               {
                  doneSignal.countDown();
               }
            }
         };
         thread.start();
      }
      startSignalWriter.countDown();
      for (int i = 0; i < reader; i++)
      {
         Thread thread = new Thread()
         {
            public void run()
            {
               try
               {
                  startSignalOthers.await();
                  for (int j = 0; j < totalTimes; j++)
                  {
                     for (int i = 0; i < totalElement; i++)
                     {
                        cache.get(new MyKey("key" + i));
                     }
                     sleep(50);
                  }
               }
               catch (Exception e)
               {
                  errors.add(e);
               }
               finally
               {
                  doneSignal.countDown();
               }
            }
         };
         thread.start();
      }
      for (int i = 0; i < remover; i++)
      {
         Thread thread = new Thread()
         {
            public void run()
            {
               try
               {
                  startSignalOthers.await();
                  for (int j = 0; j < totalTimes; j++)
                  {
                     for (int i = 0; i < totalElement; i++)
                     {
                        cache.remove(new MyKey("key" + i));
                     }
                     sleep(50);
                  }
               }
               catch (Exception e)
               {
                  errors.add(e);
               }
               finally
               {
                  doneSignal.countDown();
               }
            }
         };
         thread.start();
      }
      doneSignal.await();
      for (int i = 0; i < totalElement; i++)
      {
         cache.put(new MyKey("key" + i), "value" + i);
      }
      assertEquals(totalElement, cache.getCacheSize());
      final CountDownLatch startSignal = new CountDownLatch(1);
      final CountDownLatch doneSignal2 = new CountDownLatch(writer + cleaner);
      for (int i = 0; i < writer; i++)
      {
         Thread thread = new Thread()
         {
            public void run()
            {
               try
               {
                  startSignal.await();
                  for (int j = 0; j < totalTimes; j++)
                  {
                     for (int i = 0; i < totalElement; i++)
                     {
                        cache.put(new MyKey("key" + i), "value" + i);
                     }
                     sleep(50);
                  }
               }
               catch (Exception e)
               {
                  errors.add(e);
               }
               finally
               {
                  doneSignal2.countDown();
               }
            }
         };
         thread.start();
      }
      for (int i = 0; i < cleaner; i++)
      {
         Thread thread = new Thread()
         {
            public void run()
            {
               try
               {
                  startSignal.await();
                  for (int j = 0; j < totalTimes; j++)
                  {
                     sleep(150);
                     cache.clearCache();
                  }
               }
               catch (Exception e)
               {
                  errors.add(e);
               }
               finally
               {
                  doneSignal2.countDown();
               }
            }
         };
         thread.start();
      }
      cache.clearCache();
      assertEquals(0, cache.getCacheSize());
      if (!errors.isEmpty())
      {
         for (Exception e : errors)
         {
            e.printStackTrace();
         }
         throw errors.get(0);
      }
      cache.clearCache();
   }

   public static class MyCacheListener implements CacheListener<Serializable, Object>
   {

      public int clearCache;

      public int expire;

      public int get;

      public int put;

      public int remove;

      public void onClearCache(ExoCache<Serializable, Object> cache) throws Exception
      {
         clearCache++;
      }

      public void onExpire(ExoCache<Serializable, Object> cache, Serializable key, Object obj) throws Exception
      {
         expire++;
      }

      public void onGet(ExoCache<Serializable, Object> cache, Serializable key, Object obj) throws Exception
      {
         get++;
      }

      public void onPut(ExoCache<Serializable, Object> cache, Serializable key, Object obj) throws Exception
      {
         put++;
      }

      public void onRemove(ExoCache<Serializable, Object> cache, Serializable key, Object obj) throws Exception
      {
         remove++;
      }

      public void onClearCache(CacheListenerContext context) throws Exception
      {
         clearCache++;
      }

      public void onExpire(CacheListenerContext context, Serializable key, Object obj) throws Exception
      {
         expire++;
      }

      public void onGet(CacheListenerContext context, Serializable key, Object obj) throws Exception
      {
         get++;
      }

      public void onPut(CacheListenerContext context, Serializable key, Object obj) throws Exception
      {
         put++;
      }

      public void onRemove(CacheListenerContext context, Serializable key, Object obj) throws Exception
      {
         remove++;
      }
   }

   public static class MyKey implements Serializable
   {
      private static final long serialVersionUID = 1L;

      public String value;

      public MyKey(String value)
      {
         this.value = value;
      }

      @Override
      public boolean equals(Object paramObject)
      {
         return paramObject instanceof MyKey && ((MyKey)paramObject).value.equals(value);
      }

      @Override
      public int hashCode()
      {
         return value.hashCode();
      }

      @Override
      public String toString()
      {
         return value;
      }
   }   

   /**
    * WARNING: For Linux distributions the following JVM parameter must be set to true: java.net.preferIPv4Stack
    */
   @SuppressWarnings("unchecked")
   public void testDistributedCache() throws Exception
   {
      // If the cache is still alive this test fails due to a TimeoutException.
//      cache.cache.getCacheManager().stop();
      ExoCacheConfig config = new ExoCacheConfig();
      config.setName("MyCacheDistributed");
      config.setMaxSize(8);
      config.setLiveTime(1);
      config.setImplementation("LRU");
      config.setReplicated(true);
      ExoCacheConfig config2 = new ExoCacheConfig();
      config2.setName("MyCacheDistributed2");
      config2.setMaxSize(8);
      config2.setLiveTime(1);
      config2.setImplementation("LRU");
      config2.setReplicated(true);
      AbstractExoCache<Serializable, Object> cache1 =
         (AbstractExoCache<Serializable, Object>)getExoCacheFactoryInstance().createCache(config);
      MyCacheListener listener1 = new MyCacheListener();
      cache1.addCacheListener(listener1);
      AbstractExoCache<Serializable, Object> cache2 =
         (AbstractExoCache<Serializable, Object>)getExoCacheFactoryInstance().createCache(config);
      MyCacheListener listener2 = new MyCacheListener();
      cache2.addCacheListener(listener2);
// Cache 3 creates a conflict with cache 1 and 2      
//      AbstractExoCache<Serializable, Object> cache3 =
//         (AbstractExoCache<Serializable, Object>)getExoCacheFactoryInstance().createCache(config2);
//      MyCacheListener listener3 = new MyCacheListener();
//      cache3.addCacheListener(listener3);
      try
      {
         cache1.put(new MyKey("a"), "b");
         assertEquals(1, cache1.getCacheSize());
         assertEquals("b", cache2.get(new MyKey("a")));
         assertEquals(1, cache2.getCacheSize());
//         assertEquals(0, cache3.getCacheSize());
         assertEquals(1, listener1.put);
         assertEquals(1, listener2.put);
//         assertEquals(0, listener3.put);
         assertEquals(0, listener1.get);
         assertEquals(1, listener2.get);
//         assertEquals(0, listener3.get);
         cache2.put(new MyKey("b"), "c");
         assertEquals(2, cache1.getCacheSize());
         assertEquals(2, cache2.getCacheSize());
         assertEquals("c", cache1.get(new MyKey("b")));
//         assertEquals(0, cache3.getCacheSize());
         assertEquals(2, listener1.put);
         assertEquals(2, listener2.put);
//         assertEquals(0, listener3.put);
         assertEquals(1, listener1.get);
         assertEquals(1, listener2.get);
//         assertEquals(0, listener3.get);
//         cache3.put(new MyKey("c"), "d");
         assertEquals(2, cache1.getCacheSize());
         assertEquals(2, cache2.getCacheSize());
//         assertEquals(1, cache3.getCacheSize());
//         assertEquals("d", cache3.get(new MyKey("c")));
         assertEquals(2, listener1.put);
         assertEquals(2, listener2.put);
//         assertEquals(1, listener3.put);
         assertEquals(1, listener1.get);
         assertEquals(1, listener2.get);
//         assertEquals(1, listener3.get);
         cache2.put(new MyKey("a"), "a");
         assertEquals(2, cache1.getCacheSize());
         assertEquals(2, cache2.getCacheSize());
         assertEquals("a", cache1.get(new MyKey("a")));
         assertEquals(3, listener1.put);
         assertEquals(3, listener2.put);
//         assertEquals(1, listener3.put);
         assertEquals(2, listener1.get);
         assertEquals(1, listener2.get);
//         assertEquals(1, listener3.get);
         cache2.remove(new MyKey("a"));
         assertEquals(1, cache1.getCacheSize());
         assertEquals(1, cache2.getCacheSize());
         assertEquals(3, listener1.put);
         assertEquals(3, listener2.put);
//         assertEquals(1, listener3.put);
         assertEquals(2, listener1.get);
         assertEquals(1, listener2.get);
//         assertEquals(1, listener3.get);
         assertEquals(1, listener1.remove);
         assertEquals(1, listener2.remove);
//         assertEquals(0, listener3.remove);
         cache1.put(new MyKey("c"), "c");
         cache1.clearCache();
         assertEquals(0, cache1.getCacheSize());
         assertEquals(null, cache1.get(new MyKey("b")));
         assertEquals("c", cache2.get(new MyKey("b")));
         assertEquals("c", cache2.get(new MyKey("c")));
         assertEquals(2, cache2.getCacheSize());
         assertEquals(4, listener1.put);
         assertEquals(4, listener2.put);
//         assertEquals(1, listener3.put);
         assertEquals(3, listener1.get);
         assertEquals(3, listener2.get);
//         assertEquals(1, listener3.get);
         assertEquals(1, listener1.remove);
         assertEquals(1, listener2.remove);
//         assertEquals(0, listener3.remove);
         assertEquals(1, listener1.clearCache);
         assertEquals(0, listener2.clearCache);
//         assertEquals(0, listener3.clearCache);
         Map<Serializable, Object> values = new HashMap<Serializable, Object>();
         values.put(new MyKey("a"), "a");
         values.put(new MyKey("b"), "b");
         cache1.putMap(values);
         assertEquals(2, cache1.getCacheSize());
         Thread.sleep(40);
         assertEquals("a", cache2.get(new MyKey("a")));
         assertEquals("b", cache2.get(new MyKey("b")));
         assertEquals(3, cache2.getCacheSize());
         assertEquals(6, listener1.put);
         assertEquals(6, listener2.put);
//         assertEquals(1, listener3.put);
         assertEquals(3, listener1.get);
         assertEquals(5, listener2.get);
//         assertEquals(1, listener3.get);
         assertEquals(1, listener1.remove);
         assertEquals(1, listener2.remove);
//         assertEquals(0, listener3.remove);
         assertEquals(1, listener1.clearCache);
         assertEquals(0, listener2.clearCache);
//         assertEquals(0, listener3.clearCache);
         values = new HashMap<Serializable, Object>()
         {
            private static final long serialVersionUID = 1L;

            public Set<Entry<Serializable, Object>> entrySet()
            {
               Set<Entry<Serializable, Object>> set = new LinkedHashSet<Entry<Serializable, Object>>(super.entrySet());
               set.add(new Entry<Serializable, Object>()
               {

                  public Object setValue(Object paramV)
                  {
                     return null;
                  }

                  public Object getValue()
                  {
                     throw new RuntimeException("An exception");
                  }

                  public Serializable getKey()
                  {
                     return "c";
                  }
               });
               return set;
            }
         };
         values.put(new MyKey("e"), "e");
         values.put(new MyKey("d"), "d");
         cache1.putMap(values);
         assertEquals(2, cache1.getCacheSize());
         assertEquals(3, cache2.getCacheSize());
//         assertEquals(1, cache3.getCacheSize());
         assertEquals(6, listener1.put);
         assertEquals(6, listener2.put);
//         assertEquals(1, listener3.put);
         assertEquals(3, listener1.get);
         assertEquals(5, listener2.get);
//         assertEquals(1, listener3.get);
         assertEquals(1, listener1.remove);
         assertEquals(1, listener2.remove);
//         assertEquals(0, listener3.remove);
         assertEquals(1, listener1.clearCache);
         assertEquals(0, listener2.clearCache);
//         assertEquals(0, listener3.clearCache);
         assertEquals(0, listener1.expire);
         assertEquals(0, listener2.expire);
//         assertEquals(0, listener3.expire);
         Thread.sleep(5600);
         // The values are evicted lazily when we call it
         cache1.get(new MyKey("a"));
         cache1.get(new MyKey("b"));
         cache2.get(new MyKey("a"));
         cache2.get(new MyKey("b"));
         cache2.get(new MyKey("c"));
         assertEquals(0, cache1.getCacheSize());
         assertEquals(0, cache2.getCacheSize());
//         assertEquals(0, cache3.getCacheSize());
         assertEquals(6, listener1.put);
         assertEquals(6, listener2.put);
//         assertEquals(1, listener3.put);
         assertEquals(5, listener1.get);
         assertEquals(8, listener2.get);
//         assertEquals(1, listener3.get);
         assertEquals(1, listener1.remove);
         assertEquals(1, listener2.remove);
//         assertEquals(0, listener3.remove);
         assertEquals(1, listener1.clearCache);
         assertEquals(0, listener2.clearCache);
//         assertEquals(0, listener3.clearCache);
// Expiration events are not triggered in infinispan         
//         assertEquals(2, listener1.expire);
//         assertEquals(3, listener2.expire);
//         assertEquals(1, listener3.expire);
      }
      finally
      {
         cache1.cache.getCacheManager().stop();
         cache2.cache.getCacheManager().stop();
//         cache3.cache.getCacheManager().stop();
      }
   }

   public void testPut() throws Exception
   {
      cache.put(new MyKey("a"), "a");
      cache.put(new MyKey("b"), "b");
      cache.put(new MyKey("c"), "c");
      assertEquals(3, cache.getCacheSize());
      cache.put(new MyKey("a"), "c");
      assertEquals(3, cache.getCacheSize());
      cache.put(new MyKey("d"), "c");
      assertEquals(4, cache.getCacheSize());

      cache.clearCache();
   }

   public void testClearCache() throws Exception
   {
      cache.put(new MyKey("a"), "a");
      cache.put(new MyKey("b"), "b");
      cache.put(new MyKey("c"), "c");
      assertTrue(cache.getCacheSize() > 0);
      cache.clearCache();
      assertTrue(cache.getCacheSize() == 0);

      cache.clearCache();
   }   
}
