/*
 * Copyright (C) 2009 eXo Platform SAS.
 *
 * 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.exoplatform.services.jcr.impl.core;

import org.exoplatform.services.jcr.JcrImplBaseTest;
import org.exoplatform.services.jcr.dataflow.ItemState;
import org.exoplatform.services.jcr.datamodel.IllegalNameException;
import org.exoplatform.services.jcr.datamodel.InternalQName;
import org.exoplatform.services.jcr.datamodel.ItemType;
import org.exoplatform.services.jcr.datamodel.NodeData;
import org.exoplatform.services.jcr.datamodel.QPath;
import org.exoplatform.services.jcr.datamodel.QPathEntry;
import org.exoplatform.services.jcr.impl.Constants;
import org.exoplatform.services.jcr.impl.dataflow.TransientNodeData;
import org.exoplatform.services.jcr.impl.dataflow.TransientPropertyData;
import org.exoplatform.services.jcr.impl.dataflow.TransientValueData;
import org.exoplatform.services.jcr.impl.dataflow.session.SessionChangesLog;

import java.util.ArrayList;
import java.util.List;

import javax.jcr.InvalidItemStateException;
import javax.jcr.ItemExistsException;
import javax.jcr.Node;
import javax.jcr.PathNotFoundException;
import javax.jcr.PropertyType;
import javax.jcr.RepositoryException;
import javax.jcr.lock.LockException;
import javax.jcr.nodetype.ConstraintViolationException;
import javax.jcr.version.VersionException;

/**
 * Created by The eXo Platform SAS.
 * 
 * @author <a href="mailto:geaz@users.sourceforge.net">Gennady Azarenkov</a>
 * @version $Id: TestSessionDataManager.java 11907 2008-03-13 15:36:21Z ksm $
 */
public class TestSessionDataManager extends JcrImplBaseTest
{

   private static String TEST_ROOT = "TestSessionDataManager";

   private SessionDataManager modificationManager;

   private NodeImpl testRoot;

   @Override
   public void setUp() throws Exception
   {
      super.setUp();
      modificationManager = session.getTransientNodesManager();
      NodeImpl root = (NodeImpl)session.getRootNode();

      TransientNodeData data =
         TransientNodeData.createNodeData(root.nodeData(), new InternalQName(null, TEST_ROOT), new InternalQName(
            Constants.NS_NT_URI, "unstructured"));
      testRoot = (NodeImpl)modificationManager.update(ItemState.createAddedState(data), false);
      TransientPropertyData prop =
         TransientPropertyData.createPropertyData(data, new InternalQName(Constants.NS_JCR_URI, "primaryType"),
            PropertyType.NAME, false, new TransientValueData(new InternalQName(Constants.NS_NT_URI, "unstructured")));
      PropertyImpl prop1 = (PropertyImpl)modificationManager.update(ItemState.createAddedState(prop), false);

      assertEquals(2, modificationManager.getChangesLog().getSize());

      modificationManager.commit(data.getQPath());
      assertEquals(0, modificationManager.getChangesLog().getSize());
   }

   public void testItemReferencePool() throws Exception
   {

      SessionDataManager.ItemReferencePool pool = modificationManager.getItemsPool();
      System.gc();
      Thread.sleep(1000);

      assertEquals(1, pool.size()); // contains root node only

      NodeData parent = (NodeData)testRoot.getData();

      TransientNodeData data =
         TransientNodeData.createNodeData(parent, new InternalQName(null, "testItemReferencePool1"), new InternalQName(
            Constants.NS_NT_URI, "unstructured"));

      String uuid = data.getIdentifier();

      assertEquals(1, pool.size());
      NodeImpl node1 = (NodeImpl)modificationManager.update(ItemState.createAddedState(data), true);
      assertEquals(2, pool.size());
      assertEquals(uuid, node1.getInternalIdentifier());
      assertTrue(pool.contains(uuid));
      // return the same value
      assertEquals(node1, pool.get(node1.getData()));

      // add one more node
      data =
         TransientNodeData.createNodeData(parent, new InternalQName(null, "testItemReferencePool2"), new InternalQName(
            Constants.NS_NT_URI, "unstructured"));

      NodeImpl node2 = (NodeImpl)modificationManager.update(ItemState.createAddedState(data), true);

      List<NodeImpl> nodes = new ArrayList<NodeImpl>();
      nodes.add(node1);
      nodes.add(node2);

      List<NodeImpl> testNodes = getNodes(pool, nodes);
      assertEquals(2, testNodes.size());
      assertEquals(node1, testNodes.get(0));
      assertEquals(node2, testNodes.get(1));

      // ... add property
      TransientPropertyData prop =
         TransientPropertyData.createPropertyData(parent, new InternalQName(null, "testItemReferencePoolProp1"),
            PropertyType.STRING, false);
      PropertyImpl prop1 = (PropertyImpl)modificationManager.update(ItemState.createAddedState(prop), true);
      List<PropertyImpl> props = new ArrayList<PropertyImpl>();
      props.add(prop1);
      List<PropertyImpl> testProps = getProperties(pool, props);
      assertEquals(1, testProps.size());
      assertEquals(prop1, testProps.get(0));

      pool.remove(uuid);
      // in case for GC
      Thread.sleep(1000);
      // 1 node and 1 prop
      if (log.isDebugEnabled())
         log.debug(" >>>>> >>>> > " + modificationManager.dump());

      // /TestSessionDataManager/testItemReferencePoolProp1
      // /TestSessionDataManager
      // /TestSessionDataManager/testItemReferencePool2

      assertEquals(3, pool.size());
   }

   private List<NodeImpl> getNodes(SessionDataManager.ItemReferencePool pool, List<NodeImpl> nodes)
      throws RepositoryException
   {
      List<ItemImpl> items = (List<ItemImpl>)pool.getAll();
      List<NodeImpl> children = new ArrayList<NodeImpl>();
      for (NodeImpl node : nodes)
      {
         String id = node.getInternalIdentifier();

         NodeImpl pooled = null;
         for (ItemImpl item : items)
         {
            if (items.get(0).getData().getIdentifier().equals(id))
            {
               pooled = (NodeImpl)item;
               break;
            }
         }

         if (pooled == null)
         {
            children.add(node);
         }
         else
         {
            pooled.loadData(node.getData());
            children.add(pooled);
         }
      }
      return children;
   }

   private List<PropertyImpl> getProperties(SessionDataManager.ItemReferencePool pool, List<PropertyImpl> props)
      throws RepositoryException
   {
      List<ItemImpl> items = (List<ItemImpl>)pool.getAll();
      List<PropertyImpl> children = new ArrayList<PropertyImpl>();
      for (PropertyImpl prop : props)
      {
         String id = prop.getInternalIdentifier();

         PropertyImpl pooled = null;
         for (ItemImpl item : items)
         {
            if (items.get(0).getData().getIdentifier().equals(id))
            {
               pooled = (PropertyImpl)item;
               break;
            }
         }

         if (pooled == null)
         {
            children.add(prop);
         }
         else
         {
            pooled.loadData(prop.getData());
            children.add(pooled);
         }
      }
      return children;
   }

   public void testSessionChangesLog() throws Exception
   {
      SessionChangesLog changesLog = modificationManager.getChangesLog();

      assertEquals(0, changesLog.getAllStates().size());

      NodeData parent = (NodeData)testRoot.getData();

      TransientNodeData data =
         TransientNodeData.createNodeData(parent, new InternalQName(null, "testSessionChangesLogN1"),
            new InternalQName(Constants.NS_NT_URI, "unstructured"));

      NodeImpl node1 = (NodeImpl)modificationManager.update(ItemState.createAddedState(data), true);
      assertEquals(1, changesLog.getAllStates().size());
      assertNotNull(changesLog.getItemState(node1.getInternalIdentifier()));
      assertNotNull(changesLog.getItemState(node1.getInternalPath()));
      assertTrue(changesLog.getItemState(node1.getInternalIdentifier()).isAdded());

      // delete this node ... state should be DELETED
      modificationManager.delete(data);
      assertEquals(2, changesLog.getAllStates().size());
      List<ItemState> lst = changesLog.getItemStates(data.getIdentifier());
      assertEquals(2, lst.size());
      assertTrue(changesLog.getItemState(data.getIdentifier()).isDeleted());

      // add the same node ... state should be ADDED again
      node1 = (NodeImpl)modificationManager.update(ItemState.createAddedState(data), true);
      assertEquals(3, changesLog.getItemStates(data.getIdentifier()).size());
      assertTrue(changesLog.getItemState(data.getIdentifier()).isAdded());

      assertEquals(3, changesLog.getAllStates().size());

      // ... add property to the node1
      TransientPropertyData prop =
         TransientPropertyData.createPropertyData((NodeData)node1.getData(), new InternalQName(null,
            "testSessionChangesLogP1"), PropertyType.STRING, false, new TransientValueData(false));
      PropertyImpl prop1 = (PropertyImpl)modificationManager.update(ItemState.createAddedState(prop), true);

      assertTrue(changesLog.getItemState(node1.getInternalIdentifier()).isAdded());
      assertTrue(changesLog.getItemState(prop.getIdentifier()).isAdded());

      assertEquals(4, changesLog.getAllStates().size());

      // // 4 changes: 3 for node1, and 1- for prop1
      assertEquals(4, changesLog.getDescendantsChanges(node1.getInternalPath()).size());

   }

   public void testReadMethods() throws Exception
   {

      NodeData parent = (NodeData)testRoot.getData();
      TransientNodeData someData =
         TransientNodeData.createNodeData(parent, new InternalQName(null, "testReadMethodsN1"), new InternalQName(
            Constants.NS_NT_URI, "unstructured"));

      // add one more node
      TransientNodeData data =
         TransientNodeData.createNodeData(parent, new InternalQName(null, "testReadMethodsN2"), new InternalQName(
            Constants.NS_NT_URI, "unstructured"));
      NodeImpl node2 = (NodeImpl)modificationManager.update(ItemState.createAddedState(data), true);

      // ... add property
      TransientPropertyData prop =
         TransientPropertyData.createPropertyData(parent, new InternalQName(null, "testReadMethodsP1"),
            PropertyType.STRING, false, new TransientValueData(false));
      PropertyImpl prop1 = (PropertyImpl)modificationManager.update(ItemState.createAddedState(prop), true);

      assertNotNull(modificationManager.getItemData(data.getQPath()));
      assertEquals(data.getIdentifier(), modificationManager.getItemData(data.getQPath()).getIdentifier());

      assertEquals(node2, modificationManager.getItem(data.getQPath(), true));

      assertEquals(prop.getIdentifier(), modificationManager.getItemData(prop.getIdentifier()).getIdentifier());
      assertEquals(prop1, modificationManager.getItemByIdentifier(prop.getIdentifier(), true));

      assertTrue(modificationManager.hasPendingChanges(data.getQPath()));
      assertFalse(modificationManager.hasPendingChanges(someData.getQPath()));

      assertTrue(modificationManager.isNew(prop.getIdentifier()));
      assertFalse(modificationManager.isNew(parent.getIdentifier()));

      assertFalse(modificationManager.isModified(prop));
      modificationManager.update(ItemState.createUpdatedState(prop), true);
      assertTrue(modificationManager.isModified(prop));

      assertEquals(1, modificationManager.getChildNodesData(parent).size());
      assertEquals(2, modificationManager.getChildPropertiesData(parent).size());

      assertEquals(1, modificationManager.getChildNodesData(parent).size());
      assertEquals(2, modificationManager.getChildPropertiesData(parent).size());
   }

   public void testCommitAndRefresh() throws Exception
   {

      NodeData parent = (NodeData)testRoot.getData();
      TransientNodeData data1 =
         TransientNodeData.createNodeData(parent, new InternalQName(null, "testCommitAndRefreshN1"), new InternalQName(
            Constants.NS_NT_URI, "unstructured"));
      NodeImpl node1 = (NodeImpl)modificationManager.update(ItemState.createAddedState(data1), true);
      TransientPropertyData nt =
         TransientPropertyData.createPropertyData(data1, new InternalQName(Constants.NS_JCR_URI, "primaryType"),
            PropertyType.NAME, false, new TransientValueData(new InternalQName(Constants.NS_NT_URI, "unstructured")));
      modificationManager.update(ItemState.createAddedState(nt), true);
      assertEquals(1, modificationManager.getChildPropertiesData(data1).size());

      modificationManager.commit(data1.getQPath());

      // make sure changes are saved
      assertEquals(0, modificationManager.getChangesLog().getSize());

      assertNotNull(modificationManager.getItem(data1.getQPath(), true));
      
      assertEquals(1, modificationManager.getChildPropertiesData(parent).size());

      // ... add property
      TransientPropertyData prop =
         TransientPropertyData.createPropertyData(parent, new InternalQName(null, "testCommitAndRefreshP1"),
            PropertyType.STRING, false, new TransientValueData("test"));
      modificationManager.update(ItemState.createAddedState(prop), true);
      assertEquals("test", ((PropertyImpl)modificationManager.getItem(prop.getQPath(), true)).getString());
   }

   @SuppressWarnings("deprecation")
   public void testGetItem() throws RepositoryException, IllegalNameException
   {
      // get non-existent data by getItemData(NodeData parent, QPathEntry name)
      assertNull(modificationManager.getItemData((NodeData)((NodeImpl)root).getData(), new QPathEntry("",
         "testgetitemNode", 0), ItemType.NODE));
      // get non-existent data by ItemData getItemData(QPath path)
      assertNull(modificationManager.getItemData(QPath.makeChildPath(((NodeImpl)root).getData().getQPath(),
         new InternalQName("", "testgetitemNode"))));

      NodeImpl testNode = (NodeImpl)root.addNode("testgetitemNode");

      // get data by getItemData(NodeData parent, QPathEntry name)
      assertNotNull(modificationManager.getItemData((NodeData)((NodeImpl)root).getData(), new QPathEntry("",
         "testgetitemNode", 0), ItemType.NODE));
      // get data by ItemData getItemData(QPath path)
      assertNotNull(modificationManager.getItemData(QPath.makeChildPath(((NodeImpl)root).getData().getQPath(),
         new InternalQName("", "testgetitemNode"))));
   }

   public void testIsDeleted() throws ItemExistsException, PathNotFoundException, VersionException,
      ConstraintViolationException, LockException, RepositoryException
   {
      NodeImpl testNode = (NodeImpl)root.addNode("testNode");
      String identifier = testNode.getIdentifier();
      QPath itemPath = testNode.getData().getQPath();

      assertFalse(modificationManager.isDeleted(identifier));
      assertFalse(modificationManager.isDeleted(itemPath));

      testNode.remove();
      assertTrue(modificationManager.isDeleted(identifier));
      assertTrue(modificationManager.isDeleted(itemPath));
   }

   public void testGetACLWhenNodeIsRoot() throws RepositoryException
   {
      NodeImpl rootNode = (NodeImpl)root;
      assertEquals(rootNode.getACL(), modificationManager.getACL(rootNode.getData().getQPath()));
   }
   
   public void testUpdateWhenStateIsDeleted() throws ItemExistsException, PathNotFoundException, VersionException,
      ConstraintViolationException, LockException, RepositoryException
   {
      NodeImpl testNode = (NodeImpl)root.addNode("testNode");
      ItemState state = ItemState.createDeletedState(testNode.getData());

      try
      {
         modificationManager.update(state, false);
         fail();
      }
      catch (RepositoryException e)
      {
      }
   }

   public void testUpdateStateWhenStateIsDeleted() throws ItemExistsException, PathNotFoundException, VersionException,
      ConstraintViolationException, LockException, RepositoryException
   {
      NodeImpl testNode = (NodeImpl)root.addNode("testNode");
      ItemState state = ItemState.createDeletedState(testNode.getData());

      try
      {
         modificationManager.updateItemState(state);
         fail();
      }
      catch (RepositoryException e)
      {
      }
   }

   public void testRefreshWhenNodeHasModifiedStatus() throws ItemExistsException, PathNotFoundException,
      VersionException, ConstraintViolationException, LockException, RepositoryException
   {
      NodeImpl testNode = (NodeImpl)root.addNode("testNode");

      try
      {
         modificationManager.refresh(testNode.getData());
         fail();
      }
      catch (InvalidItemStateException e)
      {
      }
   }

   /*
    * This test is testing problem described in JCR-1864.
    * We add 2 node to the system node and we check lastOrderNum for system node.
    * Last orderNum from method SessionDataManager.getLastOrderNumber must be equal orderNum from last added node. 
    */
   public void testGetLastOrderNumFromSystemNode() throws RepositoryException
   {
      SessionImpl session2 = (SessionImpl)repository.login(credentials, "ws1");
      SessionDataManager dataManager = session2.getTransientNodesManager();

      Node systemNode = session2.getRootNode().getNode("jcr:system");
      systemNode.addNode("node1");
      NodeImpl node2 = (NodeImpl)systemNode.addNode("node2");

      session2.save();

      int lastOrderNum = dataManager.getLastOrderNumber((NodeData)dataManager.getItemData(Constants.SYSTEM_UUID));
      int orderNum = ((NodeData)dataManager.getItemData(node2.getIdentifier())).getOrderNumber();

      assertEquals(lastOrderNum, orderNum);
   }
}