package com.atlassian.stash.internal.pull;

import com.atlassian.bitbucket.annotation.Dao;
import com.atlassian.bitbucket.annotation.DataSet;
import com.atlassian.bitbucket.pull.*;
import com.atlassian.bitbucket.user.TestApplicationUser;
import com.atlassian.bitbucket.util.Page;
import com.atlassian.bitbucket.util.PageUtils;
import com.atlassian.stash.internal.AbstractDaoTest;
import com.atlassian.stash.internal.DataSets;
import com.atlassian.stash.internal.comment.CommentDao;
import com.atlassian.stash.internal.comment.HibernateCommentDao;
import com.atlassian.stash.internal.repository.HibernateRepositoryDao;
import com.atlassian.stash.internal.repository.InternalRepository;
import com.atlassian.stash.internal.repository.RepositoryDao;
import com.atlassian.stash.internal.user.HibernateStashUserDao;
import com.atlassian.stash.internal.user.StashUserDao;
import com.atlassian.stash.internal.watcher.HibernateWatcherDao;
import com.atlassian.stash.internal.watcher.WatcherDao;
import com.google.common.collect.*;
import org.junit.Test;
import org.springframework.util.CollectionUtils;

import java.util.*;

import static org.hamcrest.collection.IsIterableContainingInAnyOrder.containsInAnyOrder;

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

    @Dao(HibernateCommentDao.class)
    private CommentDao commentDao;
    @Dao
    private HibernatePullRequestDao dao;
    @Dao(HibernateRepositoryDao.class)
    private RepositoryDao repositoryDao;
    @Dao(HibernateStashUserDao.class)
    private StashUserDao userDao;
    @Dao(HibernateWatcherDao.class)
    private WatcherDao watcherDao;

    @Test
    public void testCountAllByStatus() {
        Map<PullRequestState, Long> initialCountAllByStatus = dao.countAllByStatus();
        InternalRepository repository = repositoryDao.loadById(1);
        InternalPullRequest pullRequest = dao.create(new InternalPullRequest.Builder()
                .createdDate(new Date())
                .description("description")
                .fromRef(new InternalPullRequestRef.Builder()
                        .displayId("1.0")
                        .hash("404da710dda735f5955ec36df2c556764a47183d")
                        .id("refs/heads/1.0")
                        .repository(repository)
                        .build())
                .state(PullRequestState.OPEN)
                .title("title")
                .toRef(new InternalPullRequestRef.Builder()
                        .displayId("master")
                        .hash("c81fa2d12655e231539379650e417f93a2bebcfb")
                        .id("refs/heads/master")
                        .repository(repository)
                        .build())
                .updatedDate(new Date())
                .build());

        Map<PullRequestState, Long> countAllByStatus = dao.countAllByStatus();
        assertEquals(initialCountAllByStatus.get(PullRequestState.OPEN) + 1, (long) countAllByStatus.get(PullRequestState.OPEN));

        dao.update(new InternalPullRequest.Builder(pullRequest)
                .state(PullRequestState.MERGED)
                .build());

        countAllByStatus = dao.countAllByStatus();
        assertEquals(1, (long) countAllByStatus.get(PullRequestState.MERGED));

        dao.update(new InternalPullRequest.Builder(pullRequest)
                .state(PullRequestState.DECLINED)
                .build());

        countAllByStatus = dao.countAllByStatus();
        assertEquals(initialCountAllByStatus.get(PullRequestState.DECLINED) + 1, (long) countAllByStatus.get(PullRequestState.DECLINED));
    }

    @DataSet({DataSets.USERS, DataSets.PROJECTS, DataSets.REPOSITORIES})
    @Test
    public void testCountAllByStatusWithNoPullRequests() {
        assertTrue(dao.countAllByStatus().isEmpty());
    }

    @Test
    public void testCountMatching() {
        runCountTests(dao::countMatching);
    }

    @Test
    public void testCreate() {
        InternalRepository repository = repositoryDao.loadById(1);
        String description = "QA pull request: " + DESCRIPTION_CHARS;
        String title = "Pull Request: \u5718\u96c6";

        InternalPullRequest pullRequest = new InternalPullRequest.Builder()
                .createdDate(new Date())
                .description(description)
                .fromRef(new InternalPullRequestRef.Builder()
                        .displayId("1.0")
                        .hash("404da710dda735f5955ec36df2c556764a47183d")
                        .id("refs/heads/1.0")
                        .repository(repository)
                        .build())
                .state(PullRequestState.OPEN)
                .title(title)
                .toRef(new InternalPullRequestRef.Builder()
                        .displayId("master")
                        .hash("c81fa2d12655e231539379650e417f93a2bebcfb")
                        .id("refs/heads/master")
                        .repository(repository)
                        .build())
                .updatedDate(new Date())
                .build();
        InternalPullRequest created = dao.create(pullRequest);
        assertEquals(description, pullRequest.getDescription());
        assertEquals(title, pullRequest.getTitle());
        assertEquals(created.getGlobalId(), pullRequest.getGlobalId());
        assertEquals(created.getScopedId(), pullRequest.getScopedId());

        //Ensure the session has been flushed so that the pull request will be written (but not committed!)
        flush();
        //Evict the pull request from the session so that the get has to go back to the database
        evict(created, created.getGlobalId());

        //Test the database roundtrip for the Unicode name and description, that we get back what we put in
        InternalPullRequest retrieved = dao.getById(created.getGlobalId());
        assertNotNull(retrieved);
        assertNotSame("The evicted pull request was still returned; the test is not valid!", created, retrieved);
        assertEquals(description, pullRequest.getDescription());
        assertEquals(title, pullRequest.getTitle());
    }

    @Test
    public void testDeclineByFromRepository() {
        //Has 2 open pull requests, so both should be declined
        assertEquals(2, dao.declineByFromRepository(2));
        //Has 2 pull requests but one is already declined, so only one should be updated
        assertEquals(1, dao.declineByFromRepository(1));
        //Has no open pull requests
        assertEquals(0, dao.declineByFromRepository(4));
        //Does not exist (and, implicitly, has no open pull requests)
        assertEquals(0, dao.declineByFromRepository(5));
    }

    @Test
    public void testDelete() {
        //Note: By implication, this also tests cascade deletion of the participants
        InternalPullRequest pullRequest = dao.getById(1L);
        assertNotNull(pullRequest);
        assertFalse("Pull request 1 should have at least one participant",
                CollectionUtils.isEmpty(pullRequest.getAllParticipants()));
        assertFalse("Pull request 1 should have at least one watcher",
                CollectionUtils.isEmpty(pullRequest.getWatchers()));
        dao.delete(pullRequest);

        flush();
        clear();

        assertNull(dao.getById(1L));

        // Did we remove the watcher of our pull request?
        // DataSets#PULL_REQUEST_WATCHERS establishes user 5 as a watcher of pull request 1
        assertEquals(0, watcherDao.countWatchers(pullRequest));
    }

    /**
     * Tests deleteByToRepository for a repository with only pull requests without comments.
     */
    @Test
    public void testDeleteByToRepositoryWithoutComments() {
        InternalPullRequest two = dao.getById(2L);
        InternalPullRequest three = dao.getById(3L);
        assertNotNull(two);
        assertNotNull(three);

        assertEquals(1, dao.deleteByToRepository(2));

        flush();
        clear();

        assertNull(dao.getById(2L));    //Two is FROM repository 2 TO repository 2; it should be deleted
        assertNotNull(dao.getById(3L)); //Three is FROM repository 2, but TO repository 3; it shouldn't be deleted

        // Did we remove the watchers of our pull requests?
        // DataSets#PULL_REQUEST_WATCHERS establishes users 5 and 6 as watchers of pull request 2 and user 9 as a
        // watcher of pull request 3.
        assertEquals("Failed to remove watchers for #2", 0, watcherDao.countWatchers(two));
        assertEquals("Accidentally removed watches for #3", 1, watcherDao.countWatchers(three));
    }

    @Test
    public void testDeleteByToRepositoryWithActivities() {
        assertEquals(2, dao.deleteByToRepository(1));

        flush();
        clear();

        assertNull(dao.getById(1L));
        assertNull(dao.getById(4L));

        // validate that all comments have also been deleted
        assertNull(commentDao.getById(1L));
        assertNull(commentDao.getById(2L));
        assertNull(commentDao.getById(3L));
        assertNull(commentDao.getById(9L));

        // validate that we haven't accidentally deleted all comments
        assertNotNull(commentDao.getById(4L));
    }

    @Test
    public void testDeleteByToRepositoryWhereNoPullRequests() {
        assertEquals(0, dao.deleteByToRepository(4));
    }

    @Test
    public void testDeleteByToRepositoryForNoSuchRepository() {
        assertEquals(0, dao.deleteByToRepository(9001)); //It's over 9000!
    }

    @Test
    public void testFindAll() {
        Page<InternalPullRequest> page = dao.findAll(PageUtils.newRequest(0, 10));
        assertNotNull(page);
        assertTrue(page.getIsLastPage());
        assertEquals(6, page.getSize());

        Iterator<InternalPullRequest> iterator = page.getValues().iterator();
        assertEquals(6L, iterator.next().getGlobalId());
        assertEquals(5L, iterator.next().getGlobalId());
        assertEquals(4L, iterator.next().getGlobalId());
        assertEquals(3L, iterator.next().getGlobalId());
        assertEquals(2L, iterator.next().getGlobalId());
        assertEquals(1L, iterator.next().getGlobalId());
    }

    @Test
    public void testFindByCommentFindsPrFromTopLevelCommentId() throws Exception {
        InternalPullRequest pullRequest = dao.findByComment(1);
        assertNotNull("Should find a pull request by top-level comment ID", pullRequest);
        assertEquals("Should find pull request 1", 1L, pullRequest.getId());
    }

    @Test
    public void testFindByCommentFindsPrFromReplyCommentId() throws Exception {
        InternalPullRequest pullRequest = dao.findByComment(3);
        assertNotNull("Should find a pull request by reply comment ID", pullRequest);
        assertEquals("Should find pull request 1", 1L, pullRequest.getId());
    }

    @Test
    public void testFindByCommentReturnsNullIfPullRequestNotFound() throws Exception {
        // comment 8 is in a commit discussion, not a PR
        InternalPullRequest pullRequest = dao.findByComment(8);
        assertNull("Should return null if unable to find pull request for given comment ID", pullRequest);
    }

    @Test
    public void testFindByRepositoryScopedId() {
        assertEquals(1L, dao.findByRepositoryScopedId(1, 1L).getGlobalId());
        assertEquals(2L, dao.findByRepositoryScopedId(2, 1L).getGlobalId());
        assertEquals(3L, dao.findByRepositoryScopedId(3, 1L).getGlobalId());
        assertEquals(4L, dao.findByRepositoryScopedId(1, 2L).getGlobalId());
        assertNull(dao.findByRepositoryScopedId(1, 100L));
    }

    @Test
    public void testFindByRefs() {
        InternalRepository repository = repositoryDao.loadById(1);

        InternalPullRequestRef from = new InternalPullRequestRef.Builder()
                .id("refs/heads/1.2")
                .repository(repository)
                .build();
        InternalPullRequestRef to = new InternalPullRequestRef.Builder()
                .id("refs/heads/master")
                .repository(repository)
                .build();
        assertNull(dao.findByRefs(from, to));

        from = new InternalPullRequestRef.Builder(from)
                .id("refs/heads/1.1")
                .build();
        assertEquals(1L, dao.findByRefs(from, to).getGlobalId());
    }

    @Test
    public void testFindUnmergedByFromRepository() {
        //Verifies that pagination is applied correctly
        Page<InternalPullRequest> page = dao.findUnmergedByFromRepository(1, PageUtils.newRequest(0, 1));
        assertNotNull(page);
        assertFalse(page.getIsLastPage());
        assertEquals(1, page.getSize());

        List<InternalPullRequest> requests = ImmutableList.copyOf(page.getValues());
        assertEquals(1L, requests.get(0).getGlobalId()); //OPEN state

        page = dao.findUnmergedByFromRepository(1, page.getNextPageRequest());
        assertNotNull(page);
        assertTrue(page.getIsLastPage());
        assertEquals(1, page.getSize());

        requests = ImmutableList.copyOf(page.getValues());
        assertEquals(4L, requests.get(0).getGlobalId()); //DECLINED state

        //Verifies ordering within a page
        page = dao.findUnmergedByFromRepository(2, PageUtils.newRequest(0, 25));
        assertNotNull(page);
        assertTrue(page.getIsLastPage());
        assertEquals(2, page.getSize());

        requests = ImmutableList.copyOf(page.getValues());
        assertEquals(2L, requests.get(0).getGlobalId());
        assertEquals(3L, requests.get(1).getGlobalId());

        //Repository 3 has a pull request TO it, but none FROM it
        page = dao.findUnmergedByFromRepository(3, PageUtils.newRequest(0, 25));
        assertNotNull(page);
        assertTrue(page.getIsLastPage());
        assertEquals(0, page.getSize());

        //There is no repository 5
        page = dao.findUnmergedByFromRepository(5, PageUtils.newRequest(0, 25));
        assertNotNull(page);
        assertTrue(page.getIsLastPage());
        assertEquals(0, page.getSize());
    }

    @Test
    public void testGetById() {
        InternalPullRequest pullRequest = dao.getById(1L);
        assertNotNull(pullRequest);
        assertNull(pullRequest.getDescription());
        assertEquals(1L, pullRequest.getGlobalId());
        assertEquals("Merge 1.1 to master", pullRequest.getTitle());
        assertEquals(0, pullRequest.getVersion());
        assertPullRequestRef("refs/heads/1.1", "1.1", "e53829aceffede831f69137bab313928fa932ce1", 1,
                pullRequest.getFromRef());
        assertPullRequestRef("refs/heads/master", "master", "c81fa2d12655e231539379650e417f93a2bebcfb", 1,
                pullRequest.getToRef());
    }

    /**
     * Tests that we can retrieve the one and only watcher of a pull request.
     * <p>
     * In this case, we're dependant on {@link DataSets#PULL_REQUEST_WATCHERS}, which establishes user 5 as the
     * only watcher of pull request 1.
     */
    @Test
    public void testGetOnlyWatcher() {
        InternalPullRequest pullRequest = dao.getById(1L);
        assertEquals(5, Iterables.getOnlyElement(pullRequest.getWatchers()).getUser().getId());
    }

    /**
     * Tests that we can retrieve more than one watcher of a pull request.
     * <p>
     * In this case, we're dependant on {@link DataSets#PULL_REQUEST_WATCHERS}, which establishes users 5 and 6 as the
     * watchers of pull request 2.
     */
    @Test
    public void testGetMultipleWatchers() {
        InternalPullRequest pullRequest = dao.getById(2L);
        Iterable<Integer> userIds = Collections2.transform(pullRequest.getWatchers(), watcher -> watcher.getUser().getId());
        assertThat(userIds, containsInAnyOrder(5, 6));
    }

    @Test
    public void testOverwriteFromRepository() {
        //When the test starts, repository 3 has one pull request TO it, but none FROM it
        Page<InternalPullRequest> page = dao.findUnmergedByFromRepository(3, PageUtils.newRequest(0, 25));
        assertNotNull(page);
        assertTrue(page.getIsLastPage());
        assertEquals(0, page.getSize());

        //Repository 2 has one intra-repository pull request, which will be deleted here. This is done to mimic how
        //PullRequestServiceImpl.cleanup will use overwriteFromRepository, which makes this test a bit more valuable
        assertEquals(1, dao.deleteByToRepository(2));

        //Repository 2 has one cross-repository pull request, TO repository 3. That should be updated here
        assertEquals(1, dao.overwriteFromRepository(2));

        //Don't flush; the DAO should have done it itself
        page = dao.findUnmergedByFromRepository(3, PageUtils.newRequest(0, 25));
        assertNotNull(page);
        assertTrue(page.getIsLastPage());
        assertEquals(1, page.getSize());

        //Pull request 3, which was FROM repository 2 TO repository 3, should not be FROM and TO 3
        List<InternalPullRequest> requests = ImmutableList.copyOf(page.getValues());
        InternalPullRequest request = requests.get(0);
        assertEquals(3L, request.getGlobalId());
        assertEquals(3, request.getFromRef().getRepository().getId());
        assertEquals(3, request.getToRef().getRepository().getId());
    }

    @Test
    public void testSearch() {
        runCountTests(criteria -> dao.search(criteria, PageUtils.newRequest(0, 100), ALWAYS_TRUE).getSize());
    }

    @Test
    public void testSearchBetweenBranches() {
        PullRequestSearchCriteria criteria = new PullRequestSearchCriteria.Builder()
                //There is only a single pull request originating from this branch
                .fromRefIds(Collections.singleton("refs/heads/1.2"))
                .fromRepositoryId(1)
                //There are 6 pull requests targeting this branch, 2 of which are in repository 1
                .toRefIds(Collections.singleton("refs/heads/master"))
                .toRepositoryId(1)
                .build();

        //If the fromRefIds and toRefIds are OR'd together, this will return 2 pull requests. If they're
        //AND'd together, it will only return 1.
        Page<InternalPullRequest> page = dao.search(criteria, PageUtils.newRequest(0, 10), ALWAYS_TRUE);
        assertEquals("Searching by both branches should choose a single pull request", 1, page.getSize());
        assertTrue("There should only be a single page", page.getIsLastPage());

        InternalPullRequest pullRequest = Iterables.getOnlyElement(page.getValues());
        assertEquals(4L, pullRequest.getGlobalId());
    }

    @Test
    public void testSearchPaging() {
        PullRequestSearchCriteria criteria = new PullRequestSearchCriteria.Builder()
                .fromRepositoryId(2)
                .build();

        Page<InternalPullRequest> page = dao.search(criteria, PageUtils.newRequest(0, 1), ALWAYS_TRUE);
        assertEquals("Page size should be as requested", 1, page.getSize());
        assertEquals("First page should be returned first", 0, page.getStart());
        assertEquals("There should be a second page", false, page.getIsLastPage());

        page = dao.search(criteria, PageUtils.newRequest(1, 1), ALWAYS_TRUE);
        assertEquals("Page size should be as requested", 1, page.getSize());
        assertEquals("Second page should be returned second", 1, page.getStart());
        assertEquals("Second page should be last page", true, page.getIsLastPage());
    }

    @Test
    public void testSearchWithNewestOrder() {
        PullRequestSearchCriteria criteria = new PullRequestSearchCriteria.Builder()
                .fromRepositoryId(2)
                .order(PullRequestOrder.NEWEST)
                .build();

        Page<InternalPullRequest> page = dao.search(criteria, PageUtils.newRequest(0, 2), ALWAYS_TRUE);
        assertEquals("Page size should be as requested", 2, page.getSize());
        List<InternalPullRequest> requests = ImmutableList.copyOf(page.getValues());
        assertEquals(3L, requests.get(0).getGlobalId());
        assertEquals(2L, requests.get(1).getGlobalId());
    }

    @Test
    public void testSearchWithOldestOrder() {
        PullRequestSearchCriteria criteria = new PullRequestSearchCriteria.Builder()
                .fromRepositoryId(2)
                .order(PullRequestOrder.OLDEST)
                .build();

        Page<InternalPullRequest> page = dao.search(criteria, PageUtils.newRequest(0, 2), ALWAYS_TRUE);
        assertEquals("Page size should be as requested", 2, page.getSize());
        List<InternalPullRequest> requests = ImmutableList.copyOf(page.getValues());
        assertEquals(2L, requests.get(0).getGlobalId());
        assertEquals(3L, requests.get(1).getGlobalId());
    }

    @Test
    public void testSearchWithParticipantStatusOrder() {
        PullRequestSearchCriteria criteria = new PullRequestSearchCriteria.Builder()
                .participant(new PullRequestParticipantCriteria.Builder(new TestApplicationUser("username", 8))
                        .build())
                .order(PullRequestOrder.PARTICIPANT_STATUS)
                .build();

        Page<InternalPullRequest> page = dao.search(criteria, PageUtils.newRequest(0, 5), ALWAYS_TRUE);
        assertEquals("Page size should be as requested", 4, page.getSize());
        Iterator<InternalPullRequest> iterator = page.getValues().iterator();
        assertEquals(4L, iterator.next().getGlobalId());
        assertEquals(6L, iterator.next().getGlobalId());
        assertEquals(5L, iterator.next().getGlobalId());
        assertEquals(2L, iterator.next().getGlobalId());
    }

    @Test
    public void testSearchWithDefaultOrder() {
        PullRequestSearchCriteria criteria = new PullRequestSearchCriteria.Builder()
                .fromRepositoryId(2)
                .order(null)
                .build();

        Page<InternalPullRequest> page = dao.search(criteria, PageUtils.newRequest(0, 2), ALWAYS_TRUE);
        assertEquals("Page size should be as requested", 2, page.getSize());
        List<InternalPullRequest> requests = ImmutableList.copyOf(page.getValues());
        assertEquals(3L, requests.get(0).getGlobalId());
        assertEquals(2L, requests.get(1).getGlobalId());
    }

    @Test
    public void testSearchWithUser() {
        PullRequestParticipantCriteria participant = new PullRequestParticipantCriteria
                .Builder(new TestApplicationUser("username", 5))
                .build();

        Page<InternalPullRequest> page = dao.search(new PullRequestSearchCriteria.Builder()
                .participants(ImmutableSet.of(participant))
                .build(), PageUtils.newRequest(0, 2), ALWAYS_TRUE);
        assertEquals("Page size should be as requested", 2, page.getSize());
        assertTrue("Page is last", page.getIsLastPage());
        List<InternalPullRequest> requests = ImmutableList.copyOf(page.getValues());

        assertTrue(hasParticipantWithUserId(5, requests.get(0).getAllParticipants()));
        assertTrue(hasParticipantWithUserId(5, requests.get(1).getAllParticipants()));

        participant = new PullRequestParticipantCriteria
                .Builder(new TestApplicationUser("username", 7))
                .build();
        page = dao.search(new PullRequestSearchCriteria.Builder()
                .participants(ImmutableSet.of(participant))
                .build(), PageUtils.newRequest(0, 2), ALWAYS_TRUE);
        assertEquals("Page size should be as requested", 2, page.getSize());
        assertTrue("Page is last", page.getIsLastPage());

        requests = ImmutableList.copyOf(page.getValues());

        assertTrue(hasParticipantWithUserId(7, requests.get(0).getReviewers()));

        participant = new PullRequestParticipantCriteria
                .Builder(new TestApplicationUser("username", 7))
                .statuses(ImmutableList.of(PullRequestParticipantStatus.APPROVED))
                .build();
        page = dao.search(new PullRequestSearchCriteria.Builder()
                .participants(ImmutableSet.of(participant))
                .build(), PageUtils.newRequest(0, 2), ALWAYS_TRUE);

        assertEquals("Page size should be as requested", 2, page.getSize());
        assertTrue("Page is last", page.getIsLastPage());

        requests = ImmutableList.copyOf(page.getValues());

        assertTrue(hasParticipantWithUserId(7, requests.get(0).getReviewers()));
    }

    @Test
    public void testSearchNotCompletedPullRequests() {
        PullRequestParticipantCriteria participant = new PullRequestParticipantCriteria.Builder(new TestApplicationUser("username", 8))
                .role(PullRequestRole.REVIEWER)
                .statuses(ImmutableList.of(PullRequestParticipantStatus.UNAPPROVED, PullRequestParticipantStatus.NEEDS_WORK))
                .build();

        PullRequestSearchCriteria criteria = new PullRequestSearchCriteria.Builder()
                .participant(participant)
                .build();

        Page<InternalPullRequest> pullRequests = dao.search(criteria, PageUtils.newRequest(0, 5), ALWAYS_TRUE);
        assertEquals(3, pullRequests.getSize());
        assertTrue(pullRequests.getIsLastPage());

        Iterator<InternalPullRequest> iterator = pullRequests.getValues().iterator();
        assertEquals(6, iterator.next().getGlobalId());
        assertEquals(5, iterator.next().getGlobalId());
        assertEquals(4, iterator.next().getGlobalId());
    }

    @Test
    public void testSearchWithAuthorAndReviewer() {
        ImmutableSet<PullRequestParticipantCriteria> participants = ImmutableSet.<PullRequestParticipantCriteria>builder()
                .add(new PullRequestParticipantCriteria
                        .Builder(new TestApplicationUser("username", 5))
                        .role(PullRequestRole.AUTHOR)
                        .build())
                .add(new PullRequestParticipantCriteria
                        .Builder(new TestApplicationUser("username", 7))
                        .role(PullRequestRole.REVIEWER)
                        .build())
                .build();

        Page<InternalPullRequest> page = dao.search(new PullRequestSearchCriteria.Builder()
                .participants(participants)
                .build(), PageUtils.newRequest(0, 5), ALWAYS_TRUE);
        assertEquals("Page size should be as requested", 1, page.getSize());
        assertTrue("Page is last", page.getIsLastPage());
        List<InternalPullRequest> requests = ImmutableList.copyOf(page.getValues());

        assertEquals(1, requests.get(0).getGlobalId());
    }

    @Test
    public void testSearchWithStatus() {
        ImmutableSet<PullRequestParticipantCriteria> participants = ImmutableSet.<PullRequestParticipantCriteria>builder()
                .add(new PullRequestParticipantCriteria
                        .Builder(new TestApplicationUser("username", 8))
                        .role(PullRequestRole.REVIEWER)
                        .status(PullRequestParticipantStatus.APPROVED)
                        .build())
                .build();

        Page<InternalPullRequest> page = dao.search(new PullRequestSearchCriteria.Builder()
                .participants(participants)
                .build(), PageUtils.newRequest(0, 5), ALWAYS_TRUE);
        assertEquals("Page size should be as requested", 1, page.getSize());
        assertTrue("Page is last", page.getIsLastPage());

        Iterator<InternalPullRequest> iterator = page.getValues().iterator();
        assertEquals(2, iterator.next().getGlobalId());
    }

    @Test
    public void testSearchWithMultipleStatuses() {
        ImmutableSet<PullRequestParticipantCriteria> participants = ImmutableSet.<PullRequestParticipantCriteria>builder()
                .add(new PullRequestParticipantCriteria
                        .Builder(new TestApplicationUser("username", 8))
                        .role(PullRequestRole.REVIEWER)
                        .status(PullRequestParticipantStatus.APPROVED)
                        .status(PullRequestParticipantStatus.UNAPPROVED)
                        .build())
                .build();

        Page<InternalPullRequest> page = dao.search(new PullRequestSearchCriteria.Builder()
                .participants(participants)
                .build(), PageUtils.newRequest(0, 5), ALWAYS_TRUE);
        assertEquals("Page size should be as requested", 2, page.getSize());
        assertTrue("Page is last", page.getIsLastPage());

        Iterator<InternalPullRequest> iterator = page.getValues().iterator();
        assertEquals(4, iterator.next().getGlobalId());
        assertEquals(2, iterator.next().getGlobalId());
    }

    @Test
    public void testSearchWithNullStatus() {
        ImmutableSet<PullRequestParticipantCriteria> participants = ImmutableSet.<PullRequestParticipantCriteria>builder()
                .add(new PullRequestParticipantCriteria
                        .Builder(new TestApplicationUser("username", 8))
                        .role(PullRequestRole.REVIEWER)
                        .status(null)
                        .build())
                .build();

        Page<InternalPullRequest> page = dao.search(new PullRequestSearchCriteria.Builder()
                .participants(participants)
                .build(), PageUtils.newRequest(0, 5), ALWAYS_TRUE);
        assertEquals("Page size should be as requested", 4, page.getSize());
        assertTrue("Page is last", page.getIsLastPage());

        Iterator<InternalPullRequest> iterator = page.getValues().iterator();
        assertEquals(6, iterator.next().getGlobalId());
        assertEquals(5, iterator.next().getGlobalId());
        assertEquals(4, iterator.next().getGlobalId());
        assertEquals(2, iterator.next().getGlobalId());
    }

    @Test
    public void testSearchWithAuthor() {
        ImmutableSet<PullRequestParticipantCriteria> participants = ImmutableSet.<PullRequestParticipantCriteria>builder()
                .add(new PullRequestParticipantCriteria
                        .Builder(new TestApplicationUser("username", 5))
                        .role(PullRequestRole.AUTHOR)
                        .build())
                .build();

        Page<InternalPullRequest> page = dao.search(new PullRequestSearchCriteria.Builder()
                .participants(participants)
                .build(), PageUtils.newRequest(0, 5), ALWAYS_TRUE);
        assertEquals("Page size should be as requested", 2, page.getSize());
        assertTrue("Page is last", page.getIsLastPage());
        List<InternalPullRequest> requests = ImmutableList.copyOf(page.getValues());

        assertEquals(4, requests.get(0).getGlobalId());
        assertEquals(1, requests.get(1).getGlobalId());
    }

    @Test
    public void testSearchWithReviewer() {
        ImmutableSet<PullRequestParticipantCriteria> participants = ImmutableSet.<PullRequestParticipantCriteria>builder()
                .add(new PullRequestParticipantCriteria
                        .Builder(new TestApplicationUser("username", 7))
                        .role(PullRequestRole.REVIEWER)
                        .build())
                .build();

        Page<InternalPullRequest> page = dao.search(new PullRequestSearchCriteria.Builder()
                .participants(participants)
                .build(), PageUtils.newRequest(0, 5), ALWAYS_TRUE);
        assertEquals("Page size should be as requested", 2, page.getSize());
        assertTrue("Page is last", page.getIsLastPage());
        List<InternalPullRequest> requests = ImmutableList.copyOf(page.getValues());

        assertEquals(5, requests.get(0).getGlobalId());
        assertEquals(1, requests.get(1).getGlobalId());
    }

    @Test
    public void testSearchWithNoMatches() {
        ImmutableSet<PullRequestParticipantCriteria> participants = ImmutableSet.<PullRequestParticipantCriteria>builder()
                .add(new PullRequestParticipantCriteria
                        .Builder(new TestApplicationUser("username", 7))
                        .role(PullRequestRole.AUTHOR)
                        .build())
                .build();

        Page<InternalPullRequest> page = dao.search(new PullRequestSearchCriteria.Builder()
                .participants(participants)
                .build(), PageUtils.newRequest(0, 5), ALWAYS_TRUE);
        assertEquals("Page size should be as requested", 0, page.getSize());
    }

    @Test
    public void testCountWithAuthorAndReviewer() {
        ImmutableSet<PullRequestParticipantCriteria> participants = ImmutableSet.<PullRequestParticipantCriteria>builder()
                .add(new PullRequestParticipantCriteria
                        .Builder(new TestApplicationUser("username", 6))
                        .role(PullRequestRole.AUTHOR)
                        .build())
                .add(new PullRequestParticipantCriteria
                        .Builder(new TestApplicationUser("username", 8))
                        .role(PullRequestRole.REVIEWER)
                        .build())
                .build();

        assertEquals(1, dao.countMatching(new PullRequestSearchCriteria.Builder()
                .participants(participants)
                .build()));
    }

    @Test
    public void testCountWithAuthor() {
        ImmutableSet<PullRequestParticipantCriteria> participants = ImmutableSet.<PullRequestParticipantCriteria>builder()
                .add(new PullRequestParticipantCriteria
                        .Builder(new TestApplicationUser("username", 5))
                        .role(PullRequestRole.AUTHOR)
                        .build())
                .build();

        assertEquals(2, dao.countMatching(new PullRequestSearchCriteria.Builder()
                .participants(participants)
                .build()));
    }

    @Test
    public void testCountWithReviewer() {
        ImmutableSet<PullRequestParticipantCriteria> participants = ImmutableSet.<PullRequestParticipantCriteria>builder()
                .add(new PullRequestParticipantCriteria
                        .Builder(new TestApplicationUser("username", 7))
                        .role(PullRequestRole.REVIEWER)
                        .build())
                .build();

        assertEquals(2, dao.countMatching(new PullRequestSearchCriteria.Builder()
                .participants(participants)
                .build()));
    }

    @Test
    public void testCountWithNoMatches() {
        ImmutableSet<PullRequestParticipantCriteria> participants = ImmutableSet.<PullRequestParticipantCriteria>builder()
                .add(new PullRequestParticipantCriteria
                        .Builder(new TestApplicationUser("username", 7))
                        .role(PullRequestRole.AUTHOR)
                        .build())
                .build();

        assertEquals(0, dao.countMatching(new PullRequestSearchCriteria.Builder()
                .participants(participants)
                .build()));
    }

    @Test
    public void testSearchWithMultipleFromRefBranches() {
        Page<InternalPullRequest> page = dao.search(new PullRequestSearchCriteria.Builder()
                .fromRefIds(Sets.newHashSet("refs/heads/master", "refs/heads/1.1"))
                .build(), PageUtils.newRequest(0, 5), ALWAYS_TRUE);

        List<InternalPullRequest> requests = ImmutableList.copyOf(page.getValues());

        assertEquals(2, requests.size());

        assertEquals("refs/heads/1.1", requests.get(1).getFromRef().getId());
        assertEquals("refs/heads/master", requests.get(0).getFromRef().getId());
    }

    @Test
    public void testSearchWithMultipleToRefBranches() {
        Page<InternalPullRequest> page = dao.search(new PullRequestSearchCriteria.Builder()
                .toRefIds(Sets.newHashSet("refs/heads/master", "refs/heads/1.1"))
                .build(), PageUtils.newRequest(0, 5), ALWAYS_TRUE);
        List<InternalPullRequest> requests = ImmutableList.copyOf(page.getValues());

        assertEquals(5, requests.size());
        assertEquals("refs/heads/master", requests.get(0).getToRef().getId());
        assertEquals("refs/heads/master", requests.get(1).getToRef().getId());
        assertEquals("refs/heads/master", requests.get(2).getToRef().getId());
        assertEquals("refs/heads/master", requests.get(3).getToRef().getId());
        assertEquals("refs/heads/master", requests.get(4).getToRef().getId());
    }

    @Test
    public void testUpdate() {
        Date timestamp = new Date();

        InternalPullRequest initial = dao.getById(1L);
        int version = initial.getVersion();

        InternalPullRequest modified = new InternalPullRequest.Builder(initial)
                .description("Modified for update")
                .title("Updated Pull Request")
                .toRef(new InternalPullRequestRef.Builder(initial.getToRef())
                        .hash("daa6a1d62294ac7468384e2eea46790f1fdf96a4")
                        .build())
                .updatedDate(timestamp)
                .build();
        dao.update(modified);

        //Flushing the session causes the version number to increment, but should not commit the transaction
        flush();

        InternalPullRequest updated = dao.getById(1L);
        assertEquals("Modified for update", updated.getDescription());
        assertEquals("Updated Pull Request", updated.getTitle());
        assertEquals(timestamp, updated.getUpdatedDate());
        assertFalse("Entity version was not updated", version == updated.getVersion());
    }

    private void runCountTests(CountSource counter) {
        PullRequestSearchCriteria criteria = new PullRequestSearchCriteria.Builder()
                .toRepositoryId(1)
                .build();
        assertEquals("There should be two prs found for repo 1", 2, counter.countFor(criteria));

        criteria = new PullRequestSearchCriteria.Builder()
                .toRepositoryId(1)
                .state(PullRequestState.OPEN)
                .build();
        assertEquals("There should be one open pr for repo 1", 1, counter.countFor(criteria));

        criteria = new PullRequestSearchCriteria.Builder()
                .toRepositoryId(1)
                .state(PullRequestState.DECLINED)
                .build();
        assertEquals("There should be one declined pr for repo 1", 1, counter.countFor(criteria));

        criteria = new PullRequestSearchCriteria.Builder()
                .toRepositoryId(1)
                .state(PullRequestState.MERGED)
                .build();
        assertEquals("There should be no merged prs found for repo 1", 0, counter.countFor(criteria));

        criteria = new PullRequestSearchCriteria.Builder()
                .fromRepositoryId(1)
                .build();
        assertEquals("There should only be two prs found for repo 1", 2, counter.countFor(criteria));

        criteria = new PullRequestSearchCriteria.Builder()
                .fromRepositoryId(1)
                .state(PullRequestState.OPEN)
                .build();
        assertEquals("There should be one open pr for repo 1", 1, counter.countFor(criteria));

        criteria = new PullRequestSearchCriteria.Builder()
                .fromRepositoryId(1)
                .state(PullRequestState.DECLINED)
                .build();
        assertEquals("There should be one declined pr for repo 1", 1, counter.countFor(criteria));

        criteria = new PullRequestSearchCriteria.Builder()
                .fromRepositoryId(1)
                .state(PullRequestState.MERGED)
                .build();
        assertEquals("There should be no merged prs found for repo 1", 0, counter.countFor(criteria));

        criteria = new PullRequestSearchCriteria.Builder()
                .toRepositoryId(3)
                .build();
        assertEquals("There should be one pr found for repo 3", 1, counter.countFor(criteria));

        criteria = new PullRequestSearchCriteria.Builder()
                .fromRepositoryId(3)
                .build();
        assertEquals("There should be no prs found from repo 3", 0, counter.countFor(criteria));

        criteria = new PullRequestSearchCriteria.Builder()
                .build();
        assertEquals("There should be four prs found for all repos", 6, counter.countFor(criteria));

        criteria = new PullRequestSearchCriteria.Builder()
                .fromRepositoryId(1)
                .toRepositoryId(1)
                .build();
        assertEquals("There should be two prs found between repo 1 and itself", 2, counter.countFor(criteria));

        criteria = new PullRequestSearchCriteria.Builder()
                .fromRepositoryId(1)
                .toRepositoryId(2)
                .build();
        assertEquals("There should be no prs found between repos 1 to 2", 0, counter.countFor(criteria));

        criteria = new PullRequestSearchCriteria.Builder()
                .fromRepositoryId(2)
                .toRepositoryId(2)
                .build();
        assertEquals("There should be one pr found between repo 2 and itself", 1, counter.countFor(criteria));

        criteria = new PullRequestSearchCriteria.Builder()
                .fromRepositoryId(2)
                .toRepositoryId(3)
                .build();
        assertEquals("There should be one pr found between repos 2 to 3", 1, counter.countFor(criteria));

        criteria = new PullRequestSearchCriteria.Builder()
                .fromRefIds(Collections.singleton("refs/heads/1.1"))
                .build();
        assertEquals("There should be one pr found with a branch 1.1 between any repos", 1, counter.countFor(criteria));

        criteria = new PullRequestSearchCriteria.Builder()
                .toRefIds(Collections.singleton("refs/heads/master"))
                .build();
        assertEquals("There should be four prs found to branch master between any repos", 6, counter.countFor(criteria));
    }

    private static void assertPullRequestRef(String id, String displayId, String hash, int repositoryId,
                                             InternalPullRequestRef ref) {
        assertEquals(id, ref.getId());
        assertEquals(displayId, ref.getDisplayId());
        assertEquals(hash, ref.getLatestCommit());

        InternalRepository repository = ref.getRepository();
        assertNotNull(repository);
        assertEquals(repositoryId, repository.getId());
    }

    private static boolean hasParticipantWithUserId(final int id, Collection<PullRequestParticipant> participants) {
        return participants.stream().anyMatch(participant -> participant.getUser().getId() == id);
    }

    private interface CountSource {

        long countFor(PullRequestSearchCriteria criteria);
    }
}
