package org.jboss.cache.api;

import org.jboss.cache.Cache;
import org.jboss.cache.CacheFactory;
import org.jboss.cache.CacheSPI;
import org.jboss.cache.DefaultCacheFactory;
import org.jboss.cache.Fqn;
import org.jboss.cache.Node;
import org.jboss.cache.Region;
import org.jboss.cache.config.Configuration;
import org.jboss.cache.config.ConfigurationException;
import org.jboss.cache.notifications.annotation.CacheListener;
import org.jboss.cache.notifications.annotation.NodeCreated;
import org.jboss.cache.notifications.event.Event;
import org.jboss.cache.transaction.GenericTransactionManagerLookup;
import org.jboss.cache.util.CachePrinter;
import static org.testng.AssertJUnit.*;
import org.testng.annotations.AfterMethod;
import org.testng.annotations.BeforeMethod;
import org.testng.annotations.Test;

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

/**
 * Tests the {@link org.jboss.cache.Cache} public API at a high level
 *
 * @author <a href="mailto:manik@jboss.org">Manik Surtani</a>
 */

@Test(groups = "functional")
public class CacheAPITest
{
   private Cache<String, String> cache;
   protected boolean optimistic;
   final List<String> events = new ArrayList<String>();

   @BeforeMethod(alwaysRun = true)
   public void setUp() throws Exception
   {
      // start a single cache instance
      CacheFactory<String, String> cf = new DefaultCacheFactory();
      cache = cf.createCache("META-INF/conf-test/local-tx-service.xml", false);
      cache.getConfiguration().setNodeLockingScheme(optimistic ? Configuration.NodeLockingScheme.OPTIMISTIC : Configuration.NodeLockingScheme.PESSIMISTIC);
      cache.start();
      events.clear();
   }

   @AfterMethod(alwaysRun = true)
   public void tearDown()
   {
      if (cache != null) cache.stop();
   }


   /**
    * Tests that the configuration contains the values expected, as well as immutability of certain elements
    */
   public void testConfiguration()
   {
      Configuration c = cache.getConfiguration();
      assertEquals(Configuration.CacheMode.LOCAL, c.getCacheMode());
      assertEquals(GenericTransactionManagerLookup.class.getName(), c.getTransactionManagerLookupClass());

      // note that certain values should be immutable.  E.g., CacheMode cannot be changed on the fly.
      try
      {
         c.setCacheMode(Configuration.CacheMode.REPL_SYNC);
         assert false : "Should have thrown an Exception";
      }
      catch (ConfigurationException e)
      {
         // expected
      }

      // others should be changeable though.
      c.setLockAcquisitionTimeout(100);
   }

   public void testGetMembersInLocalMode()
   {
      assert cache.getMembers() == null : "Cache members should be null if running in LOCAL mode";
   }

   /**
    * Basic usage of cache listeners
    * <p/>
    * A more complete test that tests notifications is in org.jboss.cache.notifications
    */
   public void testCacheListeners()
   {
      assertEquals(0, cache.getCacheListeners().size());

      Object dummy = new Listener();

      cache.addCacheListener(dummy);

      assertEquals(1, cache.getCacheListeners().size());

      cache.getRoot().addChild(Fqn.fromString("/blah"));

      // test that the event was captured by the listener.

      // FOR A FULL TEST ON NOTIFICATIONS SEE TESTS IN org.jboss.cache.notifications
      assertEquals(1, events.size());

      cache.removeCacheListener(dummy);

      assertEquals(0, cache.getCacheListeners().size());
   }

   /**
    * All cache operations should happen on a {@link Node} - I.e., you look up a {@link Node} and perform data operations
    * on this {@link Node}.  For convenience and familiarity with JBoss Cache 1.x, we provide some helpers in {@link Cache}
    * which dives you direct data access to nodes.
    * <p/>
    * This test exercises these.
    */
   public void testConvenienceMethods()
   {
      Fqn<String> fqn = Fqn.fromString("/test/fqn");
      String key = "key", value = "value";
      Map<String, String> data = new HashMap<String, String>();
      data.put(key, value);

      assertNull(cache.get(fqn, key));

      cache.put(fqn, key, value);

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

      cache.remove(fqn, key);

      assertNull(cache.get(fqn, key));

      cache.put(fqn, data);

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


   /**
    * Another convenience method that tests node removal
    */
   public void testNodeConvenienceNodeRemoval()
   {
      // this fqn is relative, but since it is from the root it may as well be absolute
      Fqn<String> fqn = Fqn.fromString("/test/fqn");
      cache.getRoot().addChild(fqn);
      assertTrue(cache.getRoot().hasChild(fqn));

      assertEquals(true, cache.removeNode(fqn));
      assertFalse(cache.getRoot().hasChild(fqn));
      assertEquals(false, cache.removeNode(fqn));

      System.out.println("Cache: " + CachePrinter.printCacheDetails(cache));

      // Check that it's removed if it has a child
      Fqn<String> child = Fqn.fromString("/test/fqn/child");
      cache.getRoot().addChild(child);
      assertTrue(cache.getRoot().hasChild(child));

      assertEquals(true, cache.removeNode(fqn));
      assertFalse(cache.getRoot().hasChild(fqn));
      assertEquals(false, cache.removeNode(fqn));
   }

   /**
    * Tests basic eviction
    */
   public void testEvict()
   {
      Fqn<String> one = Fqn.fromString("/one");
      Fqn<String> two = Fqn.fromString("/one/two");
      String key = "key", value = "value";

      cache.getRoot().addChild(one).put(key, value);
      cache.getRoot().addChild(two).put(key, value);

      assertTrue(cache.getRoot().hasChild(one));
      assertFalse(cache.getRoot().getChild(one).getData().isEmpty());
      assertTrue(cache.getRoot().hasChild(two));
      assertFalse(cache.getRoot().getChild(two).getData().isEmpty());

      // evict two
      cache.evict(two, false);

      assertTrue(cache.getRoot().hasChild(one));
      assertTrue(cache.getRoot().getChild(one).getKeys().contains(key));
      assertFalse(cache.getRoot().hasChild(two));

      // now add 2 again...
      cache.getRoot().addChild(two).put(key, value);

      // now evict one, NOT recursive
      cache.evict(one, false);

      // one will NOT be removed, just emptied.
      assertTrue(cache.getRoot().hasChild(one));
      assertFalse(cache.getRoot().getChild(one).getKeys().contains(key));

      // two will be unaffected
      assertTrue(cache.getRoot().hasChild(two));
      assertTrue(cache.getRoot().getChild(two).getKeys().contains(key));
   }


   /**
    * Tests recursive eviction
    */
   public void testEvictRecursive()
   {
      Fqn<String> one = Fqn.fromString("/one");
      Fqn<String> two = Fqn.fromString("/one/two");
      String key = "key", value = "value";

      cache.getRoot().addChild(one).put(key, value);
      cache.getRoot().addChild(two).put(key, value);

      assertTrue(cache.getRoot().hasChild(one));
      assertFalse(cache.getRoot().getChild(one).getData().isEmpty());
      assertTrue(cache.getRoot().hasChild(two));
      assertFalse(cache.getRoot().getChild(two).getData().isEmpty());

      // evict two
      cache.evict(two, true);

      assertTrue(cache.getRoot().hasChild(one));
      assertFalse(cache.getRoot().getChild(one).getData().isEmpty());
      assertFalse(cache.getRoot().hasChild(two));

      // now add 2 again...
      cache.getRoot().addChild(two).put(key, value);

      // now evict one, recursive
      cache.evict(one, true);

      assertFalse(cache.getRoot().hasChild(one));
      assertFalse(cache.getRoot().hasChild(two));
   }


   /**
    * Again, see org.jboss.cache for more extensive tests on Regions.  This just tests the getRegion API on cache.
    */
   public void testRegion()
   {
      Region rootRegion = cache.getRegion(Fqn.ROOT, true);
      assertNotNull(rootRegion);// guaranteed never to return null if createIfAbsent is true.
      assertSame(rootRegion, cache.getRegion(Fqn.ROOT, true));

      Region otherRegion = cache.getRegion(Fqn.fromString("/other/region"), true);
      assertNotNull(otherRegion);
      assertSame(otherRegion, cache.getRegion(Fqn.fromString("/other/region"), true));
   }

   /**
    * Again, see org.jboss.cache for more extensive tests on Regions.  This just tests the getRegion API on cache.
    */
   public void testParentRegion1()
   {
      Region rootRegion = cache.getRegion(Fqn.ROOT, true);
      assertNotNull(rootRegion);// guaranteed never to return null if createIfAbsent is true.
      assertSame(rootRegion, cache.getRegion(Fqn.ROOT, false));

      Region otherRegion = cache.getRegion(Fqn.fromString("/other/region"), false);
      // should return the same parent region as root.

      assertSame(otherRegion, rootRegion);
   }

   /**
    * Again, see org.jboss.cache for more extensive tests on Regions.  This just tests the getRegion API on cache.
    */
   public void testParentRegion2()
   {
      Region rootRegion = cache.getRegion(Fqn.ROOT, true);
      Region parentRegion = cache.getRegion(Fqn.fromString("/parent"), true);
      assertNotSame("parentRegion should be a new region in its own right", rootRegion, parentRegion);

      Region childRegion = cache.getRegion(Fqn.fromString("/parent/region"), false);
      assertSame("Expecting the same region as parentRegion", childRegion, parentRegion);
   }


   /**
    * Again, see org.jboss.cache for more extensive tests on Regions.  This just tests the getRegion API on cache.
    */
   public void testNullRegion()
   {
      Region myRegion = cache.getRegion(Fqn.fromString("/myregion"), true);
      assertNotNull(myRegion);// guaranteed never to return null if createIfAbsent is true.
      assertSame(myRegion, cache.getRegion(Fqn.fromString("/myregion"), false));

      Region otherRegion = cache.getRegion(Fqn.fromString("/other/region"), false);
      // should return the default region
      assertNotNull(otherRegion);
      assertEquals(Fqn.ROOT, otherRegion.getFqn());
   }

   public void testStopClearsData() throws Exception
   {
      Fqn a = Fqn.fromString("/a");
      Fqn b = Fqn.fromString("/a/b");
      String key = "key", value = "value";
      cache.getRoot().addChild(a).put(key, value);
      cache.getRoot().addChild(b).put(key, value);
      cache.getRoot().put(key, value);

      assertEquals(value, cache.getRoot().get(key));
      assertEquals(value, cache.getRoot().getChild(a).get(key));
      assertEquals(value, cache.getRoot().getChild(b).get(key));

      cache.stop();

      cache.start();

      assertNull(cache.getRoot().get(key));
      assertTrue(cache.getRoot().getData().isEmpty());
      assertTrue(cache.getRoot().getChildren().isEmpty());
   }

   public void testPhantomStructuralNodesOnRemove()
   {
      CacheSPI spi = (CacheSPI) cache;
      assert spi.peek(Fqn.fromString("/a/b/c"), true, true) == null;
      assert !spi.removeNode("/a/b/c");
      assert spi.peek(Fqn.fromString("/a/b/c"), true, true) == null;
      assert spi.peek(Fqn.fromString("/a/b"), true, true) == null;
      assert spi.peek(Fqn.fromString("/a"), true, true) == null;
   }

   public void testPhantomStructuralNodesOnRemoveTransactional() throws Exception
   {
      CacheSPI spi = (CacheSPI) cache;
      TransactionManager tm = spi.getTransactionManager();
      assert spi.peek(Fqn.fromString("/a/b/c"), true, true) == null;
      tm.begin();
      assert !spi.removeNode("/a/b/c");
      tm.commit();
      assert spi.peek(Fqn.fromString("/a/b/c"), true, true) == null;
      assert spi.peek(Fqn.fromString("/a/b"), true, true) == null;
      assert spi.peek(Fqn.fromString("/a"), true, true) == null;
   }

   public void testRpcManagerElements()
   {
      assertEquals("CacheMode.LOCAL cache has no address", null, cache.getLocalAddress());
      assertEquals("CacheMode.LOCAL cache has no members list", null, cache.getMembers());
   }

   @CacheListener
   public class Listener
   {

      @NodeCreated
      public void nodeCreated(Event e)
      {
         if (e.isPre()) events.add("Created");
      }
   }
}
