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

import org.apache.jackrabbit.core.RepositoryContext;
import org.apache.jackrabbit.core.RepositoryImpl;
import org.apache.jackrabbit.core.config.RepositoryConfig;
import org.apache.jackrabbit.oak.api.CommitFailedException;
import org.apache.jackrabbit.oak.commons.PathUtils;
import org.apache.jackrabbit.oak.plugins.document.DocumentMK;
import org.apache.jackrabbit.oak.plugins.document.DocumentNodeStore;
import org.apache.jackrabbit.oak.segment.SegmentNodeStore;
import org.apache.jackrabbit.oak.segment.SegmentNodeStoreBuilders;
import org.apache.jackrabbit.oak.segment.memory.MemoryStore;
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.Assert;
import org.junit.Ignore;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.jcr.Node;
import javax.jcr.RepositoryException;
import javax.jcr.Session;
import javax.jcr.SimpleCredentials;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Random;

import static org.junit.Assert.fail;

@RunWith(Parameterized.class)
public class LongNameTest {

    private static final Logger LOG = LoggerFactory.getLogger(LongNameTest.class);

    @Parameterized.Parameters(name = "{0}")
    public static Collection<Object[]> data() {
        List<Object[]> params = new ArrayList<Object[]>();

        params.add(new Object[] { "Short parent, short name", 349, 150, false });
        params.add(new Object[] { "Short parent, long name", 349, 151, false });
        params.add(new Object[] { "Long parent, short name", 350, 150, false });
        params.add(new Object[] { "Long parent, long name", 350, 151, true });

        return params;
    }

    private final String parentPath;

    private final String nodeName;

    private final boolean shouldBeSkipped;

    @Rule
    public final TemporaryFolder crxRepo = new TemporaryFolder(new File("target"));

    public LongNameTest(String name, int parentPathLength, int nodeNameLength, boolean shouldBeSkipped) {
        this.parentPath = generatePath(parentPathLength);
        this.nodeName = generateNodeName(nodeNameLength);
        this.shouldBeSkipped = shouldBeSkipped;
    }

    @Test
    public void testMigrationToDocStore() throws IOException, CommitFailedException, RepositoryException {
        SegmentNodeStore src = SegmentNodeStoreBuilders.builder(new MemoryStore()).build();
        createNodes(src);

        DocumentNodeStore dst = new DocumentMK.Builder().getNodeStore();

        RepositorySidegrade sidegrade = new RepositorySidegrade(src, dst);
        sidegrade.setFilterLongNames(true);
        sidegrade.copy();

        NodeState parent = getParent(dst);
        Assert.assertTrue("Parent should exists", parent.exists());
        if (shouldBeSkipped) {
            Assert.assertFalse("Node shouldn't exists", parent.hasChildNode(nodeName));
        } else {
            Assert.assertTrue("Node should exists", parent.hasChildNode(nodeName));
        }
    }

    @Test
    public void testNodeOnDocStore() throws CommitFailedException {
        DocumentNodeStore docStore = new DocumentMK.Builder().getNodeStore();

        try {
            createNodes(docStore);
            if (shouldBeSkipped) {
                fail("It shouldn't be possible to create a node");
            }
        } catch (CommitFailedException e) {
            if (!shouldBeSkipped) {
                LOG.warn("Unexpected exception", e);
                fail("It should be possible to create a node");
            }
        }
    }

    @Test
    @Ignore
    public void testUpgradeToDocStore() throws IOException, CommitFailedException, RepositoryException {
        File root = crxRepo.newFolder();
        File source = new File(root, "source");
        source.mkdirs();
        RepositoryImpl sourceRepository = RepositoryContext.create(RepositoryConfig.install(source)).getRepository();
        Session session = sourceRepository.login(new SimpleCredentials("admin", "admin".toCharArray()));
        try {
            Node node = session.getRootNode();
            for (String s : PathUtils.elements(parentPath)) {
                node = node.addNode(s);
            }
            node.addNode(nodeName);
            session.save();
        } finally {
            session.logout();
        }
        sourceRepository.shutdown();

        DocumentNodeStore dst = new DocumentMK.Builder().getNodeStore();
        RepositoryContext ctx = RepositoryContext.create(RepositoryConfig.install(source));
        RepositoryUpgrade upgrade = new RepositoryUpgrade(ctx, dst);
        upgrade.setCheckLongNames(true);
        try {
            upgrade.copy(null);
            if (shouldBeSkipped) {
                fail("Jackrabbit2 Lucene index should be used to inform about the node with long name");
            }
        } catch (Exception e) {
            if (!shouldBeSkipped) {
                LOG.warn("Unexpected exception", e);
                fail("Upgrade should be successful");
            }
        }

        ctx.getRepository().shutdown();
        ctx = RepositoryContext.create(RepositoryConfig.install(source));
        upgrade = new RepositoryUpgrade(ctx, dst);
        upgrade.setCheckLongNames(false);
        upgrade.copy(null);

        NodeState parent = getParent(dst);
        Assert.assertTrue("Parent should exists", parent.exists());
        if (shouldBeSkipped) {
            Assert.assertFalse("Node shouldn't exists", parent.hasChildNode(nodeName));
        } else {
            Assert.assertTrue("Node should exists", parent.hasChildNode(nodeName));
        }
    }

    private void createNodes(NodeStore ns) throws CommitFailedException {
        NodeBuilder root = ns.getRoot().builder();
        NodeBuilder nb = root;
        for (String s : PathUtils.elements(parentPath)) {
            nb = nb.child(s);
        }
        nb.child(nodeName);
        ns.merge(root, EmptyHook.INSTANCE, CommitInfo.EMPTY);
    }

    private static String generatePath(int length) {
        if (length == 1) {
            return "/";
        }

        Random random = new Random();
        StringBuilder path = new StringBuilder();
        while (path.length() < length) {
            int remaining = length - path.length();
            if (remaining == 1) {
                path.append(generateNodeName(1));
            } else {
                path.append('/');
                remaining--;
                path.append(generateNodeName(1 + random.nextInt(Math.min(remaining, 150))));
            }
        }
        return path.toString();
    }

    private static String generateNodeName(int length) {
        Random random = new Random();
        StringBuilder nodeName = new StringBuilder();
        for (int i = 0; i < length; i++) {
            nodeName.append((char) ('a' + random.nextInt('z' - 'a')));
        }
        return nodeName.toString();
    }

    private NodeState getParent(NodeStore ns) {
        NodeState node = ns.getRoot();
        for (String s : PathUtils.elements(parentPath)) {
            node = node.getChildNode(s);
        }
        return node;
    }
}