/*
 * JBoss, Home of Professional Open Source
 *
 * Distributable under LGPL license.
 * See terms of license at gnu.org.
 */
package org.jboss.cache.loader;

import junit.framework.Assert;
import org.jboss.cache.Fqn;
import org.jboss.cache.TreeCache;
import org.jboss.cache.lock.TimeoutException;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import java.util.*;

import EDU.oswego.cs.dl.util.concurrent.CountDown;
import EDU.oswego.cs.dl.util.concurrent.CopyOnWriteArraySet;

/**
 * Tests ClusteredCacheLoader
 *
 * @author <a href="mailto:manik@jboss.org">Manik Surtani (manik@jboss.org)</a>
 * @author <a href="mailto:galder.zamarreno@jboss.com">Galder Zamarreno</a>
 */
public class ClusteredCacheLoaderTest extends AbstractCacheLoaderTestBase
{
    private static Log log = LogFactory.getLog(ClusteredCacheLoaderTest.class);
    private TreeCache cache1, cache2;
    private CacheLoader loader1, loader2;
    private Fqn fqn = Fqn.fromString("/a");
    private String key = "key";
    protected boolean useRegionBasedMarshalling = false;

    protected void setUp() throws Exception
    {
        if (cache1 != null || cache2 != null) tearDown();
        cache1 = new TreeCache("CCL-Test", null, 2000);
        cache2 = new TreeCache("CCL-Test", null, 2000);

        cache1.setCacheMode(TreeCache.REPL_SYNC);
        cache2.setCacheMode(TreeCache.REPL_SYNC);

        cache1.setCacheLoaderConfiguration( getSingleCacheLoaderConfig("", "org.jboss.cache.loader.ClusteredCacheLoader", "timeout=500", false, false, false) );
        cache2.setCacheLoaderConfiguration(getSingleCacheLoaderConfig("", "org.jboss.cache.loader.ClusteredCacheLoader", "timeout=500", false, false, false));

        cache1.setUseRegionBasedMarshalling(useRegionBasedMarshalling);
        cache2.setUseRegionBasedMarshalling(useRegionBasedMarshalling);

        if (useRegionBasedMarshalling)
        {
           cache1.getRegionManager().createRegion(fqn, this.getClass().getClassLoader(), false);
           cache2.getRegionManager().createRegion(fqn, this.getClass().getClassLoader(), false);
        }

        cache1.startService();
        cache2.startService();

        loader1 = cache1.getCacheLoader();
        loader2 = cache2.getCacheLoader();
    }

    protected void tearDown()
    {
        if (cache1 != null)
        {
            cache1.stopService();
            cache1 = null;
            loader1 = null;
        }

        if (cache2 != null)
        {
            cache2.stopService();
            cache2 = null;
            loader2 = null;
        }
    }

    public void testGetKeyValue() throws Exception
    {
        cache1.put(fqn, key, "value");

        log.info("Finished put");
        // test that this has propagated.
        Assert.assertEquals("value", loader1.get(fqn).get(key));
        Assert.assertEquals("value", loader2.get(fqn).get(key));

        cache1.evict(fqn);

        // now cache 1 should not have this but cache 2 should.
        // loader1 looks at cache2 while loader2 looks at cache1
        Assert.assertEquals("value", loader1.get(fqn).get(key));
        Assert.assertNull("Expecting null", loader2.get(fqn));
//        // calling a get on cache1 should cause the loader to retrieve the node from cache2
        Assert.assertEquals("value", cache1.get(fqn, key));
//        // and now loader2 should see it
//        Assert.assertEquals("value", loader2.get(fqn).get(key));
    }

    public void testGet() throws Exception
    {
        cache1.put(fqn, key, "value");

        // test that this has propagated.
        Map map = loader1.get(fqn);
        Assert.assertTrue("Should contain key", map.containsKey(key));
        Assert.assertEquals("value", map.get(key));
        Assert.assertEquals(1, map.size());

        map = loader2.get(fqn);
        Assert.assertTrue("Should contain key", map.containsKey(key));
        Assert.assertEquals("value", map.get(key));
        Assert.assertEquals(1, map.size());

        cache1.evict(fqn);

        // now cache 1 should not have this but cache 2 should.
        // loader1 looks at cache2 while loader2 looks at cache1
        map = loader1.get(fqn);
        Assert.assertTrue(map.containsKey(key));
        Assert.assertEquals("value", map.get(key));
        Assert.assertEquals(1, map.size());

        Assert.assertNull("Expecting null", loader2.get(fqn));
        map = loader2.get(fqn);
        Assert.assertNull( "Should be null", map );

        // calling a get on cache1 should cause the loader to retrieve the node from cache2
        Assert.assertEquals("value", cache1.get(fqn, key));
        // and now loader2 should see it
        map = loader2.get(fqn);
        Assert.assertTrue(map.containsKey(key));
        Assert.assertEquals("value", map.get(key));
        Assert.assertEquals(1, map.size());
    }

    public void testGetChildrenNames() throws Exception
    {
        cache1.put(fqn, key, "value");
        Fqn child1 = new Fqn(fqn, "child1");
        Fqn child2 = new Fqn(fqn, "child2");
        Fqn child3 = new Fqn(fqn, "child3");
        cache1.put(child1, key, "value");
        cache1.put(child2, key, "value");
        cache1.put(child3, key, "value");

        // test that this has propagated.
        Set childNames = loader1.getChildrenNames(fqn);
        Assert.assertEquals(3, childNames.size());
        childNames = loader2.getChildrenNames(fqn);
        Assert.assertEquals(3, childNames.size());

        cache1.evict(child1);
        cache1.evict(child2);
        cache1.evict(child3);
        cache1.evict(fqn);

        // now cache 1 should not have this but cache 2 should.
        // loader1 looks at cache2 while loader2 looks at cache1
        childNames = loader1.getChildrenNames(fqn);
        Assert.assertEquals(3, childNames.size());

        childNames = loader2.getChildrenNames(fqn);
        Assert.assertNull("should be null", childNames);
        // calling a get on cache1 should cause the loader to retrieve the node from cache2
        Assert.assertEquals("value", cache1.get(fqn, key));
        // load up children
        Assert.assertEquals("value", cache1.get(child1, key));
        Assert.assertEquals("value", cache1.get(child2, key));
        Assert.assertEquals("value", cache1.get(child3, key));
        // and now loader2 should see it
        childNames = loader2.getChildrenNames(fqn);
        Assert.assertEquals(3, childNames.size());
    }

    public void testExists() throws Exception
    {
        cache1.put(fqn, key, "value");

        // test that this has propagated.
        Assert.assertTrue("should exist", loader1.exists(fqn));
        Assert.assertTrue("should exist", loader2.exists(fqn));

        cache1.evict(fqn);

        // now cache 1 should not have this but cache 2 should.
        // loader1 looks at cache2 while loader2 looks at cache1
        Assert.assertTrue("should exist", loader1.exists(fqn));
        Assert.assertTrue("should not exist", !loader2.exists(fqn));
        // calling a get on cache1 should cause the loader to retrieve the node from cache2
        Assert.assertEquals("value", cache1.get(fqn, key));
        // and now loader2 should see it
        Assert.assertTrue("should exist", loader2.exists(fqn));
    }

    public void testCacheLoaderThreadSafety() throws Exception
    {
       threadSafetyTest(true);
    }

    public void testCacheLoaderThreadSafetyMultipleFqns() throws Exception
    {
       threadSafetyTest(false);
    }

    protected void threadSafetyTest(final boolean singleFqn) throws Exception
    {
       final CountDown latch = new CountDown(1);
       final Fqn fqn = Fqn.fromString("/a/b/c");
       final List fqns = new ArrayList(30);
       final Random r = new Random();
       if (!singleFqn)
       {
          for (int i = 0; i < 30; i++)
          {
             Fqn f = Fqn.fromString("/a/b/c/" + i);
             fqns.add(f);
             cache2.put(f, "k", "v");
             cache1.evict(f);
          }
       }
       else
       {
          cache2.put(fqn, "k", "v");
          cache1.evict(fqn);
       }
       final int loops = 10000;
       final Set exceptions = new CopyOnWriteArraySet();

       Thread evictor = new Thread("Evictor")
       {
          public void run()
          {
             try
             {
                latch.acquire();
                for (int i = 0; i < loops; i++)
                {
                   Fqn f = singleFqn ? fqn : (Fqn)fqns.get(r.nextInt(fqns.size()));
                   cache1.evict(f);
                }
             }
             catch (TimeoutException te)
             {
                // doesn't matter if we hit these on occasion
             }
             catch (Exception e)
             {
                exceptions.add(e);
             }
          }
       };

       evictor.start();

       Thread writer = new Thread("Writer")
       {
          public void run()
          {
             try
             {
                latch.acquire();
                for (int i = 0; i < loops; i++)
                {
                   Fqn f = singleFqn ? fqn : (Fqn)fqns.get(r.nextInt(fqns.size()));
                   cache2.put(f, "k", "v");
                }
             }
             catch (Exception e)
             {
                exceptions.add(e);
             }
          }
       };

       writer.start();


       Thread reader1 = new Thread("Reader-1")
       {
          public void run()
          {
             try
             {
                latch.acquire();
                for (int i = 0; i < loops; i++)
                {
                   loader1.get(singleFqn ? fqn : (Fqn)fqns.get(r.nextInt(fqns.size())));
                }
             }
             catch (Exception e)
             {
                exceptions.add(e);
             }
          }
       };
       reader1.start();

       Thread reader2 = new Thread("Reader-2")
       {
          public void run()
          {
             try
             {
                latch.acquire();
                for (int i = 0; i < loops; i++)
                {
                   loader1.getChildrenNames(singleFqn ? fqn.getParent() : ((Fqn)fqns.get(r.nextInt(fqns.size()))).getParent());
                }
             }
             catch (Exception e)
             {
                exceptions.add(e);
             }
          }
       };
       reader2.start();

       Thread reader3 = new Thread("Reader-3")
       {
          public void run()
          {
             try
             {
                latch.acquire();
                for (int i = 0; i < loops; i++)
                {
                   loader1.getChildrenNames(singleFqn ? fqn : (Fqn)fqns.get(r.nextInt(fqns.size())));
                }
             }
             catch (Exception e)
             {
                exceptions.add(e);
             }
          }
       };
       reader3.start();

       latch.release();
       reader1.join();
       reader2.join();
       reader3.join();
       evictor.join();
       writer.join();

       Exception e;
       for(Iterator it = exceptions.iterator(); it.hasNext();)
       {
          e = (Exception)it.next();
          throw e;
       }
    }
   
}
