package com.atlassian.stash.internal.event;

import com.atlassian.bitbucket.auth.AuthenticationContext;
import com.atlassian.bitbucket.event.ApplicationEvent;
import com.atlassian.bitbucket.event.annotation.TransactionAware;
import com.atlassian.bitbucket.user.ApplicationUser;
import com.atlassian.event.api.EventPublisher;
import com.atlassian.stash.internal.spring.TransactionSynchronizer;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.runners.MockitoJUnitRunner;
import org.springframework.transaction.support.TransactionSynchronization;

import javax.annotation.Nonnull;

import static org.junit.Assert.assertSame;
import static org.mockito.Mockito.*;

@RunWith(MockitoJUnitRunner.class)
public class TransactionAwareEventPublisherTest {

    @Mock
    private AuthenticationContext authenticationContext;
    @Mock
    private EventPublisher delegate;
    @InjectMocks
    private TransactionAwareEventPublisher eventPublisher;
    @Mock
    private TransactionSynchronizer synchronizer;
    @Mock
    private ApplicationUser user;

    @Before
    public void setupEventPublisher() throws Exception {
        when(authenticationContext.getCurrentUser()).thenReturn(user);
    }

    @Test
    public void testPublishTransactionAwareAfterCommit() {
        AfterCommit event = new AfterCommit(this);

        when(synchronizer.register(isNotNull(TransactionSynchronization.class))).thenReturn(true);

        eventPublisher.publish(event);

        ArgumentCaptor<TransactionSynchronization> synchronizationCaptor =
                ArgumentCaptor.forClass(TransactionSynchronization.class);

        verify(synchronizer).register(synchronizationCaptor.capture());
        verifyZeroInteractions(delegate);

        //Shouldn't publish for afterCommit--the implementation uses afterCompletion
        synchronizationCaptor.getValue().afterCommit();
        verifyZeroInteractions(delegate);

        //Shouldn't publish if the transaction was rolled back
        synchronizationCaptor.getValue().afterCompletion(TransactionSynchronization.STATUS_ROLLED_BACK);
        verifyZeroInteractions(delegate);

        //Shouldn't publish if the transaction status isn't known
        synchronizationCaptor.getValue().afterCompletion(TransactionSynchronization.STATUS_UNKNOWN);
        verifyZeroInteractions(delegate);

        synchronizationCaptor.getValue().afterCompletion(TransactionSynchronization.STATUS_COMMITTED);
        verify(delegate).publish(same(event));
    }

    @Test
    public void testPublishTransactionAwareAfterCompletion() {
        AfterCompletion event = new AfterCompletion(this);

        when(synchronizer.register(isNotNull(TransactionSynchronization.class))).thenReturn(true);

        eventPublisher.publish(event);

        ArgumentCaptor<TransactionSynchronization> synchronizationCaptor =
                ArgumentCaptor.forClass(TransactionSynchronization.class);

        verify(synchronizer).register(synchronizationCaptor.capture());
        verifyZeroInteractions(delegate);

        synchronizationCaptor.getValue().afterCommit();
        verifyZeroInteractions(delegate);

        synchronizationCaptor.getValue().afterCompletion(TransactionSynchronization.STATUS_COMMITTED);
        synchronizationCaptor.getValue().afterCompletion(TransactionSynchronization.STATUS_ROLLED_BACK);
        synchronizationCaptor.getValue().afterCompletion(TransactionSynchronization.STATUS_UNKNOWN);
        verify(delegate, times(3)).publish(same(event));
    }

    @Test
    public void testPublishTransactionAwareImmediate() {
        Immediate event = new Immediate(this);

        eventPublisher.publish(event);

        verify(delegate).publish(same(event));
        verifyZeroInteractions(synchronizer);
    }

    @Test
    public void testPublishTransactionAwareIsImmediateWhenSynchronizationNotAvailable() {
        AfterCommit event = new AfterCommit(this);

        eventPublisher.publish(event);

        verify(synchronizer).register(isNotNull(TransactionSynchronization.class));
        verify(delegate).publish(same(event));
    }

    @Test
    public void testPublish() throws Exception {
        ApplicationEvent event = new ApplicationEvent(this) {
        };

        eventPublisher.publish(event);

        assertSame(user, event.getUser());
        verify(delegate).publish(same(event));
        verifyZeroInteractions(synchronizer);
    }

    @Test
    public void testRegister() {
        Object listener = new Object();

        eventPublisher.register(listener);

        verify(delegate).register(same(listener));
        verifyZeroInteractions(synchronizer);
    }

    @Test
    public void testUnregister() {
        Object listener = new Object();

        eventPublisher.unregister(listener);

        verify(delegate).unregister(same(listener));
        verifyZeroInteractions(synchronizer);
    }

    @Test
    public void testUnregisterAll() {
        eventPublisher.unregisterAll();

        verify(delegate).unregisterAll();
        verifyZeroInteractions(synchronizer);
    }

    @TransactionAware(TransactionAware.When.AFTER_COMMIT)
    public static class AfterCommit extends ApplicationEvent {

        private AfterCommit(@Nonnull Object source) {
            super(source);
        }
    }

    @TransactionAware(TransactionAware.When.AFTER_COMPLETION)
    public static class AfterCompletion extends ApplicationEvent {

        private AfterCompletion(@Nonnull Object source) {
            super(source);
        }
    }

    @TransactionAware(TransactionAware.When.IMMEDIATE)
    public static class Immediate extends ApplicationEvent {

        private Immediate(@Nonnull Object source) {
            super(source);
        }
    }
}
