package com.atlassian.stash.internal.repository;

import com.atlassian.bitbucket.AbstractServiceTest;
import com.atlassian.bitbucket.event.ApplicationEvent;
import com.atlassian.bitbucket.event.branch.BranchChangeRequestedEvent;
import com.atlassian.bitbucket.event.branch.BranchCreatedEvent;
import com.atlassian.bitbucket.event.branch.BranchCreationCanceledException;
import com.atlassian.bitbucket.event.branch.BranchCreationRequestedEvent;
import com.atlassian.bitbucket.event.repository.AbstractRepositoryRefsChangedEvent;
import com.atlassian.bitbucket.event.repository.RefChangeRequestedEvent;
import com.atlassian.bitbucket.event.repository.RepositoryDefaultBranchModifiedEvent;
import com.atlassian.bitbucket.event.tag.TagChangeRequestedEvent;
import com.atlassian.bitbucket.event.tag.TagCreatedEvent;
import com.atlassian.bitbucket.event.tag.TagCreationCanceledException;
import com.atlassian.bitbucket.event.tag.TagCreationRequestedEvent;
import com.atlassian.bitbucket.i18n.I18nService;
import com.atlassian.bitbucket.i18n.KeyedMessage;
import com.atlassian.bitbucket.i18n.SimpleI18nService;
import com.atlassian.bitbucket.project.Project;
import com.atlassian.bitbucket.repository.*;
import com.atlassian.bitbucket.scm.*;
import com.atlassian.bitbucket.scm.ref.CreateBranchCommandParameters;
import com.atlassian.bitbucket.scm.ref.CreateTagCommandParameters;
import com.atlassian.bitbucket.util.Page;
import com.atlassian.bitbucket.util.PageRequest;
import com.atlassian.event.api.EventPublisher;
import com.google.common.base.Strings;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import org.hamcrest.BaseMatcher;
import org.hamcrest.Description;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.*;
import org.mockito.runners.MockitoJUnitRunner;
import org.mockito.stubbing.Answer;

import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map;

import static com.atlassian.bitbucket.repository.StandardRefType.BRANCH;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.sameInstance;
import static org.mockito.Mockito.*;

@RunWith(MockitoJUnitRunner.class)
public class DefaultRefServiceTest extends AbstractServiceTest {

    @Mock
    private ScmCommandFactory commandFactory;
    @Mock
    private EventPublisher eventPublisher;
    @Mock
    private ScmExtendedCommandFactory extendedCommandFactory;
    @Spy
    private I18nService i18nService = new SimpleI18nService();
    @Mock
    private PageRequest pageRequest;
    @Mock
    private RefMetadataMapProvider refMetadataMapProvider;
    @Mock(answer = Answers.RETURNS_DEEP_STUBS)
    private Repository repository;
    @Mock(answer = Answers.RETURNS_DEEP_STUBS)
    private ScmService scmService;
    @InjectMocks
    private DefaultRefService service;

    public DefaultRefServiceTest() {
        super(DefaultRefService.class, RefService.class);
    }

    @Before
    public void setup() {
        when(scmService.getCommandFactory(same(repository))).thenReturn(commandFactory);
        when(scmService.getExtendedCommandFactory(repository)).thenReturn(extendedCommandFactory);

        service.setMaxBranches(25);
        service.setMaxTags(50);
    }

    @Test
    public void testGetBranches() throws Exception {
        @SuppressWarnings("unchecked")
        Page<Branch> page = mock(Page.class);
        Command<Page<Branch>> command = mockCommand(page);

        when(commandFactory.branches(isA(BranchesCommandParameters.class), same(pageRequest)))
                .thenReturn(command);
        when(pageRequest.buildRestrictedPageRequest(eq(25))).thenReturn(pageRequest);

        Page<Branch> branches = service.getBranches(new RepositoryBranchesRequest.Builder(repository)
                .filterText("startsWith")
                .order(RefOrder.MODIFICATION)
                .build(), pageRequest);
        assertSame(page, branches);
    }

    @Test
    public void testGetBranchesWithInvalidScmClientImplementation() throws Exception {
        Command<Page<Branch>> command = mockCommand(null);

        when(commandFactory.branches(isA(BranchesCommandParameters.class), same(pageRequest)))
                .thenReturn(command);
        when(pageRequest.buildRestrictedPageRequest(eq(25))).thenReturn(pageRequest);

        Page<Branch> branches = service.getBranches(new RepositoryBranchesRequest.Builder(repository)
                .filterText("startsWith")
                .order(RefOrder.MODIFICATION)
                .build(), pageRequest);
        assertNotNull(branches);
        assertTrue(branches.getIsLastPage());
        assertEquals(0, branches.getSize());
    }

    @Test
    public void testGetDefaultBranch() {
        Branch branch = mock(Branch.class);

        Command<Branch> command = mockCommand(branch);

        when(commandFactory.defaultBranch()).thenReturn(command);

        assertSame(branch, service.getDefaultBranch(repository));
    }

    @Test(expected = NoDefaultBranchException.class)
    public void testGetDefaultBranchThrowsWhenCommandReturnsNull() {
        Command<Branch> command = mockCommand(null);
        when(commandFactory.defaultBranch()).thenReturn(command);
        when(repository.getName()).thenReturn("test-repo");
        service.getDefaultBranch(repository);
    }

    @Test
    public void testGetMetadataByRefsShouldTruncateRefListIfTooLong() throws Exception {
        Ref baseRef = new SimpleRef.Builder().displayId("1").type(BRANCH).build();
        Iterable<Ref> refs = Arrays.asList(new Ref[]{
                new SimpleRef.Builder().displayId("2").type(BRANCH).build(),
                new SimpleRef.Builder().displayId("3").type(BRANCH).build(),
                new SimpleRef.Builder().displayId("4").type(BRANCH).build(),
                new SimpleRef.Builder().displayId("5").type(BRANCH).build(),
                new SimpleRef.Builder().displayId("6").type(BRANCH).build(),
                new SimpleRef.Builder().displayId("7").type(BRANCH).build(),
                new SimpleRef.Builder().displayId("8").type(BRANCH).build(),
                new SimpleRef.Builder().displayId("9").type(BRANCH).build()
        });

        RefMetadataMapProvider refMetadataMapProvider = mock(RefMetadataMapProvider.class);
        Answer<?> answer = invocation -> {
            Object[] args = invocation.getArguments();
            assertEquals(1, args.length);
            // unordered set so we can only check size
            assertEquals(5, ((RefMetadataContext) args[0]).getRefs().size());

            return ImmutableMap.<Ref, MetadataMap>builder().build();
        };
        when(refMetadataMapProvider.getMetadata(any(RefMetadataContext.class))).then(answer);
        service.setMaxRefMetadataRequestCount(5);

        RefMetadataRequest request = new RefMetadataRequest.Builder().repository(repository).baseRef(baseRef).refs(refs).build();
        service.getMetadataByRefs(request);
    }

    @Test
    public void testGetMetadataByRefsShouldExcludeMessagesIfRequested() throws Exception {
        Ref baseRef = new SimpleRef.Builder().displayId("1").type(BRANCH).build();
        Iterable<Ref> refs = Arrays.asList(new Ref[]{
                new SimpleRef.Builder().displayId("2").type(BRANCH).build()
        });


        RefMetadataRequest request = new RefMetadataRequest.Builder()
                .repository(repository)
                .baseRef(baseRef)
                .refs(refs)
                .context(ImmutableMap.of("withMessages", false))
                .build();

        service.getMetadataByRefs(request);

        ArgumentCaptor<RefMetadataContext> contextCaptor = ArgumentCaptor.forClass(RefMetadataContext.class);
        verify(refMetadataMapProvider).getMetadata(contextCaptor.capture());
        assertFalse((Boolean) contextCaptor.getValue().getContext().get("withMessages"));
    }

    @Test
    public void testGetTags() throws Exception {
        @SuppressWarnings("unchecked")
        Page<Tag> page = mock(Page.class);

        Command<Page<Tag>> command = mockCommand(page);

        when(commandFactory.tags(isA(TagsCommandParameters.class), same(pageRequest)))
                .thenReturn(command);
        when(pageRequest.buildRestrictedPageRequest(eq(50))).thenReturn(pageRequest);

        assertSame(page, service.getTags(new RepositoryTagsRequest.Builder(repository)
                .filterText("startsWith")
                .order(RefOrder.ALPHABETICAL)
                .build(), pageRequest));
    }

    @Test
    public void testGetTagsWithInvalidScmClientImplementation() throws Exception {
        Command<Page<Tag>> command = mockCommand(null);

        when(commandFactory.tags(isA(TagsCommandParameters.class), same(pageRequest)))
                .thenReturn(command);
        when(pageRequest.buildRestrictedPageRequest(eq(50))).thenReturn(pageRequest);

        Page<? extends Tag> page = service.getTags(new RepositoryTagsRequest.Builder(repository)
                .filterText("startsWith")
                .order(RefOrder.MODIFICATION)
                .build(), pageRequest);
        assertNotNull(page);
        assertTrue(page.getIsLastPage());
        assertEquals(0, page.getSize());
    }

    @Test
    public void testResolveRef() {
        Branch branch = mock(Branch.class);
        Ref ref = mock(Ref.class);

        Command<Branch> defaultBranch = mockCommand(branch);
        Command<Ref> resolveRef = mockCommand(ref);

        when(commandFactory.resolveRef(notNull(ResolveRefCommandParameters.class))).thenReturn(resolveRef);
        when(commandFactory.defaultBranch()).thenReturn(defaultBranch);

        assertSame(branch, service.resolveRef(new ResolveRefRequest.Builder(repository).build()));
        assertSame(ref, service.resolveRef(new ResolveRefRequest.Builder(repository)
                .refId("refId")
                .build()));
        assertSame(ref, service.resolveRef(new ResolveRefRequest.Builder(repository)
                .refId("tagId")
                .type(StandardRefType.TAG)
                .build()));

        ArgumentCaptor<ResolveRefCommandParameters> parametersCaptor =
                ArgumentCaptor.forClass(ResolveRefCommandParameters.class);

        verify(commandFactory).defaultBranch();
        verify(commandFactory, times(2)).resolveRef(parametersCaptor.capture());
        verifyNoMoreInteractions(commandFactory);

        List<ResolveRefCommandParameters> capturedParameters = parametersCaptor.getAllValues();
        assertEquals(2, capturedParameters.size());

        //First request should have no type
        ResolveRefCommandParameters parameters = capturedParameters.get(0);
        assertNotNull(parameters);
        assertEquals("refId", parameters.getRefId());
        assertFalse(parameters.getType().isPresent());

        //Second request should be for a tag
        parameters = capturedParameters.get(1);
        assertNotNull(parameters);
        assertEquals("tagId", parameters.getRefId());
        assertSame(StandardRefType.TAG, parameters.getType().orElse(null));
    }

    @SuppressWarnings("deprecation")
    @Test
    public void testResolveRefDeprecated() {
        Branch branch = mock(Branch.class);
        Ref ref = mock(Ref.class);

        Command<Branch> defaultBranch = mockCommand(branch);
        Command<Ref> resolveRef = mockCommand(ref);

        when(commandFactory.resolveRef(notNull(ResolveRefCommandParameters.class))).thenReturn(resolveRef);
        when(commandFactory.defaultBranch()).thenReturn(defaultBranch);

        assertSame(ref, service.resolveRef(repository, "objectId"));
        assertSame(branch, service.resolveRef(repository, null));

        ArgumentCaptor<ResolveRefCommandParameters> parametersCaptor =
                ArgumentCaptor.forClass(ResolveRefCommandParameters.class);

        verify(commandFactory).resolveRef(parametersCaptor.capture());
        verify(commandFactory).defaultBranch();
        verifyNoMoreInteractions(commandFactory);

        ResolveRefCommandParameters parameters = parametersCaptor.getValue();
        assertNotNull(parameters);
        assertEquals("objectId", parameters.getRefId());
        assertFalse(parameters.getType().isPresent());
    }

    @SuppressWarnings({"deprecation", "unchecked"})
    @Test
    public void testResolveRefWithNonexistentId() {
        when(commandFactory.resolveRef(notNull(ResolveRefCommandParameters.class))).thenReturn(mock(Command.class));

        assertNull(service.resolveRef(new ResolveRefRequest.Builder(repository)
                .refId("nonexistent")
                .build()));
        assertNull(service.resolveRef(repository, "nonexistent"));

        ArgumentCaptor<ResolveRefCommandParameters> parametersCaptor =
                ArgumentCaptor.forClass(ResolveRefCommandParameters.class);

        //Since a refId was supplied, the default branch should _never_ be resolved
        verify(commandFactory, never()).defaultBranch();
        verify(commandFactory, times(2)).resolveRef(parametersCaptor.capture());

        for (ResolveRefCommandParameters parameters : parametersCaptor.getAllValues()) {
            assertEquals("nonexistent", parameters.getRefId());
            assertFalse(parameters.getType().isPresent());
        }
    }

    @SuppressWarnings("deprecation")
    @Test
    public void testResolveRefWithoutIdResolvesDefaultBranch() {
        Branch branch = mock(Branch.class);
        Command<Branch> defaultBranch = mockCommand(branch);

        when(commandFactory.defaultBranch()).thenReturn(defaultBranch);

        assertSame(branch, service.resolveRef(new ResolveRefRequest.Builder(repository).build()));
        assertSame(branch, service.resolveRef(new ResolveRefRequest.Builder(repository)
                .type(StandardRefType.TAG) //The RefType should be ignored
                .build()));

        //Verify resolveRef is never called
        verify(commandFactory, never()).resolveRef(any(ResolveRefCommandParameters.class));
        verify(commandFactory, never()).resolveRef(anyString());
        //Verify the default branch was retrieved instead
        verify(commandFactory, times(2)).defaultBranch();
    }

    @SuppressWarnings({"deprecation", "unchecked"})
    @Test
    public void testResolveRefs() {
        Branch branch = mock(Branch.class);
        Map<String, Ref> expected = Collections.singletonMap("branchId", branch);

        Command<Map<String, Ref>> resolveRefs = mock(Command.class);
        when(resolveRefs.call()).thenReturn(null, expected);

        when(commandFactory.resolveRefs(notNull(ResolveRefsCommandParameters.class))).thenReturn(resolveRefs);

        Map<String, Ref> actual = service.resolveRefs(new ResolveRefsRequest.Builder(repository)
                .refId("unknownId")
                .build());
        assertNotNull(actual); //The command returned null, but the service shouldn't
        assertTrue(actual.isEmpty());

        actual = service.resolveRefs(new ResolveRefsRequest.Builder(repository)
                .branchId("branchId")
                .refId("refId")
                .tagId("tagId")
                .build());
        assertSame(expected, actual);

        ArgumentCaptor<ResolveRefsCommandParameters> parametersCaptor =
                ArgumentCaptor.forClass(ResolveRefsCommandParameters.class);

        verify(commandFactory, times(2)).resolveRefs(parametersCaptor.capture());
        verifyNoMoreInteractions(commandFactory);

        List<ResolveRefsCommandParameters> capturedParameters = parametersCaptor.getAllValues();

        ResolveRefsCommandParameters parameters = capturedParameters.get(0);
        assertNotNull(parameters);
        assertEquals(ImmutableSet.of("unknownId"), parameters.getRefIds());
        assertTrue(parameters.getBranchIds().isEmpty());
        assertTrue(parameters.getTagIds().isEmpty());

        parameters = capturedParameters.get(1);
        assertNotNull(parameters);
        assertEquals(ImmutableSet.of("branchId"), parameters.getBranchIds());
        assertEquals(ImmutableSet.of("refId"), parameters.getRefIds());
        assertEquals(ImmutableSet.of("tagId"), parameters.getTagIds());
    }

    @Test
    public void testSetDefaultBranch() {
        Command<Void> command = mockCommand(null);

        when(extendedCommandFactory.updateDefaultBranch(isNotNull(UpdateDefaultBranchCommandParameters.class)))
                .thenReturn(command);
        when(scmService.isSupported(same(repository), same(ScmFeature.UPDATE_DEFAULT_BRANCH))).thenReturn(true);

        service.setDefaultBranch(repository, "trunk");

        ArgumentCaptor<RepositoryDefaultBranchModifiedEvent> eventCaptor =
                ArgumentCaptor.forClass(RepositoryDefaultBranchModifiedEvent.class);
        ArgumentCaptor<UpdateDefaultBranchCommandParameters> parametersCaptor =
                ArgumentCaptor.forClass(UpdateDefaultBranchCommandParameters.class);

        verify(command).call();
        verify(eventPublisher).publish(eventCaptor.capture());
        verify(extendedCommandFactory).updateDefaultBranch(parametersCaptor.capture());
        verifyNoMoreInteractions(command, extendedCommandFactory);
        verifyZeroInteractions(commandFactory);

        RepositoryDefaultBranchModifiedEvent event = eventCaptor.getValue();
        assertEquals(repository, event.getRepository());
        assertEquals("trunk", event.getNewValue());

        UpdateDefaultBranchCommandParameters parameters = parametersCaptor.getValue();
        assertEquals("trunk", parameters.getBranchId());
    }

    @Test(expected = FeatureUnsupportedScmException.class)
    public void testSetDefaultBranchWithoutScmSupport() {
        Project project = mock(Project.class);
        when(project.getKey()).thenReturn("KEY");

        when(repository.getProject()).thenReturn(project);
        when(repository.getScmId()).thenReturn("git");
        when(repository.getSlug()).thenReturn("slug");
        when(scmService.getScmName(same(repository))).thenReturn("Git");

        try {
            service.setDefaultBranch(repository, "trunk");
        } finally {
            verify(i18nService).createKeyedMessage(eq("bitbucket.service.repository.scm.noupdatedefaultbranchfeature"),
                    eq("KEY"), eq("slug"), eq("Git"));
            verify(scmService).isSupported(same(repository), same(ScmFeature.UPDATE_DEFAULT_BRANCH));
        }
    }

    @Test
    public void testCreateBranch() {
        @SuppressWarnings("unchecked")
        Command<Branch> branchCreationCommand = mock(Command.class);
        when(scmService.getRefCommandFactory(repository).createBranch(any(CreateBranchCommandParameters.class)))
                .thenReturn(branchCreationCommand);

        Branch expectedBranch = createBranch();
        when(branchCreationCommand.call()).thenReturn(expectedBranch);

        CreateBranchRequest createBranchRequest = new CreateBranchRequest.Builder(repository, "this-is-a-branch-name", "abcdef")
                .message("This is a message")
                .build();

        assertSame(expectedBranch, service.createBranch(createBranchRequest));
        ArgumentCaptor<CreateBranchCommandParameters> parametersCaptor = ArgumentCaptor.forClass(CreateBranchCommandParameters.class);

        verify(scmService.getRefCommandFactory(repository)).createBranch(parametersCaptor.capture());

        CreateBranchCommandParameters parameters = parametersCaptor.getValue();
        assertEquals("this-is-a-branch-name", parameters.getName());
        assertEquals("abcdef", parameters.getStartPoint());
        assertEquals("This is a message", parameters.getMessage());

        assertCreateEvents(new BranchChangeRequestedEventMatcher(expectedBranch),
                new BranchCreatedEventMatcher(expectedBranch));
    }

    @Test
    public void testCreateBranchWithoutMessage() {
        @SuppressWarnings("unchecked")
        Command<Branch> branchCreationCommand = mock(Command.class);
        when(scmService.getRefCommandFactory(repository).createBranch(any(CreateBranchCommandParameters.class)))
                .thenReturn(branchCreationCommand);

        Branch expectedBranch = createBranch();
        when(branchCreationCommand.call()).thenReturn(expectedBranch);

        CreateBranchRequest createBranchRequest = new CreateBranchRequest.Builder(repository, "this-is-a-branch-name", "abcdef")
                .build();

        assertSame(expectedBranch, service.createBranch(createBranchRequest));
        ArgumentCaptor<CreateBranchCommandParameters> parametersCaptor = ArgumentCaptor.forClass(CreateBranchCommandParameters.class);

        verify(scmService.getRefCommandFactory(repository)).createBranch(parametersCaptor.capture());

        CreateBranchCommandParameters parameters = parametersCaptor.getValue();
        assertEquals("this-is-a-branch-name", parameters.getName());
        assertEquals("abcdef", parameters.getStartPoint());
        assertNull(parameters.getMessage());

        assertCreateEvents(new BranchChangeRequestedEventMatcher(expectedBranch),
                new BranchCreatedEventMatcher(expectedBranch));
    }

    @Test(expected = IllegalRepositoryStateException.class)
    public void testCreateBranchThrowsWhenRepositoryIsEmpty() {
        when(scmService.isEmpty(repository)).thenReturn(true);

        CreateBranchRequest createBranchRequest = new CreateBranchRequest.Builder(repository, "this-is-a-branch-name", "abcdef")
                .build();

        try {
            service.createBranch(createBranchRequest);
        } finally {
            verifyZeroInteractions(scmService.getRefCommandFactory(repository));
        }
    }

    @Test(expected = BranchCreationCanceledException.class)
    public void testCreateBranchThrowsWhenCanceled() {
        KeyedMessage cancelMessage = mock(KeyedMessage.class);
        doAnswer(invocation -> {
            BranchCreationRequestedEvent event = invocation.getArgumentAt(0, BranchCreationRequestedEvent.class);
            event.cancel(cancelMessage);
            return null;
        }).when(eventPublisher).publish(any(BranchCreationRequestedEvent.class));

        CreateBranchRequest createBranchRequest = new CreateBranchRequest.Builder(repository, "this-is-a-branch-name", "abcdef")
                .build();

        try {
            service.createBranch(createBranchRequest);
        } finally {
            verifyZeroInteractions(scmService.getRefCommandFactory(repository));
        }
    }

    @Test(expected = NoSuchBranchException.class)
    public void testCreateBranchThrowsWhenNoBranchIsCreated() {
        @SuppressWarnings("unchecked")
        Command<Branch> branchCreationCommand = mock(Command.class);
        when(branchCreationCommand.call()).thenReturn(null);

        when(scmService.getRefCommandFactory(repository).createBranch(any(CreateBranchCommandParameters.class)))
                .thenReturn(branchCreationCommand);

        try {
            service.createBranch(new CreateBranchRequest.Builder(repository, "this-is-a-branch-name", "abcdef")
                    .build());
        } finally {
            verify(eventPublisher).publish(any(BranchChangeRequestedEvent.class));
        }
    }

    @Test
    public void testCreateTag() {
        @SuppressWarnings("unchecked")
        Command<Tag> tagCreationCommand = mock(Command.class);
        when(scmService.getRefCommandFactory(repository).createTag(any(CreateTagCommandParameters.class)))
                .thenReturn(tagCreationCommand);

        Tag expectedTag = createTag();
        when(tagCreationCommand.call()).thenReturn(expectedTag);

        CreateTagRequest createTagRequest = new CreateTagRequest.Builder(repository, "this-is-a-tag-name", "abcdef")
                .message("This is a message")
                .build();

        assertSame(expectedTag, service.createTag(createTagRequest));
        ArgumentCaptor<CreateTagCommandParameters> parametersCaptor = ArgumentCaptor.forClass(CreateTagCommandParameters.class);

        verify(scmService.getRefCommandFactory(repository)).createTag(parametersCaptor.capture());

        CreateTagCommandParameters parameters = parametersCaptor.getValue();
        assertEquals("this-is-a-tag-name", parameters.getName());
        assertEquals("abcdef", parameters.getStartPoint());
        assertEquals("This is a message", parameters.getMessage());

        assertCreateEvents(new TagChangeRequestedEventMatcher(expectedTag),
                new TagCreatedEventMatcher(expectedTag));
    }

    @Test
    public void testCreateTagWithoutMessage() {
        @SuppressWarnings("unchecked")
        Command<Tag> tagCreationCommand = mock(Command.class);
        when(scmService.getRefCommandFactory(repository).createTag(any(CreateTagCommandParameters.class)))
                .thenReturn(tagCreationCommand);

        Tag expectedTag = createTag();
        when(tagCreationCommand.call()).thenReturn(expectedTag);

        CreateTagRequest createTagRequest = new CreateTagRequest.Builder(repository, "this-is-a-tag-name", "abcdef")
                .build();

        assertSame(expectedTag, service.createTag(createTagRequest));
        ArgumentCaptor<CreateTagCommandParameters> parametersCaptor = ArgumentCaptor.forClass(CreateTagCommandParameters.class);

        verify(scmService.getRefCommandFactory(repository)).createTag(parametersCaptor.capture());

        CreateTagCommandParameters parameters = parametersCaptor.getValue();
        assertEquals("this-is-a-tag-name", parameters.getName());
        assertEquals("abcdef", parameters.getStartPoint());
        assertNull(parameters.getMessage());

        assertCreateEvents(new TagChangeRequestedEventMatcher(expectedTag),
                new TagCreatedEventMatcher(expectedTag));
    }

    @Test(expected = IllegalRepositoryStateException.class)
    public void testCreateTagThrowsWhenRepositoryIsEmpty() {
        when(scmService.isEmpty(repository)).thenReturn(true);

        CreateTagRequest createTagRequest = new CreateTagRequest.Builder(repository, "this-is-a-branch-name", "abcdef")
                .build();

        try {
            service.createTag(createTagRequest);
        } finally {
            verifyZeroInteractions(scmService.getRefCommandFactory(repository));
        }
    }

    @Test(expected = TagCreationCanceledException.class)
    public void testCreateTagThrowsWhenCanceled() {
        KeyedMessage cancelMessage = mock(KeyedMessage.class);
        doAnswer(invocation -> {
            TagCreationRequestedEvent event = invocation.getArgumentAt(0, TagCreationRequestedEvent.class);
            event.cancel(cancelMessage);
            return null;
        }).when(eventPublisher).publish(any(BranchCreationRequestedEvent.class));

        CreateTagRequest request = new CreateTagRequest.Builder(repository, "this-is-a-tag-name", "abcdef")
                .build();

        try {
            service.createTag(request);
        } finally {
            verifyZeroInteractions(scmService.getRefCommandFactory(repository));
        }
    }

    @Test(expected = NoSuchTagException.class)
    public void testCreateTagThrowsWhenNoTagIsCreated() {
        @SuppressWarnings("unchecked")
        Command<Tag> tagCreationCommand = mock(Command.class);
        when(tagCreationCommand.call()).thenReturn(null);

        when(scmService.getRefCommandFactory(repository).createTag(any(CreateTagCommandParameters.class)))
                .thenReturn(tagCreationCommand);

        try {
            service.createTag(new CreateTagRequest.Builder(repository, "this-is-a-tag-name", "abcdef")
                    .build());
        } finally {
            verify(eventPublisher).publish(any(TagChangeRequestedEvent.class));
        }
    }

    @Test
    public void testStreamTags() throws Exception {
        // Setup
        TagCallback callback = mock(TagCallback.class);
        Command<Void> command = mockCommand(null);
        when(commandFactory.tags(isA(TagsCommandParameters.class), same(callback)))
                .thenReturn(command);

        // Execute
        RepositoryTagsRequest request = new RepositoryTagsRequest.Builder(repository)
                .build();
        service.streamTags(request, callback);

        // Verify
        verify(commandFactory).tags(isA(TagsCommandParameters.class), same(callback));
        verify(command).call();
    }

    private void assertCreateEvents(RefChangeRequestedEventMatcher requestEventMatcher,
                                    RefCreatedEventMatcher createEventMatcher) {
        ArgumentCaptor<ApplicationEvent> eventCaptor = ArgumentCaptor.forClass(ApplicationEvent.class);
        verify(eventPublisher, times(2)).publish(eventCaptor.capture());

        List<ApplicationEvent> events = eventCaptor.getAllValues();
        assertThat(events.get(0), requestEventMatcher);
        assertThat(events.get(1), createEventMatcher);
    }

    @SuppressWarnings("unchecked")
    private <T> Command<T> mockCommand(T result) {
        Command<T> command = mock(Command.class);
        when(command.call()).thenReturn(result);
        return command;
    }

    private Tag createTag() {
        return new SimpleTag.Builder()
                .id("refs/tags/this-is-a-tag-name")
                .hash("123456")
                .latestCommit("abcdef")
                .displayId("this-is-a-tag-name")
                .build();
    }

    private Branch createBranch() {
        return new SimpleBranch.Builder()
                .id("refs/heads/this-is-a-branch-name")
                .latestCommit("abcdef")
                .displayId("this-is-a-branch-name")
                .build();
    }

    private class BranchChangeRequestedEventMatcher extends RefChangeRequestedEventMatcher {

        private final Branch expected;

        private BranchChangeRequestedEventMatcher(Branch expected) {
            this.expected = expected;
        }

        @Override
        public boolean matches(Object item) {
            BranchChangeRequestedEvent requestedEvent = (BranchChangeRequestedEvent) item;
            return matches(expected, requestedEvent.getBranch(), requestedEvent);
        }
    }

    private class BranchCreatedEventMatcher extends RefCreatedEventMatcher {

        private final Branch expected;

        private BranchCreatedEventMatcher(Branch expected) {
            this.expected = expected;
        }

        @Override
        public boolean matches(Object item) {
            BranchCreatedEvent createdEvent = (BranchCreatedEvent) item;
            return matches(expected, createdEvent.getBranch(), createdEvent);
        }
    }

    private abstract class RefCreatedEventMatcher extends BaseMatcher<ApplicationEvent> {

        public boolean matches(Ref expected, Ref actual, AbstractRepositoryRefsChangedEvent createdEvent) {
            return sameInstance(repository).matches(createdEvent.getRepository())
                    && sameInstance(expected).matches(actual)
                    && sameInstance(service).matches(createdEvent.getSource())
                    && equalTo(1).matches(createdEvent.getRefChanges().size())
                    && equalTo(new SimpleRefChange.Builder()
                    .ref(new SimpleMinimalRef.Builder().displayId("abcdef").type(BRANCH).build())
                    .fromHash(Strings.repeat("0", 40))
                    .to(expected)
                    .type(RefChangeType.ADD)
                    .build()).matches(Iterables.getFirst(createdEvent.getRefChanges(), null));
        }

        @Override
        public void describeTo(Description description) {

        }
    }

    private abstract class RefChangeRequestedEventMatcher extends BaseMatcher<ApplicationEvent> {

        public boolean matches(Ref expected, Ref actual, RefChangeRequestedEvent requestedEvent) {
            return equalTo(expected.getId()).matches(actual.getId())
                    && equalTo(expected.getDisplayId()).matches(actual.getDisplayId())
                    && equalTo(expected.getLatestCommit()).matches(actual.getLatestCommit())
                    && sameInstance(repository).matches(requestedEvent.getRepository())
                    && sameInstance(service).matches(requestedEvent.getSource())
                    && equalTo(false).matches(requestedEvent.isCanceled());
        }

        @Override
        public void describeTo(Description description) {

        }
    }

    private class TagChangeRequestedEventMatcher extends RefChangeRequestedEventMatcher {

        private final Tag expected;

        private TagChangeRequestedEventMatcher(Tag expected) {
            this.expected = expected;
        }

        @Override
        public boolean matches(Object item) {
            TagChangeRequestedEvent requestedEvent = (TagChangeRequestedEvent) item;

            return matches(expected, requestedEvent.getTag(), requestedEvent);
        }
    }

    private class TagCreatedEventMatcher extends RefCreatedEventMatcher {

        private final Tag expected;

        private TagCreatedEventMatcher(Tag expected) {
            this.expected = expected;
        }

        @Override
        public boolean matches(Object item) {
            TagCreatedEvent createdEvent = (TagCreatedEvent) item;
            return matches(expected, createdEvent.getTag(), createdEvent);
        }
    }
}
