package com.atlassian.bitbucket.mesh.git;

import com.atlassian.bitbucket.mesh.AbstractGrpcServiceTest;
import com.atlassian.bitbucket.mesh.config.ApplicationConfig;
import com.atlassian.bitbucket.mesh.git.builder.GitProcessBuilderFactory;
import com.atlassian.bitbucket.mesh.git.builder.revlist.SingleCommitRevListStdioHandler;
import com.atlassian.bitbucket.mesh.git.commit.Commit;
import com.atlassian.bitbucket.mesh.git.fetch.FetchResult;
import com.atlassian.bitbucket.mesh.git.push.PushResult;
import com.atlassian.bitbucket.mesh.git.ref.RefUpdate;
import com.atlassian.bitbucket.mesh.io.MoreFiles;
import com.atlassian.bitbucket.mesh.repository.*;
import com.atlassian.bitbucket.mesh.rpc.v1.*;
import com.atlassian.bitbucket.mesh.test.annotation.DirtiesRepo;
import com.atlassian.bitbucket.mesh.test.annotation.Repo;
import com.atlassian.bitbucket.mesh.test.util.http.TestGitHttpServer;
import com.atlassian.bitbucket.mesh.test.util.ssh.TestGitSshServer;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.protobuf.ByteString;
import io.grpc.ManagedChannel;
import io.grpc.Status;
import io.grpc.StatusRuntimeException;
import org.apache.commons.io.FileUtils;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;

import javax.annotation.Resource;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.time.Duration;
import java.util.*;
import java.util.function.Function;
import java.util.stream.Collectors;

import static com.atlassian.bitbucket.mesh.git.builder.revlist.CommitPumpSettings.UNLIMITED_MESSAGE_LENGTH;
import static com.atlassian.bitbucket.mesh.git.gc.DefaultGarbageCollectionManager.GC_LOG;
import static com.atlassian.bitbucket.mesh.test.annotation.Repo.Type.BITBUCKET_SERVER;
import static com.atlassian.bitbucket.mesh.test.annotation.Repo.Type.MANAGED;
import static com.atlassian.bitbucket.mesh.util.ShaUtils.NULL_SHA1;
import static org.hamcrest.Matchers.*;

public class GrpcRepositoryServiceTest extends AbstractGrpcServiceTest {

    private static final String HIERARCHY = "01234567890fedcba9988";
    private static final RpcGitTimeouts TIMEOUTS = RpcGitTimeouts.newBuilder()
            .setExecution(10L)
            .setIdle(10L)
            .build();

    @Resource
    private ApplicationConfig applicationConfig;
    @Resource
    private GitProcessBuilderFactory builderFactory;
    @Repo(GitRepositories.DIFF_TREE) //Don't use QA_RESOURCES for this; it's dangerous on Windows
    private Repository diffTree; //due to some extremely long ref names
    @Resource
    private TestGitHttpServer httpServer;
    @Resource
    private ManagedChannel managedChannel;
    @Repo(value = GitRepositories.GARBAGE, type = MANAGED)
    private ManagedRepository managedRepository;
    @Repo
    private Repository repository;
    @Resource
    private RepositoryManager repositoryManager;
    @Repo(type = BITBUCKET_SERVER)
    private Repository sidecarRepository;
    @Resource
    private TestGitSshServer sshServer;
    private RepositoryServiceGrpc.RepositoryServiceBlockingStub stub;

    @Before
    public void setup() {
        stub = RepositoryServiceGrpc.newBlockingStub(managedChannel);
    }

    @After
    public void tearDown() {
        httpServer.reset();
        sshServer.reset();
    }

    @Test
    public void testCreateAndDeleteManagedRepository() {
        String repositoryId = ManagedRepository.formatId(0, "abcdefabcdef01234567", "76124");
        stub.create(RpcCreateRepositoryRequest.newBuilder()
                .setRepository(repositoryId)
                .setDefaultBranch(ByteString.copyFromUtf8("develop"))
                .build());

        assertTrue(stub.isEmpty(RpcIsRepositoryEmptyRequest.newBuilder()
                .setRepository(repositoryId)
                .build())
                .getEmpty());
        assertEquals("refs/heads/develop", stub.getDefaultBranch(RpcGetDefaultBranchRequest.newBuilder()
                .setRepository(repositoryId)
                .build())
                .getBranchId()
                .toStringUtf8());

        Repository created = repositoryManager.findById(repositoryId);
        assertNotNull(created);

        stub.delete(RpcDeleteRepositoryRequest.newBuilder().setRepository(repositoryId).build());
        awaitTrue("Repository directory was not deleted after 2 seconds",
                Duration.ofSeconds(2L), () -> !MoreFiles.dirExists(created.getPath()));
    }

    @Test
    public void testCreateAndDeleteManagedRepositoryWithUUIDIds() {
        String hierarchyId = UUID.randomUUID().toString();
        String localId = UUID.randomUUID().toString();
        String repositoryId = ManagedRepository.formatId(0, hierarchyId, localId);
        stub.create(RpcCreateRepositoryRequest.newBuilder()
                .setRepository(repositoryId)
                .setDefaultBranch(ByteString.copyFromUtf8("develop"))
                .build());

        assertTrue(stub.isEmpty(RpcIsRepositoryEmptyRequest.newBuilder()
                .setRepository(repositoryId)
                .build())
                .getEmpty());
        assertEquals("refs/heads/develop", stub.getDefaultBranch(RpcGetDefaultBranchRequest.newBuilder()
                .setRepository(repositoryId)
                .build())
                .getBranchId()
                .toStringUtf8());

        Repository created = repositoryManager.findById(repositoryId);
        assertNotNull(created);

        stub.delete(RpcDeleteRepositoryRequest.newBuilder().setRepository(repositoryId).build());
        awaitTrue("Repository directory was not deleted after 2 seconds",
                Duration.ofSeconds(2L), () -> !MoreFiles.dirExists(created.getPath()));
    }

    @Test
    public void testCreateBitbucketServerRepository() {
        Path repoDir = repository.getPath().getParent().resolve("917342");
        String repositoryId = repositoryId(917342);
        try {
            stub.create(RpcCreateRepositoryRequest.newBuilder()
                    .setRepository(repositoryId)
                    .setDefaultBranch(ByteString.copyFromUtf8("develop"))
                    .build());

            assertTrue(stub.isEmpty(RpcIsRepositoryEmptyRequest.newBuilder()
                    .setRepository(repositoryId)
                    .build())
                    .getEmpty());
            assertEquals("refs/heads/develop", stub.getDefaultBranch(RpcGetDefaultBranchRequest.newBuilder()
                    .setRepository(repositoryId)
                    .build())
                    .getBranchId()
                    .toStringUtf8());
        } finally {
            MoreFiles.deleteQuietly(repoDir);
        }
    }

    @Test
    public void testDelete() {
        Path repoDir = repository.getPath().getParent().resolve("917348");
        String repositoryId = repositoryId(917348);

        try {
            //Create a repository so we can delete it later
            Repository created = repositoryManager.create(new CreateRepositoryParameters.Builder(repositoryId)
                    .metadata(ImmutableMap.of("hierarchy", HIERARCHY))
                    .build());
            assertNotNull(created);
            assertEquals(created, repositoryManager.findById(repositoryId));

            //Delete the repository through the stub
            stub.delete(RpcDeleteRepositoryRequest.newBuilder()
                    .setHierarchyMembership(RpcHierarchyMembership.newBuilder()
                            .setOnlyMember(true)
                            .build())
                    .setRepository(repositoryId)
                    .build());

            assertNull(repositoryManager.findById(repositoryId));
            assertFalse(Files.isDirectory(created.getPath()));
        } finally {
            MoreFiles.deleteQuietly(repoDir);
        }
    }

    @Test
    public void testDeleteFork() {
        Path fork1Dir = repository.getPath().resolveSibling("917301");
        String fork1Id = repositoryId(917301);
        Path fork2Dir = repository.getPath().resolveSibling("917302");
        String fork2Id = repositoryId(917302);

        try {
            //Create a fork so we can delete it later
            Repository fork1 = repositoryManager.create(new CreateRepositoryParameters.Builder(fork1Id)
                    .origin(repository)
                    .metadata(ImmutableMap.of("hierarchy", HIERARCHY))
                    .build());
            assertNotNull(fork1);
            assertEquals(fork1, repositoryManager.findById(fork1Id));

            Repository fork2 = repositoryManager.create(new CreateRepositoryParameters.Builder(fork2Id)
                    .origin(fork1)
                    .metadata(ImmutableMap.of("hierarchy", HIERARCHY))
                    .build());
            assertNotNull(fork2);
            assertEquals(fork2, repositoryManager.findById(fork2Id));

            //Delete fork1 through the stub
            stub.delete(RpcDeleteRepositoryRequest.newBuilder()
                    .setRepository(fork1Id)
                    .setHierarchyMembership(RpcHierarchyMembership.newBuilder()
                            .addForks(fork2Id)
                            .setOnlyMember(false)
                            .setOrigin(repository.getId())
                            .build())
                    .build());

            //Fork1 should be gone, but fork2 should remain
            assertNull(repositoryManager.findById(fork1Id));
            assertEquals(fork2, repositoryManager.findById(fork2Id));

            //Verify that fork2 is still valid
            SingleCommitRevListStdioHandler handler = new SingleCommitRevListStdioHandler(
                    null, true, false, UNLIMITED_MESSAGE_LENGTH);
            Commit commit = builderFactory.builder(fork2).revList()
                    .limit(1)
                    .rev("master")
                    .format(handler.getFormat())
                    .build(handler)
                    .run();
            assertNotNull(commit);
            assertEquals("0a943a29376f2336b78312d99e65da17048951db", commit.getId());
        } finally {
            MoreFiles.deleteQuietly(fork1Dir);
            MoreFiles.deleteQuietly(fork2Dir);
        }
    }

    @Test
    public void testDeleteRemovesLfsObjectsWhenOnlyMember() {
        // Repository is final member of hierarchy, so LFS data should also be removed
        checkDeleteAlsoDeletesLfsDirectory(true);
    }

    @Test
    public void testDeleteRetainsLfsObjectsIfOtherMembers() {
        // Repository is NOT final member of hierarchy, so LFS data should be retained
        checkDeleteAlsoDeletesLfsDirectory(false);
    }

    @Test
    public void testGetRepositories() {
        List<RpcRepository> allManaged = getRepositories(RpcGetRepositoriesRequest.getDefaultInstance());
        Map<String, RpcRepository> allManagedById = allManaged.stream()
                .collect(Collectors.toMap(RpcRepository::getId, Function.identity()));

        //Because our @Repo test support caches repositories across test invocations, the contents of allManaged
        //depends a bit on what tests have run before this test. At a minimum it should contain managedRepository.
        assertTrue(allManagedById.containsKey(managedRepository.getId()));
        assertEquals(managedRepository.getVersion(), allManagedById.get(managedRepository.getId()).getVersion());

        //Verify paging support; check the first and page
        List<RpcRepository> firstPage = getRepositories(RpcGetRepositoriesRequest.newBuilder()
                .setLimit(1)
                .build());
        assertEquals(allManaged.get(0), firstPage.get(0));

        List<RpcRepository> lastPage = getRepositories(RpcGetRepositoriesRequest.newBuilder()
                .setSkip(allManaged.size() - 1)
                .build());
        assertEquals(allManaged.get(allManaged.size() - 1), lastPage.get(0));
    }

    @Test
    public void testGetRepositoriesByPartition() {
        List<RpcRepository> repositories = getRepositories(RpcGetRepositoriesRequest.newBuilder()
                .addPartitions(managedRepository.getPartition())
                .build());
        Map<String, RpcRepository> repositoryById = repositories.stream()
                .collect(Collectors.toMap(RpcRepository::getId, Function.identity()));

        //Because our @Repo test support caches repositories across test invocations, and repository -> partition
        //allocation is done randomly, there may be multiple results. At a minimum it should contain managedRepository.
        assertTrue(repositoryById.containsKey(managedRepository.getId()));
        assertEquals(managedRepository.getVersion(), repositoryById.get(managedRepository.getId()).getVersion());
    }

    @DirtiesRepo("repository")
    @Test
    public void testFetchLocalRepository() {
        FetchResult result = fetchFromRepository(RpcAnyRepository.newBuilder()
                .setRepository(diffTree.getId()));

        assertThat(result.getFailedRefs(), emptyIterable());
        List<RefUpdate> refUpdates = result.getRefUpdates();
        assertEquals(3, refUpdates.size());
        assertEquals(RefUpdate.of("refs/heads/fetched/cr-lf-branch", NULL_SHA1, "83f0ca32d75e3121c57a3cc2d549a8a01a1cfafc"),
                refUpdates.get(0));
    }

    @DirtiesRepo("repository")
    @Test
    public void testFetchRemoteHttpRepository() {
        // Start the HTTP test server to fetch from
        httpServer.start();

        String sshUrl = httpServer.getCloneUrl("some_user", diffTree.getId());
        httpServer.addCredentials("some_user", "some_password");

        FetchResult result = fetchFromRepository(RpcAnyRepository.newBuilder()
                .setRemoteRepository(RpcRemoteRepository.newBuilder()
                        .setUsernamePassword(RpcUsernamePasswordCredentials.newBuilder()
                                .setUsername(ByteString.copyFromUtf8("some_user"))
                                .setPassword(ByteString.copyFromUtf8("some_password")))
                        .setUrl(sshUrl)));

        assertThat(result.getFailedRefs(), emptyIterable());
        List<RefUpdate> refUpdates = result.getRefUpdates();
        assertEquals(3, refUpdates.size());
        assertEquals(RefUpdate.of("refs/heads/fetched/cr-lf-branch", NULL_SHA1, "83f0ca32d75e3121c57a3cc2d549a8a01a1cfafc"),
                refUpdates.get(0));
    }

    @Test
    public void testFetchRemoteHttpRepositoryThrowsWhenAuthenticationFails() {
        httpServer.start();

        String sshUrl = httpServer.getCloneUrl("some_user", repository.getId());
        StatusRuntimeException e = assertThrows(StatusRuntimeException.class, () ->
                fetchFromRepository(RpcAnyRepository.newBuilder().setRemoteRepository(RpcRemoteRepository.newBuilder()
                        .setUsernamePassword(RpcUsernamePasswordCredentials.newBuilder()
                                .setUsername(ByteString.copyFromUtf8("some_other_user"))
                                .setPassword(ByteString.copyFromUtf8("some_other_password")))
                        .setUrl(sshUrl))));

        RpcGitError error = RpcGitErrorUtils.fromThrowable(e);
        assertNotNull(error);
        assertTrue(error.hasRemoteAuthenticationFailed());
    }

    @Test
    public void testFetchRemoteHttpRepositoryThrowsWhenNoPasswordSupplied() {
        httpServer.start();

        String sshUrl = httpServer.getCloneUrl("some_user", repository.getId());
        StatusRuntimeException e = assertThrows(StatusRuntimeException.class, () ->
                fetchFromRepository(RpcAnyRepository.newBuilder().setRemoteRepository(RpcRemoteRepository.newBuilder()
                        .setUrl(sshUrl))));

        RpcGitError error = RpcGitErrorUtils.fromThrowable(e);
        assertNotNull(error);
        assertTrue(error.hasRemoteAuthenticationFailed());
    }

    @DirtiesRepo("repository")
    @Test
    public void testFetchRemoteSshRepository() throws IOException {
        // Start SSH test server to fetch from
        sshServer.start();

        String sshUrl = sshServer.getCloneUrl("some_user", diffTree.getId());
        ByteString privateKey = sshServer.generateUserCredentials("some_user");

        FetchResult result = fetchFromRepository(RpcAnyRepository.newBuilder()
                .setRemoteRepository(RpcRemoteRepository.newBuilder()
                        .setPrivateKey(RpcPrivateKeyCredentials.newBuilder().setPrivateKey(privateKey))
                        .setUrl(sshUrl)));

        assertThat(result.getFailedRefs(), emptyIterable());
        List<RefUpdate> refUpdates = result.getRefUpdates();
        assertEquals(3, refUpdates.size());
        assertEquals(RefUpdate.of("refs/heads/fetched/cr-lf-branch", NULL_SHA1, "83f0ca32d75e3121c57a3cc2d549a8a01a1cfafc"),
                refUpdates.get(0));
    }

    @Test
    public void testFetchRemoteSshRepositoryThrowsWhenAuthenticationFails() throws IOException {
        sshServer.start();

        String sshUrl = sshServer.getCloneUrl("some_user", diffTree.getId());
        // Use the wrong private key for the user in the clone URL
        ByteString privateKey = sshServer.generateUserCredentials("some_other_user");

        StatusRuntimeException e = assertThrows(StatusRuntimeException.class, () ->
                fetchFromRepository(RpcAnyRepository.newBuilder()
                        .setRemoteRepository(RpcRemoteRepository.newBuilder()
                                .setPrivateKey(RpcPrivateKeyCredentials.newBuilder().setPrivateKey(privateKey))
                                .setUrl(sshUrl))));
        RpcGitError error = RpcGitErrorUtils.fromThrowable(e);
        assertNotNull(error);
        assertTrue(error.hasRemoteAuthenticationFailed());
    }

    @Test
    public void testFetchRemoteSshRepositoryThrowsWhenNoPrivateKeySupplied() throws IOException {
        sshServer.start();

        String sshUrl = sshServer.getCloneUrl("some_user", diffTree.getId());
        StatusRuntimeException exception = assertThrows(StatusRuntimeException.class,
                () -> fetchFromRepository(RpcAnyRepository.newBuilder()
                        .setRemoteRepository(RpcRemoteRepository.newBuilder()
                                .setUrl(sshUrl))));

        RpcGitError error = RpcGitErrorUtils.fromThrowable(exception);
        assertNotNull(error);
        assertTrue(error.hasRemoteAuthenticationFailed());
    }

    @Test
    public void testForkRepository() {
        Path repoDir = repository.getPath().resolveSibling("917343");
        String repositoryId = repositoryId(917343);
        try {
            stub.create(RpcCreateRepositoryRequest.newBuilder()
                    .setRepository(repositoryId)
                    .setOrigin(repository.getId())
                    .build());

            assertFalse(stub.isEmpty(RpcIsRepositoryEmptyRequest.newBuilder()
                    .setRepository(repositoryId)
                    .build())
                    .getEmpty());
            assertEquals("refs/heads/master", stub.getDefaultBranch(RpcGetDefaultBranchRequest.newBuilder()
                    .setRepository(repositoryId)
                    .build())
                    .getBranchId()
                    .toStringUtf8());
        } finally {
            MoreFiles.deleteQuietly(repoDir);
        }
    }

    @Test
    public void testGetDefaultBranch() {
        String defaultBranch = getDefaultBranch(repository);

        assertNotNull(defaultBranch);
        assertEquals("refs/heads/master", defaultBranch);
    }

    @Test
    public void testGetDetails() {
        String repositoryId = ManagedRepository.formatId(4, "6743723452322345235a", "71622");
        Map<String, String> metadata = ImmutableMap.of("foo", "bar");
        stub.create(RpcCreateRepositoryRequest.newBuilder()
                .putAllMetadata(metadata)
                .setRepository(repositoryId)
                .setDefaultBranch(ByteString.copyFromUtf8("refs/heads/magic"))
                .build());
        stub.setTranscodeEnabled(RpcSetTranscodeEnabledRequest.newBuilder()
                .setEnabled(true)
                .setRepository(repositoryId)
                .build());

        try {
            RpcGetRepositoryDetailsResponse response = stub.getDetails(RpcGetRepositoryDetailsRequest.newBuilder()
                    .setRepository(repositoryId)
                    .build());

            assertNotNull(response);
            assertEquals(metadata, response.getMetadataMap());
            assertTrue(response.getTranscodeEnabled());
            assertEquals("refs/heads/magic", response.getDefaultBranch().toStringUtf8());
        } finally {
            stub.delete(RpcDeleteRepositoryRequest.newBuilder()
                    .setRepository(repositoryId)
                    .build());
        }
    }

    @Test
    public void testGetReplicaDetails() {
        RpcRepositoryReplicaDetails details = stub.getReplicaDetails(RpcGetRepositoryReplicaDetailsRequest.newBuilder()
                .setRepository(repository.getId())
                .build())
                .getDetails();

        assertNotNull(details);
        assertEquals("af214f95e1876b5e2b86d502ab68b5d9abcad3b9e9f37c08603112232da12a86", details.getContentHash());
        assertEquals("a00d43b206831512d56636f166a149d9f2164beadd50c30f0918cb876b83c2c4", details.getMetadataHash());
        assertEquals(RpcReplicaState.REPLICA_STATE_CONSISTENT, details.getReplicaState());
        assertThat(details.getObservedVersion(), greaterThanOrEqualTo(2L));
        assertThat(details.getVersion(), greaterThanOrEqualTo(2L));
    }

    @Test
    public void testGetSize() {
        RpcGetRepositorySizeRequest.Builder builder = RpcGetRepositorySizeRequest.newBuilder()
                .setRepository(repository.getId())
                .setTimeouts(RpcGitTimeouts.newBuilder()
                        .setExecution(100L)
                        .setIdle(100L)
                        .build());

        RpcRepositorySize size = stub.getSize(builder.build()).getSize();
        assertEquals(0L, size.getGarbageSize());
        assertEquals(0L, size.getInPackSize());
        assertThat(size.getLooseSize(), greaterThanOrEqualTo(10L));
    }

    @Test
    public void testGetSizeRemote() throws Exception {
        File objects = repository.getPath().resolve("objects").toFile();
        File sharedObjects =
                ((ManagedRepository) managedRepository).getSharedObjects().getPath().resolve("objects").toFile();
        FileUtils.copyDirectory(objects, sharedObjects);

        RpcGetRepositorySizeRequest.Builder builder = RpcGetRepositorySizeRequest.newBuilder()
                .setRepository(managedRepository.getId())
                .setTimeouts(RpcGitTimeouts.newBuilder()
                        .setExecution(100L)
                        .setIdle(100L)
                        .build());

        RpcGetRepositorySizeResponse response = stub.getSize(builder.build());

        RpcRepositorySize size = response.getSize();
        assertEquals(0L, size.getGarbageSize());
        assertThat(size.getInPackSize(), greaterThanOrEqualTo(148L));
        assertThat(size.getLooseSize(), greaterThanOrEqualTo(5L));

        RpcRepositorySize sharedSize = response.getSharedSize();
        assertEquals(0L, sharedSize.getGarbageSize());
        assertEquals(0L, sharedSize.getInPackSize());
        assertThat(sharedSize.getLooseSize(), greaterThanOrEqualTo(5L));
    }

    @Test
    public void testIsEmpty() {
        boolean empty = stub.isEmpty(RpcIsRepositoryEmptyRequest.newBuilder().setRepository(repository.getId()).build())
                .getEmpty();
        assertFalse(empty);
    }

    @DirtiesRepo("repository")
    @Test
    public void testPushRemoteHttpRepository() {
        disableGitHooks();
        httpServer.start();

        String sshUrl = httpServer.getCloneUrl("some_user", repository.getId());
        httpServer.addCredentials("some_user", "some_password");

        PushResult callback = pushToRepository(RpcRemoteRepository.newBuilder()
                .setUsernamePassword(RpcUsernamePasswordCredentials.newBuilder()
                        .setUsername(ByteString.copyFromUtf8("some_user"))
                        .setPassword(ByteString.copyFromUtf8("some_password")))
                .setUrl(sshUrl));

        assertThat(callback.getFailedRefs(), emptyIterable());
        List<RefUpdate> refUpdates = callback.getRefUpdates();
        assertEquals(3, refUpdates.size());
        assertEquals(RefUpdate.of("refs/heads/cr-lf-branch", NULL_SHA1, "83f0ca32d75e3121c57a3cc2d549a8a01a1cfafc"),
                refUpdates.get(1)); //0 is master
    }

    @Test
    public void testPushRemoteHttpRepositoryThrowsWhenAuthenticationFails() {
        httpServer.start();

        String sshUrl = httpServer.getCloneUrl("some_user", repository.getId());
        StatusRuntimeException e = assertThrows(StatusRuntimeException.class, () ->
                pushToRepository(RpcRemoteRepository.newBuilder()
                        .setUsernamePassword(RpcUsernamePasswordCredentials.newBuilder()
                                .setUsername(ByteString.copyFromUtf8("some_other_user"))
                                .setPassword(ByteString.copyFromUtf8("some_other_password")))
                        .setUrl(sshUrl)));

        RpcGitError error = RpcGitErrorUtils.fromThrowable(e);
        assertNotNull(error);
        assertTrue(error.hasRemoteAuthenticationFailed());
    }

    @Test
    public void testPushRemoteHttpRepositoryThrowsWhenNoPasswordSupplied() {
        httpServer.start();

        String sshUrl = httpServer.getCloneUrl("some_user", repository.getId());
        StatusRuntimeException e = assertThrows(StatusRuntimeException.class,
                () -> pushToRepository(RpcRemoteRepository.newBuilder().setUrl(sshUrl)));

        RpcGitError error = RpcGitErrorUtils.fromThrowable(e);
        assertNotNull(error);
        assertTrue(error.hasRemoteAuthenticationFailed());
    }

    @DirtiesRepo("repository")
    @Test
    public void testPushRemoteSshRepository() throws IOException {
        disableGitHooks();
        sshServer.start();

        String sshUrl = sshServer.getCloneUrl("some_user", repository.getId());
        ByteString privateKey = sshServer.generateUserCredentials("some_user");

        PushResult callback = pushToRepository(RpcRemoteRepository.newBuilder()
                .setPrivateKey(RpcPrivateKeyCredentials.newBuilder().setPrivateKey(privateKey))
                .setUrl(sshUrl));

        assertThat(callback.getFailedRefs(), emptyIterable());
        List<RefUpdate> refUpdates = callback.getRefUpdates();
        assertEquals(3, refUpdates.size());
        assertEquals(RefUpdate.of("refs/heads/cr-lf-branch", NULL_SHA1, "83f0ca32d75e3121c57a3cc2d549a8a01a1cfafc"),
                refUpdates.get(1)); //0 is master
    }

    @Test
    public void testPushRemoteSshRepositoryThrowsWhenAuthenticationFails() throws IOException {
        // Create a SSH test server to fetch from
        sshServer.start();

        String sshUrl = sshServer.getCloneUrl("some_user", repository.getId());
        // Use the wrong private key for the user in the clone URL
        ByteString privateKey = sshServer.generateUserCredentials("some_other_user");
        StatusRuntimeException e = assertThrows(StatusRuntimeException.class, () ->
                pushToRepository(RpcRemoteRepository.newBuilder()
                        .setPrivateKey(RpcPrivateKeyCredentials.newBuilder().setPrivateKey(privateKey))
                        .setUrl(sshUrl)));

        RpcGitError error = RpcGitErrorUtils.fromThrowable(e);
        assertNotNull(error);
        assertTrue(error.hasRemoteAuthenticationFailed());
    }

    @Test
    public void testSetDefaultBranch() {
        String branchId = "refs/heads/basic_branching";
        RpcSetDefaultBranchRequest.Builder builder = RpcSetDefaultBranchRequest.newBuilder()
                .setBranchId(ByteString.copyFromUtf8(branchId))
                .setTimeouts(TIMEOUTS);

        setDefaultBranch(repository, builder);

        String defaultBranchId = getDefaultBranch(repository);

        assertNotNull(defaultBranchId);
        assertEquals(branchId, defaultBranchId);
    }

    @Test
    public void testSetDefaultBranchInvalid() {
        String branchId = "../../../../../../../../../../../../etc/private.txt";
        RpcSetDefaultBranchRequest.Builder builder = RpcSetDefaultBranchRequest.newBuilder()
                .setBranchId(ByteString.copyFromUtf8(branchId))
                .setTimeouts(TIMEOUTS);

        StatusRuntimeException e = assertThrows(StatusRuntimeException.class,
                () -> setDefaultBranch(repository, builder));

        assertEquals(Status.INVALID_ARGUMENT.getCode(), e.getStatus().getCode());
        RpcGitError error = RpcGitErrorUtils.fromThrowable(e);
        assertNotNull(error);
        assertTrue(error.hasInvalidRefName());
    }

    @Test
    public void testSetGarbageCollectionPaused() {
        //This tests against a BitbucketServerRepository because gc pausing in only supported for such repositories

        //Verify that pausing GC creates a lock file in for the gc.log
        assertTrue(stub.setGarbageCollectionPaused(RpcSetGarbageCollectionPausedRequest.newBuilder()
                .setRepository(sidecarRepository.getId())
                .setPaused(true)
                .build())
                .getResult());

        Path lockFile = sidecarRepository.getPath().resolve(GC_LOG + ".lock");
        assertTrue(Files.isRegularFile(lockFile));

        //Verify that resuming GC removes the lock file
        assertTrue(stub.setGarbageCollectionPaused(RpcSetGarbageCollectionPausedRequest.newBuilder()
                .setRepository(sidecarRepository.getId())
                .setPaused(false)
                .build())
                .getResult());
        assertFalse(Files.isRegularFile(lockFile));

        //Verify that resuming GC when a lock file does not exist, is a no-op
        assertTrue(stub.setGarbageCollectionPaused(RpcSetGarbageCollectionPausedRequest.newBuilder()
                .setRepository(sidecarRepository.getId())
                .setPaused(false)
                .build())
                .getResult());
        assertFalse(Files.isRegularFile(lockFile));
    }

    @Test
    public void testVerifyConsistencyBitbucketServerRepo() {
        RpcVerifyRepositoryConsistencyResponse response = stub.verifyConsistency(
                RpcVerifyRepositoryConsistencyRequest.newBuilder()
                        .setRepository(sidecarRepository.getId())
                        .build());
        assertEquals(RpcVerifyRepositoryConsistencyResponse.getDefaultInstance(), response);
    }

    @Test
    public void testVerifyConsistencyManagedRepo() {
        RpcVerifyRepositoryConsistencyResponse response = stub.verifyConsistency(
                RpcVerifyRepositoryConsistencyRequest.newBuilder()
                        .setRepository(managedRepository.getId())
                        .build());

        //Verify a summary was returned with a content-hash and a version
        RpcRepositoryConsistencySummary summary = response.getSummary();
        assertNotEquals(RpcRepositoryConsistencySummary.getDefaultInstance(), summary);
        assertThat(summary.getVersion(), greaterThan(1L)); //Repository creation already puts the ID a 2
        assertNotEquals(RefHasher.NULL_HASH, summary.getContentHash());
        assertThat(summary.getConsistentReplicasList(), contains(applicationConfig.getNode().getId()));
        assertThat(summary.getInconsistentReplicasList(), empty());
        assertThat(summary.getUnavailableReplicasList(), empty());
    }

    private static String repositoryId(int repoId) {
        return "ds/0/h/" + HIERARCHY + "/r/" + repoId;
    }

    private void checkDeleteAlsoDeletesLfsDirectory(boolean onlyMember) {
        Path repoDir = repository.getPath().getParent().resolve("917348");
        String repositoryId = repositoryId(917348);

        try {
            Repository created = repositoryManager.create(new CreateRepositoryParameters.Builder(repositoryId)
                    .metadata(ImmutableMap.of("hierarchy", HIERARCHY))
                    .build());

            //The LFS directory may not exist until first object upload, so create it manually for this test
            Path lfsDir = created.getLfsDir();
            MoreFiles.mkdir(lfsDir);

            stub.delete(RpcDeleteRepositoryRequest.newBuilder()
                    .setHierarchyMembership(RpcHierarchyMembership.newBuilder()
                            .setOnlyMember(onlyMember)
                            .build())
                    .setRepository(repositoryId)
                    .build());

            //If onlyMember == true then the LFS hierarchy directory should be deleted
            assertNotEquals(onlyMember, Files.isDirectory(lfsDir));
        } finally {
            MoreFiles.deleteQuietly(repoDir);
        }
    }

    private void disableGitHooks() {
        //Disable the central git hooks on repository to allow the push to complete
        builderFactory.builder(repository).config().set("core.hooksPath", "dont-run").build().run();
    }

    private FetchResult fetchFromRepository(RpcAnyRepository.Builder sourceRepository) {
        RpcFetchRequest.Builder builder = RpcFetchRequest.newBuilder()
                .setRepository(repository.getId())
                .addRefspecs(ByteString.copyFromUtf8("+refs/heads/*:refs/heads/fetched/*"))
                .setSourceRepository(sourceRepository);

        Iterator<RpcFetchResponseFragment> it = stub.fetch(builder.build());
        FetchResult result = new FetchResult();
        while (it.hasNext()) {
            result.add(it.next());
        }
        return result;
    }

    private String getDefaultBranch(Repository repository) {
        return stub.getDefaultBranch(RpcGetDefaultBranchRequest.newBuilder()
                .setRepository(repository.getId())
                .build())
                .getBranchId()
                .toStringUtf8();
    }

    private List<RpcRepository> getRepositories(RpcGetRepositoriesRequest request) {
        Iterator<RpcGetRepositoriesResponseFragment> it = stub.getRepositories(request);
        ImmutableList.Builder<RpcRepository> builder = ImmutableList.builder();
        while (it.hasNext()) {
            RpcGetRepositoriesResponseFragment fragment = it.next();
            builder.addAll(fragment.getRepositoriesList());
        }
        return builder.build();
    }

    private PushResult pushToRepository(RpcRemoteRepository.Builder targetRepository) {
        RpcPushRequest.Builder builder = RpcPushRequest.newBuilder()
                .addRefspecs(ByteString.copyFromUtf8("+refs/heads/*:refs/heads/*"))
                .setRepository(diffTree.getId())
                .setTargetRepository(targetRepository);

        Iterator<RpcPushResponseFragment> it = stub.push(builder.build());
        PushResult result = new PushResult();
        while (it.hasNext()) {
            RpcPushResponseFragment response = it.next();
            if (response.hasResult()) {
                result.add(response.getResult());
            }
        }
        return result;
    }

    private void setDefaultBranch(Repository repository, RpcSetDefaultBranchRequest.Builder builder) {
        RpcSetDefaultBranchResponse response = stub.setDefaultBranch(builder.setRepository(repository.getId()).build());
        assertNotNull(response);
    }
}
