package com.atlassian.stash.internal.pull;

import com.atlassian.bitbucket.annotation.Dao;
import com.atlassian.bitbucket.annotation.DataSet;
import com.atlassian.bitbucket.comment.CommentAction;
import com.atlassian.bitbucket.pull.*;
import com.atlassian.bitbucket.util.Page;
import com.atlassian.bitbucket.util.PageRequestImpl;
import com.atlassian.bitbucket.util.PageUtils;
import com.atlassian.stash.internal.DataSets;
import com.atlassian.stash.internal.HibernateUtils;
import com.atlassian.stash.internal.activity.AbstractHibernateActivityDaoTest;
import com.atlassian.stash.internal.activity.ActivityType;
import com.atlassian.stash.internal.activity.InternalActivity;
import com.atlassian.stash.internal.comment.CommentDao;
import com.atlassian.stash.internal.comment.HibernateCommentDao;
import com.atlassian.stash.internal.entity.InternalAbstractEntity;
import com.atlassian.stash.internal.user.HibernateStashUserDao;
import com.atlassian.stash.internal.user.InternalApplicationUser;
import com.atlassian.stash.internal.user.StashUserDao;
import com.google.common.base.Predicates;
import com.google.common.collect.*;
import org.hamcrest.Matchers;
import org.junit.Test;
import org.mockito.Mock;
import org.springframework.beans.factory.annotation.Value;

import java.time.LocalDateTime;
import java.time.ZoneOffset;
import java.util.*;

import static java.util.Collections.emptySet;
import static java.util.Collections.singleton;
import static org.hamcrest.Matchers.either;
import static org.hamcrest.Matchers.everyItem;

@DataSet({DataSets.USERS, DataSets.PROJECTS, DataSets.REPOSITORIES, DataSets.COMMENTS,
        DataSets.PULL_REQUESTS, DataSets.PULL_REQUEST_COMMENTS, DataSets.PULL_REQUEST_ACTIVITIES})
@SuppressWarnings("ConstantConditions")
public class HibernatePullRequestActivityDaoTest extends AbstractHibernateActivityDaoTest {

    @Dao(HibernateCommentDao.class)
    private CommentDao commentDao;
    @Dao
    private HibernatePullRequestActivityDao dao;
    @Dao(HibernatePullRequestDao.class)
    private PullRequestDao pullRequestDao;
    @Dao(HibernateStashUserDao.class)
    private StashUserDao userDao;
    @Mock
    private PullRequestActivityVisitor visitor;

    @Value("${page.scan.pullrequest.activity.size}")
    int scanPageSize;
    @Value("${page.scan.pullrequest.activity.count}")
    int maxScanPageCount;

    @Test
    public void testCountCommentsByPullRequest() {
        Map<Long, Long> counts = dao.countCommentsByPullRequest(ImmutableSet.of(1L, 2L, 10L));
        assertNotNull(counts);
        assertEquals(Long.valueOf(4L), counts.get(1L)); //5 comment activities, but one is an edit
        assertNull(counts.get(2L));  //No comments
        assertNull(counts.get(10L)); //No such pull request (ergo, no comments)
    }

    @Test
    public void testCreate() {
        InternalPullRequest pullRequest = pullRequestDao.getById(1L);
        InternalApplicationUser user = userDao.getById(3);

        InternalPullRequestActivity activity = new InternalPullRequestActivity.Builder(pullRequest)
                .action(PullRequestAction.REOPENED)
                .createdDate(new Date())
                .user(user)
                .build();
        InternalPullRequestActivity created = dao.create(activity);
        assertNotNull(created.getId());
        assertEquals(created.getId(), activity.getId());
    }

    @Test
    public void testCreateDoesNotChangePullRequestVersion() {
        InternalPullRequest pullRequest = pullRequestDao.getById(1L);
        int prVersion = pullRequest.getVersion();
        InternalApplicationUser user = userDao.getById(3);

        InternalPullRequestActivity activity = new InternalPullRequestActivity.Builder(pullRequest)
                .action(PullRequestAction.REOPENED)
                .createdDate(new Date())
                .user(user)
                .build();
        dao.create(activity);

        flush();
        evict(pullRequest, pullRequest.getGlobalId());

        assertEquals(prVersion, pullRequestDao.getById(1L).getVersion());
    }

    @Test
    public void testDelete() {
        InternalPullRequestActivity activity = dao.getById(3L);
        assertNotNull(activity);
        dao.delete(activity);

        assertNull(dao.getById(3L));
    }

    @Test
    @DataSet({DataSets.USERS, DataSets.PROJECTS, DataSets.REPOSITORIES, DataSets.COMMENTS,
            DataSets.PULL_REQUESTS, DataSets.PULL_REQUEST_EXTRA_COMMENTS, DataSets.PULL_REQUEST_EXTRA_ACTIVITIES,
            DataSets.COMMIT_DISCUSSIONS, DataSets.COMMIT_DISCUSSION_EXTRA_COMMENTS,
            DataSets.COMMIT_DISCUSSION_EXTRA_ACTIVITIES})
    public void testDeleteByCommentAlsoAnchoredAtCommitDiscussion() {
        assertEquals(2, dao.deleteByComment(11L));

        flush();

        //Activities 201 (PR) and 100 (COMMIT DISCUSSION) were on comment 11
        assertNull(getCurrentSession().get(InternalActivity.class, 201L));
        assertNull(getCurrentSession().get(InternalActivity.class, 100L));
    }

    @Test
    public void testDeleteByCommentForAdd() {
        dao.deleteByComment(1L);

        flush();

        //Activities 1 and 8 were on comment 1
        assertNull(getCurrentSession().get(InternalActivity.class, 2L));
        assertNull(getCurrentSession().get(InternalActivity.class, 8L));
    }

    @Test
    public void testDeleteByCommentForReply() {
        dao.deleteByComment(3L); //Comment 3 is a reply (in terms of activities)

        flush();

        //Activity 9 was on comment 3
        assertNull(getCurrentSession().get(InternalActivity.class, 9L));
    }

    @Test
    public void testDeleteByPullRequest() {
        assertEquals(11, dao.deleteByPullRequest(1L));
        assertEquals(0, dao.deleteByPullRequest(2L));    //Pull request exists, but has no activities
        assertEquals(0, dao.deleteByPullRequest(1000L)); //No such pull request

        flush();

        Page<InternalPullRequestActivity> page = dao.findAll(PageUtils.newRequest(0, 5));
        assertNotNull(page);
        assertEquals(0, page.getSize());
        assertTrue(page.getIsLastPage());
    }

    @Test
    @DataSet({DataSets.USERS, DataSets.PROJECTS, DataSets.REPOSITORIES, DataSets.COMMENTS,
            DataSets.PULL_REQUESTS, DataSets.PULL_REQUEST_EXTRA_COMMENTS, DataSets.PULL_REQUEST_EXTRA_ACTIVITIES,
            DataSets.COMMIT_DISCUSSIONS, DataSets.COMMIT_DISCUSSION_EXTRA_COMMENTS,
            DataSets.COMMIT_DISCUSSION_EXTRA_ACTIVITIES})
    public void testDeleteByPullRequestWithCommitLevelComment() {
        assertEquals(4, dao.deleteByPullRequest(1L));

        flush();

        //Activities 201,202 (PR) and 100,101 (COMMIT DISCUSSION) were on pull request 1
        assertNull(getCurrentSession().get(InternalActivity.class, 201L));
        assertNull(getCurrentSession().get(InternalActivity.class, 202L));
        assertNull(getCurrentSession().get(InternalActivity.class, 100L));
        assertNull(getCurrentSession().get(InternalActivity.class, 101L));
    }

    @Test
    public void testDeleteByRepository() {
        assertEquals(11, dao.deleteByRepository(1));
        assertEquals(0, dao.deleteByRepository(2));    //Pull request exists, but has no activities
        assertEquals(0, dao.deleteByRepository(1000)); //No such repository

        flush();

        Page<InternalPullRequestActivity> page = dao.findAll(PageUtils.newRequest(0, 5));
        assertNotNull(page);
        assertEquals(0, page.getSize());
        assertTrue(page.getIsLastPage());
    }

    @Test
    @DataSet({DataSets.USERS, DataSets.PROJECTS, DataSets.REPOSITORIES, DataSets.COMMENTS,
            DataSets.PULL_REQUESTS, DataSets.PULL_REQUEST_EXTRA_COMMENTS, DataSets.PULL_REQUEST_EXTRA_ACTIVITIES,
            DataSets.COMMIT_DISCUSSIONS, DataSets.COMMIT_DISCUSSION_EXTRA_COMMENTS,
            DataSets.COMMIT_DISCUSSION_EXTRA_ACTIVITIES})
    public void testDeleteByRepositoryWithPullRequestWithCommitLevelComment() {
        assertEquals(4, dao.deleteByRepository(1));

        flush();

        //Activities 201, 202 (PR) and 100, 101 (COMMIT DISCUSSION) were on pull request 1 in repository 1
        assertNull(getCurrentSession().get(InternalActivity.class, 201L));
        assertNull(getCurrentSession().get(InternalActivity.class, 202L));
        assertNull(getCurrentSession().get(InternalActivity.class, 100L));
        assertNull(getCurrentSession().get(InternalActivity.class, 101L));
    }

    @DataSet({DataSets.USERS, DataSets.PROJECTS, DataSets.REPOSITORIES, DataSets.COMMENTS,
            DataSets.PULL_REQUESTS, DataSets.PULL_REQUEST_RESCOPE_ACTIVITIES})
    @Test
    public void testDeleteEmptyRescopes() {
        assertEquals(5, dao.deleteEmptyRescopes()); //2, 4, 6, 8, 10
    }

    @Test
    public void testFindAll() {
        Page<InternalPullRequestActivity> page = dao.findAll(new PageRequestImpl(0, 6));
        assertNotNull(page);
        assertFalse(page.getIsLastPage());
        assertEquals(6, page.getSize());

        List<InternalPullRequestActivity> activities = ImmutableList.copyOf(page.getValues());
        assertActivity(activities.get(0), 11, ActivityType.PULL_REQUEST_REVIEWERS_UPDATED_ACTIVITY_TYPE);
        assertActivity(activities.get(1), 10);
        assertActivity(activities.get(2), 9, ActivityType.PULL_REQUEST_COMMENT_ACTIVITY_TYPE);
        assertActivity(activities.get(3), 8, ActivityType.PULL_REQUEST_COMMENT_ACTIVITY_TYPE);
        assertActivity(activities.get(4), 7, ActivityType.PULL_REQUEST_RESCOPE_ACTIVITY_TYPE);
        assertActivity(activities.get(5), 6);

        page = dao.findAll(page.getNextPageRequest());
        assertNotNull(page);
        assertTrue(page.getIsLastPage());
        assertEquals(5, page.getSize());

        activities = ImmutableList.copyOf(page.getValues());
        assertActivity(activities.get(0), 5, ActivityType.PULL_REQUEST_COMMENT_ACTIVITY_TYPE);
        assertActivity(activities.get(1), 4);
        assertActivity(activities.get(2), 3, ActivityType.PULL_REQUEST_COMMENT_ACTIVITY_TYPE);
        assertActivity(activities.get(3), 2, ActivityType.PULL_REQUEST_COMMENT_ACTIVITY_TYPE);
        assertActivity(activities.get(4), 1);
    }

    /**
     * Verifies that certain classes of activities are omitted when retrieving a pull request's activities:
     * <ul>
     *     <li>{@link PullRequestAction#RESCOPED Rescope} activities which are of simple
     *     {@link InternalPullRequestActivity} type, omitted because they lack the value-
     *     adding data present on the {@link InternalPullRequestRescopeActivity}</li>
     *     <li>{@link PullRequestAction#UPDATED Update} activities which are of simple
     *     {@link InternalPullRequestActivity} type, omitted because they lack any data
     *     about what was updated and provide little value in the UI without it</li>
     *     <li>{@link InternalPullRequestCommentActivity Comment activities} for actions
     *     other than {@link CommentAction#ADDED add}</li>
     * </ul>
     * Ideally, these should be being restricted by something other than the DAO to allow, for example, REST clients
     * to filter down the activity stream. For the moment, though, this subtracts a lot of noise the UI would filter
     * out for itself otherwise.
     */
    @Test
    public void testFindActivitiesForPullRequest() {
        Page<InternalPullRequestActivity> page = dao.findByPullRequest(1L, new PageRequestImpl(0, 4));
        assertNotNull(page);
        assertFalse(page.getIsLastPage());
        assertEquals(4, page.getSize());

        List<InternalPullRequestActivity> activities = ImmutableList.copyOf(page.getValues());
        assertActivity(activities.get(0), 11, ActivityType.PULL_REQUEST_REVIEWERS_UPDATED_ACTIVITY_TYPE);
        assertActivity(activities.get(1), 7, ActivityType.PULL_REQUEST_RESCOPE_ACTIVITY_TYPE);
        assertActivity(activities.get(2), 5, ActivityType.PULL_REQUEST_COMMENT_ACTIVITY_TYPE);
        assertActivity(activities.get(3), 4);

        page = dao.findByPullRequest(1L, page.getNextPageRequest());
        assertNotNull(page);
        assertTrue(page.getIsLastPage());
        assertEquals(2, page.getSize());

        activities = ImmutableList.copyOf(page.getValues());
        assertActivity(activities.get(0), 2, ActivityType.PULL_REQUEST_COMMENT_ACTIVITY_TYPE);
        assertActivity(activities.get(1), 1);
    }

    @Test
    public void testFindByComment() {
        InternalPullRequestCommentActivity activity = dao.findByComment(1L, 1L);
        assertActivity(activity, 2, ActivityType.PULL_REQUEST_COMMENT_ACTIVITY_TYPE);

        //Comment 1 is not the specified pull request (99)
        assertNull(dao.findByComment(99L, 1L));

        //Comment 2 is a reply so it won't match any comment activity with CommentAction.ADDED
        assertNull(dao.findByComment(1L, 2L));
    }

    /**
     * Verifies that only rescope activities from {@link PullRequestState#OPEN open} pull
     * requests for which the added commits count is {@code null} are returned as calculable.
     * <ul>
     *     <li>Pull requests 1, 4 and 10 have rescope activities in the data</li>
     *     <li>Pull request 4 is marked declined</li>
     *     <li>Pull request 10 is marked merged</li>
     * </ul>
     * Pull request 1 has 3 rescope activities:
     * <ul>
     *     <li>Activity 1 has 2 added commits and 1 removed commit</li>
     *     <li>Activity 2 has no commits added or removed</li>
     *     <li>Activity 3 has both counts as null</li>
     * </ul>
     * Activity 3 should be the only one returned. The counts are null for all the rescope activities on pull requests
     * 4 and 10, to ensure only the state check prevents them from being returned.
     */
    @DataSet({DataSets.USERS, DataSets.PROJECTS, DataSets.REPOSITORIES, DataSets.COMMENTS,
            DataSets.PULL_REQUESTS, DataSets.PULL_REQUEST_RESCOPE_ACTIVITIES})
    @Test
    public void testFindCalculableRescopes() {
        List<Long> ids = dao.findCalculableRescopes();
        assertNotNull(ids);
        assertEquals(1, ids.size());
        assertEquals(3L, ids.get(0).longValue());
    }

    @Test
    public void testFindLatestCreatedSince() {
        LocalDateTime updatedSince = LocalDateTime.of(2012, 6, 30, 8, 9, 10, 11);
        Date createdSince = Date.from(updatedSince.toInstant(ZoneOffset.UTC));

        Page<InternalPullRequestActivity> activities = dao.findLatestCreatedSince(createdSince,
                PullRequestActivityType.COMMENT, PageUtils.newRequest(0, 25));

        List<InternalPullRequestActivity> results = ImmutableList.copyOf(activities.getValues());
        assertEquals(1, results.size());
    }

    @Test
    public void testFindPageStartingAtStartsFromBeginningIfTooFewPrecedingActivities() {
        //Tests that if less than 1/2 of a page of elements precedes the activity then
        //the returned page starts from the very beginning (to avoid paging backwards for
        //just a few elements)
        PullRequestActivityPage<InternalPullRequestActivity> page = dao.findPageStartingAt(1L, 5, PageUtils.newRequest(0, 4));
        assertNotNull(page);
        assertTrue(page.getIsFirstPage());
        assertFalse(page.getIsLastPage());
        assertEquals(4, page.getSize());
        assertEquals(11L, Iterables.get(page.getValues(), 0).getId());
        assertEquals(7L, Iterables.get(page.getValues(), 1).getId());
        assertEquals(5L, Iterables.get(page.getValues(), 2).getId());
    }

    @Test
    public void testFindPageStartingAtWhenActivitiesSplitAcrossPages() {
        PullRequestActivityPage<InternalPullRequestActivity> page = dao.findPageStartingAt(1L, 11, PageUtils.newRequest(0, 3));
        assertNotNull(page);
        assertTrue(page.getIsFirstPage());
        assertFalse(page.getIsLastPage());
        assertEquals(3, page.getSize());
        assertEquals(11, Iterables.get(page.getValues(), 0).getId());
        assertEquals(3, page.getNextPageRequest().getStart());

        page = dao.findPageStartingAt(1L, 4, PageUtils.newRequest(0, 2));
        assertNotNull(page);
        assertFalse(page.getIsFirstPage());
        assertFalse(page.getIsLastPage());
        assertEquals(2, page.getSize());
        assertEquals(7, page.getPreviousPageStartId());
        assertEquals(5, page.getNextPageRequest().getStart());
        assertEquals(4, Iterables.get(page.getValues(), 0).getId());

        page = dao.findPageStartingAt(1L, 1, PageUtils.newRequest(0, 2));
        assertNotNull(page);
        assertFalse(page.getIsFirstPage());
        assertTrue(page.getIsLastPage());
        assertEquals(1, page.getSize());
        assertEquals(4, page.getPreviousPageStartId());
        assertEquals(1, Iterables.get(page.getValues(), 0).getId());
    }

    @Test
    public void testFindPageStartingAtWhenAllActivitiesInOnePage() {
        PullRequestActivityPage<InternalPullRequestActivity> page = dao.findPageStartingAt(1L, 7, PageUtils.newRequest(0, 6));
        assertNotNull(page);
        assertTrue(page.getIsFirstPage());
        assertTrue(page.getIsLastPage());
        assertEquals(6, page.getSize());
        assertTrue(Iterables.any(page.getValues(), activity -> activity.getId() == 7));
    }

    @Test
    public void testFindPageStartingAtWhenTargetIsFirstItem() {
        PullRequestActivityPage<InternalPullRequestActivity> page = dao.findPageStartingAt(1L, 7L, PageUtils.newRequest(0, 3));
        assertNotNull(page);
        assertTrue(page.getIsFirstPage());
        assertFalse(page.getIsLastPage());
        assertPage(page, ImmutableList.of(11L, 7L, 5L));
    }

    @Test
    public void testFindPageStartingAtWhenTargetIsLastItem() {
        PullRequestActivityPage<InternalPullRequestActivity> page = dao.findPageStartingAt(1L, 4L, PageUtils.newRequest(0, 4));
        assertNotNull(page);
        assertTrue(page.getIsFirstPage());
        assertFalse(page.getIsLastPage());
        assertPage(page, ImmutableList.of(11L, 7L, 5L, 4L));
    }

    @Test
    public void testFindPageStartingAtWithOneElementLimit() {
        PullRequestActivityPage<InternalPullRequestActivity> page = dao.findPageStartingAt(1L, 4, PageUtils.newRequest(0, 1));
        assertNotNull(page);
        assertFalse(page.getIsFirstPage());
        assertFalse(page.getIsLastPage());
        assertEquals(1, page.getSize());
        assertTrue(Iterables.any(page.getValues(), activity -> activity.getId() == 4));
    }

    @Test
    public void testFindPageStartingAtWithNonexistentActivity() {
        PullRequestActivityPage<InternalPullRequestActivity> page = dao.findPageStartingAt(1L, Integer.MAX_VALUE, PageUtils.newRequest(0, 5));
        assertNotNull(page);
        assertEquals(0, page.getSize());
        assertTrue(page.getIsLastPage());
        assertTrue(page.getIsFirstPage());
    }

    @Test
    public void testFindUsersWithActivities() {
        Set<InternalApplicationUser> users = dao.findUsersWithActivities(1L, Collections.<PullRequestAction>emptySet(), Collections.<PullRequestAction>emptySet());
        assertEquals(2, users.size());
    }

    @Test
    public void testFindUsersIncludingSelectActivities() {
        Set<InternalApplicationUser> users = dao.findUsersWithActivities(1L, Collections.singleton(PullRequestAction.OPENED), Collections.<PullRequestAction>emptySet());
        assertEquals(1, users.size());
        assertEquals(5, users.iterator().next().getId());
    }

    @Test
    public void testFindUsersExcludedSomeActivities() {
        Set<InternalApplicationUser> users = dao.findUsersWithActivities(1L, Collections.<PullRequestAction>emptySet(),
                Sets.newHashSet(PullRequestAction.COMMENTED, PullRequestAction.OPENED, PullRequestAction.UPDATED));
        assertEquals(2, users.size());
        assertEquals(3, users.iterator().next().getId());
    }

    @Test
    public void testFindUsersExcludedSomeIncludedActivities() {
        Set<InternalApplicationUser> users = dao.findUsersWithActivities(
                1L, Sets.newHashSet(PullRequestAction.DECLINED, PullRequestAction.COMMENTED),
                Sets.newHashSet(PullRequestAction.COMMENTED, PullRequestAction.OPENED));
        assertEquals(1, users.size());
        assertEquals(3, users.iterator().next().getId());
    }

    @Test
    public void testSearch() throws Exception {
        //Search for comment activities
        Page<InternalPullRequestActivity> page = dao.search(new PullRequestActivitySearchCriteria.Builder()
                .pullRequestId(1L)
                .types(ImmutableSet.of(ActivityType.PULL_REQUEST_COMMENT_ACTIVITY_TYPE))
                .build(), new PageRequestImpl(0, 10));
        assertEquals(5, page.getSize());
        assertTrue(page.getIsLastPage());
        assertThat(page.getValues(), everyItem(Matchers.<InternalPullRequestActivity>instanceOf(InternalPullRequestCommentActivity.class)));
        InternalPullRequestCommentActivity activity = (InternalPullRequestCommentActivity) Iterables.get(page.getValues(), 0);
        assertEquals(1L, activity.getPullRequest().getId());
        assertNotNull(activity.getComment());
        assertNotNull(activity.getCommentAction());

        //Search for comment activities with restrictions on the comment ids
        page = dao.search(new PullRequestActivitySearchCriteria.Builder()
                .pullRequestId(1L)
                .types(ImmutableSet.of(ActivityType.PULL_REQUEST_COMMENT_ACTIVITY_TYPE))
                .commentIds(ImmutableSet.of(9L))
                .build(), new PageRequestImpl(0, 10));
        assertEquals(1, page.getSize());
        assertTrue(page.getIsLastPage());
        assertThat(page.getValues(), everyItem(Matchers.<InternalPullRequestActivity>instanceOf(InternalPullRequestCommentActivity.class)));
        activity = (InternalPullRequestCommentActivity) Iterables.getOnlyElement(page.getValues());
        assertEquals(1L, activity.getPullRequest().getId());
        assertEquals(9L, activity.getComment().getId());
        assertEquals(4L, activity.getCommentAnchor().getId());

        //Search for comment activities with restrictions on the comment action and ids
        page = dao.search(new PullRequestActivitySearchCriteria.Builder()
                .pullRequestId(1L)
                .types(ImmutableSet.of(ActivityType.PULL_REQUEST_COMMENT_ACTIVITY_TYPE))
                .commentIds(ImmutableSet.of(9L))
                .commentActions(EnumSet.of(CommentAction.ADDED))
                .build(), new PageRequestImpl(0, 10));
        assertEquals(1, page.getSize());
        assertTrue(page.getIsLastPage());
        assertThat(page.getValues(), everyItem(Matchers.<InternalPullRequestActivity>instanceOf(InternalPullRequestCommentActivity.class)));
        activity = (InternalPullRequestCommentActivity) Iterables.getOnlyElement(page.getValues());
        assertEquals(1L, activity.getPullRequest().getId());
        assertEquals(9L, activity.getComment().getId());
        assertEquals(4L, activity.getCommentAnchor().getId());

        //Search for rescope activities
        page = dao.search(new PullRequestActivitySearchCriteria.Builder()
                .pullRequestId(1L)
                .types(ImmutableSet.of(ActivityType.PULL_REQUEST_RESCOPE_ACTIVITY_TYPE))
                .build(), new PageRequestImpl(0, 10));
        assertEquals(1, page.getSize());
        assertTrue(page.getIsLastPage());
        assertThat(page.getValues(), everyItem(Matchers.<InternalPullRequestActivity>instanceOf(InternalPullRequestRescopeActivity.class)));
        InternalPullRequestRescopeActivity rescopeActivity = (InternalPullRequestRescopeActivity) Iterables.getOnlyElement(page.getValues());
        assertEquals(1L, rescopeActivity.getPullRequest().getId());
        assertEquals("fromfromfromfromfromfromfromfromfromfrom", rescopeActivity.getFromHash());
        assertEquals("prevfromprevfromprevfromprevfromprevfrom", rescopeActivity.getPreviousFromHash());

        //Search for rescope plus comment activities affecting comment 9
        page = dao.search(new PullRequestActivitySearchCriteria.Builder()
                .pullRequestId(1L)
                .types(ImmutableSet.of(ActivityType.PULL_REQUEST_RESCOPE_ACTIVITY_TYPE, ActivityType.PULL_REQUEST_COMMENT_ACTIVITY_TYPE))
                .commentIds(ImmutableSet.of(9L))
                .build(), new PageRequestImpl(0, 10));
        assertEquals(2, page.getSize());
        assertTrue(page.getIsLastPage());
        assertThat(page.getValues(),
                everyItem(either(Matchers.<InternalPullRequestActivity>instanceOf(InternalPullRequestRescopeActivity.class)).
                          or(Matchers.<InternalPullRequestActivity>instanceOf(InternalPullRequestCommentActivity.class))));
        rescopeActivity = (InternalPullRequestRescopeActivity) Iterables.getOnlyElement(Iterables.filter(page.getValues(), Predicates.instanceOf(InternalPullRequestRescopeActivity.class)));
        assertEquals(1L, rescopeActivity.getPullRequest().getId());
        assertEquals("fromfromfromfromfromfromfromfromfromfrom", rescopeActivity.getFromHash());
        assertEquals("prevfromprevfromprevfromprevfromprevfrom", rescopeActivity.getPreviousFromHash());
        activity = (InternalPullRequestCommentActivity) Iterables.getOnlyElement(Iterables.filter(page.getValues(), Predicates.instanceOf(InternalPullRequestCommentActivity.class)));
        assertEquals(1L, activity.getPullRequest().getId());
        assertEquals(9L, activity.getComment().getId());
        assertEquals(4L, activity.getCommentAnchor().getId());
    }

    @Test
    public void testUpdate() {
        InternalPullRequestRescopeActivity initial = HibernateUtils.cast(dao.getById(7L), InternalPullRequestRescopeActivity.class);
        assertFalse("Commits should not have been calculated yet", initial.isPrecalculated());

        InternalPullRequestRescopeActivity modified = new InternalPullRequestRescopeActivity.Builder(initial)
                .commit(new InternalPullRequestRescopeCommit.Builder().action(PullRequestRescopeCommitAction.ADDED)
                        .commitId("6fe655e0de3f53eeebbf9c52e159d2a7f8bbbc8a").build())
                .commit(new InternalPullRequestRescopeCommit.Builder().action(PullRequestRescopeCommitAction.REMOVED)
                        .commitId("02bab5c4a85a18ddf283d89584750b05cc57c502").build())
                .totalAdded(1)
                .totalRemoved(1)
                .build();
        dao.update(modified);

        flush();
        clear(); //Need to evict all the cached commits, too, if any

        InternalPullRequestRescopeActivity updated = HibernateUtils.cast(dao.getById(7L), InternalPullRequestRescopeActivity.class);
        assertTrue("Commits should now be calculated", updated.isPrecalculated());
        assertEquals(1, updated.getTotalAdded());
        assertEquals(1, updated.getTotalRemoved());

        Set<InternalPullRequestRescopeCommit> commits = updated.getCommits();
        assertNotNull(commits);
        assertEquals(2, commits.size());
    }

    @Test
    public void testCountActivities() {
        assertEquals(5, dao.countByUser(1, 5, emptySet(), emptySet()));
    }

    @Test
    public void testCountActivitiesFilterByInclude() {
        assertEquals(2, dao.countByUser(1, 5, singleton(PullRequestAction.COMMENTED), emptySet()));
    }

    @Test
    public void testCountActivitiesFilterByExclude() {
        assertEquals(4, dao.countByUser(1, 5, emptySet(), singleton(PullRequestAction.OPENED)));
    }

    @Test
    public void testCountActivitiesFilterByIncludeAndExclude() {
        assertEquals(2, dao.countByUser(1, 5, singleton(PullRequestAction.COMMENTED),
                singleton(PullRequestAction.OPENED)));
    }

    @Test
    public void testCountActivitiesFilterByIncludeAndExcludeSame() {
        assertEquals(1, dao.countByUser(1, 5, Sets.newHashSet(PullRequestAction.UPDATED, PullRequestAction.COMMENTED),
                Sets.newHashSet(PullRequestAction.COMMENTED, PullRequestAction.OPENED)));
    }

    @Test
    public void testCountActivitiesCancellingSets() {
        assertEquals(0, dao.countByUser(1, 5, singleton(PullRequestAction.OPENED), singleton(PullRequestAction.OPENED)));
    }

    @Test
    public void testCountActivitiesEmpty() {
        assertEquals(0, dao.countByUser(123, 456, emptySet(), emptySet()));
    }

    @Test
    public void testCreateReviewersUpdated() {
        InternalPullRequest pullRequest = pullRequestDao.getById(1L);
        InternalApplicationUser user = userDao.getById(3);
        InternalApplicationUser reviewerAdded = userDao.getById(4);
        InternalApplicationUser reviewerRemoved = userDao.getById(5);

        InternalPullRequestReviewersUpdatedActivity activity = new InternalPullRequestReviewersUpdatedActivity.Builder(pullRequest)
                .addedReviewers(singleton(reviewerAdded))
                .removedReviewers(singleton(reviewerRemoved))
                .user(user)
                .build();
        InternalPullRequestReviewersUpdatedActivity created = (InternalPullRequestReviewersUpdatedActivity) dao.create(activity);

        assertNotNull(created.getId());
        assertEquals(created.getId(), activity.getId());
        assertEquals(user, activity.getUser());
        assertEquals(pullRequest, activity.getPullRequest());
        assertEquals(reviewerAdded, Iterables.get(activity.getAddedReviewers(), 0));
        assertEquals(reviewerRemoved, Iterables.get(activity.getRemovedReviewers(), 0));
    }

    @Override
    protected int deleteByRepository(int repositoryId) {
        return dao.deleteByRepository(repositoryId);
    }

    private static void assertActivity(InternalPullRequestActivity actual, long expectedId) {
        assertActivity(actual, expectedId, ActivityType.PULL_REQUEST_ACTIVITY_TYPE);
    }

    private static void assertActivity(InternalPullRequestActivity actual, long expectedId, int expectedType) {
        assertEquals(expectedId, actual.getId());
        assertEquals(expectedType, actual.getType());
    }

    private static void assertPage(Page<InternalPullRequestActivity> page, List<Long> expectedIds) {
        assertEquals("Incorrect page size", expectedIds.size(), page.getSize());
        List<Long> actualIds = Lists.newArrayList(Iterables.transform(page.getValues(), InternalAbstractEntity::getId));

        assertEquals("Activity IDs don't match", expectedIds, actualIds);
    }
}
