/**
 * Copyright (C) 2010 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.xcmis.spi.tck;

import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;

import org.junit.BeforeClass;
import org.xcmis.spi.CmisConstants;
import org.xcmis.spi.CmisRegistry;
import org.xcmis.spi.Connection;
import org.xcmis.spi.ContentStream;
import org.xcmis.spi.FolderData;
import org.xcmis.spi.ItemsTree;
import org.xcmis.spi.ObjectNotFoundException;
import org.xcmis.spi.PropertyFilter;
import org.xcmis.spi.RenditionFilter;
import org.xcmis.spi.TypeNotFoundException;
import org.xcmis.spi.UserContext;
import org.xcmis.spi.model.ACLCapability;
import org.xcmis.spi.model.AccessControlEntry;
import org.xcmis.spi.model.BaseType;
import org.xcmis.spi.model.CmisObject;
import org.xcmis.spi.model.ContentStreamAllowed;
import org.xcmis.spi.model.IncludeRelationships;
import org.xcmis.spi.model.Permission;
import org.xcmis.spi.model.Property;
import org.xcmis.spi.model.PropertyDefinition;
import org.xcmis.spi.model.PropertyType;
import org.xcmis.spi.model.RepositoryCapabilities;
import org.xcmis.spi.model.RepositoryShortInfo;
import org.xcmis.spi.model.TypeDefinition;
import org.xcmis.spi.model.UnfileObject;
import org.xcmis.spi.model.Updatability;
import org.xcmis.spi.model.VersioningState;
import org.xcmis.spi.model.impl.BooleanProperty;
import org.xcmis.spi.model.impl.DateTimeProperty;
import org.xcmis.spi.model.impl.DecimalProperty;
import org.xcmis.spi.model.impl.HtmlProperty;
import org.xcmis.spi.model.impl.IdProperty;
import org.xcmis.spi.model.impl.IntegerProperty;
import org.xcmis.spi.model.impl.StringProperty;
import org.xcmis.spi.model.impl.UriProperty;
import org.xcmis.spi.utils.CmisUtils;
import org.xcmis.spi.utils.MimeType;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.net.URI;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Calendar;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;

/**
 * @author <a href="mailto:alexey.zavizionov@exoplatform.com">Alexey
 *         Zavizionov</a>
 * @version $Id$
 */

public class BaseTest
{

   protected static ACLCapability aclCapability;

   protected static RepositoryCapabilities capabilities;

   protected static Connection connection;

   protected static boolean isPoliciesSupported = false;

   protected static boolean isRelationshipsSupported = false;

   protected static FolderData rootFolder;

   protected static String rootFolderID;

   protected static byte[] TEST_CONTENT = "__TEST_CONTENT__".getBytes();

   protected static final ContentStream TEST_CONTENT_STREAM = new ContentStream()
   {

      MimeType mimeType = new MimeType("text", "plain");

      public String getFileName()
      {
         return "";
      }

      public MimeType getMediaType()
      {
         return mimeType;
      }

      public InputStream getStream() throws IOException
      {
         return new ByteArrayInputStream(TEST_CONTENT);
      }

      public long length()
      {
         return TEST_CONTENT.length;
      }
   };

   protected static final String TEST_POLICY_TEXT = "__TEST_POLICY__";

   @BeforeClass
   public static void init() throws Exception
   {
      CmisRegistry reg = CmisRegistry.getInstance();
      Iterator<RepositoryShortInfo> it = reg.getStorageInfos().iterator();
      if (!it.hasNext())
         fail("Could not find storages to test");
      connection = reg.getConnection(it.next().getRepositoryId());

      UserContext ctx = new UserContext("root");
      UserContext.setCurrent(ctx);

      rootFolderID = connection.getStorage().getRepositoryInfo().getRootFolderId();

      try
      {
         connection.getTypeDefinition(CmisConstants.POLICY);
         isPoliciesSupported = true;
      }
      catch (TypeNotFoundException e)
      {
      }

      try
      {
         connection.getTypeDefinition(CmisConstants.RELATIONSHIP);
         isRelationshipsSupported = true;
      }
      catch (TypeNotFoundException e)
      {
      }

      capabilities = connection.getStorage().getRepositoryInfo().getCapabilities();
      aclCapability = connection.getStorage().getRepositoryInfo().getAclCapability();
   }

   public static void clear(String root)
   {
      try
      {
         if (isRelationshipsSupported)
         {
            removeRelationships(root);
         }
         connection.deleteTree(root, true, UnfileObject.DELETE, true);
         connection.close();
      }
      catch (Exception e)
      {
         e.printStackTrace();
      }
   }

   protected static List<AccessControlEntry> createACL(String name, String... permission)
   {
      AccessControlEntry acl = new AccessControlEntry();
      acl.setPrincipal(name);
      acl.getPermissions().addAll(Arrays.asList(permission));
      ArrayList<AccessControlEntry> addACL = new ArrayList<AccessControlEntry>();
      addACL.add(acl);
      return addACL;
   }

   protected static String createDocument(String parentId, String typeId, String name, ContentStream content,
      List<AccessControlEntry> addACL, List<AccessControlEntry> removeACL, Collection<String> policies,
      VersioningState versioningState) throws Exception
   {
      TypeDefinition type = connection.getTypeDefinition(typeId, true);

      boolean versionable = type.isVersionable();
      if (!versionable)
      {
         // Prevent errors if type is not versionable.
         versioningState = VersioningState.NONE;
      }

      // Prevent error if content stream is required but not provided.
      ContentStreamAllowed streamAllowed = type.getContentStreamAllowed();
      if (streamAllowed == ContentStreamAllowed.REQUIRED && content == null)
      {
         content = TEST_CONTENT_STREAM;
      }

      Map<String, Property<?>> properties = createPropertyMap(type);
      StringProperty nameProperty = (StringProperty)properties.get(CmisConstants.NAME);
      if (nameProperty != null)
      {
         nameProperty.getValues().add(name);
      }

      String documentId = connection.createDocument(parentId, //
         properties, //
         content, //
         addACL, //
         removeACL, //
         policies, //
         versioningState);

      return documentId;
   }

   protected static String createFolder(String parentId, String typeId, String name, List<AccessControlEntry> addACL,
      List<AccessControlEntry> removeACL, Collection<String> policies) throws Exception
   {
      TypeDefinition type = connection.getTypeDefinition(typeId, true);
      Map<String, Property<?>> properties = createPropertyMap(type);
      StringProperty nameProperty = (StringProperty)properties.get(CmisConstants.NAME);
      if (nameProperty != null)
      {
         nameProperty.getValues().add(name);
      }
      String folderId = connection.createFolder(parentId, //
         properties, //
         addACL, //
         removeACL, //
         policies);
      return folderId;
   }

   protected static String createPolicy(String parentId, String typeId, String name, String policyText,
      List<AccessControlEntry> addACL, List<AccessControlEntry> removeACL, Collection<String> policies)
      throws Exception
   {
      String policyId = null;
      if (isPoliciesSupported)
      {
         TypeDefinition type = connection.getTypeDefinition(typeId, true);
         Map<String, Property<?>> properties = createPropertyMap(type);
         StringProperty nameProperty = (StringProperty)properties.get(CmisConstants.NAME);
         if (nameProperty != null)
         {
            nameProperty.getValues().add(name);
         }
         StringProperty policyTextProperty = (StringProperty)properties.get(CmisConstants.POLICY_TEXT);
         if (policyTextProperty != null)
         {
            policyTextProperty.getValues().add(policyText != null ? policyText : TEST_POLICY_TEXT);
         }
         policyId = connection.createPolicy(type.isFileable() ? parentId : null, properties, null, null, null);
      }
      return policyId;
   }

   protected static Map<String, Property<?>> createPropertyMap(TypeDefinition type) throws Exception
   {
      Map<String, Property<?>> properties = new HashMap<String, Property<?>>();
      for (PropertyDefinition<?> propertyDefinition : type.getPropertyDefinitions())
      {
         Updatability updatability = propertyDefinition.getUpdatability();
         if (updatability == Updatability.ONCREATE || updatability == Updatability.READWRITE)
         {
            Property<?> property = null;
            PropertyType propertyType = propertyDefinition.getPropertyType();
            switch (propertyType)
            {
               case BOOLEAN :
                  property =
                     new BooleanProperty(propertyDefinition.getId(), propertyDefinition.getQueryName(),
                        propertyDefinition.getLocalName(), propertyDefinition.getDisplayName(), (Boolean)null);
                  break;
               case DATETIME :
                  property =
                     new DateTimeProperty(propertyDefinition.getId(), propertyDefinition.getQueryName(),
                        propertyDefinition.getLocalName(), propertyDefinition.getDisplayName(), (Calendar)null);
                  break;
               case DECIMAL :
                  property =
                     new DecimalProperty(propertyDefinition.getId(), propertyDefinition.getQueryName(),
                        propertyDefinition.getLocalName(), propertyDefinition.getDisplayName(), (BigDecimal)null);
                  break;
               case HTML :
                  property =
                     new HtmlProperty(propertyDefinition.getId(), propertyDefinition.getQueryName(), propertyDefinition
                        .getLocalName(), propertyDefinition.getDisplayName(), (String)null);
                  break;
               case ID :
                  property =
                     new IdProperty(propertyDefinition.getId(), propertyDefinition.getQueryName(), propertyDefinition
                        .getLocalName(), propertyDefinition.getDisplayName(), (String)null);
                  break;
               case INTEGER :
                  property =
                     new IntegerProperty(propertyDefinition.getId(), propertyDefinition.getQueryName(),
                        propertyDefinition.getLocalName(), propertyDefinition.getDisplayName(), (BigInteger)null);
                  break;
               case STRING :
                  property =
                     new StringProperty(propertyDefinition.getId(), propertyDefinition.getQueryName(),
                        propertyDefinition.getLocalName(), propertyDefinition.getDisplayName(), (String)null);
                  break;
               case URI :
                  property =
                     new UriProperty(propertyDefinition.getId(), propertyDefinition.getQueryName(), propertyDefinition
                        .getLocalName(), propertyDefinition.getDisplayName(), (URI)null);
                  break;
            }
            properties.put(propertyDefinition.getId(), property);
         }
      }
      // Be sure type is set.
      PropertyDefinition<?> typeIdPropertyDefinition = type.getPropertyDefinition(CmisConstants.OBJECT_TYPE_ID);
      IdProperty typeIdProperty =
         new IdProperty(typeIdPropertyDefinition.getId(), typeIdPropertyDefinition.getQueryName(),
            typeIdPropertyDefinition.getLocalName(), typeIdPropertyDefinition.getDisplayName(), type.getId());
      properties.put(typeIdPropertyDefinition.getId(), typeIdProperty);

      return properties;
   }

   protected static String createRelationship(String typeId, String name, String sourceId, String targetId,
      List<AccessControlEntry> addACL, List<AccessControlEntry> removeACL, Collection<String> policies)
      throws Exception
   {
      String relationshipId = null;
      if (isRelationshipsSupported)
      {
         TypeDefinition type = connection.getTypeDefinition(typeId, true);
         Map<String, Property<?>> properties = createPropertyMap(type);
         StringProperty nameProperty = (StringProperty)properties.get(CmisConstants.NAME);
         if (nameProperty != null)
         {
            nameProperty.getValues().add(name);
         }
         IdProperty sourceIdProperty = (IdProperty)properties.get(CmisConstants.SOURCE_ID);
         if (sourceIdProperty != null)
         {
            sourceIdProperty.getValues().add(sourceId);
         }
         IdProperty targetIdProperty = (IdProperty)properties.get(CmisConstants.TARGET_ID);
         if (targetIdProperty != null)
         {
            targetIdProperty.getValues().add(targetId);
         }
         relationshipId = connection.createRelationship(properties, null, null, null);
      }
      return relationshipId;
   }

   protected static String generateName(TypeDefinition type, String suffix)
   {
      StringBuilder b = new StringBuilder();
      switch (type.getBaseId())
      {
         case DOCUMENT :
            b.append("document");
            break;
         case FOLDER :
            b.append("folder");
            break;
         case POLICY :
            b.append("policy");
            break;
         case RELATIONSHIP :
            b.append("relationship");
            break;
      }
      b.append('_').append(UUID.randomUUID().toString());
      if (suffix != null && suffix.length() > 0)
      {
         b.append('.').append(suffix);
      }
      return b.toString();
   }

   /**
    * Find first type which supports ACL.
    *
    * @param types tree of all available types
    * @return type which support ACL or <code>null</code> if there is no such
    *         type
    * @throws Exception if any error occurs
    */
   protected static TypeDefinition getControllableAclType(List<ItemsTree<TypeDefinition>> types) throws Exception
   {
      for (ItemsTree<TypeDefinition> item : types)
      {
         TypeDefinition container = item.getContainer();
         if (container.isControllableACL())
         {
            return container;
         }
         List<ItemsTree<TypeDefinition>> children = item.getChildren();
         if (children != null && !children.isEmpty())
         {
            return getControllableAclType(children);
         }
      }
      return null;
   }

   /**
    * Find first type which is controllable by policies.
    *
    * @param types tree of all available types
    * @return type which is controllable by policies or <code>null</code> if
    *         there is no such type
    * @throws Exception if any error occurs
    */
   protected static TypeDefinition getControllablePolicyType(List<ItemsTree<TypeDefinition>> types) throws Exception
   {
      if (isPoliciesSupported)
      {
         for (ItemsTree<TypeDefinition> item : types)
         {
            TypeDefinition container = item.getContainer();
            if (container.isControllablePolicy())
            {
               return container;
            }
            List<ItemsTree<TypeDefinition>> children = item.getChildren();
            if (children != null && !children.isEmpty())
            {
               return getControllablePolicyType(children);
            }
         }
      }
      return null;
   }

   /**
    * Find first type which does not support ACL.
    *
    * @param types tree of all available types
    * @return type which does not support ACL or <code>null</code> if there is
    *         no such type
    * @throws Exception if any error occurs
    */
   protected static TypeDefinition getNotControllableAclType(List<ItemsTree<TypeDefinition>> types) throws Exception
   {
      for (ItemsTree<TypeDefinition> item : types)
      {
         TypeDefinition container = item.getContainer();
         if (!container.isControllableACL())
         {
            return container;
         }
         List<ItemsTree<TypeDefinition>> children = item.getChildren();
         if (children != null && !children.isEmpty())
         {
            return getNotControllableAclType(children);
         }
      }
      return null;
   }

   /**
    * Find first type which is not controllable by policies.
    *
    * @param types tree of all available types
    * @return type which is not controllable by policies or <code>null</code> if
    *         there is no such type
    * @throws Exception if any error occurs
    */
   protected static TypeDefinition getNotControllablePolicyType(List<ItemsTree<TypeDefinition>> types) throws Exception
   {
      for (ItemsTree<TypeDefinition> item : types)
      {
         TypeDefinition container = item.getContainer();
         if (!container.isControllablePolicy())
         {
            return container;
         }
         List<ItemsTree<TypeDefinition>> children = item.getChildren();
         if (children != null && !children.isEmpty())
         {
            return getNotControllablePolicyType(children);
         }
      }
      return null;
   }

   /**
    * Find first document type which is not versionable.
    *
    * @param types tree of all available types
    * @return type which is not versionable or <code>null</code> if there is no
    *         such type
    * @throws Exception if any error occurs
    */
   protected static TypeDefinition getNotVersionableDocType(List<ItemsTree<TypeDefinition>> types) throws Exception
   {
      for (ItemsTree<TypeDefinition> item : types)
      {
         TypeDefinition container = item.getContainer();
         if (container.getBaseId() == BaseType.DOCUMENT)
         {
            if (!container.isVersionable())
            {
               return container;
            }
            List<ItemsTree<TypeDefinition>> children = item.getChildren();
            if (children != null && !children.isEmpty())
            {
               return getNotVersionableDocType(children);
            }
         }
      }
      return null;
   }

   /**
    * Find first document type which does not support content stream (
    * {@link TypeDefinition#getContentStreamAllowed()} is NOT_ALLOWED).
    *
    * @param types tree of all available types
    * @return type which does not support content stream or <code>null</code> if
    *         there is no such type
    * @throws Exception if any error occurs
    */
   protected static TypeDefinition getStreamNotSupportedDocType(List<ItemsTree<TypeDefinition>> types) throws Exception
   {
      for (ItemsTree<TypeDefinition> item : types)
      {
         TypeDefinition container = item.getContainer();
         if (container.getBaseId() == BaseType.DOCUMENT)
         {
            if (container.getContentStreamAllowed() == ContentStreamAllowed.NOT_ALLOWED)
            {
               return container;
            }
            List<ItemsTree<TypeDefinition>> children = item.getChildren();
            if (children != null && !children.isEmpty())
            {
               return getStreamNotSupportedDocType(children);
            }
         }
      }
      return null;
   }

   /**
    * Find first document type which required content stream (
    * {@link TypeDefinition#getContentStreamAllowed()} is REQUIRED).
    *
    * @param types tree of all available types
    * @return type which require content stream or <code>null</code> if there is
    *         no such type
    * @throws Exception if any error occurs
    */
   protected static TypeDefinition getStreamRequiredDocType(List<ItemsTree<TypeDefinition>> types) throws Exception
   {
      for (ItemsTree<TypeDefinition> item : types)
      {
         TypeDefinition container = item.getContainer();
         if (container.getBaseId() == BaseType.DOCUMENT)
         {
            if (container.getContentStreamAllowed() == ContentStreamAllowed.REQUIRED)
            {
               return container;
            }
            List<ItemsTree<TypeDefinition>> children = item.getChildren();
            if (children != null && !children.isEmpty())
            {
               return getStreamRequiredDocType(children);
            }
         }
      }
      return null;
   }

   /**
    * Find first document type which is versionable.
    *
    * @param types tree of all available types
    * @return type which is versionable or <code>null</code> if there is no such
    *         type
    * @throws Exception if any error occurs
    */
   protected static TypeDefinition getVersionableDocType(List<ItemsTree<TypeDefinition>> types) throws Exception
   {
      for (ItemsTree<TypeDefinition> item : types)
      {
         TypeDefinition container = item.getContainer();
         if (container.getBaseId() == BaseType.DOCUMENT)
         {
            if (container.isVersionable())
            {
               return container;
            }
            List<ItemsTree<TypeDefinition>> children = item.getChildren();
            if (children != null && !children.isEmpty())
            {
               return getVersionableDocType(children);
            }
         }
      }
      return null;
   }

   protected static void removeRelationships(String testroot)
   {
      try
      {
         List<ItemsTree<CmisObject>> descendants =
            connection.getDescendants(testroot, -1, false, IncludeRelationships.BOTH, false, true, PropertyFilter.ALL,
               RenditionFilter.NONE);
         for (ItemsTree<CmisObject> tr : descendants)
         {
            for (CmisObject relationship : tr.getContainer().getRelationship())
            {
               try
               {
                  connection.deleteObject(relationship.getObjectInfo().getId(), null);
               }
               catch (ObjectNotFoundException e)
               {
               }
            }
            List<ItemsTree<CmisObject>> children = tr.getChildren();
            if (children != null && children.size() > 0)
            {
               removeRelationships(tr.getContainer().getObjectInfo().getId());
            }
         }
      }
      catch (Exception e)
      {
         e.printStackTrace();
      }
   }

   /**
    * Check that two ACL are matched. It minds <code>actual</code> contains at
    * least all ACEs from <code>expected</code> but may have other ACEs.
    *
    * @param expected expected ACEs
    * @param actual actual ACEs
    */
   protected void checkACL(List<AccessControlEntry> expected, List<AccessControlEntry> actual)
   {
      Map<String, Set<String>> m1 = new HashMap<String, Set<String>>();
      CmisUtils.addAclToPermissionMap(m1, expected);
      Map<String, Set<String>> m2 = new HashMap<String, Set<String>>();
      CmisUtils.addAclToPermissionMap(m2, actual);
      for (Map.Entry<String, Set<String>> e : m1.entrySet())
      {
         String principal = e.getKey();
         Set<String> permissions2 = m2.get(principal);
         if (permissions2 == null)
         {
            fail("ACLs are not matched.");
         }
         if (!actual.contains("cmis:all"))
         {
            for (String permission : e.getValue())
            {
               assertTrue("ACLs are not matched. Permission " + permission + " for principal " + principal
                  + " not found", permissions2.contains(permission));
            }
         }
      }
   }

   protected List<CmisObject> objectTreeAsList(List<ItemsTree<CmisObject>> source)
   {
      List<CmisObject> result = new ArrayList<CmisObject>();
      for (ItemsTree<CmisObject> one : source)
      {
         CmisObject o = one.getContainer();
         if (one.getChildren() != null)
         {
            result.addAll(objectTreeAsList(one.getChildren()));
         }
         result.add(o);
      }
      return result;
   }

   protected List<TypeDefinition> typeTreeAsList(List<ItemsTree<TypeDefinition>> source)
   {
      List<TypeDefinition> result = new ArrayList<TypeDefinition>();
      for (ItemsTree<TypeDefinition> one : source)
      {
         TypeDefinition type = one.getContainer();
         if (one.getChildren() != null)
         {
            result.addAll(typeTreeAsList(one.getChildren()));
         }
         result.add(type);
      }
      return result;
   }

   /**
    * Validate that ACL contains only valid permissions.
    *
    * @param actual actual ACEs
    */
   protected void validateACL(List<AccessControlEntry> actual)
   {
      Map<String, Set<String>> m1 = new HashMap<String, Set<String>>();
      CmisUtils.addAclToPermissionMap(m1, actual);
      Set<String> allowed = new HashSet<String>();
      for (Permission p : aclCapability.getPermissions())
      {
         allowed.add(p.getPermission());
      }
      for (Map.Entry<String, Set<String>> e : m1.entrySet())
      {
         for (String permission : e.getValue())
         {
            assertTrue("ACLs contains unknown permission: " + permission, allowed.contains(permission));
         }
      }
   }
}
