/*
 * 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.apache.jackrabbit.core.query;

import static javax.jcr.query.Query.JCR_SQL2;

import java.util.ArrayList;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.NoSuchElementException;
import java.util.Set;

import javax.jcr.Node;
import javax.jcr.NodeIterator;
import javax.jcr.RepositoryException;
import javax.jcr.Value;
import javax.jcr.query.Query;
import javax.jcr.query.QueryManager;
import javax.jcr.query.QueryResult;
import javax.jcr.query.Row;
import javax.jcr.query.RowIterator;
import javax.jcr.query.qom.QueryObjectModelFactory;

import org.apache.jackrabbit.commons.JcrUtils;
import org.apache.jackrabbit.commons.iterator.NodeIteratorAdapter;
import org.apache.jackrabbit.core.query.lucene.SearchIndex;
import org.apache.jackrabbit.test.AbstractJCRTest;

/**
 * Abstract base class for query test cases.
 */
public class AbstractQueryTest extends AbstractJCRTest {

    protected QueryManager qm;

    protected QueryObjectModelFactory qomFactory;

    protected void setUp() throws Exception {
        super.setUp();
        qm = superuser.getWorkspace().getQueryManager();
        qomFactory = qm.getQOMFactory();
    }

    protected void tearDown() throws Exception {
        qm = null;
        qomFactory = null;
        super.tearDown();
    }

    /**
     * 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(QueryResult result, int hits)
            throws RepositoryException {
        int count = 0;
        log.println("Rows:");
        for (Row row : JcrUtils.getRows(result)) {
            log.println(" " + row);
            count++;
        }
        if (count == 0) {
            log.println(" NONE");
        }
        assertEquals("Wrong hit count.", hits, count);
    }

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

    /**
     * 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 Node[] toArray(NodeIterator it) {
        List<Node> nodes = new ArrayList<Node>();
        while (it.hasNext()) {
            nodes.add(it.nextNode());
        }
        return nodes.toArray(new Node[nodes.size()]);
    }

    /**
     * Executes the <code>xpath</code> query and checks the results against
     * the specified <code>nodes</code>.
     * @param xpath the xpath query.
     * @param nodes the expected result nodes.
     * @throws RepositoryException if an error occurs while executing the query
     *                             or checking the result.
     */
    protected void executeXPathQuery(String xpath, Node[] nodes)
            throws RepositoryException {
        getSearchIndex().flush();
        QueryResult res = qm.createQuery(xpath, Query.XPATH).execute();
        checkResult(res, nodes);
    }

    /**
     * Executes the <code>sql</code> query and checks the results against
     * the specified <code>nodes</code>.
     * @param sql the sql query.
     * @param nodes the expected result nodes.
     * @throws RepositoryException if an error occurs while executing the query
     *                             or checking the result.
     */
    protected void executeSQLQuery(String sql, Node[] nodes)
            throws RepositoryException {
        getSearchIndex().flush();
        QueryResult res = qm.createQuery(sql, Query.SQL).execute();
        checkResult(res, nodes);
    }

    /**
     * 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.
     * @throws RepositoryException if an error occurs while reading from the result.
     */
    protected void checkResult(QueryResult result, Node[] nodes)
            throws RepositoryException {
        checkResult(result.getNodes(), nodes);
    }

    /**
     * Checks if the result contains exactly the <code>nodes</code>.
     * @param result the query result.
     * @param nodes the expected nodes in the result set.
     * @throws RepositoryException if an error occurs while reading from the result.
     */
    protected void checkResult(RowIterator result, Node[] nodes)
            throws RepositoryException {
        checkResult(new NodeIteratorAdapter(result) {
            public Object next() throws NoSuchElementException {
                Row next = (Row) super.next();
                try {
                    return superuser.getItem(next.getValue("jcr:path").getString());
                } catch (RepositoryException e) {
                    throw new NoSuchElementException();
                }
            }
        }, nodes);
    }

    /**
     * Checks if the result contains exactly the <code>nodes</code>.
     * @param result the query result.
     * @param nodes the expected nodes in the result set.
     * @throws RepositoryException if an error occurs while reading from the result.
     */
    protected void checkResult(NodeIterator result, Node[] nodes)
            throws RepositoryException {
        // collect paths
        Set<String> expectedPaths = new HashSet<String>();
        for (Node n : nodes) {
            expectedPaths.add(n.getPath());
        }
        Set<String> resultPaths = new HashSet<String>();
        while (result.hasNext()) {
            resultPaths.add(result.nextNode().getPath());
        }
        // check if all expected are in result
        for (Iterator<String> it = expectedPaths.iterator(); it.hasNext();) {
            String path = it.next();
            assertTrue(path + " is not part of the result set "+ expectedPaths, resultPaths.contains(path));
        }
        // check result does not contain more than expected
        for (Iterator<String> it = resultPaths.iterator(); it.hasNext();) {
            String path = it.next();
            assertTrue(path + " is not expected to be part of the result set " + expectedPaths, expectedPaths.contains(path));
        }
    }

    /**
     * Checks if the result set contains exactly the <code>nodes</code> in the
     * given sequence.
     *
     * @param result the query result.
     * @param nodes the expected nodes in the result set.
     * @throws RepositoryException if an error occurs while reading from the result.
     */
    protected void checkResultSequence(RowIterator result, Node[] nodes)
            throws RepositoryException {
        for (int i = 0; i < nodes.length; i++) {
            assertTrue("No more results, expected " + nodes[i].getPath(), result.hasNext());
            String path = result.nextRow().getValue("jcr:path").getString();
            assertEquals("Wrong sequence", nodes[i].getPath(), path);
        }
        assertFalse("No more result expected", result.hasNext());
    }

    /**
     * Executes the query specified by <code>statement</code> and returns the
     * query result.
     *
     * @param statement either a SQL or XPath statement.
     * @return the query result.
     * @throws RepositoryException if an error occurs.
     */
    protected QueryResult executeQuery(String statement)
            throws RepositoryException {
        if (statement.trim().toLowerCase().startsWith("select")) {
            return qm.createQuery(statement, Query.SQL).execute();
        } else {
            return qm.createQuery(statement, Query.XPATH).execute();
        }
    }

    protected QueryResult executeSQL2Query(String statement)
            throws RepositoryException {
        return qm.createQuery(statement, JCR_SQL2).execute();
    }

    protected void executeSQL2Query(String statement, Node[] nodes)
            throws RepositoryException {
        getSearchIndex().flush();
        QueryResult res = qm.createQuery(statement, JCR_SQL2).execute();
        checkResult(res, nodes);
    }

    /**
     * Returns a reference to the underlying search index.
     *
     * @return the query handler inside the {@link #qm query manager}.
     */
    protected SearchIndex getSearchIndex() {
        return (SearchIndex) ((QueryManagerImpl) qm).getQueryHandler();
    }
}
