/*
 * (C) Copyright 2006-2016 Nuxeo SA (http://nuxeo.com/) and others.
 *
 * Licensed 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.
 *
 * Contributors:
 *     Florent Guillaume
 */
package org.nuxeo.ecm.core;

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;

import java.util.concurrent.CountDownLatch;

import javax.inject.Inject;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.nuxeo.ecm.core.api.CloseableCoreSession;
import org.nuxeo.ecm.core.api.CoreInstance;
import org.nuxeo.ecm.core.api.CoreSession;
import org.nuxeo.ecm.core.api.DocumentModel;
import org.nuxeo.ecm.core.api.impl.DocumentModelImpl;
import org.nuxeo.ecm.core.api.security.SecurityConstants;
import org.nuxeo.ecm.core.test.CoreFeature;
import org.nuxeo.ecm.core.test.annotations.Granularity;
import org.nuxeo.ecm.core.test.annotations.RepositoryConfig;
import org.nuxeo.runtime.test.runner.Features;
import org.nuxeo.runtime.test.runner.FeaturesRunner;
import org.nuxeo.runtime.transaction.TransactionHelper;

/**
 * Tests locking behavior under transaction. Subclass tests with no transaction.
 */
@RunWith(FeaturesRunner.class)
@Features(CoreFeature.class)
@RepositoryConfig(cleanup = Granularity.METHOD)
public class TestSQLRepositoryLocking {

    protected static final Log log = LogFactory.getLog(TestSQLRepositoryJTAJCA.class);

    @SuppressWarnings("deprecation")
    private static final String ADMINISTRATOR = SecurityConstants.ADMINISTRATOR;

    @Inject
    protected CoreSession session;

    protected void nextTransaction() {
        if (TransactionHelper.isTransactionActiveOrMarkedRollback()) {
            TransactionHelper.commitOrRollbackTransaction();
            TransactionHelper.startTransaction();
        }
    }

    @SuppressWarnings("deprecation")
    @Test
    public void testLocking() throws Exception {
        DocumentModel root = session.getRootDocument();

        DocumentModel doc = session.createDocumentModel("/", "doc", "File");
        doc = session.createDocument(doc);
        assertNull(doc.getLockInfo());
        assertFalse(doc.isLocked());
        session.save();

        nextTransaction();

        doc = session.getChild(root.getRef(), "doc");
        doc.setLock();

        assertEquals(ADMINISTRATOR, doc.getLockInfo().getOwner());
        assertNotNull(doc.getLockInfo().getCreated());
        assertTrue(doc.isLocked());

        nextTransaction();

        doc = session.getChild(root.getRef(), "doc");

        assertEquals(ADMINISTRATOR, doc.getLockInfo().getOwner());
        assertNotNull(doc.getLockInfo().getCreated());
        assertTrue(doc.isLocked());

        nextTransaction();

        doc = session.getChild(root.getRef(), "doc");
        doc.removeLock();

        assertNull(doc.getLockInfo());
        assertFalse(doc.isLocked());

        nextTransaction();

        doc = session.getChild(root.getRef(), "doc");

        assertFalse(doc.isLocked());
    }

    @Test
    public void testLockingBeforeSave() throws Exception {
        DocumentModel root = session.getRootDocument();
        DocumentModel doc = session.createDocumentModel("/", "doc", "File");
        doc = session.createDocument(doc);
        doc.setLock();
        session.save();

        nextTransaction();

        doc = session.getChild(root.getRef(), "doc");
        assertTrue(doc.isLocked());
    }

    // check we don't have a SQL-level locking error due to the lock manager
    // connection reading a row that was written but not yet committed by the
    // main connection
    @Test
    public void testGetLockAfterCreate() throws Exception {
        DocumentModel doc1 = session.createDocumentModel("/", "doc1", "File");
        doc1 = session.createDocument(doc1);
        session.save();
        // read lock after save (SQL INSERT)
        assertNull(doc1.getLockInfo());

        DocumentModel doc2 = session.createDocumentModel("/", "doc2", "File");
        doc2 = session.createDocument(doc2);
        session.save();
        // set lock after save (SQL INSERT)
        doc2.setLock();
    }

    protected CountDownLatch threadStartLatch;

    protected CountDownLatch lockingLatch;

    protected volatile boolean locked;

    @Test
    public void testLockingWithMultipleThreads() throws Exception {
        final String repositoryName = session.getRepositoryName();
        DocumentModel root = session.getRootDocument();
        DocumentModel doc = session.createDocumentModel("/", "doc", "File");
        doc = session.createDocument(doc);
        session.save();

        nextTransaction();

        doc = session.getChild(root.getRef(), "doc");
        assertFalse(doc.isLocked());

        // start other thread
        threadStartLatch = new CountDownLatch(1);
        lockingLatch = new CountDownLatch(1);
        Thread t = new Thread() {
            @Override
            public void run() {
                TransactionHelper.startTransaction();
                try {
                    try (CloseableCoreSession session2 = CoreInstance.openCoreSession(repositoryName, ADMINISTRATOR)) {
                        DocumentModel root2 = session2.getRootDocument();
                        DocumentModel doc2 = session2.getChild(root2.getRef(), "doc");
                        // let main thread continue
                        threadStartLatch.countDown();
                        // wait main thread trigger
                        lockingLatch.await();
                        locked = doc2.isLocked();
                    }
                } catch (Exception e) {
                    throw new RuntimeException(e);
                } finally {
                    threadStartLatch.countDown(); // error recovery
                    TransactionHelper.commitOrRollbackTransaction();
                }
            }
        };
        t.start();
        threadStartLatch.await();

        doc.setLock();
        assertTrue(doc.isLocked());

        // trigger other thread check
        lockingLatch.countDown();
        t.join();

        assertTrue(locked);
    }

}
