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

import org.apache.jackrabbit.oak.InitialContent;
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.ContentSession;
import org.apache.jackrabbit.oak.api.PropertyState;
import org.apache.jackrabbit.oak.api.Root;
import org.apache.jackrabbit.oak.api.Tree;
import org.apache.jackrabbit.oak.api.Type;
import org.apache.jackrabbit.oak.commons.PathUtils;
import org.apache.jackrabbit.oak.plugins.index.AsyncIndexUpdate;
import org.apache.jackrabbit.oak.plugins.index.property.Multiplexers;
import org.apache.jackrabbit.oak.plugins.index.property.PropertyIndexEditorProvider;
import org.apache.jackrabbit.oak.plugins.memory.MemoryNodeStore;
import org.apache.jackrabbit.oak.spi.mount.Mount;
import org.apache.jackrabbit.oak.spi.mount.MountInfoProvider;
import org.apache.jackrabbit.oak.spi.mount.Mounts;
import org.apache.jackrabbit.oak.spi.security.OpenSecurityProvider;
import org.apache.jackrabbit.oak.spi.state.NodeState;
import org.apache.jackrabbit.oak.spi.state.NodeStore;
import org.apache.jackrabbit.oak.spi.whiteboard.Whiteboard;
import org.apache.jackrabbit.oak.spi.whiteboard.WhiteboardUtils;
import org.jetbrains.annotations.Nullable;
import org.junit.Before;
import org.junit.Test;

import java.util.Arrays;
import java.util.concurrent.TimeUnit;
import java.util.function.Predicate;

import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;

public class MountsNodeCounterTest {

    private NodeStore nodeStore;

    private Root root;

    private MountInfoProvider mip;

    private Whiteboard wb;

    @Before
    public void before() throws Exception {
        ContentSession session = createRepository().login(null, null);
        root = session.getLatestRoot();
    }

    @Test
    public void testMultipleMounts() throws CommitFailedException {
        root.getTree("/oak:index/counter").setProperty("resolution", 1);
        root.commit();

        Tree rootTree = root.getTree("/");

        Tree apps = rootTree.addChild("apps");
        Tree libs = rootTree.addChild("libs");
        Tree content = rootTree.addChild("content");
        Tree nested = rootTree.addChild("nested");
        Tree nestedMount = nested.addChild("mount");
        Tree fragments = rootTree.addChild("var").addChild("fragments").addChild("oak:mount-libs");

        addChildren(apps, 100);
        addChildren(libs, 200);
        addChildren(content, 400);
        addChildren(nested, 800);
        addChildren(nestedMount, 1600);
        addChildren(fragments, 3200);

        root.commit();
        runAsyncIndex();

        // leaves:
        Mount defaultMount = mip.getDefaultMount();
        Mount libsMount = mip.getMountByName("libs");

        assertCountEquals(100, libsMount, "apps");
        assertCountEquals(200, libsMount, "libs");
        assertCountEquals(400, defaultMount, "content");
        assertCountEquals(800, defaultMount, "nested");
        assertCountEquals(1600, libsMount, "nested/mount");
        assertCountEquals(3200, libsMount, "var");
        assertCountEquals(3200, libsMount, "var/fragments");
        assertCountEquals(3200, libsMount, "var/fragments/oak:mount-libs");
        assertCountEquals(0, defaultMount, "var");
        assertCountEquals(0, defaultMount, "var/fragments");

        assertCountEquals(100 + 200 + 1600 + 3200, libsMount, "");
        assertCountEquals(1600, libsMount, "nested");
    }

    private void assertCountEquals(int expectedCount, Mount mount, String path) {
        String p = PathUtils.concat("/oak:index/counter", Multiplexers.getNodeForMount(mount, ":index"), path);
        NodeState s = nodeStore.getRoot();
        for (String element : PathUtils.elements(p)) {
            s = s.getChildNode(element);
            if (s == null) {
                if (expectedCount == 0) {
                    return;
                }
                fail("Can't find node " + p);
            }
        }
        PropertyState ps = s.getProperty(":cnt");
        if (ps == null) {
            if (expectedCount == 0) {
                return;
            }
            fail("There's no :cnt property on " + p);
        }
        long v = ps.getValue(Type.LONG);


        assertTrue("expected:<" + expectedCount + "> but was:<" + v + ">", Math.abs(expectedCount - v) < 10);
    }

    private static void addChildren(Tree tree, int count) {
        for (int i = 0; i < count; i++) {
            tree.addChild("n-" + i);
        }
    }

    protected ContentRepository createRepository() {
        Mounts.Builder builder = Mounts.newBuilder();
        builder.mount("libs", false, Arrays.asList("/var/fragments"), Arrays.asList("/apps", "/libs", "/nested/mount"));
        mip = builder.build();

        nodeStore = new MemoryNodeStore();
        Oak oak = new Oak(nodeStore)
                .with(new InitialContent())
                .with(new OpenSecurityProvider())
                .with(new PropertyIndexEditorProvider().with(mip))
                .with(new NodeCounterEditorProvider().with(mip))
                //Effectively disable async indexing auto run
                //such that we can control run timing as per test requirement
                .withAsyncIndexing("async", TimeUnit.DAYS.toSeconds(1));

        wb = oak.getWhiteboard();
        return oak.createContentRepository();
    }

    private void runAsyncIndex() {
        Runnable async = WhiteboardUtils.getService(wb, Runnable.class, new Predicate<Runnable>() {
            @Override
            public boolean test(@Nullable Runnable input) {
                return input instanceof AsyncIndexUpdate;
            }
        });
        assertNotNull(async);
        async.run();
        root.refresh();
    }

}
