/*
 * JBoss, Home of Professional Open Source
 * Copyright 2005, JBoss Inc., and individual contributors as indicated
 * by the @authors tag. See the copyright.txt in the distribution for a
 * full listing of individual contributors.
 *
 * This is free software; you can redistribute it and/or modify it
 * under the terms of the GNU Lesser General Public License as
 * published by the Free Software Foundation; either version 2.1 of
 * the License, or (at your option) any later version.
 *
 * This software is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this software; if not, write to the Free
 * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
 * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
 */
package org.jboss.test.cache.test.local;

import junit.framework.Test;
import junit.framework.TestCase;
import junit.framework.TestSuite;
import org.jboss.cache.GlobalTransaction;
import org.jboss.cache.Node;
import org.jboss.cache.TreeCache;
import org.jboss.cache.lock.IdentityLock;
import org.jboss.cache.lock.IsolationLevel;
import org.jboss.cache.transaction.DummyTransactionManager;

import javax.naming.Context;
import javax.naming.InitialContext;
import javax.transaction.UserTransaction;
import java.util.HashMap;
import java.util.Map;
import java.util.Properties;
import java.util.Set;

/**
 * Tests transactional access to a local TreeCache.
 * Note: we use DummpyTranasctionManager to replace jta
 *
 * @version $Id: TxUnitTestCase.java,v 1.4.2.6 2005/12/01 19:50:26 dimitris Exp $
 */
public class TxUnitTestCase extends TestCase {
   TreeCache cache=null;
   UserTransaction tx=null;
   Properties p=null;
   String old_factory=null;
   final String FACTORY="org.jboss.cache.transaction.DummyContextFactory";


   public TxUnitTestCase(String name) {
      super(name);
   }

   public void setUp() throws Exception {
      super.setUp();
      old_factory=System.getProperty(Context.INITIAL_CONTEXT_FACTORY);
      System.setProperty(Context.INITIAL_CONTEXT_FACTORY, FACTORY);
      DummyTransactionManager.getInstance();
      if(p == null) {
         p=new Properties();
         p.put(Context.INITIAL_CONTEXT_FACTORY, "org.jboss.cache.transaction.DummyContextFactory");
      }

      tx=(UserTransaction)new InitialContext(p).lookup("UserTransaction");
      cache=new TreeCache("test", null, 10000);
      cache.setTransactionManagerLookupClass("org.jboss.cache.DummyTransactionManagerLookup");
      cache.setIsolationLevel(IsolationLevel.SERIALIZABLE);
      cache.createService();
      cache.startService();
   }

   public void tearDown() throws Exception {
      super.tearDown();
      if(cache != null)
         cache.stopService();

      // BW. kind of a hack to destroy jndi binding and thread local tx before next run.
      DummyTransactionManager.destroy();
      if(old_factory != null) {
         System.setProperty(Context.INITIAL_CONTEXT_FACTORY, old_factory);
         old_factory=null;
      }

      if(tx != null) {
         try {
            tx.rollback();
         }
         catch(Throwable t) {
         }
         tx=null;
      }
   }


   public void testPutTx() {
      try {
         tx.begin();
         cache.put("/a/b/c", "age", new Integer(38));
         assertEquals(new Integer(38), cache.get("/a/b/c", "age"));

         cache.put("/a/b/c", "age", new Integer(39));
         tx.commit();

         // This test is done outside the TX, it wouldn't work if someone else
         // modified "age". This works because we're the only TX running.
         assertEquals(new Integer(39), cache.get("/a/b/c", "age"));
      }
      catch(Throwable t) {
         t.printStackTrace();
         fail(t.toString());
      }
   }


   public void testRollbackTx1() {
      try {
         tx.begin();
         cache.put("/a/b/c", "age", new Integer(38));
         cache.put("/a/b/c", "age", new Integer(39));
         tx.rollback();

         // This test is done outside the TX, it wouldn't work if someone else
         // modified "age". This works because we're the only TX running.
         assertNull(cache.get("/a/b/c", "age"));
      }
      catch(Throwable t) {
         t.printStackTrace();
         fail(t.toString());
      }
   }


   public void testRollbackTx2() {
      try {
         tx.begin();
         cache.put("/a/b/c", "age", new Integer(38));
         cache.remove("/a/b/c", "age");
         tx.rollback();

         // This test is done outside the TX, it wouldn't work if someone else
         // modified "age". This works because we're the only TX running.
         assertNull(cache.get("/a/b/c", "age"));
      }
      catch(Throwable t) {
         t.printStackTrace();
         fail(t.toString());
      }
   }

   public void testRollbackTx2a() {
      try {
         cache.put("/a/b/c", "age", new Integer(38));
         tx.begin();
         cache.remove("/a/b/c", "age");
         tx.rollback();

         // This test is done outside the TX, it wouldn't work if someone else
         // modified "age". This works because we're the only TX running.
         assertEquals(new Integer(38), cache.get("/a/b/c", "age"));
      }
      catch(Throwable t) {
         t.printStackTrace();
         fail(t.toString());
      }
   }

   public void testRollbackTx3() {
      try {
         java.util.Map map1=new java.util.HashMap();
         map1.put("age", new Integer(38));
         java.util.Map map2=new java.util.HashMap();
         map2.put("age", new Integer(39));
         tx.begin();
         cache.put("/a/b/c", map1);
         cache.put("/a/b/c", map2);
         tx.rollback();

         // This test is done outside the TX, it wouldn't work if someone else
         // modified "age". This works because we're the only TX running.
         assertNull(cache.get("/a/b/c", "age"));
      }
      catch(Throwable t) {
         t.printStackTrace();
         fail(t.toString());
      }
   }


   public void testRollbackTx4() {
      try {
         Map map=new HashMap();
         map.put("age", new Integer(38));
         tx.begin();
         cache.put("/a/b/c", map);
         cache.remove("/a/b/c");
         tx.rollback();

         // This test is done outside the TX, it wouldn't work if someone else
         // modified "age". This works because we're the only TX running.
         assertNull(cache.get("/a/b/c", "age"));
      }
      catch(Throwable t) {
         t.printStackTrace();
         fail(t.toString());
      }
   }

   public void testNodeCreationRollback() {
      try {
         tx.begin();
         System.out.println("initial state:\n" + cache);
         cache.put("/bela/ban", null);
         System.out.println("after put():\n" + cache);
         tx.rollback();
         System.out.println("after rollback():\n" + cache);

         assertNull("node should be not existent", cache.get("/bela/ban"));
      }
      catch(Throwable t) {
         t.printStackTrace();
         fail(t.toString());
      }
   }

   public void testNodeCreationRollback2() {
      try {
         cache.put("/bela/ban", null);
         tx.begin();
         cache.put("/bela/ban/michelle", null);
         tx.rollback();
         assertNotNull("node should be not null", cache.get("/bela/ban"));
         assertNull("node should be not existent", cache.get("/bela/ban/michelle"));
      }
      catch(Throwable t) {
         t.printStackTrace();
         fail(t.toString());
      }
   }

   public void testNodeDeletionRollback() {
      try {
         cache.put("/a/b/c", null);
         tx.begin();
         cache.remove("/a/b/c");
         assertNull(cache.get("/a/b/c"));
         cache.remove("/a/b");
         assertNull(cache.get("/a/b"));
         cache.remove("/a");
         assertNull(cache.get("/a"));
         tx.rollback();
         assertNotNull(cache.get("/a/b/c"));
         assertNotNull(cache.get("/a/b"));
         assertNotNull(cache.get("/a"));
      }
      catch(Throwable t) {
         t.printStackTrace();
         fail(t.toString());
      }
   }

   public void testNodeDeletionRollback2() {
      try {
         cache.put("/a/b/c", null);
         cache.put("/a/b/c1", null);
         cache.put("/a/b/c2", null);
         tx.begin();
         cache.remove("/a");
         assertNull(cache.get("/a/b/c"));
         assertNull(cache.get("/a/b/c1"));
         assertNull(cache.get("/a/b/c2"));
         assertNull(cache.get("/a/b"));
         assertNull(cache.get("/a"));
         Set children=cache.getChildrenNames("/a/b");
         assertNull(children);
         children=cache.getChildrenNames("/a");
         assertNull(children);
         tx.rollback();
         assertNotNull(cache.get("/a"));
         assertNotNull(cache.get("/a/b"));
         assertNotNull(cache.get("/a/b/c"));
         assertNotNull(cache.get("/a/b/c1"));
         assertNotNull(cache.get("/a/b/c2"));
         children=cache.getChildrenNames("/a/b");
         assertTrue(children.size() == 3);
      }
      catch(Throwable t) {
         t.printStackTrace();
         fail(t.toString());
      }
   }



   public void testNodeDeletionRollback3() {
      GlobalTransaction gtx;
      try {
         tx.begin();
         gtx=cache.getCurrentTransaction();
         cache.put("/a/b/c1", null);
         checkLock(gtx, "/a", false);
         checkLock(gtx, "/a/b", false);
         checkLock(gtx, "/a/b/c1", true);

         cache.put("/a/b/c2", null);
         checkLock(gtx, "/a/b/c2", true);

         cache.put("/a/b/c3", null);
         cache.put("/a/b/c1/one", null);
         checkLock(gtx, "/a/b/c1", true);
         checkLock(gtx, "/a/b/c1/one", true);

         cache.put("/a/b/c1/two", null);
         cache.put("/a/b/c1/one/1", null);
         checkLock(gtx, "/a/b/c1", true);
         checkLock(gtx, "/a/b/c1/one", true);
         checkLock(gtx, "/a/b/c1/one/1", true);

         cache.put("/a/b/c1/two/2/3/4", null);
         checkLock(gtx, "/a/b/c1", true);
         checkLock(gtx, "/a/b/c1/two", true);
         checkLock(gtx, "/a/b/c1/two/2", false);
         checkLock(gtx, "/a/b/c1/two/2/3", false);
         checkLock(gtx, "/a/b/c1/two/2/3/4", true);

         System.out.println("locks: " + cache.printLockInfo());

         cache.remove("/a/b");
         tx.rollback();
         assertNull(cache.getChildrenNames("/a/b"));
      }
      catch(Throwable t) {
         t.printStackTrace();
         fail(t.toString());
      }
   }

   public void testDoubleLocks() {
      try {
         tx.begin();
         cache.put("/a/b/c", null);
         cache.put("/a/b/c", null);

         Node n=cache.get("/a");
         IdentityLock lock=n.getImmutableLock();
         int num=lock.getReaderOwners().size();
         assertTrue(num == 1);

         n=cache.get("/a/b");
         lock=n.getImmutableLock();
         num=lock.getReaderOwners().size();
         assertTrue(num == 1);
      }
      catch(Throwable t) {
         t.printStackTrace();
         fail(t.toString());
      }
   }

   private void checkLock(Object owner, String fqn, boolean write_locked) throws Exception {
      Node n=cache.get(fqn);
      IdentityLock lock=n.getImmutableLock();
      if(owner == null)
         owner=Thread.currentThread();
      if(lock.isLocked() == false)
         throw new Exception("node " + fqn + " is not locked");
      if(write_locked) {
         if(lock.isWriteLocked() == false)
            throw new Exception("node " + fqn + " is not write-locked");
      }
      else {
         if(lock.isReadLocked() == false)
            throw new Exception("node " + fqn + " is not read-locked");
      }
      if(lock.isOwner(owner) == false)
         throw new Exception("owner " + owner + "is not owner");
   }

   public void testRemoveKeyRollback() {
      try {
         cache.put("/bela/ban", "name", "Bela");
         tx.begin();
         cache.remove("/bela/ban", "name");
         assertNull(cache.get("/bela/ban", "name"));
         tx.rollback();
         assertEquals("Bela", cache.get("/bela/ban", "name"));
      }
      catch(Throwable t) {
         t.printStackTrace();
         fail(t.toString());
      }
   }


   public void testRemoveKeyRollback2() {
      try {
         Map m=new HashMap();
         m.put("name", "Bela");
         m.put("id", new Integer(322649));
         cache.put("/bela/ban", m);
         tx.begin();
         cache.remove("/bela/ban", "name");
         assertNull(cache.get("/bela/ban", "name"));
         tx.rollback();
         assertEquals("Bela", cache.get("/bela/ban", "name"));
      }
      catch(Throwable t) {
         t.printStackTrace();
         fail(t.toString());
      }
   }

   public void testRemoveKeyRollback3() {
      try {
         cache.put("/bela/ban", "name", "Bela");
         tx.begin();
         cache.put("/bela/ban", "name", "Michelle");
         cache.remove("/bela/ban", "name");
         assertNull(cache.get("/bela/ban", "name"));
         tx.rollback();
         assertEquals("Bela", cache.get("/bela/ban", "name"));
      }
      catch(Throwable t) {
         t.printStackTrace();
         fail(t.toString());
      }
   }




   public void testDoubleRemovalOfSameData() {
      try {
         tx.begin();
         cache.put("/foo/1", "item", new Integer(1));
         assertEquals(cache.get("/foo/1", "item"), new Integer(1));
         cache.remove("/foo/1");
         assertNull(cache.get("/foo/1", "item"));
         cache.remove("/foo/1");
         assertNull(cache.get("/foo/1", "item"));
         tx.rollback();
         assertFalse(cache.exists("/foo/1"));
         assertNull(cache.get("/foo/1", "item"));
      }
      catch(Throwable t) {
         t.printStackTrace();
         fail(t.toString());
      }
   }

   /**
    * put(Fqn, Map) with a previous null map
    */
   public void testPutDataRollback1() {
      try {
         cache.put("/bela/ban", null); // create a node /bela/ban with a null map
         tx.begin();
         Map m=new HashMap();
         m.put("name", "Bela");
         m.put("id", new Integer(322649));
         cache.put("/bela/ban", m);
         tx.rollback();

         Node n=cache.get("/bela/ban");
         if(n.getData() == null) return;
         assertEquals("map should be empty", 0, n.getData().size());
      }
      catch(Throwable t) {
         t.printStackTrace();
         fail(t.toString());
      }
   }

   /**
    * put(Fqn, Map) with a previous non-null map
    */
   public void testputDataRollback2() {
      Map m1, m2;
      m1=new HashMap();
      m1.put("name", "Bela");
      m1.put("id", new Integer(322649));
      m2=new HashMap();
      m2.put("other", "bla");
      m2.put("name", "Michelle");

      try {
         cache.put("/bela/ban", m1);
         tx.begin();

         cache.put("/bela/ban", m2);
         Map tmp=cache.get("/bela/ban").getData();
         assertTrue(tmp.size() == 3);
         assertTrue(tmp.get("name").equals("Michelle"));
         assertTrue(tmp.get("id").equals(new Integer(322649)));
         assertTrue(tmp.get("other").equals("bla"));
         tx.rollback();

         tmp=cache.get("/bela/ban").getData();
         assertTrue(tmp.size() == 2);
         assertTrue(tmp.get("name").equals("Bela"));
         assertTrue(tmp.get("id").equals(new Integer(322649)));
      }
      catch(Throwable t) {
         t.printStackTrace();
         fail(t.toString());
      }
   }


   public void testPutRollback() {
      try {
         cache.put("/bela/ban", null); // /bela/ban needs to exist
         tx.begin();
         cache.put("/bela/ban", "name", "Bela");
         assertEquals("Bela", cache.get("/bela/ban", "name"));
         tx.rollback();
         assertNull(cache.get("/bela/ban", "name"));
      }
      catch(Throwable t) {
         t.printStackTrace();
         fail(t.toString());
      }
   }


   public void testPutRollback2() {
      try {
         cache.put("/bela/ban", "name", "Bela"); // /bela/ban needs to exist
         tx.begin();
         cache.put("/bela/ban", "name", "Michelle");
         assertEquals("Michelle", cache.get("/bela/ban", "name"));
         tx.rollback();
         assertEquals("Bela", cache.get("/bela/ban", "name"));
      }
      catch(Throwable t) {
         t.printStackTrace();
         fail(t.toString());
      }
   }



//   public void testRaceConditionOnNotInCacheCondition() throws Exception {
//      cache.setIsolationLevel(IsolationLevel.SERIALIZABLE);
//
//      tx.begin();
//      // we now read the null entry, and decide that we need to go do something.
//
//      Object cachedObject=cache.get("/SecurityInfo/", Integer.toString(23));
//      assertNull(cachedObject); // we expect this in this test
//
//      /**
//       * now start another Thread to go do the same action, looking for the value, but it SHOULD
//       * see the result of the main thread put once it commits.
//       */
//      Thread thread=new Thread(new Runnable() {
//         UserTransaction tx2=(UserTransaction)new InitialContext(p).lookup("UserTransaction");
//         public void run() {
//            try {
//               tx2.begin();
//               log("OtherThread: inspecting the cache");
//               Object cachedObject=cache.get("/SecurityInfo", Integer.toString(23));
//
//               log("OtherThread: read from cache: " + cachedObject);
//               Thread.sleep(3000);
//
//               cachedObject=cache.get("/SecurityInfo", Integer.toString(23));
//               log("OtherThread: read(second time) from cache:" + cachedObject);
//
//               /**
//                * This should really fail because the other thread should actually have put something else there.
//                */
//               cache.put("/SecurityInfo", Integer.toString(23), "HelloWorldDIRTY!");
//
//               log("OtherThread: Has put something in the cache tha shouldn't be there");
//            }
//            catch(Exception e) {
//               e.printStackTrace();
//            }
//            finally {
//               if(tx2 != null)
//                  try {tx2.commit();} catch(Exception e) {e.printStackTrace();}
//            }
//            log("OthreThread: exiting");
//
//         }
//      });
//
//      thread.start();
//      log("MainThread is now waiting a little bit");
//      Thread.sleep(2000); // wait long enough for the other thread to block a bit. Simulate a DB read
//      log("MainThread is now putting something in the cache");
//      cache.put("/SecurityInfo", Integer.toString(23), "HelloWorld");
//      Thread.sleep(2000); // wait long enough for the other thread to block a bit. Simulate a DB read
//      tx.commit();
//      log("MainThread: committed");
//      thread.join(30000);
//
//      log(cache.get("/SecurityInfo", Integer.toString(23)).toString());
//   }


//   public void testConcurrentReadsWithSerializableIsolationLevel() throws CacheException, InterruptedException {
//      final long TIMEOUT=5000;
//      cache.setIsolationLevel(IsolationLevel.SERIALIZABLE);
//      cache.put("/testfqn", "testkey", "testvalue"); // add initial value, to be read by threads
//      Reader r1, r2;
//      r1=new Reader("reader1", 3000);
//      r2=new Reader("reader2", 0);
//      r1.start();
//      pause(100); // make sure thread1 starts and acquires the lock before thread2
//      r2.start();
//      r1.join(TIMEOUT);
//      r2.join(TIMEOUT);
//   }
//
//   class Reader extends Thread {
//      long timeout;
//
//      public Reader(String name, long timeout) {
//         super(name);
//         this.timeout=timeout;
//      }
//
//      public void run() {
//         UserTransaction trans=null;
//         try {
//            trans=(UserTransaction)new InitialContext(p).lookup("UserTransaction");
//            trans.begin();
//            Object retval=null;
//            log2("accessing tree");
//            retval=cache.get("testfqn", "testkey");
//            log2("retval: " + retval);
//            if(timeout > 0) {
//               log2("sleeping for " + timeout + " ms");
//               pause(timeout);
//            }
//         }
//         catch(Exception e) {
//            e.printStackTrace();
//         }
//         finally {
//            try {trans.commit();} catch(Throwable t) {}
//            log2("done");
//         }
//      }
//   }

//   private void pause(long timeout) {
//      try {
//         Thread.sleep(timeout);
//      }
//      catch(InterruptedException e) {
//      }
//   }
//
//   private void log(String msg) {
//      System.out.println("-- [" + Thread.currentThread() + "]: " + msg);
//   }
//
//   private void log2(String msg) {
//      System.out.println("-- [" + System.currentTimeMillis() + " " + Thread.currentThread() + "]: " + msg);
//   }


   public static Test suite() throws Exception {
      // return getDeploySetup(TxUnitTestCase.class, "cachetest.jar");
      return new TestSuite(TxUnitTestCase.class);
   }

   public static void main(String[] args) throws Exception {
      junit.textui.TestRunner.run(suite());
   }


}
