package org.jboss.cache.loader;

import org.jboss.cache.CacheSPI;
import org.jboss.cache.DefaultCacheFactory;
import org.jboss.cache.Fqn;
import org.jboss.cache.util.TestingUtil;
import org.jboss.cache.config.CacheLoaderConfig;
import org.jboss.cache.factories.XmlConfigurationParser;
import org.jboss.cache.marshall.NodeData;
import org.jboss.cache.statetransfer.StateTransferManager;
import org.jboss.cache.xml.XmlHelper;
import org.jboss.util.stream.MarshalledValueInputStream;
import org.jboss.util.stream.MarshalledValueOutputStream;
import static org.testng.AssertJUnit.*;
import org.testng.annotations.AfterMethod;
import org.testng.annotations.BeforeMethod;
import org.testng.annotations.Test;
import org.w3c.dom.Element;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.Properties;

/**
 * Tests the compatibility between <tt>JDBCCacheLoader</tt> and <tt>JDBCCacheLoaderOld</tt>. More exactly,
 * it tests whether the new <tt>JDBCCacheLoader</tt> works fine on data previously created
 * <tt>JDBCCacheLoaderOld</tt>.
 *
 * @author Mircea.Markus@iquestint.com
 * @version 1.0
 */
@Test(groups = {"functional"})
@SuppressWarnings("deprecation")
public class AdjListJDBCCacheLoaderCompatibilityTest
{

   @SuppressWarnings("deprecation")
   private JDBCCacheLoaderOld oldImpl;
   private JDBCCacheLoader newImpl;
   private CacheSPI cache, cache2;


   /**
    * Note : newImpl is not started here but in each individual test. That's because on start it performs
    * some backward compatibility logic.
    *
    * @see JDBCCacheLoader#start()
    */
   @BeforeMethod(alwaysRun = true)
   public void setUp() throws Exception
   {
      newImpl = getNewCacheLoader();
      oldImpl = getOldLoader();
      cache = (CacheSPI) new DefaultCacheFactory().createCache();
      cache2 = (CacheSPI) new DefaultCacheFactory().createCache();
      newImpl.setCache(cache);//this is needed for marshaller
      oldImpl.setCache(cache2);
      oldImpl.start();
      oldImpl.remove(Fqn.ROOT);
   }

   @AfterMethod(alwaysRun = true)
   public void tearDown() throws Exception
   {
      oldImpl.remove(Fqn.ROOT);
      oldImpl.stop();
      newImpl.stop();

      TestingUtil.killCaches(cache, cache2);
   }

   public void testCommonOperations() throws Exception
   {
      newImpl.start();
      oldImpl.put(Fqn.fromString("/a/b/c"), "key1", "value1");
      oldImpl.put(Fqn.fromString("/a/b/d"), "key2", "value2");
      oldImpl.put(Fqn.fromString("/a/b/e"), "key3", "value3");
      assertTrue(newImpl.exists(Fqn.fromString("/a/b/c")));
      assertTrue(newImpl.exists(Fqn.fromString("/a/b/d")));
      assertTrue(newImpl.exists(Fqn.fromString("/a/b/e")));
      assertEquals("value1", newImpl.get(Fqn.fromString("/a/b/c")).get("key1"));
      assertEquals("value2", newImpl.get(Fqn.fromString("/a/b/d")).get("key2"));
      assertEquals("value3", newImpl.get(Fqn.fromString("/a/b/e")).get("key3"));
   }

   /**
    * Does the new implementation manage to successfully remove nodes created by old one?
    */
   public void testRemove() throws Exception
   {
      oldImpl.put(Fqn.fromString("/a/b/c"), "key1", "value1");
      oldImpl.put(Fqn.fromString("/a/b/d"), "key2", "value2");
      oldImpl.put(Fqn.fromString("/a/b/e"), "key3", "value3");
      oldImpl.put(Fqn.fromString("/a/f/e"), "key4", "value4");
      assertTrue(newImpl.exists(Fqn.fromString("/a/b/c")));
      assertTrue(newImpl.exists(Fqn.fromString("/a/b/d")));
      assertTrue(newImpl.exists(Fqn.fromString("/a/b/e")));
      newImpl.start();
      newImpl.remove(Fqn.fromString("/a/b"));
      assertFalse(newImpl.exists(Fqn.fromString("/a/b/c")));
      assertFalse(newImpl.exists(Fqn.fromString("/a/b/d")));
      assertFalse(newImpl.exists(Fqn.fromString("/a/b/e")));
      assertTrue(newImpl.exists(Fqn.fromString("/a/f")));
      assertTrue(newImpl.exists(Fqn.fromString("/a/f/e")));
   }

   public void testLoadEntireState() throws Exception
   {
      oldImpl.put(Fqn.fromString("/a/b/c"), "key1", "value1");
      oldImpl.put(Fqn.fromString("/a/b/d"), "key2", "value2");
      oldImpl.put(Fqn.fromString("/a/b/e"), "key3", "value3");
      oldImpl.put(Fqn.fromString("/a/f/e"), "key4", "value4");
      oldImpl.put(Fqn.ROOT, "root_key", "root_value");

      ByteArrayOutputStream newBaos = new ByteArrayOutputStream(1024);
      MarshalledValueOutputStream newOs = new MarshalledValueOutputStream(newBaos);
      newImpl.start();
      newImpl.loadEntireState(newOs);
      newImpl.getMarshaller().objectToObjectStream(StateTransferManager.STREAMING_DELIMITER_NODE, newOs);
      newOs.close();
      newImpl.remove(Fqn.ROOT);
      assertNull(newImpl.get(Fqn.fromString("/a/b/c")));
      assertNull(newImpl.get(Fqn.fromString("/a/b/d")));
      assertNull(newImpl.get(Fqn.fromString("/a/b/e")));
      assertNull(newImpl.get(Fqn.fromString("/a/f/e")));
      assertNull(newImpl.get(Fqn.ROOT));
      ByteArrayInputStream bais = new ByteArrayInputStream(newBaos.toByteArray());
      MarshalledValueInputStream is = new MarshalledValueInputStream(bais);
      newImpl.storeEntireState(is);
      assertEquals(newImpl.get(Fqn.fromString("/a/b/c")).get("key1"), "value1");
      assertEquals(newImpl.get(Fqn.fromString("/a/b/d")).get("key2"), "value2");
      assertEquals(newImpl.get(Fqn.fromString("/a/b/e")).get("key3"), "value3");
      assertEquals(newImpl.get(Fqn.fromString("/a/f/e")).get("key4"), "value4");
      assertEquals("root_value", newImpl.get(Fqn.ROOT).get("root_key"));
      assertEquals(newImpl.getNodeCount(), 8);
   }

   public void testLoadNodeState() throws Exception
   {
      oldImpl.put(Fqn.fromString("/a/b/c"), "key1", "value1");
      oldImpl.put(Fqn.fromString("/a/b/d"), "key2", "value2");
      oldImpl.put(Fqn.fromString("/a/b/e"), "key3", "value3");
      oldImpl.put(Fqn.fromString("/a/f/e"), "key4", "value4");
      oldImpl.put(Fqn.ROOT, "root_key", "root_value");

      ByteArrayOutputStream newBaos = new ByteArrayOutputStream(1024);
      MarshalledValueOutputStream newOs = new MarshalledValueOutputStream(newBaos);
      newImpl.start();
      newImpl.loadState(Fqn.fromString("/a/b"), newOs);
      newImpl.getMarshaller().objectToObjectStream(StateTransferManager.STREAMING_DELIMITER_NODE, newOs);
      newOs.close();

      newImpl.remove(Fqn.fromString("/a/b"));
      assertNull(newImpl.get(Fqn.fromString("/a/b/c")));
      assertNull(newImpl.get(Fqn.fromString("/a/b/d")));
      assertNull(newImpl.get(Fqn.fromString("/a/b/e")));
      assertNull(newImpl.get(Fqn.fromString("/a/b")));

      ByteArrayInputStream bais = new ByteArrayInputStream(newBaos.toByteArray());
      MarshalledValueInputStream is = new MarshalledValueInputStream(bais);
      newImpl.storeState(Fqn.fromString("/a/b"), is);

      assertEquals(newImpl.get(Fqn.fromString("/a/b/c")).get("key1"), "value1");
      assertEquals(newImpl.get(Fqn.fromString("/a/b/d")).get("key2"), "value2");
      assertEquals(newImpl.get(Fqn.fromString("/a/b/e")).get("key3"), "value3");
      assertEquals(newImpl.get(Fqn.fromString("/a/f/e")).get("key4"), "value4");
      assertEquals(newImpl.get(Fqn.ROOT).get("root_key"), "root_value");
      assertEquals(newImpl.getNodeCount(), 8);
   }

   /**
    * getNodeDataList is a template method on which the serialisation process relies. We check here that the new
    * implementation works exactelly as the old one.
    */
   public void testGetNodeData() throws Exception
   {
      oldImpl.put(Fqn.fromString("/a/b/c"), "key1", "value1");
      oldImpl.put(Fqn.fromString("/a/b/d"), "key2", "value2");
      oldImpl.put(Fqn.fromString("/a/b/e"), "key3", "value3");
      oldImpl.put(Fqn.fromString("/a/f/e"), "key4", "value4");
      oldImpl.put(Fqn.ROOT, "root_key", "root_value");
      newImpl.start();
      ArrayList<NodeData> oldList = new ArrayList<NodeData>();
      oldImpl.getNodeDataList(Fqn.ROOT, oldList);
      ArrayList<NodeData> newList = new ArrayList<NodeData>();
      newImpl.getNodeDataList(Fqn.ROOT, newList);
      assertEquals(new HashSet<NodeData>(oldList), new HashSet<NodeData>(newList));
   }

   /**
    * Tests performs some backward copatibility work. See {@link JDBCCacheLoader#start()} for details.
    */
   public void testStartWork() throws Exception
   {
      oldImpl.put(Fqn.fromString("/a/b/c"), "key1", "value1");
      oldImpl.put(Fqn.fromString("/a/b/d"), "key2", "value2");
      assertNull(oldImpl.get(Fqn.ROOT));
      newImpl.start();
      assertNotNull(newImpl.get(Fqn.ROOT));
   }


   protected CacheLoaderConfig getSingleCacheLoaderConfig(String preload, String cacheloaderClass, String properties) throws Exception
   {
      String xml = "<config>\n" +
            "<passivation>false</passivation>\n" +
            "<preload>" + preload + "</preload>\n" +
            "<cacheloader>\n" +
            "<class>" + cacheloaderClass + "</class>\n" +
            "<properties>" + properties + "</properties>\n" +
            "<async>false</async>\n" +
            "<shared>false</shared>\n" +
            "<fetchPersistentState>true</fetchPersistentState>\n" +
            "<purgeOnStartup>false</purgeOnStartup>\n" +
            "</cacheloader>\n" +
            "</config>";
      Element element = XmlHelper.stringToElement(xml);
      return XmlConfigurationParser.parseCacheLoaderConfig(element);
   }

   protected Properties getProperties() throws Exception
   {
      Properties prop = new Properties();
      try
      {
         prop.load(this.getClass().getClassLoader().getResourceAsStream("cache-jdbc.properties"));
         return prop;
      }
      catch (Exception e)
      {
         throw new Exception("Error loading jdbc properties ", e);
      }
   }

   private JDBCCacheLoader getNewCacheLoader() throws Exception
   {
      Properties prop = getProperties();

      String props = "cache.jdbc.driver =" + prop.getProperty("cache.jdbc.driver") + "\n" +
            "cache.jdbc.url=" + prop.getProperty("cache.jdbc.url") + "\n" +
            "cache.jdbc.user=" + prop.getProperty("cache.jdbc.user") + "\n" +
            "cache.jdbc.password=" + prop.getProperty("cache.jdbc.password") + "\n" +
            "cache.jdbc.node.type=" + prop.getProperty("cache.jdbc.node.type") + "\n" +
            "cache.jdbc.sql-concat=" + prop.getProperty("cache.jdbc.sql-concat") + "\n";


      CacheLoaderConfig.IndividualCacheLoaderConfig base = getSingleCacheLoaderConfig("", "org.jboss.cache.loader.JDBCCacheLoader", props).getFirstCacheLoaderConfig();

      JDBCCacheLoader jdbcCacheLoader = new JDBCCacheLoader();
      jdbcCacheLoader.setConfig(base);
      return jdbcCacheLoader;
   }


   private JDBCCacheLoaderOld getOldLoader() throws Exception
   {
      Properties prop = getProperties();

      String props = "cache.jdbc.driver =" + prop.getProperty("cache.jdbc.driver") + "\n" +
            "cache.jdbc.url=" + prop.getProperty("cache.jdbc.url") + "\n" +
            "cache.jdbc.user=" + prop.getProperty("cache.jdbc.user") + "\n" +
            "cache.jdbc.password=" + prop.getProperty("cache.jdbc.password") + "\n" +
            "cache.jdbc.node.type=" + prop.getProperty("cache.jdbc.node.type") + "\n" +
            "cache.jdbc.sql-concat=" + prop.getProperty("cache.jdbc.sql-concat");// + "\n" +
//                "cache.jdbc.connection.factory=org.jboss.cache.manualtests.cacheloader.OneConnectionFactory";


      CacheLoaderConfig.IndividualCacheLoaderConfig base = getSingleCacheLoaderConfig("", "org.jboss.cache.loader.JDBCCacheLoader", props).getFirstCacheLoaderConfig();
      JDBCCacheLoaderOld jdbcCacheLoader = new JDBCCacheLoaderOld();
      jdbcCacheLoader.setConfig(base);
      return jdbcCacheLoader;
   }

}

