package com.atlassian.stash.internal.audit;

import com.atlassian.bitbucket.audit.AuditEntry;
import com.atlassian.bitbucket.audit.AuditEntryBuilder;
import com.atlassian.bitbucket.audit.AuditEntryConverter;
import com.atlassian.bitbucket.audit.Priority;
import com.atlassian.event.api.EventPublisher;
import com.atlassian.bitbucket.event.annotation.Audited;
import com.atlassian.bitbucket.event.audit.AuditEvent;
import com.atlassian.bitbucket.request.RequestManager;
import com.google.common.collect.Sets;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.runners.MockitoJUnitRunner;
import org.mockito.stubbing.Answer;

import javax.annotation.Nonnull;
import java.util.Collections;
import java.util.Set;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import static org.mockito.Matchers.any;
import static org.mockito.Mockito.*;

@RunWith(MockitoJUnitRunner.class)
public class AuditedAnnotatedEventListenerTest {

    static AuditEntry lastEntry;

    @Mock
    private AuditEntryLoggingService auditLoggingService;
    @InjectMocks
    private AuditedAnnotatedEventListener listener;
    @Mock
    private EventPublisher publisher;
    @Mock
    private RequestManager requestManager;

    private AuditEvent lastPublished;
    private boolean publisherThrowEx;

    @Before
    public void setup() {
        lastPublished = null;
        publisherThrowEx = false;

        Answer<Void> answer = new Answer<Void>() {
            @Override
            public Void answer(InvocationOnMock invocation) throws Throwable {
                if (publisherThrowEx) {
                    throw new NullPointerException();
                }

                lastPublished = (AuditEvent) invocation.getArguments()[0];
                return null;
            }
        };
        doAnswer(answer).when(publisher).publish(anyObject());
    }

    @Test
    public void testListenAudited() throws Exception {
        listener.onEvent(new NotNaughty());
        assertEquals(lastPublished.getEntry(), lastEntry);
        assertEquals(Sets.newHashSet("1", "2"), lastPublished.getChannels());
        assertEquals(Priority.MEDIUM, lastPublished.getPriority());
    }

    @Test
    public void testListenAuditedPriorityOnChildren() throws Exception {
        listener.onEvent(new NotNaughty2());
        assertEquals(lastPublished.getEntry(), lastEntry);
        assertEquals(Sets.newHashSet("1"), lastPublished.getChannels());
        assertEquals(Priority.HIGH, lastPublished.getPriority());
    }

    @Test
    public void testListenAuditedAuditEvent() throws Exception {
        listener.onEvent(new Naughty(this, mock(AuditEntry.class), Collections.<String>emptySet(), Priority.MEDIUM));
        assertTrue(lastPublished == null);
    }

    @Test
    public void testListenNonAudited() throws Exception {
        listener.onEvent(new Object());
        assertTrue(lastPublished == null);
    }

    @Test(expected = InstantiationException.class)
    public void testListenNaughtyTranslatorEvent() throws Exception {
        try {
            listener.onEvent(new NaughtyTranslatorEvent());
        } finally {
            assertTrue(this.lastPublished == null);
            verify(auditLoggingService).log(any(AuditEntry.class));
            verifyNoMoreInteractions(auditLoggingService);
        }
    }

    @Test(expected = IllegalArgumentException.class)
    public void testListenExceptionalNaughtyTranslatorEvent() throws Exception {
        try {
            listener.onEvent(new ExceptionalNaughtyTranslatorEvent());
        } finally {
            assertTrue(this.lastPublished == null);
            verify(auditLoggingService).log(any(AuditEntry.class));
            verifyNoMoreInteractions(auditLoggingService);
        }
    }

    @Test(expected = NullPointerException.class)
    public void testListenAuditedBadPublish() throws Exception {
        publisherThrowEx = true;
        try {
            listener.onEvent(new NotNaughty());
        } finally {
            assertTrue(this.lastPublished == null);
            verify(auditLoggingService, times(2)).log(any(AuditEntry.class));
            verifyNoMoreInteractions(auditLoggingService);
        }
    }

    @Audited(converter = NotNaughtyTranslator.class, channels = {"1", "2"}, priority = Priority.MEDIUM)
    private static class NotNaughty {
    }

    @Audited(converter = NotNaughtyTranslator.class, channels = {"1"}, priority = Priority.HIGH)
    private static class NotNaughty2 extends NotNaughty {
    }

    public static class NotNaughtyTranslator implements AuditEntryConverter<NotNaughty> {

        public NotNaughtyTranslator() {
        }

        @Nonnull
        @Override
        public AuditEntry convert(@Nonnull NotNaughty event, AuditEntryBuilder builder) {
            lastEntry = mock(AuditEntry.class);
            return lastEntry;
        }
    }

    @Audited(converter = BasicEventTranslator.class, priority = Priority.MEDIUM)
    public static class Naughty extends AuditEvent {
        public Naughty(@Nonnull Object source, @Nonnull AuditEntry entry, @Nonnull Set<String> channels, Priority priority) {
            super(source, entry, channels, priority);
        }
    }

    public static class BasicEventTranslator implements AuditEntryConverter {
        @Nonnull
        @Override
        public AuditEntry convert(@Nonnull Object event, AuditEntryBuilder builder) {
            return mock(AuditEntry.class);
        }
    }

    @SuppressWarnings("UnusedParameters")
    public static class NaughtyTranslator implements AuditEntryConverter {
        public NaughtyTranslator(Object feedME) {
        }

        @Nonnull
        @Override
        public AuditEntry convert(@Nonnull Object event, AuditEntryBuilder builder) {
            return mock(AuditEntry.class);
        }
    }

    @Audited(converter = NaughtyTranslator.class, priority = Priority.MEDIUM)
    public static class NaughtyTranslatorEvent {
    }

    public static class ExceptionalNaughtyTranslator implements AuditEntryConverter {
        @Nonnull
        @Override
        public AuditEntry convert(@Nonnull Object event, AuditEntryBuilder builder) {
            throw new IllegalArgumentException("I am naughty!");
        }
    }

    @Audited(converter = ExceptionalNaughtyTranslator.class, priority = Priority.MEDIUM)
    public static class ExceptionalNaughtyTranslatorEvent {
    }

}
