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

import javax.jcr.RepositoryException;

import org.apache.jackrabbit.JcrConstants;
import org.apache.jackrabbit.oak.AbstractSecurityTest;
import org.apache.jackrabbit.oak.api.CommitFailedException;
import org.apache.jackrabbit.oak.api.Tree;
import org.apache.jackrabbit.oak.api.Type;
import org.apache.jackrabbit.oak.namepath.NamePathMapper;
import org.apache.jackrabbit.oak.spi.nodetype.NodeTypeConstants;
import org.apache.jackrabbit.oak.spi.version.VersionConstants;
import org.apache.jackrabbit.oak.util.NodeUtil;
import org.apache.jackrabbit.oak.plugins.tree.TreeUtil;
import org.jetbrains.annotations.NotNull;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;

import static java.util.Objects.requireNonNull;
import static org.apache.jackrabbit.JcrConstants.JCR_ISCHECKEDOUT;
import static org.apache.jackrabbit.JcrConstants.JCR_UUID;
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 ReadOnlyVersionManagerTest extends AbstractSecurityTest {

    protected Tree versionable;
    private String workspaceName;

    protected ReadOnlyVersionManager versionManager;

    @Override
    @Before
    public void before() throws Exception {
        super.before();

        NodeUtil node = new NodeUtil(root.getTree("/"));
        NodeUtil a = node.addChild("a", NodeTypeConstants.NT_OAK_UNSTRUCTURED);
        a.addChild("b", NodeTypeConstants.NT_OAK_UNSTRUCTURED).addChild("c", NodeTypeConstants.NT_OAK_UNSTRUCTURED);


        TreeUtil.addMixin(a.getTree(), JcrConstants.MIX_VERSIONABLE, root.getTree(NodeTypeConstants.NODE_TYPES_PATH), null);
        root.commit();

        versionable = root.getTree("/a");

        // force the creation of a version that has a frozen node
        versionable.setProperty(JCR_ISCHECKEDOUT, Boolean.FALSE, Type.BOOLEAN);
        root.commit();
        versionable.setProperty(JCR_ISCHECKEDOUT, Boolean.TRUE, Type.BOOLEAN);
        root.commit();

        versionManager = ReadOnlyVersionManager.getInstance(root, NamePathMapper.DEFAULT);
        workspaceName = root.getContentSession().getWorkspaceName();
    }

    @Override
    @After
    public void after() throws Exception {
        try {
            root.refresh();

            Tree a = root.getTree("/a");
            if (a.exists()) {
                a.remove();
                root.commit();
            }
        } finally {
            super.after();
        }
    }

    @Test
    public void testIsVersionStoreTree() throws Exception {
        assertFalse(ReadOnlyVersionManager.isVersionStoreTree(root.getTree("/")));
        assertFalse(ReadOnlyVersionManager.isVersionStoreTree(root.getTree("/a")));
        assertFalse(ReadOnlyVersionManager.isVersionStoreTree(root.getTree("/a/b/c")));

        assertTrue(ReadOnlyVersionManager.isVersionStoreTree(root.getTree(VersionConstants.VERSION_STORE_PATH)));

        Tree versionHistory = versionManager.getVersionHistory(root.getTree("/a"));
        assertNotNull(versionHistory);
        assertFalse(ReadOnlyVersionManager.isVersionStoreTree(versionHistory));
        assertTrue(ReadOnlyVersionManager.isVersionStoreTree(versionHistory.getParent()));
    }

    @Test
    public void testGetVersionable() throws Exception {
        Tree versionHistory = requireNonNull(versionManager.getVersionHistory(root.getTree("/a")));
        assertVersionable("/a", versionHistory);

        Tree rootVersion = versionHistory.getChild(JcrConstants.JCR_ROOTVERSION);
        assertVersionable("/a", rootVersion);

        Tree baseVersion = requireNonNull(versionManager.getBaseVersion(versionable));
        assertVersionable("/a", baseVersion);

        Tree frozen = baseVersion.getChild(VersionConstants.JCR_FROZENNODE);
        assertVersionable("/a", frozen);

        Tree frozenB = frozen.getChild("b");
        assertVersionable("/a/b", frozenB);

        Tree frozenC = frozenB.getChild("c");
        assertVersionable("/a/b/c", frozenC);
    }


    private void assertVersionable(@NotNull String expectedPath, @NotNull Tree versionTree) {
        String p = versionTree.getPath();
        assertTrue(p, versionTree.exists());

        Tree versionable = versionManager.getVersionable(versionTree, workspaceName);
        assertNotNull(p, versionable);
        assertEquals(p, expectedPath, versionable.getPath());
    }

    @Test
    public void testGetVersionableForNonVersionTree() throws Exception {
        assertNull(versionManager.getVersionable(versionManager.getVersionStorage(), workspaceName));
        assertNull(versionManager.getVersionable(versionable, workspaceName));
        assertNull(versionManager.getVersionable(root.getTree("/"), workspaceName));
    }

    @Test
    public void testGetVersionableMissingPathProperty() throws Exception {
        Tree versionHistory = requireNonNull(versionManager.getVersionHistory(root.getTree("/a")));
        versionHistory.removeProperty(workspaceName);

        assertNull(versionManager.getVersionable(versionHistory, workspaceName));
        assertNull(versionManager.getVersionable(versionHistory.getChild(JcrConstants.JCR_ROOTVERSION), workspaceName));
    }

    @Test
    public void testGetVersionableNonExistingWorkspace() throws Exception {
        Tree versionHistory = requireNonNull(versionManager.getVersionHistory(root.getTree("/a")));

        assertNull(versionManager.getVersionable(versionHistory, "nonExistingWorkspaceName"));
        assertNull(versionManager.getVersionable(versionHistory.getChild(JcrConstants.JCR_ROOTVERSION), "nonExistingWorkspaceName"));
    }

    @Test
    public void testGetVersionableTargetRemoved() throws Exception {
        Tree baseVersion = requireNonNull(versionManager.getBaseVersion(versionable));

        versionable.remove();
        root.commit();

        Tree t = versionManager.getVersionable(baseVersion, workspaceName);
        assertNotNull(t);
        assertFalse(t.exists());
    }

    @Test
    public void testRemoveEmptyHistoryAfterRemovingVersionable() throws RepositoryException, CommitFailedException {
        NodeUtil node = new NodeUtil(root.getTree("/"));
        NodeUtil testVersionable = node.addChild("testVersionable", NodeTypeConstants.NT_OAK_UNSTRUCTURED);
        TreeUtil.addMixin(testVersionable.getTree(), JcrConstants.MIX_VERSIONABLE, root.getTree(NodeTypeConstants.NODE_TYPES_PATH), null);
        root.commit();

        String historyPath = versionManager.getVersionHistory(testVersionable.getTree()).getPath();
        assertTrue(root.getTree(historyPath).exists());

        testVersionable.getTree().remove();
        root.commit();

        assertFalse(root.getTree(historyPath).exists());
    }

    @Test
    public void testPreserveNonEmptyHistoryAfterRemovingVersionable() throws RepositoryException, CommitFailedException {
        String historyPath = versionManager.getVersionHistory(versionable).getPath();
        assertTrue(root.getTree(historyPath).exists());

        versionable.remove();
        root.commit();

        assertTrue(root.getTree(historyPath).exists());
    }

    @Test
    public void testPreserveHistoryAfterMovingVersionable() throws RepositoryException, CommitFailedException {
        NodeUtil node = new NodeUtil(root.getTree("/"));
        NodeUtil testVersionable = node.addChild("testVersionable", NodeTypeConstants.NT_OAK_UNSTRUCTURED);
        TreeUtil.addMixin(testVersionable.getTree(), JcrConstants.MIX_VERSIONABLE, root.getTree(NodeTypeConstants.NODE_TYPES_PATH), null);
        root.commit();

        Tree history = versionManager.getVersionHistory(testVersionable.getTree());
        assertTrue(history.exists());
        String historyUuid = history.getProperty(JCR_UUID).getValue(Type.STRING);

        assertTrue(root.move("/testVersionable", "/testVersionable2"));
        root.commit();

        history = versionManager.getVersionHistory(root.getTree("/testVersionable2"));
        assertTrue(history.exists());
        assertEquals(historyUuid, history.getProperty(JCR_UUID).getValue(Type.STRING));
    }

    @Test
    public void testCheckedOutNonVersionable() throws RepositoryException, CommitFailedException {
        NodeUtil node = new NodeUtil(root.getTree("/"));
        node.addChild("nonVersionable", NodeTypeConstants.NT_OAK_UNSTRUCTURED);
        root.commit();

        Tree nonVersionable = root.getTree("/nonVersionable");
        nonVersionable.setProperty(JCR_ISCHECKEDOUT, Boolean.FALSE, Type.BOOLEAN);
        root.commit();

        assertTrue(versionManager.isCheckedOut(nonVersionable));
    }}
