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

import javax.jcr.Node;
import javax.jcr.Property;
import javax.jcr.Repository;
import javax.jcr.RepositoryException;
import javax.jcr.Session;
import javax.jcr.lock.Lock;
import javax.jcr.lock.LockException;
import javax.jcr.lock.LockManager;
import javax.jcr.nodetype.ConstraintViolationException;

import org.apache.jackrabbit.test.AbstractJCRTest;
import org.apache.jackrabbit.test.NotExecutableException;
import org.apache.jackrabbit.test.RepositoryStub;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/** <code>AbstractLockTest</code>... */
public abstract class AbstractLockTest extends AbstractJCRTest {

    private static Logger log = LoggerFactory.getLogger(AbstractLockTest.class);

    protected LockManager lockMgr;
    protected Node lockedNode;
    protected Node childNode;
    protected Lock lock;

    protected void setUp() throws Exception {
        // check for lock support before creating the session in the super.setup
        checkSupportedOption(Repository.OPTION_LOCKING_SUPPORTED);
        
        super.setUp();
        
        lockedNode = testRootNode.addNode(nodeName1, testNodeType);
        ensureMixinType(lockedNode, mixLockable);
        childNode = lockedNode.addNode(nodeName2, testNodeType);
        testRootNode.save();

        lockMgr = getLockManager(testRootNode.getSession());
        lock = lockMgr.lock(lockedNode.getPath(), isDeep(), isSessionScoped(), getTimeoutHint(), getLockOwner());
    }

    protected void tearDown() throws Exception {
        // release the lock created during setup
        if (lockMgr != null && lockedNode != null && lockMgr.isLocked(lockedNode.getPath())) {
            try {
                lockMgr.unlock(lockedNode.getPath());
            } catch (RepositoryException e) {
                // ignore
            }
        }
        super.tearDown();
    }

    protected abstract boolean isSessionScoped();
    protected abstract boolean isDeep();

    protected void assertLockable(Node n)
            throws RepositoryException, NotExecutableException {
        ensureMixinType(n, mixLockable);
        n.getSession().save();
    }

    protected long getTimeoutHint() throws RepositoryException {
        String timoutStr = getProperty(RepositoryStub.PROP_LOCK_TIMEOUT);
        long hint = Long.MAX_VALUE;
        if (timoutStr != null) {
            try {
                hint = Long.parseLong(timoutStr);
            } catch (NumberFormatException e) {
                log.warn(e.getMessage());
            }
        }
        return hint;
    }

    protected String getLockOwner() throws RepositoryException {
        String ownerStr = getProperty(RepositoryStub.PROP_LOCK_OWNER);
        if (ownerStr == null) {
            ownerStr = superuser.getUserID();
        }
        return ownerStr;
    }

    protected static LockManager getLockManager(Session session) throws RepositoryException {
        return session.getWorkspace().getLockManager();
    }

    /**
     * Test {@link javax.jcr.lock.Lock#isDeep()}.
     */
    public void testIsDeep() {
        assertEquals("Lock.isDeep must be consistent with lock call.", isDeep(), lock.isDeep());
    }

    /**
     * Test {@link javax.jcr.lock.Lock#isLive()}.
     */
    public void testIsLive() throws RepositoryException {
        assertTrue("Lock.isLive must be true.", lock.isLive());
    }

    /**
     * Test {@link javax.jcr.lock.Lock#refresh()} on a released lock.
     *
     * @throws Exception
     */
    public void testRefresh() throws RepositoryException {
        // refresh must succeed
        lock.refresh();
    }

    // TODO: test if timeout gets reset upon Lock.refresh()
    
    /**
     * Test {@link javax.jcr.lock.Lock#refresh()} on a released lock.
     *
     * @throws Exception
     */
    public void testRefreshNotLive() throws Exception {
        // release the lock
        lockMgr.unlock(lockedNode.getPath());
        // refresh
        try {
            lock.refresh();
            fail("Refresh on a lock that is not alive must fail");
        } catch (LockException e) {
            // success
        }
    }

    /**
     * Test {@link javax.jcr.lock.Lock#getNode()}.
     *
     * @throws RepositoryException If an exception occurs.
     */
    public void testLockHoldingNode() throws RepositoryException {
        assertTrue("Lock.getNode() must be lockholding node.", lock.getNode().isSame(lockedNode));
    }

    /**
     * Test {@link LockManager#isLocked(String)} and {@link javax.jcr.Node#isLocked()}.
     *
     * @throws RepositoryException If an exception occurs.
     */
    public void testNodeIsLocked() throws RepositoryException {
        assertTrue("Node must be locked after lock creation.", lockedNode.isLocked());
        assertTrue("Node must be locked after lock creation.", lockMgr.isLocked(lockedNode.getPath()));
    }

    /**
     * Test {@link LockManager#holdsLock(String)} and {@link javax.jcr.Node#holdsLock()}. 
     *
     * @throws RepositoryException If an exception occurs.
     */
    public void testNodeHoldsLocked() throws RepositoryException {
        assertTrue("Node must hold lock after lock creation.", lockedNode.holdsLock());
        assertTrue("Node must hold lock after lock creation.", lockMgr.holdsLock(lockedNode.getPath()));
    }


    /**
     * A locked node must also be locked if accessed by some other session.
     */
    public void testLockVisibility() throws RepositoryException {
        Session otherSession = getHelper().getReadWriteSession();
        try {
            Node ln = (Node) otherSession.getItem(lockedNode.getPath());
            assertTrue("Locked node must also be locked for another session", ln.isLocked());
            assertTrue("Locked node must also be locked for another session", ln.holdsLock());
            assertTrue("Locked node must also be locked for another session", getLockManager(otherSession).holdsLock(ln.getPath()));
        } finally {
            otherSession.logout();
        }
    }

    /**
     * Test {@link javax.jcr.lock.Lock#isSessionScoped()}
     */
    public void testIsSessionScoped() {
        assertEquals("Lock.isSessionScoped must be consistent with lock call.", isSessionScoped(), lock.isSessionScoped());
    }

    /**
     * Test {@link javax.jcr.lock.Lock#isLockOwningSession()}
     *
     * @throws RepositoryException If an exception occurs.
     */
    public void testIsLockOwningSession() throws RepositoryException {
        assertTrue("Session must be lock owner", lock.isLockOwningSession());
        assertTrue("Session must be lock owner", lockedNode.getLock().isLockOwningSession());
        assertTrue("Session must be lock owner", lockMgr.getLock(lockedNode.getPath()).isLockOwningSession());

        Session otherSession = getHelper().getReadOnlySession();
        try {
            Lock lck = otherSession.getNode(lockedNode.getPath()).getLock();
            assertFalse("Session must not be lock owner", lck.isLockOwningSession());

            Lock lck2 = getLockManager(otherSession).getLock(lockedNode.getPath());
            assertFalse("Session must not be lock owner", lck2.isLockOwningSession());
        } finally {
            otherSession.logout();
        }

        Session otherAdmin = getHelper().getSuperuserSession();
        try {
            Lock lck = otherAdmin.getNode(lockedNode.getPath()).getLock();
            assertFalse("Other Session for the same userID must not be lock owner", lck.isLockOwningSession());

            Lock lck2 = getLockManager(otherAdmin).getLock(lockedNode.getPath());
            assertFalse("Other Session for the same userID must not be lock owner", lck2.isLockOwningSession());

        } finally {
            otherAdmin.logout();
        }
    }

    /**
     * Test {@link javax.jcr.lock.Lock#getSecondsRemaining()} 
     */
    public void testGetSecondsRemaining() throws RepositoryException {
        if (lock.isLive()) {
            assertTrue("Seconds remaining must be a positive long.", lock.getSecondsRemaining() > 0);            
        } else {
            assertTrue("Seconds remaining must be a negative long.", lock.getSecondsRemaining() < 0);
        }
    }

    /**
     * Test {@link javax.jcr.lock.Lock#getSecondsRemaining()}
     */
    public void testGetSecondsRemainingAfterUnlock() throws RepositoryException {
        lockMgr.unlock(lockedNode.getPath());
        assertTrue("Lock has been released: seconds remaining must be a negative long.", lock.getSecondsRemaining() < 0);
    }

    /**
     * Test expiration of the lock
     */
    public synchronized void testLockExpiration()
            throws RepositoryException, NotExecutableException {
        lockedNode.unlock();

        long hint = 1;
        lock = lockMgr.lock(
                lockedNode.getPath(), isDeep(), isSessionScoped(), hint, null);

        // only test if timeout hint was respected.
        long remaining = lock.getSecondsRemaining();
        if (remaining <= hint) {
            if (remaining > 0) {
                try {
                    wait(remaining * 4000); // wait four time as long to be safe
                } catch (InterruptedException ignore) {
                }
            }
            long secs = lock.getSecondsRemaining();
            assertTrue(
                    "A released lock must return a negative number of seconds, was: " + secs,
                    secs < 0);
            String message = "If the timeout hint is respected the lock"
                + " must be automatically released.";
            assertFalse(message, lock.isLive());
            assertFalse(message, lockedNode.isLocked());
            assertFalse(message, lockMgr.isLocked(lockedNode.getPath()));
            assertFalse(message, lockedNode.hasProperty(Property.JCR_LOCK_IS_DEEP));
            assertFalse(message, lockedNode.hasProperty(Property.JCR_LOCK_OWNER));
        } else {
            throw new NotExecutableException("timeout hint was ignored.");
        }
    }

    /**
     * Test expiration of the lock
     */
    public synchronized void testOwnerHint()
            throws RepositoryException, NotExecutableException {
        lockedNode.unlock();

        lock = lockMgr.lock(lockedNode.getPath(), isDeep(), isSessionScoped(), Long.MAX_VALUE, "test");

        String owner = lock.getLockOwner();
        if (!"test".equals(lock.getLockOwner())) {
            throw new NotExecutableException();
        } else {
            assertTrue(lockedNode.hasProperty(Property.JCR_LOCK_OWNER));
            assertEquals("test", lockedNode.getProperty(Property.JCR_LOCK_OWNER).getString());
        }
    }

    /**
     * Test if Lock is properly released.
     * 
     * @throws RepositoryException
     */
    public void testUnlock() throws RepositoryException {
        // release the lock
        lockMgr.unlock(lockedNode.getPath());
        
        // assert: lock must not be alive
        assertFalse("lock must not be alive", lock.isLive());
    }

    /**
     * Test {@link LockManager#unlock(String)} for a session that is not
     * lock owner.
     * 
     * @throws RepositoryException
     * @throws NotExecutableException
     */
    public void testUnlockByOtherSession() throws RepositoryException, NotExecutableException {
        Session otherSession = getHelper().getReadWriteSession();
        try {
            getLockManager(otherSession).unlock(lockedNode.getPath());
            fail("Another session must not be allowed to unlock.");
        } catch (LockException e) {
            // success
            // make sure the node is still locked and the lock properties are
            // still present.
            assertTrue(lockMgr.isLocked(lockedNode.getPath()));
            assertTrue(lockedNode.hasProperty(jcrlockIsDeep));
            assertTrue(lockedNode.hasProperty(jcrLockOwner));
        } finally {
            otherSession.logout();
        }
    }

    public void testIsLockedChild() throws RepositoryException {
        assertEquals("Child node must be locked according to isDeep flag.", isDeep(), childNode.isLocked());
        assertEquals("Child node must be locked according to isDeep flag.", isDeep(), lockMgr.isLocked(childNode.getPath()));
    }

    public void testIsLockedNewChild() throws RepositoryException {
        Node newChild = lockedNode.addNode(nodeName3, testNodeType);
        assertEquals("New child node must be locked according to isDeep flag.", isDeep(),
                newChild.isLocked());
        assertEquals("New child node must be locked according to isDeep flag.", isDeep(),
                lockMgr.isLocked(newChild.getPath()));
    }

    public void testHoldsLockChild() throws RepositoryException {
        assertFalse("Child node below a locked node must never be lock holder",
                childNode.holdsLock());
        assertFalse("Child node below a locked node must never be lock holder",
                lockMgr.holdsLock(childNode.getPath()));
    }

    public void testHoldsLockNewChild() throws RepositoryException {
        Node newChild = lockedNode.addNode(nodeName3, testNodeType);
        assertFalse("Child node below a locked node must never be lock holder",
                newChild.holdsLock());
        assertFalse("Child node below a locked node must never be lock holder",
                lockMgr.holdsLock(newChild.getPath()));
    }

    public void testGetLockOnChild() throws RepositoryException {
        if (isDeep()) {
            // get lock must succeed even if child is not lockable.
            Lock lock = childNode.getLock();
            assertNotNull(lock);
            assertTrue("Lock.getNode() must return the lock holding node", lockedNode.isSame(lock.getNode()));

            Lock lock2 = lockMgr.getLock(childNode.getPath());
            assertNotNull(lock2);
            assertTrue("Lock.getNode() must return the lock holding node", lockedNode.isSame(lock2.getNode()));
        } else {
            try {
                childNode.getLock();
                fail("Node.getLock() must throw if node is not locked.");
            } catch (LockException e) {
                // success
            }
            try {
                lockMgr.getLock(childNode.getPath());
                fail("LockManager.getLock(String) must throw if node is not locked.");
            } catch (LockException e) {
                // success
            }
        }
    }

    public void testGetLockOnNewChild() throws RepositoryException {
        Node newChild = lockedNode.addNode(nodeName3, testNodeType);
        if (isDeep()) {
            // get lock must succeed even if child is not lockable.
            Lock lock = newChild.getLock();
            assertNotNull(lock);
            assertTrue("Lock.getNode() must return the lock holding node", lockedNode.isSame(lock.getNode()));

            Lock lock2 = lockMgr.getLock(newChild.getPath());
            assertNotNull(lock2);
            assertTrue("Lock.getNode() must return the lock holding node", lockedNode.isSame(lock2.getNode()));
        } else {
            try {
                newChild.getLock();
                fail("Node.getLock() must throw if node is not locked.");
            } catch (LockException e) {
                // success
            }
            try {
                lockMgr.getLock(newChild.getPath());
                fail("LockManager.getLock(String) must throw if node is not locked.");
            } catch (LockException e) {
                // success
            }
        }
    }

    public void testRemoveMixLockableFromLockedNode() throws RepositoryException,
            NotExecutableException {
        try {
            lockedNode.removeMixin(mixLockable);
            lockedNode.save();

            // the mixin got removed -> the lock should implicitly be released
            // as well in order not to have inconsistencies
            String msg = "Lock should have been released.";
            assertFalse(msg, lock.isLive());
            assertFalse(msg, lockedNode.isLocked());
            assertFalse(msg, lockMgr.isLocked(lockedNode.getPath()));

            assertFalse(msg, lockedNode.hasProperty(jcrLockOwner));
            assertFalse(msg, lockedNode.hasProperty(jcrlockIsDeep));

        } catch (ConstraintViolationException e) {
            // cannot remove the mixin -> ok
            // consequently the node must still be locked, the lock still live...
            String msg = "Lock must still be live.";
            assertTrue(msg, lock.isLive());
            assertTrue(msg, lockedNode.isLocked());
            assertTrue(msg, lockMgr.isLocked(lockedNode.getPath()));

            assertTrue(msg, lockedNode.hasProperty(jcrLockOwner));
            assertTrue(msg, lockedNode.hasProperty(jcrlockIsDeep));
        } finally {
            // ev. re-add the mixin in order to be able to unlock the node
            if (lockedNode.isLocked()) {
                ensureMixinType(lockedNode, mixLockable);
                lockedNode.save();
            }
        }
    }
}