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

import junit.framework.TestCase;
import org.jboss.cache.TreeCache;
import org.jboss.cache.interceptors.OrderedSynchronizationHandler;
import org.jboss.cache.misc.TestingUtil;
import org.jgroups.JChannel;
import org.jgroups.blocks.RpcDispatcher;

import javax.transaction.RollbackException;
import javax.transaction.Synchronization;
import javax.transaction.SystemException;
import javax.transaction.Transaction;
import javax.transaction.TransactionManager;

/**
 * @author <a href="mailto:manik@jboss.org">Manik Surtani (manik@jboss.org)</a>
 */
public class AbortionTest extends TestCase
{
    private MyTC cache1, cache2, cache3;

    protected void setUp() throws Exception
    {
        super.setUp();
        System.out.println("********* START: SET UP *************");
        cache1 = initCache(false);
        TestingUtil.sleepThread(1500); // to ensure cache1 is the coordinator
        cache2 = initCache(false);
        cache3 = initCache(true);
        System.out.println("********* END: SET UP *************");
    }


    protected void tearDown() throws Exception
    {
        System.out.println("********* START: TEAR DOWN *************");
        destroyCache(cache3);
        destroyCache(cache2);
        destroyCache(cache1);
        cache1 = null;
        cache2 = null;
        cache3 = null;
        super.tearDown();
        System.out.println("********* END: TEAR DOWN *************");
    }

    private MyTC initCache(boolean notifying) throws Exception
    {
        MyTC c = new MyTC();
        c.setCacheMode("REPL_SYNC");
        c.setClusterProperties( getJGroupsStack() );
        c.setFetchStateOnStartup(false);
        c.setFetchInMemoryState(false);
        if (!notifying)
        {
            c.setTransactionManagerLookupClass("org.jboss.cache.DummyTransactionManagerLookup");
        }
        else
        {
            c.setTransactionManagerLookupClass("org.jboss.cache.transaction.NotifyingTransactionManager");
        }
        c.startService();
        return c;
    }

    // we need a 'special' stack that does not attempt redelivery since we kill a channel midway during a tx in this test.
    private String getJGroupsStack()
    {
//       return "UDP(mcast_addr=224.0.0.36;mcast_port=55566;ip_ttl=32;" +
//          "mcast_send_buf_size=150000;mcast_recv_buf_size=80000):" +
//          "PING(timeout=10;num_initial_members=1):" +
//          "pbcast.NAKACK(gc_lag=50;max_xmit_size=8192;retransmit_timeout=10):" +
//          "UNICAST(timeout=600):" +
//          "FRAG(frag_size=8192;down_thread=false;up_thread=false):" +
//          "pbcast.GMS(join_timeout=5000;join_retry_timeout=2000;" +
//          "shun=false;print_local_addr=true):" +
//          "pbcast.STATE_TRANSFER";
        return JChannel.DEFAULT_PROTOCOL_STACK;
    }

    private void destroyCache(MyTC c)
    {
        if (c != null)
        {
            c.stopService();
            c.destroyService();
        }
    }

    public void testSyncCaches() throws Exception
    {
        performTest(false, false);
    }

    public void testSyncCachesSyncCommitRollback() throws Exception
    {
        performTest(true, false);
    }

    /**
     * Note that this tests a *remote* beforeCompletion abort - which is a part of the calling instance's afterCompletion.
     *
     * @throws Exception
     */
    public void testAbortBeforeCompletion() throws Exception
    {
        performTest(true, true);
    }

    private void performTest(boolean syncCommitRollback, boolean abortBeforeCompletion) throws Exception
    {
        cache1.setSyncCommitPhase(syncCommitRollback);
        cache1.setSyncRollbackPhase(syncCommitRollback);
        cache2.setSyncCommitPhase(syncCommitRollback);
        cache2.setSyncRollbackPhase(syncCommitRollback);
        cache3.setSyncCommitPhase(syncCommitRollback);
        cache3.setSyncRollbackPhase(syncCommitRollback);

        TransactionManager mgr1 = cache1.getTransactionManager();
        TransactionManager mgr2 = cache2.getTransactionManager();
        NotifyingTransactionManager mgr3 = (NotifyingTransactionManager) cache3.getTransactionManager();

        assertSame(mgr1, mgr2);
        assertNotSame(mgr1, mgr3);
        assertNotSame(mgr2, mgr3);

        assertTrue(mgr1 instanceof DummyTransactionManager);
        assertTrue(mgr2 instanceof DummyTransactionManager);
        assertTrue(mgr3 instanceof NotifyingTransactionManager);


        cache1.put("/test", "key", "value");

        assertEquals("value", cache1.get("/test", "key"));
        assertEquals("value", cache2.get("/test", "key"));
        assertEquals("value", cache3.get("/test", "key"));

        // replicates
        final boolean fAbortBeforeCompletion = abortBeforeCompletion;
        mgr3.notification = new NotifyingTransactionManager.Notification()
        {
            public void notify(Transaction tx) throws SystemException, RollbackException
            {
                final Transaction finalTx = tx;
                System.out.println("Notify called.");
                // add an aborting sync handler.
                Synchronization abort = new Synchronization()
                {

                    Transaction t = finalTx;

                    public void beforeCompletion()
                    {
                        if (fAbortBeforeCompletion)
                        {
                            cache3.myChannel.close();
                            System.out.println("Returning from abort.beforeCompletion");
                            try
                            {
                                finalTx.setRollbackOnly();
                            }
                            catch (SystemException e)
                            {
                                throw new RuntimeException("Unable to set rollback", e);
                            }
                            throw new RuntimeException("Dummy exception");
                        }
                    }

                    public void afterCompletion(int i)
                    {
                        if (!fAbortBeforeCompletion)
                        {
                            cache3.myChannel.close();
                            System.out.println("Returning from abort.afterCompletion");
                            throw new RuntimeException("Dummy exception");
                        }
                    }
                };

                OrderedSynchronizationHandler osh = OrderedSynchronizationHandler.getInstance(tx);
                osh.registerAtHead(abort);
                System.out.println("Added sync handler.");
            }
        };

        mgr1.begin();
        Transaction tx = mgr1.getTransaction();
        cache1.put("/test", "key", "value2");
        tx.commit();

        TestingUtil.sleepThread(5000);

        // only test cache1 and cache2.  Assume cache3 has crashed out.
        assertEquals(0, cache1.getNumberOfLocksHeld());
        assertEquals(0, cache2.getNumberOfLocksHeld());
        assertEquals("put in transaction should NOT have been rolled back", "value2", cache1.get("/test", "key"));
        assertEquals("put in transaction should NOT have been rolled back", "value2", cache2.get("/test", "key"));

    }


    public static class MyTC extends TreeCache
    {
        JChannel myChannel;
        RpcDispatcher myDispatcher;

        public MyTC() throws Exception
        {
            super();
        }

        public void startService() throws Exception
        {
            super.startService();
            myChannel = channel;
            myDispatcher = disp;
        }
    }

}
