/*
 * 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.plugins.index.search;

import java.util.Collections;
import java.util.List;
import java.util.Set;

import javax.jcr.PropertyType;

import org.apache.jackrabbit.JcrConstants;
import org.apache.jackrabbit.oak.api.Type;
import org.apache.jackrabbit.oak.plugins.index.IndexConstants;
import org.apache.jackrabbit.oak.plugins.index.search.IndexDefinition.IndexingRule;
import org.apache.jackrabbit.oak.plugins.index.search.FulltextIndexConstants.IndexingMode;
import org.apache.jackrabbit.oak.plugins.index.search.util.IndexDefinitionBuilder;
import org.apache.jackrabbit.oak.spi.state.NodeBuilder;
import org.apache.jackrabbit.oak.spi.state.NodeState;
import org.junit.Test;

import static javax.jcr.PropertyType.TYPENAME_LONG;
import static javax.jcr.PropertyType.TYPENAME_STRING;
import static org.apache.jackrabbit.JcrConstants.NT_BASE;
import static org.apache.jackrabbit.oak.InitialContentHelper.INITIAL_CONTENT;
import static org.apache.jackrabbit.oak.api.Type.NAMES;
import static org.apache.jackrabbit.oak.api.Type.STRING;
import static org.apache.jackrabbit.oak.api.Type.STRINGS;
import static org.apache.jackrabbit.oak.plugins.index.IndexConstants.INDEX_DEFINITIONS_NAME;
import static org.apache.jackrabbit.oak.plugins.index.search.FulltextIndexConstants.INCLUDE_PROPERTY_NAMES;
import static org.apache.jackrabbit.oak.plugins.index.search.FulltextIndexConstants.INCLUDE_PROPERTY_TYPES;
import static org.apache.jackrabbit.oak.plugins.index.search.FulltextIndexConstants.INDEX_RULES;
import static org.apache.jackrabbit.oak.plugins.index.search.FulltextIndexConstants.PROP_NAME;
import static org.apache.jackrabbit.oak.plugins.index.search.FulltextIndexConstants.PROP_NODE;
import static org.apache.jackrabbit.oak.plugins.index.search.FulltextIndexConstants.TIKA;
import static org.apache.jackrabbit.oak.plugins.index.search.TestUtil.registerTestNodeType;
import static org.apache.jackrabbit.oak.plugins.index.search.util.IndexHelper.newFTIndexDefinition;
import static org.apache.jackrabbit.oak.plugins.index.search.util.IndexHelper.newFTPropertyIndexDefinition;
import static org.apache.jackrabbit.oak.plugins.memory.EmptyNodeState.EMPTY_NODE;
import static org.apache.jackrabbit.oak.plugins.memory.PropertyStates.createProperty;
import static org.apache.jackrabbit.oak.plugins.tree.TreeConstants.OAK_CHILD_ORDER;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.containsInAnyOrder;
import static org.hamcrest.Matchers.empty;
import static org.hamcrest.Matchers.is;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;

public class IndexDefinitionTest {

    private NodeState root = INITIAL_CONTENT;

    private final NodeBuilder builder = root.builder();

    @Test
    public void defaultConfig() {
        IndexDefinition idxDefn = new IndexDefinition(root, builder.getNodeState(), "/foo");
        assertFalse(idxDefn.hasSyncPropertyDefinitions());
    }

    @Test
    public void fullTextEnabled() {
        IndexDefinition idxDefn = new IndexDefinition(root, builder.getNodeState(), "/foo");
        IndexDefinition.IndexingRule rule = idxDefn.getApplicableIndexingRule(NT_BASE);
        assertTrue("By default fulltext is enabled", idxDefn.isFullTextEnabled());
        assertTrue("By default everything is indexed", rule.isIndexed("foo"));
        assertTrue("Property types need to be defined", rule.includePropertyType(PropertyType.DATE));
        assertTrue("For fulltext storage is enabled", rule.getConfig("foo").stored);

        assertFalse(rule.getConfig("foo").skipTokenization("foo"));
        assertTrue(rule.getConfig("jcr:uuid").skipTokenization("jcr:uuid"));
    }

    @Test
    public void propertyTypes() throws Exception {
        builder.setProperty(createProperty(INCLUDE_PROPERTY_TYPES, Set.of(TYPENAME_LONG), STRINGS));
        builder.setProperty(createProperty(INCLUDE_PROPERTY_NAMES, Set.of("foo", "bar"), STRINGS));
        builder.setProperty(FulltextIndexConstants.FULL_TEXT_ENABLED, false);
        IndexDefinition idxDefn = new IndexDefinition(root, builder.getNodeState(), "/foo");
        IndexDefinition.IndexingRule rule = idxDefn.getApplicableIndexingRule(NT_BASE);
        assertFalse(idxDefn.isFullTextEnabled());
        assertFalse("If fulltext disabled then nothing stored", rule.getConfig("foo").stored);

        assertTrue(rule.includePropertyType(PropertyType.LONG));
        assertFalse(rule.includePropertyType(PropertyType.STRING));

        assertTrue(rule.isIndexed("foo"));
        assertTrue(rule.isIndexed("bar"));
        assertFalse(rule.isIndexed("baz"));

        assertTrue(rule.getConfig("foo").skipTokenization("foo"));
    }


    @Test
    public void queryPathsAsString() {
        builder.setProperty(createProperty(IndexConstants.QUERY_PATHS, "/foo/bar", STRING));
        IndexDefinition idxDefn = new IndexDefinition(root, builder.getNodeState(), "/foo");
        assertEquals(new String[]{"/foo/bar"}, idxDefn.getQueryPaths());
    }

    @Test
    public void propertyDefinition() {
        builder.child(PROP_NODE).child("foo").setProperty(FulltextIndexConstants.PROP_TYPE, PropertyType.TYPENAME_DATE);
        builder.setProperty(createProperty(INCLUDE_PROPERTY_NAMES, Set.of("foo", "bar"), STRINGS));
        IndexDefinition idxDefn = new IndexDefinition(root, builder.getNodeState(), "/foo");
        IndexDefinition.IndexingRule rule = idxDefn.getApplicableIndexingRule(NT_BASE);

        assertTrue(rule.isIndexed("foo"));
        assertTrue(rule.isIndexed("bar"));

        assertEquals(PropertyType.DATE, rule.getConfig("foo").getType());
    }

    @Test
    public void propertyDefinitionWithExcludes() {
        builder.child(PROP_NODE).child("foo").setProperty(FulltextIndexConstants.PROP_TYPE, PropertyType.TYPENAME_DATE);
        IndexDefinition idxDefn = new IndexDefinition(root, builder.getNodeState(), "/foo");
        IndexDefinition.IndexingRule rule = idxDefn.getApplicableIndexingRule(NT_BASE);
        assertTrue(rule.isIndexed("foo"));
        assertTrue(rule.isIndexed("bar"));

        assertEquals(PropertyType.DATE, rule.getConfig("foo").getType());
    }

    @Test
    public void indexRuleSanity() {
        NodeBuilder rules = builder.child(INDEX_RULES);
        rules.child("nt:folder").setProperty(FulltextIndexConstants.FIELD_BOOST, 2.0);
        TestUtil.child(rules, "nt:folder/properties/prop1")
                .setProperty(FulltextIndexConstants.FIELD_BOOST, 3.0)
                .setProperty(FulltextIndexConstants.PROP_TYPE, PropertyType.TYPENAME_BOOLEAN);

        IndexDefinition defn = new IndexDefinition(root, builder.getNodeState(), "/foo");

        assertNull(defn.getApplicableIndexingRule(asState(newNode("nt:base"))));

        IndexingRule rule1 = defn.getApplicableIndexingRule(asState(newNode("nt:folder")));
        assertNotNull(rule1);
        assertEquals(2.0f, rule1.boost, 0);

        assertTrue(rule1.isIndexed("prop1"));
        assertFalse(rule1.isIndexed("prop2"));

        PropertyDefinition pd = rule1.getConfig("prop1");
        assertEquals(3.0f, pd.boost, 0);
        assertEquals(PropertyType.BOOLEAN, pd.getType());
    }

    @Test
    public void indexRuleInheritance() {
        NodeBuilder rules = builder.child(INDEX_RULES);
        builder.setProperty(PROP_NAME, "testIndex");
        rules.child("nt:hierarchyNode").setProperty(FulltextIndexConstants.FIELD_BOOST, 2.0);

        IndexDefinition defn = new IndexDefinition(root, builder.getNodeState(), "/foo");

        assertNull(defn.getApplicableIndexingRule(asState(newNode("nt:base"))));
        assertNotNull(defn.getApplicableIndexingRule(asState(newNode("nt:hierarchyNode"))));
        assertNotNull(defn.getApplicableIndexingRule(asState(newNode("nt:folder"))));
    }

    @Test
    public void indexRuleMixin() {
        NodeBuilder rules = builder.child(INDEX_RULES);
        rules.child("mix:title");
        TestUtil.child(rules, "mix:title/properties/jcr:title")
                .setProperty(FulltextIndexConstants.FIELD_BOOST, 3.0);

        IndexDefinition defn = new IndexDefinition(root, builder.getNodeState(), "/foo");

        assertNotNull(defn.getApplicableIndexingRule(asState(newNode("nt:folder", "mix:title"))));
        assertNull(defn.getApplicableIndexingRule(asState(newNode("nt:folder"))));
    }

    @Test
    public void indexRuleMixinInheritance() {
        NodeBuilder rules = builder.child(INDEX_RULES);
        rules.child("mix:mimeType");
        TestUtil.child(rules, "mix:mimeType/properties/jcr:mimeType")
                .setProperty(FulltextIndexConstants.FIELD_BOOST, 3.0);

        IndexDefinition defn = new IndexDefinition(root, builder.getNodeState(), "/foo");

        assertNotNull(defn.getApplicableIndexingRule(asState(newNode("nt:folder", "mix:mimeType"))));
        assertNull(defn.getApplicableIndexingRule(asState(newNode("nt:folder"))));

        //nt:resource > mix:mimeType
        assertNotNull(defn.getApplicableIndexingRule(asState(newNode("nt:resource"))));
    }

    @Test
    public void indexRuleInheritanceDisabled() {
        NodeBuilder rules = builder.child(INDEX_RULES);
        builder.setProperty(PROP_NAME, "testIndex");
        rules.child("nt:hierarchyNode")
                .setProperty(FulltextIndexConstants.FIELD_BOOST, 2.0)
                .setProperty(FulltextIndexConstants.RULE_INHERITED, false);

        IndexDefinition defn = new IndexDefinition(root, builder.getNodeState(), "/foo");

        assertNull(defn.getApplicableIndexingRule(asState(newNode("nt:base"))));
        assertNotNull(defn.getApplicableIndexingRule(asState(newNode("nt:hierarchyNode"))));
        assertNull("nt:folder should not be index as rule is not inheritable",
                defn.getApplicableIndexingRule(asState(newNode("nt:folder"))));
    }

    @Test
    public void indexRuleInheritanceOrdering() {
        NodeBuilder rules = builder.child(INDEX_RULES);
        rules.setProperty(OAK_CHILD_ORDER, List.of("nt:hierarchyNode", "nt:base"), NAMES);
        rules.child("nt:hierarchyNode").setProperty(FulltextIndexConstants.FIELD_BOOST, 2.0);
        rules.child("nt:base").setProperty(FulltextIndexConstants.FIELD_BOOST, 3.0);

        IndexDefinition defn = new IndexDefinition(root, builder.getNodeState(), "/foo");

        assertEquals(3.0, getRule(defn, "nt:base").boost, 0);
        assertEquals(2.0, getRule(defn, "nt:hierarchyNode").boost, 0);
        assertEquals(3.0, getRule(defn, "nt:query").boost, 0);
    }

    @Test
    public void indexRuleInheritanceOrdering2() {
        NodeBuilder rules = builder.child(INDEX_RULES);
        rules.setProperty(OAK_CHILD_ORDER, List.of("nt:base", "nt:hierarchyNode"), NAMES);
        rules.child("nt:hierarchyNode").setProperty(FulltextIndexConstants.FIELD_BOOST, 2.0);
        rules.child("nt:base").setProperty(FulltextIndexConstants.FIELD_BOOST, 3.0);

        IndexDefinition defn = new IndexDefinition(root, builder.getNodeState(), "/foo");

        //As nt:base is defined earlier it would supercede everything
        assertEquals(3.0, getRule(defn, "nt:base").boost, 0);
        assertEquals(3.0, getRule(defn, "nt:hierarchyNode").boost, 0);
        assertEquals(3.0, getRule(defn, "nt:file").boost, 0);
    }

    @Test
    public void indexRuleWithPropertyRegEx() {
        NodeBuilder rules = builder.child(INDEX_RULES);
        rules.child("nt:folder");
        TestUtil.child(rules, "nt:folder/properties/prop1")
                .setProperty(FulltextIndexConstants.FIELD_BOOST, 3.0);
        TestUtil.child(rules, "nt:folder/properties/prop2")
                .setProperty(PROP_NAME, "foo.*")
                .setProperty(FulltextIndexConstants.PROP_IS_REGEX, true)
                .setProperty(FulltextIndexConstants.FIELD_BOOST, 4.0);

        IndexDefinition defn = new IndexDefinition(root, builder.getNodeState(), "/foo");

        IndexingRule rule1 = defn.getApplicableIndexingRule(asState(newNode("nt:folder")));
        assertNotNull(rule1);

        assertTrue(rule1.isIndexed("prop1"));
        assertFalse(rule1.isIndexed("prop2"));
        assertTrue(rule1.isIndexed("fooProp"));

        PropertyDefinition pd = rule1.getConfig("fooProp2");
        assertEquals(4.0f, pd.boost, 0);
    }

    @Test
    public void indexRuleWithPropertyRegEx2() {
        NodeBuilder rules = builder.child(INDEX_RULES);
        rules.child("nt:folder");
        TestUtil.child(rules, "nt:folder/properties/prop1")
                .setProperty(PROP_NAME, ".*")
                .setProperty(FulltextIndexConstants.PROP_IS_REGEX, true);
        TestUtil.child(rules, "nt:folder/properties/prop2")
                .setProperty(PROP_NAME, "metadata/.*")
                .setProperty(FulltextIndexConstants.PROP_IS_REGEX, true)
                .setProperty(FulltextIndexConstants.FIELD_BOOST, 4.0);


        IndexDefinition defn = new IndexDefinition(root, builder.getNodeState(), "/foo");

        IndexingRule rule1 = defn.getApplicableIndexingRule(asState(newNode("nt:folder")));
        assertNotNull(rule1);

        assertTrue(rule1.isIndexed("prop1"));
        assertTrue(rule1.isIndexed("prop2"));
        assertFalse(rule1.isIndexed("jcr:content/prop1"));

        assertTrue(rule1.isIndexed("metadata/foo"));
        assertFalse(rule1.isIndexed("metadata/foo/bar"));
    }

    @Test
    public void indexRuleWithPropertyOrdering() {
        NodeBuilder rules = builder.child(INDEX_RULES);
        rules.child("nt:folder");
        TestUtil.child(rules, "nt:folder/properties/prop1")
                .setProperty(PROP_NAME, "foo.*")
                .setProperty(FulltextIndexConstants.PROP_IS_REGEX, true)
                .setProperty(FulltextIndexConstants.FIELD_BOOST, 3.0);
        TestUtil.child(rules, "nt:folder/properties/prop2")
                .setProperty(PROP_NAME, ".*")
                .setProperty(FulltextIndexConstants.PROP_IS_REGEX, true)
                .setProperty(FulltextIndexConstants.FIELD_BOOST, 4.0);

        rules.child("nt:folder").child(PROP_NODE).setProperty(OAK_CHILD_ORDER, List.of("prop2", "prop1"), NAMES);

        IndexDefinition defn = new IndexDefinition(root, builder.getNodeState(), "/foo");

        IndexingRule rule1 = defn.getApplicableIndexingRule(asState(newNode("nt:folder")));
        assertNotNull(rule1);

        assertTrue(rule1.isIndexed("prop1"));
        assertTrue(rule1.isIndexed("fooProp"));

        assertEquals(4.0f, rule1.getConfig("bazProp2").boost, 0);
        //As prop2 is ordered before prop1 its regEx is evaluated first
        //hence even with a specific regex of foo.* the defn used is from .*
        assertEquals(4.0f, rule1.getConfig("fooProp").boost, 0);

        //Order it correctly to get expected result
        rules.child("nt:folder").child(PROP_NODE).setProperty(OAK_CHILD_ORDER, List.of("prop1", "prop2"), NAMES);
        defn = new IndexDefinition(root, builder.getNodeState(), "/foo");
        rule1 = defn.getApplicableIndexingRule(asState(newNode("nt:folder")));
        assertEquals(3.0f, rule1.getConfig("fooProp").boost, 0);
    }

    @Test
    public void propertyConfigCaseInsensitive() {
        NodeBuilder rules = builder.child(INDEX_RULES);
        rules.child("nt:folder");
        TestUtil.child(rules, "nt:folder/properties/foo")
                .setProperty(PROP_NAME, "Foo")
                .setProperty(FulltextIndexConstants.PROP_PROPERTY_INDEX, true);
        TestUtil.child(rules, "nt:folder/properties/bar")
                .setProperty(PROP_NAME, "BAR")
                .setProperty(FulltextIndexConstants.PROP_PROPERTY_INDEX, true);

        IndexDefinition defn = new IndexDefinition(root, builder.getNodeState(), "/foo");

        IndexingRule rule1 = defn.getApplicableIndexingRule(asState(newNode("nt:folder")));
        assertNotNull(rule1);

        assertTrue(rule1.isIndexed("Foo"));
        assertTrue(rule1.isIndexed("foo"));
        assertTrue(rule1.isIndexed("fOO"));
        assertTrue(rule1.isIndexed("bar"));
        assertFalse(rule1.isIndexed("baz"));
    }

    @Test
    public void skipTokenization() {
        NodeBuilder rules = builder.child(INDEX_RULES);
        rules.child("nt:folder");
        TestUtil.child(rules, "nt:folder/properties/prop2")
                .setProperty(PROP_NAME, ".*")
                .setProperty(FulltextIndexConstants.PROP_IS_REGEX, true)
                .setProperty(FulltextIndexConstants.PROP_ANALYZED, true);

        IndexDefinition defn = new IndexDefinition(root, builder.getNodeState(), "/foo");

        IndexingRule rule = defn.getApplicableIndexingRule(asState(newNode("nt:folder")));
        assertFalse(rule.getConfig("foo").skipTokenization("foo"));
        assertTrue(rule.getConfig(JcrConstants.JCR_UUID).skipTokenization(JcrConstants.JCR_UUID));
    }

    @Test
    public void formatUpdate() {
        NodeBuilder defnb = newFTIndexDefinition(builder.child(INDEX_DEFINITIONS_NAME), "foo",
                "lucene", Set.of(TYPENAME_STRING), Set.of("foo", "Bar"), "async");
        IndexDefinition defn = new IndexDefinition(root, defnb.getNodeState(), "/foo");
        assertTrue(defn.isOfOldFormat());

        NodeBuilder updated = IndexDefinition.updateDefinition(defnb.getNodeState().builder());
        IndexDefinition defn2 = new IndexDefinition(root, updated.getNodeState(), "/foo");

        assertFalse(defn2.isOfOldFormat());
        IndexingRule rule = defn2.getApplicableIndexingRule(asState(newNode("nt:base")));
        assertNotNull(rule);
        assertFalse(rule.getConfig("foo").index);
        assertFalse(rule.getConfig("Bar").index);
    }

    @Test
    public void propertyRegExAndRelativeProperty() {
        NodeBuilder defnb = newFTIndexDefinition(builder.child(INDEX_DEFINITIONS_NAME), "foo",
                "lucene", Set.of(TYPENAME_STRING), Set.of("foo"), "async");
        IndexDefinition defn = new IndexDefinition(root, defnb.getNodeState(), "/foo");
        assertTrue(defn.isOfOldFormat());

        NodeBuilder updated = IndexDefinition.updateDefinition(defnb.getNodeState().builder());
        IndexDefinition defn2 = new IndexDefinition(root, updated.getNodeState(), "/foo");

        IndexingRule rule = defn2.getApplicableIndexingRule(asState(newNode("nt:base")));
        assertNotNull(rule.getConfig("foo"));
        assertNull("Property regex used should not allow relative properties", rule.getConfig("foo/bar"));
    }

    @Test
    public void fulltextEnabledAndAggregate() {
        NodeBuilder defnb = newFTPropertyIndexDefinition(builder.child(INDEX_DEFINITIONS_NAME), "foo",
                "lucene", Set.of("foo"), "async");
        IndexDefinition defn = new IndexDefinition(root, defnb.getNodeState(), "/foo");
        assertFalse(defn.isFullTextEnabled());

        NodeBuilder aggregates = defnb.child(FulltextIndexConstants.AGGREGATES);
        NodeBuilder aggFolder = aggregates.child("nt:base");
        aggFolder.child("i1").setProperty(FulltextIndexConstants.AGG_PATH, "*");

        defn = new IndexDefinition(root, defnb.getNodeState(), "/foo");
        assertTrue(defn.isFullTextEnabled());
    }

    @Test
    public void costConfig() {
        NodeBuilder defnb = newFTPropertyIndexDefinition(builder.child(INDEX_DEFINITIONS_NAME), "foo",
                "lucene", Set.of("foo"), "async");
        IndexDefinition defn = new IndexDefinition(root, defnb.getNodeState(), "/foo");
        assertEquals(1.0, defn.getCostPerEntry(), 0);
        assertEquals(1.0, defn.getCostPerExecution(), 0);
        assertEquals(IndexDefinition.DEFAULT_ENTRY_COUNT, defn.getEntryCount());
        assertFalse(defn.isEntryCountDefined());

        defnb.setProperty(FulltextIndexConstants.COST_PER_ENTRY, 2.0);
        defnb.setProperty(FulltextIndexConstants.COST_PER_EXECUTION, 3.0);
        defnb.setProperty(IndexConstants.ENTRY_COUNT_PROPERTY_NAME, 500);

        IndexDefinition defn2 = new IndexDefinition(root, defnb.getNodeState(), "/foo");
        assertEquals(2.0, defn2.getCostPerEntry(), 0);
        assertEquals(3.0, defn2.getCostPerExecution(), 0);
        assertEquals(500, defn2.getEntryCount());
    }

    @Test
    public void fulltextCost() {
        NodeBuilder defnb = newFTPropertyIndexDefinition(builder.child(INDEX_DEFINITIONS_NAME), "foo",
                "lucene", Set.of("foo"), "async");
        IndexDefinition defn = new IndexDefinition(root, defnb.getNodeState(), "/foo");
        assertEquals(300, defn.getFulltextEntryCount(300));
        assertEquals(IndexDefinition.DEFAULT_ENTRY_COUNT + 100,
                defn.getFulltextEntryCount(IndexDefinition.DEFAULT_ENTRY_COUNT + 100));

        //Once count is explicitly defined then it would influence the cost
        defnb.setProperty(IndexConstants.ENTRY_COUNT_PROPERTY_NAME, 100);
        defn = new IndexDefinition(root, defnb.getNodeState(), "/foo");
        assertEquals(100, defn.getFulltextEntryCount(300));
        assertEquals(50, defn.getFulltextEntryCount(50));
    }

    @Test
    public void customTikaConfig() {
        NodeBuilder defnb = newFTIndexDefinition(builder.child(INDEX_DEFINITIONS_NAME), "foo",
                "lucene", Set.of(TYPENAME_STRING));
        IndexDefinition defn = new IndexDefinition(root, defnb.getNodeState(), "/foo");
        assertFalse(defn.hasCustomTikaConfig());

        defnb.child(FulltextIndexConstants.TIKA)
                .child(FulltextIndexConstants.TIKA_CONFIG)
                .child(JcrConstants.JCR_CONTENT)
                .setProperty(JcrConstants.JCR_DATA, "hello".getBytes());
        defn = new IndexDefinition(root, defnb.getNodeState(), "/foo");
        assertTrue(defn.hasCustomTikaConfig());
    }

    @Test
    public void customTikaMimeTypes() {
        NodeBuilder defnb = newFTIndexDefinition(builder.child(INDEX_DEFINITIONS_NAME), "foo",
                "lucene", Set.of(TYPENAME_STRING));
        IndexDefinition defn = new IndexDefinition(root, defnb.getNodeState(), "/foo");
        assertEquals("application/test", defn.getTikaMappedMimeType("application/test"));

        NodeBuilder app = defnb.child(FulltextIndexConstants.TIKA)
                .child(FulltextIndexConstants.TIKA_MIME_TYPES)
                .child("application");
        app.child("test").setProperty(FulltextIndexConstants.TIKA_MAPPED_TYPE, "text/plain");
        app.child("test2").setProperty(FulltextIndexConstants.TIKA_MAPPED_TYPE, "text/plain");
        defn = new IndexDefinition(root, defnb.getNodeState(), "/foo");
        assertEquals("text/plain", defn.getTikaMappedMimeType("application/test"));
        assertEquals("text/plain", defn.getTikaMappedMimeType("application/test2"));
        assertEquals("application/test-unmapped", defn.getTikaMappedMimeType("application/test-unmapped"));
    }

    @Test
    public void maxExtractLength() {
        NodeBuilder defnb = newFTIndexDefinition(builder.child(INDEX_DEFINITIONS_NAME), "foo",
                "lucene", Set.of(TYPENAME_STRING));
        IndexDefinition defn = new IndexDefinition(root, defnb.getNodeState(), "/foo");
        assertEquals(-IndexDefinition.DEFAULT_MAX_EXTRACT_LENGTH * IndexDefinition.DEFAULT_MAX_FIELD_LENGTH,
                defn.getMaxExtractLength());


        defnb.child(TIKA).setProperty(FulltextIndexConstants.TIKA_MAX_EXTRACT_LENGTH, 1000);

        defn = new IndexDefinition(root, defnb.getNodeState(), "/foo");
        assertEquals(1000, defn.getMaxExtractLength());
    }

    @Test(expected = IllegalStateException.class)
    public void nullCheckEnabledWithNtBase() {
        builder.child(PROP_NODE).child("foo").setProperty(FulltextIndexConstants.PROP_NULL_CHECK_ENABLED, true);
        IndexDefinition idxDefn = new IndexDefinition(root, builder.getNodeState(), "/foo");
    }

    @Test(expected = IllegalStateException.class)
    public void nullCheckEnabledWithRegex() {
        NodeBuilder rules = builder.child(INDEX_RULES);
        rules.child(TestUtil.NT_TEST);
        TestUtil.child(rules, "oak:TestNode/properties/prop2")
                .setProperty(PROP_NAME, ".*")
                .setProperty(FulltextIndexConstants.PROP_IS_REGEX, true)
                .setProperty(FulltextIndexConstants.PROP_NULL_CHECK_ENABLED, true);
        root = registerTestNodeType(builder).getNodeState();
        IndexDefinition idxDefn = new IndexDefinition(root, builder.getNodeState(), "/foo");
    }

    @Test
    public void nullCheckEnabledWithTestNode() {
        NodeBuilder rules = builder.child(INDEX_RULES);
        TestUtil.child(rules, "oak:TestNode/properties/prop2")
                .setProperty(PROP_NAME, "foo")
                .setProperty(FulltextIndexConstants.PROP_NULL_CHECK_ENABLED, true);
        root = registerTestNodeType(builder).getNodeState();
        IndexDefinition idxDefn = new IndexDefinition(root, builder.getNodeState(), "/foo");
        assertTrue(!idxDefn.getApplicableIndexingRule(TestUtil.NT_TEST).getNullCheckEnabledProperties().isEmpty());
    }

    @Test
    public void notNullCheckEnabledWithTestNode() {
        NodeBuilder rules = builder.child(INDEX_RULES);
        TestUtil.child(rules, "oak:TestNode/properties/prop2")
                .setProperty(PROP_NAME, "foo")
                .setProperty(FulltextIndexConstants.PROP_NOT_NULL_CHECK_ENABLED, true);
        root = registerTestNodeType(builder).getNodeState();
        IndexDefinition idxDefn = new IndexDefinition(root, builder.getNodeState(), "/foo");
        assertTrue(!idxDefn.getApplicableIndexingRule(TestUtil.NT_TEST).getNotNullCheckEnabledProperties().isEmpty());
    }

    //OAK-2477
    @Test
    public void testSuggestFrequency() {
        int suggestFreq = 40;
        //default config
        NodeBuilder indexRoot = builder;
        IndexDefinition idxDefn = new IndexDefinition(root, indexRoot.getNodeState(), "/foo");
        assertEquals("Default config", 10, idxDefn.getSuggesterUpdateFrequencyMinutes());

        //namespaced config shadows old method
        indexRoot = builder.child("shadowConfigRoot");
        indexRoot.setProperty(FulltextIndexConstants.SUGGEST_UPDATE_FREQUENCY_MINUTES, suggestFreq);
        indexRoot.child(FulltextIndexConstants.SUGGESTION_CONFIG);
        idxDefn = new IndexDefinition(root, indexRoot.getNodeState(), "/foo");
        assertEquals("Namespaced config node should shadow global config",
                10, idxDefn.getSuggesterUpdateFrequencyMinutes());

        //config for backward config
        indexRoot = builder.child("backwardCompatibilityRoot");
        indexRoot.setProperty(FulltextIndexConstants.SUGGEST_UPDATE_FREQUENCY_MINUTES, suggestFreq);
        idxDefn = new IndexDefinition(root, indexRoot.getNodeState(), "/foo");
        assertEquals("Backward compatibility config", suggestFreq, idxDefn.getSuggesterUpdateFrequencyMinutes());

        indexRoot = builder.child("indexRoot");
        indexRoot.child(FulltextIndexConstants.SUGGESTION_CONFIG)
                .setProperty(FulltextIndexConstants.SUGGEST_UPDATE_FREQUENCY_MINUTES, suggestFreq);
        idxDefn = new IndexDefinition(root, indexRoot.getNodeState(), "/foo");
        assertEquals("Set config", suggestFreq, idxDefn.getSuggesterUpdateFrequencyMinutes());
    }

    //OAK-2477
    @Test
    public void testSuggestAnalyzed() {
        //default config
        NodeBuilder indexRoot = builder;
        IndexDefinition idxDefn = new IndexDefinition(root, indexRoot.getNodeState(), "/foo");
        assertFalse("Default config", idxDefn.isSuggestAnalyzed());

        //namespaced config shadows old method
        indexRoot = builder.child("shadowConfigRoot");
        indexRoot.setProperty(FulltextIndexConstants.SUGGEST_ANALYZED, true);
        indexRoot.child(FulltextIndexConstants.SUGGESTION_CONFIG);
        idxDefn = new IndexDefinition(root, indexRoot.getNodeState(), "/foo");
        assertFalse("Namespaced config node should shadow global config", idxDefn.isSuggestAnalyzed());

        //config for backward config
        indexRoot = builder.child("backwardCompatibilityRoot");
        indexRoot.setProperty(FulltextIndexConstants.SUGGEST_ANALYZED, true);
        idxDefn = new IndexDefinition(root, indexRoot.getNodeState(), "/foo");
        assertTrue("Backward compatibility config", idxDefn.isSuggestAnalyzed());

        indexRoot = builder.child("indexRoot");
        indexRoot.child(FulltextIndexConstants.SUGGESTION_CONFIG)
                .setProperty(FulltextIndexConstants.SUGGEST_ANALYZED, true);
        idxDefn = new IndexDefinition(root, indexRoot.getNodeState(), "/foo");
        assertTrue("Set config", idxDefn.isSuggestAnalyzed());
    }

    @Test
    public void testSuggestEnabledOnNamedProp() {
        NodeBuilder rules = builder.child(INDEX_RULES);
        TestUtil.child(rules, "oak:TestNode/properties/prop2")
                .setProperty(PROP_NAME, "foo")
                .setProperty(FulltextIndexConstants.PROP_USE_IN_SUGGEST, true);
        root = registerTestNodeType(builder).getNodeState();
        IndexDefinition idxDefn = new IndexDefinition(root, builder.getNodeState(), "/foo");
        assertTrue(idxDefn.isSuggestEnabled());
    }

    @Test
    public void testSuggestEnabledOnRegexProp() {
        NodeBuilder rules = builder.child(INDEX_RULES);
        rules.child(TestUtil.NT_TEST);
        TestUtil.child(rules, "oak:TestNode/properties/prop2")
                .setProperty(PROP_NAME, ".*")
                .setProperty(FulltextIndexConstants.PROP_IS_REGEX, true)
                .setProperty(FulltextIndexConstants.PROP_USE_IN_SUGGEST, true);
        root = registerTestNodeType(builder).getNodeState();
        IndexDefinition idxDefn = new IndexDefinition(root, builder.getNodeState(), "/foo");
        assertTrue(idxDefn.isSuggestEnabled());
    }

    @Test
    public void testSuggestDisabled() {
        NodeBuilder rules = builder.child(INDEX_RULES);
        TestUtil.child(rules, "oak:TestNode/properties/prop2")
                .setProperty(PROP_NAME, "foo");
        root = registerTestNodeType(builder).getNodeState();
        IndexDefinition idxDefn = new IndexDefinition(root, builder.getNodeState(), "/foo");
        assertFalse(idxDefn.isSuggestEnabled());
    }

    @Test
    public void analyzedEnabledForBoostedField() {
        NodeBuilder rules = builder.child(INDEX_RULES);
        rules.child("nt:folder");
        TestUtil.child(rules, "nt:folder/properties/prop1")
                .setProperty(FulltextIndexConstants.FIELD_BOOST, 3.0)
                .setProperty(FulltextIndexConstants.PROP_NODE_SCOPE_INDEX, true);
        TestUtil.child(rules, "nt:folder/properties/prop2")
                .setProperty(FulltextIndexConstants.PROP_ANALYZED, true)
                .setProperty(FulltextIndexConstants.PROP_NODE_SCOPE_INDEX, true);
        TestUtil.child(rules, "nt:folder/properties/prop3")
                .setProperty(FulltextIndexConstants.PROP_PROPERTY_INDEX, true)
                .setProperty(FulltextIndexConstants.PROP_NODE_SCOPE_INDEX, true);

        IndexDefinition defn = new IndexDefinition(root, builder.getNodeState(), "/foo");

        IndexingRule rule1 = defn.getApplicableIndexingRule(asState(newNode("nt:folder")));
        assertNotNull(rule1);

        PropertyDefinition pd = rule1.getConfig("prop1");
        assertEquals(3.0f, pd.boost, 0);
        assertTrue("Analyzed should be assumed to be true for boosted fields", pd.analyzed);
        assertFalse(rule1.getConfig("prop3").analyzed);

        assertEquals(2, rule1.getNodeScopeAnalyzedProps().size());
    }

    @Test
    public void nodeFullTextIndexed_Regex() {
        NodeBuilder rules = builder.child(INDEX_RULES);
        rules.child("nt:folder");
        TestUtil.child(rules, "nt:folder/properties/prop1")
                .setProperty(PROP_NAME, ".*")
                .setProperty(FulltextIndexConstants.PROP_ANALYZED, true)
                .setProperty(FulltextIndexConstants.PROP_IS_REGEX, true);

        IndexDefinition defn = new IndexDefinition(root, builder.getNodeState(), "/foo");
        IndexingRule rule = defn.getApplicableIndexingRule(asState(newNode("nt:folder")));
        assertNotNull(rule);
        assertFalse(rule.isNodeFullTextIndexed());

        TestUtil.child(rules, "nt:folder/properties/prop1")
                .setProperty(FulltextIndexConstants.PROP_NODE_SCOPE_INDEX, true);
        defn = new IndexDefinition(root, builder.getNodeState(), "/foo");
        rule = defn.getApplicableIndexingRule(asState(newNode("nt:folder")));
        assertTrue(rule.isNodeFullTextIndexed());
        assertTrue(rule.indexesAllNodesOfMatchingType());
    }

    @Test
    public void nodeFullTextIndexed_Simple() {
        NodeBuilder rules = builder.child(INDEX_RULES);
        rules.child("nt:folder");
        TestUtil.child(rules, "nt:folder/properties/prop1")
                .setProperty(PROP_NAME, "foo")
                .setProperty(FulltextIndexConstants.PROP_ANALYZED, true);

        IndexDefinition defn = new IndexDefinition(root, builder.getNodeState(), "/foo");
        IndexingRule rule = defn.getApplicableIndexingRule(asState(newNode("nt:folder")));
        assertNotNull(rule);
        assertFalse(rule.isNodeFullTextIndexed());

        TestUtil.child(rules, "nt:folder/properties/prop1")
                .setProperty(FulltextIndexConstants.PROP_NODE_SCOPE_INDEX, true);
        defn = new IndexDefinition(root, builder.getNodeState(), "/foo");
        rule = defn.getApplicableIndexingRule(asState(newNode("nt:folder")));
        assertTrue(rule.isNodeFullTextIndexed());
        assertTrue(rule.indexesAllNodesOfMatchingType());
    }

    @Test
    public void nodeFullTextIndexed_Aggregates() {
        NodeBuilder rules = builder.child(INDEX_RULES);
        rules.child("nt:folder");
        TestUtil.child(rules, "nt:folder/properties/prop1")
                .setProperty(PROP_NAME, "foo")
                .setProperty(FulltextIndexConstants.PROP_ANALYZED, true);

        NodeBuilder aggregates = builder.child(FulltextIndexConstants.AGGREGATES);
        NodeBuilder aggFolder = aggregates.child("nt:folder");
        aggFolder.child("i1").setProperty(FulltextIndexConstants.AGG_PATH, "*");

        IndexDefinition defn = new IndexDefinition(root, builder.getNodeState(), "/foo");
        IndexingRule rule = defn.getApplicableIndexingRule(asState(newNode("nt:folder")));
        assertNotNull(rule);
        assertTrue(rule.isNodeFullTextIndexed());
        assertTrue(rule.indexesAllNodesOfMatchingType());
    }

    @Test
    public void nonIndexPropShouldHaveAllOtherConfigDisabled() {
        NodeBuilder rules = builder.child(INDEX_RULES);
        rules.child("nt:folder");
        TestUtil.child(rules, "nt:folder/properties/prop1")
                .setProperty(PROP_NAME, "foo")
                .setProperty(FulltextIndexConstants.PROP_INDEX, false)
                .setProperty(FulltextIndexConstants.PROP_USE_IN_SUGGEST, true)
                .setProperty(FulltextIndexConstants.PROP_USE_IN_SPELLCHECK, true)
                .setProperty(FulltextIndexConstants.PROP_NULL_CHECK_ENABLED, true)
                .setProperty(FulltextIndexConstants.PROP_NOT_NULL_CHECK_ENABLED, true)
                .setProperty(FulltextIndexConstants.PROP_USE_IN_EXCERPT, true)
                .setProperty(FulltextIndexConstants.PROP_NODE_SCOPE_INDEX, true)
                .setProperty(FulltextIndexConstants.PROP_ORDERED, true)
                .setProperty(FulltextIndexConstants.PROP_ANALYZED, true);
        IndexDefinition defn = new IndexDefinition(root, builder.getNodeState(), "/foo");
        IndexingRule rule = defn.getApplicableIndexingRule(asState(newNode("nt:folder")));
        assertNotNull(rule);

        PropertyDefinition pd = rule.getConfig("foo");
        //Assert that all other config is false if the index=false for any property
        assertFalse(pd.index);
        assertFalse(pd.nodeScopeIndex);
        assertFalse(pd.useInSuggest);
        assertFalse(pd.useInSpellcheck);
        assertFalse(pd.nullCheckEnabled);
        assertFalse(pd.notNullCheckEnabled);
        assertFalse(pd.stored);
        assertFalse(pd.ordered);
        assertFalse(pd.analyzed);

    }

    @Test
    public void costPerEntry() {
        builder.setProperty(FulltextIndexConstants.COMPAT_MODE, 2);
        IndexDefinition defn = new IndexDefinition(root, builder.getNodeState(), "/foo");
        assertEquals(1.0, defn.getCostPerEntry(), 0.0);
    }

    @Test
    public void sync() throws Exception {
        TestUtil.enableIndexingMode(builder, IndexingMode.SYNC);
        IndexDefinition idxDefn = new IndexDefinition(root, builder.getNodeState(), "/foo");
        assertTrue(idxDefn.isSyncIndexingEnabled());
    }

    @Test
    public void hasPersistedIndex() {
        assertFalse(IndexDefinition.hasPersistedIndex(builder.getNodeState()));
        builder.child(":status");
        assertTrue(IndexDefinition.hasPersistedIndex(builder.getNodeState()));
    }

    @Test
    public void uniqueIdForFreshIndex() {
        IndexDefinition defn = IndexDefinition.newBuilder(root, builder.getNodeState(), "/foo").build();
        assertEquals("0", defn.getUniqueId());

        builder.child(":status");
        defn = IndexDefinition.newBuilder(root, builder.getNodeState(), "/foo").build();
        assertNull(defn.getUniqueId());
    }

    @Test
    public void nodeTypeChange() {
        IndexDefinition defn = IndexDefinition.newBuilder(root, builder.getNodeState(), "/foo").build();
        NodeBuilder b2 = root.builder();
        TestUtil.registerNodeType(b2, TestUtil.TEST_NODE_TYPE);
        NodeState root2 = b2.getNodeState();


        NodeBuilder b3 = root.builder();
        b3.child("x");
        NodeState root3 = b3.getNodeState();

        assertFalse(defn.hasMatchingNodeTypeReg(root2));
        assertTrue(defn.hasMatchingNodeTypeReg(root3));
    }

    @Test
    public void uniqueIsSync() {
        IndexDefinitionBuilder defnb = new IndexDefinitionBuilder();
        defnb.indexRule("nt:base").property("foo").unique();

        IndexDefinition defn = IndexDefinition.newBuilder(root, defnb.build(), "/foo").build();
        assertTrue(defn.getApplicableIndexingRule("nt:base").getConfig("foo").sync);
        assertTrue(defn.getApplicableIndexingRule("nt:base").getConfig("foo").unique);
        assertTrue(defn.getApplicableIndexingRule("nt:base").getConfig("foo").propertyIndex);
    }

    @Test
    public void syncIsProperty() {
        IndexDefinitionBuilder defnb = new IndexDefinitionBuilder();
        defnb.indexRule("nt:base").property("foo").sync();

        IndexDefinition defn = IndexDefinition.newBuilder(root, defnb.build(), "/foo").build();
        assertTrue(defn.getApplicableIndexingRule("nt:base").getConfig("foo").sync);
        assertTrue(defn.getApplicableIndexingRule("nt:base").getConfig("foo").propertyIndex);
    }

    @Test
    public void syncPropertyDefinitions() {
        IndexDefinitionBuilder defnb = new IndexDefinitionBuilder();
        defnb.indexRule("nt:base").property("foo").sync();

        IndexDefinition defn = IndexDefinition.newBuilder(root, defnb.build(), "/foo").build();
        assertTrue(defn.hasSyncPropertyDefinitions());
    }

    //~----------------------------------< nodetype >


    final String testNodeTypeDefn = "[oak:TestMixA]\n" +
            "  mixin\n" +
            "\n" +
            "[oak:TestSuperType]\n" +
            "- * (UNDEFINED) multiple\n" +
            "\n" +
            "[oak:TestTypeA] > oak:TestSuperType\n" +
            "- * (UNDEFINED) multiple\n" +
            "\n" +
            "[oak:TestTypeB] > oak:TestSuperType, oak:TestMixA\n" +
            "- * (UNDEFINED) multiple";

    @Test
    public void nodeTypeIndexed() {
        TestUtil.registerNodeType(builder, testNodeTypeDefn);
        root = builder.getNodeState();


        IndexDefinitionBuilder defnb = new IndexDefinitionBuilder();
        defnb.nodeTypeIndex();
        defnb.indexRule("oak:TestSuperType");

        IndexDefinition defn = IndexDefinition.newBuilder(root, defnb.build(), "/foo").build();
        assertFalse(defn.hasSyncPropertyDefinitions());

        IndexingRule ruleSuper = getRule(defn, "oak:TestSuperType");
        assertNotNull(ruleSuper);
        assertTrue(defn.isPureNodeTypeIndex());
        assertTrue(ruleSuper.getConfig(JcrConstants.JCR_PRIMARYTYPE).propertyIndex);
        assertTrue(ruleSuper.getConfig(JcrConstants.JCR_MIXINTYPES).propertyIndex);
        assertTrue(ruleSuper.indexesAllNodesOfMatchingType());

        assertNotNull(getRule(defn, "oak:TestTypeA"));
        assertTrue(getRule(defn, "oak:TestTypeA").indexesAllNodesOfMatchingType());
        assertNotNull(getRule(defn, "oak:TestTypeB"));
        assertNull(getRule(defn, "oak:TestMixA"));
    }

    @Test
    public void nodeTypeIndexedSync() {
        TestUtil.registerNodeType(builder, testNodeTypeDefn);
        root = builder.getNodeState();

        IndexDefinitionBuilder defnb = new IndexDefinitionBuilder();
        defnb.nodeTypeIndex();
        defnb.indexRule("oak:TestSuperType").sync();

        IndexDefinition defn = IndexDefinition.newBuilder(root, defnb.build(), "/foo").build();
        assertTrue(defn.hasSyncPropertyDefinitions());

        IndexingRule ruleSuper = getRule(defn, "oak:TestSuperType");
        assertNotNull(ruleSuper);
        assertTrue(defn.isPureNodeTypeIndex());
        assertTrue(ruleSuper.getConfig(JcrConstants.JCR_PRIMARYTYPE).propertyIndex);
        assertTrue(ruleSuper.getConfig(JcrConstants.JCR_PRIMARYTYPE).sync);
        assertTrue(ruleSuper.getConfig(JcrConstants.JCR_MIXINTYPES).propertyIndex);
        assertTrue(ruleSuper.getConfig(JcrConstants.JCR_MIXINTYPES).sync);
        assertTrue(ruleSuper.indexesAllNodesOfMatchingType());
    }

    @Test
    public void nodeTypeIndexed_IgnoreOtherProps() {
        TestUtil.registerNodeType(builder, testNodeTypeDefn);
        root = builder.getNodeState();

        IndexDefinitionBuilder defnb = new IndexDefinitionBuilder();
        defnb.nodeTypeIndex();
        defnb.indexRule("oak:TestSuperType").sync();
        defnb.indexRule("oak:TestSuperType").property("foo").propertyIndex();


        IndexDefinition defn = IndexDefinition.newBuilder(root, defnb.build(), "/foo").build();

        IndexingRule ruleSuper = getRule(defn, "oak:TestSuperType");
        assertNotNull(ruleSuper);

        assertNull(ruleSuper.getConfig("foo"));
        assertNotNull(ruleSuper.getConfig(JcrConstants.JCR_PRIMARYTYPE));
        assertNotNull(ruleSuper.getConfig(JcrConstants.JCR_MIXINTYPES));
    }

    @Test
    public void nodeTypeIndexed_IgnoreAggregates() {
        TestUtil.registerNodeType(builder, testNodeTypeDefn);
        root = builder.getNodeState();

        IndexDefinitionBuilder defnb = new IndexDefinitionBuilder();
        defnb.nodeTypeIndex();
        defnb.indexRule("oak:TestSuperType").sync();
        defnb.aggregateRule("oak:TestSuperType").include("*");


        IndexDefinition defn = IndexDefinition.newBuilder(root, defnb.build(), "/foo").build();

        IndexingRule ruleSuper = getRule(defn, "oak:TestSuperType");
        assertNotNull(ruleSuper);

        assertNull(ruleSuper.getConfig("foo"));
        assertTrue(ruleSuper.getAggregate().getIncludes().isEmpty());
        assertNotNull(ruleSuper.getConfig(JcrConstants.JCR_PRIMARYTYPE));
        assertNotNull(ruleSuper.getConfig(JcrConstants.JCR_MIXINTYPES));
    }

    @Test
    public void nodeTypeIndex_mixin() {
        TestUtil.registerNodeType(builder, testNodeTypeDefn);
        root = builder.getNodeState();


        IndexDefinitionBuilder defnb = new IndexDefinitionBuilder();
        defnb.nodeTypeIndex();
        defnb.indexRule("oak:TestMixA");

        IndexDefinition defn = IndexDefinition.newBuilder(root, defnb.build(), "/foo").build();
        assertFalse(defn.hasSyncPropertyDefinitions());


        assertNotNull(getRule(defn, "oak:TestTypeB"));
        assertTrue(getRule(defn, "oak:TestTypeB").indexesAllNodesOfMatchingType());
        assertNotNull(getRule(defn, "oak:TestMixA"));
        assertTrue(getRule(defn, "oak:TestMixA").indexesAllNodesOfMatchingType());

        assertNull(getRule(defn, "oak:TestTypeA"));
        assertNull(getRule(defn, "oak:TestSuperType"));
    }

    @Test
    public void mixinAndPrimaryType() {
        TestUtil.registerNodeType(builder, testNodeTypeDefn);
        root = builder.getNodeState();


        IndexDefinitionBuilder defnb = new IndexDefinitionBuilder();
        defnb.indexRule("oak:TestMixA").property(JcrConstants.JCR_PRIMARYTYPE).propertyIndex();
        defnb.indexRule("oak:TestSuperType").property(JcrConstants.JCR_PRIMARYTYPE).propertyIndex().sync();

        IndexDefinition defn = IndexDefinition.newBuilder(root, defnb.build(), "/foo").build();

        IndexingRule a = getRule(defn, "oak:TestMixA");
        assertNotNull(a.getConfig(JcrConstants.JCR_PRIMARYTYPE));
        assertNotNull(a.getConfig(JcrConstants.JCR_MIXINTYPES));
        assertFalse(a.getConfig(JcrConstants.JCR_MIXINTYPES).sync);

        IndexingRule b = getRule(defn, "oak:TestSuperType");
        assertNotNull(b.getConfig(JcrConstants.JCR_PRIMARYTYPE));
        assertNotNull(b.getConfig(JcrConstants.JCR_MIXINTYPES));
        assertTrue(b.getConfig(JcrConstants.JCR_PRIMARYTYPE).sync);
        assertTrue(b.getConfig(JcrConstants.JCR_MIXINTYPES).sync);
    }

    @Test
    public void relativeNodeNames_None() {
        IndexDefinitionBuilder defnb = new IndexDefinitionBuilder();
        defnb.indexRule("nt:base").property("foo").propertyIndex();

        IndexDefinition defn = IndexDefinition.newBuilder(root, defnb.build(), "/foo").build();
        assertTrue(defn.getRelativeNodeNames().isEmpty());
        assertFalse(defn.indexesRelativeNodes());
    }

    @Test
    public void relativeNodeNames_RelativeProp() {
        IndexDefinitionBuilder defnb = new IndexDefinitionBuilder();
        defnb.indexRule("nt:base").property("jcr:content/foo").propertyIndex();
        defnb.indexRule("nt:base").property("bar").propertyIndex();

        IndexDefinition defn = IndexDefinition.newBuilder(root, defnb.build(), "/foo").build();
        assertThat(defn.getRelativeNodeNames(), containsInAnyOrder("jcr:content"));
        assertTrue(defn.indexesRelativeNodes());
    }

    @Test
    public void relativeNodeNames_Aggregate() {
        IndexDefinitionBuilder defnb = new IndexDefinitionBuilder();
        defnb.indexRule("nt:base").property("jcr:content/foo").propertyIndex();
        defnb.aggregateRule("nt:base").include("jcr:content/metadata");
        defnb.aggregateRule("nt:base").include("jcr:content/metadata/type/*");
        defnb.aggregateRule("nt:base").include("*");

        IndexDefinition defn = IndexDefinition.newBuilder(root, defnb.build(), "/foo").build();
        assertThat(defn.getRelativeNodeNames(), containsInAnyOrder("jcr:content", "metadata", "type"));
        assertTrue(defn.indexesRelativeNodes());
    }

    @Test
    public void regexAllProps() {
        IndexDefinitionBuilder builder = new IndexDefinitionBuilder();
        builder.indexRule("nt:base").property("p");
        builder.indexRule("nt:base").property("all", FulltextIndexConstants.REGEX_ALL_PROPS, true);

        IndexDefinition def = IndexDefinition.newBuilder(root, builder.build(), "/foo").build();
        IndexingRule rule = def.getApplicableIndexingRule(root);
        assertNotNull(rule);

        PropertyDefinition pd = rule.getConfig("p");
        assertNotNull(pd);
        assertFalse(pd.isRegexp);
        assertFalse(pd.relative);
        assertEquals(0, pd.ancestors.length);

        pd = rule.getConfig("all");
        assertNotNull(pd);
        assertTrue(pd.isRegexp);
        assertFalse(pd.relative);
        assertEquals(0, pd.ancestors.length);

        assertThat(rule.getAggregate().getIncludes(), is(empty()));
        assertFalse(rule.getAggregate().hasNodeAggregates());
        List<Aggregate.Matcher> matchers = rule.getAggregate()
                .createMatchers(new TestRoot("/"));
        assertThat(matchers, is(empty()));
        assertThat(def.getRelativeNodeNames(), is(empty()));
    }

    //TODO indexesAllNodesOfMatchingType - with nullCheckEnabled

    private static IndexingRule getRule(IndexDefinition defn, String typeName) {
        return defn.getApplicableIndexingRule(asState(newNode(typeName)));
    }

    private static NodeState asState(NodeBuilder nb) {
        return nb.getNodeState();
    }

    private static NodeBuilder newNode(String typeName) {
        NodeBuilder builder = EMPTY_NODE.builder();
        builder.setProperty(JcrConstants.JCR_PRIMARYTYPE, typeName, Type.NAME);
        return builder;
    }

    private static NodeBuilder newNode(String typeName, String mixins) {
        NodeBuilder builder = EMPTY_NODE.builder();
        builder.setProperty(JcrConstants.JCR_PRIMARYTYPE, typeName);
        builder.setProperty(JcrConstants.JCR_MIXINTYPES, Collections.singleton(mixins), Type.NAMES);
        return builder;
    }

    private static class TestRoot implements Aggregate.AggregateRoot {

        private final String path;

        public TestRoot(String path) {
            this.path = path;
        }

        @Override
        public void markDirty() {
        }

        @Override
        public String getPath() {
            return path;
        }
    }
}
