/*
 * 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.reference;

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

import javax.jcr.PropertyType;

import org.apache.jackrabbit.oak.api.CommitFailedException;
import org.apache.jackrabbit.oak.api.Type;
import org.apache.jackrabbit.oak.plugins.index.IndexUpdateProvider;
import org.apache.jackrabbit.oak.query.NodeStateNodeTypeInfoProvider;
import org.apache.jackrabbit.oak.query.QueryEngineSettings;
import org.apache.jackrabbit.oak.query.ast.NodeTypeInfo;
import org.apache.jackrabbit.oak.query.ast.NodeTypeInfoProvider;
import org.apache.jackrabbit.oak.query.ast.Operator;
import org.apache.jackrabbit.oak.query.ast.SelectorImpl;
import org.apache.jackrabbit.oak.query.index.FilterImpl;
import org.apache.jackrabbit.oak.spi.commit.CommitInfo;
import org.apache.jackrabbit.oak.spi.commit.EditorHook;
import org.apache.jackrabbit.oak.spi.mount.MountInfoProvider;
import org.apache.jackrabbit.oak.spi.mount.Mounts;
import org.apache.jackrabbit.oak.spi.query.Cursor;
import org.apache.jackrabbit.oak.spi.query.Filter;
import org.apache.jackrabbit.oak.spi.query.QueryIndex;
import org.apache.jackrabbit.oak.spi.state.NodeBuilder;
import org.apache.jackrabbit.oak.spi.state.NodeState;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;

import static org.apache.jackrabbit.guava.common.collect.ImmutableList.of;
import static org.apache.jackrabbit.guava.common.collect.Lists.newArrayList;
import static org.apache.jackrabbit.JcrConstants.JCR_UUID;
import static org.apache.jackrabbit.JcrConstants.NT_BASE;
import static org.apache.jackrabbit.oak.plugins.memory.PropertyStates.createProperty;
import static org.apache.jackrabbit.oak.InitialContentHelper.INITIAL_CONTENT;
import static org.apache.jackrabbit.oak.plugins.memory.PropertyValues.newReference;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.assertEquals;

public class ReferenceIndexTest {

    @Rule
    public ExpectedException thrown = ExpectedException.none();

    @Test
    public void basicReferenceHandling() throws Exception{
        NodeState root = INITIAL_CONTENT;

        NodeBuilder builder = root.builder();
        NodeState before = builder.getNodeState();

        builder.child("a").setProperty(createProperty("foo", "u1", Type.REFERENCE));
        builder.child("b").setProperty(createProperty("foo", "u1", Type.REFERENCE));
        builder.child("c").setProperty(createProperty("foo", "u2", Type.WEAKREFERENCE));

        NodeState after = builder.getNodeState();
        EditorHook hook = new EditorHook(
                new IndexUpdateProvider(new ReferenceEditorProvider()));

        NodeState indexed = hook.processCommit(before, after, CommitInfo.EMPTY);
        FilterImpl f = createFilter(indexed, NT_BASE);
        f.restrictProperty("*", Operator.EQUAL, newReference("u1"), PropertyType.REFERENCE);

        assertFilter(f, new ReferenceIndex(), indexed, of("/a", "/b"));

        FilterImpl f2 = createFilter(indexed, NT_BASE);
        f2.restrictProperty("*", Operator.EQUAL, newReference("u2"), PropertyType.WEAKREFERENCE);
        assertFilter(f2, new ReferenceIndex(), indexed, of("/c"));
    }

    @Test
    public void referenceHandlingWithMounts() throws Exception{
        NodeState root = INITIAL_CONTENT;

        NodeBuilder builder = root.builder();
        NodeState before = builder.getNodeState();

        builder.child("a").child("x").setProperty(createProperty("foo", "u1", Type.REFERENCE));
        builder.child("b").setProperty(createProperty("foo", "u1", Type.REFERENCE));
        builder.child("c").setProperty(createProperty("foo", "u1", Type.WEAKREFERENCE));

        builder.child("d").setProperty(createProperty("foo", "u2", Type.WEAKREFERENCE));
        builder.child("a").child("y").setProperty(createProperty("foo", "u1", Type.WEAKREFERENCE));

        NodeState after = builder.getNodeState();

        MountInfoProvider mip = Mounts.newBuilder()
                .mount("foo", "/a")
                .build();

        EditorHook hook = new EditorHook(
                new IndexUpdateProvider(new ReferenceEditorProvider().with(mip)));

        ReferenceIndex referenceIndex = new ReferenceIndex(mip);

        NodeState indexed = hook.processCommit(before, after, CommitInfo.EMPTY);
        FilterImpl f = createFilter(indexed, NT_BASE);
        f.restrictProperty("*", Operator.EQUAL, newReference("u1"), PropertyType.REFERENCE);

        // System.out.println(NodeStateUtils.toString(NodeStateUtils.getNode(indexed, "/oak:index/reference")));
        assertFilter(f, referenceIndex, indexed, of("/a/x", "/b"));

        FilterImpl f2 = createFilter(indexed, NT_BASE);
        f2.restrictProperty("*", Operator.EQUAL, newReference("u1"), PropertyType.WEAKREFERENCE);
        assertFilter(f2, referenceIndex, indexed, of("/c", "/a/y"));
    }

    @Test
    public void removeReferencedNode() throws Exception{
        NodeState root = INITIAL_CONTENT;

        NodeBuilder builder = root.builder();
        NodeState before = builder.getNodeState();

        builder.child("a").setProperty(createProperty(JCR_UUID, "u1", Type.STRING));
        builder.child("b").setProperty(createProperty("foo", "u1", Type.REFERENCE));

        NodeState after = builder.getNodeState();
        EditorHook hook = new EditorHook(
                new IndexUpdateProvider(new ReferenceEditorProvider()));

        NodeState indexed = hook.processCommit(before, after, CommitInfo.EMPTY);

        builder = indexed.builder();
        builder.getChildNode("a").remove();

        thrown.expect(CommitFailedException.class);
        thrown.expectMessage("OakIntegrity0001: Unable to delete referenced node: u1");
        hook.processCommit(indexed, builder.getNodeState(), CommitInfo.EMPTY);
    }

    @Test
    public void removeWeaklyReferencedNode() throws Exception{
        NodeState root = INITIAL_CONTENT;

        NodeBuilder builder = root.builder();
        NodeState before = builder.getNodeState();

        builder.child("a").setProperty(createProperty(JCR_UUID, "u1", Type.STRING));
        builder.child("b").setProperty(createProperty("foo", "u1", Type.WEAKREFERENCE));

        NodeState after = builder.getNodeState();
        EditorHook hook = new EditorHook(
                new IndexUpdateProvider(new ReferenceEditorProvider()));

        NodeState indexed = hook.processCommit(before, after, CommitInfo.EMPTY);

        builder = indexed.builder();
        builder.getChildNode("a").remove();

        hook.processCommit(indexed, builder.getNodeState(), CommitInfo.EMPTY);
    }

    @SuppressWarnings("Duplicates")
    private static FilterImpl createFilter(NodeState root, String nodeTypeName) {
        NodeTypeInfoProvider nodeTypes = new NodeStateNodeTypeInfoProvider(root);
        NodeTypeInfo type = nodeTypes.getNodeTypeInfo(nodeTypeName);
        SelectorImpl selector = new SelectorImpl(type, nodeTypeName);
        return new FilterImpl(selector, "SELECT * FROM [" + nodeTypeName + "]", new QueryEngineSettings());
    }

    private static List<String> assertFilter(Filter filter, QueryIndex queryIndex,
                                             NodeState indexed, List<String> expected) {
        Cursor cursor = queryIndex.query(filter, indexed);
        List<String> paths = newArrayList();
        while (cursor.hasNext()) {
            paths.add(cursor.next().getPath());
        }
        Collections.sort(paths);
        for (String p : expected) {
            assertTrue("Expected path " + p + " not found", paths.contains(p));
        }
        assertEquals("Result set size is different \nExpected: " +
                expected + "\nActual: " + paths, expected.size(), paths.size());
        return paths;
    }

}