package com.atlassian.stash.internal.activity;

import com.atlassian.stash.internal.AbstractDaoTest;
import com.atlassian.bitbucket.util.concurrent.LockGuard;
import com.atlassian.stash.internal.spring.SpringTransactionUtils;
import org.junit.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.support.TransactionCallbackWithoutResult;
import org.springframework.transaction.support.TransactionTemplate;

import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.locks.ReentrantReadWriteLock;

import static org.junit.Assume.assumeTrue;

/**
 * A base class for activity DAO tests which verifies their create/delete locking semantics.
 */
public abstract class AbstractHibernateActivityDaoTest extends AbstractDaoTest {

    @Autowired
    private ActivityLock activityLock;
    @Autowired
    private PlatformTransactionManager transactionManager;

    @Test(timeout = 30000) //30 second timeout just in case things go sideways...
    public void testDeleteLocking() throws Exception {
        //Only run on databases that actually need activity locking
        assumeTrue(activityLock instanceof MySqlActivityLock);

        final Thread deleter = Thread.currentThread();
        final AtomicReference<Exception> exception = new AtomicReference<>();
        final CountDownLatch latch = new CountDownLatch(1);
        //There's not actually any contention for this; it just can't be primitive and final
        final AtomicBoolean unlocked = new AtomicBoolean(false);
        Thread creator = new Thread("activity-creator") {

            @Override
            public void run() {
                //Calling Hibernate on another thread requires a transaction
                TransactionTemplate template = new TransactionTemplate(transactionManager,
                        SpringTransactionUtils.REQUIRES_NEW);
                template.setReadOnly(true);
                template.execute(new TransactionCallbackWithoutResult() {

                    @Override
                    protected void doInTransactionWithoutResult(TransactionStatus status) {
                        try {
                            test();
                        } catch (Exception e) {
                            exception.set(e);
                        }
                    }

                    private void test() throws Exception {
                        //Acquire the creation lock. While the lock is held, deletions should not be allowed
                        LockGuard guard = activityLock.forCreate();
                        latch.countDown();

                        //Wait for the test thread to try to delete
                        ReentrantReadWriteLock lock = ((MySqlActivityLock) activityLock).getLock();
                        while (!lock.hasQueuedThreads()) {
                            Thread.sleep(50L); //Just to prevent a really aggressive spin-lock
                        }
                        assertTrue(lock.hasQueuedThread(deleter));

                        //Release the creation lock to allow the deletion to proceed
                        unlocked.set(true);
                        guard.close();
                    }
                });
            }
        };
        creator.setDaemon(true);
        creator.start();

        latch.await(); //Wait for the creator thread to acquire the creation lock

        assertTrue(deleteByRepository(1) > 0);
        assertTrue(unlocked.get());

        creator.join(); //Wait for the creator thread to finish up

        Exception thrown = exception.get();
        if (thrown != null) {
            throw thrown; //This will propagate any exceptions (assertion failures) from the creator thread
        }
    }

    protected abstract int deleteByRepository(int repositoryId);
}
