/*****************************************
 *                                       *
 *  JBoss Portal: The OpenSource Portal  *
 *                                       *
 *   Distributable under LGPL license.   *
 *   See terms of license at gnu.org.    *
 *                                       *
 *****************************************/
package org.jboss.cache.notifications;

import org.jboss.cache.Cache;
import org.jboss.cache.DefaultCacheFactory;
import org.jboss.cache.Fqn;
import org.jboss.cache.Node;
import org.jboss.cache.config.Configuration;
import org.jboss.cache.lock.IsolationLevel;
import org.jboss.cache.notifications.event.Event;
import static org.jboss.cache.notifications.event.Event.Type.*;
import org.jboss.cache.notifications.event.EventImpl;
import static org.jboss.cache.notifications.event.NodeModifiedEvent.ModificationType.*;
import static org.testng.AssertJUnit.assertEquals;
import static org.testng.AssertJUnit.assertNull;
import org.testng.annotations.AfterMethod;
import org.testng.annotations.BeforeMethod;
import org.testng.annotations.Test;

import javax.transaction.Transaction;
import javax.transaction.TransactionManager;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * Note that this is significantly different from the old <b>TreeCacheListenerTest</b> of the JBoss Cache 1.x series, and
 * exercises the new CacheListener annotation.
 *
 * @since 2.0.0
 */
@Test(groups = "functional")
public class CacheListenerTest
{
   protected boolean optLocking = false;

   private Cache<Object, Object> cache;
   private TransactionManager tm;
   private EventLog eventLog = new EventLog();
   private Fqn<String> fqn = Fqn.fromString("/test");

   @BeforeMethod(alwaysRun = true)
   public void setUp() throws Exception
   {
      Configuration c = new Configuration();
      c.setCacheMode(Configuration.CacheMode.LOCAL);
      c.setIsolationLevel(IsolationLevel.REPEATABLE_READ);
      if (optLocking)
         c.setNodeLockingScheme(Configuration.NodeLockingScheme.OPTIMISTIC);
      c.setTransactionManagerLookupClass("org.jboss.cache.transaction.DummyTransactionManagerLookup");
      cache = new DefaultCacheFactory().createCache(c);
      tm = cache.getConfiguration().getRuntimeConfig().getTransactionManager();
      eventLog.events.clear();
      cache.addCacheListener(eventLog);
   }

   @AfterMethod(alwaysRun = true)
   public void tearDown() throws Exception
   {
      Transaction t = tm.getTransaction();
      if (t != null)
         tm.rollback();
      cache.stop();
      cache.destroy();
   }

   // simple tests first

   public void testCreation() throws Exception
   {
      assertEquals("Event log should be empty", Collections.emptyList(), eventLog.events);
      cache.put(fqn, "key", "value");
      Map<Object, Object> data = new HashMap<Object, Object>();
      data.put("key", "value");

      //expected
      List<Event> expected = new ArrayList<Event>();
      if (optLocking)
         expected.add(new EventImpl(false, cache, null, null, null, null, true, null, false, null, TRANSACTION_REGISTERED));
      expected.add(new EventImpl(true, cache, null, null, fqn, null, true, null, false, null, NODE_CREATED));
      expected.add(new EventImpl(false, cache, null, null, fqn, null, true, null, false, null, NODE_CREATED));
      expected.add(new EventImpl(true, cache, PUT_DATA, Collections.emptyMap(), fqn, null, true, null, false, null, NODE_MODIFIED));
      expected.add(new EventImpl(false, cache, PUT_DATA, data, fqn, null, true, null, false, null, NODE_MODIFIED));
      if (optLocking)
      {
         expected.add(new EventImpl(false, cache, null, null, null, null, true, null, true, null, TRANSACTION_COMPLETED));
         eventLog.scrubImplicitTransactions();
      }
      assertEquals(expected, eventLog.events);
      assertEquals("value", cache.get(fqn, "key"));
   }

   public void testOnlyModification() throws Exception
   {
      assertEquals("Event log should be empty", Collections.emptyList(), eventLog.events);
      cache.put(fqn, "key", "value");
      Map<Object, Object> oldData = new HashMap<Object, Object>();
      oldData.put("key", "value");

      // clear Event log
      eventLog.events.clear();
      assertEquals("Event log should be empty", Collections.emptyList(), eventLog.events);

      // modify existing node
      cache.put(fqn, "key", "value2");
      Map<Object, Object> newData = new HashMap<Object, Object>();
      newData.put("key", "value2");

      //expected
      List<Event> expected = new ArrayList<Event>();
      if (optLocking)
         expected.add(new EventImpl(false, cache, null, null, null, null, true, null, false, null, TRANSACTION_REGISTERED));
      expected.add(new EventImpl(true, cache, PUT_DATA, oldData, fqn, null, true, null, false, null, NODE_MODIFIED));
      expected.add(new EventImpl(false, cache, PUT_DATA, newData, fqn, null, true, null, false, null, NODE_MODIFIED));
      if (optLocking)
      {
         expected.add(new EventImpl(false, cache, null, null, null, null, true, null, true, null, TRANSACTION_COMPLETED));
         eventLog.scrubImplicitTransactions();
      }

      assertEquals(expected.size(), eventLog.events.size());
      assertEquals(expected, eventLog.events);
   }

   public void testOnlyRemoval() throws Exception
   {
      assertEquals("Event log should be empty", Collections.emptyList(), eventLog.events);
      cache.put(fqn, "key", "value");
      Map<Object, Object> oldData = new HashMap<Object, Object>();
      oldData.put("key", "value");

      assertEquals("value", cache.get(fqn, "key"));

      // clear Event log
      eventLog.events.clear();
      assertEquals("Event log should be empty", Collections.emptyList(), eventLog.events);

      // modify existing node
      cache.removeNode(fqn);

      //expected
      List<Event> expected = new ArrayList<Event>();
      if (optLocking)
         expected.add(new EventImpl(false, cache, null, null, null, null, true, null, false, null, TRANSACTION_REGISTERED));
      expected.add(new EventImpl(true, cache, null, oldData, fqn, null, true, null, false, null, NODE_REMOVED));
      expected.add(new EventImpl(false, cache, null, null, fqn, null, true, null, false, null, NODE_REMOVED));
      if (optLocking)
      {
         expected.add(new EventImpl(false, cache, null, null, null, null, true, null, true, null, TRANSACTION_COMPLETED));
         eventLog.scrubImplicitTransactions();
      }

      assertEquals(expected, eventLog.events);

      // test that the node has in fact been removed.
      assertNull("Should be null", cache.getRoot().getChild(fqn));
   }

   public void testNonexistentRemove() throws Exception
   {
      cache.removeNode("/does/not/exist");
      List<Event> expected = new ArrayList<Event>();

      if (optLocking)
      {
         expected.add(new EventImpl(false, cache, null, null, null, null, true, null, false, null, TRANSACTION_REGISTERED));
         expected.add(new EventImpl(false, cache, null, null, null, null, true, null, true, null, TRANSACTION_COMPLETED));
         eventLog.scrubImplicitTransactions();
      }
      assertEquals(expected, eventLog.events);
   }

   public void testRemoveData() throws Exception
   {
      assertEquals("Event log should be empty", Collections.emptyList(), eventLog.events);
      cache.put(fqn, "key", "value");
      cache.put(fqn, "key2", "value2");
      Map<Object, Object> oldData = new HashMap<Object, Object>();
      oldData.put("key", "value");
      oldData.put("key2", "value2");

      // clear Event log
      eventLog.events.clear();
      assertEquals("Event log should be empty", Collections.emptyList(), eventLog.events);

      // modify existing node
      cache.remove(fqn, "key2");
      Map<Object, Object> removedData = new HashMap<Object, Object>();
      removedData.put("key2", "value2");

      //expected
      List<Event> expected = new ArrayList<Event>();

      if (optLocking)
         expected.add(new EventImpl(false, cache, null, null, null, null, true, null, false, null, TRANSACTION_REGISTERED));
      expected.add(new EventImpl(true, cache, REMOVE_DATA, oldData, fqn, null, true, null, false, null, NODE_MODIFIED));
      expected.add(new EventImpl(false, cache, REMOVE_DATA, removedData, fqn, null, true, null, false, null, NODE_MODIFIED));
      if (optLocking)
      {
         expected.add(new EventImpl(false, cache, null, null, null, null, true, null, true, null, TRANSACTION_COMPLETED));
         eventLog.scrubImplicitTransactions();
      }

      assertEquals(expected, eventLog.events);
   }

   public void testPutMap() throws Exception
   {
      assertEquals("Event log should be empty", Collections.emptyList(), eventLog.events);
      Map<Object, Object> oldData = new HashMap<Object, Object>();
      oldData.put("key", "value");
      oldData.put("key2", "value2");

      // clear Event log
      eventLog.events.clear();
      assertEquals("Event log should be empty", Collections.emptyList(), eventLog.events);

      // modify existing node
      cache.put(fqn, oldData);

      //expected
      List<Event> expected = new ArrayList<Event>();
      if (optLocking)
         expected.add(new EventImpl(false, cache, null, null, null, null, true, null, false, null, TRANSACTION_REGISTERED));
      expected.add(new EventImpl(true, cache, null, null, fqn, null, true, null, false, null, NODE_CREATED));
      expected.add(new EventImpl(false, cache, null, null, fqn, null, true, null, false, null, NODE_CREATED));
      expected.add(new EventImpl(true, cache, PUT_MAP, Collections.emptyMap(), fqn, null, true, null, false, null, NODE_MODIFIED));
      expected.add(new EventImpl(false, cache, PUT_MAP, oldData, fqn, null, true, null, false, null, NODE_MODIFIED));
      if (optLocking)
      {
         expected.add(new EventImpl(false, cache, null, null, null, null, true, null, true, null, TRANSACTION_COMPLETED));
         eventLog.scrubImplicitTransactions();
      }

      assertEquals(expected, eventLog.events);
   }

   public void testMove()
   {
      assertEquals("Event log should be empty", Collections.emptyList(), eventLog.events);
      Fqn<String> newParent = Fqn.fromString("/a");
      cache.put(fqn, "key", "value");
      cache.put(newParent, "key", "value");

      Node<Object, Object> n1 = cache.getRoot().getChild(fqn);
      Node<Object, Object> n2 = cache.getRoot().getChild(newParent);
      eventLog.events.clear();// clear events
      assertEquals("Event log should be empty", Collections.emptyList(), eventLog.events);

      cache.move(n1.getFqn(), n2.getFqn());
      //expected
      Fqn newFqn = Fqn.fromRelativeElements(newParent, fqn.getLastElement());
      List<Event> expected = new ArrayList<Event>();

      if (optLocking)
         expected.add(new EventImpl(false, cache, null, null, null, null, true, null, false, null, TRANSACTION_REGISTERED));
      expected.add(new EventImpl(true, cache, null, null, fqn, null, true, newFqn, false, null, NODE_MOVED));
      expected.add(new EventImpl(false, cache, null, null, fqn, null, true, newFqn, false, null, NODE_MOVED));
      if (optLocking)
      {
         expected.add(new EventImpl(false, cache, null, null, null, null, true, null, true, null, TRANSACTION_COMPLETED));
         eventLog.scrubImplicitTransactions();
      }

      assertEquals(expected, eventLog.events);
   }

   // -- now the transactional ones

   public void testTxNonexistentRemove() throws Exception
   {
      tm.begin();
      Transaction tx = tm.getTransaction();
      cache.removeNode("/does/not/exist");
      tm.commit();
      List<Event> expected = new ArrayList<Event>();
      expected.add(new EventImpl(false, cache, null, null, null, tx, true, null, false, null, TRANSACTION_REGISTERED));
      expected.add(new EventImpl(false, cache, null, null, null, tx, true, null, true, null, TRANSACTION_COMPLETED));
      assertEquals(expected, eventLog.events);
   }

   public void testTxCreationCommit() throws Exception
   {
      assertEquals("Event log should be empty", Collections.emptyList(), eventLog.events);
      tm.begin();
      Transaction tx = tm.getTransaction();
      cache.put(fqn, "key", "value");
      //expected
      Map<Object, Object> data = new HashMap<Object, Object>();
      data.put("key", "value");
      List<Event> expected = new ArrayList<Event>();

      expected.add(new EventImpl(false, cache, null, null, null, tx, true, null, false, null, TRANSACTION_REGISTERED));
      expected.add(new EventImpl(true, cache, null, null, fqn, tx, true, null, false, null, NODE_CREATED));
      expected.add(new EventImpl(false, cache, null, null, fqn, tx, true, null, false, null, NODE_CREATED));
      expected.add(new EventImpl(true, cache, PUT_DATA, Collections.emptyMap(), fqn, tx, true, null, false, null, NODE_MODIFIED));
      expected.add(new EventImpl(false, cache, PUT_DATA, data, fqn, tx, true, null, false, null, NODE_MODIFIED));
      assertEquals(expected, eventLog.events);
      tm.commit();
      expected.add(new EventImpl(false, cache, null, null, null, tx, true, null, true, null, TRANSACTION_COMPLETED));
      assertEquals(expected, eventLog.events);
      assertEquals("value", cache.get(fqn, "key"));
   }

   public void testTxCreationRollback() throws Exception
   {
      assertEquals("Event log should be empty", Collections.emptyList(), eventLog.events);
      tm.begin();
      Transaction tx = tm.getTransaction();
      cache.put(fqn, "key", "value");
      //expected
      Map<Object, Object> data = new HashMap<Object, Object>();
      data.put("key", "value");
      List<Event> expected = new ArrayList<Event>();
      expected.add(new EventImpl(false, cache, null, null, null, tx, true, null, false, null, TRANSACTION_REGISTERED));
      expected.add(new EventImpl(true, cache, null, null, fqn, tx, true, null, false, null, NODE_CREATED));
      expected.add(new EventImpl(false, cache, null, null, fqn, tx, true, null, false, null, NODE_CREATED));
      expected.add(new EventImpl(true, cache, PUT_DATA, Collections.emptyMap(), fqn, tx, true, null, false, null, NODE_MODIFIED));
      expected.add(new EventImpl(false, cache, PUT_DATA, data, fqn, tx, true, null, false, null, NODE_MODIFIED));

      assertEquals(expected, eventLog.events);
      tm.rollback();
      expected.add(new EventImpl(false, cache, null, null, null, tx, true, null, false, null, TRANSACTION_COMPLETED));
      assertEquals(expected, eventLog.events);
   }

   public void testTxOnlyModification() throws Exception
   {
      assertEquals("Event log should be empty", Collections.emptyList(), eventLog.events);
      cache.put(fqn, "key", "value");
      Map<Object, Object> oldData = new HashMap<Object, Object>();
      oldData.put("key", "value");

      // clear Event log
      eventLog.events.clear();
      assertEquals("Event log should be empty", Collections.emptyList(), eventLog.events);

      // modify existing node
      tm.begin();
      Transaction tx = tm.getTransaction();
      cache.put(fqn, "key", "value2");
      Map<Object, Object> newData = new HashMap<Object, Object>();
      newData.put("key", "value2");

      //expected
      List<Event> expected = new ArrayList<Event>();
      expected.add(new EventImpl(false, cache, null, null, null, tx, true, null, false, null, TRANSACTION_REGISTERED));
      expected.add(new EventImpl(true, cache, PUT_DATA, oldData, fqn, tx, true, null, false, null, NODE_MODIFIED));
      expected.add(new EventImpl(false, cache, PUT_DATA, newData, fqn, tx, true, null, false, null, NODE_MODIFIED));

      assertEquals(expected, eventLog.events);
      tm.commit();
      expected.add(new EventImpl(false, cache, null, null, null, tx, true, null, true, null, TRANSACTION_COMPLETED));
      assertEquals(expected, eventLog.events);
   }

   public void testTxOnlyRemoval() throws Exception
   {
      assertEquals("Event log should be empty", Collections.emptyList(), eventLog.events);
      cache.put(fqn, "key", "value");
      Map<Object, Object> oldData = new HashMap<Object, Object>();
      oldData.put("key", "value");

      assertEquals("value", cache.get(fqn, "key"));

      // clear Event log
      eventLog.events.clear();
      assertEquals("Event log should be empty", Collections.emptyList(), eventLog.events);

      // modify existing node
      tm.begin();
      Transaction tx = tm.getTransaction();
      cache.removeNode(fqn);
      //expected
      List<Event> expected = new ArrayList<Event>();

      expected.add(new EventImpl(false, cache, null, null, null, tx, true, null, false, null, TRANSACTION_REGISTERED));
      expected.add(new EventImpl(true, cache, null, oldData, fqn, tx, true, null, false, null, NODE_REMOVED));
      expected.add(new EventImpl(false, cache, null, null, fqn, tx, true, null, false, null, NODE_REMOVED));

      assertEquals(expected, eventLog.events);
      tm.commit();
      expected.add(new EventImpl(false, cache, null, null, null, tx, true, null, true, null, TRANSACTION_COMPLETED));
      assertEquals(expected, eventLog.events);
      // test that the node has in fact been removed.
      assertNull("Should be null", cache.getRoot().getChild(fqn));
   }

   public void testTxRemoveData() throws Exception
   {
      assertEquals("Event log should be empty", Collections.emptyList(), eventLog.events);
      cache.put(fqn, "key", "value");
      cache.put(fqn, "key2", "value2");
      Map<Object, Object> oldData = new HashMap<Object, Object>();
      oldData.put("key", "value");
      oldData.put("key2", "value2");

      // clear Event log
      eventLog.events.clear();
      assertEquals("Event log should be empty", Collections.emptyList(), eventLog.events);

      // modify existing node
      tm.begin();
      Transaction tx = tm.getTransaction();
      cache.remove(fqn, "key2");
      Map<Object, Object> removedData = new HashMap<Object, Object>();
      removedData.put("key2", "value2");

      //expected
      List<Event> expected = new ArrayList<Event>();
      expected.add(new EventImpl(false, cache, null, null, null, tx, true, null, false, null, TRANSACTION_REGISTERED));
      expected.add(new EventImpl(true, cache, REMOVE_DATA, oldData, fqn, tx, true, null, false, null, NODE_MODIFIED));
      expected.add(new EventImpl(false, cache, REMOVE_DATA, removedData, fqn, tx, true, null, false, null, NODE_MODIFIED));

      tm.commit();
      expected.add(new EventImpl(false, cache, null, null, null, tx, true, null, true, null, TRANSACTION_COMPLETED));
      assertEquals(expected, eventLog.events);

      assertEquals(expected, eventLog.events);
   }

   public void testTxMove() throws Exception
   {
      assertEquals("Event log should be empty", Collections.emptyList(), eventLog.events);
      Fqn<String> newParent = Fqn.fromString("/a");
      cache.put(fqn, "key", "value");
      cache.put(newParent, "key", "value");

      Node<Object, Object> n1 = cache.getRoot().getChild(fqn);
      Node<Object, Object> n2 = cache.getRoot().getChild(newParent);
      eventLog.events.clear();// clear events
      assertEquals("Event log should be empty", Collections.emptyList(), eventLog.events);

      tm.begin();
      Transaction tx = tm.getTransaction();
      cache.move(n1.getFqn(), n2.getFqn());
      //expected
      Fqn newFqn = Fqn.fromRelativeElements(newParent, fqn.getLastElement());
      List<Event> expected = new ArrayList<Event>();
      expected.add(new EventImpl(false, cache, null, null, null, tx, true, null, false, null, TRANSACTION_REGISTERED));
      expected.add(new EventImpl(true, cache, null, null, fqn, tx, true, newFqn, false, null, NODE_MOVED));
      expected.add(new EventImpl(false, cache, null, null, fqn, tx, true, newFqn, false, null, NODE_MOVED));

      assertEquals(expected, eventLog.events);
      tm.commit();
      expected.add(new EventImpl(false, cache, null, null, null, tx, true, null, true, null, TRANSACTION_COMPLETED));
      assertEquals(expected, eventLog.events);
   }
}
