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

import java.util.HashSet;
import java.util.List;
import java.util.Set;

import org.jboss.cache.TreeCache;
import org.jboss.cache.Fqn;
import org.jboss.cache.config.Option;
import org.jboss.cache.interceptors.Interceptor;
import org.jboss.cache.marshall.JBCMethodCall;
import org.jboss.cache.marshall.MethodDeclarations;
import org.jboss.cache.misc.TestingUtil;
import org.jgroups.blocks.MethodCall;

/**
 * Tests behaviour when data owners fail - essentially this tests data gravitation
 *
 * @author <a href="mailto:manik@jboss.org">Manik Surtani (manik@jboss.org)</a>
 */
public class BuddyReplicationFailoverTest extends BuddyReplicationTestsBase
{
    protected boolean optimisticLocks = false;
    private String key = "key";
    private String value = "value";
    private TreeCache[] caches;

    protected void tearDown() throws Exception
    {
        if (caches != null)
        {
            cleanup(caches);
            caches = null;
        }
    }


    public void testDataGravitationKillOwner() throws Exception
    {
        testDataGravitation(true);
    }

    public void testDataGravitationDontKillOwner() throws Exception
    {
        testDataGravitation(false);
    }

    private void testDataGravitation(boolean killOwner) throws Exception
    {
        caches = createCaches(3, false, true, optimisticLocks);

        String fqn = "/test";
        String backupFqn = "/" + BuddyManager.BUDDY_BACKUP_SUBTREE + "/" + BuddyManager.getGroupNameFromAddress(caches[0].getLocalAddress()) + fqn;

        dumpCacheContents(caches);

        caches[0].put(fqn, key, value);

        dumpCacheContents(caches);

        assertEquals("Value should exist", value, caches[0].get(fqn, key));

        dumpCacheContents(caches);

        // use exists instead of get() to prevent going up the interceptor stack
        assertTrue("Should be false", !caches[1].exists(fqn));
        assertTrue("Should be false", !caches[2].exists(fqn));

        assertNull("Should be null", caches[0].get(backupFqn, key));
        assertEquals("Value should exist", value, caches[1].get(backupFqn, key));
        assertNull("Should be null", caches[2].get(backupFqn, key));

        if (killOwner)
        {
            caches[0].stopService();
            caches[0] = null;
            TestingUtil.sleepThread(500);
        }

        System.out.println("***** Killed original data owner, about to call a get on a different cache instance.  *****");

        // according to data gravitation, a call to *any* cache should retrieve the data, and move the data to the new cache.
        assertEquals("Value should have gravitated", value, caches[2].get(fqn, key));

        TestingUtil.sleepThread(500);

        dumpCacheContents(caches);
        
        // now lets test the eviction part of gravitation
        String newBackupFqn = "/" + BuddyManager.BUDDY_BACKUP_SUBTREE + "/" + BuddyManager.getGroupNameFromAddress(caches[2].getLocalAddress()) + fqn;

        // use exists instead of get() to prevent going up the interceptor stack
        if (!killOwner) assertTrue("Should be false", !caches[0].exists(fqn));
        assertTrue("Should be false", !caches[1].exists(fqn));

        // the old backup should no longer exist
        if (!killOwner) assertNull("Should be null", caches[0].get(backupFqn, key));
        assertNull("Should be null", caches[1].get(backupFqn, key));
        assertNull("Should be null", caches[2].get(backupFqn, key));

        // and the backup should now exist in caches[2]'s buddy which is caches[0]
        if (killOwner)
        {
            assertEquals("Value should exist", value, caches[1].get(newBackupFqn, key));
        }
        else
        {
            assertEquals("Value should exist", value, caches[0].get(newBackupFqn, key));
            assertNull("Should be null", caches[1].get(newBackupFqn, key));
        }
        assertNull("Should be null", caches[2].get(newBackupFqn, key));
    }

    public void testDataReplicationSuppression() throws Exception
    {
        caches = createCaches(3, false, false, optimisticLocks);

        Fqn fqn = Fqn.fromString("/test");
        Fqn backupFqn = Fqn.fromString("/" + BuddyManager.BUDDY_BACKUP_SUBTREE + "/" + BuddyManager.getGroupNameFromAddress(caches[0].getLocalAddress())
                + "/test");
        caches[0].put(fqn, key, value);

        dumpCacheContents(caches);

        assertEquals("value", caches[0].get(fqn, key));
        assertNull(caches[0].get(backupFqn, key));
        assertEquals("value", caches[1].get(backupFqn, key));
        assertNull(caches[1].get(fqn, key));
        assertNull(caches[2].get(fqn, key));
        assertNull(caches[2].get(backupFqn, key));
        
        assertNoLocks(caches);
        
        // Now force a gravitation
        Option force = new Option();
        force.setForceDataGravitation(true);
        backupFqn = Fqn.fromString("/" + BuddyManager.BUDDY_BACKUP_SUBTREE + "/" + BuddyManager.getGroupNameFromAddress(caches[1].getLocalAddress())
              + "/test");

        System.out.println("*** Calling get() on cache[1] with force option");

        assertEquals("value", caches[1].get(fqn, key, force));

        dumpCacheContents(caches);

        assertNull(caches[1].get(backupFqn, key));
        assertEquals("value", caches[2].get(backupFqn, key));
        assertNull(caches[2].get(fqn, key));
        assertNull(caches[0].get(fqn, key));
        assertNull(caches[0].get(backupFqn, key));
    }

    public void testSubtreeRetrieval() throws Exception
    {
        caches = createCaches(3, false, true, optimisticLocks);

        Fqn fqn = Fqn.fromString("/test");
        Fqn fqn2 = Fqn.fromString("/test/subtree");

        Fqn backupFqn = Fqn.fromString("/" + BuddyManager.BUDDY_BACKUP_SUBTREE + "/" + BuddyManager.getGroupNameFromAddress(caches[0].getLocalAddress()) + "/test");
        Fqn backupFqn2 = Fqn.fromString("/" + BuddyManager.BUDDY_BACKUP_SUBTREE + "/" + BuddyManager.getGroupNameFromAddress(caches[0].getLocalAddress()) + "/test/subtree");



        caches[0].put(fqn, key, value);
        caches[0].put(fqn2, key, value);

        // test backup replication to buddy
        assertEquals(value, caches[0].get(fqn, key));
        assertEquals(value, caches[0].get(fqn2, key));
        assertEquals(value, caches[1].get(backupFqn, key));
        assertEquals(value, caches[1].get(backupFqn2, key));

        assertTrue(!caches[0].exists(backupFqn));
        assertTrue(!caches[0].exists(backupFqn2));
        assertTrue(!caches[1].exists(fqn));
        assertTrue(!caches[1].exists(fqn2));
        assertTrue(!caches[2].exists(fqn));
        assertTrue(!caches[2].exists(fqn2));
        assertTrue(!caches[2].exists(backupFqn));
        assertTrue(!caches[2].exists(backupFqn2));

        assertNoLocks(caches);

        // gravitate to 2:
        caches[2].get(fqn);  // expect entire subtree to gravitate.

        Fqn newBackupFqn = Fqn.fromString("/" + BuddyManager.BUDDY_BACKUP_SUBTREE + "/" + BuddyManager.getGroupNameFromAddress(caches[2].getLocalAddress()) + "/test");
        Fqn newBackupFqn2 = Fqn.fromString("/" + BuddyManager.BUDDY_BACKUP_SUBTREE + "/" + BuddyManager.getGroupNameFromAddress(caches[2].getLocalAddress()) + "/test/subtree");

        assertEquals(value, caches[2].get(fqn, key));
        assertTrue(caches[2].exists(fqn2));
        assertEquals(value, caches[0].get(newBackupFqn, key));
        assertTrue(caches[0].exists(newBackupFqn2));

        assertTrue(!caches[2].exists(newBackupFqn));
        assertTrue(!caches[2].exists(newBackupFqn2));
        assertTrue(!caches[0].exists(fqn));
        assertTrue(!caches[0].exists(fqn2));
        assertTrue(!caches[1].exists(fqn));
        assertTrue(!caches[1].exists(fqn2));
        assertTrue(!caches[1].exists(newBackupFqn));
        assertTrue(!caches[1].exists(newBackupFqn2));

        for (int i=0; i<caches.length; i++)
        {
            assertTrue(!caches[i].exists(backupFqn));
            assertTrue(!caches[i].exists(backupFqn2));
        }

        assertNoLocks(caches);
    }
    
    /**
     * Checks that adding a data gravitation delay to the nodes that 
     * actually have the data doesn't prevent successful gravitation. 
     * Test for JBCACHE-1194. 
     * 
     * FIXME This test is currently meaningless because JBCACHE-1192 means
     * the delaying mechanism doesn't actually work.  Test checks for this
     * and fails at the end with a "known issue" message.  Once JBCACHE-1192
     * is fixed, this test should start working.
     * 
     * @throws Exception
     */
    public void testDelayedResponse() throws Exception
    {
       caches = createCaches(4, false, false, optimisticLocks);

       Fqn fqn = Fqn.fromString("/test");
       Fqn buddyFqn = Fqn.fromString("/" + BuddyManager.BUDDY_BACKUP_SUBTREE + "/" + BuddyManager.getGroupNameFromAddress(caches[0].getLocalAddress()));
       Fqn backupFqn = new Fqn(buddyFqn, fqn);
       
       dumpCacheContents(caches);

       caches[0].put(fqn, key, value);

       dumpCacheContents(caches);

       assertEquals("Value should exist on node0", value, caches[0].get(fqn, key));
       assertNull("Value should not exist on node1", caches[1].get(fqn, key));
       assertEquals("Backup should exist on node1", value, caches[1].get(backupFqn, key));
       assertNull("Value should not exist on node2", caches[2].get(fqn, key));
       assertNull("Backup should not exist on node2", caches[2].get(backupFqn, key));
       assertNull("Value should not exist on node3", caches[3].get(fqn, key));
       assertNull("Backup should not exist on node3", caches[3].get(backupFqn, key));
       
       // Add a listener that will delay data gravitation from node0
       BlockingInterceptor interceptor0 = insertBlockingInterceptor(caches[0]);       
       
       // Add a listener that will delay data gravitation from node1
       BlockingInterceptor interceptor1 = insertBlockingInterceptor(caches[1]);
       
       Option opt = new Option();
       opt.setForceDataGravitation(true);
       assertEquals("Gravitation to node4 successful", value, caches[3].get(fqn, key, opt));       
       
       // Just double-check the listeners worked as expected
       synchronized (interceptor0.monitor)
       {
          interceptor0.monitor.notifyAll();
       }
       synchronized (interceptor1.monitor)
       {
          interceptor1.monitor.notifyAll();
       }
       
       TestingUtil.sleepThread(5);
       
       assertNull("interceptor0 saw no exception", interceptor0.exception);
       assertNull("interceptor1 saw no exception", interceptor1.exception);       
       assertTrue("interceptor0 blocked " + fqn, interceptor0.blocked.contains(fqn));
       assertTrue("interceptor1 blocked " + backupFqn, interceptor1.blocked.contains(backupFqn));
       
       
    }
    
    private BlockingInterceptor insertBlockingInterceptor(TreeCache cache)
    {
       BlockingInterceptor interceptor = new BlockingInterceptor();
       
       List interceptors = cache.getInterceptors();
       Interceptor first = (Interceptor) interceptors.get(0);
       Interceptor next = first.getNext();
       
       interceptor.setNext(next);
       first.setNext(interceptor);
       
       return interceptor;
    }
    
    private class BlockingInterceptor extends Interceptor
    {
      Exception exception = null;
      Set blocked = new HashSet();
      Object monitor = new Object();
      
      public Object invoke(MethodCall m) throws Throwable
      {         
         JBCMethodCall call = (JBCMethodCall) m;
         if (call.getMethodId() == MethodDeclarations.getNodeMethodLocal_id) 
         {
            try
            {
               Object[] args = call.getArgs();
               Fqn fqn = (Fqn) args[0];
               synchronized (monitor)
               {
                  monitor.wait(2000);
               }      
               blocked.add(fqn);
            }
            catch (Exception e)
            {
               exception = e;
            }
            
         }
         return super.invoke(m);
      }
       
    }
    
    
}
