package com.atlassian.bitbucket.mesh.git;

import com.atlassian.bitbucket.mesh.AbstractGrpcTest;
import com.atlassian.bitbucket.mesh.git.binary.GitBinary;
import com.atlassian.bitbucket.mesh.git.builder.GitProcessBuilderFactory;
import com.atlassian.bitbucket.mesh.git.transcode.TranscodeService;
import com.atlassian.bitbucket.mesh.junit.LogListener;
import com.atlassian.bitbucket.mesh.process.handler.StringStdioHandler;
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.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.protobuf.ByteString;
import io.grpc.Deadline;
import io.grpc.ManagedChannel;
import org.junit.Rule;
import org.junit.Test;

import javax.annotation.Resource;
import java.nio.file.Files;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import java.util.function.BiConsumer;
import java.util.stream.Collectors;

import static org.hamcrest.Matchers.containsString;

public class GrpcTransactionServiceTest extends AbstractGrpcTest {

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

    @Resource
    private GitAgent agent;
    @Resource
    private GitBinary binary;
    @Resource
    private GitProcessBuilderFactory builderFactory;
    @Resource
    private ManagedChannel managedChannel;
    @Resource
    private RepositoryManager repositoryManager;
    @Resource
    private TranscodeService transcodeService;
    @Repo(type = Repo.Type.MANAGED)
    private Repository repository;

    @DirtiesRepo
    @Test
    public void testApplyTransactions() {
        testWithManagedHierarchy((origin, fork) -> {
            String hierarchyId = ((ManagedRepository) origin).getHierarchyId();
            String fqHierarchyId = getQualifiedHierarchyId(origin.getId());
            String fork2Id = fqHierarchyId + "fork2";

            List<RpcTransaction> transactions = ImmutableList.<RpcTransaction>builder()
                    //Update 2 refs
                    .add(transaction(origin)
                            .setUpdateRefs(RpcUpdateRefsRequest.newBuilder()
                                    .addRefUpdates(RpcProposedRefUpdate.newBuilder()
                                            .setRefId(ByteString.copyFromUtf8("refs/heads/new-branch-1"))
                                            .setNewHash("53979b3304bece75a3852a3d250dbaed64d45430"))
                                    .addRefUpdates(RpcProposedRefUpdate.newBuilder()
                                            .setRefId(ByteString.copyFromUtf8("refs/heads/master"))
                                            .setNewHash("9c05f43f859375e392d90d23a13717c16d0fdcda"))
                                    .setRepository(origin.getId()))
                            .build())
                    //Create a pull request ref (public and dark)
                    .add(transaction(fork)
                            .setUpdateRefs(RpcUpdateRefsRequest.newBuilder()
                                    .addRefUpdates(RpcProposedRefUpdate.newBuilder()
                                            .setRefId(ByteString.copyFromUtf8("dark-refs/pull-requests/123/from"))
                                            .setNewHash("d6edcbf924697ab811a867421dab60d954ccad99"))
                                    .addRefUpdates(RpcProposedRefUpdate.newBuilder()
                                            .setRefId(ByteString.copyFromUtf8("refs/pull-requests/123/from"))
                                            .setNewHash("d6edcbf924697ab811a867421dab60d954ccad99"))
                                    .setRepository(fork.getId()))
                            .build())
                    //Update the default branch
                    .add(transaction(fork)
                            .setSetDefaultBranch(RpcSetDefaultBranchRequest.newBuilder()
                                    .setBranchId(ByteString.copyFromUtf8("refs/heads/basic_branching"))
                                    .setRepository(fork.getId()))
                            .build())
                    //Update some metadata
                    .add(transaction(origin)
                            .setSetMetadata(RpcSetMetadataRequest.newBuilder()
                                    .putMetadata("project", "NEW-PROJECT")
                                    .putMetadata("hierarchy", hierarchyId)
                                    .putMetadata("repository", "fork-renamed")
                                    .setRepository(origin.getId()))
                            .build())
                    //Create a new fork in the hierarchy
                    .add(transaction(fork2Id)
                            .setCreateRepository(RpcCreateRepositoryRequest.newBuilder()
                                    .putMetadata("project", "PROJ3")
                                    .putMetadata("hierarchy", hierarchyId)
                                    .putMetadata("repository", "fork2")
                                    .setDefaultBranch(ByteString.copyFromUtf8("refs/heads/master"))
                                    .setOrigin(origin.getId())
                                    .setRepository(fork2Id))
                            .build())
                    //Enable transcoding
                    .add(transaction(fork2Id)
                            .setSetTranscode(RpcSetTranscodeEnabledRequest.newBuilder()
                                    .setEnabled(true)
                                    .setRepository(fork2Id))
                            .build())
                    //And finally a transaction that should fail. Update ref to a non-existing commit
                    .add(transaction(origin)
                            .setUpdateRefs(RpcUpdateRefsRequest.newBuilder()
                                    .addRefUpdates(RpcProposedRefUpdate.newBuilder()
                                            .setRefId(ByteString.copyFromUtf8("refs/heads/this-should-fail"))
                                            .setNewHash("00000000001111111111222222222223333333333"))
                                    .setRepository(origin.getId()))
                            .build())
                    .build();

            try {
                RpcApplyTransactionsResponse response = stub()
                        .withDeadline(Deadline.after(10, TimeUnit.SECONDS))
                        .applyTransactions(RpcApplyTransactionsRequest.newBuilder()
                                .addAllTransactions(transactions)
                                .build());

                //Verify the response
                assertNotNull(response);
                assertEquals(transactions.size() - 1, response.getSuccessCount());
                assertTrue(response.getGitError().hasFailedCommand());
                assertThat(response.getGitError().getFailedCommand().getStderr(),
                        containsString("00000000001111111111222222222223333333333"));

                //Verify the state of the repositories
                assertRefs(origin, ImmutableMap.of(
                        "refs/heads/new-branch-1", "53979b3304bece75a3852a3d250dbaed64d45430",
                        "refs/heads/master", "9c05f43f859375e392d90d23a13717c16d0fdcda"));
                assertRefs(fork, ImmutableMap.of(
                        "refs/pull-requests/123/from", "d6edcbf924697ab811a867421dab60d954ccad99",
                        "dark-refs/pull-requests/123/from", "d6edcbf924697ab811a867421dab60d954ccad99",
                        "HEAD", "d6edcbf924697ab811a867421dab60d954ccad99" //basic_branching
                ));
                assertTrue(Files.isRegularFile(fork.getPath().resolve("logs/dark-refs/pull-requests/123/from")));
                Repository fork2 = repositoryManager.getById(fork2Id);
                assertRefs(fork2, ImmutableMap.of(
                        "refs/heads/new-branch-1", "53979b3304bece75a3852a3d250dbaed64d45430",
                        "refs/heads/master", "9c05f43f859375e392d90d23a13717c16d0fdcda",
                        "HEAD", "9c05f43f859375e392d90d23a13717c16d0fdcda" //master
                ));
                assertTrue(transcodeService.isEnabled(fork2));
                String metadataProject = builderFactory.builder(origin).config()
                        .get("bitbucket.project")
                        .build(new StringStdioHandler())
                        .run();
                assertEquals("NEW-PROJECT", metadataProject);

                //Verify that the failed transaction has been logged
                logListener.assertError("Failed to apply transaction 6: repository: \"" + repository.getId() + "\"");
                logListener.assertError("refs/heads/this-should-fail");
            } finally {
                deleteRepository(repositoryManager.getById(fork2Id));
            }
        });
    }

    @Test
    public void testDeleteRepository() {
        testWithManagedHierarchy((origin, fork) -> {

            RpcApplyTransactionsResponse response = stub().applyTransactions(RpcApplyTransactionsRequest.newBuilder()
                    .addAllTransactions(ImmutableList.of(transaction(fork)
                            .setDeleteRepository(RpcDeleteRepositoryRequest.newBuilder()
                                    .setRepository(fork.getId())
                                    .build())
                            .build()))
                    .build());

            assertEquals(RpcApplyTransactionsResponse.newBuilder()
                    .setSuccessCount(1)
                    .build(), response);

            assertNull(repositoryManager.findById(fork.getId()));
            assertNotNull(repositoryManager.findById(origin.getId()));
        });
    }

    private static String getQualifiedHierarchyId(String repositoryId) {
        int index = repositoryId.lastIndexOf('/');
        assertNotEquals(-1, index);
        return repositoryId.substring(0, index + 1);
    }

    private static RpcTransaction.Builder transaction(Repository repository) {
        return transaction(repository.getId());
    }

    private static RpcTransaction.Builder transaction(String repositoryId) {
        return RpcTransaction.newBuilder()
                .setRepository(repositoryId)
                .setTimestamp(System.currentTimeMillis())
                .setPerson(RpcPerson.newBuilder()
                        .setName(ByteString.copyFromUtf8("User"))
                        .setEmail("user@email.com"));
    }

    private void assertRefs(Repository repository, Map<String, String> expectedRefs) {
        Map<String, String> actualRefs = agent.shallowResolveRefs(repository, expectedRefs.keySet(), true).entrySet()
                .stream()
                .collect(Collectors.toMap(
                        Map.Entry::getKey,
                        entry -> entry.getValue() == null ? null : entry.getValue().getLatestCommit()));
        assertEquals(expectedRefs, actualRefs);
    }

    private void deleteRepository(Repository repository) {
        if (repository != null) {
            try {
                repositoryManager.delete(new DeleteRepositoryParameters.Builder(repository).build());
            } catch (RepositoryNotFoundException e) {
                //already deleted
            }
        }
    }

    private void testWithManagedHierarchy(BiConsumer<Repository, Repository> callback) {
        Repository fork = null;
        try {
            //First prepare a repository with a fork
            String forkId = getQualifiedHierarchyId(repository.getId()) + "fork";
            fork = repositoryManager.create(new CreateRepositoryParameters.Builder(forkId)
                    .metadata(ImmutableMap.of(
                            "project", "PROJ",
                            "hierarchy", ((ManagedRepository) repository).getHierarchyId(),
                            "repository", "fork"))
                    .defaultBranch("refs/heads/master")
                    .origin(repository)
                    .build());
            builderFactory.builder(fork)
                    .fetch()
                    .refspec("+refs/tags/*:refs/tags/*")
                    .repository(repository)
                    .build(new StringStdioHandler())
                    .run();

            callback.accept(repository, fork);
        } finally {
            deleteRepository(fork);
        }
    }

    private TransactionServiceGrpc.TransactionServiceBlockingStub stub() {
        return TransactionServiceGrpc.newBlockingStub(managedChannel);
    }
}
