package com.atlassian.bitbucket.mesh.logback;

import ch.qos.logback.classic.LoggerContext;
import ch.qos.logback.classic.spi.LoggingEvent;
import ch.qos.logback.core.status.Status;
import ch.qos.logback.core.status.StatusManager;
import com.atlassian.bitbucket.mesh.request.RequestContext;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentMatchers;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnitRunner;

import java.nio.charset.StandardCharsets;
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 com.atlassian.bitbucket.mesh.logback.AccessLogEncoder.*;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.endsWith;
import static org.hamcrest.Matchers.startsWith;
import static org.hamcrest.Matchers.*;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNull;
import static org.mockito.Mockito.*;

@RunWith(MockitoJUnitRunner.class)
public class AccessLogEncoderTest {

    private AccessLogEncoder encoder;
    @Mock
    private LoggerContext loggerContext;
    @Mock
    private RequestContext requestContext;

    @Before
    public void setup() {
        //Logback invariants: A LoggerContext is always set and start() is always called before
        //encode(ILoggingEvent) is ever called
        encoder = new AccessLogEncoder();
        encoder.setContext(loggerContext);
        encoder.start();
    }

    @Test
    public void testEncodeAtEnd() {
        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);

        String message = encodePhase(PHASE_END);
        assertThat(message, allOf(
                startsWith("address | protocol | o@id | correlationId | user | "),
                endsWith(" | action | agent | 0 | 2 | 7 | 85 | 7636 | first, second | 1895\n")
        ));
    }

    @Test
    public void testEncodeAtStart() {
        prepareCommonFields();

        String message = encodePhase(PHASE_START);
        assertThat(message, allOf(
                startsWith("address | protocol | i@id | correlationId | user | "),
                //The date goes between these
                endsWith(" | action | agent" + START_PLACEHOLDERS + "\n")
        ));
    }

    @Test
    public void testEncodeWithEmptyArguments() {
        StatusManager statusManager = mock(StatusManager.class);
        when(loggerContext.getStatusManager()).thenReturn(statusManager);

        LoggingEvent event = new LoggingEvent();
        event.setArgumentArray(new Object[0]);

        String message = encodeEvent(event);
        assertEquals("-", message);

        assertErrorStatus(statusManager);
    }

    @Test
    public void testEncodeWithTooFewArguments() {
        StatusManager statusManager = mock(StatusManager.class);
        when(loggerContext.getStatusManager()).thenReturn(statusManager);

        LoggingEvent event = new LoggingEvent();
        event.setArgumentArray(new Object[]{requestContext});

        String message = encodeEvent(event);
        assertEquals("-", message);

        assertErrorStatus(statusManager);
    }

    @Test
    public void testEncodeWithTooManyArguments() {
        StatusManager statusManager = mock(StatusManager.class);
        when(loggerContext.getStatusManager()).thenReturn(statusManager);

        LoggingEvent event = new LoggingEvent();
        event.setArgumentArray(new Object[]{requestContext, PHASE_START, "unexpected"});

        String message = encodeEvent(event);
        assertEquals("-", message);

        assertErrorStatus(statusManager);
    }

    @Test
    public void testEncodeWithoutArguments() {
        StatusManager statusManager = mock(StatusManager.class);
        when(loggerContext.getStatusManager()).thenReturn(statusManager);

        String message = encodeEvent(new LoggingEvent());
        assertEquals("-", message);

        assertErrorStatus(statusManager);
    }

    @Test
    public void testFooterBytes() {
        assertNull(encoder.footerBytes());
    }

    @Test
    public void testHeaderBytes() {
        assertNull(encoder.headerBytes());
    }

    //This test verifies the start and end lines have the same number of fields, which verifies the
    //repeat used to prepare the placeholders matches the number of detail fields
    @Test
    public void testPlaceholderCount() {
        prepareCommonFields();

        when(requestContext.getDuration()).thenReturn(Duration.ZERO);

        String start = encodePhase(PHASE_START);
        String end = encodePhase(PHASE_END);

        String[] startFields = start.split("\\|");
        String[] endFields = end.split("\\|");

        assertEquals("Start and end field counts don't match", startFields.length, endFields.length);
    }

    private static void assertErrorStatus(StatusManager statusManager) {
        verify(statusManager).add(ArgumentMatchers.<Status>argThat(status -> Status.ERROR == status.getLevel()));
    }

    private String encodeEvent(LoggingEvent event) {
        return new String(encoder.encode(event), StandardCharsets.UTF_8);
    }

    private String encodePhase(String phase) {
        return encodeEvent(prepareEvent(phase));
    }

    private void prepareCommonFields() {
        when(requestContext.getAction()).thenReturn("action");
        when(requestContext.getAgent()).thenReturn("agent");
        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 LoggingEvent prepareEvent(String phase) {
        LoggingEvent event = new LoggingEvent();
        event.setArgumentArray(new Object[]{requestContext, phase});
        event.setTimeStamp(System.currentTimeMillis());

        return event;
    }
}
