package com.atlassian.bitbucket.mesh.event;

import com.atlassian.bitbucket.mesh.event.TransactionAware.When;
import com.atlassian.bitbucket.mesh.transaction.Transaction;
import com.atlassian.bitbucket.mesh.transaction.TransactionManager;
import com.atlassian.bitbucket.mesh.transaction.TransactionState;
import com.atlassian.bitbucket.mesh.transaction.TransactionListener;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnitRunner;
import org.springframework.context.ApplicationEventPublisher;

import static org.mockito.Mockito.*;

@RunWith(MockitoJUnitRunner.class)
@SuppressWarnings("JUnitTestCaseWithNoTests")
public class SpringEventPublisherTest {

    @Mock
    private ApplicationEventPublisher applicationEventPublisher;
    @InjectMocks
    private SpringEventPublisher eventPublisher;
    @Mock
    private Transaction transaction;
    @Mock
    private TransactionManager transactionManager;
    @Captor
    private ArgumentCaptor<TransactionListener> transactionSynchronizationCaptor;

    @Before
    public void setUpActiveTransaction() {
        when(transactionManager.addListener(any())).thenReturn(true);
    }

    @Test
    public void testAfterCommitEventIsNotPublishedAfterRollback() {
        AfterCommitEvent event = new AfterCommitEvent();
        eventPublisher.publish(event);

        rollBack();

        //Publishing should not happen at all if the transaction is rolled back
        verifyNoInteractions(applicationEventPublisher);
    }

    @Test
    public void testAfterCommitEventIsPublishedAfterCommit() {
        AfterCommitEvent event = new AfterCommitEvent();
        eventPublisher.publish(event);

        //Publishing should be deferred
        verifyNoInteractions(applicationEventPublisher);

        commit();

        //Publishing should happen after commit
        verify(applicationEventPublisher).publishEvent(event);
    }

    @Test
    public void testAfterCompletionEventIsPublishedAfterRollback() {
        AfterCompletionEvent event = new AfterCompletionEvent();
        eventPublisher.publish(event);

        rollBack();

        //Publishing should happen after rollback
        verify(applicationEventPublisher).publishEvent(event);
    }

    @Test
    public void testAfterCompletionEventIsPublishedAfterCommit() {
        AfterCompletionEvent event = new AfterCompletionEvent();
        eventPublisher.publish(event);

        //Publishing should be deferred
        verifyNoInteractions(applicationEventPublisher);

        commit();

        //Publishing should happen after commit
        verify(applicationEventPublisher).publishEvent(event);
    }

    @Test
    public void testDeferredEventIsPublishedImmediatelyIfNoTransactionIsActive() {
        when(transactionManager.addListener(any())).thenReturn(false);

        AfterCommitEvent event = new AfterCommitEvent();
        eventPublisher.publish(event);

        //Publishing should happen immediately if no transaction was active
        verify(applicationEventPublisher).publishEvent(event);
    }

    @Test
    public void testImmediateEventIsPublishedImmediately() {
        ImmediateEvent event = new ImmediateEvent();
        eventPublisher.publish(event);

        //Should be published immediately
        verify(applicationEventPublisher).publishEvent(event);
    }

    @Test
    public void testOverriddenImmediateEventIsPublishedImmediately() {
        OverriddenImmediateEvent event = new OverriddenImmediateEvent();
        eventPublisher.publish(event);

        //Should be published immediately
        verify(applicationEventPublisher).publishEvent(event);
    }

    @Test
    public void testUnannotatedEventIsPublishedImmediately() {
        UnannotatedEvent event = new UnannotatedEvent();
        eventPublisher.publish(event);

        //Should be published immediately
        verify(applicationEventPublisher).publishEvent(event);
    }
    
    private void commit() {
        verify(transactionManager).addListener(transactionSynchronizationCaptor.capture());
        TransactionListener synchronization = transactionSynchronizationCaptor.getValue();

        when(transaction.getState()).thenReturn(TransactionState.COMMITTED);
        synchronization.afterCommit(transaction);
        synchronization.afterCompletion(transaction);
    }

    private void rollBack() {
        verify(transactionManager).addListener(transactionSynchronizationCaptor.capture());
        TransactionListener synchronization = transactionSynchronizationCaptor.getValue();

        when(transaction.getState()).thenReturn(TransactionState.ROLLED_BACK);
        synchronization.afterCompletion(transaction);
    }

    @TransactionAware(When.AFTER_COMMIT)
    private static class AfterCommitEvent {
    }

    @TransactionAware(When.AFTER_COMPLETION)
    private static class AfterCompletionEvent {
    }

    @TransactionAware(When.IMMEDIATE)
    private static class ImmediateEvent {
    }

    @TransactionAware(When.IMMEDIATE)
    private static class OverriddenImmediateEvent extends AfterCommitEvent {
    }

    private static class UnannotatedEvent {
    }
}