package org.jboss.cache.marshall;

import org.jboss.cache.Cache;
import org.jboss.cache.DefaultCacheFactory;
import org.jboss.cache.Fqn;
import org.jboss.cache.Region;
import org.jboss.cache.config.Configuration;
import org.jboss.cache.lock.IsolationLevel;
import static org.jboss.cache.marshall.CustomCollectionTest.MarshallingMode.*;
import static org.testng.AssertJUnit.*;
import org.testng.annotations.AfterMethod;
import org.testng.annotations.BeforeMethod;
import org.testng.annotations.Test;

import java.io.Serializable;
import java.util.List;
import java.util.Map;
import java.util.Set;

/**
 * Unit test to cover replication and marshalling of custom collections
 *
 * @author <a href="mailto:galder.zamarreno@jboss.com">Galder Zamarreno</a>
 * @author <a href="mailto:manik@jboss.org">Manik Surtani (manik@jboss.org)</a>
 */
@Test(groups = {"functional"})
public class CustomCollectionTest extends RegionBasedMarshallingTestBase implements Serializable
{
   private transient Cache<Object, Object> cache1 = null;
   private transient Cache<Object, Object> cache2 = null;
   private String myListClass = MyList.class.getName();
   private String mySetClass = MySet.class.getName();
   private String myMapClass = MyMap.class.getName();

   @BeforeMethod(alwaysRun = true)
   public void setUp() throws Exception
   {
      cache1 = createCache();
      cache2 = createCache();
   }

   @AfterMethod(alwaysRun = true)
   public void tearDown() throws Exception
   {
      cache1.stop();
      cache2.stop();
   }

   public void testMap() throws Exception
   {
      doMapTest(DEFAULT_WITHOUT_MARSHALLED_VALUES);
   }

   public void testMapWithRegions() throws Exception
   {
      doMapTest(CUSTOM_CLASSLOADER_WITH_REGIONS);
   }

   public void testMapWithMarshalledValues() throws Exception
   {
      doMapTest(CUSTOM_CLASSLOADER_WITH_MARSHALLEDVALUES);
   }

   public void testMapWithMarshalledValuesDefaultClassloader() throws Exception
   {
      doMapTest(DEFAULT_WITH_MARSHALLED_VALUES);
   }

   public void testSet() throws Exception
   {
      doSetTest(DEFAULT_WITHOUT_MARSHALLED_VALUES);
   }

   public void testSetWithRegions() throws Exception
   {
      doSetTest(CUSTOM_CLASSLOADER_WITH_REGIONS);
   }

   public void testSetWithMarshalledValues() throws Exception
   {
      doSetTest(CUSTOM_CLASSLOADER_WITH_MARSHALLEDVALUES);
   }

   public void testSetWithMarshalledValuesDefaultClassloader() throws Exception
   {
      doSetTest(DEFAULT_WITH_MARSHALLED_VALUES);
   }

   public void testList() throws Exception
   {
      doListTest(DEFAULT_WITHOUT_MARSHALLED_VALUES);
   }

   public void testListWithRegions() throws Exception
   {
      doListTest(CUSTOM_CLASSLOADER_WITH_REGIONS);
   }

   public void testListWithMarshalledValues() throws Exception
   {
      doListTest(CUSTOM_CLASSLOADER_WITH_MARSHALLEDVALUES);
   }

   public void testListWithMarshalledValuesDefaultClassloader() throws Exception
   {
      doListTest(DEFAULT_WITH_MARSHALLED_VALUES);
   }

   enum MarshallingMode
   {
      DEFAULT_WITH_MARSHALLED_VALUES, DEFAULT_WITHOUT_MARSHALLED_VALUES, CUSTOM_CLASSLOADER_WITH_REGIONS, CUSTOM_CLASSLOADER_WITH_MARSHALLEDVALUES;

      boolean isUsingCustomClassLoader()
      {
         return this == CUSTOM_CLASSLOADER_WITH_MARSHALLEDVALUES || this == CUSTOM_CLASSLOADER_WITH_REGIONS;
      }
   }

   @SuppressWarnings("unchecked")
   private void doMapTest(MarshallingMode marshallingMode) throws Exception
   {
      ClassLoader customClassLoader = getCollectionsClassLoader();
      Class mapClass = customClassLoader.loadClass(myMapClass);
      Map map = (Map) (marshallingMode.isUsingCustomClassLoader() ? mapClass.newInstance() : new MyMap());

      map.put("k", "v");

      configureCaches(customClassLoader, marshallingMode);

      cache1.start();
      cache2.start();

      if (marshallingMode == CUSTOM_CLASSLOADER_WITH_MARSHALLEDVALUES)
         Thread.currentThread().setContextClassLoader(customClassLoader);
      cache1.put(fqn("/a"), "key", map);
      Object o = cache2.get(fqn("/a"), "key");
      if (marshallingMode == CUSTOM_CLASSLOADER_WITH_MARSHALLEDVALUES) resetContextClassLoader();

      assertTrue(o instanceof Map);
      if (marshallingMode.isUsingCustomClassLoader())
      {
         assertNotSame(MyMap.class, o.getClass());
         assertSame(mapClass, o.getClass());
      }
      else
      {
         assertSame(MyMap.class, o.getClass());
         assertNotSame(mapClass, o.getClass());
      }
      assertEquals("v", ((Map) o).get("k"));
   }

   @SuppressWarnings("unchecked")
   private void doSetTest(MarshallingMode marshallingMode) throws Exception
   {
      ClassLoader customClassLoader = getCollectionsClassLoader();
      Class setClass = customClassLoader.loadClass(mySetClass);
      Set set = (Set) (marshallingMode.isUsingCustomClassLoader() ? setClass.newInstance() : new MySet());

      set.add("k");

      configureCaches(customClassLoader, marshallingMode);

      cache1.start();
      cache2.start();

      if (marshallingMode == CUSTOM_CLASSLOADER_WITH_MARSHALLEDVALUES)
         Thread.currentThread().setContextClassLoader(customClassLoader);
      cache1.put(fqn("/a"), "key", set);
      Object o = cache2.get(fqn("/a"), "key");
      if (marshallingMode == CUSTOM_CLASSLOADER_WITH_MARSHALLEDVALUES) resetContextClassLoader();

      assertTrue(o instanceof Set);
      if (marshallingMode.isUsingCustomClassLoader())
      {
         assertNotSame(MySet.class, o.getClass());
         assertSame(setClass, o.getClass());
      }
      else
      {
         assertSame(MySet.class, o.getClass());
         assertNotSame(setClass, o.getClass());
      }
      assertTrue(((Set) o).contains("k"));
   }

   @SuppressWarnings("unchecked")
   private void doListTest(MarshallingMode marshallingMode) throws Exception
   {

      ClassLoader customClassLoader = getCollectionsClassLoader();
      Class listClass = customClassLoader.loadClass(myListClass);

      List list = (List) (marshallingMode.isUsingCustomClassLoader() ? listClass.newInstance() : new MyList());

      list.add("k");

      configureCaches(customClassLoader, marshallingMode);

      cache1.start();
      cache2.start();

      if (marshallingMode == CUSTOM_CLASSLOADER_WITH_MARSHALLEDVALUES)
         Thread.currentThread().setContextClassLoader(customClassLoader);
      cache1.put(fqn("/a"), "key", list);
      Object o = cache2.get(fqn("/a"), "key");
      if (marshallingMode == CUSTOM_CLASSLOADER_WITH_MARSHALLEDVALUES) resetContextClassLoader();

      assertTrue(o instanceof List);
      if (marshallingMode.isUsingCustomClassLoader())
      {
         assertSame(listClass, o.getClass());
         assertNotSame(MyList.class, o.getClass());
      }
      else
      {
         assertSame(MyList.class, o.getClass());
         assertNotSame(listClass, o.getClass());
      }
      assertTrue(((List) o).contains("k"));
   }

   public void testHarnessClassLoader() throws Exception
   {
      Class myListFromCustomLoader = getCollectionsClassLoader().loadClass(myListClass);
      assertNotNull(myListFromCustomLoader);
      assertFalse(MyList.class.equals(myListFromCustomLoader));

      Object customLoaderMyList = myListFromCustomLoader.newInstance();
      try
      {
         @SuppressWarnings("unused")
         MyList m = (MyList) customLoaderMyList;
         fail("Should have barfed");
      }
      catch (ClassCastException cce)
      {
         // expected
      }
   }

   private void configureCaches(ClassLoader customClassLoader, MarshallingMode marshallingMode)
   {
      switch (marshallingMode)
      {
         case CUSTOM_CLASSLOADER_WITH_REGIONS:
            cache1.getConfiguration().setUseRegionBasedMarshalling(true);
            Region region1 = cache1.getRegion(fqn("/a"), true);
            region1.registerContextClassLoader(customClassLoader);
            cache2.getConfiguration().setUseRegionBasedMarshalling(true);
            Region region2 = cache2.getRegion(fqn("/a"), true);
            region2.registerContextClassLoader(customClassLoader);
            // and carry on to the same setting as DEFAULT_WITHOUT_MARSHALLED_VALUES
         case DEFAULT_WITHOUT_MARSHALLED_VALUES:
            cache1.getConfiguration().setUseLazyDeserialization(false);
            cache2.getConfiguration().setUseLazyDeserialization(false);
            break;
         case DEFAULT_WITH_MARSHALLED_VALUES:
         case CUSTOM_CLASSLOADER_WITH_MARSHALLEDVALUES:
            cache1.getConfiguration().setUseLazyDeserialization(true);
            cache2.getConfiguration().setUseLazyDeserialization(true);
            break;
      }
   }

   private ClassLoader getCollectionsClassLoader()
   {
      String[] includesClasses = {myListClass, mySetClass, myMapClass};
      String[] excludesClasses = {};
      ClassLoader cl = Thread.currentThread().getContextClassLoader();
      return new SelectedClassnameClassLoader(includesClasses, excludesClasses, cl);
   }

   private Cache<Object, Object> createCache()
   {
      Cache<Object, Object> cache = new DefaultCacheFactory().createCache(false);
      cache.getConfiguration().setCacheMode(Configuration.CacheMode.REPL_SYNC);
      cache.getConfiguration().setTransactionManagerLookupClass("org.jboss.cache.transaction.DummyTransactionManagerLookup");
      cache.getConfiguration().setIsolationLevel(IsolationLevel.REPEATABLE_READ);
      return cache;
   }

   private static Fqn fqn(String fqn)
   {
      return Fqn.fromString(fqn);
   }
}