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

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Iterator;
import java.util.List;

import javax.jcr.AccessDeniedException;
import javax.jcr.Node;
import javax.jcr.PathNotFoundException;
import javax.jcr.PropertyIterator;
import javax.jcr.Repository;
import javax.jcr.RepositoryException;
import javax.jcr.lock.LockException;
import javax.jcr.retention.Hold;
import javax.jcr.retention.RetentionManager;
import javax.jcr.version.VersionException;

import org.apache.jackrabbit.test.NotExecutableException;

/**
 * <code>RetentionManagerTest</code>...
 */
public class HoldTest extends AbstractRetentionTest {

    private static boolean containsHold(Hold[] holds, Hold toTest) throws RepositoryException {
        for (int i = 0; i < holds.length; i++) {
            if (holds[i].getName().equals(toTest.getName()) && holds[i].isDeep() == toTest.isDeep()) {
                return true;
            }
        }
        return false;
    }

    public void testAddHold() throws RepositoryException, NotExecutableException {
        Hold hold = retentionMgr.addHold(testNodePath, getHoldName(), false);
        Hold[] holds = retentionMgr.getHolds(testNodePath);
        assertTrue("getHolds must return the hold added before.", holds.length >= 1);
        assertTrue("getHolds doesn't return the hold added before", containsHold(holds, hold));
    }

    public void testAddHold2() throws RepositoryException, NotExecutableException {
        Hold[] holdsBefore = retentionMgr.getHolds(testNodePath);
        Hold hold = retentionMgr.addHold(testNodePath, getHoldName(), false);
        assertFalse("The hold added must not have been present before.", containsHold(holdsBefore, hold));
    }

    public void testAddHoldIsTransient() throws RepositoryException, NotExecutableException {
        Hold hold = retentionMgr.addHold(testNodePath, getHoldName(), false);
        Hold[] holds = retentionMgr.getHolds(testNodePath);

        // revert the changes made
        superuser.refresh(false);
        Hold[] holds2 = retentionMgr.getHolds(testNodePath);

        assertEquals("Reverting transient changes must revert the hold added.",
                holds.length -1, holds2.length);
        assertFalse("Reverting transient changes must revert the hold added.",
                containsHold(holds2, hold));
    }
    
    public void testRemoveHold() throws RepositoryException, NotExecutableException {
        Hold hold = retentionMgr.addHold(testNodePath, getHoldName(), false);

        Hold[] holds = retentionMgr.getHolds(testNodePath);

        retentionMgr.removeHold(testNodePath, hold);
        Hold[] holds2 = retentionMgr.getHolds(testNodePath);

        assertEquals("RetentionManager.removeHold should removed the hold added before.",
                holds.length -1, holds2.length);
        assertFalse("RetentionManager.removeHold should removed the hold added before.",
                containsHold(holds2, hold));
    }

    public void testRemoveHoldIsTransient() throws RepositoryException, NotExecutableException {
        Hold hold = retentionMgr.addHold(testNodePath, getHoldName(), false);
        superuser.save();
        try {
            Hold[] holds = retentionMgr.getHolds(testNodePath);

            retentionMgr.removeHold(testNodePath, hold);
            superuser.refresh(false);

            Hold[] holds2 = retentionMgr.getHolds(testNodePath);
            assertEquals("Reverting transient hold removal must restore the original state.",
                    Arrays.asList(holds), Arrays.asList(holds2));
        } finally {
            // clear the hold that was permanently added before.
            retentionMgr.removeHold(testNodePath, hold);
            superuser.save();
        }
    }

    public void testRemoveHoldFromChild() throws RepositoryException, NotExecutableException {
        String childPath = testRootNode.addNode(nodeName2, testNodeType).getPath();
        Hold hold = retentionMgr.addHold(testNodePath, getHoldName(), false);

        try {
            retentionMgr.removeHold(childPath, hold);
            fail("Removing hold from another node must fail");
        } catch (RepositoryException e) {
            // success
            assertTrue(containsHold(retentionMgr.getHolds(testNodePath), hold));
        }

        // check again with persisted hold
        superuser.save();
        try {
            retentionMgr.removeHold(childPath, hold);
            fail("Removing hold from another node must fail");
        } catch (RepositoryException e) {
            // success
            assertTrue(containsHold(retentionMgr.getHolds(testNodePath), hold));
        } finally {
            // clear the hold that was permanently added before.
            retentionMgr.removeHold(testNodePath, hold);
            superuser.save();
        }
    }

    public void testInvalidPath() throws RepositoryException, NotExecutableException {
        String invalidPath = testPath; // not an absolute path.
        try {
            retentionMgr.getHolds(invalidPath);
            fail("Accessing holds an invalid path must throw RepositoryException.");
        } catch (RepositoryException e) {
            // success
        }
        try {
            retentionMgr.addHold(invalidPath, getHoldName(), true);
            fail("Adding a hold at an invalid path must throw RepositoryException.");
        } catch (RepositoryException e) {
            // success
        }
        try {
            Hold h = retentionMgr.addHold(testNodePath, getHoldName(), true);
            retentionMgr.removeHold(invalidPath, h);
            fail("Removing a hold at an invalid path must throw RepositoryException.");
        } catch (RepositoryException e) {
            // success
        }
    }

    public void testNonExistingNodePath() throws RepositoryException, NotExecutableException {
        String invalidPath = testNodePath + "/nonexisting";
        int cnt = 0;
        while (superuser.nodeExists(invalidPath)) {
            invalidPath += cnt++;
        }

        try {
            retentionMgr.getHolds(invalidPath);
            fail("Accessing holds from non-existing node must throw PathNotFoundException.");
        } catch (PathNotFoundException e) {
            // success
        }
        try {
            retentionMgr.addHold(invalidPath, getHoldName(), true);
            fail("Adding a hold for a non-existing node must throw PathNotFoundException.");
        } catch (PathNotFoundException e) {
            // success
        }
        try {
            Hold h = retentionMgr.addHold(testNodePath, getHoldName(), true);
            retentionMgr.removeHold(invalidPath, h);
            fail("Removing a hold at a non-existing node must throw PathNotFoundException.");
        } catch (PathNotFoundException e) {
            // success
        }
    }

    public void testPropertyPath() throws RepositoryException, NotExecutableException {
        String propPath = null;
        for (PropertyIterator it = testRootNode.getProperties(); it.hasNext();) {
            String path = it.nextProperty().getPath();
            if (! superuser.nodeExists(path)) {
                propPath = path;
                break;
            }
        }
        if (propPath == null) {
            throw new NotExecutableException();
        }
        try {
            retentionMgr.getHolds(propPath);
            fail("Accessing holds from non-existing node must throw PathNotFoundException.");
        } catch (PathNotFoundException e) {
            // success
        }
        try {
            retentionMgr.addHold(propPath, getHoldName(), true);
            fail("Adding a hold for a non-existing node must throw PathNotFoundException.");
        } catch (PathNotFoundException e) {
            // success
        }
        try {
            Hold h = retentionMgr.addHold(testNodePath, getHoldName(), true);
            retentionMgr.removeHold(propPath, h);
            fail("Removing a hold at a non-existing node must throw PathNotFoundException.");
        } catch (PathNotFoundException e) {
            // success
        }
    }

    public void testInvalidName() {
        try {
            String invalidName = "*.[y]";
            retentionMgr.addHold(testNodePath, invalidName, false);
            fail("Adding a hold with an invalid JCR name must fail.");
        } catch (RepositoryException e) {
            // success
        }
    }

    public void testReadOnlySession() throws NotExecutableException, RepositoryException {
        javax.jcr.Session s = getHelper().getReadOnlySession();
        try {
            RetentionManager rmgr = getRetentionManager(s);
            try {
                rmgr.getHolds(testNodePath);
                fail("Read-only session doesn't have sufficient privileges to retrieve holds.");
            } catch (AccessDeniedException e) {
                // success
            }
            try {
                rmgr.addHold(testNodePath, getHoldName(), false);
                fail("Read-only session doesn't have sufficient privileges to retrieve holds.");
            } catch (AccessDeniedException e) {
                // success
            }
        } finally {
            s.logout();
        }
    }

    public void testAddHoldOnLockedNode() throws NotExecutableException, RepositoryException {
        Node child = getLockedChildNode();
        // remember current holds for clean up.
        List<Hold> holdsBefore = Arrays.asList(retentionMgr.getHolds(child.getPath()));

        // get another session.
        javax.jcr.Session otherS = getHelper().getSuperuserSession();
        try {
            RetentionManager rmgr = getRetentionManager(otherS);            
            rmgr.addHold(child.getPath(), getHoldName(), false);
            otherS.save();

            fail("Adding hold on a locked node must throw LockException.");
        } catch (LockException e) {
            // success
        } finally {
            otherS.logout();

            // clear holds (in case of test failure)
            List<Hold> holds = new ArrayList<Hold>(Arrays.asList(retentionMgr.getHolds(child.getPath())));
            if (holds.removeAll(holdsBefore)) {
                for (Iterator<Hold> it = holds.iterator(); it.hasNext();) {
                    retentionMgr.removeHold(child.getPath(), (Hold) it.next());
                }
            }
            superuser.save();
        }
    }

    public void testRemoveHoldOnLockedNode() throws NotExecutableException, RepositoryException {
        Node child = getLockedChildNode();
        Hold h = retentionMgr.addHold(child.getPath(), getHoldName(), false);
        testRootNode.save();

        javax.jcr.Session otherS = getHelper().getSuperuserSession();
        try {
            RetentionManager rmgr = getRetentionManager(otherS);
            Hold[] holds = rmgr.getHolds(child.getPath());

            if (holds.length > 0) {
                rmgr.removeHold(child.getPath(), holds[0]);
                otherS.save();
                fail("Removing a hold on a locked node must throw LockException.");
            }
        } catch (LockException e) {
            // success
        } finally {
            otherS.logout();

            // clear hold added before
            try {
                retentionMgr.removeHold(child.getPath(), h);
                superuser.save();
            } catch (RepositoryException e) {
                // should not get here if test is correctly executed.
            }
        }
    }

    private Node getLockedChildNode() throws NotExecutableException, RepositoryException {
        checkSupportedOption(Repository.OPTION_LOCKING_SUPPORTED);
        Node child = testRootNode.addNode(nodeName2, testNodeType);
        ensureMixinType(child, mixLockable);
        testRootNode.save();
        child.lock(false, true); // session-scoped lock clean upon superuser-logout.
        return child;
    }

    public void testAddHoldOnCheckedInNode() throws NotExecutableException, RepositoryException {
        Node child = getVersionableChildNode();
        child.checkout();
        child.checkin();

        // get another session.
        javax.jcr.Session otherS = getHelper().getSuperuserSession();
        try {
            RetentionManager rmgr = getRetentionManager(otherS);
            rmgr.addHold(child.getPath(), getHoldName(), false);
            otherS.save();

            fail("Adding hold on a checked-in node must throw VersionException.");
        } catch (VersionException e) {
            // success
        } finally {
            otherS.logout();

            // clear holds (in case of test failure)
            child.checkout();
            Hold[] holds = retentionMgr.getHolds(child.getPath());
            for (int i = 0; i < holds.length; i++) {
                retentionMgr.removeHold(child.getPath(), holds[i]);
            }
            superuser.save();
        }
    }

    public void testRemoveHoldOnCheckedInNode() throws NotExecutableException, RepositoryException {
        Node vn = getVersionableChildNode();
        vn.checkout();
        Node n = vn.addNode(nodeName2);
        Hold h = retentionMgr.addHold(n.getPath(), getHoldName(), false);
        superuser.save();

        // checkin on the parent node make the hold-containing node checked-in.
        vn.checkin();

        javax.jcr.Session otherS = getHelper().getSuperuserSession();
        try {
            RetentionManager rmgr = getRetentionManager(otherS);
            Hold[] holds = rmgr.getHolds(n.getPath());

            if (holds.length > 0) {
                rmgr.removeHold(n.getPath(), holds[0]);
                otherS.save();
                fail("Removing a hold on a checked-in node must throw VersionException.");
            }
        } catch (VersionException e) {
            // success
        } finally {
            otherS.logout();

            // clear hold added before
            vn.checkout();
            try {
                retentionMgr.removeHold(n.getPath(), h);
                superuser.save();
            } catch (RepositoryException e) {
                // should not get here if test is correctly executed.
            }
        }
    }

    private Node getVersionableChildNode() throws NotExecutableException, RepositoryException {
        checkSupportedOption(Repository.OPTION_VERSIONING_SUPPORTED);
        Node child = testRootNode.addNode(nodeName2, testNodeType);
        ensureMixinType(child, mixVersionable);
        testRootNode.save();
        return child;
    }

    public void testHoldGetName() throws RepositoryException, NotExecutableException {
        String holdName = getHoldName();
        Hold h = retentionMgr.addHold(testNodePath, getHoldName(), false);
        assertEquals("Hold.getName() must return the specified name.",holdName, h.getName());
    }

    public void testHoldGetName2() throws RepositoryException, NotExecutableException {
        String holdName = getHoldName();
        Hold h = retentionMgr.addHold(testNodePath, getHoldName(), true);
        assertEquals("Hold.getName() must return the specified name.",holdName, h.getName());
    }

    public void testHoldIsDeep() throws RepositoryException, NotExecutableException {
        String holdName = getHoldName();
        Hold h = retentionMgr.addHold(testNodePath, getHoldName(), false);
        assertEquals("Hold.isDeep() must reflect the specified flag.", false, h.isDeep());
    }

    public void testHoldIsDeep2() throws RepositoryException, NotExecutableException {
        String holdName = getHoldName();
        Hold h = retentionMgr.addHold(testNodePath, getHoldName(), true);
        assertEquals("Hold.isDeep() must reflect the specified flag.", true, h.isDeep());
    }
}