/*
 * 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.oak.query.index;

import static org.apache.jackrabbit.guava.common.base.Preconditions.checkNotNull;
import static org.apache.jackrabbit.guava.common.collect.ImmutableList.of;
import static org.apache.jackrabbit.JcrConstants.JCR_PRIMARYTYPE;
import static org.apache.jackrabbit.JcrConstants.NT_UNSTRUCTURED;
import static org.apache.jackrabbit.oak.api.Type.LONG;
import static org.apache.jackrabbit.oak.api.Type.NAME;

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

import org.apache.jackrabbit.oak.Oak;
import org.apache.jackrabbit.oak.api.CommitFailedException;
import org.apache.jackrabbit.oak.api.ContentRepository;
import org.apache.jackrabbit.oak.api.Tree;
import org.apache.jackrabbit.oak.api.Type;
import org.apache.jackrabbit.oak.InitialContent;
import org.apache.jackrabbit.oak.query.AbstractQueryTest;
import org.apache.jackrabbit.oak.spi.security.OpenSecurityProvider;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.junit.Ignore;
import org.junit.Test;

import org.apache.jackrabbit.guava.common.collect.ImmutableList;

/**
 * Tests the query engine using the default index implementation: the
 * {@link TraversingIndex}
 */
public class TraversingIndexQueryTest extends AbstractQueryTest {

    @Override
    protected ContentRepository createRepository() {
        return new Oak()
            .with(new OpenSecurityProvider())
            .with(new InitialContent())
            .createContentRepository();
    }

    @Test
    public void sql1() throws Exception {
        test("sql1.txt");
    }

    @Test
    public void sql2() throws Exception {
        test("sql2.txt");
    }

    @Test
    @Ignore("OAK-2050")
    public void testFullTextTerm() throws Exception {
        //OAK-1024 allow '/' in a full-text query 
        Tree node = root.getTree("/").addChild("content");
        node.setProperty("jcr:mimeType", "text/plain");
        root.commit();
        assertQuery("//*[jcr:contains(., 'text/plain')]", "xpath",
                ImmutableList.of("/content"));
    }

    @Test
    @Ignore("OAK-2050")
    public void testFullTextTermName() throws Exception {
        Tree c = root.getTree("/").addChild("content");
        c.addChild("testFullTextTermNameSimple");
        c.addChild("testFullTextTermNameFile.txt");
        root.commit();
        assertQuery("//*[jcr:contains(., 'testFullTextTermNameSimple')]",
                "xpath",
                ImmutableList.of("/content/testFullTextTermNameSimple"));
        assertQuery("//*[jcr:contains(., 'testFullTextTermNameFile.txt')]",
                "xpath",
                ImmutableList.of("/content/testFullTextTermNameFile.txt"));
    }

    @Test
    public void testMultiNotEqual() throws Exception {
        Tree c = root.getTree("/").addChild("content");

        c.addChild("one").setProperty("prop", "value");
        c.addChild("two").setProperty("prop",
                ImmutableList.of("aaa", "value", "bbb"), Type.STRINGS);
        c.addChild("three").setProperty("prop",
                ImmutableList.of("aaa", "bbb", "ccc"), Type.STRINGS);
        root.commit();

        assertQuery("//*[@prop != 'value']", "xpath",
                ImmutableList.of("/content/two", "/content/three"));
    }

    @Test
    public void testMultiAndEquals() throws Exception {
        Tree c = root.getTree("/").addChild("content");

        c.addChild("one").setProperty("prop", "aaa");
        c.addChild("two").setProperty("prop",
                ImmutableList.of("aaa", "bbb", "ccc"), Type.STRINGS);
        c.addChild("three").setProperty("prop", ImmutableList.of("aaa", "bbb"),
                Type.STRINGS);
        root.commit();

        assertQuery("//*[(@prop = 'aaa' and @prop = 'bbb' and @prop = 'ccc')]",
                "xpath", ImmutableList.of("/content/two"));
    }

    @Test
    public void testMultiAndLike() throws Exception {
        Tree c = root.getTree("/").addChild("content");

        c.addChild("one").setProperty("prop", "aaaBoom");
        c.addChild("two").setProperty("prop",
                ImmutableList.of("aaaBoom", "bbbBoom", "cccBoom"), Type.STRINGS);
        c.addChild("three").setProperty("prop", ImmutableList.of("aaaBoom", "bbbBoom"),
                Type.STRINGS);
        root.commit();

        assertQuery("//*[(jcr:like(@prop, 'aaa%') and jcr:like(@prop, 'bbb%') and jcr:like(@prop, 'ccc%'))]",
                "xpath", ImmutableList.of("/content/two"));
    }

    @Test
    public void testSubPropertyMultiAndEquals() throws Exception {
        Tree c = root.getTree("/").addChild("content");

        c.addChild("one").addChild("child").setProperty("prop", "aaa");
        c.addChild("two")
                .addChild("child")
                .setProperty("prop", ImmutableList.of("aaa", "bbb", "ccc"),
                        Type.STRINGS);
        c.addChild("three")
                .addChild("child")
                .setProperty("prop", ImmutableList.of("aaa", "bbb"),
                        Type.STRINGS);
        root.commit();

        assertQuery(
                "//*[(child/@prop = 'aaa' and child/@prop = 'bbb' and child/@prop = 'ccc')]",
                "xpath", ImmutableList.of("/content/two"));
    }

    @Test
    public void testSubPropertyMultiAndLike() throws Exception {
        Tree c = root.getTree("/").addChild("content");

        c.addChild("one").addChild("child").setProperty("prop", "aaaBoom");
        c.addChild("two")
                .addChild("child")
                .setProperty("prop", ImmutableList.of("aaaBoom", "bbbBoom", "cccBoom"),
                        Type.STRINGS);
        c.addChild("three")
                .addChild("child")
                .setProperty("prop", ImmutableList.of("aaaBoom", "bbbBoom"),
                        Type.STRINGS);
        root.commit();

        assertQuery(
                "//*[(jcr:like(child/@prop, 'aaa%') and jcr:like(child/@prop, 'bbb%') and jcr:like(child/@prop, 'ccc%'))]",
                "xpath", ImmutableList.of("/content/two"));
    }

    @Test
    public void testOak1301() throws Exception {
        Tree t1 = root.getTree("/").addChild("home").addChild("users")
                .addChild("testing").addChild("socialgraph_test_user_4");
        t1.setProperty("jcr:primaryType", "rep:User");
        t1.setProperty("rep:authorizableId", "socialgraph_test_user_4");

        Tree s = t1.addChild("social");
        s.setProperty("jcr:primaryType", "sling:Folder");

        Tree r = s.addChild("relationships");
        r.setProperty("jcr:primaryType", "sling:Folder");

        Tree f = r.addChild("friend");
        f.setProperty("jcr:primaryType", "sling:Folder");

        Tree sg = f.addChild("socialgraph_test_group");
        sg.setProperty("jcr:primaryType", "nt:unstructured");
        sg.setProperty("id", "socialgraph_test_group");

        Tree t2 = root.getTree("/").addChild("home").addChild("groups")
                .addChild("testing").addChild("socialgraph_test_group");
        root.commit();

        // select [jcr:path], [jcr:score], * from [nt:base] as a where [id/*] =
        // 'socialgraph_test_group' and isdescendantnode(a, '/home') /* xpath:
        // /jcr:root/home//*[id='socialgraph_test_group'] */
        assertQuery(
                "/jcr:root/home//*[@id='socialgraph_test_group']",
                "xpath",
                ImmutableList
                        .of("/home/users/testing/socialgraph_test_user_4/social/relationships/friend/socialgraph_test_group"));

        // sql2 select c.[jcr:path] as [jcr:path], c.[jcr:score] as [jcr:score],
        // c.* from [nt:base] as a inner join [nt:base] as b on ischildnode(b,
        // a) inner join [nt:base] as c on isdescendantnode(c, b) where name(a)
        // = 'social' and isdescendantnode(a, '/home') and name(b) =
        // 'relationships' and c.[id/*] = 'socialgraph_test_group' /* xpath:
        // /jcr:root/home//social/relationships//*[id='socialgraph_test_group']
        // */
        assertQuery(
                "/jcr:root/home//social/relationships//*[@id='socialgraph_test_group']",
                "xpath",
                ImmutableList
                        .of("/home/users/testing/socialgraph_test_user_4/social/relationships/friend/socialgraph_test_group"));

    }

    @Test
    public void testRelativeProperties() throws Exception {
        root.getTree("/").addChild("content").addChild("node1")
                .setProperty("prop", 128);
        root.commit();

        assertQuery("//*[(@prop > 1)]", "xpath",
                ImmutableList.of("/content/node1"));
        assertQuery("//*[(@prop > 2)]", "xpath",
                ImmutableList.of("/content/node1"));
        assertQuery("//*[(@prop > 20)]", "xpath",
                ImmutableList.of("/content/node1"));
        assertQuery("//*[(@prop > 100)]", "xpath",
                ImmutableList.of("/content/node1"));
        assertQuery("//*[(@prop > 200)]", "xpath", new ArrayList<String>());
        assertQuery("//*[(@prop > 1000)]", "xpath", new ArrayList<String>());

        assertQuery("//*[(*/@prop > 1)]", "xpath", ImmutableList.of("/content"));
        assertQuery("//*[(*/@prop > 2)]", "xpath", ImmutableList.of("/content"));
        assertQuery("//*[(*/@prop > 20)]", "xpath",
                ImmutableList.of("/content"));
        assertQuery("//*[(*/@prop > 100)]", "xpath",
                ImmutableList.of("/content"));
        assertQuery("//*[(*/@prop > 200)]", "xpath", new ArrayList<String>());
        assertQuery("//*[(*/@prop > 1000)]", "xpath", new ArrayList<String>());
    }

    @Test // OAK-2062
    public void testRelativeProperties2() throws Exception {
        Tree t = root.getTree("/").addChild("content").addChild("nodes");
        Tree a = t.addChild("a");
        a.setProperty(JCR_PRIMARYTYPE, "nt:unstructured", Type.NAME);
        Tree b = a.addChild("b");
        b.setProperty(JCR_PRIMARYTYPE, "nt:unstructured", Type.NAME);
        Tree c = b.addChild("c");
        c.setProperty(JCR_PRIMARYTYPE, "nt:unstructured", Type.NAME);

        Tree d1 = c.addChild("d1");
        d1.setProperty("prop", 10);
        d1.setProperty(JCR_PRIMARYTYPE, "nt:unstructured", Type.NAME);
        Tree d2 = c.addChild("d2");
        d2.setProperty("prop", 20);
        d2.setProperty(JCR_PRIMARYTYPE, "nt:unstructured", Type.NAME);
        Tree d3 = c.addChild("d3");
        d3.setProperty("prop", 30);
        d3.setProperty(JCR_PRIMARYTYPE, "nt:unstructured", Type.NAME);
        root.commit();

        assertQuery("/jcr:root/content/nodes//*[(*/*/*/@prop >= 9)]", "xpath",
                ImmutableList.of("/content/nodes/a"));

        assertQuery(
                "/jcr:root/content/nodes//element(*, nt:unstructured)[(*/*/*/@prop >= 9)]",
                "xpath", ImmutableList.of("/content/nodes/a"));

        assertQuery("/jcr:root/content/nodes//*[(*/*/*/@prop >= 10)]", "xpath",
                ImmutableList.of("/content/nodes/a"));
        assertQuery(
                "/jcr:root/content/nodes//element(*, nt:unstructured)[(*/*/*/@prop >= 10)]",
                "xpath", ImmutableList.of("/content/nodes/a"));

        assertQuery("/jcr:root/content/nodes//*[(*/*/*/@prop >= 15)]", "xpath",
                ImmutableList.of("/content/nodes/a"));
        assertQuery(
                "/jcr:root/content/nodes//element(*, nt:unstructured)[(*/*/*/@prop >= 15)]",
                "xpath", ImmutableList.of("/content/nodes/a"));

        assertQuery("/jcr:root/content/nodes//*[(*/*/*/@prop >= 20)]", "xpath",
                ImmutableList.of("/content/nodes/a"));
        assertQuery(
                "/jcr:root/content/nodes//element(*, nt:unstructured)[(*/*/*/@prop >= 20)]",
                "xpath", ImmutableList.of("/content/nodes/a"));

        assertQuery("/jcr:root/content/nodes//*[(*/*/*/@prop >= 30)]", "xpath",
                ImmutableList.of("/content/nodes/a"));
        assertQuery(
                "/jcr:root/content/nodes//element(*, nt:unstructured)[(*/*/*/@prop >= 30)]",
                "xpath", ImmutableList.of("/content/nodes/a"));
    }
    
    /**
     * tests range queries, long comparisons and relative properties 
     * @throws CommitFailedException 
     */
    @Test // OAK-2062
    public void testRangeRelativeProperties() throws CommitFailedException {
        final List<String> emptyList = new ArrayList<String>();
        final String property = "prop";
        Tree contentNodes, t;
        
        contentNodes = root.getTree("/").addChild("content").addChild("nodes");
        
        /* creating content structure
         * content : {
         *   nodes : {
         *     a9 {
         *       b : {
         *         c : {
         *           d9 : {
         *             prop : 9
         *           }
         *         }
         *       }
         *     },
         *     a10 {
         *       b : {
         *         c : {
         *           d10 : {
         *             prop : 10
         *           }
         *         }
         *       }
         *     },
         *     a20 {
         *       b : {
         *         c : {
         *           d20 : {
         *             prop : 20
         *           }
         *         }
         *       }
         *     },
         *     a30 {
         *       b : {
         *         c : {
         *           d30 : {
         *             prop : 30
         *           }
         *         }
         *       }
         *     },
         *   }
         * }
         * 
         */
        t = addNtUnstructuredChild(contentNodes, "a9", null, null);
        t = addNtUnstructuredChild(t, "b", null, null);
        t = addNtUnstructuredChild(t, "c", null, null);
        t = addNtUnstructuredChild(t, "d9", property, 9L);
        t = addNtUnstructuredChild(contentNodes, "a10", null, null);
        t = addNtUnstructuredChild(t, "b", null, null);
        t = addNtUnstructuredChild(t, "c", null, null);
        t = addNtUnstructuredChild(t, "d10", property, 10L);
        t = addNtUnstructuredChild(contentNodes, "a20", null, null);
        t = addNtUnstructuredChild(t, "b", null, null);
        t = addNtUnstructuredChild(t, "c", null, null);
        t = addNtUnstructuredChild(t, "d20", property, 20L);
        t = addNtUnstructuredChild(contentNodes, "a30", null, null);
        t = addNtUnstructuredChild(t, "b", null, null);
        t = addNtUnstructuredChild(t, "c", null, null);
        t = addNtUnstructuredChild(t, "d30", property, 30L);

        root.commit();
        
        assertQuery("/jcr:root/content/nodes//*[(*/*/*/@prop >= 9)]", "xpath",
            of("/content/nodes/a9", "/content/nodes/a10", "/content/nodes/a20",
                "/content/nodes/a30"));
        assertQuery("/jcr:root/content/nodes//*[(*/*/*/@prop >= 10)]", "xpath",
            of("/content/nodes/a10", "/content/nodes/a20", "/content/nodes/a30"));
        assertQuery("/jcr:root/content/nodes//*[(*/*/*/@prop >= 20)]", "xpath",
            of("/content/nodes/a20", "/content/nodes/a30"));
        assertQuery("/jcr:root/content/nodes//*[(*/*/*/@prop >= 30)]", "xpath",
            of("/content/nodes/a30"));
        assertQuery("/jcr:root/content/nodes//*[(*/*/*/@prop >= 40)]", "xpath", emptyList);

        assertQuery("/jcr:root/content/nodes//*[(*/*/*/@prop <= 8)]", "xpath", emptyList);
        assertQuery("/jcr:root/content/nodes//*[(*/*/*/@prop <= 9)]", "xpath",
            of("/content/nodes/a9"));
        assertQuery("/jcr:root/content/nodes//*[(*/*/*/@prop <= 10)]", "xpath",
            of("/content/nodes/a9", "/content/nodes/a10"));
        assertQuery("/jcr:root/content/nodes//*[(*/*/*/@prop <= 20)]", "xpath",
            of("/content/nodes/a9", "/content/nodes/a10", "/content/nodes/a20"));
        assertQuery("/jcr:root/content/nodes//*[(*/*/*/@prop <= 30)]", "xpath",
            of("/content/nodes/a9", "/content/nodes/a10", "/content/nodes/a20",
                "/content/nodes/a30"));
        
        assertQuery(
            "/jcr:root/content/nodes//element(*, nt:unstructured)[(*/*/*/@prop >= 9)]",
            "xpath",
            of("/content/nodes/a9", "/content/nodes/a10", "/content/nodes/a20",
                "/content/nodes/a30"));
        assertQuery("/jcr:root/content/nodes//element(*, nt:unstructured)[(*/*/*/@prop >= 10)]",
            "xpath", of("/content/nodes/a10", "/content/nodes/a20", "/content/nodes/a30"));
        assertQuery("/jcr:root/content/nodes//element(*, nt:unstructured)[(*/*/*/@prop >= 20)]",
            "xpath", of("/content/nodes/a20", "/content/nodes/a30"));
        assertQuery("/jcr:root/content/nodes//element(*, nt:unstructured)[(*/*/*/@prop >= 30)]",
            "xpath", of("/content/nodes/a30"));
        assertQuery("/jcr:root/content/nodes//element(*, nt:unstructured)[(*/*/*/@prop >= 40)]",
            "xpath", emptyList);

        assertQuery("/jcr:root/content/nodes//element(*, nt:unstructured)[(*/*/*/@prop <= 8)]",
            "xpath", emptyList);
        assertQuery("/jcr:root/content/nodes//element(*, nt:unstructured)[(*/*/*/@prop <= 9)]",
            "xpath", of("/content/nodes/a9"));
        assertQuery("/jcr:root/content/nodes//element(*, nt:unstructured)[(*/*/*/@prop <= 10)]",
            "xpath", of("/content/nodes/a9", "/content/nodes/a10"));
        assertQuery("/jcr:root/content/nodes//element(*, nt:unstructured)[(*/*/*/@prop <= 20)]",
            "xpath", of("/content/nodes/a9", "/content/nodes/a10", "/content/nodes/a20"));
        assertQuery(
            "/jcr:root/content/nodes//element(*, nt:unstructured)[(*/*/*/@prop <= 30)]",
            "xpath",
            of("/content/nodes/a9", "/content/nodes/a10", "/content/nodes/a20",
                "/content/nodes/a30"));
    }

    /**
     * adds a child of type {@link JcrConstants#NT_UNSTRUCTURED} under the provided {@code parent}
     * with the provided {@code name} and an optional {@code propertyName} and {@code value}. If
     * either {@code propertyName} or {@code value} are null the property won't be set.
     * 
     * @param parent
     * @param name
     * @param propertyName
     * @param value
     * @return
     */
    private static Tree addNtUnstructuredChild(@NotNull final Tree parent, 
                                               @NotNull final String name, 
                                               @Nullable final String propertyName, 
                                               @Nullable final Long value) {
        checkNotNull(parent);
        checkNotNull(name);
        
        Tree ret = parent.addChild(name);
        ret.setProperty(JCR_PRIMARYTYPE, NT_UNSTRUCTURED, NAME);
        
        if (propertyName != null && value != null) {
            ret.setProperty(propertyName, value, LONG);
        }
        
        return ret;
    }
    
    @Test
    public void testMultipleRelativeProperties() throws Exception {
        Tree content = root.getTree("/").addChild("content");

        content.addChild("node1").setProperty("a", 128);
        content.addChild("node2").setProperty("a", "abc");
        content.addChild("node3").setProperty("a", "1280");

        content.addChild("node1").setProperty("b", 128);
        content.addChild("node2").setProperty("b", 1024);
        content.addChild("node3").setProperty("b", 2048);

        content.addChild("node1").setProperty("c", 10.3);
        content.addChild("node2").setProperty("c", -10.3);
        content.addChild("node3").setProperty("c", 9.8);

        content.addChild("node1").setProperty("d", Arrays.asList("x", "y"), Type.STRINGS);
        content.addChild("node2").setProperty("d", 10);
        content.addChild("node3").setProperty("d", Arrays.asList(1L, 2L), Type.LONGS);

        root.commit();

        assertQuery("//*[*/@a > 2]", "xpath", Arrays.asList("/content"));
        assertQuery("//*[*/@a > '1']", "xpath", Arrays.asList("/content"));
        assertQuery("//*[*/@a > 'abd']", "xpath", Arrays.<String>asList());
        assertQuery("//*[*/@a = 'abc']", "xpath", Arrays.asList("/content"));
        // this may be unexpected: it is evaluated as
        // ['128', 'abc', '1280'] >= 'abc'
        assertQuery("//*[*/@a >= 'abc']", "xpath", Arrays.asList("/content"));

        assertQuery("//*[*/@b > 2]", "xpath", Arrays.asList("/content"));
        assertQuery("//*[*/@b > 2048]", "xpath", Arrays.<String>asList());
        assertQuery("//*[*/@b > '1']", "xpath", Arrays.asList("/content"));
        assertQuery("//*[*/@b = 128]", "xpath", Arrays.asList("/content"));

        assertQuery("//*[*/@c > 10]", "xpath", Arrays.asList("/content"));
        assertQuery("//*[*/@c > 11]", "xpath", Arrays.<String>asList());
        assertQuery("//*[*/@c > '1']", "xpath", Arrays.asList("/content"));
        assertQuery("//*[*/@c = 9.8]", "xpath", Arrays.asList("/content"));

        assertQuery("//*[*/@d > 10]", "xpath", Arrays.asList("/content"));
        assertQuery("//*[*/@d > 11]", "xpath", Arrays.asList("/content"));
        assertQuery("//*[*/@d > '1']", "xpath", Arrays.asList("/content"));
        assertQuery("//*[*/@d = 10]", "xpath", Arrays.asList("/content"));
        // this may be unexpected: it is evaluated as
        // ['x', 'y', '10', '1', '2'] < '3'
        assertQuery("//*[*/@d < 3]", "xpath", Arrays.asList("/content"));
    }

    @Test
    public void testLowercaseOnArrays() throws Exception {
        // OAK-1829
        Tree content = root.getTree("/").addChild("content");
        content.setProperty("array", Arrays.asList("X", "Y"), Type.STRINGS);
        root.commit();
        assertQuery("//*[jcr:like(fn:lower-case(@array), '%x%')]", "xpath",
                Arrays.asList("/content"));
    }

}
