/*
 * JBoss, the OpenSource J2EE webOS
 *
 * Distributable under LGPL license.
 * See terms of license at gnu.org.
 */
package org.jboss.cache.interceptors;

import org.jboss.cache.CacheSPI;
import org.jboss.cache.DefaultCacheFactory;
import org.jboss.cache.Fqn;
import org.jboss.cache.Node;
import org.jboss.cache.NodeSPI;
import org.jboss.cache.Region;
import org.jboss.cache.RegionManager;
import org.jboss.cache.util.TestingUtil;
import org.jboss.cache.commands.VisitableCommand;
import org.jboss.cache.commands.read.GetKeyValueCommand;
import org.jboss.cache.commands.read.GetNodeCommand;
import org.jboss.cache.commands.write.PutDataMapCommand;
import org.jboss.cache.commands.write.PutKeyValueCommand;
import org.jboss.cache.commands.write.ClearDataCommand;
import org.jboss.cache.commands.write.RemoveKeyCommand;
import org.jboss.cache.commands.write.RemoveNodeCommand;
import org.jboss.cache.config.EvictionConfig;
import org.jboss.cache.config.EvictionRegionConfig;
import org.jboss.cache.eviction.DummyEvictionConfiguration;
import org.jboss.cache.eviction.EvictedEventNode;
import org.jboss.cache.eviction.NodeEventType;
import org.jboss.cache.factories.CommandsFactory;
import org.jboss.cache.lock.IsolationLevel;
import static org.testng.AssertJUnit.*;
import org.testng.annotations.AfterMethod;
import org.testng.annotations.BeforeMethod;
import org.testng.annotations.Test;

import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;

/**
 * @author Daniel Huang (dhuang@jboss.org)
 * @version $Revision: $
 */
@Test(groups = "functional")
public class EvictionInterceptorTest
{
   private static final String fqn1 = "/a/b/c";
   private static final String fqn2 = "/a/b";
   private static final String fqn3 = "/a/b/d";
   private static final String fqn4 = "/d/e/f";

   private CacheSPI<Object, Object> cache;
   private InterceptorChain invoker;
   private RegionManager regionManager;
   private CommandsFactory commandsFactory;

   @BeforeMethod(alwaysRun = true)
   public void setUp() throws Exception
   {
      cache = (CacheSPI<Object, Object>) new DefaultCacheFactory().createCache(false);
      cache.getConfiguration().setTransactionManagerLookupClass("org.jboss.cache.transaction.DummyTransactionManagerLookup");
      cache.getConfiguration().setIsolationLevel(IsolationLevel.SERIALIZABLE);
      cache.getConfiguration().setCacheMode("LOCAL");
      EvictionConfig ec = new EvictionConfig();

      List<EvictionRegionConfig> ercs = new LinkedList<EvictionRegionConfig>();
      ercs.add(new EvictionRegionConfig(Fqn.ROOT, new DummyEvictionConfiguration()));
      ercs.add(new EvictionRegionConfig(Fqn.fromString("/a/b/c"), new DummyEvictionConfiguration()));
      ercs.add(new EvictionRegionConfig(Fqn.fromString("/a/b/c/d"), new DummyEvictionConfiguration()));
      ercs.add(new EvictionRegionConfig(Fqn.fromString("/a/b"), new DummyEvictionConfiguration()));
      ercs.add(new EvictionRegionConfig(Fqn.fromString("/d/e/f"), new DummyEvictionConfiguration()));
      ercs.add(new EvictionRegionConfig(Fqn.fromString("/d/e/g"), new DummyEvictionConfiguration()));
      ercs.add(new EvictionRegionConfig(Fqn.fromString("/d/e"), new DummyEvictionConfiguration()));

      ec.setEvictionRegionConfigs(ercs);
      cache.getConfiguration().setEvictionConfig(ec);
      cache.start();

      invoker = TestingUtil.extractComponentRegistry(cache).getComponent(InterceptorChain.class);
      commandsFactory = TestingUtil.extractCommandsFactory(cache);
      regionManager = cache.getRegionManager();
   }

   @AfterMethod(alwaysRun = true)
   public void tearDown() throws Exception
   {
      TestingUtil.killCaches(cache);
   }

   @SuppressWarnings("unchecked")
   private NodeSPI<Object, Object> cast(Node node)
   {
      return (NodeSPI<Object, Object>) node;
   }

   public void testVisitNode() throws Throwable
   {
      // make sure node that doesn't exist does not result in a node visit event.

      VisitableCommand command = commandsFactory.buildGetNodeCommand(Fqn.fromString(fqn1));
      invoker.invoke(command);
      Region regionABC = regionManager.getRegion(fqn1, false);
      assertNull(regionABC.takeLastEventNode());

      putQuietly(fqn1, "key", "value");
      NodeSPI<Object, Object> node = cast(cache.peek(Fqn.fromString(fqn1), false, false));
      assertNotNull(node);
      assertEquals("value", node.getDirect("key"));

      putQuietly(fqn3, "key", "value");
      node = cast(cache.peek(Fqn.fromString(fqn3), false, false));
      assertNotNull(node);
      assertEquals("value", node.getDirect("key"));


      command = commandsFactory.buildGetNodeCommand(Fqn.fromString(fqn1));
      invoker.invoke(command);

      regionABC = regionManager.getRegion(fqn1, false);
      EvictedEventNode event = regionABC.takeLastEventNode();
      assertEquals(NodeEventType.VISIT_NODE_EVENT, event.getEventType());
      assertEquals(fqn1, event.getFqn().toString());
      assertNull(regionABC.takeLastEventNode());

      command = commandsFactory.buildGetNodeCommand(Fqn.fromString(fqn2));
      invoker.invoke(command);

      Region regionAB = regionManager.getRegion(fqn2, false);
      event = regionAB.takeLastEventNode();
      assertEquals(NodeEventType.VISIT_NODE_EVENT, event.getEventType());
      assertEquals(fqn2, event.getFqn().toString());
      assertNull(regionAB.takeLastEventNode());

      command = commandsFactory.buildGetNodeCommand(Fqn.fromString(fqn3));
      invoker.invoke(command);
      Region regionABD = regionManager.getRegion(fqn3, false);
      event = regionABD.takeLastEventNode();
      assertEquals(NodeEventType.VISIT_NODE_EVENT, event.getEventType());
      assertEquals(fqn3, event.getFqn().toString());
      assertNull(regionABD.takeLastEventNode());

      for (int i = 0; i < 10; i++)
      {
         command = commandsFactory.buildGetNodeCommand(Fqn.fromString(fqn3));
         invoker.invoke(command);
      }

      for (int i = 0; i < 10; i++)
      {
         Region region = regionManager.getRegion(fqn3, false);
         event = region.takeLastEventNode();
         assertEquals(NodeEventType.VISIT_NODE_EVENT, event.getEventType());
         assertEquals(fqn3, event.getFqn().toString());
      }

      assertNull(regionManager.getRegion(fqn3, false).takeLastEventNode());

      // check null handling.
      command = commandsFactory.buildGetDataMapCommand(null);
      invoker.invoke(command);

   }

   /**
    * Helper to quietly add something into the cache without generating eviction events
    *
    * @param fqn   fqn to add
    * @param key   key
    * @param value value
    */
   private void putQuietly(String fqn, Object key, Object value)
   {
      putQuietly(Fqn.fromString(fqn), key, value);
   }

   /**
    * Helper to quietly add something into the cache without generating eviction events
    *
    * @param fqn   fqn to add
    * @param key   key
    * @param value value
    */
   private void putQuietly(Fqn fqn, Object key, Object value)
   {
      NodeSPI root = cache.getRoot();
      NodeSPI child = root;
      for (int i = 0; i < fqn.size(); i++)
      {
         child = child.addChildDirect(Fqn.fromElements(fqn.get(i)));
      }

      assert child.getFqn().equals(fqn);

      child.putDirect(key, value);
   }

   public void testVisitElement() throws Throwable
   {
      // make sure a get/visit on an empty node and empty element results in no cache events being added to event queue
      // aka MarshRegion.
      Fqn fqn = Fqn.fromString(fqn4);
      Object key = "key";
      GetKeyValueCommand command = commandsFactory.buildGetKeyValueCommand(fqn, key, false);
      invoker.invoke(command);
      Region region = regionManager.getRegion(fqn.toString(), false);
      assertNull(region.takeLastEventNode());

      // add the node but try to get on a null element should result in no cache events being added to Region.
      putQuietly(fqn, "wrongkey", "");

      command = commandsFactory.buildGetKeyValueCommand(fqn, key, false);
      invoker.invoke(command);
      assertNull(region.takeLastEventNode());

      // now make sure if we try to get on the node/key we just created in cache, that this DOES add a EvictedEventNode to
      // the MarshRegion.
      command = commandsFactory.buildGetKeyValueCommand(fqn, "wrongkey", false);
      invoker.invoke(command);
      EvictedEventNode event = region.takeLastEventNode();
      assertEquals(fqn, event.getFqn());
      assertEquals(NodeEventType.VISIT_NODE_EVENT, event.getEventType());

      assertNull(region.takeLastEventNode());

      putQuietly(fqn4, key, "value");

      // test on element granularity configured node.
      fqn = Fqn.fromString(fqn4);
      command = commandsFactory.buildGetKeyValueCommand(fqn, key, false);
      invoker.invoke(command);

      region = regionManager.getRegion(fqn.toString(), false);
      event = region.takeLastEventNode();

      assertEquals(NodeEventType.VISIT_NODE_EVENT, event.getEventType());
      assertEquals(fqn, event.getFqn());

      assertNull(region.takeLastEventNode());

      fqn = Fqn.fromString("/d/e/g");
      for (int i = 0; i < 100; i++)
      {
         key = i;

         putQuietly("/d/e/g", key, "");

         command = commandsFactory.buildGetKeyValueCommand(fqn, key, false);
         invoker.invoke(command);
      }

      region = regionManager.getRegion(fqn.toString(), false);

      for (int i = 0; i < 100; i++)
      {
         event = region.takeLastEventNode();
         assertEquals(fqn, event.getFqn());
         assertEquals(NodeEventType.VISIT_NODE_EVENT, event.getEventType());
      }

      putQuietly("/a/b/c", key, "");
      fqn = Fqn.fromString("/a/b/c");

      command = commandsFactory.buildGetKeyValueCommand(fqn, key, false);
      invoker.invoke(command);

      region = regionManager.getRegion(fqn.toString(), false);
      event = region.takeLastEventNode();

      assertNull(region.takeLastEventNode());
      assertEquals(NodeEventType.VISIT_NODE_EVENT, event.getEventType());
      assertEquals(fqn, event.getFqn());

      for (int i = 0; i < 100; i++)
      {
         key = i;

         putQuietly(fqn, key, "");

         command = commandsFactory.buildGetKeyValueCommand(fqn, key, false);
         invoker.invoke(command);
      }

      for (int i = 0; i < 100; i++)
      {
         event = region.takeLastEventNode();
         assertEquals(NodeEventType.VISIT_NODE_EVENT, event.getEventType());
         assertEquals(fqn, event.getFqn());
      }

      assertNull(region.takeLastEventNode());
   }

   public void testCreateNode() throws Throwable
   {
      Map<Object, Object> data = new HashMap<Object, Object>();
      for (int i = 0; i < 100; i++)
      {
         data.put(i, i);
      }

      // this region is node granularity
      Fqn fqn = Fqn.fromString("/a/b/c");

      PutDataMapCommand putDataMapCommand = commandsFactory.buildPutDataMapCommand(null, fqn, data);
      invoker.invoke(putDataMapCommand);

      Region region = regionManager.getRegion(fqn.toString(), false);
      EvictedEventNode event = region.takeLastEventNode();

      assertEquals(NodeEventType.ADD_NODE_EVENT, event.getEventType());
      assertEquals(fqn, event.getFqn());
      assertEquals(100, event.getElementDifference());

      NodeSPI<Object, Object> node = cast(cache.peek(fqn, false, false));
      assertNotNull(node);

      for (int i = 0; i < 100; i++)
      {
         assertTrue(node.getDataDirect().containsKey(i));
         assertEquals(i, node.getDirect(i));
      }

      for (int i = 0; i < 100; i++)
      {
         PutKeyValueCommand pkvCommand = commandsFactory.buildPutKeyValueCommand(null, (Fqn<?>) fqn, i, "value");
         invoker.invoke(pkvCommand);

         assertEquals("value", cache.peek(fqn, false, false).getDirect(i));
      }

      for (int i = 0; i < 100; i++)
      {
         event = region.takeLastEventNode();
         assertNotNull(event);
         assertEquals(fqn, event.getFqn());
         assertEquals(NodeEventType.ADD_ELEMENT_EVENT, event.getEventType());
      }

      assertNull(region.takeLastEventNode());

      fqn = Fqn.fromString("/a/b");
      PutDataMapCommand putCommand = commandsFactory.buildPutDataMapCommand(null, fqn, data);
      invoker.invoke(putCommand);
      event = regionManager.getRegion(fqn.toString(), false).takeLastEventNode();
      assertEquals(NodeEventType.ADD_NODE_EVENT, event.getEventType());
      assertEquals(fqn, event.getFqn());
      assertEquals(100, event.getElementDifference());
      assertNull(regionManager.getRegion(fqn.toString(), false).takeLastEventNode());
      node = cast(cache.peek(fqn, false, false));
      assertEquals(100, node.getDataDirect().size());

      assertNotNull(node);

      for (int i = 0; i < 100; i++)
      {
         assertTrue(node.getDataDirect().containsKey(i));
         assertEquals(i, node.getDirect(i));
      }

      PutDataMapCommand putDataMap = commandsFactory.buildPutDataMapCommand(null, fqn, data);
      invoker.invoke(putDataMap);
      event = regionManager.getRegion(fqn.toString(), false).takeLastEventNode();
      assertEquals(NodeEventType.ADD_NODE_EVENT, event.getEventType());
      assertEquals(fqn, event.getFqn());
      assertEquals(100, event.getElementDifference());
      assertNull(regionManager.getRegion(fqn.toString(), false).takeLastEventNode());


      node = cast(cache.getNode(fqn));
      assertEquals(100, node.getData().size());
      assertNotNull(node);

      for (int i = 0; i < 100; i++)
      {
         assertTrue(node.getDataDirect().containsKey(i));
         assertEquals(i, node.getDirect(i));
      }

   }

   public void testCreateElement() throws Throwable
   {
      Fqn fqn = Fqn.fromString("/d/e/f");
      Region region = regionManager.getRegion(fqn.toString(), false);
      Object key = "key";
      Object value = "value";

      PutKeyValueCommand command = commandsFactory.buildPutKeyValueCommand(null, (Fqn<?>) fqn, key, value);
      invoker.invoke(command);
      assertEquals("value", cache.peek(fqn, false, false).getDirect(key));
      EvictedEventNode event = region.takeLastEventNode();
      assertEquals(NodeEventType.ADD_ELEMENT_EVENT, event.getEventType());
      assertEquals(1, event.getElementDifference());
      assertEquals(fqn, event.getFqn());
      assertEquals("value", cache.peek(fqn, false, false).getDirect(key));
      assertNull(region.takeLastEventNode());

      command = commandsFactory.buildPutKeyValueCommand(null, (Fqn<?>) fqn, key, value);
      invoker.invoke(command);
      assertEquals("value", cache.peek(fqn, false, false).getDirect(key));
      event = region.takeLastEventNode();
      assertEquals(NodeEventType.ADD_ELEMENT_EVENT, event.getEventType());
      assertEquals(1, event.getElementDifference());
      assertEquals(fqn, event.getFqn());
      assertEquals("value", cache.peek(fqn, false, false).getDirect(key));
      assertNull(region.takeLastEventNode());

   }

   public void testRemoveNode() throws Throwable
   {
      Fqn fqn = Fqn.fromString("/a/b/c");
      putQuietly(fqn, "a", "b");
      putQuietly(fqn, "b", "c");

      ClearDataCommand clearDataCommand = commandsFactory.buildClearDataCommand(null, fqn);
      invoker.invoke(clearDataCommand);

      assertEquals(0, cache.peek(fqn, false, false).getDataDirect().size());
      Region region = regionManager.getRegion(fqn.toString(), false);
      EvictedEventNode event = region.takeLastEventNode();
      assertEquals(NodeEventType.REMOVE_NODE_EVENT, event.getEventType());
      assertEquals(fqn, event.getFqn());
      assertNull(region.takeLastEventNode());

      RemoveNodeCommand removeNodeCommand = commandsFactory.buildRemoveNodeCommand(null, fqn);
      invoker.invoke(removeNodeCommand);

      assertNull(cache.peek(fqn, false, false));
      event = region.takeLastEventNode();
      assertEquals(NodeEventType.REMOVE_NODE_EVENT, event.getEventType());
      assertEquals(fqn, event.getFqn());
      assertNull(region.takeLastEventNode());
   }

   public void testRemoveElement() throws Throwable
   {
      Fqn fqn = Fqn.fromString("/a/b/c");
      putQuietly(fqn, "a", "b");
      putQuietly(fqn, "b", "c");

      RemoveKeyCommand removeKeyCommand = commandsFactory.buildRemoveKeyCommand(null, fqn, "a");
      invoker.invoke(removeKeyCommand);

      assertNull(cache.get(fqn, "a"));
      Region region = regionManager.getRegion(fqn.toString(), false);
      EvictedEventNode event = region.takeLastEventNode();
      assertEquals(NodeEventType.REMOVE_ELEMENT_EVENT, event.getEventType());
      assertEquals(fqn, event.getFqn());
      assertEquals(1, event.getElementDifference());
      assertNull(region.takeLastEventNode());

      RemoveKeyCommand removeKeyCommand2 = commandsFactory.buildRemoveKeyCommand(null, fqn, "b");
      invoker.invoke(removeKeyCommand2);

      assertNull(cache.get(fqn, "b"));
      event = region.takeLastEventNode();
      assertEquals(NodeEventType.REMOVE_ELEMENT_EVENT, event.getEventType());
      assertEquals(fqn, event.getFqn());
      assertEquals(1, event.getElementDifference());
      assertNull(region.takeLastEventNode());

      RemoveKeyCommand removeKeyCommand3 = commandsFactory.buildRemoveKeyCommand(null, fqn, "a");
      invoker.invoke(removeKeyCommand3);

      event = region.takeLastEventNode();
      assertNull(event);
   }

   public void testMixedEvent() throws Throwable
   {
      Map<Object, Object> data = new HashMap<Object, Object>();
      for (int i = 0; i < 100; i++)
      {
         data.put(i, i);
      }

      // this region is node granularity
      Fqn fqn = Fqn.fromString("/a/b/c");

      PutDataMapCommand putDataCommand = commandsFactory.buildPutDataMapCommand(null, fqn, data);
      invoker.invoke(putDataCommand);

      Region region = regionManager.getRegion(fqn.toString(), false);
      EvictedEventNode event = region.takeLastEventNode();

      assertEquals(NodeEventType.ADD_NODE_EVENT, event.getEventType());
      assertEquals(fqn, event.getFqn());
      assertEquals(100, event.getElementDifference());
      assertNull(region.takeLastEventNode());

      GetNodeCommand getNodeCommand = commandsFactory.buildGetNodeCommand(fqn);
      invoker.invoke(getNodeCommand);
      event = region.takeLastEventNode();
      assertEquals(NodeEventType.VISIT_NODE_EVENT, event.getEventType());
      assertEquals(fqn, event.getFqn());
      assertNull(region.takeLastEventNode());

      RemoveNodeCommand removeNodeCommand = commandsFactory.buildRemoveNodeCommand(null, fqn);
      invoker.invoke(removeNodeCommand);
      assertNull(cache.getNode(fqn));
      event = region.takeLastEventNode();
      assertEquals(NodeEventType.REMOVE_NODE_EVENT, event.getEventType());
      assertEquals(fqn, event.getFqn());
      assertNull(region.takeLastEventNode());

      Object key = "key";
      Object value = "value";
      PutKeyValueCommand putKeyValueCommand = commandsFactory.buildPutKeyValueCommand(null, (Fqn<?>) fqn, key, value);
      invoker.invoke(putKeyValueCommand);
      assertEquals("value", cache.peek(fqn, false, false).getDirect(key));
      event = region.takeLastEventNode();
      assertEquals(NodeEventType.ADD_ELEMENT_EVENT, event.getEventType());
      assertEquals(1, event.getElementDifference());
      assertEquals(fqn, event.getFqn());
      assertEquals("value", cache.peek(fqn, false, false).getDirect(key));
      assertNull(region.takeLastEventNode());

      GetKeyValueCommand getKeyValueCommand = commandsFactory.buildGetKeyValueCommand(fqn, key, false);
      invoker.invoke(getKeyValueCommand);
      region = regionManager.getRegion(fqn.toString(), false);
      event = region.takeLastEventNode();
      assertEquals(NodeEventType.VISIT_NODE_EVENT, event.getEventType());
      assertEquals(fqn, event.getFqn());
      assertNull(region.takeLastEventNode());

      RemoveKeyCommand removeKeyCommand = commandsFactory.buildRemoveKeyCommand(null, fqn, key);
      invoker.invoke(removeKeyCommand);

      assertNull(cache.get(fqn, key));
      event = region.takeLastEventNode();
      assertEquals(NodeEventType.REMOVE_ELEMENT_EVENT, event.getEventType());
      assertEquals(fqn, event.getFqn());
      assertEquals(1, event.getElementDifference());
      assertNull(region.takeLastEventNode());
   }
}