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

import org.apache.jackrabbit.test.NotExecutableException;

import javax.jcr.version.VersionException;
import javax.jcr.version.VersionManager;
import javax.jcr.Node;
import javax.jcr.RepositoryException;
import javax.jcr.InvalidItemStateException;
import javax.jcr.NoSuchWorkspaceException;
import javax.jcr.Property;
import javax.jcr.Value;
import javax.jcr.MergeException;
import javax.jcr.Session;
import javax.jcr.Repository;
import javax.jcr.lock.LockException;

/**
 * <code>MergeNodeTest</code> contains tests dealing with general merge node
 * calls.
 *
 * @test
 * @sources MergeNodeTest.java
 * @executeClass org.apache.jackrabbit.test.api.version.MergeNodeTest
 * @keywords versioning
 */

public class MergeNodeTest extends AbstractMergeTest {

    /**
     * node to merge
     */
    Node nodeToMerge;

    protected void setUp() throws Exception {
        super.setUp();

        nodeToMerge = testRootNodeW2.getNode(nodeName1);
        // node has to be checked out while merging
        VersionManager versionManager = nodeToMerge.getSession().getWorkspace().getVersionManager();
        versionManager.checkout(nodeToMerge.getPath());

    }

    protected void tearDown() throws Exception {
        nodeToMerge = null;
        super.tearDown();
    }

    /**
     * Node.merge(): InvalidItemStateException if unsaved changes within the
     * current Session<br>
     */
    public void testMergeNodeWithUnsavedStates() throws RepositoryException {
        // set a property and do not save workspace
        nodeToMerge.setProperty(propertyName1, CHANGED_STRING);
        try {
            nodeToMerge.merge(workspace.getName(), false);
            fail("InvalidItemStateException if unsaved changes within the current Session was expected.");
        } catch (InvalidItemStateException e) {
            // success
        }
    }

    /**
     * VersionManager.merge(): InvalidItemStateException if unsaved changes within the
     * current Session<br>
     */
    public void testMergeNodeWithUnsavedStatesJcr2() throws RepositoryException {
        // set a property and do not save workspace
        nodeToMerge.setProperty(propertyName1, CHANGED_STRING);
        try {
            nodeToMerge.getSession().getWorkspace().getVersionManager().merge(
                    nodeToMerge.getPath(), workspace.getName(), false);
            fail("InvalidItemStateException if unsaved changes within the current Session was expected.");
        } catch (InvalidItemStateException e) {
            // success
        }
    }

    /**
     * Perform a merge on a node with a unkwnown workspacename
     */
    public void testMergeUnknownWorkspaceName() throws RepositoryException {
        try {
            nodeToMerge.merge(getNonExistingWorkspaceName(superuser), false);
        } catch (NoSuchWorkspaceException e) {
            // success expected exception
        }
    }

    /**
     * Perform a merge on a node with a unkwnown workspacename
     */
    public void testMergeUnknownWorkspaceNameJcr2() throws RepositoryException {
        try {
            nodeToMerge.getSession().getWorkspace().getVersionManager().merge(
                    nodeToMerge.getPath(), getNonExistingWorkspaceName(superuser), false);
        } catch (NoSuchWorkspaceException e) {
            // success expected exception
        }
    }

    /**
     * Node.merge(): If this node does not have a corresponding node in the
     * indicated workspace <br> then the merge method returns quietly and no
     * changes are made.<br>
     */
    public void testMergeNodeNonCorrespondingNode() throws RepositoryException {
        // create new node - this node has no corresponding node in default workspace
        Node subNode = nodeToMerge.addNode(nodeName3, versionableNodeType);
        subNode.setProperty(propertyName1, CHANGED_STRING);
        superuserW2.save();
        subNode.checkin();

        subNode.merge(workspace.getName(), true);
        assertTrue(subNode.getProperty(propertyName1).getString().equals(CHANGED_STRING));
    }

    /**
     * VersionManager.merge(): If this node does not have a corresponding node in the
     * indicated workspace <br> then the merge method returns quietly and no
     * changes are made.<br>
     */
    public void testMergeNodeNonCorrespondingNodeJcr2() throws RepositoryException {
        // create new node - this node has no corresponding node in default workspace
        Node subNode = nodeToMerge.addNode(nodeName3, versionableNodeType);
        subNode.setProperty(propertyName1, CHANGED_STRING);
        superuserW2.save();
        VersionManager versionManager = subNode.getSession().getWorkspace().getVersionManager();
        String path = subNode.getPath();
        versionManager.checkin(path);

        versionManager.merge(path, workspace.getName(), true);
        assertTrue(subNode.getProperty(propertyName1).getString().equals(CHANGED_STRING));
    }

    /**
     * Node.merge(): versionable subNode N checked-in: If V is neither a
     * successor of, predecessor of, nor identical with V', then the merge
     * result for N is failed<br>
     */
    public void testMergeNodeVersionAmbiguous() throws RepositoryException {
        // create 2 independent versions for a node and its corresponding node
        // so merge fails for this node

        // default workspace
        Node originalNode = testRootNode.getNode(nodeName1);
        originalNode.checkout();
        originalNode.checkin();

        // second workspace
        nodeToMerge.checkin();

        // "merge" the clonedNode with the newNode from the default workspace
        // besteffort set to false to stop at the first failure
        try {
            nodeToMerge.checkout();
            nodeToMerge.merge(workspace.getName(), false);
            fail("Node has ambigous versions. Merge must throw a MergeException");
        } catch (MergeException e) {
            // success if the merge exception thrown
        }
    }

    /**
     * VersionManager.merge(): versionable subNode N checked-in: If V is neither a
     * successor of, predecessor of, nor identical with V', then the merge
     * result for N is failed<br>
     */
    public void testMergeNodeVersionAmbiguousJcr2() throws RepositoryException {
        // create 2 independent versions for a node and its corresponding node
        // so merge fails for this node

        // default workspace
        Node originalNode = testRootNode.getNode(nodeName1);
        VersionManager vmWsp1 = originalNode.getSession().getWorkspace().getVersionManager();
        String originalPath = originalNode.getPath();
        vmWsp1.checkout(originalPath);
        vmWsp1.checkin(originalPath);

        // second workspace
        VersionManager vmWsp2 = nodeToMerge.getSession().getWorkspace().getVersionManager();
        String path = nodeToMerge.getPath();
        vmWsp2.checkin(path);

        // "merge" the clonedNode with the newNode from the default workspace
        // besteffort set to false to stop at the first failure
        try {
            vmWsp2.checkout(path);
            vmWsp2.merge(path, workspace.getName(), false);
            fail("Node has ambigous versions. Merge must throw a MergeException");
        } catch (MergeException e) {
            // success if the merge exception thrown
        }
    }

    /**
     * Node.merge(): bestEffort is true > any merge-failure (represented by the
     * version in the workspace) is reported in the jcrMergeFailed property<br>
     */
    public void testMergeNodeBestEffortTrueCheckMergeFailedProperty() throws RepositoryException {
        // create 2 independent versions for a node and its corresponding node
        // so merge fails for this node

        // default workspace
        Node originalNode = testRootNode.getNode(nodeName1);
        originalNode.checkout();
        originalNode.checkin();

        // second workspace
        nodeToMerge.checkin();

        // "merge" the clonedNode with the newNode from the default workspace
        // besteffort set to true to report all failures
        nodeToMerge.checkout();
        nodeToMerge.merge(workspace.getName(), true);

        // success merge exception was raised as expected
        // jcrMergeFailed should contains reference to the V' as it is a different branche
        String expectedReferenceUUID = originalNode.getBaseVersion().getUUID();
        Property mergeFailedProperty = nodeToMerge.getProperty(jcrMergeFailed);
        Value[] references = mergeFailedProperty.getValues();
        boolean referenceFound = false;
        if (references != null) {
            for (int i = 0; i < references.length; i++) {
                String referenceUUID = references[i].getString();
                if (referenceUUID.equals(expectedReferenceUUID)) {
                    referenceFound = true;
                    break; // it's not necessary to loop thru all the references
                }
            }

            assertTrue("reference to expected version that give the failure wasnt found in the mergeFailed", referenceFound);
        }
    }

    /**
     * VersionManager.merge(): bestEffort is true > any merge-failure (represented by the
     * version in the workspace) is reported in the jcrMergeFailed property<br>
     */
    public void testMergeNodeBestEffortTrueCheckMergeFailedPropertyJcr2() throws RepositoryException {
        // create 2 independent versions for a node and its corresponding node
        // so merge fails for this node

        // default workspace
        Node originalNode = testRootNode.getNode(nodeName1);
        VersionManager vmWsp1 = originalNode.getSession().getWorkspace().getVersionManager();
        String originalPath = originalNode.getPath();
        vmWsp1.checkout(originalPath);
        vmWsp1.checkin(originalPath);

        // second workspace
        VersionManager vmWsp2 = nodeToMerge.getSession().getWorkspace().getVersionManager();
        String path = nodeToMerge.getPath();
        vmWsp2.checkin(path);

        // "merge" the clonedNode with the newNode from the default workspace
        // besteffort set to true to report all failures
        vmWsp2.checkout(path);
        vmWsp2.merge(path, workspace.getName(), true);

        // success merge exception was raised as expected
        // jcrMergeFailed should contains reference to the V' as it is a different branche
        String expectedReferenceUUID = originalNode.getBaseVersion().getUUID();
        Property mergeFailedProperty = nodeToMerge.getProperty(jcrMergeFailed);
        Value[] references = mergeFailedProperty.getValues();
        boolean referenceFound = false;
        if (references != null) {
            for (int i = 0; i < references.length; i++) {
                String referenceUUID = references[i].getString();
                if (referenceUUID.equals(expectedReferenceUUID)) {
                    referenceFound = true;
                    break; // it's not necessary to loop thru all the references
                }
            }

            assertTrue("reference to expected version that give the failure wasnt found in the mergeFailed", referenceFound);
        }
    }

    /**
     * if mergeFailedProperty is present > VersionException<br>
     */
    public void disable_testMergeNodeForceFailure() throws RepositoryException {
        // create 2 independent versions for a node and its corresponding node
        // so merge fails for this node

        // default workspace
        Node originalNode = testRootNode.getNode(nodeName1);
        originalNode.checkout();
        originalNode.checkin();

        // second workspace
        nodeToMerge.checkin();

        // "merge" the clonedNode with the newNode from the default workspace
        // besteffort set to true to report all failures
        nodeToMerge.checkout();
        nodeToMerge.merge(workspace.getName(), true);

        try {
            nodeToMerge.merge(workspace.getName(), true);
            fail("Merge failed for node in earlier merge operations. Because the mergeFailedProperty is present, merge must throw a VersionException");
        } catch (VersionException e) {
            // success version exception expected
        }
    }

    /**
     * if mergeFailedProperty is present > VersionException<br>
     */
    public void disable_testMergeNodeForceFailureJcr2() throws RepositoryException {
        // create 2 independent versions for a node and its corresponding node
        // so merge fails for this node

        // default workspace
        Node originalNode = testRootNode.getNode(nodeName1);
        VersionManager vmWsp1 = originalNode.getSession().getWorkspace().getVersionManager();
        String originalPath = originalNode.getPath();
        vmWsp1.checkout(originalPath);
        vmWsp1.checkin(originalPath);

        // second workspace
        VersionManager vmWsp2 = nodeToMerge.getSession().getWorkspace().getVersionManager();
        String path = nodeToMerge.getPath();
        vmWsp2.checkin(path);

        // "merge" the clonedNode with the newNode from the default workspace
        // besteffort set to true to report all failures
        vmWsp2.checkout(path);
        vmWsp2.merge(path, workspace.getName(), true);

        try {
            vmWsp2.merge(path, workspace.getName(), true);
            fail("Merge failed for node in earlier merge operations. Because the mergeFailedProperty is present, merge must throw a VersionException");
        } catch (VersionException e) {
            // success version exception expected
        }
    }

    /**
     * Node.merge(): bestEffort is false and any merge fails a MergeException is
     * thrown.<br>
     */
    public void testMergeNodeBestEffortFalse() throws RepositoryException {
        /// create successor versions for a node
        // so merge fails for this node

        // default workspace
        Node originalNode = testRootNode.getNode(nodeName1);
        originalNode.checkout();
        originalNode.checkin();

        // "merge" the clonedNode with the newNode from the default workspace
        // merge, besteffort set to false
        try {
            nodeToMerge.merge(workspace.getName(), false);
            fail("bestEffort is false and any merge should throw a MergeException.");
        } catch (MergeException e) {
            // successful
        }
    }

    /**
     * VersionManager.merge(): bestEffort is false and any merge fails a MergeException is
     * thrown.<br>
     */
    public void testMergeNodeBestEffortFalseJcr2() throws RepositoryException {
        /// create successor versions for a node
        // so merge fails for this node

        // default workspace
        Node originalNode = testRootNode.getNode(nodeName1);
        VersionManager vmWsp1 = originalNode.getSession().getWorkspace().getVersionManager();
        String originalPath = originalNode.getPath();
        vmWsp1.checkout(originalPath);
        vmWsp1.checkin(originalPath);

        // "merge" the clonedNode with the newNode from the default workspace
        // merge, besteffort set to false
        try {
            nodeToMerge.getSession().getWorkspace().getVersionManager().merge(
                    nodeToMerge.getPath(), workspace.getName(), false);
            fail("bestEffort is false and any merge should throw a MergeException.");
        } catch (MergeException e) {
            // successful
        }
    }

    /**
     * A MergeVersionException is thrown if bestEffort is false and a
     * versionable node is encountered whose corresponding node's base version
     * is on a divergent branch from this node's base version.
     */
    public void testMergeNodeBestEffortFalseAmbiguousVersions() throws RepositoryException {
        /// create 2 independent base versions for a node and its corresponding node
        // so merge fails for this node

        // default workspace
        Node originalNode = testRootNode.getNode(nodeName1);
        originalNode.checkout();
        originalNode.checkin();

        // second workspace
        nodeToMerge.checkin();

        // "merge" the clonedNode with the newNode from the default workspace
        nodeToMerge.checkout();

        // merge, besteffort set to false
        try {
            nodeToMerge.merge(workspace.getName(), false);
            fail("BestEffort is false and corresponding node's version is ambiguous. Merge should throw a MergeException.");
        } catch (MergeException e) {
            // successful
        }
    }

    /**
     * A MergeVersionException is thrown if bestEffort is false and a
     * versionable node is encountered whose corresponding node's base version
     * is on a divergent branch from this node's base version.
     */
    public void testMergeNodeBestEffortFalseAmbiguousVersionsJcr2() throws RepositoryException {
        /// create 2 independent base versions for a node and its corresponding node
        // so merge fails for this node

        // default workspace
        Node originalNode = testRootNode.getNode(nodeName1);
        VersionManager vmWsp1 = originalNode.getSession().getWorkspace().getVersionManager();
        String originalPath = originalNode.getPath();
        vmWsp1.checkout(originalPath);
        vmWsp1.checkin(originalPath);

        // second workspace
        VersionManager vmWsp2 = nodeToMerge.getSession().getWorkspace().getVersionManager();
        String path = nodeToMerge.getPath();
        vmWsp2.checkin(path);

        // "merge" the clonedNode with the newNode from the default workspace
        vmWsp2.checkout(path);

        // merge, besteffort set to false
        try {
            vmWsp2.merge(path, workspace.getName(), false);
            fail("BestEffort is false and corresponding node's version is ambiguous. Merge should throw a MergeException.");
        } catch (MergeException e) {
            // successful
        }
    }

    /**
     * Tests if a {@link LockException} is thrown when merge is called on a
     * locked node.
     * @throws NotExecutableException if repository does not support locking.
     */
    public void disable_testMergeLocked()
            throws NotExecutableException, RepositoryException {

        if (!isSupported(Repository.OPTION_LOCKING_SUPPORTED)) {
            throw new NotExecutableException("Locking is not supported.");
        }

        // try to make nodeToMerge lockable if it is not
        ensureMixinType(nodeToMerge, mixLockable);
        nodeToMerge.getParent().save();

        // lock the node
        // remove first slash of path to get rel path to root
        String pathRelToRoot = nodeToMerge.getPath().substring(1);
        // access node through another session to lock it
        Session session2 = getHelper().getSuperuserSession();
        try {
            Node node2 = session2.getRootNode().getNode(pathRelToRoot);
            node2.lock(false, false);

            try {
                nodeToMerge.merge(workspace.getName(), false);
                fail("merge must throw a LockException if applied on a " +
                        "locked node");
            } catch (LockException e) {
                // success
            }

            node2.unlock();
        } finally {
            session2.logout();
        }
    }

    /**
     * Tests if a {@link LockException} is thrown when merge is called on a
     * locked node.
     * @throws NotExecutableException if repository does not support locking.
     */
    public void disable_testMergeLockedJcr2()
            throws NotExecutableException, RepositoryException {

        if (!isSupported(Repository.OPTION_LOCKING_SUPPORTED)) {
            throw new NotExecutableException("Locking is not supported.");
        }

        // try to make nodeToMerge lockable if it is not
        ensureMixinType(nodeToMerge, mixLockable);
        nodeToMerge.getParent().getSession().save();

        // lock the node
        // remove first slash of path to get rel path to root
        String pathRelToRoot = nodeToMerge.getPath().substring(1);
        // access node through another session to lock it
        Session session2 = getHelper().getSuperuserSession();
        try {
            Node node2 = session2.getRootNode().getNode(pathRelToRoot);
            node2.getSession().getWorkspace().getLockManager().lock(node2.getPath(), false, false, 60, "");

            try {
                nodeToMerge.getSession().getWorkspace().getVersionManager().merge(
                        nodeToMerge.getPath(), workspace.getName(), false);
                fail("merge must throw a LockException if applied on a " +
                        "locked node");
            } catch (LockException e) {
                // success
            }

            node2.getSession().getWorkspace().getLockManager().unlock(node2.getPath());
        } finally {
            session2.logout();
        }
    }

    /**
     * initialize a versionable node on default and second workspace
     */
    protected void initNodes() throws RepositoryException {

        VersionManager versionManager = testRootNode.getSession().getWorkspace().getVersionManager();

        // create a versionable node
        // nodeName1
        Node topVNode = testRootNode.addNode(nodeName1, versionableNodeType);
        topVNode.setProperty(propertyName1, topVNode.getName());
        String path = topVNode.getPath();

        // save default workspace
        testRootNode.getSession().save();
        versionManager.checkin(path);
        versionManager.checkout(path);

        log.println("test nodes created successfully on " + workspace.getName());

        // clone the newly created node from src workspace into second workspace
        // todo clone on testRootNode does not seem to work.
        // workspaceW2.clone(workspace.getName(), testRootNode.getPath(), testRootNode.getPath(), true);
        workspaceW2.clone(workspace.getName(), topVNode.getPath(), topVNode.getPath(), true);
        log.println(topVNode.getPath() + " cloned on " + superuserW2.getWorkspace().getName() + " at " + topVNode.getPath());

        testRootNodeW2 = (Node) superuserW2.getItem(testRoot);
    }
}
