/*
 * JBoss, Home of Professional Open Source.
 * Copyright 2006, Red Hat Middleware LLC, and individual contributors
 * as indicated by the @author tags. See the copyright.txt file 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.cache.pojo.optimistic;

import static org.testng.AssertJUnit.assertEquals;
import static org.testng.AssertJUnit.assertNotSame;
import static org.testng.AssertJUnit.fail;

import java.util.ArrayList;
import java.util.List;
import java.util.Properties;
import java.util.concurrent.CountDownLatch;

import javax.naming.Context;
import javax.naming.InitialContext;
import javax.naming.NamingException;
import javax.transaction.NotSupportedException;
import javax.transaction.RollbackException;
import javax.transaction.SystemException;
import javax.transaction.UserTransaction;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.jboss.cache.CacheSPI;
import org.jboss.cache.Fqn;
import org.jboss.cache.pojo.PojoCache;
import org.jboss.cache.pojo.test.Address;
import org.jboss.cache.pojo.test.Person;
import org.jboss.cache.pojo.util.CacheApiUtil;
import org.jboss.cache.transaction.DummyTransactionManager;
import org.testng.annotations.AfterMethod;
import org.testng.annotations.BeforeMethod;
import org.testng.annotations.Test;

/**
 */
@Test(groups = {"functional"})
public class LocalTxTest extends AbstractOptimisticTestCase
{
   Log log = LogFactory.getLog(LocalTxTest.class);
   PojoCache cache;
   final String FACTORY = "org.jboss.cache.transaction.DummyContextFactory";
   DummyTransactionManager tx_mgr;
   Throwable t1_ex, t2_ex;
   long start = 0;

   @BeforeMethod(alwaysRun = true)
   protected void setUp() throws Exception
   {
      log.info("setUp() ....");

      cache = createCache();
//      cache = createPessimisticCache();

      System.setProperty(Context.INITIAL_CONTEXT_FACTORY, FACTORY);

      tx_mgr = DummyTransactionManager.getInstance();
      t1_ex = t2_ex = null;
   }

   @AfterMethod(alwaysRun = true)
   protected void tearDown()
   {
      cache.stop();

      DummyTransactionManager.destroy();
   }

//   public void testDummy() {}

   UserTransaction getTransaction() throws SystemException, NotSupportedException, NamingException
   {
      Properties prop = new Properties();
      prop.put(Context.INITIAL_CONTEXT_FACTORY,
               "org.jboss.cache.transaction.DummyContextFactory");
      return (UserTransaction) new InitialContext(prop).lookup("UserTransaction");
   }

   private Person createPerson(String id, String name, int age)
   {
      Person p = new Person();
      p.setName(name);
      p.setAge(age);
      cache.attach(id, p);
      return p;
   }

   public void testSimple() throws Exception
   {
      log.info("testSimple() ....");
      UserTransaction tx = getTransaction();
      tx.begin();
      Person p = createPerson("/person/test1", "Harald Gliebe", 32);
      tx.commit();
      tx.begin();
      p.setName("Benoit");
      tx.commit();
      Person p1 = (Person) cache.find("/person/test1");
      assertEquals("Benoit", p.getName());
      assertEquals("Benoit", p1.getName());
      tx.begin();
      p1.setAge(41);
      tx.commit();
      assertEquals(41, p.getAge());
      assertEquals(41, p1.getAge());
   }

   public void testFailure() throws Exception
   {
      log.info("testFailure() ....");
      UserTransaction tx = getTransaction();
      tx.begin();
      createPerson("/person/test1", "Harald Gliebe", 32);
      tx.commit();

      tx.begin();
      createPerson("/person/test1", "Harald Gliebe", 32);
      tx.commit();
   }

   public void testFailure1() throws Exception
   {
      log.info("testFailure1() ....");
      UserTransaction tx = getTransaction();
      Fqn<String> f = Fqn.fromString("/person/test2");
      tx.begin();
      cache.getCache().put(f, "test", "test");
      tx.commit();

      tx.begin();
      cache.getCache().removeNode(f);
      cache.getCache().put(f, "test", "test");
      tx.commit();
   }

   public void testFailure2() throws Exception
   {
      Fqn<String> f0 = Fqn.fromString("/person/test");
      Fqn<String> f1 = new Fqn<String>(f0, "1");
      Fqn<String> f2 = new Fqn<String>(f0, "2");

      cache.getCache().put(f1, "test", "test");
      cache.getCache().put(f2, "test", "test");

      int size = CacheApiUtil.getNodeChildren((CacheSPI<Object, Object>) cache.getCache(), f0).size();
      assertEquals("Size ", 2, size);

      UserTransaction tx = getTransaction();
      tx.begin();
      cache.getCache().removeNode(f1);
      size = CacheApiUtil.getNodeChildren((CacheSPI<Object, Object>) cache.getCache(), f0).size();
      assertEquals("Size ", 1, size);
      cache.getCache().put(f1, "test", "test");
      size = CacheApiUtil.getNodeChildren((CacheSPI<Object, Object>) cache.getCache(), f0).size();
      assertEquals("Size ", 2, size);
      tx.commit();
   }

   public void testModification() throws Exception
   {
      UserTransaction tx = getTransaction();
      tx.begin();
      Person p = createPerson("/person/test2", "Harald", 32);
      p.setName("Harald Gliebe");
      tx.commit();
      Person p1 = (Person) cache.find("/person/test2");
      tx.begin();
      p1.setName("Benoit");
      tx.commit();
      assertEquals(p.getName(), "Benoit");
      assertEquals(p1.getName(), "Benoit");
      tx.begin();
      p1.setName("Harald");
      tx.rollback();
      assertEquals(p.getName(), "Benoit");
      assertEquals(p1.getName(), "Benoit");
   }

   public void testConcurrentSimplePutsI() throws Exception
   {
      final CountDownLatch latch1 = new CountDownLatch(1);
      final CountDownLatch latch2 = new CountDownLatch(1);
      Thread t1 = new Thread("t1")
      {
         public void run()
         {
            try
            {
               Person p = createPerson("/person/test6", "p6", 41);
               Address addr = new Address();
               addr.setCity("San Jose");
               List<String> list = new ArrayList<String>();
               list.add("English");
               list.add("French");
               p.setLanguages(list);
               UserTransaction tx = getTransaction();
               tx.begin();
               p.setAddress(addr);
               latch1.countDown();
               latch2.await();
               tx.commit();
            }
            catch (RollbackException rollback)
            {
               ;
            }
            catch (Exception ex)
            {
               t1_ex = ex;
            }
            finally
            {
               latch1.countDown();
            }
         }
      };

      Thread t2 = new Thread("t2")
      {
         public void run()
         {
            UserTransaction tx = null;
            try
            {
               latch1.await();
               Person p = createPerson("/person/test7", "p7", 40);
               Address addr = new Address();
               addr.setCity("Santa Clara");
               tx = getTransaction();
               tx.begin();
               p.setAddress(addr);
               tx.commit();
            }
            catch (RollbackException rollback)
            {
               ;
            }
            catch (Exception ex)
            {
//               t2_ex = ex;
               try
               {
                  tx.rollback();
               }
               catch (SystemException e)
               {
                  e.printStackTrace();
                  t2_ex = e;
               }
            }
            finally
            {
               latch2.countDown();
            }
         }
      };

      createPerson("/person/test5", "p5", 30);

      t1.start();
      t2.start();

      t1.join();
      t2.join();

      // t2 should rollback due to timeout while t2 should succeed
      if (t2_ex != null)
         fail("Thread2 failed: " + t2_ex);
      if (t1_ex != null)
         fail("Thread1 failed: " + t1_ex);

   }

   public void testConcurrentSimplePutsII() throws Exception
   {
      final CountDownLatch latch1 = new CountDownLatch(1);
      final CountDownLatch latch2 = new CountDownLatch(1);
      Thread t1 = new Thread("t1")
      {
         public void run()
         {
            try
            {
               Person p = (Person) cache.find("/person/test6");
               Address addr = new Address();
               addr.setCity("San Jose");
               UserTransaction tx = getTransaction();
               tx.begin();
               p.setAddress(addr);
               latch1.countDown();
               latch2.await();
               tx.commit();
            }
            catch (RollbackException rollback)
            {
               ;
            }
            catch (Exception ex)
            {
               t1_ex = ex;
            }
            finally
            {
               latch1.countDown();
            }
         }
      };

      Thread t2 = new Thread("t2")
      {
         public void run()
         {
            UserTransaction tx = null;
            try
            {
               latch1.await();
               Person p = (Person) cache.find("/person/test6");
               Address addr = new Address();
               addr.setCity("Santa Clara");
               tx = getTransaction();
               tx.begin();
               p.setAddress(addr);
               tx.commit();
            }
            catch (RollbackException rollback)
            {
               ;
            }
            catch (Exception ex)
            {
//               t2_ex = ex;
               try
               {
                  tx.rollback();
               }
               catch (SystemException e)
               {
                  e.printStackTrace();
                  t2_ex = e;
               }
            }
            finally
            {
               latch2.countDown();
            }
         }
      };

      Person p = createPerson("/person/test6", "p6", 40);

      t1.start();
      t2.start();

      t1.join();
      t2.join();

      // t2 should rollback due to timeout while t2 should succeed
      if (t2_ex != null)
         fail("Thread1 failed: " + t2_ex);
      if (t1_ex != null)
         fail("Thread2 failed: " + t1_ex);

      // This would fail because we are writing to __JBossInteral__ for every attach now.
      // As a result, there is version conflict for optimistic locking and cuasing
      // rollback.
      assertNotSame("City should be different ", "San Jose", p.getAddress().getCity());
   }

   public void testConcurrentPuts() throws Exception
   {
      final CountDownLatch latch1 = new CountDownLatch(1);
      final CountDownLatch latch2 = new CountDownLatch(1);
      Thread t1 = new Thread("t1")
      {
         public void run()
         {
            try
            {
               List<String> lang = ((Person) cache.find("/person/test6")).getLanguages();
               UserTransaction tx = getTransaction();
               tx.begin();
               lang.add("German");
               latch1.countDown();
               latch2.await();
               tx.commit();
            }
            catch (RollbackException rollback)
            {
               ;
            }
            catch (Exception ex)
            {
               t1_ex = ex;
            }
            finally
            {
               latch1.countDown();
            }
         }
      };

      Thread t2 = new Thread("t2")
      {
         public void run()
         {
            UserTransaction tx = null;
            try
            {
               latch1.await();
               List<String> lang = ((Person) cache.find("/person/test6")).getLanguages();
               tx = getTransaction();
               tx.begin();
               lang.add("English");
               tx.commit();
            }
            catch (RollbackException rollback)
            {
               ;
            }
            catch (Exception ex)
            {
               try
               {
                  tx.rollback();
               }
               catch (SystemException e)
               {
                  e.printStackTrace();
                  t2_ex = e;
               }
            }
            finally
            {
               latch2.countDown();
            }
         }
      };

      // FIXME - JBCACHE-1034 - This only works with an explicit transaction.
      UserTransaction tx = getTransaction();
      tx.begin();
      Person p = createPerson("/person/test6", "p6", 50);
      List<String> lang = new ArrayList<String>();
      lang.add("German");
      p.setLanguages(lang);
      tx.commit();

      t1.start();
      t2.start();

      t1.join();
      t2.join();

      // t2 should rollback due to timeout while t2 should succeed
      if (t2_ex != null)
      {
         t2_ex.printStackTrace(System.err);
         fail("Thread2 failed: " + t2_ex);
      }
      if (t1_ex != null)
      {
         t1_ex.printStackTrace(System.err);
         fail("Thread1 failed: " + t1_ex);
      }

      int size = ((Person) cache.find("/person/test6")).getLanguages().size();
      assertEquals("number of languages ",
                   2, size);
   }

   void log(String s)
   {
      long now;
      if (start == 0)
         start = System.currentTimeMillis();
      now = System.currentTimeMillis();

      System.out.println("[" + Thread.currentThread().getName() + "] [" + (now - start) + "] " + s);
   }





}
