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

import com.mongodb.ReadPreference;

import org.apache.jackrabbit.oak.api.CommitFailedException;
import org.apache.jackrabbit.oak.commons.PathUtils;
import org.apache.jackrabbit.oak.plugins.document.AbstractMongoConnectionTest;
import org.apache.jackrabbit.oak.plugins.document.Collection;
import org.apache.jackrabbit.oak.plugins.document.DocumentMK;
import org.apache.jackrabbit.oak.plugins.document.DocumentNodeStore;
import org.apache.jackrabbit.oak.plugins.document.LeaseCheckMode;
import org.apache.jackrabbit.oak.plugins.document.util.MongoConnection;
import org.apache.jackrabbit.oak.plugins.document.util.Utils;
import org.apache.jackrabbit.oak.spi.commit.CommitInfo;
import org.apache.jackrabbit.oak.spi.commit.EmptyHook;
import org.apache.jackrabbit.oak.spi.state.NodeBuilder;
import org.apache.jackrabbit.oak.spi.state.NodeState;
import org.apache.jackrabbit.oak.spi.state.NodeStore;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;

import static org.apache.jackrabbit.guava.common.collect.Iterables.*;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;

public class CacheInvalidationIT extends AbstractMongoConnectionTest {

    private DocumentNodeStore c1;
    private DocumentNodeStore c2;
    private int initialCacheSizeC1;

    @Before
    public void prepareStores() throws Exception {
        // TODO start with clusterNodeId 2, because 1 has already been
        // implicitly allocated in the base class
        c1 = createNS(2);
        c2 = createNS(3);
        initialCacheSizeC1 = getCurrentCacheSize(c1);
    }

    private int createScenario() throws CommitFailedException {
        //          a
        //        / | \
        //       /  c  \
        //      b       d
        //     /|\      |
        //    / | \     h
        //   e  f  g
        String[] paths = {
                "/a",
                "/a/c",
                "/a/b",
                "/a/b/e",
                "/a/b/f",
                "/a/b/g",
                "/a/d",
                "/a/d/h",
        };
        NodeBuilder root = getRoot(c1).builder();
        createTree(root, paths);
        c1.merge(root, EmptyHook.INSTANCE, CommitInfo.EMPTY);

        assertEquals(initialCacheSizeC1 + paths.length, getCurrentCacheSize(c1));

        runBgOps(c1, c2);
        return paths.length;
    }

    @Test
    public void testCacheInvalidation() throws CommitFailedException {
        final int totalPaths = createScenario();

        NodeBuilder b2 = getRoot(c2).builder();
        builder(b2, "/a/d").setProperty("foo", "bar");
        c2.merge(b2, EmptyHook.INSTANCE, CommitInfo.EMPTY);

        //Push pending changes at /a
        c2.runBackgroundOperations();

        //Refresh the head for c1
        c1.runBackgroundOperations();

        //Only 2 entries /a and /a/d would be invalidated
        // '/' would have been added to cache in start of backgroundRead
        //itself
        assertEquals(initialCacheSizeC1 + totalPaths - 2, size(ds(c1).getNodeDocumentCache().keys()));
    }

    @Test
    public void testCacheInvalidationHierarchicalNotExist()
            throws CommitFailedException {

        NodeBuilder b2 = getRoot(c2).builder();
        // we create x/other, so that x is known to have a child node
        b2.child("x").child("other");
        b2.child("y");
        c2.merge(b2, EmptyHook.INSTANCE, CommitInfo.EMPTY);
        c2.runBackgroundOperations();
        c1.runBackgroundOperations();

        // we check for the existence of "x/futureX", which
        // should create a negative entry in the cache
        NodeState x = getRoot(c1).getChildNode("x");
        assertTrue(x.exists());
        assertFalse(x.getChildNode("futureX").exists());
        // we don't check for the existence of "y/futureY"
        NodeState y = getRoot(c1).getChildNode("y");
        assertTrue(y.exists());

        // now we add both "futureX" and "futureY"
        // in the other cluster node
        b2.child("x").child("futureX").setProperty("z", "1");
        c2.merge(b2, EmptyHook.INSTANCE, CommitInfo.EMPTY);
        b2.child("y").child("futureY").setProperty("z", "2");
        c2.merge(b2, EmptyHook.INSTANCE, CommitInfo.EMPTY);
        
        c2.runBackgroundOperations();
        c1.runBackgroundOperations();

        // both nodes should now be visible
        assertTrue(getRoot(c1).getChildNode("y").getChildNode("futureY").exists());
        assertTrue(getRoot(c1).getChildNode("x").getChildNode("futureX").exists());

    }

    private int getCurrentCacheSize(DocumentNodeStore ds){
        return size(ds(ds).getNodeDocumentCache().keys());
    }

    private static void refreshHead(DocumentNodeStore store) {
        ds(store).find(Collection.NODES, Utils.getIdFromPath("/"), 0);
    }


    private static MongoDocumentStore ds(DocumentNodeStore ns) {
        return (MongoDocumentStore) ns.getDocumentStore();
    }

    private static void createTree(NodeBuilder node, String[] paths) {
        for (String path : paths) {
            createPath(node, path);
        }
    }

    private static NodeBuilder builder(NodeBuilder builder, String path) {
        for (String name : PathUtils.elements(path)) {
            builder = builder.getChildNode(name);
        }
        return builder;
    }

    private static void createPath(NodeBuilder node, String path) {
        for (String element : PathUtils.elements(path)) {
            node = node.child(element);
        }
    }

    private static NodeState getRoot(NodeStore store) {
        return store.getRoot();
    }

    @After
    public void closeStores() {
        if (c2 != null) {
            c2.dispose();
        }
        if (c1 != null) {
            c1.dispose();
        }
    }

    private static void runBgOps(DocumentNodeStore... stores) {
        for (DocumentNodeStore ns : stores) {
            ns.runBackgroundOperations();
        }
    }

    private DocumentNodeStore createNS(int clusterId) throws Exception {
        MongoConnection mc = connectionFactory.getConnection();
        DocumentNodeStore ns = new DocumentMK.Builder()
                          .setMongoDB(mc.getMongoClient(), mc.getDBName())
                          .setClusterId(clusterId)
                          //Set delay to 0 so that effect of changes are immediately reflected
                          .setAsyncDelay(0)
                          .setBundlingDisabled(true)
                          .setLeaseCheckMode(LeaseCheckMode.DISABLED)
                          .getNodeStore();
        // enforce primary read preference, otherwise test fails on a replica
        // set with a read preference configured to secondary.
        MongoTestUtils.setReadPreference(ns, ReadPreference.primary());
        return ns;
    }

}
