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

import org.jboss.cache.CacheFactory;
import org.jboss.cache.CacheSPI;
import org.jboss.cache.DefaultCacheFactory;
import org.jboss.cache.Fqn;
import org.jboss.cache.Node;
import org.jboss.cache.config.Configuration;
import static org.testng.AssertJUnit.*;
import org.testng.annotations.AfterMethod;
import org.testng.annotations.BeforeMethod;
import org.testng.annotations.Test;

import javax.transaction.SystemException;
import javax.transaction.TransactionManager;
import java.util.HashMap;
import java.util.Map;

/**
 * Tests the cache mode local override in various scenarios.  To be subclassed to test REPL_SYNC, REPL_ASYNC, INVALIDATION_SYNC, INVALIDATION_ASYNC for Opt and Pess locking.
 * <p/>
 * Option.setCacheModeLocal() only applies to put() and remove() methods.
 *
 * @author <a href="mailto:manik@jboss.org">Manik Surtani (manik@jboss.org)</a>
 */
@Test(groups = {"functional"})
public abstract class CacheModeLocalTestBase
{
   // to be subclassed.
   protected Configuration.CacheMode cacheMode;
   protected String nodeLockingScheme;
   /**
    * set this to true if the implementing class plans to use an invalidating cache mode *
    */
   protected boolean isInvalidation;

   private CacheSPI<String, String> cache1;
   private CacheSPI<String, String> cache2;

   private Fqn fqn = Fqn.fromString("/a");
   private String key = "key";

   @BeforeMethod(alwaysRun = true)
   public void setUp() throws Exception
   {
      // force a tear down if the test runner didn't run one before (happens in IDEA)
      if (cache1 != null || cache2 != null)
         tearDown();

      CacheFactory<String, String> instance = new DefaultCacheFactory();
      cache1 = (CacheSPI<String, String>) instance.createCache(false);
      cache1.getConfiguration().setClusterName("test");
      cache1.getConfiguration().setStateRetrievalTimeout(1000);
      cache1.getConfiguration().setTransactionManagerLookupClass("org.jboss.cache.transaction.DummyTransactionManagerLookup");
      cache1.getConfiguration().setNodeLockingScheme(nodeLockingScheme);
      cache1.getConfiguration().setCacheMode(cacheMode);
      cache1.start();

      cache2 = (CacheSPI<String, String>) instance.createCache(false);
      cache2.getConfiguration().setClusterName("test");
      cache2.getConfiguration().setStateRetrievalTimeout(1000);
      cache2.getConfiguration().setTransactionManagerLookupClass("org.jboss.cache.transaction.DummyTransactionManagerLookup");
      cache2.getConfiguration().setNodeLockingScheme(nodeLockingScheme);
      cache2.getConfiguration().setCacheMode(cacheMode);
      cache2.start();
   }

   @AfterMethod(alwaysRun = true)
   public void tearDown()
   {
      if (cache1 != null)
      {
         cache1.stop();
         flushTxs(cache1.getTransactionManager());
         cache1 = null;
      }

      if (cache2 != null)
      {
         cache2.stop();
         flushTxs(cache2.getTransactionManager());
         cache2 = null;
      }
   }

   private void flushTxs(TransactionManager mgr)
   {
      if (mgr != null)
      {
         try
         {
            if (mgr.getTransaction() != null)
            {
               mgr.rollback();
            }
         }
         catch (SystemException e)
         {
            // do nothing
         }
      }
   }

   public void testPutKeyValue() throws Exception
   {
      cache1.getInvocationContext().getOptionOverrides().setCacheModeLocal(true);
      cache1.put(fqn, key, "value");
      delay();
      // cache1 should still have this
      assertEquals("value", cache1.get(fqn, key));

      // cache 2 should not
      assertNull("Should be null", cache2.get(fqn, key));

      // now try again with passing the default options
      cache1.getInvocationContext().getOptionOverrides().reset();
      cache1.put(fqn, key, "value");
      delay();
      // cache1 should still have this
      assertEquals("value", cache1.get(fqn, key));

      // cache 2 should as well
      if (!isInvalidation)
      {
         assertEquals("value", cache2.get(fqn, key));
      }
      else
      {
         assertNull("should be invalidated", cache2.get(fqn, key));
      }

      // now cache2
      cache2.getInvocationContext().getOptionOverrides().setCacheModeLocal(true);
      cache2.put(fqn, key, "value2");
      delay();
      assertEquals("value2", cache2.get(fqn, key));
      assertEquals("value", cache1.get(fqn, key));

      cache2.getInvocationContext().getOptionOverrides().reset();
      cache2.put(fqn, key, "value2");
      delay();
      assertEquals("value2", cache2.get(fqn, key));
      if (!isInvalidation)
      {
         assertEquals("value2", cache1.get(fqn, key));
      }
      else
      {
         assertNull("should be invalidated", cache1.get(fqn, key));
      }
   }

   public void testPutKeyValueViaNodeAPI() throws Exception
   {
      Node node1 = cache1.getRoot().addChild(fqn);
      cache1.getInvocationContext().getOptionOverrides().setCacheModeLocal(true);
      node1.put(key, "value");
      delay();
      // cache1 should still have this
      assertEquals("value", cache1.get(fqn, key));

      // cache 2 should not
      assertNull("Should be null", cache2.get(fqn, key));

      // now try again with passing the default options
      cache1.getInvocationContext().getOptionOverrides().reset();
      node1.put(key, "value");
      delay();
      // cache1 should still have this
      assertEquals("value", cache1.get(fqn, key));

      // cache 2 should as well
      if (!isInvalidation)
      {
         assertEquals("value", cache2.get(fqn, key));
      }
      else
      {
         assertNull("should be invalidated", cache2.get(fqn, key));
      }

      // now cache2
      cache2.getInvocationContext().getOptionOverrides().setCacheModeLocal(true);
      Node node2 = cache2.getRoot().addChild(fqn);
      cache2.getInvocationContext().getOptionOverrides().setCacheModeLocal(true);
      node2.put(key, "value2");
      delay();
      assertEquals("value2", cache2.get(fqn, key));
      assertEquals("value", cache1.get(fqn, key));

      cache2.getInvocationContext().getOptionOverrides().reset();
      node2.put(key, "value2");
      delay();
      assertEquals("value2", cache2.get(fqn, key));
      if (!isInvalidation)
      {
         assertEquals("value2", cache1.get(fqn, key));
      }
      else
      {
         assertNull("should be invalidated", cache1.get(fqn, key));
      }
   }

   public void testPutData() throws Exception
   {
      Map<String, String> map = new HashMap<String, String>();
      map.put(key, "value");

      cache1.getInvocationContext().getOptionOverrides().setCacheModeLocal(true);
      cache1.put(fqn, map);
      delay();
      // cache1 should still have this
      assertEquals("value", cache1.get(fqn, key));
      // cache 2 should not
      assertNull("Should be null", cache2.get(fqn, key));

      // now try again with passing the default options
      cache1.getInvocationContext().getOptionOverrides().setCacheModeLocal(false);
      cache1.put(fqn, map);
      delay();
      // cache1 should still have this
      assertEquals("value", cache1.get(fqn, key));
      // cache 2 should as well
      if (!isInvalidation)
      {
         assertEquals("value", cache2.get(fqn, key));
      }
      else
      {
         assertNull("should be invalidated", cache2.get(fqn, key));
      }

      // now cache2
      map.put(key, "value2");
      cache2.getInvocationContext().getOptionOverrides().setCacheModeLocal(true);
      cache2.put(fqn, map);
      delay();
      assertEquals("value2", cache2.get(fqn, key));
      assertEquals("value", cache1.get(fqn, key));

      cache2.getInvocationContext().getOptionOverrides().reset();
      cache2.put(fqn, key, "value2");
      delay();
      assertEquals("value2", cache2.get(fqn, key));
      if (!isInvalidation)
      {
         assertEquals("value2", cache1.get(fqn, key));
      }
      else
      {
         assertNull("should be invalidated", cache1.get(fqn, key));
      }
   }

   public void testPutDataViaNodeAPI() throws Exception
   {
      Map<String, String> map = new HashMap<String, String>();
      map.put(key, "value");

      Node node1 = cache1.getRoot().addChild(fqn);
      cache1.getInvocationContext().getOptionOverrides().setCacheModeLocal(true);
      node1.putAll(map);
      delay();
      // cache1 should still have this
      assertEquals("value", cache1.get(fqn, key));
      // cache 2 should not
      assertNull("Should be null", cache2.get(fqn, key));

      // now try again with passing the default options
      cache1.getInvocationContext().getOptionOverrides().setCacheModeLocal(false);
      node1.putAll(map);
      delay();
      // cache1 should still have this
      assertEquals("value", cache1.get(fqn, key));
      // cache 2 should as well
      if (!isInvalidation)
      {
         assertEquals("value", cache2.get(fqn, key));
      }
      else
      {
         assertNull("should be invalidated", cache2.get(fqn, key));
      }

      // now cache2
      cache2.getInvocationContext().getOptionOverrides().setCacheModeLocal(true);
      Node node2 = cache2.getRoot().addChild(fqn);
      map.put(key, "value2");
      cache2.getInvocationContext().getOptionOverrides().setCacheModeLocal(true);
      node2.putAll(map);
      delay();
      assertEquals("value2", cache2.get(fqn, key));
      assertEquals("value", cache1.get(fqn, key));

      cache2.getInvocationContext().getOptionOverrides().reset();
      node2.put(key, "value2");
      delay();
      assertEquals("value2", cache2.get(fqn, key));
      if (!isInvalidation)
      {
         assertEquals("value2", cache1.get(fqn, key));
      }
      else
      {
         assertNull("should be invalidated", cache1.get(fqn, key));
      }
   }

   public void testRemoveNode() throws Exception
   {
      // put some stuff in the cache first
      // make sure we cleanup thread local vars.
      cache1.getInvocationContext().setOptionOverrides(null);
      cache1.put(fqn, key, "value");
      delay();
      assertEquals("value", cache1.get(fqn, key));
      if (isInvalidation)
      {
         assertNull("Should be null", cache2.get(fqn, key));
      }
      else
      {
         assertEquals("value", cache2.get(fqn, key));
      }

      cache1.getInvocationContext().getOptionOverrides().setCacheModeLocal(true);
      cache1.removeNode(fqn);
      delay();

      // should be removed in cache1
      assertNull("should be null", cache1.get(fqn, key));
      // Not in cache2
      if (isInvalidation)
      {
         assertNull("Should be null", cache2.get(fqn, key));
      }
      else
      {
         assertEquals("value", cache2.get(fqn, key));
      }

      // replace cache entries
      cache1.put(fqn, key, "value");
      delay();
      assertEquals("value", cache1.get(fqn, key));
      if (isInvalidation)
      {
         assertNull("Should be null", cache2.get(fqn, key));
      }
      else
      {
         assertEquals("value", cache2.get(fqn, key));
      }

      // now try again with passing the default options
      cache1.getInvocationContext().getOptionOverrides().setCacheModeLocal(false);
      cache1.removeNode(fqn);
      delay();

      // both should be null
      assertNull("should be null", cache1.get(fqn, key));
      assertNull("should be null", cache2.get(fqn, key));
   }

   public void testRemoveNodeViaNodeAPI() throws Exception
   {
      // put some stuff in the cache first
      // make sure we cleanup thread local vars.
      cache1.getInvocationContext().setOptionOverrides(null);
      cache1.put(fqn, key, "value");
      delay();
      assertEquals("value", cache1.get(fqn, key));
      if (isInvalidation)
      {
         assertNull("Should be null", cache2.get(fqn, key));
      }
      else
      {
         assertEquals("value", cache2.get(fqn, key));
      }

      cache1.getInvocationContext().getOptionOverrides().setCacheModeLocal(true);
      cache1.getRoot().removeChild(fqn);
      delay();

      // should be removed in cache1
      assertNull("should be null", cache1.get(fqn, key));
      // Not in cache2
      if (isInvalidation)
      {
         assertNull("Should be null", cache2.get(fqn, key));
      }
      else
      {
         assertEquals("value", cache2.get(fqn, key));
      }

      // replace cache entries
      cache1.put(fqn, key, "value");
      delay();
      assertEquals("value", cache1.get(fqn, key));
      if (isInvalidation)
      {
         assertNull("Should be null", cache2.get(fqn, key));
      }
      else
      {
         assertEquals("value", cache2.get(fqn, key));
      }

      // now try again with passing the default options
      cache1.getInvocationContext().getOptionOverrides().setCacheModeLocal(false);
      cache1.getRoot().removeChild(fqn);
      delay();

      // both should be null
      assertNull("should be null", cache1.get(fqn, key));
      assertNull("should be null", cache2.get(fqn, key));
   }

   public void testRemoveKey() throws Exception
   {
      // put some stuff in the cache first
      cache1.getInvocationContext().setOptionOverrides(null);
      cache1.put(fqn, key, "value");
      delay();
      assertEquals("value", cache1.get(fqn, key));
      if (isInvalidation)
      {
         assertNull("Should be null", cache2.get(fqn, key));
      }
      else
      {
         assertEquals("value", cache2.get(fqn, key));
      }

      cache1.getInvocationContext().getOptionOverrides().setCacheModeLocal(true);
      cache1.remove(fqn, key);
      delay();

      // should be removed in cache1
      assertNull("should be null", cache1.get(fqn, key));
      // Not in cache2
      if (isInvalidation)
      {
         assertNull("Should be null", cache2.get(fqn, key));
      }
      else
      {
         assertEquals("value", cache2.get(fqn, key));
      }

      // replace cache entries
      cache1.put(fqn, key, "value");
      delay();
      assertEquals("value", cache1.get(fqn, key));
      if (isInvalidation)
      {
         assertNull("Should be null", cache2.get(fqn, key));
      }
      else
      {
         assertEquals("value", cache2.get(fqn, key));
      }

      // now try again with passing the default options
      cache1.getInvocationContext().getOptionOverrides().reset();
      cache1.remove(fqn, key);
      delay();

      // both should be null
      assertNull("should be null", cache1.get(fqn, key));
      assertNull("should be null", cache2.get(fqn, key));
   }

   public void testRemoveKeyViaNodeAPI() throws Exception
   {
      // put some stuff in the cache first
      Node node1 = cache1.getRoot().addChild(fqn);
      cache1.getInvocationContext().setOptionOverrides(null);
      node1.put(key, "value");
      delay();
      assertEquals("value", cache1.get(fqn, key));
      if (isInvalidation)
      {
         assertNull("Should be null", cache2.get(fqn, key));
      }
      else
      {
         assertEquals("value", cache2.get(fqn, key));
      }

      cache1.getInvocationContext().getOptionOverrides().setCacheModeLocal(true);
      node1.remove(key);
      delay();

      // should be removed in cache1
      assertNull("should be null", cache1.get(fqn, key));
      // Not in cache2
      if (isInvalidation)
      {
         assertNull("Should be null", cache2.get(fqn, key));
      }
      else
      {
         assertEquals("value", cache2.get(fqn, key));
      }

      // replace cache entries
      node1.put(key, "value");
      delay();
      assertEquals("value", cache1.get(fqn, key));
      if (isInvalidation)
      {
         assertNull("Should be null", cache2.get(fqn, key));
      }
      else
      {
         assertEquals("value", cache2.get(fqn, key));
      }

      // now try again with passing the default options
      cache1.getInvocationContext().getOptionOverrides().reset();
      node1.remove(key);
      delay();

      // both should be null
      assertNull("should be null", cache1.get(fqn, key));
      assertNull("should be null", cache2.get(fqn, key));
   }

   public void testTransactionalBehaviourCommit() throws Exception
   {
      TransactionManager mgr = cache1.getTransactionManager();
      mgr.begin();
      cache1.getInvocationContext().getOptionOverrides().reset();
      cache1.put(fqn, key, "value1");
      cache1.getInvocationContext().getOptionOverrides().setCacheModeLocal(true);
      cache1.put(fqn, key, "value2");
      mgr.commit();
      delay();
      // cache1 should still have this
      assertEquals("value2", cache1.get(fqn, key));

      if (!isInvalidation)
      {
         assertEquals("value1", cache2.get(fqn, key));
      }
      else
      {
         assertNull(cache2.get(fqn, key));
      }

      // now try again with passing the default options
      mgr.begin();
      cache1.getInvocationContext().getOptionOverrides().setCacheModeLocal(true);
      cache1.put(fqn, key, "value3");
      cache1.getInvocationContext().getOptionOverrides().reset();
      cache1.put(fqn, key, "value");
      mgr.commit();
      delay();
      // cache1 should still have this
      assertEquals("value", cache1.get(fqn, key));

      // cache 2 should as well
      if (!isInvalidation)
      {
         assertEquals("value", cache2.get(fqn, key));
      }
      else
      {
         assertNull("should be invalidated", cache2.get(fqn, key));
      }

      // now cache2
      mgr = cache2.getTransactionManager();
      mgr.begin();
      cache2.getInvocationContext().getOptionOverrides().setCacheModeLocal(false);
      cache2.put(fqn, key, "value3");
      cache2.getInvocationContext().getOptionOverrides().setCacheModeLocal(true);
      cache2.put(fqn, key, "value2");
      mgr.commit();
      delay();

      assertEquals("value2", cache2.get(fqn, key));

      if (!isInvalidation)
      {
         assertEquals("value3", cache1.get(fqn, key));
      }
      else
      {
         assertNull(cache1.get(fqn, key));
      }

      mgr.begin();
      cache2.getInvocationContext().getOptionOverrides().setCacheModeLocal(true);
      cache2.put(fqn, key, "value2");
      cache2.getInvocationContext().getOptionOverrides().reset();
      cache2.put(fqn, key, "value4");
      mgr.commit();
      delay();
      assertEquals("value4", cache2.get(fqn, key));
      if (!isInvalidation)
      {
         assertEquals("value4", cache1.get(fqn, key));
      }
      else
      {
         assertNull("should be invalidated", cache1.get(fqn, key));
      }

   }

   public void testTransactionalBehaviourRollback() throws Exception
   {
      TransactionManager mgr = cache1.getTransactionManager();

      // create these first ...
      cache1.put("/a", key, "old");
      cache1.put("/b", key, "old");
      delay();
      mgr.begin();
      cache1.getInvocationContext().getOptionOverrides().reset();
      cache1.put("/a", key, "value1");
      cache1.getInvocationContext().getOptionOverrides().setCacheModeLocal(true);
      cache1.put("/b", key, "value2");
      mgr.rollback();
      delay();
      // cache1 should NOT have this
      assert cache1.get("/a", key).equals("old");
      assert cache1.get("/b", key).equals("old");

      if (isInvalidation)
      {
         assert cache2.get("/a", key) == null;
         assert cache2.get("/b", key) == null;
      }
      else
      {
         assert cache2.get("/a", key).equals("old");
         assert cache2.get("/b", key).equals("old");
      }
   }

   public void testTransactionalBehaviourViaNodeAPI() throws Exception
   {
      Node node1 = cache1.getRoot().addChild(fqn);
      TransactionManager mgr = cache1.getTransactionManager();
      mgr.begin();
      cache1.getInvocationContext().getOptionOverrides().reset();
      node1.put(key, "value1");
      cache1.getInvocationContext().getOptionOverrides().setCacheModeLocal(true);
      node1.put(key, "value2");
      mgr.commit();
      delay();
      // cache1 should still have this
      assertEquals("value2", cache1.get(fqn, key));

      if (!isInvalidation)
      {
         assertEquals("value1", cache2.get(fqn, key));
      }
      else
      {
         assertNull(cache2.get(fqn, key));
      }

      // now try again with passing the default options
      mgr.begin();
      cache1.getInvocationContext().getOptionOverrides().setCacheModeLocal(true);
      node1.put(key, "value3");
      cache1.getInvocationContext().getOptionOverrides().reset();
      node1.put(key, "value");
      mgr.commit();
      delay();
      // cache1 should still have this
      assertEquals("value", cache1.get(fqn, key));

      // cache 2 should as well
      if (!isInvalidation)
      {
         assertEquals("value", cache2.get(fqn, key));
      }
      else
      {
         assertNull("should be invalidated", cache2.get(fqn, key));
      }

      // now cache2
      Node node2 = cache2.getRoot().addChild(fqn);
      mgr = cache2.getTransactionManager();
      mgr.begin();
      cache2.getInvocationContext().getOptionOverrides().setCacheModeLocal(false);
      node2.put(key, "value3");
      cache2.getInvocationContext().getOptionOverrides().setCacheModeLocal(true);
      node2.put(key, "value2");
      mgr.commit();
      delay();

      assertEquals("value2", cache2.get(fqn, key));

      if (!isInvalidation)
      {
         assertEquals("value3", cache1.get(fqn, key));
      }
      else
      {
         assertNull(cache1.get(fqn, key));
      }

      mgr.begin();
      cache2.getInvocationContext().getOptionOverrides().setCacheModeLocal(true);
      node2.put(key, "value2");
      cache2.getInvocationContext().getOptionOverrides().reset();
      node2.put(key, "value4");
      mgr.commit();
      delay();
      assertEquals("value4", cache2.get(fqn, key));
      if (!isInvalidation)
      {
         assertEquals("value4", cache1.get(fqn, key));
      }
      else
      {
         assertNull("should be invalidated", cache1.get(fqn, key));
      }

   }

   public void testAddChild() throws Exception
   {
      cache1.getInvocationContext().getOptionOverrides().setCacheModeLocal(true);
      cache1.getRoot().addChild(fqn);
      delay();
      // cache1 should still have this
      assertTrue(cache1.getRoot().hasChild(fqn));
      // cache 2 should not
      Node node2 = cache2.getRoot().getChild(fqn);
      assertTrue("Should be null", node2 == null || (isInvalidation && !node2.isValid()));

      // now try again with passing the default options
      cache1.getRoot().removeChild(fqn);
      cache1.getInvocationContext().getOptionOverrides().setCacheModeLocal(false);
      cache1.getRoot().addChild(fqn);
      delay();
      // cache1 should still have this
      assertTrue(cache1.getRoot().hasChild(fqn));
      // cache 2 should as well
      if (!isInvalidation)
      {
         assertTrue(cache2.getRoot().hasChild(fqn));
      }
      else
      {
         assertTrue("Should be null", node2 == null || !node2.isValid());
      }
   }

   protected abstract void delay();

}