/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.xcmis.search.query.content;

import static org.hamcrest.core.Is.is;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.mock;

import org.apache.commons.io.FileUtils;
import org.apache.commons.lang.NotImplementedException;
import org.junit.After;
import org.junit.Before;
import org.xcmis.search.SearchService;
import org.xcmis.search.config.IndexConfiguration;
import org.xcmis.search.config.SearchServiceConfiguration;
import org.xcmis.search.content.ContentEntry;
import org.xcmis.search.content.InMemorySchema;
import org.xcmis.search.content.IndexModificationException;
import org.xcmis.search.content.Property;
import org.xcmis.search.content.Schema;
import org.xcmis.search.content.InMemorySchema.Builder;
import org.xcmis.search.content.Property.SimpleValue;
import org.xcmis.search.content.interceptors.ContentReaderInterceptor;
import org.xcmis.search.lucene.content.SchemaTableResolver;
import org.xcmis.search.model.Query;
import org.xcmis.search.model.constraint.Operator;
import org.xcmis.search.query.QueryBuilder;
import org.xcmis.search.result.ScoredRow;
import org.xcmis.search.value.CastSystem;
import org.xcmis.search.value.NameConverter;
import org.xcmis.search.value.PropertyType;
import org.xcmis.search.value.ToStringNameConverter;
import org.xcmis.spi.utils.Logger;

import java.io.File;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;

/**
 * Abstract base class for query test cases.
 */
public abstract class AbstractQueryTest
{
   /** Logger. */
   private static final Logger LOG = Logger.getLogger(AbstractQueryTest.class);

   /**
    * Resolved Name for jcr:score
    */
   protected String jcrScore;

   /**
    * Resolved Name for jcr:path
    */
   protected String jcrPath;

   /**
    * Resolved Name for jcr:root
    */
   protected String jcrRoot;

   /**
    * Resolved Name for jcr:contains
    */
   protected String jcrContains;

   /**
    * Resolved Name for jcr:deref
    */
   protected String jcrDeref;

   /**
    * The string /${jcrRoot}${testRoot} with all components of the test path
    * properly escaped for XPath.
    * 
    * @see <a href="https://issues.apache.org/jira/browse/JCR-714">JCR-714</a>
    */
   protected String xpathRoot;

   /**
    * The query object model factory for {@link #superuser}.
    */
   protected QueryBuilder qf;

   /**
    * The query manager for {@link #superuser}
    */
   protected SearchService qm;

   protected Node testRootNode;

   protected String rootNodeType = "rootNodeType";

   protected String testNodeType = "testNodeType";

   protected String testNodeType2 = "testNodeType2";

   protected String nodeName1 = "nodeName1";

   protected String nodeName2 = "nodeName2";

   protected String nodeName3 = "nodeName3";

   protected String propertyName1 = "propertyName1";

   protected String propertyName2 = "propertyName2";

   protected String propertyName4 = "propertyName4";

   private File tempDir;

   private Schema schema;

   protected SearchService searchService;

   private String testRoot;

   /**
    * Set-up the configuration values used for the test. Per default retrieves a
    * session, configures testRoot, and nodetype and checks if the query
    * language for the current language is available.<br>
    */
   @Before
   public void setUp() throws Exception
   {

      tempDir = new File(System.getProperty("java.io.tmpdir"), "search-service");
      if (tempDir.exists())
      {
         assertThat(FileUtils.deleteQuietly(tempDir), is(true));
      }
      assertThat(tempDir.mkdirs(), is(true));

      Builder schemaBuilder = InMemorySchema.createBuilder();
      schema =
         schemaBuilder.addTable(rootNodeType, propertyName1, propertyName2).addTable(testNodeType,
            new String[]{propertyName4})
            .addColumn(testNodeType, propertyName1, PropertyType.STRING, true, Operator.ALL).addColumn(testNodeType,
               propertyName2, PropertyType.STRING, true, Operator.ALL).addTable(testNodeType2, propertyName4).build();
      qf = new QueryBuilder(mock(CastSystem.class));

      testRootNode =
         new Node(null, "", new String[]{rootNodeType}, UUID.randomUUID().toString(),
            new String[]{Node.ROOT_PARENT_UUID}, new Property[0]);

      //testRoot = testRootNode.getPath();

      //value
      NameConverter<String> nameConverter = new ToStringNameConverter();
      SchemaTableResolver tableResolver = new SchemaTableResolver(nameConverter, schema);

      //index configuration
      IndexConfiguration indexConfuration =
         new IndexConfiguration(testRootNode.getParentIdentifiers()[0], testRootNode.getIdentifier());

      //search service configuration
      SearchServiceConfiguration configuration =
         new SearchServiceConfiguration(schema, tableResolver, mock(ContentReaderInterceptor.class), indexConfuration);
      searchService = new SearchService(configuration);
      searchService.start();

      List<ContentEntry> contentEntries = new ArrayList<ContentEntry>();
      contentEntries.add(testRootNode);
      searchService.update(contentEntries, Collections.EMPTY_SET);
   }

   @After
   public void tearDown() throws Exception
   {
      Set<String> removed = new HashSet<String>();
      //removed.add(testRootNode.getIdentifier());

      for (ContentEntry entry : testRootNode.getTree())
      {
         removed.add(entry.getIdentifier());
      }
      searchService.update(Collections.EMPTY_LIST, removed);

      qm = null;
      qf = null;

   }

   /**
    * Create a {@link Query} for a given {@link Statement}.
    * 
    * @param statement
    *           the query should be created for
    * @return
    * 
    * @throws RepositoryException
    * @see #createQuery(String, String)
    */
   protected Query createQuery(String statement)
   {
      throw new NotImplementedException();
   }

   /**
    * Creates a {@link Query} for the given statement in the requested language,
    * treating optional languages gracefully
    * 
    * @throws RepositoryException
    */
   protected Query createQuery(String statement, String language)
   {
      throw new NotImplementedException();
   }

   /**
    * Creates and executes a {@link Query} for the given {@link Statement}
    * 
    * @param statement
    *           to execute
    * @return
    * 
    * @throws RepositoryException
    * @see #execute(String, String)
    */
   protected void execute(String statement)
   {
      throw new NotImplementedException();
   }

   /**
    * Creates and executes a {@link Query} for a given Statement in a given
    * query language
    * 
    * @param statement
    *           the query should be build for
    * @param language
    *           query language the stement is written in
    * @return
    * 
    * @throws RepositoryException
    */
   protected void execute(String statement, String language)
   {
      throw new NotImplementedException();
   }

   /**
    * Checks if the <code>result</code> contains a number of <code>hits</code>.
    * 
    * @param result
    *           the <code>QueryResult</code>.
    * @param hits
    *           the number of expected hits.
    * @throws RepositoryException
    *            if an error occurs while iterating over the result nodes.
    */
   protected void checkResult(List<ScoredRow> result, int hits)
   {

      long count = result.size();
      if (count == 0)
      {
         LOG.info(" NONE");
      }
      else if (count == -1)
      {
         // have to count in a loop
         count = 0;
         for (ScoredRow scoredRow : result)
         {
            count++;
         }

      }
      assertEquals("Wrong hit count.", hits, count);
   }

   /**
    * Checks if the <code>result</code> contains a number of <code>hits</code>
    * and <code>properties</code>.
    * 
    * @param result
    *           the <code>QueryResult</code>.
    * @param hits
    *           the number of expected hits.
    * @param properties
    *           the number of expected properties.
    * @throws RepositoryException
    *            if an error occurs while iterating over the result nodes.
    */
   protected void checkResult(List<ScoredRow> result, int hits, int properties)
   {
      //      checkResult(result, hits);
      //      // now check property count
      //      int count = 0;
      //      LOG.info("Properties:");
      //      String[] propNames = result.getColumnNames();
      //      for (RowIterator it = result.getRows(); it.hasNext();)
      //      {
      //         StringBuffer msg = new StringBuffer();
      //         Value[] values = it.nextRow().getValues();
      //         for (int i = 0; i < propNames.length; i++, count++)
      //         {
      //            msg.append("  ").append(propNames[i]).append(": ");
      //            if (values[i] == null)
      //            {
      //               msg.append("null");
      //            }
      //            else
      //            {
      //               msg.append(values[i].getString());
      //            }
      //         }
      //         LOG.info(msg);
      //      }
      //      if (count == 0)
      //      {
      //         LOG.info("  NONE");
      //      }
      //      assertEquals("Wrong property count.", properties, count);
      throw new NotImplementedException();
   }

   /**
    * Checks if the {@link QueryResult} is ordered according order property in
    * direction of related argument.
    * 
    * @param queryResult
    *           to be tested
    * @param propName
    *           Name of the porperty to order by
    * @param descending
    *           if <code>true</code> order has to be descending
    * @throws RepositoryException
    * @throws NotExecutableException
    *            in case of less than two results or all results have same size
    *            of value in its order-property
    */
   protected void evaluateResultOrder(List<ScoredRow> result, String propName, boolean descending)
   {
      //      NodeIterator nodes = queryResult.getNodes();
      //      if (getSize(nodes) < 2)
      //      {
      //         fail("Workspace does not contain sufficient content to test ordering on result nodes.");
      //      }
      //      // need to re-aquire nodes, {@link #getSize} may consume elements.
      //      nodes = queryResult.getNodes();
      //      int changeCnt = 0;
      //      String last = descending ? "\uFFFF" : "";
      //      while (nodes.hasNext())
      //      {
      //         String value = nodes.nextNode().getProperty(propName).getString();
      //         int cp = value.compareTo(last);
      //         // if value changed evaluate if the ordering is correct
      //         if (cp != 0)
      //         {
      //            changeCnt++;
      //            if (cp > 0 && descending)
      //            {
      //               fail("Repository doesn't order properly descending");
      //            }
      //            else if (cp < 0 && !descending)
      //            {
      //               fail("Repository doesn't order properly ascending");
      //            }
      //         }
      //         last = value;
      //      }
      //      if (changeCnt < 1)
      //      {
      //         fail("Workspace does not contain distinct values for " + propName);
      //      }
      throw new NotImplementedException();
   }

   /**
    * Executes the <code>sql</code> query and checks the results against the
    * specified <code>nodes</code>.
    * 
    * @param session
    *           the session to use for the query.
    * @param sql
    *           the sql query.
    * @param nodes
    *           the expected result nodes.
    * @throws NotExecutableException
    */
   protected void executeSqlQuery(String sql, String[] nodes)
   {
      throw new NotImplementedException();
   }

   protected void save(Node node) throws IndexModificationException
   {
      searchService.update(node.getTree(), Collections.EMPTY_SET);
   }

   /**
    * Checks if the result set contains exactly the <code>nodes</code>.
    * 
    * @param result
    *           the query result.
    * @param nodes
    *           the expected nodes in the result set.
    */
   protected void checkResult(List<ScoredRow> result, String selectorName, Node[] nodes)
   {
      // collect paths
      Set<String> expectedPaths = new HashSet<String>();
      for (Node node : nodes)
      {
         expectedPaths.add(node.getIdentifier());
      }
      Set<String> resultPaths = new HashSet<String>();
      for (ScoredRow object : result)
      {

         resultPaths.add(object.getNodeIdentifer(selectorName));
      }
      // check if all expected are in result
      for (Object element : expectedPaths)
      {
         String path = (String)element;
         assertTrue(path + " is not part of the result set", resultPaths.contains(path));
      }
      // check result does not contain more than expected
      for (Object element : resultPaths)
      {
         String path = (String)element;
         assertTrue(path + " is not expected to be part of the result set", expectedPaths.contains(path));
      }
      //throw new NotImplementedException();
   }

   //   /**
   //    * Returns the nodes in <code>it</code> as an array of Nodes.
   //    * @param it the NodeIterator.
   //    * @return the elements of the iterator as an array of Nodes.
   //    */
   //   protected String[] toArray(NodeIterator it)
   //   {
   //      List nodes = new ArrayList();
   //      while (it.hasNext())
   //      {
   //         nodes.add(it.nextNode());
   //      }
   //      return (Node[])nodes.toArray(new Node[nodes.size()]);
   //   }

   /**
    * Escape an identifier suitable for the SQL parser
    * 
    * @TODO currently only handles dash character
    */
   protected String escapeIdentifierForSQL(String identifier)
   {

      boolean needsEscaping = identifier.indexOf('-') >= 0;

      if (!needsEscaping)
      {
         return identifier;
      }
      else
      {
         return '"' + identifier + '"';
      }
   }

   //   /**
   //    * @param language a query language.
   //    * @return <code>true</code> if <code>language</code> is supported;
   //    *         <code>false</code> otherwise.
   //    * @throws RepositoryException if an error occurs.
   //    */
   //   protected boolean isSupportedLanguage(String language) throws RepositoryException
   //   {
   //      return Arrays.asList(qm.getSupportedQueryLanguages()).contains(language);
   //   }

   public static class Node extends ContentEntry
   {
      public static final String ROOT_PARENT_UUID = "";

      private final List<Node> childNodes;

      private final Map<String, Property<?>> properties;

      private final Node parentNode;

      /**
       * @param name
       * @param tableNames
       * @param identifer
       * @param parentIdentifiers
       * @param properties
       */
      public Node(Node parentNode, String name, String[] tableNames, String identifer, String[] parentIdentifiers,
         Property<?>[] properties)
      {
         super(name, tableNames, identifer, parentIdentifiers, properties);
         this.parentNode = parentNode;
         this.childNodes = new ArrayList<Node>();
         this.properties = new HashMap<String, Property<?>>();
      }

      /**
       * @param nodeName1
       * @param testNodeType
       * @return
       */
      public Node addNode(String nodeName, String testNodeType)
      {

         Node child =
            new Node(this, nodeName, new String[]{testNodeType}, UUID.randomUUID().toString(),
               new String[]{getIdentifier()}, new Property[0]);
         childNodes.add(child);
         return child;
      }

      /**
       * @param propertyName1
       * @param string
       */
      public void setProperty(String propertyName1, String string)
      {
         properties.put(propertyName1, new Property<String>(PropertyType.STRING, propertyName1,
            new SimpleValue<String>(string)));
      }

      /**
       * @see org.xcmis.search.content.ContentEntry#getProperties()
       */
      @Override
      public Property<?>[] getProperties()
      {
         return properties.values().toArray(new Property[properties.size()]);
      }

      /**
       * @return
       */
      public String getPath()
      {
         if (parentNode == null)
         {
            return "/";
         }
         return parentNode.getPath() + "/" + getName();
      }

      public List<ContentEntry> getTree()
      {
         List<ContentEntry> nodes = new ArrayList<ContentEntry>();
         nodes.add(this);
         for (Node childNode : childNodes)
         {
            nodes.addAll(childNode.getTree());
         }
         return nodes;
      }
   }
}
