/*
 * Created on 17-Feb-2005
 *
 */
package org.jboss.cache.optimistic;

import junit.framework.Assert;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.jboss.cache.Fqn;
import org.jboss.cache.GlobalTransaction;
import org.jboss.cache.OptimisticTransactionEntry;
import org.jboss.cache.TransactionTable;
import org.jboss.cache.TreeCache;
import org.jboss.cache.config.Option;
import org.jboss.cache.interceptors.Interceptor;
import org.jboss.cache.interceptors.OptimisticCreateIfNotExistsInterceptor;
import org.jboss.cache.interceptors.OptimisticNodeInterceptor;
import org.jboss.cache.interceptors.OptimisticReplicationInterceptor;
import org.jboss.cache.interceptors.TxInterceptor;
import org.jboss.cache.loader.SamplePojo;
import org.jboss.cache.marshall.MethodCallFactory;
import org.jboss.cache.marshall.MethodDeclarations;
import org.jboss.cache.transaction.DummyTransactionManager;
import org.jgroups.Address;
import org.jgroups.blocks.MethodCall;

import javax.transaction.RollbackException;
import javax.transaction.Transaction;
import javax.transaction.TransactionManager;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.io.ObjectInput;
import java.io.ObjectOutput;
import java.util.List;
import java.util.HashMap;

/**
 * Tests cache operations on optimistic cache
 *
 * @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 CacheTest extends AbstractOptimisticTestCase
{
    Log log = LogFactory.getLog(CacheTest.class);

    public CacheTest(String s)
    {
        super(s);
    }


    public void testExplicitTxFailure() throws Exception
    {
        final TreeCache c = createCache();

        // explicit.
        TransactionManager mgr = c.getTransactionManager();
        try
        {
            mgr.begin();
            c.put("/a", "k", "v");
            Transaction t = mgr.suspend();
            c.put("/a", "k2", "v2");
            mgr.resume(t);
            mgr.commit();
            Assert.assertTrue("Expecting a rollback exception!", false);
        }
        catch (RollbackException re)
        {
            Assert.assertTrue("Expecting a rollback exception!", true);
        }
        c.stopService();
    }

    public void testImplicitTxFailure() throws Exception
    {
        final TreeCache c = createCache();

        // implicit (much harder to orchestrate...
        int numThreads = 50;
        ExceptionThread thread[] = new ExceptionThread[numThreads];

        for (int i = 0; i < numThreads; i++)
        {
            thread[i] = new ExceptionThread()
            {
                public void run()
                {
                    try
                    {
                        c.put("/a", "k", "v");
                    }
                    catch (Exception e)
                    {
                        log.fatal("*** Thew an exception!!", e);
                        setException(e);
                    }
                }
            };
        }

        for (int i = 0; i < numThreads; i++) thread[i].start();
        for (int i = 0; i < numThreads; i++) thread[i].join();
        // test exceptions.
        for (int i = 0; i < numThreads; i++)
        {
            Assert.assertNull("Thread " + thread[i].getName() + " threw exception!", thread[i].getException());
        }
        c.stopService();
    }

    public void testLocalTransaction() throws Exception
    {

        TreeCache cache = createCacheWithListener();

        Interceptor txInterceptor = new TxInterceptor();
        txInterceptor.setCache(cache);
        Interceptor replicationInterceptor = new OptimisticReplicationInterceptor();
        replicationInterceptor.setCache(cache);
        Interceptor createInterceptor = new OptimisticCreateIfNotExistsInterceptor();
        createInterceptor.setCache(cache);
        Interceptor nodeInterceptor = new OptimisticNodeInterceptor();
        nodeInterceptor.setCache(cache);
        MockInterceptor dummy = new MockInterceptor();
        dummy.setCache(cache);

        txInterceptor.setNext(replicationInterceptor);
        replicationInterceptor.setNext(createInterceptor);
        createInterceptor.setNext(nodeInterceptor);
        nodeInterceptor.setNext(dummy);

        cache.setInterceptorChain(txInterceptor);

        DummyTransactionManager mgr = DummyTransactionManager.getInstance();
        assertNull(mgr.getTransaction());

        mgr.begin();

        assertEquals(0, cache.getTransactionTable().getNumGlobalTransactions());
        assertEquals(0, cache.getTransactionTable().getNumLocalTransactions());

        SamplePojo pojo = new SamplePojo(21, "test");

        cache.put("/one/two", "key1", pojo);

        mgr.commit();

        assertNull(mgr.getTransaction());
        assertEquals(0, cache.getTransactionTable().getNumGlobalTransactions());
        assertEquals(0, cache.getTransactionTable().getNumLocalTransactions());

        //make sure all calls were done in right order

        List calls = dummy.getAllCalled();

        assertEquals(MethodDeclarations.optimisticPrepareMethod, calls.get(0));
        assertEquals(MethodDeclarations.commitMethod, calls.get(1));
        //flesh this out a bit more


        destroyCache(cache);
    }

    public void testRollbackTransaction() throws Exception
    {

        TreeCache cache = createCacheWithListener();

        Interceptor txInterceptor = new TxInterceptor();
        txInterceptor.setCache(cache);
        Interceptor replicationInterceptor = new OptimisticReplicationInterceptor();
        replicationInterceptor.setCache(cache);
        Interceptor createInterceptor = new OptimisticCreateIfNotExistsInterceptor();
        createInterceptor.setCache(cache);
        Interceptor nodeInterceptor = new OptimisticNodeInterceptor();
        nodeInterceptor.setCache(cache);
        MockInterceptor dummy = new MockInterceptor();
        dummy.setCache(cache);

        txInterceptor.setNext(replicationInterceptor);
        replicationInterceptor.setNext(createInterceptor);
        createInterceptor.setNext(nodeInterceptor);
        nodeInterceptor.setNext(dummy);
        cache.setInterceptorChain(txInterceptor);

        DummyTransactionManager mgr = DummyTransactionManager.getInstance();
        assertNull(mgr.getTransaction());
        assertEquals(0, cache.getTransactionTable().getNumGlobalTransactions());
        assertEquals(0, cache.getTransactionTable().getNumLocalTransactions());

        SamplePojo pojo = new SamplePojo(21, "test");
        mgr.begin();
        cache.put("/one/two", "key1", pojo);
        mgr.rollback();
        assertNull(mgr.getTransaction());
        assertEquals(0, cache.getTransactionTable().getNumGlobalTransactions());
        assertEquals(0, cache.getTransactionTable().getNumLocalTransactions());

        //make sure all calls were done in right order

        List calls = dummy.getAllCalled();

        assertEquals(1, calls.size());
        assertEquals(MethodDeclarations.rollbackMethod, calls.get(0));
        destroyCache(cache);

    }

    public void testRemotePrepareTransaction() throws Exception
    {

        TreeCache cache = createCacheWithListener();

        Interceptor txInterceptor = new TxInterceptor();
        txInterceptor.setCache(cache);
        Interceptor replicationInterceptor = new OptimisticReplicationInterceptor();
        replicationInterceptor.setCache(cache);
        Interceptor createInterceptor = new OptimisticCreateIfNotExistsInterceptor();
        createInterceptor.setCache(cache);
        Interceptor nodeInterceptor = new OptimisticNodeInterceptor();
        nodeInterceptor.setCache(cache);
        MockInterceptor dummy = new MockInterceptor();
        dummy.setCache(cache);

        txInterceptor.setNext(replicationInterceptor);
        replicationInterceptor.setNext(createInterceptor);
        createInterceptor.setNext(nodeInterceptor);
        nodeInterceptor.setNext(dummy);

        cache.setInterceptorChain(txInterceptor);
        DummyTransactionManager mgr = DummyTransactionManager.getInstance();

        //start local transaction
        mgr.begin();
        Transaction tx = mgr.getTransaction();

        //this sets
        cache.getCurrentTransaction(tx);

        SamplePojo pojo = new SamplePojo(21, "test");

        cache.put("/one/two", "key1", pojo);

        GlobalTransaction gtx = cache.getCurrentTransaction(tx);
        TransactionTable table = cache.getTransactionTable();
        OptimisticTransactionEntry entry = (OptimisticTransactionEntry) table.get(gtx);
        assertNotNull(mgr.getTransaction());
        mgr.commit();


        GlobalTransaction remoteGtx = new GlobalTransaction();

        remoteGtx.setAddress(new Address()
        {

            public int compareTo(Object arg0)
            {
                return 0;
            }

            public void readFrom(DataInputStream arg0) throws IOException,
                    IllegalAccessException, InstantiationException
            {

            }

            public void writeTo(DataOutputStream arg0) throws IOException
            {

            }

            public void readExternal(ObjectInput arg0) throws IOException,
                    ClassNotFoundException
            {

            }

            public void writeExternal(ObjectOutput arg0) throws IOException
            {

            }

            public int size()
            {
                return 0;
            }

            public boolean isMulticastAddress()
            {
                return false;
            }

        });
        //hack the method call to make it have the remote gtx
        MethodCall meth = (MethodCall) entry.getModifications().get(0);

        meth.getArgs()[0] = remoteGtx;
        //call our remote method
        MethodCall prepareMethod = MethodCallFactory.create(MethodDeclarations.optimisticPrepareMethod, new Object[]{remoteGtx, injectDataVersion(entry.getModifications()), null, remoteGtx.getAddress(), Boolean.FALSE});
        try
        {
            cache._replicate(prepareMethod);
        }
        catch (Throwable t)
        {
            fail();
        }

        //our thread should be null
        assertNull(mgr.getTransaction());

        //	 there should be a registration for the remote gtx
        assertNotNull(table.get(remoteGtx));
        assertNotNull(table.getLocalTransaction(remoteGtx));
        //assert that this is populated
        assertEquals(1, table.get(remoteGtx).getModifications().size());

        //assert that the remote prepare has populated the local workspace
        OptimisticTransactionEntry opEntry = (OptimisticTransactionEntry) table.get(gtx);

        assertEquals(3, entry.getTransactionWorkSpace().getNodes().size());
        assertEquals(1, entry.getModifications().size());
        List calls = dummy.getAllCalled();
        assertEquals(MethodDeclarations.optimisticPrepareMethod, calls.get(2));

        assertEquals(1, cache.getTransactionTable().getNumGlobalTransactions());
        assertEquals(1, cache.getTransactionTable().getNumLocalTransactions());
        destroyCache(cache);

    }
   public void testRemoteCacheBroadcast() throws Exception
    {


        TreeCache cache = createReplicatedCache(TreeCache.REPL_SYNC);
        TreeCache cache2 = createReplicatedCache(TreeCache.REPL_SYNC);

        DummyTransactionManager mgr = DummyTransactionManager.getInstance();

        //start local transaction
        mgr.begin();
        Transaction tx = mgr.getTransaction();

        //this sets
        GlobalTransaction gtx = cache.getCurrentTransaction(tx);

        SamplePojo pojo = new SamplePojo(21, "test");

        cache.put("/one/two", "key1", pojo);

        //GlobalTransaction gtx = cache.getCurrentTransaction(tx);
        TransactionTable table = cache.getTransactionTable();
        assertNotNull(mgr.getTransaction());
        mgr.commit();


        assertNull(mgr.getTransaction());

        //assert that the local cache is in the right state
        assertEquals(0, cache.getTransactionTable().getNumGlobalTransactions());
        assertEquals(0, cache.getTransactionTable().getNumLocalTransactions());

        assertTrue(cache.exists(Fqn.fromString("/one/two")));
        assertTrue(cache.exists(Fqn.fromString("/one")));
        assertEquals(pojo, cache.get(Fqn.fromString("/one/two"), "key1"));

        assertEquals(0, cache2.getTransactionTable().getNumGlobalTransactions());
        assertEquals(0, cache2.getTransactionTable().getNumLocalTransactions());

        assertTrue(cache2.exists(Fqn.fromString("/one/two")));
        assertTrue(cache2.exists(Fqn.fromString("/one")));
        assertEquals(pojo, cache2.get(Fqn.fromString("/one/two"), "key1"));


        destroyCache(cache);
        destroyCache(cache2);
    }


    public void testTwoWayRemoteCacheBroadcast() throws Exception
    {


        TreeCache cache = createReplicatedCache(TreeCache.REPL_SYNC);
        TreeCache cache2 = createReplicatedCache(TreeCache.REPL_SYNC);

        DummyTransactionManager mgr = DummyTransactionManager.getInstance();

        //start local transaction
        mgr.begin();
        Transaction tx = mgr.getTransaction();

        //this sets
        cache.getCurrentTransaction(tx);

        SamplePojo pojo = new SamplePojo(21, "test");

        cache.put("/one/two", "key1", pojo);

        GlobalTransaction gtx = cache.getCurrentTransaction(tx);
        TransactionTable table = cache.getTransactionTable();
        assertNotNull(mgr.getTransaction());
        mgr.commit();


        assertNull(mgr.getTransaction());

        //assert that the local cache is in the right state
        assertEquals(0, cache.getTransactionTable().getNumGlobalTransactions());
        assertEquals(0, cache.getTransactionTable().getNumLocalTransactions());

        assertTrue(cache.exists(Fqn.fromString("/one/two")));
        assertTrue(cache.exists(Fqn.fromString("/one")));
        assertEquals(pojo, cache.get(Fqn.fromString("/one/two"), "key1"));


        assertEquals(0, cache2.getTransactionTable().getNumGlobalTransactions());
        assertEquals(0, cache2.getTransactionTable().getNumLocalTransactions());

        assertTrue(cache2.exists(Fqn.fromString("/one/two")));
        assertTrue(cache2.exists(Fqn.fromString("/one")));

        assertEquals(pojo, cache2.get(Fqn.fromString("/one/two"), "key1"));


        destroyCache(cache);
        destroyCache(cache2);


    }


    public void testRemotePessCacheBroadcast() throws Exception
    {


        TreeCache cache = createPessimisticCache();
        TreeCache cache2 = createPessimisticCache();

        DummyTransactionManager mgr = DummyTransactionManager.getInstance();

        //start local transaction
        mgr.begin();
        Transaction tx = mgr.getTransaction();

        //this sets
        cache.getCurrentTransaction(tx);

        SamplePojo pojo = new SamplePojo(21, "test");

        cache.put("/one/two", "key1", pojo);


        mgr.commit();

        destroyCache(cache);
        destroyCache(cache2);

    }

    public void testConcurrentNodeRemoval() throws Exception
    {
        TreeCache cache = createCache();

        cache.put(fqn, "key", "value");

        // now start a tx to change the value in fqn
        TransactionManager mgr = cache.getTransactionManager();
        mgr.begin();

        cache.put(fqn, "key2", "value2");

        Transaction tx = mgr.suspend();

        // now remove the original node...
        cache.remove(fqn);

        mgr.resume(tx);
        // now try and commit this - this should fail.
        boolean ok = false;
        try
        {
            mgr.commit();
        }
        catch (RollbackException rbe)
        {
            ok = true;
        }

        Assert.assertTrue("Concurrent mod should result in a rollback", ok);
        // now assert that the node has in fact been removed.
        Assert.assertTrue("The node should have been removed!", !cache.exists(fqn));

    }

    public void testConcurrentNodeModification() throws Exception
    {
        TreeCache cache = createCache();

        cache.put(fqn, "key", "value");

        // now start a tx to change the value in fqn
        TransactionManager mgr = cache.getTransactionManager();
        mgr.begin();

        cache.put(fqn, "key2", "value2");

        Transaction tx = mgr.suspend();

        // now change the original node...
        cache.put(fqn, "key3", "value3");

        mgr.resume(tx);
        // now try and commit this - this should fail.
        boolean ok = false;
        try
        {
            mgr.commit();
        }
        catch (RollbackException rbe)
        {
            ok = true;
        }

        Assert.assertTrue("Concurrent mod should result in a rollback", ok);
    }

    public void testRemoveAndCreate() throws Exception
    {
        TreeCache cache = createCache();
        TransactionManager tm = cache.getTransactionManager();
        Fqn f = Fqn.fromString("/person/test");
        Fqn f1 = new Fqn(f, "1");
        Fqn f2 = new Fqn(f, "2");
       
        tm.begin();
        cache.put(f1, "test", "test");
        cache.put(f2, "test", "test");
        tm.commit();

        assertEquals(2, cache.get(f).getChildren().size());

        tm.begin();
        cache.remove(f1);
        cache.put(f1, "test2", "test2");
        tm.commit();

        assertEquals("test2", cache.get(f1, "test2"));
        assertEquals(2, cache.get(f).getChildren().size());
        destroyCache(cache);
    }

   public void testRemoveChildAfterRemoveParent() throws Exception
   {
      TreeCache c = createCache();
      TransactionManager tm = c.getTransactionManager();
      c.put(Fqn.fromString("/a/b"), "k", "v");
      tm.begin();
      c.remove(Fqn.fromString("/a"));
      c.remove(Fqn.fromString("/a/b"));
      tm.commit();

      destroyCache(c);
   }

   public void testAddChildAfterRemoveParent() throws Exception
   {
      TreeCache c = createCache();
      TransactionManager tm = c.getTransactionManager();
      c.put(Fqn.fromString("/a/b"), "k", "v");
      tm.begin();
      c.remove(Fqn.fromString("/a"));
      c.put(Fqn.fromString("/a/b"), "k", "v");
      tm.commit();

      destroyCache(c);
   }

}
