package com.atlassian.bitbucket.mesh.git;

import com.atlassian.bitbucket.mesh.config.HostingConfig;
import com.atlassian.bitbucket.mesh.event.EventPublisher;
import com.atlassian.bitbucket.mesh.git.builder.GitProcessBuilder;
import com.atlassian.bitbucket.mesh.git.builder.GitProcessBuilderFactory;
import com.atlassian.bitbucket.mesh.git.builder.httpbackend.GitHttpBackendBuilder;
import com.atlassian.bitbucket.mesh.git.cache.PackCacheService;
import com.atlassian.bitbucket.mesh.git.hook.GitHookService;
import com.atlassian.bitbucket.mesh.git.hook.HookRequestHandle;
import com.atlassian.bitbucket.mesh.git.hosting.GitReceivePackFilter;
import com.atlassian.bitbucket.mesh.git.hosting.GitReceivePackListener;
import com.atlassian.bitbucket.mesh.management.SidebandManager;
import com.atlassian.bitbucket.mesh.repository.ManagedRepository;
import com.atlassian.bitbucket.mesh.repository.Repository;
import com.atlassian.bitbucket.mesh.repository.RepositoryManager;
import com.atlassian.bitbucket.mesh.request.RequestManager;
import com.atlassian.bitbucket.mesh.rpc.v1.*;
import com.atlassian.bitbucket.mesh.transaction.UpdateRefsContextHolder;
import com.google.common.collect.ImmutableList;
import com.google.protobuf.ByteString;
import com.google.protobuf.MessageOrBuilder;
import io.grpc.stub.StreamObserver;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnitRunner;

import java.nio.file.Paths;
import java.time.Duration;

import static com.atlassian.bitbucket.mesh.git.hosting.GitPackets.FLUSH_PACKET;
import static com.atlassian.bitbucket.mesh.rpc.v1.RpcHostingOperationType.*;
import static java.util.Collections.singletonList;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.equalTo;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.*;
import static org.mockito.Mockito.*;

@RunWith(MockitoJUnitRunner.class)
public class HttpBackendFragmentHandlerTest {

    @Mock
    private GitProcessBuilderFactory builderFactory;
    @Mock
    private EventPublisher eventPublisher;
    private HttpBackendFragmentHandler handler;
    @Mock
    private HookRequestHandle hookHandle;
    @Mock
    private GitHookService hookService;
    @Mock
    private HostingConfig hostingConfig;
    @Mock
    private GitHttpBackendBuilder httpBackendBuilder;
    @Mock
    private StreamObserver<RpcHostingWriteStreamFragment> observer;
    @Mock
    private PackCacheService packCacheService;
    @Mock
    private GitProcessBuilder processBuilder;
    @Mock
    private GrpcPushTracer pushTracer;
    @Mock
    private GitQuarantineRegistry quarantineRegistry;
    @Mock
    private GitReceivePackListener receivePackListener;
    @Mock
    private Repository repository;
    @Mock
    private RepositoryManager repositoryManager;
    @Mock
    private RequestManager requestManager;
    @Mock
    private SidebandManager sidebandManager;
    @Mock
    private UpdateRefsContextHolder updateRefsHelper;

    @Before
    public void setUp() {
        Duration hookTimeout = Duration.ofSeconds(30);

        when(builderFactory.builder(any(MessageOrBuilder.class))).thenReturn(processBuilder);
        when(hookHandle.configure(any(GitHttpBackendBuilder.class))).thenReturn(httpBackendBuilder);
        when(hookService.registerRequest(any(Repository.class), any())).thenReturn(hookHandle);
        when(hostingConfig.isFilterAllowed()).thenReturn(true);
        when(packCacheService.isEnabledForHttp()).thenReturn(true);
        when(processBuilder.httpBackend()).thenReturn(httpBackendBuilder);
        when(repositoryManager.getById(anyString())).thenReturn(repository);
        when(repository.getPath()).thenReturn(Paths.get("Hello", "darkness", "my", "old", "friend", "..."));

        when(httpBackendBuilder.withConfiguration(anyString(), anyBoolean())).thenReturn(httpBackendBuilder);
        when(httpBackendBuilder.withEnvironment(anyString(), anyString())).thenReturn(httpBackendBuilder);

        handler = new HttpBackendFragmentHandler(builderFactory, eventPublisher, hookService, hookTimeout,
                hostingConfig, 1024L, observer, packCacheService, pushTracer, quarantineRegistry,
                ImmutableList.of(receivePackListener), repositoryManager, requestManager, sidebandManager, updateRefsHelper);
    }

    @Test
    public void testRefAdvertisement() {
        handler.createBuilder(RpcHttpBackendFragment.newBuilder()
                .setRequest(RpcHttpBackendRequest.newBuilder()
                        .setCgiEnvironment(RpcCgiEnvironment.newBuilder()
                                .addHttpAccept("application/x-git-upload-pack-result")
                                .addAllHttpAcceptEncoding(ImmutableList.of("deflate, gzip"))
                                .setContentType("application/x-git-upload-pack-request")
                                .setPathInfo("/pkey/slug.git/info/refs")
                                .setQueryString("service=git-upload-pack")
                                .setRequestMethod("GET")
                                .build())
                        .setRepository("The Tougher You Acted, The More Scared You Looked")
                        .build())
                .build());

        assertFalse("is not an update", handler.isUpdate());
        assertThat(handler.getOperationType(), equalTo(HOSTING_OP_REF_ADVERTISEMENT));
        verifyNoInteractions(receivePackListener);
    }

    @Test
    public void testTreatsPushWithNoPayloadAsUnspecified() {
        handler.createBuilder(RpcHttpBackendFragment.newBuilder()
                .setRequest(RpcHttpBackendRequest.newBuilder()
                        .setCgiEnvironment(RpcCgiEnvironment.newBuilder()
                                .addHttpAccept("application/x-git-receive-pack-result")
                                .addAllHttpAcceptEncoding(ImmutableList.of("deflate, gzip"))
                                .setContentLength(4L)
                                .setContentType("application/x-git-receive-pack-request")
                                .setPathInfo("/pkey/slug.git/git-receive-pack")
                                .setRequestMethod("POST")
                                .build())
                        .setRepository("These Aren't The Droids You're Looking For")
                        .build())
                .build());

        handler.onFragment(RpcHttpBackendFragment.newBuilder()
                .setStdin(RpcStreamChunk.newBuilder()
                        .setData(ByteString.copyFromUtf8(FLUSH_PACKET))
                        .build())
                .build());

        assertFalse("is not an update", handler.isUpdate());
        assertThat(handler.getOperationType(), equalTo(HOSTING_OP_UNSPECIFIED));

        verify(receivePackListener).createFilter(repository);
    }

    @Test
    public void testTreatsPushAsUpdate() {
        GitReceivePackFilter filter = mock(GitReceivePackFilter.class);
        when(receivePackListener.createFilter(any())).thenReturn(filter);
        when(filter.wantsPacketLines()).thenReturn(true);
        when(filter.onPacketLine(any())).thenAnswer(invocation -> invocation.getArgument(0));

        handler.createBuilder(RpcHttpBackendFragment.newBuilder()
                .setRequest(RpcHttpBackendRequest.newBuilder()
                        .setCgiEnvironment(RpcCgiEnvironment.newBuilder()
                                .setContentType("application/x-git-receive-pack-request")
                                .setContentLength(445)
                                .setPathInfo("/git-receive-pack")
                                .setRemoteAddr("0:0:0:0:0:0:0:1")
                                .setRemoteUser("admin")
                                .setRequestMethod("POST")
                                .setServerProtocol("HTTP/1.1")
                                .setServerSoftware("Apache Tomcat/9.0.39")
                                .addAllHttpAccept(singletonList("application/x-git-receive-pack-result"))
                                .addAllHttpAcceptEncoding(singletonList("deflate, gzip, zstd"))
                                .build())
                        .setRepository(ManagedRepository.formatId(0, "abcdeabcdeabcdeabcde", "local-id"))
                        .build())
                .build());

        //Length of input is 106 bytes = 0x6a
        String refPacketLine = "006a58b8128a3a3aed8add0eb86246590783ec9ed785 58b8128a3a3aed8add0eb86246590783ec9ed785" +
                " refs/heads/branch123";
        RpcStreamChunk chunk = RpcStreamChunk.newBuilder()
                .setData(ByteString.copyFromUtf8(refPacketLine + FLUSH_PACKET))
                .build();
        handler.onFragment(RpcHttpBackendFragment.newBuilder().setStdin(chunk).build());

        assertTrue("is an update", handler.isUpdate());
        assertThat(handler.getOperationType(), equalTo(HOSTING_OP_PUSH));
        verify(receivePackListener).createFilter(repository);
        verify(filter).onPacketLine(RpcStreamChunk.newBuilder()
                .setData(ByteString.copyFromUtf8(refPacketLine))
                .build());
        verify(filter).onPacketLine(RpcStreamChunk.newBuilder()
                .setData(ByteString.copyFromUtf8(FLUSH_PACKET))
                .build());
    }

    @Test
    public void testTreatsNonPushOperationsAsNotAnUpdate() {
        handler.createBuilder(RpcHttpBackendFragment.newBuilder()
                .setRequest(RpcHttpBackendRequest.newBuilder()
                        .setCgiEnvironment(RpcCgiEnvironment.newBuilder()
                                .setContentType("application/x-git-upload-pack-request")
                                .setContentLength(445)
                                .setPathInfo("/git-upload-pack")
                                .setRemoteAddr("0:0:0:0:0:0:0:1")
                                .setRemoteUser("admin")
                                .setRequestMethod("POST")
                                .setServerProtocol("HTTP/1.1")
                                .setServerSoftware("Apache Tomcat/9.0.39")
                                .addAllHttpAccept(singletonList("application/x-git-upload-pack-result"))
                                .addAllHttpAcceptEncoding(singletonList("deflate, gzip, zstd"))
                                .build())
                        .setRepository("The Tougher You Acted, The More Scared You Looked")
                        .build())
                .build());

        assertFalse("is not an update", handler.isUpdate());
        verifyNoInteractions(receivePackListener);
    }

    @Test
    public void testCapabilitiesProtocolV2() {
        handler.createBuilder(RpcHttpBackendFragment.newBuilder()
                .setRequest(RpcHttpBackendRequest.newBuilder()
                        .setCgiEnvironment(RpcCgiEnvironment.newBuilder()
                                .addHttpAccept("application/x-git-upload-pack-result")
                                .addAllHttpAcceptEncoding(ImmutableList.of("deflate, gzip"))
                                .setContentType("application/x-git-upload-pack-request")
                                .setPathInfo("/pkey/slug.git/info/refs")
                                .setQueryString("service=git-upload-pack")
                                .setRequestMethod("GET")
                                .build())
                        .setProtocolHeader("version=2")
                        .build())
                .build());

        assertThat(handler.getOperationType(), equalTo(HOSTING_OP_CAPABILITIES));
        verifyNoInteractions(receivePackListener);
    }

    @Test
    public void testReceivePackRefAdvertisement() {
        handler.createBuilder(RpcHttpBackendFragment.newBuilder()
                .setRequest(RpcHttpBackendRequest.newBuilder()
                        .setCgiEnvironment(RpcCgiEnvironment.newBuilder()
                                .addHttpAccept("*/*")
                                .addAllHttpAcceptEncoding(ImmutableList.of("deflate, gzip"))
                                .setPathInfo("/info/refs")
                                .setQueryString("service=git-receive-pack")
                                .setRequestMethod("GET")
                                .build())
                        .build())
                .build());

        assertThat(handler.getOperationType(), equalTo(HOSTING_OP_REF_ADVERTISEMENT));
        verifyNoInteractions(receivePackListener);
    }

    @Test
    public void testRefAdvertisementProtocolV0() {
        handler.createBuilder(RpcHttpBackendFragment.newBuilder()
                .setRequest(RpcHttpBackendRequest.newBuilder()
                        .setCgiEnvironment(RpcCgiEnvironment.newBuilder()
                                .addHttpAccept("*/*")
                                .addAllHttpAcceptEncoding(ImmutableList.of("deflate, gzip"))
                                .setPathInfo("/info/refs")
                                .setQueryString("service=git-upload-pack")
                                .setRequestMethod("GET")
                                .build())
                        .setProtocolHeader("version=0")
                )
                .build());

        assertThat(handler.getOperationType(), equalTo(HOSTING_OP_REF_ADVERTISEMENT));
        verifyNoInteractions(receivePackListener);
    }

    @Test
    public void testRefAdvertisementProtocolV2() {
        handler.createBuilder(RpcHttpBackendFragment.newBuilder()
                .setRequest(RpcHttpBackendRequest.newBuilder()
                        .setCgiEnvironment(RpcCgiEnvironment.newBuilder()
                                .addHttpAccept("application/x-git-upload-pack-result")
                                .addAllHttpAcceptEncoding(ImmutableList.of("deflate, gzip"))
                                .setContentType("application/x-git-upload-pack-request")
                                .setPathInfo("/git-upload-pack")
                                .setRequestMethod("POST")
                                .build())
                        .setProtocolHeader("version=2")
                )
                .build());

        assertThat(handler.getOperationType(), equalTo(HOSTING_OP_REF_ADVERTISEMENT));
        verifyNoInteractions(receivePackListener);
    }

    @Test
    public void testNegotiationProtocolV0() {
        handler.createBuilder(RpcHttpBackendFragment.newBuilder()
                .setRequest(RpcHttpBackendRequest.newBuilder()
                        .setCgiEnvironment(RpcCgiEnvironment.newBuilder()
                                .addHttpAccept("application/x-git-upload-pack-result")
                                .addAllHttpAcceptEncoding(ImmutableList.of("deflate, gzip"))
                                .setContentType("application/x-git-upload-pack-request")
                                .setPathInfo("/git-upload-pack")
                                .setRequestMethod("POST")
                                .build())
                        .setProtocolHeader("version=0")
                )
                .build());

        assertThat(handler.getOperationType(), equalTo(HOSTING_OP_NEGOTIATION));
        verifyNoInteractions(receivePackListener);
    }

    @Test
    public void testUploadPackWhenCachingDisabled() {
        when(packCacheService.isEnabledForHttp()).thenReturn(false);

        handler.createBuilder(RpcHttpBackendFragment.newBuilder()
                .setRequest(uploadPackBuilder().build())
                .build());

        assertThat(handler.getOperationType(), equalTo(HOSTING_OP_UNSPECIFIED));
        verifyNoInteractions(receivePackListener);
    }

    @Test
    public void testUploadPackProtocolV0WhenCachingEnabled() {
        handler.createBuilder(RpcHttpBackendFragment.newBuilder()
                .setRequest(uploadPackBuilder()
                        .setProtocolHeader("version=0")
                        .build())
                .build());

        assertThat(handler.getOperationType(), equalTo(HOSTING_OP_NEGOTIATION));
        verifyNoInteractions(receivePackListener);
    }

    @Test
    public void testUploadPackProtocolV2WhenCachingEnabled() {
        handler.createBuilder(RpcHttpBackendFragment.newBuilder()
                .setRequest(uploadPackBuilder()
                        .setProtocolHeader("version=2")
                        .build())
                .build());

        assertThat(handler.getOperationType(), equalTo(HOSTING_OP_REF_ADVERTISEMENT));
        verifyNoInteractions(receivePackListener);
    }

    private static RpcHttpBackendRequest.Builder uploadPackBuilder() {
        return RpcHttpBackendRequest.newBuilder()
                .setCgiEnvironment(RpcCgiEnvironment.newBuilder()
                        .addHttpAccept("application/x-git-upload-pack-result")
                        .addAllHttpAcceptEncoding(ImmutableList.of("deflate", "gzip"))
                        .setContentType("application/x-git-upload-pack-request")
                        .setPathInfo("/git-upload-pack")
                        .setRequestMethod("POST")
                        .build())
                .setRepository("The Tougher You Acted, The More Scared You Looked");
    }
}