package com.atlassian.bitbucket.mesh.git;

import com.atlassian.bitbucket.mesh.AbstractGrpcServiceTest;
import com.atlassian.bitbucket.mesh.repository.Repository;
import com.atlassian.bitbucket.mesh.rpc.v1.*;
import com.atlassian.bitbucket.mesh.test.HasGitError;
import com.atlassian.bitbucket.mesh.test.annotation.Repo;
import com.google.protobuf.ByteString;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;

import java.util.Arrays;

import static com.atlassian.bitbucket.mesh.rpc.v1.RpcNodeType.*;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.greaterThanOrEqualTo;

@SuppressWarnings("deprecation")
public class GrpcContentServiceTest extends AbstractGrpcServiceTest {

    @Rule
    public final ExpectedException thrown = ExpectedException.none();

    @Repo(GitRepositories.QA_RESOURCES)
    private Repository qaResources;
    @Repo
    private Repository repository;
    @Repo(GitRepositories.SUBREPOSITORIES)
    private Repository subrepositories;

    @Test
    public void testGetNodeType() {
        RpcNodeType type = getNodeType(repository, RpcGetNodeTypeRequest.newBuilder()
                .setCommitish(ByteString.copyFromUtf8("master"))
                .setPath(ByteString.copyFromUtf8("modification")));
        assertSame(NODE_DIRECTORY, type);
    }

    @Test
    public void testGetNodeTypeForPathsWithColons() {
        RpcGetNodeTypeRequest.Builder builder = RpcGetNodeTypeRequest.newBuilder()
                .setCommitish(ByteString.copyFromUtf8("master"));

        for (String path : Arrays.asList("::test", ":test", "te:st", "test:", "test::")) {
            RpcNodeType type = getNodeType(qaResources, builder.setPath(ByteString.copyFromUtf8("colons/" + path)));
            assertSame(NODE_DIRECTORY, type);
        }
    }

    @Test
    public void testGetNodeTypeForPathsWithSpaces() {
        RpcGetNodeTypeRequest.Builder builder = RpcGetNodeTypeRequest.newBuilder()
                .setCommitish(ByteString.copyFromUtf8("master"));

        for (String path : Arrays.asList(" leading-space", "middle space", "trailing-space ")) {
            RpcNodeType type = getNodeType(qaResources, builder.setPath(ByteString.copyFromUtf8("spaces/" + path)));
            assertSame(NODE_DIRECTORY, type);
        }
    }

    @Test
    public void testGetNodeTypeForRoot() {
        RpcNodeType type = getNodeType(repository, RpcGetNodeTypeRequest.newBuilder()
                .setCommitish(ByteString.copyFromUtf8("master")));
        assertSame(NODE_DIRECTORY, type);
    }

    @Test
    public void testGetNodeTypeForSubmodule() {
        RpcNodeType type = getNodeType(subrepositories, RpcGetNodeTypeRequest.newBuilder()
                .setCommitish(ByteString.copyFromUtf8("master"))
                .setPath(ByteString.copyFromUtf8("submodule/stash")));
        assertSame(NODE_SUBMODULE, type);
    }

    @Test
    public void testGetNodeTypeForUnicodePath() {
        RpcNodeType type = getNodeType(qaResources, RpcGetNodeTypeRequest.newBuilder()
                .setCommitish(ByteString.copyFromUtf8("master"))
                .setPath(ByteString.copyFromUtf8("国際交流基金海外運営専門員.txt")));
        assertSame(NODE_FILE, type);
    }

    @Test
    public void testGetNodeTypeWithNonexistentCommit() {
        thrown.expect(HasGitError.noSuchObject("nonexistent"));

        //Uses git cat-file because there's no path
        getNodeType(repository, RpcGetNodeTypeRequest.newBuilder()
                .setCommitish(ByteString.copyFromUtf8("nonexistent")));
    }

    @Test
    public void testGetNodeTypeWithNonexistentCommitWithPath() {
        thrown.expect(HasGitError.noSuchObject("nonexistent"));

        //Uses git ls-tree because there is a path
        getNodeType(repository, RpcGetNodeTypeRequest.newBuilder()
                .setCommitish(ByteString.copyFromUtf8("nonexistent"))
                .setPath(ByteString.copyFromUtf8("irrelevant")));
    }

    @Test
    public void testGetNodeTypeWithNonexistentDirectory() {
        thrown.expect(HasGitError.noSuchPath("master", "nonexistent"));

        getNodeType(repository, RpcGetNodeTypeRequest.newBuilder()
                .setCommitish(ByteString.copyFromUtf8("master"))
                .setPath(ByteString.copyFromUtf8("nonexistent/file.txt")));
    }

    @Test
    public void testGetNodeTypeWithNonexistentFile() {
        thrown.expect(HasGitError.noSuchPath("master", "nonexistent"));

        getNodeType(repository, RpcGetNodeTypeRequest.newBuilder()
                .setCommitish(ByteString.copyFromUtf8("master"))
                .setPath(ByteString.copyFromUtf8("nonexistent")));
    }

    @Test
    public void testGetNodeTypeWithTrailingSlash() {
        RpcNodeType type = getNodeType(qaResources, RpcGetNodeTypeRequest.newBuilder()
                .setCommitish(ByteString.copyFromUtf8("master"))
                .setPath(ByteString.copyFromUtf8("SourceView/")));
        assertSame(NODE_DIRECTORY, type);
    }

    @Test
    public void testGetObjectInfoWithColonPaths() {
        //Each of these directories contains a file.txt. Unfortunately, they don't have a consistent size and different
        // hash, so the validation here needs to be a bit flexible
        for (String path : Arrays.asList("::test", ":test", "te:st", "test:", "test::")) {
            RpcGetObjectInfoResponse response = getObjectInfoResponse(qaResources, RpcGetObjectInfoRequest.newBuilder()
                    .setCommitish(ByteString.copyFromUtf8("master"))
                    .setPath(ByteString.copyFromUtf8("colons/" + path + "/file.txt")));

            RpcGitObjectInfo objectInfo = response.getObjectInfo();
            assertThat(objectInfo.getSize(), greaterThanOrEqualTo(15L)); //Actual range, 18-20
            assertThat(objectInfo.getType(), equalTo(RpcGitObjectType.OBJECT_BLOB));
        }
    }

    @Test
    public void testGetObjectInfoWithCommitOnly() {
        RpcGetObjectInfoResponse response = getObjectInfoResponse(repository, RpcGetObjectInfoRequest.newBuilder()
                .setCommitish(ByteString.copyFromUtf8("master")));

        RpcGitObjectInfo objectInfo = response.getObjectInfo();
        assertEquals("0a943a29376f2336b78312d99e65da17048951db", objectInfo.getObjectId().toStringUtf8());
        assertEquals(280L, objectInfo.getSize());
        assertEquals(RpcGitObjectType.OBJECT_COMMIT, objectInfo.getType());
    }

    @Test
    public void testGetObjectInfoWithNoCommitFound() {
        thrown.expect(HasGitError.noSuchCommit("nonexistent"));

        getObjectInfoResponse(repository, RpcGetObjectInfoRequest.newBuilder()
                .setCommitish(ByteString.copyFromUtf8("nonexistent")));
    }

    @Test
    public void testGetObjectInfoWithNoCommitFoundWithPath() {
        thrown.expect(HasGitError.noSuchCommit("nonexistent"));

        getObjectInfoResponse(repository, RpcGetObjectInfoRequest.newBuilder()
                .setCommitish(ByteString.copyFromUtf8("nonexistent"))
                .setPath(ByteString.copyFromUtf8("irrelevant")));
    }

    @Test
    public void testGetObjectInfoWithNoPathFound() {
        thrown.expect(HasGitError.noSuchPath("master", "nonexistent"));

        getObjectInfoResponse(repository, RpcGetObjectInfoRequest.newBuilder()
                .setCommitish(ByteString.copyFromUtf8("master"))
                .setPath(ByteString.copyFromUtf8("nonexistent")));
    }

    @Test
    public void testGetObjectInfoWithNoPathFoundWithNonexistentDirectory() {
        thrown.expect(HasGitError.noSuchPath("master", "nonexistent/file.txt"));

        getObjectInfoResponse(repository, RpcGetObjectInfoRequest.newBuilder()
                .setCommitish(ByteString.copyFromUtf8("master"))
                .setPath(ByteString.copyFromUtf8("nonexistent/file.txt")));
    }

    @Test
    public void testGetObjectInfoWithNoPathFoundWithNonexistentFile() {
        thrown.expect(HasGitError.noSuchPath("master", "nonexistent.txt"));

        getObjectInfoResponse(repository, RpcGetObjectInfoRequest.newBuilder()
                .setCommitish(ByteString.copyFromUtf8("master"))
                .setPath(ByteString.copyFromUtf8("nonexistent.txt")));
    }

    @Test
    public void testGetObjectInfoWithPath() {
        RpcGetObjectInfoResponse response = getObjectInfoResponse(repository, RpcGetObjectInfoRequest.newBuilder()
                .setCommitish(ByteString.copyFromUtf8("master"))
                .setPath(ByteString.copyFromUtf8("modification/mod_file.txt")));

        RpcGitObjectInfo objectInfo = response.getObjectInfo();
        assertEquals("b1d99e581f0bb8138b8997f9268294e41faee05b", objectInfo.getObjectId().toStringUtf8());
        assertEquals(33L, objectInfo.getSize());
        assertEquals(RpcGitObjectType.OBJECT_BLOB, objectInfo.getType());
    }

    @Test
    public void testGetObjectInfoWithSpacedPaths() {
        //Each of these directories contains a file.txt. Unfortunately, they don't have a consistent size and different
        // hash, so the validation here needs to be a bit flexible
        for (String path : Arrays.asList(" leading-space", "middle space", "trailing-space ")) {
            RpcGetObjectInfoResponse response = getObjectInfoResponse(qaResources, RpcGetObjectInfoRequest.newBuilder()
                    .setCommitish(ByteString.copyFromUtf8("master"))
                    .setPath(ByteString.copyFromUtf8("spaces/" + path + "/file.txt")));

            RpcGitObjectInfo objectInfo = response.getObjectInfo();
            assertThat(objectInfo.getSize(), greaterThanOrEqualTo(30L)); //Actual range, 34-36
            assertThat(objectInfo.getType(), equalTo(RpcGitObjectType.OBJECT_BLOB));
        }
    }

    @Test
    public void testGetObjectInfoWithTrailingSlashPath() {
        RpcGetObjectInfoResponse response = getObjectInfoResponse(qaResources, RpcGetObjectInfoRequest.newBuilder()
                .setCommitish(ByteString.copyFromUtf8("master"))
                .setPath(ByteString.copyFromUtf8("SourceView/")));

        RpcGitObjectInfo objectInfo = response.getObjectInfo();
        assertEquals("a784797fc56057b86b0e438d1fd163fed1e48caf", objectInfo.getObjectId().toStringUtf8());
        assertEquals(1659L, objectInfo.getSize());
        assertEquals(RpcGitObjectType.OBJECT_TREE, objectInfo.getType());
    }

    @Test
    public void testGetObjectInfoWithUnicodePath() {
        RpcGetObjectInfoResponse response = getObjectInfoResponse(qaResources, RpcGetObjectInfoRequest.newBuilder()
                .setCommitish(ByteString.copyFromUtf8("master"))
                .setPath(ByteString.copyFromUtf8("国際交流基金海外運営専門員.txt")));

        RpcGitObjectInfo objectInfo = response.getObjectInfo();
        assertEquals("23a6b39161ef14b56fc0ed0c1f2af5c6dacec17a", objectInfo.getObjectId().toStringUtf8());
        assertEquals(1449149L, objectInfo.getSize());
        assertEquals(RpcGitObjectType.OBJECT_BLOB, objectInfo.getType());
    }

    private ContentServiceGrpc.ContentServiceBlockingStub stub() {
        return ContentServiceGrpc.newBlockingStub(managedChannel);
    }

    private RpcNodeType getNodeType(Repository repository, RpcGetNodeTypeRequest.Builder builder) {
        builder.setRepository("path:" + repository.getPath().toAbsolutePath());
        RpcGetNodeTypeResponse response = stub().getNodeType(builder.build());
        return response.getType();
    }

    private RpcGetObjectInfoResponse getObjectInfoResponse(Repository repository,
                                                           RpcGetObjectInfoRequest.Builder builder) {
        builder.setRepository("path:" + repository.getPath().toAbsolutePath());
        return stub().getObjectInfo(builder.build());
    }
}