package com.atlassian.bitbucket.mesh.git;

import ch.qos.logback.classic.Level;
import ch.qos.logback.classic.Logger;
import com.atlassian.bitbucket.mesh.execution.GrpcExecutionManager;
import com.atlassian.bitbucket.mesh.git.hosting.GitReceivePackFilter;
import com.atlassian.bitbucket.mesh.junit.LogListener;
import com.atlassian.bitbucket.mesh.repository.Repository;
import com.atlassian.bitbucket.mesh.rpc.v1.RpcHostingStreamResult;
import com.atlassian.bitbucket.mesh.rpc.v1.RpcStreamChunk;
import com.atlassian.bitbucket.mesh.test.ExecutionContextRule;
import com.google.protobuf.ByteString;
import org.junit.After;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.slf4j.LoggerFactory;

import static org.hamcrest.Matchers.allOf;
import static org.hamcrest.Matchers.containsString;
import static org.junit.Assert.*;
import static org.mockito.Mockito.mock;

public class LoggingGrpcPushTracerTest {

    @Rule
    public final ExecutionContextRule executionContextRule = new ExecutionContextRule(new GrpcExecutionManager());
    @Rule
    public final LogListener logListener = new LogListener(LoggingGrpcPushTracer.class);

    private Repository repository;
    private LoggingGrpcPushTracer tracer;

    @Before
    public void setUp() {
        repository = mock(Repository.class);
        tracer = new LoggingGrpcPushTracer(executionContextRule.getExecutionManager(), 17);
        setLoggingEnabled(true);
    }

    @After
    public void tearDown() {
        setLoggingEnabled(false);
    }

    @Test
    public void testDoesNotLogOnNormalHappyFlow() {
        GitReceivePackFilter filter = tracer.createFilter(repository);
        assertNotNull(filter);
        assertTrue(filter.wantsPack());
        assertTrue(filter.wantsPacketLines());

        RpcStreamChunk stdin = chunk("stdin line", false);
        //verify stdin packet lines are returned unchanged
        assertSame(stdin, filter.onPacketLine(stdin));
        //verify stdin pack data is returned unchanged
        RpcStreamChunk pack = chunk("PACK02364979432", false);
        assertSame(pack, filter.onPack(pack));
        //Once the pack has been detected, the filter opts out of further processing
        assertFalse(filter.wantsPack());
        assertFalse(filter.wantsPacketLines());

        //Mock some error being written to stdout
        tracer.onStdout(chunk("stdout chunk", false));
        tracer.onStdout(chunk("", true));
        tracer.onStderr(chunk("", true));
        tracer.onPreReceive(0);
        tracer.onPostReceive(0);
        tracer.onResult(RpcHostingStreamResult.newBuilder()
                .setExitCode(0)
                .build());

        logListener.assertNothingLogged();
    }

    @Test
    public void testDoesNotLogWhenRejectedByPreReceive() {
        GitReceivePackFilter filter = tracer.createFilter(repository);
        assertNotNull(filter);
        assertTrue(filter.wantsPack());
        assertTrue(filter.wantsPacketLines());

        RpcStreamChunk stdin = chunk("stdin line", false);
        //verify stdin packet lines are returned unchanged
        assertSame(stdin, filter.onPacketLine(stdin));
        //verify stdin pack data is returned unchanged
        RpcStreamChunk pack = chunk("PACK02364979432", false);
        assertSame(pack, filter.onPack(pack));
        //Once the pack has been detected, the filter opts out of further processing
        assertFalse(filter.wantsPack());
        assertFalse(filter.wantsPacketLines());

        //Mock some error being written to stdout
        tracer.onStdout(chunk("rejected by pre-receive hook", false));
        tracer.onStdout(chunk("", true));
        tracer.onStderr(chunk("", true));
        tracer.onPreReceive(1);
        tracer.onResult(RpcHostingStreamResult.newBuilder()
                .setExitCode(0)
                .build());

        logListener.assertNothingLogged();
    }

    @Test
    public void testLogsWhenNoHooksCalledExactFitToRingBuffer() {
        GitReceivePackFilter filter = tracer.createFilter(repository);
        assertNotNull(filter);
        assertTrue(filter.wantsPack());
        assertTrue(filter.wantsPacketLines());

        RpcStreamChunk stdin = chunk("stdin line", false);
        //verify stdin packet lines are returned unchanged
        assertSame(stdin, filter.onPacketLine(stdin));
        //verify stdin pack data is returned unchanged
        RpcStreamChunk pack = chunk("PACK02364979432", false);
        assertSame(pack, filter.onPack(pack));
        //Once the pack has been detected, the filter opts out of further processing
        assertFalse(filter.wantsPack());
        assertFalse(filter.wantsPacketLines());

        //Mock some error being written to stdout
        tracer.onStdout(chunk("this fits exactly", false));
        tracer.onStdout(chunk("", true));
        tracer.onStderr(chunk("", true));
        tracer.onPreReceive(0);
        tracer.onResult(RpcHostingStreamResult.newBuilder()
                .setExitCode(0)
                .build());

        logListener.assertDebug(allOf(
                containsString("Push did not complete as expected; {"),
                containsString("request: \"stdin line\",\n"),
                containsString("stdout: \"this fits exactly\",\n"),
                containsString("stderr: \"\""), //empty
                containsString("pre-receive: {exit: 0}"),
                containsString("post-receive: not called")));
    }

    @Test
    public void testLogsWhenNoHooksCalledNotTruncated() {
        GitReceivePackFilter filter = tracer.createFilter(repository);
        assertNotNull(filter);
        assertTrue(filter.wantsPack());
        assertTrue(filter.wantsPacketLines());

        RpcStreamChunk stdin = chunk("stdin line", false);
        //verify stdin packet lines are returned unchanged
        assertSame(stdin, filter.onPacketLine(stdin));
        //verify stdin pack data is returned unchanged
        RpcStreamChunk pack = chunk("PACK02364979432", false);
        assertSame(pack, filter.onPack(pack));
        //Once the pack has been detected, the filter opts out of further processing
        assertFalse(filter.wantsPack());
        assertFalse(filter.wantsPacketLines());

        //Mock some error being written to stdout
        tracer.onStdout(chunk("stdout chunk", false));
        tracer.onStdout(chunk("", true));
        tracer.onStderr(chunk("", true));
        tracer.onPreReceive(0);
        tracer.onResult(RpcHostingStreamResult.newBuilder()
                .setExitCode(0)
                .build());

        logListener.assertDebug(allOf(
                containsString("Push did not complete as expected; {"),
                containsString("request: \"stdin line\",\n"),
                containsString("stdout: \"stdout chunk\",\n"),
                containsString("stderr: \"\""), //empty
                containsString("pre-receive: {exit: 0}"),
                containsString("post-receive: not called")));
    }

    @Test
    public void testLogsWhenNoHooksCalledTruncated() {
        GitReceivePackFilter filter = tracer.createFilter(repository);
        assertNotNull(filter);
        assertTrue(filter.wantsPack());
        assertTrue(filter.wantsPacketLines());

        RpcStreamChunk stdin1 = chunk("this is stdin line 1", false);
        //verify stdin packet lines are returned unchanged
        assertSame(stdin1, filter.onPacketLine(stdin1));
        //verify stdin packet lines are returned unchanged
        RpcStreamChunk stdin2 = chunk("this is stdin line 2", false);
        assertSame(stdin2, filter.onPacketLine(stdin2));
        //verify stdin pack data is returned unchanged
        RpcStreamChunk pack = chunk("PACK02364979432", false);
        assertSame(pack, filter.onPack(pack));
        //Once the pack has been detected, the filter opts out of further processing
        assertFalse(filter.wantsPack());
        assertFalse(filter.wantsPacketLines());

        //Mock some error being written to stdout
        tracer.onStdout(chunk("this stdout chunk is longer than the max buffer size and should be truncated", false));
        tracer.onStderr(chunk("this stderr chunk is longer than the max buffer size and should be truncated", false));
        tracer.onStderr(chunk(". More?", false));
        tracer.onResult(RpcHostingStreamResult.newBuilder()
                .setExitCode(0)
                .build());

        logListener.assertDebug(allOf(
                containsString("Push did not complete as expected; {"),
                //stdin lines should be truncated
                containsString("request: \"this is stdin lin...\",\n"),
                //stdout lines should be truncated
                containsString("stdout: \"...ould be truncated\",\n"),
                containsString("stderr: \"... truncated. More?\""), //empty
                containsString("pre-receive: not called"),
                containsString("post-receive: not called")));
    }

    @Test
    public void testNoOpWhenTraceLoggingDisabled() {
        setLoggingEnabled(false);

        assertNull(tracer.createFilter(repository));

        //Mock some error being written to stdout
        tracer.onStdout(chunk("stdout chunk", false));
        tracer.onStdout(chunk("", true));
        tracer.onStderr(chunk("", true));
        tracer.onPreReceive(0);
        tracer.onResult(RpcHostingStreamResult.newBuilder()
                .setExitCode(0)
                .build());

        //Nothing should be logged
        logListener.assertNothingLogged();
    }

    private static RpcStreamChunk chunk(String data, boolean closed) {
        return RpcStreamChunk.newBuilder()
                .setData(data == null ? ByteString.empty() : ByteString.copyFromUtf8(data))
                .setClosed(closed)
                .build();
    }

    private static void setLoggingEnabled(boolean enabled) {
        Logger logger = (Logger) LoggerFactory.getLogger(LoggingGrpcPushTracer.class);
        logger.setLevel(enabled ? Level.DEBUG : Level.INFO);
    }
}