package org.jboss.cache.factories;

import org.jboss.cache.Cache;
import org.jboss.cache.CacheException;
import org.jboss.cache.CacheSPI;
import org.jboss.cache.CacheStatus;
import org.jboss.cache.DefaultCacheFactory;
import org.jboss.cache.Fqn;
import org.jboss.cache.ReplicationException;
import org.jboss.cache.SuspectException;
import org.jboss.cache.util.TestingUtil;
import org.jboss.cache.config.Configuration;
import org.jboss.cache.notifications.annotation.CacheListener;
import org.jboss.cache.notifications.annotation.CacheStarted;
import org.jboss.cache.notifications.annotation.CacheStopped;
import org.jboss.cache.notifications.event.Event;
import org.testng.AssertJUnit;
import static org.testng.AssertJUnit.*;
import org.testng.annotations.AfterMethod;
import org.testng.annotations.Test;

import javax.transaction.NotSupportedException;
import javax.transaction.RollbackException;
import javax.transaction.SystemException;
import javax.transaction.TransactionManager;
import java.util.Arrays;
import java.util.LinkedList;
import java.util.List;

/**
 * Tests restart (stop-destroy-create-start) of ComponentRegistry
 *
 * @author Bela Ban
 * @version $Id: LifeCycleTest.java 5906 2008-05-29 07:24:18Z mircea.markus $
 */
@Test(groups = "functional")
public class LifeCycleTest
{
   private CacheSPI[] c;

   @AfterMethod
   public void tearDown()
   {
      TestingUtil.killCaches(c);
      c = null;
   }

   private void createAndRegisterCache(Configuration.CacheMode mode, boolean start) throws Exception
   {
      Cache cache = createCache(mode);
      List<Cache> caches = new LinkedList<Cache>();
      if (c != null) caches.addAll(Arrays.asList(c));
      caches.add(cache);
      c = caches.toArray(new CacheSPI[]{});
      if (start)
      {
         cache.start();
         if (c.length > 1) TestingUtil.blockUntilViewsReceived(c, 10000);
      }
   }


   public void testLocalRestartNoTransactions() throws Exception
   {
      createAndRegisterCache(Configuration.CacheMode.LOCAL, true);

      c[0].put("/a/b/c", null);
      assertTrue(c[0].getNumberOfNodes() > 0);
      assertEquals(0, c[0].getNumberOfLocksHeld());

      restartCache(c[0]);

      assertEquals(0, c[0].getNumberOfNodes());
      assertEquals(0, c[0].getNumberOfLocksHeld());
   }

   public void testLocalRestartWithTransactions() throws Exception
   {
      createAndRegisterCache(Configuration.CacheMode.LOCAL, true);

      TransactionManager tm = beginTransaction();

      c[0].put("/a/b/c", null);
      assertTrue(c[0].getNumberOfNodes() > 0);
      assertEquals(4, c[0].getNumberOfLocksHeld());

      restartCache(c[0]);

      //assertEquals(4, cache.getNumberOfLocksHeld());
      assertEquals(0, c[0].getNumberOfNodes());

      tm.rollback();
      assertEquals(0, c[0].getNumberOfLocksHeld());
   }

   public void testStartNoCreate() throws Exception
   {
      createAndRegisterCache(Configuration.CacheMode.LOCAL, false);
      c[0].start();

      c[0].put("/a/b/c", null);
      assertTrue(c[0].getNumberOfNodes() > 0);
      assertEquals(0, c[0].getNumberOfLocksHeld());

      restartCache(c[0]);

      assertEquals(0, c[0].getNumberOfNodes());
      assertEquals(0, c[0].getNumberOfLocksHeld());
   }

   public void testReStartNoCreate() throws Exception
   {
      createAndRegisterCache(Configuration.CacheMode.LOCAL, false);
      c[0].start();
      c[0].stop();
      c[0].start();

      c[0].put("/a/b/c", null);
      assertTrue(c[0].getNumberOfNodes() > 0);
      assertEquals(0, c[0].getNumberOfLocksHeld());

      restartCache(c[0]);

      assertEquals(0, c[0].getNumberOfNodes());
      assertEquals(0, c[0].getNumberOfLocksHeld());
   }

   public void testDuplicateInvocation() throws Exception
   {
      createAndRegisterCache(Configuration.CacheMode.LOCAL, false);
      c[0].create();
      c[0].start();
      c[0].create();
      c[0].start();

      c[0].put("/a/b/c", null);
      assertTrue(c[0].getNumberOfNodes() > 0);
      assertEquals(0, c[0].getNumberOfLocksHeld());

      restartCache(c[0]);

      assertEquals(0, c[0].getNumberOfNodes());
      assertEquals(0, c[0].getNumberOfLocksHeld());

      c[0].stop();
      c[0].destroy();
      c[0].stop();
      c[0].destroy();
   }

   public void testFailedStart() throws Exception
   {

      createAndRegisterCache(Configuration.CacheMode.LOCAL, false);
      AssertJUnit.assertEquals("Correct state", CacheStatus.INSTANTIATED, c[0].getCacheStatus());

      DisruptLifecycleListener listener = new DisruptLifecycleListener();
      c[0].addCacheListener(listener);

      c[0].create();

      listener.disrupt = true;

      assertEquals("Correct state", CacheStatus.CREATED, c[0].getCacheStatus());
      try
      {
         c[0].start();
         fail("Listener did not prevent start");
      }
      catch (CacheException good)
      {
      }

      assertEquals("Correct state", CacheStatus.FAILED, c[0].getCacheStatus());

      c[0].addCacheListener(listener);
      listener.disrupt = false;

      c[0].start();

      assertEquals("Correct state", CacheStatus.STARTED, c[0].getCacheStatus());

      c[0].put("/a/b/c", null);
      assertTrue(c[0].getNumberOfNodes() > 0);
      assertEquals(0, c[0].getNumberOfLocksHeld());

      listener.disrupt = true;
      c[0].addCacheListener(listener);

      try
      {
         c[0].stop();
         fail("Listener did not prevent stop");
      }
      catch (CacheException good)
      {
      }

      assertEquals("Correct state", CacheStatus.FAILED, c[0].getCacheStatus());

      listener.disrupt = false;

      c[0].stop();
      assertEquals("Correct state", CacheStatus.STOPPED, c[0].getCacheStatus());
      c[0].destroy();
      assertEquals("Correct state", CacheStatus.DESTROYED, c[0].getCacheStatus());
   }

   public void testInvalidStateInvocations() throws Exception
   {
      createAndRegisterCache(Configuration.CacheMode.LOCAL, false);
      try
      {
         c[0].get(Fqn.ROOT, "k");
         fail("Cache isn't ready!");
      }
      catch (IllegalStateException good)
      {
      }

      c[0].create();
      try
      {
         c[0].get(Fqn.ROOT, "k");
         fail("Cache isn't ready!");
      }
      catch (IllegalStateException good)
      {
      }

      c[0].start();
      c[0].get(Fqn.ROOT, "k"); // should work

      c[0].stop();

      try
      {
         c[0].get(Fqn.ROOT, "k");
         fail("Cache isn't ready!");
      }
      catch (IllegalStateException good)
      {
      }

      c[0].destroy();
      try
      {
         c[0].get(Fqn.ROOT, "k");
         fail("Cache isn't ready!");
      }
      catch (IllegalStateException good)
      {
      }
   }

   public void testRemoteInvalidStateInvocations() throws Exception
   {
      createAndRegisterCache(Configuration.CacheMode.REPL_SYNC, true);
      createAndRegisterCache(Configuration.CacheMode.REPL_SYNC, true);
      try
      {
         // now DIRECTLY change the status of c2.
         // emulate the race condition where the remote cache is stopping but hasn't disconnected from the channel.
         ComponentRegistry cr1 = TestingUtil.extractComponentRegistry(c[1]);
         cr1.state = CacheStatus.STOPPING;

         // Thanks to JBCACHE-1179, this should only log a warning and not throw an exception
         c[0].put(Fqn.ROOT, "k", "v");
      }
      finally
      {
         // reset c[1] to running so the tearDown method can clean it up
         ComponentRegistry cr1 = TestingUtil.extractComponentRegistry(c[1]);
         cr1.state = CacheStatus.STARTED;
      }
   }

   public void testRemoteInvalidStateInvocations2() throws Exception
   {
      createAndRegisterCache(Configuration.CacheMode.REPL_SYNC, true);
      createAndRegisterCache(Configuration.CacheMode.REPL_SYNC, true);
      TestingUtil.blockUntilViewsReceived(c, 10000);
      try
      {
         // now DIRECTLY change the status of c2.
         // emulate the race condition where the remote cache is stopping but hasn't disconnected from the channel.

         // there is a lousy race condition here - we need to make sure cache[1]'s start() method doesn't set status to STARTED
         // after we attempt to change this.
         TestingUtil.blockUntilCacheStatusAchieved(c[1], CacheStatus.STARTED, 1000);
         ComponentRegistry cr1 = TestingUtil.extractComponentRegistry(c[1]);
         cr1.state = CacheStatus.STARTING;
         try
         {
            // This call should wait for up to StateRetrievalTimeout secs or until c[1] has entered the STARTED state, and then barf.
            c[0].put(Fqn.ROOT, "k", "v");
            fail("Should barf!");
         }
         catch (Exception good)
         {

         }

         // now kick off another thread to sleep for a few secs and then set c[1] to STARTED
         final int sleepTime = 500;
         new Thread()
         {
            public void run()
            {
               TestingUtil.sleepThread(sleepTime);
               ComponentRegistry cr1 = TestingUtil.extractComponentRegistry(c[1]);
               cr1.state = CacheStatus.STARTED;
            }
         }.start();

         // should succeed but should take at least 1000ms.
         long startTime = System.currentTimeMillis();
         c[0].put(Fqn.ROOT, "k", "v");
         assert System.currentTimeMillis() > (startTime + sleepTime) : "Should wait till c[1] has STARTED state";

      }
      finally
      {
         // reset c[1] to running so the tearDown method can clean it up
         ComponentRegistry cr1 = TestingUtil.extractComponentRegistry(c[1]);
         cr1.state = CacheStatus.STARTED;
      }
   }

   public void testInvalidStateTxCommit() throws Exception
   {
      createAndRegisterCache(Configuration.CacheMode.LOCAL, true);
      c[0].getTransactionManager().begin();
      c[0].put(Fqn.ROOT, "k1", "v1");
      c[0].put(Fqn.ROOT, "k2", "v2");

      // now DIRECTLY change the status of c.
      ComponentRegistry cr0 = TestingUtil.extractComponentRegistry(c[0]);
      cr0.state = CacheStatus.STOPPING;

      try
      {
         c[0].getTransactionManager().commit();
         fail("Cache isn't STARTED!");
      }
      catch (RollbackException good)
      {
      }
   }

   @SuppressWarnings("unchecked")
   public void testStopInstanceWhileOtherInstanceSends() throws Exception
   {
      final Fqn fqn = Fqn.fromString("/a");
      final List<Boolean> running = new LinkedList<Boolean>();
      final List<Exception> exceptions = new LinkedList<Exception>();
      running.add(true);

      createAndRegisterCache(Configuration.CacheMode.REPL_SYNC, true);
      createAndRegisterCache(Configuration.CacheMode.REPL_SYNC, true);

      c[0].put(fqn, "k", "v");

      assert "v".equals(c[0].get(fqn, "k"));
      assert "v".equals(c[1].get(fqn, "k"));

      // now kick start a thread on c[1] that will constantly update the fqn

      Thread updater = new Thread()
      {
         public void run()
         {
            int i = 0;
            while (running.get(0))
            {
               try
               {
                  i++;
                  if (running.get(0)) c[1].put(fqn, "k", "v" + i);
               }
               catch (ReplicationException re)
               {
                  // this sometimes happens when JGroups suspects the remote node.  This is ok, as long as we don't get an ISE.
               }
               catch (SuspectException se)
               {
                  // this sometimes happens when JGroups suspects the remote node.  This is ok, as long as we don't get an ISE.
               }
               catch (Exception e)
               {
                  exceptions.add(e);
               }
               TestingUtil.sleepThread(20);

            }
         }
      };

      updater.start();

      c[0].stop();
      running.add(false);
      running.remove(true);
      updater.join();

      for (Exception e : exceptions) throw e;
   }

   public void testInvalidStateTxRollback() throws Exception
   {
      createAndRegisterCache(Configuration.CacheMode.LOCAL, true);
      c[0].getTransactionManager().begin();
      c[0].put(Fqn.ROOT, "k1", "v1");
      c[0].put(Fqn.ROOT, "k2", "v2");

      // now DIRECTLY change the status of c.
      ComponentRegistry cr0 = TestingUtil.extractComponentRegistry(c[0]);
      cr0.state = CacheStatus.STOPPING;

      // rollbacks should just log a message
      c[0].getTransactionManager().rollback();
   }


   private CacheSPI<Object, Object> createCache(Configuration.CacheMode cache_mode)
   {
      CacheSPI<Object, Object> retval = (CacheSPI<Object, Object>) new DefaultCacheFactory().createCache(false);
      retval.getConfiguration().setCacheMode(cache_mode);
      retval.getConfiguration().setTransactionManagerLookupClass("org.jboss.cache.transaction.DummyTransactionManagerLookup");
      return retval;
   }


   private TransactionManager beginTransaction() throws SystemException, NotSupportedException
   {
      TransactionManager mgr = c[0].getConfiguration().getRuntimeConfig().getTransactionManager();
      mgr.begin();
      return mgr;
   }


   private void startCache(CacheSPI c)
   {
      c.create();
      c.start();
   }

   private void stopCache(CacheSPI c)
   {
      c.stop();
      c.destroy();
   }

   private void restartCache(CacheSPI c) throws Exception
   {
      stopCache(c);
      startCache(c);
   }

   @CacheListener
   public class DisruptLifecycleListener
   {
      private boolean disrupt;

      @CacheStarted
      public void cacheStarted(Event e)
      {
         if (disrupt) throw new IllegalStateException("I don't want to start");
      }

      @CacheStopped
      public void cacheStopped(Event e)
      {
         if (disrupt) throw new IllegalStateException("I don't want to stop");
      }

      public void setDisrupt(boolean disrupt)
      {
         this.disrupt = disrupt;
      }
   }
}
