package com.atlassian.bitbucket.mesh.logback;

import ch.qos.logback.classic.spi.LoggingEvent;
import com.atlassian.bitbucket.mesh.junit.LogListener;
import com.atlassian.bitbucket.mesh.request.RequestContext;
import com.fasterxml.jackson.core.JsonGenerator;
import org.hamcrest.core.StringStartsWith;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnitRunner;

import java.io.IOException;
import java.time.Duration;
import java.util.Arrays;
import java.util.TreeSet;

import static com.atlassian.bitbucket.mesh.logback.AccessLog.PHASE_END;
import static com.atlassian.bitbucket.mesh.logback.AccessLog.PHASE_START;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoInteractions;
import static org.mockito.Mockito.verifyNoMoreInteractions;
import static org.mockito.Mockito.when;

@RunWith(MockitoJUnitRunner.class)
public class AccessLogJsonProviderTest {

    private AccessLogJsonProvider provider;

    @Rule
    public final LogListener logListener = new LogListener(AccessLogJsonProvider.class);

    @Mock
    private JsonGenerator jsonGenerator;
    @Mock
    private RequestContext requestContext;

    @Before
    public void setup() {
        provider = new AccessLogJsonProvider();
    }

    @Test
    public void testWriteEnd() throws IOException {
        prepareCommonFields();

        when(requestContext.getBytesReceived()).thenReturn(85L);
        when(requestContext.getBytesSent()).thenReturn(7636L);
        when(requestContext.getDuration()).thenReturn(Duration.ofMillis(1895L));
        when(requestContext.getLabels()).thenReturn(new TreeSet<>(Arrays.asList("first", "second")));
        when(requestContext.getMessagesReceived()).thenReturn(2);
        when(requestContext.getMessagesSent()).thenReturn(7);
        when(requestContext.getResult()).thenReturn(0);

        provider.writeTo(jsonGenerator, prepareEvent(PHASE_END));

        verify(jsonGenerator).writeObjectField("phase", "o");
        verifyCommonFields();

        verify(jsonGenerator).writeObjectField("bytes_received", 85L);
        verify(jsonGenerator).writeObjectField("bytes_sent", 7636L);
        verify(jsonGenerator).writeObjectField("duration", Duration.ofMillis(1895L));
        verify(jsonGenerator).writeObjectField("labels", new TreeSet<>(Arrays.asList("first", "second")));
        verify(jsonGenerator).writeObjectField("messages_received", 2);
        verify(jsonGenerator).writeObjectField("messages_sent", 7);
        verify(jsonGenerator).writeObjectField("result", 0);

        verifyNoMoreInteractions(jsonGenerator);
    }

    @Test
    public void testWriteStart() throws IOException {
        prepareCommonFields();

        provider.writeTo(jsonGenerator, prepareEvent(PHASE_START));

        verify(jsonGenerator).writeObjectField("phase", "i");
        verifyCommonFields();

        verifyNoMoreInteractions(jsonGenerator);
    }

    @Test
    public void testWriteWithEmptyArguments() throws IOException {
        LoggingEvent event = new LoggingEvent();
        event.setArgumentArray(new Object[0]);

        provider.writeTo(jsonGenerator, event);

        verifyNoInteractions(jsonGenerator);

        logListener.assertError(new StringStartsWith("Unexpected arguments for access log:"));
    }

    @Test
    public void testWriteWithTooFewArguments() throws IOException {
        LoggingEvent event = new LoggingEvent();
        event.setArgumentArray(new Object[]{requestContext});

        provider.writeTo(jsonGenerator, event);

        verifyNoInteractions(jsonGenerator);

        logListener.assertError(new StringStartsWith("Unexpected arguments for access log:"));
    }

    @Test
    public void testWriteWithTooManyArguments() throws IOException {
        LoggingEvent event = new LoggingEvent();
        event.setArgumentArray(new Object[]{requestContext, PHASE_START, "unexpected"});

        provider.writeTo(jsonGenerator, event);

        verifyNoInteractions(jsonGenerator);

        logListener.assertError(new StringStartsWith("Unexpected arguments for access log:"));
    }

    @Test
    public void testWriteWithoutArguments() throws IOException {
        LoggingEvent event = new LoggingEvent();

        provider.writeTo(jsonGenerator, event);

        verifyNoInteractions(jsonGenerator);

        logListener.assertError(new StringStartsWith("Unexpected arguments for access log:"));
    }

    private void prepareCommonFields() {
        when(requestContext.getAction()).thenReturn("action");
        when(requestContext.getAgent()).thenReturn(null); // Test null fields included
        when(requestContext.getCorrelationId()).thenReturn("correlationId");
        when(requestContext.getId()).thenReturn("id");
        when(requestContext.getProtocol()).thenReturn("protocol");
        when(requestContext.getRemoteAddress()).thenReturn("address");
        when(requestContext.getUser()).thenReturn("user");
    }

    private void verifyCommonFields() throws IOException {
        verify(jsonGenerator).writeObjectField("action", "action");
        verify(jsonGenerator).writeObjectField("agent", null);
        verify(jsonGenerator).writeObjectField("correlation_id", "correlationId");
        verify(jsonGenerator).writeObjectField("id", "id");
        verify(jsonGenerator).writeObjectField("protocol", "protocol");
        verify(jsonGenerator).writeObjectField("remote_address", "address");
        verify(jsonGenerator).writeObjectField("user", "user");
    }

    private LoggingEvent prepareEvent(String phase) {
        LoggingEvent event = new LoggingEvent();
        event.setArgumentArray(new Object[]{requestContext, phase});
        event.setTimeStamp(System.currentTimeMillis());
        return event;
    }
}
