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.ref.*;
import com.atlassian.bitbucket.mesh.repository.RefsUpdatedEvent;
import com.atlassian.bitbucket.mesh.repository.Repository;
import com.atlassian.bitbucket.mesh.rpc.v1.*;
import com.atlassian.bitbucket.mesh.test.ExpectedEvents;
import com.atlassian.bitbucket.mesh.test.HasGitError;
import com.atlassian.bitbucket.mesh.test.annotation.DirtiesRepo;
import com.atlassian.bitbucket.mesh.test.annotation.Repo;
import com.google.protobuf.ByteString;
import io.grpc.Status;
import io.grpc.StatusRuntimeException;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;

import javax.annotation.Resource;

import static com.atlassian.bitbucket.mesh.git.GitRefPattern.TAGS;
import static org.apache.commons.lang3.StringUtils.repeat;

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

    //Attempts to exploit the command builder to use the -F command line option to read in an arbitrary file
    //from the filesystem and use it as the tag's message, effectively "stealing" its contents
    private static final String INVALID_NAME = "-F/etc/passwd";

    private static final RpcPerson.Builder AUTHOR = RpcPerson.newBuilder()
            .setName(ByteString.copyFromUtf8("Flying Circus"))
            .setEmail("biggus@example.com");

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

    @Resource
    @Rule
    public ExpectedEvents publishedEvents;

    @Resource
    private GitAgent agent;
    @Resource
    private GitProcessBuilderFactory builderFactory;
    @Resource
    private ApplicationConfig config;
    @Resource
    private RefManager refManager;
    @Repo
    private Repository repository;
    private RefServiceGrpc.RefServiceBlockingStub stub;

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

    @Test
    public void testCheckFormat() {
        stub.checkFormat(RpcCheckFormatRequest.newBuilder()
                .setRefId(ByteString.copyFromUtf8("branch-mc-branchy-face"))
                .build());
    }

    @Test
    public void testCheckFormatWithBlankRefId() {
        StatusRuntimeException thrown = assertThrows(StatusRuntimeException.class,
                () -> stub.checkFormat(RpcCheckFormatRequest.newBuilder()
                        .setRefId(ByteString.EMPTY)
                        .build()));

        assertSame(Status.Code.INVALID_ARGUMENT, thrown.getStatus().getCode());
        assertThat(thrown, HasGitError.invalidRefName(RpcInvalidRefName.Reason.BLANK, ""));
    }

    @Test
    public void testCheckFormatWithInvalidRefId() {
        StatusRuntimeException thrown = assertThrows(StatusRuntimeException.class,
                () -> stub.checkFormat(RpcCheckFormatRequest.newBuilder()
                        .setRefId(ByteString.copyFromUtf8("-this-is-invalid"))
                        .build()));

        assertSame(Status.Code.INVALID_ARGUMENT, thrown.getStatus().getCode());
        assertThat(thrown, HasGitError.invalidRefName(RpcInvalidRefName.Reason.INVALID, "-this-is-invalid"));
    }

    @Test
    public void testCheckFormatWithLongRefId() {
        String refId = repeat("a", 112);

        StatusRuntimeException thrown = assertThrows(StatusRuntimeException.class,
                () -> stub.checkFormat(RpcCheckFormatRequest.newBuilder()
                        .setRefId(ByteString.copyFromUtf8(refId))
                        .build()));

        assertSame(Status.Code.INVALID_ARGUMENT, thrown.getStatus().getCode());
        assertThat(thrown, HasGitError.invalidRefName(111, RpcInvalidRefName.Reason.TOO_LONG, refId));
    }

    @Test
    public void testCheckFormatWithWhitespaceRefId() {
        StatusRuntimeException thrown = assertThrows(StatusRuntimeException.class,
                () -> stub.checkFormat(RpcCheckFormatRequest.newBuilder()
                        .setRefId(ByteString.copyFromUtf8("there is some whitespace here"))
                        .build()));

        assertSame(Status.Code.INVALID_ARGUMENT, thrown.getStatus().getCode());
        assertThat(thrown,
                HasGitError.invalidRefName(RpcInvalidRefName.Reason.WHITESPACE, "there is some whitespace here"));
    }

    @DirtiesRepo
    @Test
    public void testCreateBranch() {
        publishedEvents.expect(RefsUpdatedEvent.class);

        RpcCreateBranchResponse response = stub.createBranch(RpcCreateBranchRequest.newBuilder()
                .setAuthor(AUTHOR)
                .setName(ByteString.copyFromUtf8("branch-mc-branchy-face"))
                .setRepository(repository.getId())
                .setStartPoint(ByteString.copyFromUtf8("master"))
                .build());

        assertEquals("refs/heads/branch-mc-branchy-face", response.getBranch().getId().toStringUtf8());
    }

    @Test
    public void testCreateBranchFailsFailsInReadOnlyMode() {
        repository.setReadOnly(true);
        try {
            publishedEvents.expectNone(RefsUpdatedEvent.class);

            StatusRuntimeException thrown = assertThrows(StatusRuntimeException.class,
                    () -> stub.createBranch(RpcCreateBranchRequest.newBuilder()
                            .setAuthor(AUTHOR)
                            .setName(ByteString.copyFromUtf8("created-in-read-only-mode"))
                            .setRepository(repository.getId())
                            .setStartPoint(ByteString.copyFromUtf8("master"))
                            .build()));

            RpcError error = RpcErrorUtils.fromThrowable(thrown);
            assertNotNull(error);
            assertTrue(error.hasRepositoryReadOnly());
        } finally {
            repository.setReadOnly(false);
        }
    }

    @DirtiesRepo
    @Test
    public void testCreateBranchUnicode() {
        publishedEvents.expect(RefsUpdatedEvent.class);

        RpcCreateBranchResponse response = stub.createBranch(RpcCreateBranchRequest.newBuilder()
                .setAuthor(AUTHOR)
                .setName(ByteString.copyFromUtf8("道可道，非常道。名可名，非常名。"))
                .setRepository(repository.getId())
                .setStartPoint(ByteString.copyFromUtf8("master"))
                .build());

        assertEquals("refs/heads/道可道，非常道。名可名，非常名。", response.getBranch().getId().toStringUtf8());
    }

    @Test
    public void testInvalidNameBlank() {
        publishedEvents.expectNone(RefsUpdatedEvent.class);

        StatusRuntimeException thrown = assertThrows(StatusRuntimeException.class,
                () -> stub.createBranch(RpcCreateBranchRequest.newBuilder()
                        .setAuthor(AUTHOR)
                        .setName(ByteString.copyFromUtf8("              "))
                        .setRepository(repository.getId())
                        .setStartPoint(ByteString.copyFromUtf8("master"))
                        .build()));

        assertSame(Status.Code.INVALID_ARGUMENT, thrown.getStatus().getCode());
        assertThat(thrown,
                HasGitError.invalidRefName(RpcInvalidRefName.Reason.BLANK, "              "));
    }

    @Test
    public void testInvalidNameConflictDirFileLooseRef() {
        publishedEvents.expect(RefsUpdatedEvent.class);

        stub.createBranch(RpcCreateBranchRequest.newBuilder()
                .setAuthor(AUTHOR)
                .setName(ByteString.copyFromUtf8("hello"))
                .setRepository(repository.getId())
                .setStartPoint(ByteString.copyFromUtf8("master"))
                .build());

        StatusRuntimeException thrown = assertThrows(StatusRuntimeException.class,
                () -> stub.createBranch(RpcCreateBranchRequest.newBuilder()
                        .setAuthor(AUTHOR)
                        .setName(ByteString.copyFromUtf8("hello/world"))
                        .setRepository(repository.getId())
                        .setStartPoint(ByteString.copyFromUtf8("master"))
                        .build()));

        assertSame(Status.Code.INVALID_ARGUMENT, thrown.getStatus().getCode());
        assertThat(thrown, HasGitError.refNameConflict("refs/heads/hello", "refs/heads/hello/world"));
    }

    @Test
    public void testInvalidNameConflictFileDirLooseRef() {
        publishedEvents.expect(RefsUpdatedEvent.class);

        stub.createBranch(RpcCreateBranchRequest.newBuilder()
                .setAuthor(AUTHOR)
                .setName(ByteString.copyFromUtf8("hello/world"))
                .setRepository(repository.getId())
                .setStartPoint(ByteString.copyFromUtf8("master"))
                .build());

        StatusRuntimeException thrown = assertThrows(StatusRuntimeException.class,
                () -> stub.createBranch(RpcCreateBranchRequest.newBuilder()
                        .setAuthor(AUTHOR)
                        .setName(ByteString.copyFromUtf8("hello"))
                        .setRepository(repository.getId())
                        .setStartPoint(ByteString.copyFromUtf8("master"))
                        .build()));

        assertSame(Status.Code.INVALID_ARGUMENT, thrown.getStatus().getCode());
        assertThat(thrown, HasGitError.refNameConflict("refs/heads/hello/world", "refs/heads/hello"));
    }

    @Test
    public void testInvalidNameConflictPackedRef() {
        publishedEvents.expect(RefsUpdatedEvent.class);

        stub.createBranch(RpcCreateBranchRequest.newBuilder()
                .setAuthor(AUTHOR)
                .setName(ByteString.copyFromUtf8("hello/world"))
                .setRepository(repository.getId())
                .setStartPoint(ByteString.copyFromUtf8("master"))
                .build());

        runGarbageCollection();

        StatusRuntimeException thrown = assertThrows(StatusRuntimeException.class,
                () -> stub.createBranch(RpcCreateBranchRequest.newBuilder()
                        .setAuthor(AUTHOR)
                        .setName(ByteString.copyFromUtf8("hello"))
                        .setRepository(repository.getId())
                        .setStartPoint(ByteString.copyFromUtf8("master"))
                        .build()));

        assertSame(Status.Code.INVALID_ARGUMENT, thrown.getStatus().getCode());
        assertThat(thrown, HasGitError.refNameConflict("refs/heads/hello/world", "refs/heads/hello"));
    }

    @DirtiesRepo
    @Test
    public void testInvalidNameDuplicate() {
        publishedEvents.expect(RefsUpdatedEvent.class);

        RpcCreateBranchRequest request = RpcCreateBranchRequest.newBuilder()
                .setAuthor(AUTHOR)
                .setName(ByteString.copyFromUtf8("double-double-toil-and-trouble"))
                .setRepository(repository.getId())
                .setStartPoint(ByteString.copyFromUtf8("master"))
                .build();

        stub.createBranch(request);

        StatusRuntimeException thrown = assertThrows(StatusRuntimeException.class, () -> stub.createBranch(request));

        assertSame(Status.Code.INVALID_ARGUMENT, thrown.getStatus().getCode());
        assertThat(thrown,
                HasGitError.invalidBranchName("double-double-toil-and-trouble", RpcInvalidBranchName.Reason.DUPLICATE));
    }

    @Test
    public void testInvalidNameInvalid() {
        publishedEvents.expectNone(RefsUpdatedEvent.class);

        StatusRuntimeException thrown = assertThrows(StatusRuntimeException.class,
                () -> stub.createBranch(RpcCreateBranchRequest.newBuilder()
                        .setAuthor(AUTHOR)
                        .setName(ByteString.copyFromUtf8("!!!!--[3104558]--!!!!"))
                        .setRepository(repository.getId())
                        .setStartPoint(ByteString.copyFromUtf8("master"))
                        .build()));

        assertSame(Status.Code.INVALID_ARGUMENT, thrown.getStatus().getCode());
        assertThat(thrown,
                HasGitError.invalidBranchName("!!!!--[3104558]--!!!!", RpcInvalidBranchName.Reason.NAME_INVALID));
    }

    @Test
    public void testInvalidNameTooLong() {
        publishedEvents.expectNone(RefsUpdatedEvent.class);

        String name = repeat("Nananananananananananananananana", 5) + "BATMAN!";

        StatusRuntimeException thrown = assertThrows(StatusRuntimeException.class,
                () -> stub.createBranch(RpcCreateBranchRequest.newBuilder()
                        .setAuthor(AUTHOR)
                        .setName(ByteString.copyFromUtf8(name))
                        .setRepository(repository.getId())
                        .setStartPoint(ByteString.copyFromUtf8("master"))
                        .build()));

        assertSame(Status.Code.INVALID_ARGUMENT, thrown.getStatus().getCode());
        assertThat(thrown,
                HasGitError.invalidRefName(111, RpcInvalidRefName.Reason.TOO_LONG, name));
    }

    @Test
    public void testInvalidNameWhitespaces() {
        publishedEvents.expectNone(RefsUpdatedEvent.class);

        String name = "“Bit of a hermit.” “A hermit? With friends” “Hermits United. We meet up every " +
                "10 years, swap stories about caves. It’s good fun… for a hermit.”";

        StatusRuntimeException thrown = assertThrows(StatusRuntimeException.class,
                () -> stub.createBranch(RpcCreateBranchRequest.newBuilder()
                        .setAuthor(AUTHOR)
                        .setName(ByteString.copyFromUtf8(name))
                        .setRepository(repository.getId())
                        .setStartPoint(ByteString.copyFromUtf8("master"))
                        .build()));

        assertSame(Status.Code.INVALID_ARGUMENT, thrown.getStatus().getCode());
        assertThat(thrown,
                HasGitError.invalidRefName(RpcInvalidRefName.Reason.WHITESPACE, name));
    }

    @Test
    public void testStartPointNotExists() {
        publishedEvents.expectNone(RefsUpdatedEvent.class);

        StatusRuntimeException thrown = assertThrows(StatusRuntimeException.class,
                () -> stub.createBranch(RpcCreateBranchRequest.newBuilder()
                        .setAuthor(AUTHOR)
                        .setName(ByteString.copyFromUtf8("branch-mc-branchy-face"))
                        .setRepository(repository.getId())
                        .setStartPoint(ByteString.copyFromUtf8("cant-touch-this"))
                        .build()));

        assertSame(Status.Code.INVALID_ARGUMENT, thrown.getStatus().getCode());
        assertThat(thrown,
                HasGitError.invalidBranchName("cant-touch-this", RpcInvalidBranchName.Reason.START_POINT_NOT_FOUND));
    }

    @DirtiesRepo
    @Test
    public void testCreateTag() {
        publishedEvents.expect(RefsUpdatedEvent.class);

        Branch originalHead = agent.resolveHead(repository);
        String tagName = "new-tag" + System.currentTimeMillis();

        stub.createTag(RpcCreateTagRequest.newBuilder()
                .setAuthor(AUTHOR)
                .setMessage(ByteString.copyFromUtf8("A new ANNOTATED tag"))
                .setName(ByteString.copyFromUtf8(tagName))
                .setRepository(repository.getId())
                .build());

        ResolveRefsParameters parameters = new ResolveRefsParameters.Builder(repository)
                .tagId(tagName)
                .build();
        Ref ref = refManager.resolveRefs(parameters).get(tagName);

        assertTrue(ref instanceof Tag);
        assertEquals(tagName, TAGS.unqualify(ref.getId()));
        assertEquals(TAGS.qualify(tagName), ref.getId());
        assertEquals(originalHead.getLatestCommit(), ref.getLatestCommit());
        assertNotNull(((Tag) ref).getHash());
    }

    @DirtiesRepo
    @Test
    public void testCreateLightweightTag() {
        publishedEvents.expect(RefsUpdatedEvent.class);

        Branch originalHead = agent.resolveHead(repository);
        String tagName = "new-tag" + System.currentTimeMillis();

        stub.createTag(RpcCreateTagRequest.newBuilder()
                .setAuthor(AUTHOR)
                .setMessage(ByteString.copyFromUtf8("A new ANNOTATED tag"))
                .setName(ByteString.copyFromUtf8(tagName))
                .setRepository(repository.getId())
                .setType(RpcTagType.TAG_LIGHTWEIGHT)
                .build());

        ResolveRefsParameters parameters = new ResolveRefsParameters.Builder(repository)
                .tagId(tagName)
                .build();
        Ref ref = refManager.resolveRefs(parameters).get(tagName);

        assertTrue(ref instanceof Tag);
        assertEquals(tagName, TAGS.unqualify(ref.getId()));
        assertEquals(TAGS.qualify(tagName), ref.getId());
        assertEquals(originalHead.getLatestCommit(), ref.getLatestCommit());
        assertNull(((Tag) ref).getHash());
    }

    @Test
    public void testCreateTagWithInvalidName() {
        publishedEvents.expectNone(RefsUpdatedEvent.class);

        StatusRuntimeException thrown = assertThrows(StatusRuntimeException.class,
                () -> stub.createTag(RpcCreateTagRequest.newBuilder()
                        .setAuthor(AUTHOR)
                        .setName(ByteString.copyFromUtf8(INVALID_NAME))
                        .setRepository(repository.getId())
                        .setType(RpcTagType.TAG_LIGHTWEIGHT)
                        .build()));

        assertSame(Status.Code.INVALID_ARGUMENT, thrown.getStatus().getCode());
        assertThat(thrown,
                HasGitError.invalidRefName(RpcInvalidRefName.Reason.INVALID, INVALID_NAME));
    }

    @Test
    public void testCreateTagWithInvalidStartPoint() {
        publishedEvents.expectNone(RefsUpdatedEvent.class);

        StatusRuntimeException thrown = assertThrows(StatusRuntimeException.class,
                () -> stub.createTag(RpcCreateTagRequest.newBuilder()
                        .setAuthor(AUTHOR)
                        .setName(ByteString.copyFromUtf8("stolen-tag"))
                        .setRepository(repository.getId())
                        .setStartPoint(ByteString.copyFromUtf8(INVALID_NAME))
                        .setType(RpcTagType.TAG_LIGHTWEIGHT)
                        .build()));

        assertSame(Status.Code.INVALID_ARGUMENT, thrown.getStatus().getCode());
        assertThat(thrown, HasGitError.invalidTagName(RpcInvalidTagName.Reason.START_POINT_NOT_FOUND, "stolen-tag"));
    }

    @DirtiesRepo
    @Test
    public void testCreateTagWithStartPoint() {
        publishedEvents.expect(RefsUpdatedEvent.class);

        Branch branch = agent.resolveBranch(repository, "basic_branching");
        assertNotNull(branch);

        String tagName = "new-tag" + System.currentTimeMillis();

        stub.createTag(RpcCreateTagRequest.newBuilder()
                .setAuthor(AUTHOR)
                .setMessage(ByteString.copyFromUtf8("A new ANNOTATED tag"))
                .setName(ByteString.copyFromUtf8(tagName))
                .setRepository(repository.getId())
                .setStartPoint(ByteString.copyFromUtf8("basic_branching"))
                .build());

        ResolveRefsParameters parameters = new ResolveRefsParameters.Builder(repository)
                .tagId(tagName)
                .build();
        Ref ref = refManager.resolveRefs(parameters).get(tagName);

        assertTrue(ref instanceof Tag);
        assertEquals(tagName, TAGS.unqualify(ref.getId()));
        assertEquals(TAGS.qualify(tagName), ref.getId());
        assertEquals(branch.getLatestCommit(), ref.getLatestCommit());
        assertNotNull(((Tag) ref).getHash());
    }

    @DirtiesRepo
    @Test
    public void testCreateTagWithForceShouldUpdateExistingTag() {
        publishedEvents.expect(RefsUpdatedEvent.class, 2);

        Branch branch = agent.resolveBranch(repository, "basic_branching");
        assertNotNull(branch);

        String tagName = "new-tag" + System.currentTimeMillis();

        stub.createTag(RpcCreateTagRequest.newBuilder()
                .setAuthor(AUTHOR)
                .setName(ByteString.copyFromUtf8(tagName))
                .setRepository(repository.getId())
                .build());

        stub.createTag(RpcCreateTagRequest.newBuilder()
                .setAuthor(AUTHOR)
                .setForce(true)
                .setMessage(ByteString.copyFromUtf8("A tag pointing to basic_branching"))
                .setName(ByteString.copyFromUtf8(tagName))
                .setRepository(repository.getId())
                .setStartPoint(ByteString.copyFromUtf8("basic_branching"))
                .build());

        ResolveRefsParameters parameters = new ResolveRefsParameters.Builder(repository)
                .tagId(tagName)
                .build();
        Ref ref = refManager.resolveRefs(parameters).get(tagName);

        assertTrue(ref instanceof Tag);
        assertEquals(tagName, TAGS.unqualify(ref.getId()));
        assertEquals(TAGS.qualify(tagName), ref.getId());
        assertEquals(branch.getLatestCommit(), ref.getLatestCommit());
        assertNotNull(((Tag) ref).getHash());
    }

    @DirtiesRepo
    @Test
    public void testCreateTagWithExistingTagNameShouldFail() {
        publishedEvents.expect(RefsUpdatedEvent.class);

        String tagName = "new-tag" + System.currentTimeMillis();

        stub.createTag(RpcCreateTagRequest.newBuilder()
                .setAuthor(AUTHOR)
                .setName(ByteString.copyFromUtf8(tagName))
                .setRepository(repository.getId())
                .build());

        StatusRuntimeException thrown = assertThrows(StatusRuntimeException.class,
                () -> stub.createTag(RpcCreateTagRequest.newBuilder()
                        .setAuthor(AUTHOR)
                        .setName(ByteString.copyFromUtf8(tagName))
                        .setMessage(ByteString.copyFromUtf8("Second try with the same tag name"))
                        .setRepository(repository.getId())
                        .build()));

        assertSame(Status.Code.INVALID_ARGUMENT, thrown.getStatus().getCode());
        assertThat(thrown, HasGitError.invalidTagName(RpcInvalidTagName.Reason.DUPLICATE, tagName));
    }

    @DirtiesRepo
    @Test
    public void testCreateConflictingTagFileDirLoose() {
        publishedEvents.expect(RefsUpdatedEvent.class);

        stub.createTag(RpcCreateTagRequest.newBuilder()
                .setAuthor(AUTHOR)
                .setName(ByteString.copyFromUtf8("release/v1.0"))
                .setRepository(repository.getId())
                .build());

        StatusRuntimeException thrown = assertThrows(StatusRuntimeException.class,
                () -> stub.createTag(RpcCreateTagRequest.newBuilder()
                        .setAuthor(AUTHOR)
                        .setName(ByteString.copyFromUtf8("release"))
                        .setRepository(repository.getId())
                        .build()));

        assertSame(Status.Code.INVALID_ARGUMENT, thrown.getStatus().getCode());
        assertThat(thrown, HasGitError.refNameConflict("refs/tags/release/v1.0", "refs/tags/release"));
    }

    @DirtiesRepo
    @Test
    public void testCreateConflictingTagDirFileLoose() {
        publishedEvents.expect(RefsUpdatedEvent.class);

        stub.createTag(RpcCreateTagRequest.newBuilder()
                .setAuthor(AUTHOR)
                .setName(ByteString.copyFromUtf8("release"))
                .setRepository(repository.getId())
                .build());

        StatusRuntimeException thrown = assertThrows(StatusRuntimeException.class,
                () -> stub.createTag(RpcCreateTagRequest.newBuilder()
                        .setAuthor(AUTHOR)
                        .setName(ByteString.copyFromUtf8("release/v1.0"))
                        .setRepository(repository.getId())
                        .build()));

        assertSame(Status.Code.INVALID_ARGUMENT, thrown.getStatus().getCode());
        assertThat(thrown, HasGitError.refNameConflict("refs/tags/release", "refs/tags/release/v1.0"));
    }

    @DirtiesRepo
    @Test
    public void testCreateConflictingTagFileDirPacked() {
        publishedEvents.expect(RefsUpdatedEvent.class);

        stub.createTag(RpcCreateTagRequest.newBuilder()
                .setAuthor(AUTHOR)
                .setName(ByteString.copyFromUtf8("release/v1.0"))
                .setRepository(repository.getId())
                .build());

        runGarbageCollection();

        StatusRuntimeException thrown = assertThrows(StatusRuntimeException.class,
                () -> stub.createTag(RpcCreateTagRequest.newBuilder()
                        .setAuthor(AUTHOR)
                        .setName(ByteString.copyFromUtf8("release"))
                        .setRepository(repository.getId())
                        .build()));

        assertSame(Status.Code.INVALID_ARGUMENT, thrown.getStatus().getCode());
        assertThat(thrown, HasGitError.refNameConflict("refs/tags/release/v1.0", "refs/tags/release"));
    }

    @DirtiesRepo
    @Test
    public void testCreateConflictingTagDirFilePacked() {
        publishedEvents.expect(RefsUpdatedEvent.class);

        stub.createTag(RpcCreateTagRequest.newBuilder()
                .setAuthor(AUTHOR)
                .setName(ByteString.copyFromUtf8("release"))
                .setRepository(repository.getId())
                .build());

        runGarbageCollection();

        StatusRuntimeException thrown = assertThrows(StatusRuntimeException.class,
                () -> stub.createTag(RpcCreateTagRequest.newBuilder()
                        .setAuthor(AUTHOR)
                        .setName(ByteString.copyFromUtf8("release/v1.0"))
                        .setRepository(repository.getId())
                        .build()));

        assertSame(Status.Code.INVALID_ARGUMENT, thrown.getStatus().getCode());
        assertThat(thrown, HasGitError.refNameConflict("refs/tags/release", "refs/tags/release/v1.0"));
    }

    @Test
    public void testCreateTagWithInvalidTagNameShouldFail() {
        publishedEvents.expectNone(RefsUpdatedEvent.class);

        StatusRuntimeException thrown = assertThrows(StatusRuntimeException.class,
                () -> stub.createTag(RpcCreateTagRequest.newBuilder()
                        .setAuthor(AUTHOR)
                        .setName(ByteString.copyFromUtf8("&^%$&*^invalid_tag_name"))
                        .setRepository(repository.getId())
                        .build()));

        assertSame(Status.Code.INVALID_ARGUMENT, thrown.getStatus().getCode());
        assertThat(thrown, HasGitError.invalidTagName(RpcInvalidTagName.Reason.NAME_INVALID, "&^%$&*^invalid_tag_name"));
    }

    @Test
    public void testCreateTagWithInvalidStartPointShouldFail() {
        publishedEvents.expectNone(RefsUpdatedEvent.class);

        StatusRuntimeException thrown = assertThrows(StatusRuntimeException.class,
                () -> stub.createTag(RpcCreateTagRequest.newBuilder()
                        .setAuthor(AUTHOR)
                        .setName(ByteString.copyFromUtf8("this_tag_should_not_be_created"))
                        .setRepository(repository.getId())
                        .setStartPoint(ByteString.copyFromUtf8("*&%^invalid-start-point^%"))
                        .build()));

        assertSame(Status.Code.INVALID_ARGUMENT, thrown.getStatus().getCode());
        assertThat(thrown,
                HasGitError.invalidTagName(RpcInvalidTagName.Reason.START_POINT_NOT_FOUND, "*&%^invalid-start-point^%"));
    }

    @DirtiesRepo
    @Test
    public void testDeleteTag() {
        publishedEvents.expect(RefsUpdatedEvent.class);

        String tagName = "backdated_annotated_tag";

        stub.deleteTag(RpcDeleteTagRequest.newBuilder()
                .setAuthor(AUTHOR)
                .setName(ByteString.copyFromUtf8(tagName))
                .setRepository(repository.getId())
                .build());

        ResolveRefsParameters parameters = new ResolveRefsParameters.Builder(repository)
                .tagId(tagName)
                .build();

        assertNull(refManager.resolveRefs(parameters).get(tagName));
    }

    @Test
    public void testDeleteTagShouldFailIfTagDoesNotExist() {
        publishedEvents.expectNone(RefsUpdatedEvent.class);

        StatusRuntimeException thrown = assertThrows(StatusRuntimeException.class,
                () -> stub.deleteTag(RpcDeleteTagRequest.newBuilder()
                        .setAuthor(AUTHOR)
                        .setName(ByteString.copyFromUtf8("invalid_tag_name"))
                        .setRepository(repository.getId())
                        .build()));

        assertSame(Status.Code.NOT_FOUND, thrown.getStatus().getCode());
        assertThat(thrown, HasGitError.noSuchObject("invalid_tag_name"));
    }

    private void runGarbageCollection() {
        builderFactory.builder(repository).gc().aggressive(true).build().run();
    }
}
