package org.jboss.cache.api.pfer;

import org.easymock.EasyMock;
import static org.easymock.EasyMock.*;
import org.jboss.cache.Cache;
import org.jboss.cache.CacheFactory;
import org.jboss.cache.CacheSPI;
import org.jboss.cache.DefaultCacheFactory;
import org.jboss.cache.Fqn;
import org.jboss.cache.NodeSPI;
import org.jboss.cache.RPCManager;
import org.jboss.cache.util.TestingUtil;
import org.jboss.cache.util.internals.ReplicationListener;
import org.jboss.cache.commands.ReplicableCommand;
import org.jboss.cache.commands.write.*;
import org.jboss.cache.config.Configuration;
import org.jboss.cache.config.Configuration.CacheMode;
import org.jboss.cache.factories.ComponentRegistry;
import org.jboss.cache.factories.UnitTestCacheConfigurationFactory;
import org.jboss.cache.lock.NodeLock;
import org.jboss.cache.optimistic.TransactionWorkspace;
import org.jboss.cache.transaction.GlobalTransaction;
import org.jboss.cache.transaction.OptimisticTransactionEntry;
import org.jgroups.Address;
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.Transaction;
import javax.transaction.TransactionManager;
import java.util.List;
import java.util.Vector;

@Test(groups = {"functional", "jgroups", "transaction"})
public abstract class PutForExternalReadTestBase
{
   protected CacheSPI<String, String> cache1, cache2;

   ReplicationListener replListener1;
   ReplicationListener replListener2;


   protected TransactionManager tm1, tm2;

   protected Fqn<String> fqn = Fqn.fromString("/one/two");
   protected Fqn<String> parentFqn = fqn.getParent();

   protected String key = "k", value = "v", value2 = "v2";

   protected boolean useTx, optimistic;
   protected Configuration.CacheMode cacheMode;

   @BeforeMethod(alwaysRun = true)
   public void setUp()
   {
      CacheFactory<String, String> cf = new DefaultCacheFactory();

      cache1 = (CacheSPI<String, String>) cf.createCache(UnitTestCacheConfigurationFactory.createConfiguration(cacheMode), false);
      cache1.getConfiguration().setTransactionManagerLookupClass("org.jboss.cache.transaction.DummyTransactionManagerLookup");
      cache1.getConfiguration().setNodeLockingScheme(optimistic ? Configuration.NodeLockingScheme.OPTIMISTIC : Configuration.NodeLockingScheme.PESSIMISTIC);

      cache1.start();
      tm1 = cache1.getConfiguration().getRuntimeConfig().getTransactionManager();

      cache2 = (CacheSPI<String, String>) cf.createCache(UnitTestCacheConfigurationFactory.createConfiguration(cacheMode), false);
      cache2.getConfiguration().setTransactionManagerLookupClass("org.jboss.cache.transaction.DummyTransactionManagerLookup");
      cache2.getConfiguration().setNodeLockingScheme(optimistic ? Configuration.NodeLockingScheme.OPTIMISTIC : Configuration.NodeLockingScheme.PESSIMISTIC);

      cache2.start();
      tm2 = cache2.getConfiguration().getRuntimeConfig().getTransactionManager();

      TestingUtil.blockUntilViewsReceived(10000, cache1, cache2);
   }

   @AfterMethod(alwaysRun = true)
   public void tearDown()
   {
      TestingUtil.killCaches(cache1, cache2);
   }

   /**
    * Locks could only occur on the parent node is write locked since if the child node exists it is a no-op anyway.
    * If the parent node is read locked as well, there is no issue.
    */
   public void testNoOpWhenLockedAnd0msTimeout() throws Exception
   {
      // create the parent node first ...
      cache1.put(parentFqn, key, value);

      tm1.begin();
      cache1.put(parentFqn, key, value2);
      NodeSPI parentNode = null;
      TransactionWorkspace workspace = null;

      if (optimistic)
         workspace = extractTransactionWorkspace(cache1);
      else
         parentNode = (NodeSPI<String, String>) cache1.getRoot().getChild(parentFqn);

      Transaction t = tm1.suspend();

      assertLocked(parentFqn, parentNode, workspace, true);

      // parentFqn should be write-locked.
      long startTime = System.currentTimeMillis();
      cache1.putForExternalRead(fqn, key, value);

      long waited = System.currentTimeMillis() - startTime;
      // crappy way to test that pFER does not block, but it is effective.
      assertTrue("Should not wait " + waited + " millis for lock timeout, should attempt to acquite lock with 0ms!", waited < cache1.getConfiguration().getLockAcquisitionTimeout());
      // should not block.

      tm1.resume(t);
      tm1.commit();

      asyncWait();

      assertEquals("Parent node write should have succeeded", value2, cache1.get(parentFqn, key));
      if (isUsingInvalidation())
         assertNull("Parent node write should have invalidated", cache2.get(parentFqn, key));
      else
         assertEquals("Parent node write should have replicated", value2, cache2.get(parentFqn, key));

      if (!optimistic)
      {
         // doesn't apply with optimistic locking since both gtx2EntryMap will succeed here.
         assertNull("PFER should have been a no-op", cache1.get(fqn, key));
         assertNull("PFER should have been a no-op", cache2.get(fqn, key));
      }
   }

   public void testNoOpWhenNodePresent()
   {
      cache1.putForExternalRead(fqn, key, value);
      asyncWait();

      assertEquals("PFER should have succeeded", value, cache1.get(fqn, key));
      if (isUsingInvalidation())
         assertNull("PFER should not have effected cache2", cache2.get(fqn, key));
      else
         assertEquals("PFER should have replicated", value, cache2.get(fqn, key));

      // reset
      cache1.removeNode(fqn);
      asyncWait();

      assertFalse("Should have reset", cache1.getRoot().hasChild(fqn));
      assertFalse("Should have reset", cache2.getRoot().hasChild(fqn));

      cache1.put(fqn, key, value);
      asyncWait();

      // now this pfer should be a no-op
      cache1.putForExternalRead(fqn, key, value2);

      assertEquals("PFER should have been a no-op", value, cache1.get(fqn, key));
      if (isUsingInvalidation())
         assertNull("PFER should have been a no-op", cache2.get(fqn, key));
      else
         assertEquals("PFER should have been a no-op", value, cache2.get(fqn, key));
   }

   private Vector<Address> anyAddresses()
   {
      anyObject();
      return null;
   }

   public void testAsyncForce() throws Exception
   {
      RPCManager rpcManager = EasyMock.createNiceMock(RPCManager.class);
      RPCManager originalRpcManager = cache1.getConfiguration().getRuntimeConfig().getRPCManager();
      List<Address> memberList = originalRpcManager.getMembers();
      expect(rpcManager.getMembers()).andReturn(memberList).anyTimes();
      // inject a mock RPC manager so that we can test whether calls made are sync or async.
      ComponentRegistry cr = TestingUtil.extractComponentRegistry(cache1);
      cr.registerComponent(rpcManager, RPCManager.class);
      cr.rewire();

      // invalidations will not trigger any rpc call sfor PFER
      if (!isUsingInvalidation())
      {
         // specify what we expect called on the mock Rpc Manager.  For params we don't care about, just use ANYTHING.
         // setting the mock object to expect the "sync" param to be false.
         expect(rpcManager.callRemoteMethods(anyAddresses(), (ReplicableCommand) anyObject(), eq(false), anyLong(), anyBoolean())).andReturn(null);
      }

      replay(rpcManager);

      // now try a simple replication.  Since the RPCManager is a mock object it will not actually replicate anything.
      cache1.putForExternalRead(fqn, key, value);
      verify(rpcManager);

      // cleanup
      TestingUtil.extractComponentRegistry(cache1).registerComponent(originalRpcManager, RPCManager.class);
      cache1.removeNode(fqn);
   }

   public void testTxSuspension() throws Exception
   {
      // create parent node first
      cache1.put(parentFqn, key, value);

      // start a tx and do some stuff.
      tm1.begin();
      cache1.get(parentFqn, key);
      NodeSPI parentNode = null;
      TransactionWorkspace workspace = null;
      if (optimistic)
         workspace = extractTransactionWorkspace(cache1);
      else
         parentNode = (NodeSPI<String, String>) cache1.getRoot().getChild(parentFqn);

      cache1.putForExternalRead(fqn, key, value); // should have happened in a separate tx and have committed already.
      Transaction t = tm1.suspend();

      asyncWait();

      assertLocked(parentFqn, parentNode, workspace, false);

      assertEquals("PFER should have completed", value, cache1.get(fqn, key));
      if (isUsingInvalidation())
         assertNull("PFER should not have effected cache2", cache2.get(fqn, key));
      else
         assertEquals("PFER should have completed", value, cache2.get(fqn, key));

      tm1.resume(t);
      tm1.commit();

      asyncWait();

      assertEquals("parent fqn tx should have completed", value, cache1.get(parentFqn, key));
      if (isUsingInvalidation())
         assertNull("parent fqn tx should have invalidated cache2", cache2.get(parentFqn, key));
      else
         assertEquals("parent fqn tx should have completed", value, cache2.get(parentFqn, key));
   }

   public void testExceptionSuppression() throws Exception
   {
      RPCManager barfingRpcManager = EasyMock.createNiceMock(RPCManager.class);
      RPCManager originalRpcManager = cache1.getConfiguration().getRuntimeConfig().getRPCManager();
      try
      {
         List<Address> memberList = originalRpcManager.getMembers();
         expect(barfingRpcManager.getMembers()).andReturn(memberList).anyTimes();
         expect(barfingRpcManager.getLocalAddress()).andReturn(originalRpcManager.getLocalAddress()).anyTimes();
         expect(barfingRpcManager.callRemoteMethods(anyAddresses(), (ReplicableCommand) anyObject(), anyBoolean(), anyLong(), anyBoolean())).andThrow(new RuntimeException("Barf!")).anyTimes();
         replay(barfingRpcManager);

         TestingUtil.extractComponentRegistry(cache1).registerComponent(barfingRpcManager, RPCManager.class);
         cache1.getConfiguration().getRuntimeConfig().setRPCManager(barfingRpcManager);
         TestingUtil.extractComponentRegistry(cache1).rewire();

         try
         {
            cache1.put(fqn, key, value);
            if (!optimistic) fail("Should have barfed");
         }
         catch (RuntimeException re)
         {
         }

         if (optimistic && !isUsingInvalidation())
         {
            // proves that the put did, in fact, barf.  Doesn't work for invalidations since the inability to invalidate will not cause a rollback.
            assertNull(cache1.get(fqn, key));
         }
         else
         {
            // clean up any indeterminate state left over
            try
            {
               cache1.removeNode(fqn);
               // as above, the inability to invalidate will not cause an exception
               if (!isUsingInvalidation()) fail("Should have barfed");
            }
            catch (RuntimeException re)
            {
            }
         }

         assertNull("Should have cleaned up", cache1.get(fqn, key));

         // should not barf
         cache1.putForExternalRead(fqn, key, value);
      }
      finally
      {
         TestingUtil.extractComponentRegistry(cache1).registerComponent(originalRpcManager, RPCManager.class);
      }
   }

   public void testBasicPropagation() throws Exception
   {
      assert !cache1.exists(fqn);
      assert !cache2.exists(fqn);

      cache1.putForExternalRead(fqn, key, value);

      asyncWait();

      assertEquals("PFER updated cache1", value, cache1.get(fqn, key));
      Object expected = isUsingInvalidation() ? null : value;
      assertEquals("PFER propagated to cache2 as expected", expected, cache2.get(fqn, key));

      cache2.putForExternalRead(fqn, key, value);

      asyncWait();

      assertEquals("PFER updated cache2", value, cache2.get(fqn, key));
      assertEquals("Cache1 should be unaffected", value, cache1.get(fqn, key));
   }

   /**
    * Tests that setting a cacheModeLocal=true Option prevents propagation
    * of the putForExternalRead().
    *
    * @throws Exception
    */
   public void testSimpleCacheModeLocal() throws Exception
   {
      cacheModeLocalTest(false);
   }

   /**
    * Tests that setting a cacheModeLocal=true Option prevents propagation
    * of the putForExternalRead() when the call occurs inside a transaction.
    *
    * @throws Exception
    */
   public void testCacheModeLocalInTx() throws Exception
   {
      cacheModeLocalTest(true);
   }

   /**
    * Tests that suspended transactions do not leak.  See JBCACHE-1246.
    */
   public void testMemLeakOnSuspendedTransactions() throws Exception
   {
      Fqn fqn2 = Fqn.fromString("/fqn/two");

      tm1.begin();
      cache1.putForExternalRead(fqn, key, value);
      tm1.commit();

      asyncWait();

      assert cache1.getTransactionTable().getNumGlobalTransactions() == 0 : "Cache 1 should have no stale global TXs";
      assert cache1.getTransactionTable().getNumLocalTransactions() == 0 : "Cache 1 should have no stale local TXs";
      assert cache2.getTransactionTable().getNumGlobalTransactions() == 0 : "Cache 2 should have no stale global TXs";
      assert cache2.getTransactionTable().getNumLocalTransactions() == 0 : "Cache 2 should have no stale local TXs";

      tm1.begin();
      cache1.putForExternalRead(fqn, key, value);
      cache1.put(fqn2, key, value);
      tm1.commit();

      asyncWait();

      assert cache1.getTransactionTable().getNumGlobalTransactions() == 0 : "Cache 1 should have no stale global TXs";
      assert cache1.getTransactionTable().getNumLocalTransactions() == 0 : "Cache 1 should have no stale local TXs";
      assert cache2.getTransactionTable().getNumGlobalTransactions() == 0 : "Cache 2 should have no stale global TXs";
      assert cache2.getTransactionTable().getNumLocalTransactions() == 0 : "Cache 2 should have no stale local TXs";

      tm1.begin();
      cache1.put(fqn2, key, value);
      cache1.putForExternalRead(fqn, key, value);
      tm1.commit();

      asyncWait();

      assert cache1.getTransactionTable().getNumGlobalTransactions() == 0 : "Cache 1 should have no stale global TXs";
      assert cache1.getTransactionTable().getNumLocalTransactions() == 0 : "Cache 1 should have no stale local TXs";
      assert cache2.getTransactionTable().getNumGlobalTransactions() == 0 : "Cache 2 should have no stale global TXs";
      assert cache2.getTransactionTable().getNumLocalTransactions() == 0 : "Cache 2 should have no stale local TXs";

      tm1.begin();
      cache1.put(fqn2, key, value);
      cache1.putForExternalRead(fqn, key, value);
      cache1.put(fqn2, key, value);
      tm1.commit();

      asyncWait();

      assert cache1.getTransactionTable().getNumGlobalTransactions() == 0 : "Cache 1 should have no stale global TXs";
      assert cache1.getTransactionTable().getNumLocalTransactions() == 0 : "Cache 1 should have no stale local TXs";
      assert cache2.getTransactionTable().getNumGlobalTransactions() == 0 : "Cache 2 should have no stale global TXs";
      assert cache2.getTransactionTable().getNumLocalTransactions() == 0 : "Cache 2 should have no stale local TXs";
   }

   /**
    * Tests that setting a cacheModeLocal=true Option prevents propagation
    * of the putForExternalRead().
    *
    * @throws Exception
    */
   private void cacheModeLocalTest(boolean transactional) throws Exception
   {
      RPCManager rpcManager = EasyMock.createMock(RPCManager.class);
      RPCManager originalRpcManager = cache1.getConfiguration().getRuntimeConfig().getRPCManager();

      // inject a mock RPC manager so that we can test whether calls made are sync or async.
      cache1.getConfiguration().getRuntimeConfig().setRPCManager(rpcManager);

      // specify that we expect nothing will be called on the mock Rpc Manager.
      replay(rpcManager);

      // now try a simple replication.  Since the RPCManager is a mock object it will not actually replicate anything.
      if (transactional)
         tm1.begin();

      cache1.getInvocationContext().getOptionOverrides().setCacheModeLocal(true);
      cache1.putForExternalRead(fqn, key, value);

      if (transactional)
         tm1.commit();

      verify(rpcManager);
      // cleanup
      cache1.getConfiguration().getRuntimeConfig().setRPCManager(originalRpcManager);
      cache1.removeNode(fqn);
   }

   protected void assertLocked(Fqn fqn, NodeSPI n, TransactionWorkspace workspace, boolean write_locked) throws Exception
   {
      // this needs to cater for "optimistically locked" nodes as well.
      if (workspace != null)
      {
         // scan workspaces for this node
         assertNotNull("node " + fqn + " should be in transaction workspace", workspace.getNode(fqn));
      }
      else
      {
         NodeLock lock = n.getLock();
         assertTrue("node " + fqn + " is not locked", lock.isLocked());
         if (write_locked)
         {
            assertTrue("node " + fqn + " is not write-locked" + (lock.isReadLocked() ? " but is read-locked instead!" : "!"), lock.isWriteLocked());
         }
         else
         {
            assertTrue("node " + fqn + " is not read-locked" + (lock.isWriteLocked() ? " but is write-locked instead!" : "!"), lock.isReadLocked());
         }
      }
   }

   protected TransactionWorkspace extractTransactionWorkspace(Cache c)
   {
      CacheSPI cs = (CacheSPI) c;
      try
      {
         GlobalTransaction gtx = cs.getTransactionTable().get(cs.getTransactionManager().getTransaction());
         OptimisticTransactionEntry entry = (OptimisticTransactionEntry) cs.getTransactionTable().get(gtx);
         return entry.getTransactionWorkSpace();
      }
      catch (SystemException e)
      {
         e.printStackTrace();
         fail("Unable to extract transaction workspace from cache");
      }
      return null;
   }

   protected void asyncWait()
   {
      TestingUtil.sleepThread(500);
   }

   protected boolean isUsingInvalidation()
   {
      return cacheMode == CacheMode.INVALIDATION_ASYNC
            || cacheMode == CacheMode.INVALIDATION_SYNC;
   }
}
