/*
 * 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 java.io.InputStream;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Callable;

import javax.jcr.Node;
import javax.jcr.RepositoryException;
import javax.jcr.nodetype.NodeType;
import javax.jcr.query.InvalidQueryException;
import javax.jcr.query.Query;
import javax.jcr.query.QueryResult;
import javax.jcr.query.Row;

import org.apache.jackrabbit.commons.iterator.RowIterable;

/**
 * Performs tests with the <code>CONTAINS</code> function.
 */
public class FulltextQueryTest extends AbstractQueryTest {

    public void testFulltextSimpleSQL1() throws Exception {
        Node foo = testRootNode.addNode("foo");
        foo.setProperty("mytext", new String[]{"the quick brown fox jumps over the lazy dog."});

        testRootNode.save();

        String sql = "SELECT * FROM nt:unstructured"
                + " WHERE jcr:path LIKE '" + testRoot + "/%"
                + "' AND CONTAINS(., 'fox')";
        Query q = superuser.getWorkspace().getQueryManager().createQuery(sql, Query.SQL);
        QueryResult result = q.execute();
        checkResult(result, 1);
    }

    public void testFulltextSimpleSQL2() throws Exception {
        Node foo = testRootNode.addNode("foo");
        foo.setProperty("mytext", new String[]{"the quick brown fox jumps over the lazy dog."});

        testRootNode.save();

        String sql = "SELECT * FROM nt:unstructured"
                + " WHERE \"jcr:path\" = '" + testRoot + "/foo"
                + "' AND CONTAINS(., 'fox')";
        Query q = superuser.getWorkspace().getQueryManager().createQuery(sql, Query.SQL);
        QueryResult result = q.execute();
        checkResult(result, 1);
    }

    public void testFulltextMultiWordSQL() throws Exception {
        Node n = testRootNode.addNode("node1");
        n.setProperty("title", new String[]{"test text"});
        n.setProperty("mytext", new String[]{"the quick brown fox jumps over the lazy dog."});

        n = testRootNode.addNode("node2");
        n.setProperty("title", new String[]{"other text"});
        n.setProperty("mytext", new String[]{"the quick brown fox jumps over the lazy dog."});

        testRootNode.save();

        String sql = "SELECT * FROM nt:unstructured"
                + " WHERE \"jcr:path\" LIKE '" + testRoot + "/%"
                + "' AND CONTAINS(., 'fox test')";
        Query q = superuser.getWorkspace().getQueryManager().createQuery(sql, Query.SQL);
        QueryResult result = q.execute();
        checkResult(result, 1);
    }

    public void testFulltextPhraseSQL() throws Exception {
        Node n = testRootNode.addNode("node1");
        n.setProperty("title", new String[]{"test text"});
        n.setProperty("mytext", new String[]{"the quick brown jumps fox over the lazy dog."});

        n = testRootNode.addNode("node2");
        n.setProperty("title", new String[]{"other text"});
        n.setProperty("mytext", new String[]{"the quick brown fox jumps over the lazy dog."});

        testRootNode.save();

        String sql = "SELECT * FROM nt:unstructured"
                + " WHERE \"jcr:path\" LIKE '" + testRoot + "/%"
                + "' AND CONTAINS(., 'text \"fox jumps\"')";
        Query q = superuser.getWorkspace().getQueryManager().createQuery(sql, Query.SQL);
        QueryResult result = q.execute();
        checkResult(result, 1);
    }

    public void testFulltextExcludeSQL() throws Exception {
        Node n = testRootNode.addNode("node1");
        n.setProperty("title", new String[]{"test text"});
        n.setProperty("mytext", new String[]{"the quick brown fox jumps over the lazy dog."});

        n = testRootNode.addNode("node2");
        n.setProperty("title", new String[]{"other text"});
        n.setProperty("mytext", new String[]{"the quick brown fox jumps over the lazy dog."});

        superuser.getRootNode().save();

        String sql = "SELECT * FROM nt:unstructured"
                + " WHERE \"jcr:path\" LIKE '" + testRoot + "/%"
                + "' AND CONTAINS(., 'text ''fox jumps'' -other')";
        Query q = superuser.getWorkspace().getQueryManager().createQuery(sql, Query.SQL);
        QueryResult result = q.execute();
        checkResult(result, 1);
    }

    public void testFulltextOrSQL() throws Exception {
        Node n = testRootNode.addNode("node1");
        n.setProperty("title", new String[]{"test text"});
        n.setProperty("mytext", new String[]{"the quick brown fox jumps over the lazy dog."});

        n = testRootNode.addNode("node2");
        n.setProperty("title", new String[]{"other text"});
        n.setProperty("mytext", new String[]{"the quick brown fox jumps over the lazy dog."});

        testRootNode.save();

        String sql = "SELECT * FROM nt:unstructured"
                + " WHERE \"jcr:path\" LIKE '" + testRoot + "/%"
                + "' AND CONTAINS(., '''fox jumps'' test OR other')";
        Query q = superuser.getWorkspace().getQueryManager().createQuery(sql, Query.SQL);
        QueryResult result = q.execute();
        checkResult(result, 2);
    }

    public void testFulltextIntercapSQL() throws Exception {
        Node n = testRootNode.addNode("node1");
        n.setProperty("title", new String[]{"tEst text"});
        n.setProperty("mytext", new String[]{"The quick brown Fox jumps over the lazy dog."});

        n = testRootNode.addNode("node2");
        n.setProperty("title", new String[]{"Other text"});
        n.setProperty("mytext", new String[]{"the quick brown FOX jumPs over the lazy dog."});

        testRootNode.save();

        String sql = "SELECT * FROM nt:unstructured"
                + " WHERE \"jcr:path\" LIKE '" + testRoot + "/%"
                + "' AND CONTAINS(., '''fox juMps'' Test OR otheR')";
        Query q = superuser.getWorkspace().getQueryManager().createQuery(sql, Query.SQL);
        QueryResult result = q.execute();
        checkResult(result, 2);
    }

    public void testContainsStarSQL() throws RepositoryException {
        Node n = testRootNode.addNode("node1");
        n.setProperty("title", new String[]{"tEst text"});
        n.setProperty("mytext", new String[]{"The quick brown Fox jumps over the lazy dog."});

        n = testRootNode.addNode("node2");
        n.setProperty("title", new String[]{"The quick brown Fox jumps over the lazy dog."});
        n.setProperty("mytext", new String[]{"text text"});

        testRootNode.save();

        String sql = "SELECT * FROM nt:unstructured"
                + " WHERE jcr:path LIKE '" + testRoot + "/%"
                + "' AND CONTAINS(., 'fox jumps')";
        Query q = superuser.getWorkspace().getQueryManager().createQuery(sql, Query.SQL);
        checkResult(q.execute(), 2);
    }

    public void testContainsStarXPath() throws RepositoryException {
        Node n = testRootNode.addNode("node1");
        n.setProperty("title", new String[]{"tEst text"});
        n.setProperty("mytext", new String[]{"The quick brown Fox jumps over the lazy dog."});

        n = testRootNode.addNode("node2");
        n.setProperty("title", new String[]{"The quick brown Fox jumps over the lazy dog."});
        n.setProperty("mytext", new String[]{"text text"});

        testRootNode.save();

        String sql = "/jcr:root" + testRoot + "/element(*, nt:unstructured)"
                + "[jcr:contains(., 'quick fox')]";
        Query q = superuser.getWorkspace().getQueryManager().createQuery(sql, Query.XPATH);
        checkResult(q.execute(), 2);
    }

    public void testContainsPropScopeSQL() throws RepositoryException {
        Node n = testRootNode.addNode("node1");
        n.setProperty("title", new String[]{"tEst text"});
        n.setProperty("mytext", new String[]{"The quick brown Fox jumps over the lazy dog."});

        n = testRootNode.addNode("node2");
        n.setProperty("title", new String[]{"The quick brown Fox jumps over the lazy dog."});
        n.setProperty("mytext", new String[]{"text text"});

        testRootNode.save();

        String sql = "SELECT * FROM nt:unstructured"
                + " WHERE jcr:path LIKE '" + testRoot + "/%"
                + "' AND CONTAINS(title, 'fox jumps')";
        Query q = superuser.getWorkspace().getQueryManager().createQuery(sql, Query.SQL);
        checkResult(q.execute(), 1);
    }

    public void testContainsPropScopeXPath() throws RepositoryException {
        Node n = testRootNode.addNode("node1");
        n.setProperty("title", new String[]{"tEst text"});
        n.setProperty("mytext", new String[]{"The quick brown Fox jumps over the lazy dog."});

        n = testRootNode.addNode("node2");
        n.setProperty("title", new String[]{"The quick brown Fox jumps over the lazy dog."});
        n.setProperty("mytext", new String[]{"text text"});

        testRootNode.save();

        String sql = "/jcr:root" + testRoot + "/element(*, nt:unstructured)"
                + "[jcr:contains(@title, 'quick fox')]";
        Query q = superuser.getWorkspace().getQueryManager().createQuery(sql, Query.XPATH);
        checkResult(q.execute(), 1);
    }

    public void testWildcard() throws RepositoryException {
        String content = "The quick brown Fox jumps over the lazy dog.";

        // single * wildcard
        executeContainsQuery("qu*", content, true);
        executeContainsQuery("qu*ck", content, true);
        executeContainsQuery("quick*", content, true);
        executeContainsQuery("*quick", content, true);
        executeContainsQuery("qu*Ck", content, true);

        // multiple * wildcard
        executeContainsQuery("*o*", content, true);
        executeContainsQuery("*ump*", content, true);
        executeContainsQuery("qu**ck", content, true);
        executeContainsQuery("q***u**c*k", content, true);
        executeContainsQuery("*uMp*", content, true);

        // single ? wildcard
        executeContainsQuery("quic?", content, true);
        executeContainsQuery("?uick", content, true);
        executeContainsQuery("qu?ck", content, true);
        executeContainsQuery("qu?cK", content, true);

        // multiple ? wildcard
        executeContainsQuery("q??ck", content, true);
        executeContainsQuery("?uic?", content, true);
        executeContainsQuery("??iCk", content, true);

        // no matches
        executeContainsQuery("*ab*", content, false);
        executeContainsQuery("q***j**c*k", content, false);

    }

    public void testMultiByte() throws RepositoryException {
        String content = "some text with multi byte \u7530\u4e2d characters.";

        executeContainsQuery("\u7530\u4e2d*", content, true);
    }

    public void testPredefinedEntityReference() throws RepositoryException {
        String content = "Max&Moritz";

        executeContainsQuery("max&moritz", content, true);
    }

    public void testColonInContains() throws RepositoryException {
        executeContainsQuery("foo:bar", "foo:bar", true);
    }

    public void testMultipleOrExpressions() throws RepositoryException {
        Node n = testRootNode.addNode("node1");
        n.setProperty("prop1", "foo");
        n.setProperty("prop2", "bar");
        n.setProperty("prop3", "baz");

        n = testRootNode.addNode("node2");
        n.setProperty("prop1", "bar");
        n.setProperty("prop2", "foo");
        n.setProperty("prop3", "baz");

        n = testRootNode.addNode("node3");
        n.setProperty("prop1", "bar");
        n.setProperty("prop2", "baz");
        n.setProperty("prop3", "foo");

        superuser.save();

        List<String> r1 = new ArrayList<String>();
        QueryResult result = qm.createQuery(testPath + "/*[jcr:contains(@prop1, 'foo') or jcr:contains(@prop2, 'foo') or jcr:contains(@prop3, 'foo')] order by @jcr:score descending", Query.XPATH).execute();
        for (Row r : new RowIterable(result.getRows())) {
            r1.add(r.getPath() + ":" + (int) (r.getScore() * 1000));
        }

        List<String> r2 = new ArrayList<String>();
        result = qm.createQuery(testPath + "/*[jcr:contains(@prop3, 'foo') or jcr:contains(@prop1, 'foo') or jcr:contains(@prop2, 'foo')] order by @jcr:score descending", Query.XPATH).execute();
        for (Row r : new RowIterable(result.getRows())) {
            r2.add(r.getPath() + ":" + (int) (r.getScore() * 1000));
        }

        List<String> r3 = new ArrayList<String>();
        result = qm.createQuery(testPath + "/*[jcr:contains(@prop2, 'foo') or jcr:contains(@prop3, 'foo') or jcr:contains(@prop1, 'foo')] order by @jcr:score descending", Query.XPATH).execute();
        for (Row r : new RowIterable(result.getRows())) {
            r3.add(r.getPath() + ":" + (int) (r.getScore() * 1000));
        }

        assertEquals(r1, r2);
        assertEquals(r1, r3);
    }

    /**
     * Executes a query and checks if the query matched the test node.
     *
     * @param statement the query statement.
     * @param content   the content for the test node.
     * @param match     if the query matches the node.
     * @throws RepositoryException if an error occurs.
     */
    private void executeContainsQuery(String statement,
                              String content,
                              boolean match) throws RepositoryException {
        while (testRootNode.hasNode(nodeName1)) {
            testRootNode.getNode(nodeName1).remove();
        }
        testRootNode.addNode(nodeName1).setProperty("text", content);
        testRootNode.save();

        assertContainsQuery(statement, match);
    }

    public void testFileContains() throws Exception {
        assertFileContains(
                "test.txt", "text/plain", "AE502DBEA2C411DEBD340AD156D89593");
        assertFileContains(
                "test.rtf", "text/rtf", "quick brown fox");
    }

    private void assertFileContains(
            String name, String type, String... statements) throws Exception {
        while (testRootNode.hasNode(nodeName1)) {
            testRootNode.getNode(nodeName1).remove();
        }
        Node resource = testRootNode.addNode(nodeName1, NodeType.NT_RESOURCE);
        resource.setProperty("jcr:mimeType", type);
        InputStream stream = FulltextQueryTest.class.getResourceAsStream(name);
        try {
            resource.setProperty("jcr:data", stream);
        } finally {
            stream.close();
        }
        testRootNode.save();
        getSearchIndex().flush();

        for (String statement : statements) {
            assertContainsQuery(statement, true);
        }
    }

    private void assertContainsQuery(String statement, boolean match)
            throws InvalidQueryException, RepositoryException {
        StringBuffer stmt = new StringBuffer();
        stmt.append("/jcr:root").append(testRoot).append("/*");
        stmt.append("[jcr:contains(., '").append(statement);
        stmt.append("')]");

        Query q = superuser.getWorkspace().getQueryManager().createQuery(stmt.toString(), Query.XPATH);
        checkResult(q.execute(), match ? 1 : 0);

        stmt = new StringBuffer();
        stmt.append("SELECT * FROM nt:base ");
        stmt.append("WHERE jcr:path LIKE '").append(testRoot).append("/%' ");
        stmt.append("AND CONTAINS(., '").append(statement).append("')");

        q = superuser.getWorkspace().getQueryManager().createQuery(stmt.toString(), Query.SQL);
        checkResult(q.execute(), match ? 1 : 0);
    }

}
