package com.atlassian.stash.internal.pull;

import com.atlassian.bitbucket.AuthorisationException;
import com.atlassian.bitbucket.auth.AuthenticationContext;
import com.atlassian.bitbucket.event.pull.*;
import com.atlassian.bitbucket.hamcrest.PageRequestMatcher;
import com.atlassian.bitbucket.hamcrest.SimpleTypeSafeMatcher;
import com.atlassian.bitbucket.i18n.I18nService;
import com.atlassian.bitbucket.i18n.SimpleI18nService;
import com.atlassian.bitbucket.permission.Permission;
import com.atlassian.bitbucket.permission.PermissionService;
import com.atlassian.bitbucket.permission.PermissionValidationService;
import com.atlassian.bitbucket.pull.*;
import com.atlassian.bitbucket.user.ApplicationUser;
import com.atlassian.bitbucket.user.UserService;
import com.atlassian.bitbucket.util.Page;
import com.atlassian.bitbucket.util.PageRequest;
import com.atlassian.bitbucket.util.PageUtils;
import com.atlassian.event.api.EventPublisher;
import com.atlassian.stash.internal.repository.InternalRepository;
import com.atlassian.stash.internal.user.InternalApplicationUser;
import com.atlassian.stash.internal.watcher.DefaultWatcherService;
import com.atlassian.stash.internal.watcher.InternalWatcher;
import com.atlassian.stash.internal.watcher.InternalWatcherService;
import com.atlassian.stash.internal.watcher.WatcherDao;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Answers;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.Spy;
import org.mockito.runners.MockitoJUnitRunner;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.util.ReflectionUtils;

import java.lang.reflect.Field;
import java.util.List;
import java.util.Set;

import static java.util.Collections.emptySet;
import static java.util.Collections.singleton;
import static java.util.Comparator.comparing;
import static java.util.stream.Collectors.toList;
import static java.util.stream.Collectors.toSet;
import static org.junit.Assert.*;
import static org.mockito.Matchers.any;
import static org.mockito.Matchers.argThat;
import static org.mockito.Mockito.*;

@SuppressWarnings("unchecked")
@RunWith(MockitoJUnitRunner.class)
public class InternalPullRequestParticipantHelperTest {

    private static final int CURRENT_USER_ID = 98;
    private static final long PULL_REQUEST_GLOBAL_ID = 99;
    private static final String PULL_REQUEST_LATEST_COMMIT = "57150c54c38d6570b2fd5e6d6cfc19476de44e84";
    private static final int TARGET_REPOSITORY_ID = 100;
    private static final int USER_ID = 101;

    @Mock
    private PullRequestActivityDao activityDao;
    @Mock
    private AuthenticationContext authenticationContext;
    @Mock
    private InternalApplicationUser currentUser;
    @Mock
    private EventPublisher eventPublisher;
    @Spy
    private I18nService i18nService = new SimpleI18nService();
    @Mock
    private PullRequestParticipantDao participantDao;
    @Mock
    private PermissionService permissionService;
    @Mock
    private PermissionValidationService permissionValidationService;
    @Mock
    private InternalPullRequest pullRequest;
    @Mock
    private InternalRepository sourceRepository;
    @Mock
    private InternalRepository targetRepository;
    @Mock
    private PlatformTransactionManager transactionManager;
    @Mock
    private InternalApplicationUser user;
    @Mock
    private UserService userService;
    @Mock
    private InternalWatcherService watcherService;

    @InjectMocks
    private InternalPullRequestParticipantHelper participantHelper;

    @Before
    public void setup() throws Exception {
        when(authenticationContext.getCurrentUser()).thenReturn(currentUser);

        when(currentUser.getId()).thenReturn(CURRENT_USER_ID);

        when(participantDao.create(any(InternalPullRequestParticipant.class)))
                .thenAnswer(invocation -> invocation.getArgumentAt(0, InternalPullRequestParticipant.class));
        when(participantDao.update(any(InternalPullRequestParticipant.class)))
                .thenAnswer(invocation -> invocation.getArgumentAt(0, InternalPullRequestParticipant.class));

        when(permissionService.hasGlobalPermission(user, Permission.LICENSED_USER)).thenReturn(true);
        when(permissionService.hasRepositoryPermission(user, targetRepository, Permission.REPO_READ)).thenReturn(true);

        InternalPullRequestRef fromRef = mock(InternalPullRequestRef.class);
        when(fromRef.getLatestCommit()).thenReturn(PULL_REQUEST_LATEST_COMMIT);

        when(pullRequest.getFromRef()).thenReturn(fromRef);
        when(pullRequest.getGlobalId()).thenReturn(PULL_REQUEST_GLOBAL_ID);
        when(pullRequest.getScopeRepository()).thenReturn(targetRepository);
        when(pullRequest.getState()).thenReturn(PullRequestState.OPEN);

        when(targetRepository.getId()).thenReturn(TARGET_REPOSITORY_ID);

        when(user.getId()).thenReturn(USER_ID);

        Field field = ReflectionUtils.findField(InternalPullRequestParticipantHelper.class, "maxUserPageSize");
        ReflectionUtils.makeAccessible(field);
        field.set(participantHelper, 1000);
    }

    @Test(expected = InvalidPullRequestParticipantException.class)
    public void testAddReviewerNotLicensed() {
        when(permissionService.hasGlobalPermission(user, Permission.LICENSED_USER)).thenReturn(false);

        try {
            participantHelper.addReviewer(pullRequest, user);
        } finally {
            verify(permissionService).hasGlobalPermission(user, Permission.LICENSED_USER);
            verifyNoMoreInteractions(permissionService);
            verifyZeroInteractions(activityDao, eventPublisher, participantDao);
        }
    }

    @Test(expected = InvalidPullRequestParticipantException.class)
    public void testAddReviewerCantReadRepository() {
        when(permissionService.hasGlobalPermission(user, Permission.LICENSED_USER)).thenReturn(true);
        when(permissionService.hasRepositoryPermission(user, targetRepository, Permission.REPO_READ)).thenReturn(false);

        try {
            participantHelper.addReviewer(pullRequest, user);
        } finally {
            verifyZeroInteractions(activityDao, eventPublisher, participantDao);
        }
    }

    @Test(expected = IllegalPullRequestStateException.class)
    public void testAddReviewerToDeclinedPullRequest() {
        when(participantDao.findByPullRequestAndUser(pullRequest.getGlobalId(), user.getId())).thenReturn(null);
        when(pullRequest.getState()).thenReturn(PullRequestState.DECLINED);

        try {
            participantHelper.addReviewer(pullRequest, user);
        } finally {
            verifyZeroInteractions(activityDao, eventPublisher, participantDao);
        }
    }

    @Test(expected = IllegalPullRequestStateException.class)
    public void testAddReviewerToMergedPullRequest() {
        when(participantDao.findByPullRequestAndUser(pullRequest.getGlobalId(), user.getId())).thenReturn(null);
        when(pullRequest.getState()).thenReturn(PullRequestState.MERGED);

        try {
            participantHelper.addReviewer(pullRequest, user);
        } finally {
            verifyZeroInteractions(activityDao, eventPublisher, participantDao);
        }
    }

    @Test(expected = UnmodifiablePullRequestRoleException.class)
    public void testAddReviewerWhenAuthor() {
        when(participantDao.findByPullRequestAndUser(pullRequest.getGlobalId(), user.getId()))
                .thenReturn(new InternalPullRequestParticipant.Builder(pullRequest)
                        .user(user)
                        .role(PullRequestRole.AUTHOR)
                        .status(PullRequestParticipantStatus.UNAPPROVED)
                        .build());

        try {
            participantHelper.addReviewer(pullRequest, user);
        } finally {
            verify(participantDao).findByPullRequestAndUser(pullRequest.getGlobalId(), user.getId());
            verifyNoMoreInteractions(participantDao);
            verifyZeroInteractions(activityDao, eventPublisher);
        }
    }

    @Test
    public void testAddReviewerNotAParticipant() {
        when(participantDao.findByPullRequestAndUser(pullRequest.getGlobalId(), user.getId())).thenReturn(null);

        participantHelper.addReviewer(pullRequest, user);

        verify(participantDao).findByPullRequestAndUser(pullRequest.getGlobalId(), user.getId());
        verify(participantDao).create(argThat(new ParticipantMatcher(PullRequestRole.REVIEWER,
                PullRequestParticipantStatus.UNAPPROVED)));

        verify(eventPublisher).publish(argThat(new ParticipantsUpdatedEventMatcher(singleton(user), emptySet())));
        verify(eventPublisher).publish(argThat(new ReviewersUpdatedEventMatcher(singleton(user), emptySet())));
        verify(activityDao).create(argThat(new ReviewersUpdatedActivityMatcher(singleton(user), emptySet())));
        verify(eventPublisher).publish(argThat(new ReviewersUpdatedActivityEventMatcher(singleton(user), emptySet())));
        verify(watcherService).addUserAsWatcher(pullRequest, user);

        verifyNoMoreInteractions(eventPublisher, activityDao, participantDao);
    }

    @Test
    public void testAddReviewerNotAParticipantButWatcher() {
        when(participantDao.findByPullRequestAndUser(pullRequest.getGlobalId(), user.getId())).thenReturn(null);

        InternalWatcher watcher = mock(InternalWatcher.class);
        WatcherDao watcherDao = mock(WatcherDao.class);
        when(watcherDao.findByWatchableAndUser(pullRequest, user)).thenReturn(watcher);
        InternalWatcherService localWatcherService = new DefaultWatcherService(authenticationContext, watcherDao,
                eventPublisher);

        InternalPullRequestParticipantHelper participantHelperWithWatcherService =
                new InternalPullRequestParticipantHelper(activityDao, authenticationContext, eventPublisher,
                        i18nService, participantDao, permissionService, permissionValidationService, transactionManager,
                        userService, localWatcherService);

        participantHelperWithWatcherService.addReviewer(pullRequest, user);

        verify(participantDao).findByPullRequestAndUser(pullRequest.getGlobalId(), user.getId());
        verify(participantDao).create(argThat(new ParticipantMatcher(PullRequestRole.REVIEWER,
                PullRequestParticipantStatus.UNAPPROVED)));

        verify(eventPublisher).publish(argThat(new ParticipantsUpdatedEventMatcher(singleton(user), emptySet())));
        verify(eventPublisher).publish(argThat(new ReviewersUpdatedEventMatcher(singleton(user), emptySet())));
        verify(activityDao).create(argThat(new ReviewersUpdatedActivityMatcher(singleton(user), emptySet())));
        verify(eventPublisher).publish(argThat(new ReviewersUpdatedActivityEventMatcher(singleton(user), emptySet())));
        verify(watcherDao).findByWatchableAndUser(pullRequest, user);

        verifyNoMoreInteractions(eventPublisher, activityDao, participantDao, watcherDao);
    }

    @Test
    public void testAddReviewerAlreadyAParticipant() {
        when(participantDao.findByPullRequestAndUser(pullRequest.getGlobalId(), user.getId()))
                .thenReturn(new InternalPullRequestParticipant.Builder(pullRequest)
                        .user(user)
                        .role(PullRequestRole.PARTICIPANT)
                        .status(PullRequestParticipantStatus.UNAPPROVED)
                        .build());

        participantHelper.addReviewer(pullRequest, user);

        verify(participantDao).findByPullRequestAndUser(pullRequest.getGlobalId(), user.getId());
        verify(participantDao).update(argThat(new ParticipantMatcher(PullRequestRole.REVIEWER,
                PullRequestParticipantStatus.UNAPPROVED)));

        verify(eventPublisher).publish(argThat(new ReviewersUpdatedEventMatcher(singleton(user), emptySet())));
        verify(activityDao).create(argThat(new ReviewersUpdatedActivityMatcher(singleton(user), emptySet())));
        verify(eventPublisher).publish(argThat(new ReviewersUpdatedActivityEventMatcher(singleton(user), emptySet())));
        verify(watcherService).addUserAsWatcher(pullRequest, user);

        verifyNoMoreInteractions(eventPublisher, activityDao, participantDao);
    }

    @Test
    public void testAddReviewerAlreadyAParticipantAndWatcher() {
        when(participantDao.findByPullRequestAndUser(pullRequest.getGlobalId(), user.getId()))
                .thenReturn(new InternalPullRequestParticipant.Builder(pullRequest)
                        .user(user)
                        .role(PullRequestRole.PARTICIPANT)
                        .status(PullRequestParticipantStatus.UNAPPROVED)
                        .build());

        InternalWatcher watcher = mock(InternalWatcher.class);
        WatcherDao watcherDao = mock(WatcherDao.class);
        when(watcherDao.findByWatchableAndUser(pullRequest, user)).thenReturn(watcher);
        InternalWatcherService localWatcherService = new DefaultWatcherService(authenticationContext, watcherDao,
                eventPublisher);

        InternalPullRequestParticipantHelper participantHelperWithWatcherService =
                new InternalPullRequestParticipantHelper(activityDao, authenticationContext, eventPublisher,
                        i18nService, participantDao, permissionService, permissionValidationService, transactionManager,
                        userService, localWatcherService);

        participantHelperWithWatcherService.addReviewer(pullRequest, user);

        verify(participantDao).findByPullRequestAndUser(pullRequest.getGlobalId(), user.getId());
        verify(participantDao).update(argThat(new ParticipantMatcher(PullRequestRole.REVIEWER,
                PullRequestParticipantStatus.UNAPPROVED)));

        verify(eventPublisher).publish(argThat(new ReviewersUpdatedEventMatcher(singleton(user), emptySet())));
        verify(activityDao).create(argThat(new ReviewersUpdatedActivityMatcher(singleton(user), emptySet())));
        verify(eventPublisher).publish(argThat(new ReviewersUpdatedActivityEventMatcher(singleton(user), emptySet())));
        verify(watcherDao).findByWatchableAndUser(pullRequest, user);

        verifyNoMoreInteractions(eventPublisher, activityDao, participantDao, watcherDao);
    }

    @Test
    public void testAddReviewerAlreadyAReviewer() {
        InternalPullRequestParticipant participant = new InternalPullRequestParticipant.Builder(pullRequest)
                .user(user)
                .role(PullRequestRole.REVIEWER)
                .status(PullRequestParticipantStatus.UNAPPROVED)
                .build();

        when(participantDao.findByPullRequestAndUser(pullRequest.getGlobalId(), user.getId())).thenReturn(participant);

        assertSame(participant, participantHelper.addReviewer(pullRequest, user));

        verify(participantDao).findByPullRequestAndUser(pullRequest.getGlobalId(), user.getId());
        verifyNoMoreInteractions(participantDao);
        verifyZeroInteractions(activityDao, eventPublisher, watcherService);
    }

    @Test
    public void testCreateParticipants() {
        participantHelper.createParticipants(pullRequest, currentUser, singleton(user));

        verify(participantDao).create(argThat(new ParticipantMatcher(currentUser, PullRequestRole.AUTHOR,
                PullRequestParticipantStatus.UNAPPROVED)));
        verify(participantDao).create(argThat(new ParticipantMatcher(user, PullRequestRole.REVIEWER,
                PullRequestParticipantStatus.UNAPPROVED)));
        verifyNoMoreInteractions(participantDao);
    }

    @Test
    public void testGetParticipants() {
        Set<InternalPullRequestParticipant> expected = ImmutableSet.<InternalPullRequestParticipant>builder()
                .add(new InternalPullRequestParticipant.Builder(pullRequest)
                        .user(currentUser)
                        .role(PullRequestRole.AUTHOR)
                        .status(PullRequestParticipantStatus.UNAPPROVED)
                        .build())
                .add(new InternalPullRequestParticipant.Builder(pullRequest)
                        .user(user)
                        .role(PullRequestRole.REVIEWER)
                        .status(PullRequestParticipantStatus.APPROVED)
                        .build())
                .build();
        PageRequest pageRequest = PageUtils.newRequest(0, 100);
        Page<InternalPullRequestParticipant> expectedPage = PageUtils.createPage(expected, pageRequest);
        when(participantDao.findByPullRequest(pullRequest.getGlobalId(), pageRequest))
                .thenReturn(expectedPage);

        Page<InternalPullRequestParticipant> actualPage = participantHelper.getParticipants(pullRequest, pageRequest);

        assertSame(expectedPage, actualPage);
    }

    @Test
    public void testMakeCurrentUserParticipantAndWatcher() {
        when(participantDao.findByPullRequestAndUser(pullRequest.getGlobalId(), currentUser.getId())).thenReturn(null);

        participantHelper.makeCurrentUserParticipantAndWatcher(pullRequest);

        verify(participantDao).findByPullRequestAndUser(pullRequest.getGlobalId(), currentUser.getId());
        verify(participantDao).create(argThat(new ParticipantMatcher(currentUser, PullRequestRole.PARTICIPANT,
                PullRequestParticipantStatus.UNAPPROVED)));

        verify(watcherService).addUserAsWatcher(pullRequest, currentUser);
        verify(eventPublisher).publish(argThat(new ParticipantsUpdatedEventMatcher(singleton(currentUser), emptySet())));
        verifyNoMoreInteractions(eventPublisher, participantDao);
        verifyZeroInteractions(activityDao);
    }

    @Test
    public void testMakeCurrentUserParticipantAndWatcherWhenAlreadyAParticipant() {
        when(participantDao.findByPullRequestAndUser(pullRequest.getGlobalId(), currentUser.getId()))
                .thenReturn(new InternalPullRequestParticipant.Builder(pullRequest)
                        .user(currentUser)
                        .role(PullRequestRole.PARTICIPANT)
                        .status(PullRequestParticipantStatus.UNAPPROVED)
                        .build());

        participantHelper.makeCurrentUserParticipantAndWatcher(pullRequest);

        verify(participantDao).findByPullRequestAndUser(pullRequest.getGlobalId(), currentUser.getId());
        verifyNoMoreInteractions(participantDao);
        verifyZeroInteractions(activityDao, eventPublisher, watcherService);
    }

    @Test
    public void testOnPullRequestRescoped() {
        PullRequestRescopedEvent event = mock(PullRequestRescopedEvent.class);
        when(event.isFromHashUpdated()).thenReturn(true);

        when(event.getPullRequest()).thenReturn(pullRequest);

        participantHelper.onPullRequestRescoped(event);

        verify(participantDao).resetStatusByPullRequest(pullRequest.getGlobalId());
        verifyNoMoreInteractions(participantDao);
    }

    @Test
    public void testOnPullRequestRescopedFromRefUnchanged() {
        PullRequestRescopedEvent event = mock(PullRequestRescopedEvent.class);
        when(event.isFromHashUpdated()).thenReturn(false);

        participantHelper.onPullRequestRescoped(event);

        verify(participantDao, never()).resetStatusByPullRequest(pullRequest.getGlobalId());
    }

    @Test(expected = IllegalPullRequestStateException.class)
    public void testRemoveReviewerFromDeclinedPullRequest() {
        when(pullRequest.getState()).thenReturn(PullRequestState.DECLINED);

        try {
            participantHelper.removeReviewer(pullRequest, user);
        } finally {
            verifyZeroInteractions(activityDao, eventPublisher, participantDao);
        }
    }

    @Test(expected = IllegalPullRequestStateException.class)
    public void testRemoveReviewerFromMergedPullRequest() {
        when(pullRequest.getState()).thenReturn(PullRequestState.MERGED);

        try {
            participantHelper.removeReviewer(pullRequest, user);
        } finally {
            verifyZeroInteractions(activityDao, eventPublisher, participantDao);
        }
    }

    @Test
    public void testRemoveReviewerIfNotAParticipant() {
        when(participantDao.findByPullRequestAndUser(pullRequest.getGlobalId(), user.getId())).thenReturn(null);

        participantHelper.removeReviewer(pullRequest, user);

        verify(participantDao).findByPullRequestAndUser(pullRequest.getGlobalId(), user.getId());
        verifyNoMoreInteractions(participantDao);
        verifyZeroInteractions(activityDao, eventPublisher);
    }

    @Test(expected = UnmodifiablePullRequestRoleException.class)
    public void testRemoveReviewerIfAuthor() {
        when(participantDao.findByPullRequestAndUser(pullRequest.getGlobalId(), user.getId()))
                .thenReturn(new InternalPullRequestParticipant.Builder(pullRequest)
                        .role(PullRequestRole.AUTHOR)
                        .build());
        try {
            participantHelper.removeReviewer(pullRequest, user);
        } finally {
            verify(participantDao).findByPullRequestAndUser(pullRequest.getGlobalId(), user.getId());
            verifyNoMoreInteractions(participantDao);
            verifyZeroInteractions(activityDao, eventPublisher);
        }
    }

    @Test
    public void testRemoveReviewerIfHaveNotContributed() {
        when(participantDao.findByPullRequestAndUser(pullRequest.getGlobalId(), user.getId()))
                .thenReturn(new InternalPullRequestParticipant.Builder(pullRequest)
                        .user(user)
                        .role(PullRequestRole.REVIEWER)
                        .build());

        when(activityDao.countByUser(pullRequest.getGlobalId(), user.getId(), emptySet(),
                singleton(PullRequestAction.RESCOPED))).thenReturn(0L);

        participantHelper.removeReviewer(pullRequest, user);

        verify(participantDao).findByPullRequestAndUser(pullRequest.getGlobalId(), user.getId());
        verify(participantDao).delete(argThat(new ParticipantMatcher(PullRequestRole.REVIEWER,
                PullRequestParticipantStatus.UNAPPROVED)));

        verify(activityDao).countByUser(pullRequest.getGlobalId(), user.getId(), emptySet(),
                singleton(PullRequestAction.RESCOPED));
        verify(activityDao).create(argThat(new ReviewersUpdatedActivityMatcher(emptySet(), singleton(user))));
        verify(eventPublisher).publish(argThat(new ReviewersUpdatedActivityEventMatcher(emptySet(), singleton(user))));
        verify(eventPublisher).publish(argThat(new ParticipantsUpdatedEventMatcher(emptySet(), singleton(user))));
        verify(eventPublisher).publish(argThat(new ReviewersUpdatedEventMatcher(emptySet(), singleton(user))));
        verify(watcherService).removeUserAsWatcher(pullRequest, user);
        verifyNoMoreInteractions(activityDao, eventPublisher, participantDao);
    }

    @Test
    public void testRemoveReviewerIfHaveContributed() {
        when(participantDao.findByPullRequestAndUser(pullRequest.getGlobalId(), user.getId()))
                .thenReturn(new InternalPullRequestParticipant.Builder(pullRequest)
                        .user(user)
                        .role(PullRequestRole.REVIEWER)
                        .status(PullRequestParticipantStatus.APPROVED)
                        .build());

        when(activityDao.countByUser(pullRequest.getGlobalId(), user.getId(), emptySet(),
                singleton(PullRequestAction.RESCOPED))).thenReturn(10L);

        participantHelper.removeReviewer(pullRequest, user);

        verify(participantDao).findByPullRequestAndUser(pullRequest.getGlobalId(), user.getId());
        verify(participantDao).update(argThat(new ParticipantMatcher(PullRequestRole.PARTICIPANT,
                PullRequestParticipantStatus.UNAPPROVED)));

        verify(activityDao).countByUser(pullRequest.getGlobalId(), user.getId(), emptySet(),
                singleton(PullRequestAction.RESCOPED));
        verify(activityDao).create(argThat(new ReviewersUpdatedActivityMatcher(emptySet(), singleton(user))));
        verify(eventPublisher).publish(argThat(new ReviewersUpdatedActivityEventMatcher(emptySet(), singleton(user))));
        verify(eventPublisher).publish(argThat(new ReviewersUpdatedEventMatcher(emptySet(), singleton(user))));
        verifyNoMoreInteractions(activityDao, eventPublisher, participantDao);
    }

    @Test(expected = InvalidPullRequestReviewersException.class)
    public void testResolveReviewersInactive() {
        Set<String> reviewers = singleton("inactive");

        when(user.getName()).thenReturn("inactive");
        when(user.isActive()).thenReturn(false);
        when(userService.getUsersByName(reviewers, true)).thenReturn(singleton(user));

        try {
            participantHelper.resolveReviewers(targetRepository, reviewers);
        } finally {
            verifyZeroInteractions(participantDao);
        }
    }

    @Test(expected = InvalidPullRequestReviewersException.class)
    public void testResolveReviewersNotFound() {
        Set<String> reviewers = singleton("reviewer");
        when(userService.getUsersByName(reviewers, true)).thenReturn(emptySet());

        participantHelper.resolveReviewers(targetRepository, reviewers);
    }

    @Test
    public void testResolveReviewersEmptySet() {
        when(userService.getUsersByName(emptySet(), true)).thenReturn(emptySet());

        try {
            assertTrue(participantHelper.resolveReviewers(targetRepository, emptySet()).isEmpty());
        } finally {
            verifyZeroInteractions(participantDao);
        }
    }

    @Test(expected = InvalidPullRequestReviewersException.class)
    public void testResolveReviewersNotLicensed() {
        Set<String> reviewers = singleton("not_licensed");

        when(user.getName()).thenReturn("not_licensed");
        when(user.isActive()).thenReturn(true);

        when(userService.getUsersByName(reviewers, true)).thenReturn(singleton(user));

        when(permissionService.hasGlobalPermission(user, Permission.LICENSED_USER)).thenReturn(false);

        try {
            participantHelper.resolveReviewers(targetRepository, reviewers);
        } finally {
            verifyZeroInteractions(participantDao);
        }
    }

    @Test(expected = InvalidPullRequestReviewersException.class)
    public void testResolveReviewersCantReadRepo() {
        Set<String> reviewers = ImmutableSet.of("cant_read", "reviewer");

        when(user.getName()).thenReturn("reviewer");
        when(user.isActive()).thenReturn(true);

        ApplicationUser cantRead = mock(ApplicationUser.class);
        when(cantRead.getName()).thenReturn("cant_read");
        when(cantRead.isActive()).thenReturn(true);

        when(userService.getUsersByName(reviewers, true)).thenReturn(ImmutableSet.of(user, cantRead));

        when(permissionService.hasGlobalPermission(user, Permission.LICENSED_USER)).thenReturn(true);
        when(permissionService.hasRepositoryPermission(user, targetRepository, Permission.REPO_READ)).thenReturn(false);

        try {
            participantHelper.resolveReviewers(targetRepository, reviewers);
        } finally {
            verifyZeroInteractions(participantDao);
        }
    }

    @Test
    public void testResolveReviewers() {
        Set<String> reviewers = singleton("reviewer");

        when(user.getName()).thenReturn("reviewer");
        when(user.isActive()).thenReturn(true);

        when(userService.getUsersByName(reviewers, true)).thenReturn(singleton(user));

        when(permissionService.hasGlobalPermission(user, Permission.LICENSED_USER)).thenReturn(true);
        when(permissionService.hasRepositoryPermission(user, targetRepository, Permission.REPO_READ)).thenReturn(true);

        try {
            assertEquals(singleton(user), participantHelper.resolveReviewers(targetRepository, reviewers));
        } finally {
            verifyZeroInteractions(participantDao);
        }
    }

    @Test(expected = AuthorisationException.class)
    public void testSearchUsersCantReadSourceRepository() {
        doThrow(AuthorisationException.class)
                .when(permissionValidationService).validateForRepository(sourceRepository, Permission.REPO_READ);

        try {
            participantHelper.searchUsers(new PullRequestParticipantSearchRequest.Builder()
                    .fromRepository(sourceRepository)
                    .build(), PageUtils.newRequest(0, 100));
        } finally {
            verifyZeroInteractions(participantDao);
        }

    }

    @Test(expected = AuthorisationException.class)
    public void testSearchUsersCantReadTargetRepository() {
        doThrow(AuthorisationException.class)
                .when(permissionValidationService).validateForRepository(targetRepository, Permission.REPO_READ);

        try {
            participantHelper.searchUsers(new PullRequestParticipantSearchRequest.Builder()
                    .fromRepository(sourceRepository)
                    .toRepository(targetRepository)
                    .build(), PageUtils.newRequest(0, 100));
        } finally {
            verifyZeroInteractions(participantDao);
        }
    }

    @Test
    public void testSearchParticipantsNoFilter() {
        PageRequest pageRequest = spy(PageUtils.newRequest(0, 5));
        when(pageRequest.buildRestrictedPageRequest(1000)).thenReturn(pageRequest);

        List<ApplicationUser> expectedParticipants = getExpectedParticipants().stream()
                .sorted(comparing(ApplicationUser::getDisplayName, String.CASE_INSENSITIVE_ORDER))
                .collect(toList());
        Page page = PageUtils.createPage(expectedParticipants, pageRequest);

        when(participantDao.searchUsers(any(PullRequestParticipantSearchCriteria.class), any(PageRequest.class))).thenReturn(page);

        Page<ApplicationUser> actualParticipants = participantHelper.searchUsers(new PullRequestParticipantSearchRequest.Builder()
                .fromRepository(sourceRepository)
                .role(PullRequestRole.AUTHOR)
                .toRepository(targetRepository)
                .build(), pageRequest);
        assertTrue(Iterables.elementsEqual(expectedParticipants, actualParticipants.getValues()));

        verify(pageRequest).buildRestrictedPageRequest(1000);

        SearchCriteriaMatcher criteriaMatcher = new SearchCriteriaMatcher(sourceRepository, targetRepository, PullRequestRole.AUTHOR);
        PageRequestMatcher pageRequestMatcher = new PageRequestMatcher(pageRequest.getStart() + pageRequest.getLimit());
        verify(participantDao).searchUsers(argThat(criteriaMatcher), argThat(pageRequestMatcher));
        verifyNoMoreInteractions(participantDao);
    }

    @Test
    public void testSearchParticipantsWithCommonNameFilter() {
        PageRequest pageRequest = spy(PageUtils.newRequest(0, 5));
        when(pageRequest.buildRestrictedPageRequest(1000)).thenReturn(pageRequest);

        List<ApplicationUser> expectedParticipants = getExpectedParticipants().stream()
                .filter(user -> user.getEmailAddress() != null)
                .sorted(comparing(ApplicationUser::getDisplayName, String.CASE_INSENSITIVE_ORDER))
                .collect(toList());
        Page page = PageUtils.createPage(expectedParticipants, pageRequest);

        when(participantDao.searchUsers(any(PullRequestParticipantSearchCriteria.class), any(PageRequest.class))).thenReturn(page);

        Page<ApplicationUser> actualParticipants = participantHelper.searchUsers(new PullRequestParticipantSearchRequest.Builder()
                .fromRepository(sourceRepository)
                .role(PullRequestRole.AUTHOR)
                .toRepository(targetRepository)
                .filter("@cricket.com")
                .build(), pageRequest);
        assertTrue(Iterables.elementsEqual(expectedParticipants, actualParticipants.getValues()));

        verify(pageRequest).buildRestrictedPageRequest(1000);

        SearchCriteriaMatcher criteriaMatcher = new SearchCriteriaMatcher(sourceRepository, targetRepository, PullRequestRole.AUTHOR);
        PageRequestMatcher pageRequestMatcher = new PageRequestMatcher(pageRequest.getStart() + pageRequest.getLimit() * 2); // See InternalPageUtils.filterPages
        verify(participantDao).searchUsers(argThat(criteriaMatcher), argThat(pageRequestMatcher));
        verifyNoMoreInteractions(participantDao);
    }

    @Test
    public void testSearchParticipantsWithEmailFilter() {
        PageRequest pageRequest = spy(PageUtils.newRequest(0, 5));
        when(pageRequest.buildRestrictedPageRequest(1000)).thenReturn(pageRequest);

        Set<ApplicationUser> expectedParticipants = getExpectedParticipants();
        Page page = PageUtils.createPage(expectedParticipants, pageRequest);

        when(participantDao.searchUsers(any(PullRequestParticipantSearchCriteria.class), any(PageRequest.class))).thenReturn(page);

        Page<ApplicationUser> actualParticipants = participantHelper.searchUsers(new PullRequestParticipantSearchRequest.Builder()
                .fromRepository(sourceRepository)
                .role(PullRequestRole.REVIEWER)
                .toRepository(targetRepository)
                .filter("Steve.Waugh@cricket.com")
                .build(), pageRequest);
        assertEquals(1, actualParticipants.getSize());
        assertEquals("Steve.Waugh@cricket.com", actualParticipants.getValues().iterator().next().getEmailAddress());

        verify(pageRequest).buildRestrictedPageRequest(1000);

        SearchCriteriaMatcher criteriaMatcher = new SearchCriteriaMatcher(sourceRepository, targetRepository, PullRequestRole.REVIEWER);
        PageRequestMatcher pageRequestMatcher = new PageRequestMatcher(pageRequest.getStart() + pageRequest.getLimit() * 2); // See InternalPageUtils.filterPages
        verify(participantDao).searchUsers(argThat(criteriaMatcher), argThat(pageRequestMatcher));
        verifyNoMoreInteractions(participantDao);
    }

    @Test
    public void testSearchParticipantsWithNameFilter() {
        PageRequest pageRequest = spy(PageUtils.newRequest(0, 5));
        when(pageRequest.buildRestrictedPageRequest(1000)).thenReturn(pageRequest);

        Set<ApplicationUser> expectedParticipants = getExpectedParticipants();
        Page page = PageUtils.createPage(expectedParticipants, pageRequest);

        when(participantDao.searchUsers(any(PullRequestParticipantSearchCriteria.class), any(PageRequest.class))).thenReturn(page);

        Page<ApplicationUser> actualParticipants = participantHelper.searchUsers(new PullRequestParticipantSearchRequest.Builder()
                .fromRepository(sourceRepository)
                .role(PullRequestRole.REVIEWER)
                .toRepository(targetRepository)
                .filter("mark")
                .build(), pageRequest);
        assertEquals(1, actualParticipants.getSize());
        assertEquals("mark", actualParticipants.getValues().iterator().next().getName());

        verify(pageRequest).buildRestrictedPageRequest(1000);

        SearchCriteriaMatcher criteriaMatcher = new SearchCriteriaMatcher(sourceRepository, targetRepository, PullRequestRole.REVIEWER);
        PageRequestMatcher pageRequestMatcher = new PageRequestMatcher(pageRequest.getStart() + pageRequest.getLimit() * 2); // See InternalPageUtils.filterPages
        verify(participantDao).searchUsers(argThat(criteriaMatcher), argThat(pageRequestMatcher));
        verifyNoMoreInteractions(participantDao);
    }

    @Test
    public void testSetReviewersAddUsersNotParticipating() {
        InternalApplicationUser author = mock(InternalApplicationUser.class);
        when(author.getName()).thenReturn("author");

        when(pullRequest.getAuthor()).thenReturn(new InternalPullRequestParticipant.Builder(pullRequest)
                .user(author)
                .role(PullRequestRole.AUTHOR)
                .status(PullRequestParticipantStatus.UNAPPROVED)
                .build());

        Set<ApplicationUser> expectedParticipants = getExpectedParticipants();
        expectedParticipants.forEach(user -> {
            when(user.isActive()).thenReturn(true);
            when(permissionService.hasGlobalPermission(user, Permission.LICENSED_USER)).thenReturn(true);
            when(permissionService.hasRepositoryPermission(user, targetRepository, Permission.REPO_READ)).thenReturn(true);
        });

        Set<String> addedReviewers = expectedParticipants.stream()
                .map(ApplicationUser::getName)
                .collect(toSet());
        when(userService.getUsersByName(addedReviewers, true)).thenReturn(expectedParticipants);

        participantHelper.setReviewers(pullRequest, addedReviewers);

        expectedParticipants.forEach(participant -> {
            verify(participantDao).create(argThat(new ParticipantMatcher(participant, PullRequestRole.REVIEWER,
                    PullRequestParticipantStatus.UNAPPROVED)));
            verify(watcherService).addUserAsWatcher(pullRequest, participant);
        });

        verify(eventPublisher).publish(argThat(new ParticipantsUpdatedEventMatcher(expectedParticipants, emptySet())));
        verify(eventPublisher).publish(argThat(new ReviewersUpdatedEventMatcher(expectedParticipants, emptySet())));
        verify(activityDao).create(argThat(new ReviewersUpdatedActivityMatcher(expectedParticipants, emptySet())));
        verify(eventPublisher).publish(argThat(new ReviewersUpdatedActivityEventMatcher(expectedParticipants, emptySet())));
        verifyNoMoreInteractions(eventPublisher, activityDao, participantDao);
    }

    @Test
    public void testSetReviewersAddUsersAlreadyParticipating() {
        InternalApplicationUser author = mock(InternalApplicationUser.class);
        when(author.getName()).thenReturn("author");

        when(pullRequest.getAuthor()).thenReturn(new InternalPullRequestParticipant.Builder(pullRequest)
                .user(author)
                .role(PullRequestRole.AUTHOR)
                .status(PullRequestParticipantStatus.UNAPPROVED)
                .build());

        when(user.getName()).thenReturn("participant");
        when(user.isActive()).thenReturn(true);

        Set<PullRequestParticipant> participants = singleton(new InternalPullRequestParticipant.Builder(pullRequest)
                .user(user)
                .role(PullRequestRole.PARTICIPANT)
                .status(PullRequestParticipantStatus.UNAPPROVED)
                .build());
        when(pullRequest.getParticipants()).thenReturn(participants);
        when(pullRequest.getAllParticipants()).thenReturn(participants);

        Set<String> addedReviewers = singleton("participant");
        when(userService.getUsersByName(addedReviewers, true)).thenReturn(singleton(user));

        when(permissionService.hasGlobalPermission(user, Permission.LICENSED_USER)).thenReturn(true);
        when(permissionService.hasRepositoryPermission(user, targetRepository, Permission.REPO_READ)).thenReturn(true);

        participantHelper.setReviewers(pullRequest, addedReviewers);

        verify(participantDao).update(argThat(new ParticipantMatcher(user, PullRequestRole.REVIEWER,
                PullRequestParticipantStatus.UNAPPROVED)));
        verifyZeroInteractions(watcherService);

        verify(eventPublisher).publish(argThat(new ReviewersUpdatedEventMatcher(singleton(user), emptySet())));
        verify(activityDao).create(argThat(new ReviewersUpdatedActivityMatcher(singleton(user), emptySet())));
        verify(eventPublisher).publish(argThat(new ReviewersUpdatedActivityEventMatcher(singleton(user), emptySet())));
        verifyNoMoreInteractions(eventPublisher, activityDao, participantDao);
    }

    @Test
    public void testSetReviewersAddUserAuthor() {
        InternalApplicationUser author = mock(InternalApplicationUser.class);
        when(author.getName()).thenReturn("author");

        when(pullRequest.getAuthor()).thenReturn(new InternalPullRequestParticipant.Builder(pullRequest)
                .user(author)
                .role(PullRequestRole.AUTHOR)
                .status(PullRequestParticipantStatus.UNAPPROVED)
                .build());

        Set<String> addedReviewers = singleton("author");
        when(userService.getUsersByName(addedReviewers, true)).thenReturn(singleton(author));

        participantHelper.setReviewers(pullRequest, addedReviewers);

        verifyZeroInteractions(participantDao, permissionService, watcherService, eventPublisher, activityDao);
    }

    @Test(expected = InvalidPullRequestReviewersException.class)
    public void testSetReviewersAddUserInactive() {
        InternalApplicationUser author = mock(InternalApplicationUser.class);
        when(author.getName()).thenReturn("author");

        when(pullRequest.getAuthor()).thenReturn(new InternalPullRequestParticipant.Builder(pullRequest)
                .user(author)
                .role(PullRequestRole.AUTHOR)
                .status(PullRequestParticipantStatus.UNAPPROVED)
                .build());

        when(user.getName()).thenReturn("inactive");
        when(user.isActive()).thenReturn(false);

        Set<String> addedReviewers = singleton("inactive");
        when(userService.getUsersByName(addedReviewers, true)).thenReturn(singleton(user));

        try {
            participantHelper.setReviewers(pullRequest, addedReviewers);
        } finally {
            verifyZeroInteractions(participantDao, permissionService, watcherService, eventPublisher, activityDao);
        }
    }

    @Test(expected = InvalidPullRequestReviewersException.class)
    public void testSetReviewersAddUserUnlicensed() {
        InternalApplicationUser author = mock(InternalApplicationUser.class);
        when(author.getName()).thenReturn("author");

        when(pullRequest.getAuthor()).thenReturn(new InternalPullRequestParticipant.Builder(pullRequest)
                .user(author)
                .role(PullRequestRole.AUTHOR)
                .status(PullRequestParticipantStatus.UNAPPROVED)
                .build());

        when(user.getName()).thenReturn("unlicensed");
        when(user.isActive()).thenReturn(true);

        when(permissionService.hasGlobalPermission(user, Permission.LICENSED_USER)).thenReturn(false);

        Set<String> addedReviewers = singleton("unlicensed");
        when(userService.getUsersByName(addedReviewers, true)).thenReturn(singleton(user));

        try {
            participantHelper.setReviewers(pullRequest, addedReviewers);
        } finally {
            verify(permissionService).hasGlobalPermission(user, Permission.LICENSED_USER);
            verifyNoMoreInteractions(permissionService);
            verifyZeroInteractions(participantDao, watcherService, eventPublisher, activityDao);
        }
    }

    @Test(expected = InvalidPullRequestReviewersException.class)
    public void testSetReviewersAddUserWithoutRepoRead() {
        InternalApplicationUser author = mock(InternalApplicationUser.class);
        when(author.getName()).thenReturn("author");

        when(pullRequest.getAuthor()).thenReturn(new InternalPullRequestParticipant.Builder(pullRequest)
                .user(author)
                .role(PullRequestRole.AUTHOR)
                .status(PullRequestParticipantStatus.UNAPPROVED)
                .build());

        when(user.getName()).thenReturn("cant_read");
        when(user.isActive()).thenReturn(true);

        when(permissionService.hasGlobalPermission(user, Permission.LICENSED_USER)).thenReturn(true);
        when(permissionService.hasRepositoryPermission(user, targetRepository, Permission.REPO_READ)).thenReturn(false);

        Set<String> addedReviewers = singleton("cant_read");
        when(userService.getUsersByName(addedReviewers, true)).thenReturn(singleton(user));

        try {
            participantHelper.setReviewers(pullRequest, addedReviewers);
        } finally {
            verifyZeroInteractions(participantDao, watcherService, eventPublisher, activityDao);
        }
    }

    @Test
    public void testSetReviewersRemoveUsersWithActivity() {
        InternalApplicationUser author = mock(InternalApplicationUser.class);
        when(author.getName()).thenReturn("author");

        when(pullRequest.getAuthor()).thenReturn(new InternalPullRequestParticipant.Builder(pullRequest)
                .user(author)
                .role(PullRequestRole.AUTHOR)
                .status(PullRequestParticipantStatus.UNAPPROVED)
                .build());

        when(user.getName()).thenReturn("reviewer");
        when(user.isActive()).thenReturn(true);

        Set<PullRequestParticipant> participants = singleton(new InternalPullRequestParticipant.Builder(pullRequest)
                .user(user)
                .role(PullRequestRole.REVIEWER)
                .status(PullRequestParticipantStatus.APPROVED)
                .build());
        when(pullRequest.getReviewers()).thenReturn(participants);
        when(pullRequest.getAllParticipants()).thenReturn(participants);

        Set<String> removedReviewers = singleton("reviewer");
        when(userService.getUsersByName(removedReviewers, true)).thenReturn(singleton(user));

        when(activityDao.findUsersWithActivities(pullRequest.getGlobalId(), emptySet(), singleton(PullRequestAction.RESCOPED)))
                .thenReturn(singleton(user));

        participantHelper.setReviewers(pullRequest, emptySet());

        verify(participantDao).update(argThat(new ParticipantMatcher(user, PullRequestRole.PARTICIPANT,
                PullRequestParticipantStatus.UNAPPROVED)));

        verify(eventPublisher).publish(argThat(new ReviewersUpdatedEventMatcher(emptySet(), singleton(user))));
        verify(activityDao)
                .findUsersWithActivities(pullRequest.getGlobalId(), emptySet(), singleton(PullRequestAction.RESCOPED));
        verify(activityDao).create(argThat(new ReviewersUpdatedActivityMatcher(emptySet(), singleton(user))));
        verify(eventPublisher).publish(argThat(new ReviewersUpdatedActivityEventMatcher(emptySet(), singleton(user))));
        verifyNoMoreInteractions(eventPublisher, activityDao);
    }

    @Test
    public void testSetReviewersRemoveUsersWithoutActivity() {
        InternalApplicationUser author = mock(InternalApplicationUser.class);
        when(author.getName()).thenReturn("author");

        when(pullRequest.getAuthor()).thenReturn(new InternalPullRequestParticipant.Builder(pullRequest)
                .user(author)
                .role(PullRequestRole.AUTHOR)
                .status(PullRequestParticipantStatus.UNAPPROVED)
                .build());

        when(user.getName()).thenReturn("reviewer");
        when(user.isActive()).thenReturn(true);

        Set<PullRequestParticipant> participants = singleton(new InternalPullRequestParticipant.Builder(pullRequest)
                .user(user)
                .role(PullRequestRole.REVIEWER)
                .status(PullRequestParticipantStatus.APPROVED)
                .build());
        when(pullRequest.getReviewers()).thenReturn(participants);
        when(pullRequest.getAllParticipants()).thenReturn(participants);

        Set<String> removedReviewers = singleton("reviewer");
        when(userService.getUsersByName(removedReviewers, true)).thenReturn(singleton(user));

        when(activityDao.findUsersWithActivities(pullRequest.getGlobalId(), emptySet(), singleton(PullRequestAction.RESCOPED)))
                .thenReturn(emptySet());

        participantHelper.setReviewers(pullRequest, emptySet());

        verify(participantDao).delete(argThat(new ParticipantMatcher(user, PullRequestRole.REVIEWER,
                PullRequestParticipantStatus.APPROVED)));

        verify(eventPublisher).publish(argThat(new ParticipantsUpdatedEventMatcher(emptySet(), singleton(user))));
        verify(eventPublisher).publish(argThat(new ReviewersUpdatedEventMatcher(emptySet(), singleton(user))));
        verify(activityDao)
                .findUsersWithActivities(pullRequest.getGlobalId(), emptySet(), singleton(PullRequestAction.RESCOPED));
        verify(activityDao).create(argThat(new ReviewersUpdatedActivityMatcher(emptySet(), singleton(user))));
        verify(eventPublisher).publish(argThat(new ReviewersUpdatedActivityEventMatcher(emptySet(), singleton(user))));
        verifyNoMoreInteractions(eventPublisher, activityDao);
    }

    @Test(expected = IllegalPullRequestStateException.class)
    public void testSetStatusInDeclinedPullRequest() {
        when(pullRequest.getState()).thenReturn(PullRequestState.DECLINED);
        try {
            participantHelper.setStatus(pullRequest, PullRequestParticipantStatus.APPROVED);
        } finally {
            verifyZeroInteractions(activityDao, eventPublisher, participantDao);
        }
    }

    @Test(expected = IllegalPullRequestStateException.class)
    public void testSetStatusInMergedPullRequest() {
        when(pullRequest.getState()).thenReturn(PullRequestState.MERGED);
        try {
            participantHelper.setStatus(pullRequest, PullRequestParticipantStatus.APPROVED);
        } finally {
            verifyZeroInteractions(activityDao, eventPublisher, participantDao);
        }
    }

    @Test(expected = NoSuchParticipantException.class)
    public void testSetStatusToUnapprovedIfNotAParticipant() {
        when(participantDao.findByPullRequestAndUser(pullRequest.getGlobalId(), currentUser.getId())).thenReturn(null);
        try {
            participantHelper.setStatus(pullRequest, PullRequestParticipantStatus.UNAPPROVED);
        } finally {
            verify(participantDao).findByPullRequestAndUser(pullRequest.getGlobalId(), currentUser.getId());
            verifyNoMoreInteractions(participantDao);
            verifyZeroInteractions(activityDao, eventPublisher, participantDao);
        }
    }

    @Test(expected = InvalidPullRequestRoleException.class)
    public void testSetStatusIfAuthor() {
        when(participantDao.findByPullRequestAndUser(pullRequest.getGlobalId(), currentUser.getId()))
                .thenReturn(new InternalPullRequestParticipant.Builder(pullRequest)
                        .user(currentUser)
                        .role(PullRequestRole.AUTHOR)
                        .status(PullRequestParticipantStatus.UNAPPROVED)
                        .build());

        try {
            participantHelper.setStatus(pullRequest, PullRequestParticipantStatus.NEEDS_WORK);
        } finally {
            verify(participantDao).findByPullRequestAndUser(pullRequest.getGlobalId(), currentUser.getId());
            verifyNoMoreInteractions(participantDao);
            verifyZeroInteractions(activityDao, eventPublisher, participantDao);
        }
    }

    @Test
    public void testSetStatusIfNotAParticipant() {
        when(participantDao.findByPullRequestAndUser(pullRequest.getGlobalId(), currentUser.getId())).thenReturn(null);

        participantHelper.setStatus(pullRequest, PullRequestParticipantStatus.NEEDS_WORK);

        verify(participantDao).findByPullRequestAndUser(pullRequest.getGlobalId(), currentUser.getId());
        verify(participantDao).create(argThat(new ParticipantMatcher(currentUser, PullRequestRole.PARTICIPANT,
                PullRequestParticipantStatus.UNAPPROVED)));
        ParticipantMatcher reviewerMatcher = new ParticipantMatcher(currentUser, PULL_REQUEST_LATEST_COMMIT,
                PullRequestRole.REVIEWER, PullRequestParticipantStatus.NEEDS_WORK);
        verify(participantDao).update(argThat(reviewerMatcher));
        verify(watcherService).addUserAsWatcher(pullRequest, currentUser);

        verify(eventPublisher).publish(argThat(new ParticipantsUpdatedEventMatcher(singleton(currentUser), emptySet())));
        verify(eventPublisher).publish(argThat(new ParticipantStatusUpdatedEventMatcher(reviewerMatcher,
                PullRequestParticipantStatus.UNAPPROVED)));
        verify(eventPublisher).publish(argThat(new ReviewersUpdatedEventMatcher(singleton(currentUser), emptySet())));
        verify(eventPublisher).publish(argThat(new PullRequestActivityEventMatcher(pullRequest, PullRequestAction.REVIEWED)));
        verify(activityDao).create(argThat(new ReviewersUpdatedActivityMatcher(singleton(currentUser), emptySet())));
        verify(eventPublisher).publish(argThat(new ReviewersUpdatedActivityEventMatcher(singleton(currentUser), emptySet())));
        verify(activityDao).create(argThat(new ReviewersStatusUpdatedActivityMatcher(PullRequestAction.REVIEWED)));
        verifyNoMoreInteractions(eventPublisher, activityDao, participantDao, watcherService);
    }

    @Test
    public void testSetStatusIfNotAReviewer() {
        when(participantDao.findByPullRequestAndUser(pullRequest.getGlobalId(), currentUser.getId()))
                .thenReturn(new InternalPullRequestParticipant.Builder(pullRequest)
                    .user(currentUser)
                    .role(PullRequestRole.PARTICIPANT)
                    .status(PullRequestParticipantStatus.UNAPPROVED)
                    .build());

        participantHelper.setStatus(pullRequest, PullRequestParticipantStatus.NEEDS_WORK);

        verify(participantDao).findByPullRequestAndUser(pullRequest.getGlobalId(), currentUser.getId());
        ParticipantMatcher reviewerMatcher = new ParticipantMatcher(currentUser, PULL_REQUEST_LATEST_COMMIT,
                PullRequestRole.REVIEWER, PullRequestParticipantStatus.NEEDS_WORK);
        verify(participantDao).update(argThat(reviewerMatcher));

        verify(eventPublisher).publish(argThat(new ReviewersUpdatedEventMatcher(singleton(currentUser), emptySet())));
        verify(eventPublisher).publish(argThat(new ParticipantStatusUpdatedEventMatcher(reviewerMatcher,
                PullRequestParticipantStatus.UNAPPROVED)));
        verify(eventPublisher).publish(argThat(new PullRequestActivityEventMatcher(pullRequest, PullRequestAction.REVIEWED)));
        verify(activityDao).create(argThat(new ReviewersUpdatedActivityMatcher(singleton(currentUser), emptySet())));
        verify(eventPublisher).publish(argThat(new ReviewersUpdatedActivityEventMatcher(singleton(currentUser), emptySet())));
        verify(activityDao).create(argThat(new ReviewersStatusUpdatedActivityMatcher(PullRequestAction.REVIEWED)));
        verifyNoMoreInteractions(eventPublisher, activityDao, participantDao);
        verifyZeroInteractions(watcherService);
    }

    @Test
    public void testSetStatusToSameStatus() {
        when(participantDao.findByPullRequestAndUser(pullRequest.getGlobalId(), currentUser.getId()))
                .thenReturn(new InternalPullRequestParticipant.Builder(pullRequest)
                        .user(currentUser)
                        .role(PullRequestRole.REVIEWER)
                        .status(PullRequestParticipantStatus.NEEDS_WORK)
                        .build());

        participantHelper.setStatus(pullRequest, PullRequestParticipantStatus.NEEDS_WORK);

        verify(participantDao).findByPullRequestAndUser(pullRequest.getGlobalId(), currentUser.getId());
        verifyNoMoreInteractions(participantDao);
        verifyZeroInteractions(eventPublisher, activityDao, watcherService);
    }

    @Test
    public void testSetStatusToApprovedUpdatesLatestReviewedCommit() {
        when(participantDao.findByPullRequestAndUser(pullRequest.getGlobalId(), currentUser.getId()))
                .thenReturn(new InternalPullRequestParticipant.Builder(pullRequest)
                        .user(currentUser)
                        .role(PullRequestRole.REVIEWER)
                        .status(PullRequestParticipantStatus.UNAPPROVED)
                        .build());

        participantHelper.setStatus(pullRequest, PullRequestParticipantStatus.APPROVED);

        verify(participantDao).findByPullRequestAndUser(pullRequest.getGlobalId(), currentUser.getId());
        ParticipantMatcher reviewerMatcher = new ParticipantMatcher(currentUser, PULL_REQUEST_LATEST_COMMIT,
                PullRequestRole.REVIEWER, PullRequestParticipantStatus.APPROVED);
        verify(participantDao).update(argThat(reviewerMatcher));
    }

    @Test
    public void testSetStatusToApprovedUpdatesLatestReviewedCommitForParticipant() {
        when(participantDao.findByPullRequestAndUser(pullRequest.getGlobalId(), currentUser.getId()))
                .thenReturn(new InternalPullRequestParticipant.Builder(pullRequest)
                        .user(currentUser)
                        .role(PullRequestRole.PARTICIPANT)
                        .status(PullRequestParticipantStatus.UNAPPROVED)
                        .build());

        participantHelper.setStatus(pullRequest, PullRequestParticipantStatus.APPROVED);

        verify(participantDao).findByPullRequestAndUser(pullRequest.getGlobalId(), currentUser.getId());
        ParticipantMatcher reviewerMatcher = new ParticipantMatcher(currentUser, PULL_REQUEST_LATEST_COMMIT,
                PullRequestRole.REVIEWER, PullRequestParticipantStatus.APPROVED);
        verify(participantDao).update(argThat(reviewerMatcher));
    }

    @Test
    public void testSetStatusToNeedsWorkUpdatesLatestReviewedCommit() {
        when(participantDao.findByPullRequestAndUser(pullRequest.getGlobalId(), currentUser.getId()))
                .thenReturn(new InternalPullRequestParticipant.Builder(pullRequest)
                        .user(currentUser)
                        .role(PullRequestRole.REVIEWER)
                        .status(PullRequestParticipantStatus.UNAPPROVED)
                        .build());

        participantHelper.setStatus(pullRequest, PullRequestParticipantStatus.NEEDS_WORK);

        verify(participantDao).findByPullRequestAndUser(pullRequest.getGlobalId(), currentUser.getId());
        ParticipantMatcher reviewerMatcher = new ParticipantMatcher(currentUser, PULL_REQUEST_LATEST_COMMIT,
                PullRequestRole.REVIEWER, PullRequestParticipantStatus.NEEDS_WORK);
        verify(participantDao).update(argThat(reviewerMatcher));
    }

    @Test
    public void testSetStatusToNeedsWorkUpdatesLatestReviewedCommitForParticipant() {
        when(participantDao.findByPullRequestAndUser(pullRequest.getGlobalId(), currentUser.getId()))
                .thenReturn(new InternalPullRequestParticipant.Builder(pullRequest)
                        .user(currentUser)
                        .role(PullRequestRole.PARTICIPANT)
                        .status(PullRequestParticipantStatus.UNAPPROVED)
                        .build());

        participantHelper.setStatus(pullRequest, PullRequestParticipantStatus.NEEDS_WORK);

        verify(participantDao).findByPullRequestAndUser(pullRequest.getGlobalId(), currentUser.getId());
        ParticipantMatcher reviewerMatcher = new ParticipantMatcher(currentUser, PULL_REQUEST_LATEST_COMMIT,
                PullRequestRole.REVIEWER, PullRequestParticipantStatus.NEEDS_WORK);
        verify(participantDao).update(argThat(reviewerMatcher));
    }

    @Test
    public void testSetStatusToUnapprovedDoesNotUpdateLatestReviewedCommit() {
        when(participantDao.findByPullRequestAndUser(pullRequest.getGlobalId(), currentUser.getId()))
                .thenReturn(new InternalPullRequestParticipant.Builder(pullRequest)
                        .user(currentUser)
                        .role(PullRequestRole.REVIEWER)
                        .status(PullRequestParticipantStatus.NEEDS_WORK)
                        .build());

        participantHelper.setStatus(pullRequest, PullRequestParticipantStatus.UNAPPROVED);

        verify(participantDao).findByPullRequestAndUser(pullRequest.getGlobalId(), currentUser.getId());
        ParticipantMatcher reviewerMatcher = new ParticipantMatcher(currentUser, PullRequestRole.REVIEWER,
                PullRequestParticipantStatus.UNAPPROVED);
        verify(participantDao).update(argThat(reviewerMatcher));
    }

    @Test
    public void testActivityForSwitchingStatusFromUnapprovedToNeedsWork() {
        InternalPullRequestParticipant participant = new InternalPullRequestParticipant.Builder(pullRequest)
                .status(PullRequestParticipantStatus.UNAPPROVED)
                .role(PullRequestRole.REVIEWER)
                .user(user)
                .build();

        when(participantDao.findByPullRequestAndUser(pullRequest.getGlobalId(), currentUser.getId())).thenReturn(participant);

        participantHelper.setStatus(pullRequest, PullRequestParticipantStatus.NEEDS_WORK);

        verify(activityDao).create(argThat(new ReviewersStatusUpdatedActivityMatcher(PullRequestAction.REVIEWED)));
        verifyNoMoreInteractions(activityDao);
        verify(eventPublisher, times(2)).publish(any(PullRequestEvent.class));
    }

    @Test
    public void testActivityForSwitchingStatusFromUnapprovedToApproved() {
        InternalPullRequestParticipant participant = new InternalPullRequestParticipant.Builder(pullRequest)
                .status(PullRequestParticipantStatus.UNAPPROVED)
                .role(PullRequestRole.REVIEWER)
                .user(user)
                .build();

        when(participantDao.findByPullRequestAndUser(pullRequest.getGlobalId(), currentUser.getId())).thenReturn(participant);

        participantHelper.setStatus(pullRequest, PullRequestParticipantStatus.APPROVED);

        verify(activityDao).create(argThat(new ReviewersStatusUpdatedActivityMatcher(PullRequestAction.APPROVED)));
        verifyNoMoreInteractions(activityDao);
        verify(eventPublisher, times(3)).publish(any(PullRequestEvent.class));

    }

    @Test
    public void testActivityForSwitchingStatusFromNeedsWorkToUnapproved() {
        InternalPullRequestParticipant participant = new InternalPullRequestParticipant.Builder(pullRequest)
                .status(PullRequestParticipantStatus.NEEDS_WORK)
                .role(PullRequestRole.REVIEWER)
                .user(user)
                .build();

        when(participantDao.findByPullRequestAndUser(pullRequest.getGlobalId(), currentUser.getId())).thenReturn(participant);

        participantHelper.setStatus(pullRequest, PullRequestParticipantStatus.UNAPPROVED);

        verifyZeroInteractions(activityDao);
        verify(eventPublisher, times(2)).publish(any(PullRequestEvent.class));
    }

    @Test
    public void testActivityForSwitchingStatusFromNeedsWorkToApproved() {
        InternalPullRequestParticipant participant = new InternalPullRequestParticipant.Builder(pullRequest)
                .status(PullRequestParticipantStatus.NEEDS_WORK)
                .role(PullRequestRole.REVIEWER)
                .user(user)
                .build();

        when(participantDao.findByPullRequestAndUser(pullRequest.getGlobalId(), currentUser.getId())).thenReturn(participant);

        participantHelper.setStatus(pullRequest, PullRequestParticipantStatus.APPROVED);

        verify(activityDao).create(argThat(new ReviewersStatusUpdatedActivityMatcher(PullRequestAction.APPROVED)));
        verifyNoMoreInteractions(activityDao);
        verify(eventPublisher, times(3)).publish(any(PullRequestEvent.class));
    }

    @Test
    public void testActivityForSwitchingStatusFromApprovedToUnapproved() {
        InternalPullRequestParticipant participant = new InternalPullRequestParticipant.Builder(pullRequest)
                .status(PullRequestParticipantStatus.APPROVED)
                .role(PullRequestRole.REVIEWER)
                .user(user)
                .build();

        when(participantDao.findByPullRequestAndUser(pullRequest.getGlobalId(), currentUser.getId())).thenReturn(participant);

        participantHelper.setStatus(pullRequest, PullRequestParticipantStatus.UNAPPROVED);

        verify(activityDao).create(argThat(new ReviewersStatusUpdatedActivityMatcher(PullRequestAction.UNAPPROVED)));
        verifyNoMoreInteractions(activityDao);
        verify(eventPublisher, times(3)).publish(any(PullRequestEvent.class));
    }

    @Test
    public void testActivityForSwitchingStatusFromApprovedToNeedsWork() {
        InternalPullRequestParticipant participant = new InternalPullRequestParticipant.Builder(pullRequest)
                .status(PullRequestParticipantStatus.APPROVED)
                .role(PullRequestRole.REVIEWER)
                .user(user)
                .build();

        when(participantDao.findByPullRequestAndUser(pullRequest.getGlobalId(), currentUser.getId())).thenReturn(participant);

        participantHelper.setStatus(pullRequest, PullRequestParticipantStatus.NEEDS_WORK);

        verify(activityDao).create(argThat(new ReviewersStatusUpdatedActivityMatcher(PullRequestAction.REVIEWED)));
        verifyNoMoreInteractions(activityDao);
        verify(eventPublisher, times(2)).publish(any(PullRequestEvent.class));
    }

    private Set<ApplicationUser> getExpectedParticipants() {
        ImmutableSet.Builder<ApplicationUser> builder = ImmutableSet.builder();

        InternalApplicationUser internalUser = mock(InternalApplicationUser.class);
        when(internalUser.getId()).thenReturn(20);
        when(internalUser.getName()).thenReturn("mark");
        when(internalUser.getDisplayName()).thenReturn("mark");
        when(internalUser.getEmailAddress()).thenReturn("Mark.Waugh@cricket.com");
        builder.add(internalUser);

        internalUser = mock(InternalApplicationUser.class);
        when(internalUser.getId()).thenReturn(22);
        when(internalUser.getName()).thenReturn("steve");
        when(internalUser.getDisplayName()).thenReturn("steve");
        when(internalUser.getEmailAddress()).thenReturn("Steve.Waugh@cricket.com");
        builder.add(internalUser);

        internalUser = mock(InternalApplicationUser.class);
        when(internalUser.getId()).thenReturn(24);
        when(internalUser.getName()).thenReturn("clark");
        when(internalUser.getDisplayName()).thenReturn("clark");
        when(internalUser.getEmailAddress()).thenReturn("Michael.Clark@cricket.com");
        builder.add(internalUser);

        internalUser = mock(InternalApplicationUser.class);
        when(internalUser.getId()).thenReturn(25);
        when(internalUser.getName()).thenReturn("hussey");
        when(internalUser.getDisplayName()).thenReturn("hussey");
        when(internalUser.getEmailAddress()).thenReturn(null);
        builder.add(internalUser);

        return builder.build();
    }

    private class ParticipantMatcher extends SimpleTypeSafeMatcher<InternalPullRequestParticipant> {

        private final ApplicationUser expectedUser;
        private final String lastReviewedCommit;
        private final PullRequestRole role;
        private final PullRequestParticipantStatus status;

        private ParticipantMatcher(PullRequestRole role, PullRequestParticipantStatus status) {
            this(user, null, role, status);
        }

        private ParticipantMatcher(ApplicationUser expectedUser, PullRequestRole role,
                                   PullRequestParticipantStatus status) {
            this(expectedUser, null, role, status);
        }

        private ParticipantMatcher(ApplicationUser expectedUser, String lastReviewedCommit,
                                   PullRequestRole role, PullRequestParticipantStatus status) {
            this.expectedUser = expectedUser;
            this.lastReviewedCommit = lastReviewedCommit;
            this.role = role;
            this.status = status;
        }

        @Override
        protected boolean matchesSafely(InternalPullRequestParticipant actual) {
            return expectedUser == actual.getUser() && lastReviewedCommit == actual.getLastReviewedCommit()
                    && pullRequest == actual.getPullRequest() && role == actual.getRole() && status == actual.getStatus();
        }
    }

    private class ParticipantStatusUpdatedEventMatcher extends SimpleTypeSafeMatcher<PullRequestParticipantStatusUpdatedEvent> {

        private final ParticipantMatcher participantMatcher;
        private final PullRequestParticipantStatus previousStatus;

        private ParticipantStatusUpdatedEventMatcher(ParticipantMatcher participantMatcher,
                                                     PullRequestParticipantStatus previousStatus) {
            this.participantMatcher = participantMatcher;
            this.previousStatus = previousStatus;
        }

        @Override
        protected boolean matchesSafely(PullRequestParticipantStatusUpdatedEvent actual) {
            return pullRequest == actual.getPullRequest() &&
                    participantMatcher.matchesSafely((InternalPullRequestParticipant) actual.getParticipant()) &&
                    previousStatus == actual.getPreviousStatus();
        }
    }

    private class PullRequestActivityEventMatcher extends SimpleTypeSafeMatcher<PullRequestActivityEvent> {

        private final PullRequest pr;
        private final PullRequestAction action;

        private PullRequestActivityEventMatcher(PullRequest pr, PullRequestAction action) {
            this.pr = pr;
            this.action = action;
        }

        @Override
        protected boolean matchesSafely(PullRequestActivityEvent actual) {
            return pr == actual.getPullRequest() && action == actual.getActivity().getAction();
        }
    }

    private class ParticipantsUpdatedEventMatcher extends SimpleTypeSafeMatcher<PullRequestParticipantsUpdatedEvent> {

        private final PullRequest pullRequest;
        private final Set<ApplicationUser> added;
        private final Set<ApplicationUser> removed;

        private ParticipantsUpdatedEventMatcher(Set<ApplicationUser> added, Set<ApplicationUser> removed) {
            this(InternalPullRequestParticipantHelperTest.this.pullRequest, added, removed);
        }

        private ParticipantsUpdatedEventMatcher(PullRequest pullRequest, Set<ApplicationUser> added, Set<ApplicationUser> removed) {
            this.pullRequest = pullRequest;
            this.added = added;
            this.removed = removed;
        }

        @Override
        protected boolean matchesSafely(PullRequestParticipantsUpdatedEvent actual) {
            return pullRequest == actual.getPullRequest() && actual.getAddedParticipants().equals(added) &&
                    actual.getRemovedParticipants().equals(removed);
        }
    }

    private class ReviewersStatusUpdatedActivityMatcher extends SimpleTypeSafeMatcher<InternalPullRequestActivity> {

        private final PullRequestAction action;

        private ReviewersStatusUpdatedActivityMatcher(PullRequestAction action) {
            this.action = action;
        }

        @Override
        protected boolean matchesSafely(InternalPullRequestActivity actual) {
            return pullRequest == actual.getPullRequest() && currentUser == actual.getUser() &&
                    action == actual.getAction();
        }
    }

    private class ReviewersUpdatedActivityEventMatcher extends SimpleTypeSafeMatcher<PullRequestReviewersUpdatedActivityEvent> {

        private final Set<ApplicationUser> added;
        private final Set<ApplicationUser> removed;

        private ReviewersUpdatedActivityEventMatcher(Set<ApplicationUser> added, Set<ApplicationUser> removed) {
            this.added = added;
            this.removed = removed;
        }

        @Override
        protected boolean matchesSafely(PullRequestReviewersUpdatedActivityEvent event) {
            return  pullRequest.equals(event.getPullRequest())
                    && added.equals(event.getActivity().getAddedReviewers())
                    && removed.equals(event.getActivity().getRemovedReviewers());
        }
    }
    
    private class ReviewersUpdatedActivityMatcher extends SimpleTypeSafeMatcher<InternalPullRequestReviewersUpdatedActivity> {

        private final Set<ApplicationUser> added;
        private final Set<ApplicationUser> removed;

        private ReviewersUpdatedActivityMatcher(Set<ApplicationUser> added, Set<ApplicationUser> removed) {
            this.added = added;
            this.removed = removed;
        }

        @Override
        protected boolean matchesSafely(InternalPullRequestReviewersUpdatedActivity actual) {
            return pullRequest == actual.getPullRequest() && actual.getAddedReviewers().equals(added) &&
                    actual.getRemovedReviewers().equals(removed) && PullRequestAction.UPDATED == actual.getAction() &&
                    currentUser == actual.getUser();
        }
    }

    private class ReviewersUpdatedEventMatcher extends SimpleTypeSafeMatcher<PullRequestReviewersUpdatedEvent> {

        private final Set<ApplicationUser> added;
        private final Set<ApplicationUser> removed;

        private ReviewersUpdatedEventMatcher(Set<ApplicationUser> added, Set<ApplicationUser> removed) {
            this.added = added;
            this.removed = removed;
        }

        @Override
        protected boolean matchesSafely(PullRequestReviewersUpdatedEvent actual) {
            return pullRequest == actual.getPullRequest() && actual.getAddedReviewers().equals(added) &&
                    actual.getRemovedReviewers().equals(removed);
        }
    }

    private class SearchCriteriaMatcher extends SimpleTypeSafeMatcher<PullRequestParticipantSearchCriteria> {

        private final InternalRepository sourceRepository;
        private final InternalRepository targetRepository;
        private final PullRequestRole role;

        private SearchCriteriaMatcher(InternalRepository sourceRepository, InternalRepository targetRepository,
                                      PullRequestRole role) {
            this.sourceRepository = sourceRepository;
            this.targetRepository = targetRepository;
            this.role = role;
        }

        @Override
        protected boolean matchesSafely(PullRequestParticipantSearchCriteria actual) {
            return sourceRepository == actual.getFromRepository().get() &&
                    targetRepository == actual.getToRepository().get() &&
                    role == actual.getRole().get();
        }
    }
}
