package com.atlassian.stash.internal.web.pull;

import com.atlassian.bitbucket.NoSuchEntityException;
import com.atlassian.bitbucket.auth.AuthenticationContext;
import com.atlassian.bitbucket.commit.*;
import com.atlassian.bitbucket.i18n.I18nService;
import com.atlassian.bitbucket.i18n.KeyedMessage;
import com.atlassian.bitbucket.i18n.SimpleI18nService;
import com.atlassian.bitbucket.nav.NavBuilder;
import com.atlassian.bitbucket.permission.Permission;
import com.atlassian.bitbucket.permission.PermissionService;
import com.atlassian.bitbucket.pull.*;
import com.atlassian.bitbucket.repository.*;
import com.atlassian.bitbucket.scm.ScmFeature;
import com.atlassian.bitbucket.scm.ScmService;
import com.atlassian.bitbucket.ui.PluginFormFragments;
import com.atlassian.bitbucket.ui.PluginFormFragmentsFactory;
import com.atlassian.bitbucket.user.ApplicationUser;
import com.atlassian.bitbucket.user.TestApplicationUser;
import com.atlassian.bitbucket.user.UserService;
import com.atlassian.bitbucket.util.PageImpl;
import com.atlassian.bitbucket.util.PageRequest;
import com.atlassian.bitbucket.util.PageRequestImpl;
import com.atlassian.sal.api.features.DarkFeatureManager;
import com.atlassian.soy.renderer.SoyException;
import com.atlassian.stash.internal.discovery.DiscoverableFeature;
import com.atlassian.stash.internal.discovery.FeatureDiscoveryService;
import com.atlassian.stash.internal.project.InternalNormalProject;
import com.atlassian.stash.internal.project.InternalPersonalProject;
import com.atlassian.stash.internal.project.InternalProject;
import com.atlassian.stash.internal.project.InternalProjectService;
import com.atlassian.stash.internal.pull.InternalPullRequest;
import com.atlassian.stash.internal.pull.InternalPullRequestParticipant;
import com.atlassian.stash.internal.pull.InternalPullRequestRef;
import com.atlassian.stash.internal.pull.InternalPullRequestService;
import com.atlassian.stash.internal.repository.InternalRepository;
import com.atlassian.stash.internal.scm.InternalScmService;
import com.atlassian.stash.internal.user.InternalNormalUser;
import com.atlassian.stash.internal.watcher.InternalWatcherService;
import com.google.common.collect.*;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.*;
import org.mockito.internal.matchers.apachecommons.ReflectionEquals;
import org.mockito.runners.MockitoJUnitRunner;
import org.mockito.stubbing.OngoingStubbing;
import org.springframework.validation.Errors;
import org.springframework.web.context.request.WebRequest;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.View;
import org.springframework.web.servlet.mvc.support.RedirectAttributes;
import org.springframework.web.servlet.view.RedirectView;

import javax.servlet.http.HttpServletResponse;
import javax.validation.ConstraintViolationException;
import java.util.Collections;
import java.util.EnumSet;
import java.util.Set;

import static com.atlassian.bitbucket.hamcrest.RepositoryMatchers.resolveRefRequest;
import static org.junit.Assert.*;
import static org.mockito.Mockito.*;

@SuppressWarnings({"unchecked", "ConstantConditions"})
@RunWith(MockitoJUnitRunner.class)
public class PullRequestControllerTest {

    private static final String BRANCH1 = "branch1";
    private static final String BRANCH2 = "branch2";
    private static final String NOT_FOUND_BRANCH = "not_found";
    private static final String PROJECT_KEY = "PROJ";
    private static final String PULL_REQUEST_OVERVIEW_FAKE_URL = "pull-request-activity-url";
    private static final String PULL_REQUEST_DESCRIPTION = "description";
    private static final String PULL_REQUEST_FAKE_URL = "pull-request-url";
    private static final String PULL_REQUEST_TITLE = "title";
    private static final String REPO_SLUG = "repo";
    private static final int FROM_REPO_ID = 0;
    private static final String FROM_PROJECT_KEY = "PROJ_2";
    private static final String FROM_REPO_SLUG = "fromRepo";
    private static final int TARGET_REPO_ID = 1;
    private static final String TARGET_REPO_SLUG = "toRepo";

    // Model map keys
    private static final String COMMIT_KEY = "commit";
    private static final String SINCE_COMMIT_ID_KEY = "sinceCommitId";
    private static final String UNTIL_COMMIT_ID_KEY = "untilCommitId";
    private static final String PULL_REQUEST_KEY = "pullRequest";
    private static final String REPOSITORY_KEY = "repository";
    private static final String TO_BRANCH_KEY = "targetBranch";
    private static final String FROM_REPOSITORY_KEY = "sourceRepository";
    private static final String FROM_BRANCH_KEY = "sourceBranch";
    private static final String FIELD_ERRORS_KEY = "fieldErrors";
    private static final String FORM_ERRORS_KEY = "formErrors";
    private static final String NOT_INITIALISED_KEY = "notInitialised";
    private static final String MAIN_HIERARCHY = "heirarchy1";

    private static final String ITERATIVE_REVIEW_DARK_FEATURE_KEY = "dark.feature.shared.iterative-review";

    @Mock
    private AuthenticationContext authenticationContext;
    private SimpleBranch branch;
    @Mock
    private CommitService commitService;
    @InjectMocks
    private PullRequestController controller;
    @Mock
    private ApplicationUser currentUser;
    @Mock
    private DarkFeatureManager darkFeatureManager;
    private Errors errors;
    @Mock
    private PluginFormFragmentsFactory fragmentsFactory;
    @Mock
    private SimpleBranch fromBranch;
    private InternalRepository fromRepo;
    @Spy
    @SuppressWarnings("unused")
    private I18nService i18nService = new SimpleI18nService();
    @Mock(answer = Answers.RETURNS_DEEP_STUBS)
    private NavBuilder navBuilder;
    @Mock
    private NavBuilder.PagedBuilder nbActivity;
    @Mock
    private NavBuilder.PullRequestOverview nbOverview;
    @Mock
    private PermissionService permissionService;
    @Mock
    private InternalProjectService projectService;
    @Mock
    private InternalPullRequestService pullRequestService;
    @Mock
    private RefService refService;
    private InternalRepository repo;
    @Mock
    private RepositoryService repositoryService;
    @Mock
    private HttpServletResponse response;
    @Mock
    private InternalScmService scmService;
    @Mock
    private SimpleBranch toBranch;
    @Mock
    private UserService userService;
    @Mock
    private InternalWatcherService watcherService;
    @Mock
    private WebRequest webRequest;
    @Mock
    private FeatureDiscoveryService discoveryService;

    @Before
    @SuppressWarnings("unchecked")
    public void setup() throws Exception {
        when(authenticationContext.getCurrentUser()).thenReturn(currentUser);
        when(discoveryService.getUndiscovered()).thenReturn(EnumSet.noneOf(DiscoverableFeature.class));
        InternalProject project = new InternalNormalProject.Builder().id(0).key(PROJECT_KEY).name(PROJECT_KEY).build();
        repo = new InternalRepository.Builder().id(0).project(project).name(REPO_SLUG).state(Repository.State.AVAILABLE).hierarchyId(MAIN_HIERARCHY).build();
        branch = new SimpleBranch.Builder().isDefault(true).displayId("master").id("master").latestCommit("123").build();

        InternalProject toProject = new InternalNormalProject.Builder().id(0).key(FROM_PROJECT_KEY).name(FROM_PROJECT_KEY).build();
        fromRepo = new InternalRepository.Builder().id(0).project(toProject).name(FROM_REPO_SLUG).state(Repository.State.AVAILABLE).hierarchyId(MAIN_HIERARCHY).build();

        NavBuilder.PullRequest pullRequestNb = navBuilder.repo(any(Repository.class)).pullRequest(anyInt());
        NavBuilder.PullRequestOverview overviewNb = pullRequestNb.overview();
        when(navBuilder.project(anyString()).repo(anyString()).pullRequest(anyInt())).thenReturn(pullRequestNb);
        when(pullRequestNb.buildRelative()).thenReturn(PULL_REQUEST_FAKE_URL);
        when(overviewNb.buildRelative()).thenReturn(PULL_REQUEST_OVERVIEW_FAKE_URL);
        when(overviewNb.withParam(anyString(), anyString())).thenReturn(overviewNb);

        when(fragmentsFactory.forKey(anyString(), anyMap())).thenReturn(mock(PluginFormFragments.class));
        when(fromBranch.getDisplayId()).thenReturn(BRANCH1);
        when(fromBranch.getLatestCommit()).thenReturn(BRANCH1);
        when(permissionService.hasGlobalPermission(Permission.LICENSED_USER)).thenReturn(Boolean.TRUE);
        when(projectService.getByKey(eq(PROJECT_KEY))).thenReturn(project);
        when(refService.resolveRef(argThat(resolveRefRequest(fromRepo, BRANCH1)))).thenReturn(fromBranch);
        when(refService.resolveRef(argThat(resolveRefRequest(repo, BRANCH1)))).thenReturn(fromBranch);
        when(refService.resolveRef(argThat(resolveRefRequest(repo, BRANCH2)))).thenReturn(toBranch);
        when(repositoryService.getBySlug(eq(PROJECT_KEY), eq(REPO_SLUG))).thenReturn(repo);
        when(repositoryService.getById(eq(FROM_REPO_ID))).thenReturn(fromRepo);
        when(scmService.isSupported(isA(Repository.class), same(ScmFeature.CROSS_REPOSITORY))).thenReturn(true);
        when(toBranch.getDisplayId()).thenReturn(BRANCH2);
        when(toBranch.getLatestCommit()).thenReturn(BRANCH2);

        errors = mock(Errors.class);
        when(errors.hasErrors()).thenReturn(false);
    }

    @Test
    public void testListPullRequests() {
        PageImpl<PullRequest> page = new PageImpl<>(new PageRequestImpl(0, 100), Collections.emptySet(), true);
        when(pullRequestService.search(any(PullRequestSearchRequest.class), any(PageRequest.class))).thenReturn(page);
        when(pullRequestService.count(any(PullRequestSearchRequest.class))).thenReturn(1L);

        ModelAndView modelAndView = controller.listPullRequestsView(PROJECT_KEY, REPO_SLUG, PullRequestState.OPEN.name(),
                PullRequestDirection.INCOMING.name(), BRANCH1, 0, 100, PullRequestOrder.NEWEST.name(), null, false);

        verify(pullRequestService).search(
                refEq(new PullRequestSearchRequest.Builder().toRepositoryId(repo.getId()).state(PullRequestState.OPEN)
                        .toRefId(BRANCH1).order(PullRequestOrder.NEWEST).build()),
                any(PageRequest.class));
        verify(pullRequestService).count(refEq(new PullRequestSearchRequest.Builder().toRepositoryId(repo.getId()).build()));

        assertEquals("The PR list view should be shown", modelAndView.getViewName(), PullRequestController.LIST_VIEW);
        assertNotNull("A repository should be available in the model", modelAndView.getModel().get(REPOSITORY_KEY));
        assertSame("The PR page should be available in the model", page, modelAndView.getModel().get("pullRequestPage"));
        assertTrue("The repositoryHasPullRequests flag should be true", (Boolean) modelAndView.getModel().get("repositoryHasPullRequests"));
    }

    @Test
    public void testListPullRequestsFilterByAuthor() {
        PullRequest pullRequest = mock(PullRequest.class, Answers.RETURNS_DEEP_STUBS.get());
        ApplicationUser author = new TestApplicationUser("author");
        doReturn(ImmutableSet.of(author)).when(userService).getUsersByName(eq(ImmutableSet.of("author")));

        PageImpl<PullRequest> page = new PageImpl<>(new PageRequestImpl(0, 100), Sets.newHashSet(pullRequest), true);
        when(pullRequestService.search(any(PullRequestSearchRequest.class), any(PageRequest.class))).thenReturn(page);

        ModelAndView modelAndView = controller.listPullRequestsView(PROJECT_KEY, REPO_SLUG, PullRequestState.OPEN.name(),
                PullRequestDirection.INCOMING.name(), BRANCH1, 0, 100, PullRequestOrder.NEWEST.name(), "author", false);

        ArgumentCaptor<PullRequestSearchRequest> searchRequestCaptor = ArgumentCaptor.forClass(PullRequestSearchRequest.class);
        verify(pullRequestService).search(searchRequestCaptor.capture(), any(PageRequest.class));

        PullRequestSearchRequest searchRequest = searchRequestCaptor.getValue();
        assertEquals(new Integer(repo.getId()), searchRequest.getToRepositoryId());
        assertEquals(PullRequestState.OPEN, searchRequest.getState());
        assertEquals(ImmutableSet.of(BRANCH1), searchRequest.getToRefIds());
        assertEquals(PullRequestOrder.NEWEST, searchRequest.getOrder());
        assertEquals(1, searchRequest.getParticipants().size());

        ImmutableMap<PullRequestRole, PullRequestParticipantRequest> byRole =
                Maps.uniqueIndex(searchRequest.getParticipants(), PullRequestParticipantRequest::getRole);

        assertThat(byRole.get(PullRequestRole.AUTHOR),
                new ReflectionEquals(new PullRequestParticipantRequest.Builder("author").role(PullRequestRole.AUTHOR).build()));

        assertEquals("The PR list view should be shown", modelAndView.getViewName(), PullRequestController.LIST_VIEW);
        assertNotNull("A repository should be available in the model", modelAndView.getModel().get(REPOSITORY_KEY));
        assertSame("The PR page should be available in the model", page, modelAndView.getModel().get("pullRequestPage"));
        assertTrue("The repositoryHasPullRequests flag should be true", (Boolean) modelAndView.getModel().get("repositoryHasPullRequests"));

        // The author should be retrieved from a PR, not the service
        verifyZeroInteractions(userService);
        assertSame("The author of the PRs should be available in the model", pullRequest.getAuthor().getUser(),
                modelAndView.getModel().get("selectedAuthor"));
    }

    @Test
    public void testListPullRequestsFilterByAuthorIsEmpty() {
        ApplicationUser author = new TestApplicationUser("author");
        when(userService.getUserByName("author")).thenReturn(author);

        PageImpl<PullRequest> page = new PageImpl<>(new PageRequestImpl(0, 100), Collections.emptySet(), true);
        when(pullRequestService.search(any(PullRequestSearchRequest.class), any(PageRequest.class))).thenReturn(page);
        when(pullRequestService.count(any(PullRequestSearchRequest.class))).thenReturn(1L);

        ModelAndView modelAndView = controller.listPullRequestsView(PROJECT_KEY, REPO_SLUG, PullRequestState.OPEN.name(),
                PullRequestDirection.INCOMING.name(), BRANCH1, 0, 100, PullRequestOrder.NEWEST.name(), "author", false);

        ArgumentCaptor<PullRequestSearchRequest> searchRequestCaptor = ArgumentCaptor.forClass(PullRequestSearchRequest.class);
        verify(pullRequestService).search(searchRequestCaptor.capture(), any(PageRequest.class));
        verify(pullRequestService).count(refEq(new PullRequestSearchRequest.Builder().toRepositoryId(repo.getId()).build()));

        PullRequestSearchRequest searchRequest = searchRequestCaptor.getValue();
        assertNotNull(searchRequest.getToRepositoryId());
        assertEquals(repo.getId(), (int) searchRequest.getToRepositoryId());
        assertEquals(PullRequestState.OPEN, searchRequest.getState());
        assertEquals(ImmutableSet.of(BRANCH1), searchRequest.getToRefIds());
        assertEquals(PullRequestOrder.NEWEST, searchRequest.getOrder());
        assertEquals(1, searchRequest.getParticipants().size());

        ImmutableMap<PullRequestRole, PullRequestParticipantRequest> byRole =
                Maps.uniqueIndex(searchRequest.getParticipants(), PullRequestParticipantRequest::getRole);

        assertThat(byRole.get(PullRequestRole.AUTHOR),
                new ReflectionEquals(new PullRequestParticipantRequest.Builder("author").role(PullRequestRole.AUTHOR).build()));

        assertEquals("The PR list view should be shown", modelAndView.getViewName(), PullRequestController.LIST_VIEW);
        assertNotNull("A repository should be available in the model", modelAndView.getModel().get(REPOSITORY_KEY));
        assertSame("The PR page should be available in the model", page, modelAndView.getModel().get("pullRequestPage"));
        assertTrue("The repositoryHasPullRequests flag should be true", (Boolean) modelAndView.getModel().get("repositoryHasPullRequests"));

        // When there are no PRs in the results we have no choice but to fall back on the service
        verify(userService).getUserByName("author");
        assertSame("The author of the PRs should be available in the model", author, modelAndView.getModel().get("selectedAuthor"));
    }

    @Test
    public void testListPullRequestsFilterByNonExistingAuthor() {
        doReturn(null).when(userService).getUserByName("author");

        PageImpl<PullRequest> page = new PageImpl<>(new PageRequestImpl(0, 100), Collections.emptySet(), true);
        when(pullRequestService.search(any(PullRequestSearchRequest.class), any(PageRequest.class))).thenReturn(page);
        when(pullRequestService.count(any(PullRequestSearchRequest.class))).thenReturn(1L);

        ModelAndView modelAndView = controller.listPullRequestsView(PROJECT_KEY, REPO_SLUG, PullRequestState.OPEN.name(),
                PullRequestDirection.INCOMING.name(), BRANCH1, 0, 100, PullRequestOrder.NEWEST.name(), "author", false);

        ArgumentCaptor<PullRequestSearchRequest> searchRequestCaptor = ArgumentCaptor.forClass(PullRequestSearchRequest.class);
        verify(pullRequestService).search(searchRequestCaptor.capture(), any(PageRequest.class));
        verify(pullRequestService).count(refEq(new PullRequestSearchRequest.Builder().toRepositoryId(repo.getId()).build()));

        PullRequestSearchRequest searchRequest = searchRequestCaptor.getValue();
        assertNotNull(searchRequest.getToRepositoryId());
        assertEquals(repo.getId(), (int) searchRequest.getToRepositoryId());
        assertEquals(PullRequestState.OPEN, searchRequest.getState());
        assertEquals(ImmutableSet.of(BRANCH1), searchRequest.getToRefIds());
        assertEquals(PullRequestOrder.NEWEST, searchRequest.getOrder());
        assertEquals(1, searchRequest.getParticipants().size());

        ImmutableMap<PullRequestRole, PullRequestParticipantRequest> byRole =
                Maps.uniqueIndex(searchRequest.getParticipants(), PullRequestParticipantRequest::getRole);

        assertThat(byRole.get(PullRequestRole.AUTHOR),
                new ReflectionEquals(new PullRequestParticipantRequest.Builder("author").role(PullRequestRole.AUTHOR).build()));

        assertEquals("The PR list view should be shown", modelAndView.getViewName(), PullRequestController.LIST_VIEW);
        assertNotNull("A repository should be available in the model", modelAndView.getModel().get(REPOSITORY_KEY));
        assertSame("The PR page should be available in the model", page, modelAndView.getModel().get("pullRequestPage"));
        assertTrue("The repositoryHasPullRequests flag should be true", (Boolean) modelAndView.getModel().get("repositoryHasPullRequests"));

        verify(userService).getUserByName("author");
        assertNull("There should not be a user to match the 'author' username", modelAndView.getModel().get("selectedAuthor"));
    }

    @Test
    public void testListPullRequestsFilterByReviewing() {
        ApplicationUser reviewer = new TestApplicationUser("reviewer");
        doReturn(reviewer).when(authenticationContext).getCurrentUser();
        doReturn(ImmutableSet.of(reviewer)).when(userService).getUsersByName(eq(ImmutableSet.of("reviewer")));

        PageImpl<PullRequest> page = new PageImpl<>(new PageRequestImpl(0, 100), Collections.emptySet(), true);
        when(pullRequestService.search(any(PullRequestSearchRequest.class), any(PageRequest.class))).thenReturn(page);
        when(pullRequestService.count(any(PullRequestSearchRequest.class))).thenReturn(1L);

        ModelAndView modelAndView = controller.listPullRequestsView(PROJECT_KEY, REPO_SLUG, PullRequestState.OPEN.name(),
                PullRequestDirection.INCOMING.name(), BRANCH1, 0, 100, PullRequestOrder.NEWEST.name(), null, true);

        ArgumentCaptor<PullRequestSearchRequest> searchRequestCaptor = ArgumentCaptor.forClass(PullRequestSearchRequest.class);
        verify(pullRequestService).search(searchRequestCaptor.capture(), any(PageRequest.class));
        verify(pullRequestService).count(refEq(new PullRequestSearchRequest.Builder().toRepositoryId(repo.getId()).build()));

        PullRequestSearchRequest searchRequest = searchRequestCaptor.getValue();
        assertEquals(new Integer(repo.getId()), searchRequest.getToRepositoryId());
        assertEquals(PullRequestState.OPEN, searchRequest.getState());
        assertEquals(ImmutableSet.of(BRANCH1), searchRequest.getToRefIds());
        assertEquals(PullRequestOrder.NEWEST, searchRequest.getOrder());
        assertEquals(1, searchRequest.getParticipants().size());

        ImmutableMap<PullRequestRole, PullRequestParticipantRequest> byRole =
                Maps.uniqueIndex(searchRequest.getParticipants(), PullRequestParticipantRequest::getRole);

        assertThat(byRole.get(PullRequestRole.REVIEWER),
                new ReflectionEquals(new PullRequestParticipantRequest.Builder("reviewer").role(PullRequestRole.REVIEWER).build()));

        assertEquals("The PR list view should be shown", modelAndView.getViewName(), PullRequestController.LIST_VIEW);
        assertNotNull("A repository should be available in the model", modelAndView.getModel().get(REPOSITORY_KEY));
        assertSame("The PR page should be available in the model", page, modelAndView.getModel().get("pullRequestPage"));
        assertTrue("The repositoryHasPullRequests flag should be true", (Boolean) modelAndView.getModel().get("repositoryHasPullRequests"));
    }

    @Test
    public void testListPullRequestsFilterByAuthorAndReviewing() {
        ApplicationUser author = new TestApplicationUser("author");
        ApplicationUser reviewer = new TestApplicationUser("reviewer");
        doReturn(reviewer).when(authenticationContext).getCurrentUser();
        doReturn(ImmutableSet.of(author, reviewer)).when(userService).getUsersByName(eq(ImmutableSet.of("author", "reviewer")));

        PageImpl<PullRequest> page = new PageImpl<>(new PageRequestImpl(0, 100), Collections.emptySet(), true);
        when(pullRequestService.search(any(PullRequestSearchRequest.class), any(PageRequest.class))).thenReturn(page);
        when(pullRequestService.count(any(PullRequestSearchRequest.class))).thenReturn(1L);

        ModelAndView modelAndView = controller.listPullRequestsView(PROJECT_KEY, REPO_SLUG, PullRequestState.OPEN.name(),
                PullRequestDirection.INCOMING.name(), BRANCH1, 0, 100, PullRequestOrder.NEWEST.name(), "author", true);

        ArgumentCaptor<PullRequestSearchRequest> searchRequestCaptor = ArgumentCaptor.forClass(PullRequestSearchRequest.class);
        verify(pullRequestService).search(searchRequestCaptor.capture(), any(PageRequest.class));
        verify(pullRequestService).count(refEq(new PullRequestSearchRequest.Builder().toRepositoryId(repo.getId()).build()));

        PullRequestSearchRequest searchRequest = searchRequestCaptor.getValue();
        assertEquals(new Integer(repo.getId()), searchRequest.getToRepositoryId());
        assertEquals(PullRequestState.OPEN, searchRequest.getState());
        assertEquals(ImmutableSet.of(BRANCH1), searchRequest.getToRefIds());
        assertEquals(PullRequestOrder.NEWEST, searchRequest.getOrder());
        assertEquals(2, searchRequest.getParticipants().size());

        ImmutableMap<PullRequestRole, PullRequestParticipantRequest> byRole =
                Maps.uniqueIndex(searchRequest.getParticipants(), PullRequestParticipantRequest::getRole);

        assertThat(byRole.get(PullRequestRole.AUTHOR),
                new ReflectionEquals(new PullRequestParticipantRequest.Builder("author").role(PullRequestRole.AUTHOR).build()));
        assertThat(byRole.get(PullRequestRole.REVIEWER),
                new ReflectionEquals(new PullRequestParticipantRequest.Builder("reviewer").role(PullRequestRole.REVIEWER).build()));

        assertEquals("The PR list view should be shown", modelAndView.getViewName(), PullRequestController.LIST_VIEW);
        assertNotNull("A repository should be available in the model", modelAndView.getModel().get(REPOSITORY_KEY));
        assertSame("The PR page should be available in the model", page, modelAndView.getModel().get("pullRequestPage"));
        assertTrue("The repositoryHasPullRequests flag should be true", (Boolean) modelAndView.getModel().get("repositoryHasPullRequests"));
    }

    @Test
    public void testListPullRequestsFilterByBranch() {
        PullRequest pullRequest = mock(PullRequest.class, Answers.RETURNS_DEEP_STUBS.get());
        PageImpl<PullRequest> page = new PageImpl<>(new PageRequestImpl(0, 100), Sets.newHashSet(pullRequest), true);
        when(pullRequestService.search(any(PullRequestSearchRequest.class), any(PageRequest.class))).thenReturn(page);

        ModelAndView modelAndView = controller.listPullRequestsView(PROJECT_KEY, REPO_SLUG, PullRequestState.OPEN.name(),
                PullRequestDirection.INCOMING.name(), BRANCH1, 0, 100, PullRequestOrder.NEWEST.name(), "author", false);

        ArgumentCaptor<PullRequestSearchRequest> searchRequestCaptor = ArgumentCaptor.forClass(PullRequestSearchRequest.class);
        verify(pullRequestService).search(searchRequestCaptor.capture(), any(PageRequest.class));

        PullRequestSearchRequest searchRequest = searchRequestCaptor.getValue();
        assertNotNull(searchRequest.getToRepositoryId());
        assertEquals(repo.getId(), (int) searchRequest.getToRepositoryId());
        assertEquals(PullRequestState.OPEN, searchRequest.getState());
        assertEquals(ImmutableSet.of(BRANCH1), searchRequest.getToRefIds());
        assertEquals(PullRequestOrder.NEWEST, searchRequest.getOrder());
        assertEquals(1, searchRequest.getParticipants().size());

        ImmutableMap<PullRequestRole, PullRequestParticipantRequest> byRole =
                Maps.uniqueIndex(searchRequest.getParticipants(), PullRequestParticipantRequest::getRole);

        assertThat(byRole.get(PullRequestRole.AUTHOR),
                new ReflectionEquals(new PullRequestParticipantRequest.Builder("author").role(PullRequestRole.AUTHOR).build()));

        assertEquals("The PR list view should be shown", modelAndView.getViewName(), PullRequestController.LIST_VIEW);
        assertNotNull("A repository should be available in the model", modelAndView.getModel().get(REPOSITORY_KEY));
        assertSame("The PR page should be available in the model", page, modelAndView.getModel().get("pullRequestPage"));
        assertTrue("The repositoryHasPullRequests flag should be true", (Boolean) modelAndView.getModel().get("repositoryHasPullRequests"));

        // The target branch should be retrieved from a PR, not the service
        verifyZeroInteractions(refService);
        assertSame("The target branch for the PRs should be available in the model",
                pullRequest.getToRef(), modelAndView.getModel().get("selectedTargetBranch"));
    }

    @Test
    public void testListPullRequestsFilterByBranchIsEmpty() {
        Ref targetBranch = mock(Ref.class);
        when(refService.resolveRef(argThat(resolveRefRequest(repo, BRANCH1)))).thenReturn(targetBranch);

        PageImpl<PullRequest> page = new PageImpl<>(new PageRequestImpl(0, 100), Collections.emptySet(), true);
        when(pullRequestService.search(any(PullRequestSearchRequest.class), any(PageRequest.class))).thenReturn(page);
        when(pullRequestService.count(any(PullRequestSearchRequest.class))).thenReturn(1L);

        ModelAndView modelAndView = controller.listPullRequestsView(PROJECT_KEY, REPO_SLUG, PullRequestState.OPEN.name(),
                PullRequestDirection.INCOMING.name(), BRANCH1, 0, 100, PullRequestOrder.NEWEST.name(), "author", false);

        ArgumentCaptor<PullRequestSearchRequest> searchRequestCaptor = ArgumentCaptor.forClass(PullRequestSearchRequest.class);
        verify(pullRequestService).search(searchRequestCaptor.capture(), any(PageRequest.class));
        verify(pullRequestService).count(refEq(new PullRequestSearchRequest.Builder().toRepositoryId(repo.getId()).build()));

        PullRequestSearchRequest searchRequest = searchRequestCaptor.getValue();
        assertNotNull(searchRequest.getToRepositoryId());
        assertEquals(repo.getId(), (int) searchRequest.getToRepositoryId());
        assertEquals(PullRequestState.OPEN, searchRequest.getState());
        assertEquals(ImmutableSet.of(BRANCH1), searchRequest.getToRefIds());
        assertEquals(PullRequestOrder.NEWEST, searchRequest.getOrder());
        assertEquals(1, searchRequest.getParticipants().size());

        ImmutableMap<PullRequestRole, PullRequestParticipantRequest> byRole =
                Maps.uniqueIndex(searchRequest.getParticipants(), PullRequestParticipantRequest::getRole);

        assertThat(byRole.get(PullRequestRole.AUTHOR),
                new ReflectionEquals(new PullRequestParticipantRequest.Builder("author").role(PullRequestRole.AUTHOR).build()));

        assertEquals("The PR list view should be shown", modelAndView.getViewName(), PullRequestController.LIST_VIEW);
        assertNotNull("A repository should be available in the model", modelAndView.getModel().get(REPOSITORY_KEY));
        assertSame("The PR page should be available in the model", page, modelAndView.getModel().get("pullRequestPage"));
        assertTrue("The repositoryHasPullRequests flag should be true", (Boolean) modelAndView.getModel().get("repositoryHasPullRequests"));

        // When there are no PRs in the results we have no choice but to fall back on the service
        verify(refService).resolveRef(argThat(resolveRefRequest(repo, BRANCH1)));
        assertSame("The target branch for the PRs should be available in the model", targetBranch, modelAndView.getModel().get("selectedTargetBranch"));
    }

    @Test
    public void testListPullRequestsFilterByUnresolvableBranch() {
        when(refService.resolveRef(argThat(resolveRefRequest(repo, BRANCH1)))).thenReturn(null);

        PageImpl<PullRequest> page = new PageImpl<>(new PageRequestImpl(0, 100), Collections.emptySet(), true);
        when(pullRequestService.search(any(PullRequestSearchRequest.class), any(PageRequest.class))).thenReturn(page);
        when(pullRequestService.count(any(PullRequestSearchRequest.class))).thenReturn(1L);

        ModelAndView modelAndView = controller.listPullRequestsView(PROJECT_KEY, REPO_SLUG, PullRequestState.OPEN.name(),
                PullRequestDirection.INCOMING.name(), BRANCH1, 0, 100, PullRequestOrder.NEWEST.name(), "author", false);

        ArgumentCaptor<PullRequestSearchRequest> searchRequestCaptor = ArgumentCaptor.forClass(PullRequestSearchRequest.class);
        verify(pullRequestService).search(searchRequestCaptor.capture(), any(PageRequest.class));
        verify(pullRequestService).count(refEq(new PullRequestSearchRequest.Builder().toRepositoryId(repo.getId()).build()));

        PullRequestSearchRequest searchRequest = searchRequestCaptor.getValue();
        assertNotNull(searchRequest.getToRepositoryId());
        assertEquals(repo.getId(), (int) searchRequest.getToRepositoryId());
        assertEquals(PullRequestState.OPEN, searchRequest.getState());
        assertEquals(ImmutableSet.of(BRANCH1), searchRequest.getToRefIds());
        assertEquals(PullRequestOrder.NEWEST, searchRequest.getOrder());
        assertEquals(1, searchRequest.getParticipants().size());

        ImmutableMap<PullRequestRole, PullRequestParticipantRequest> byRole =
                Maps.uniqueIndex(searchRequest.getParticipants(), PullRequestParticipantRequest::getRole);

        assertThat(byRole.get(PullRequestRole.AUTHOR),
                new ReflectionEquals(new PullRequestParticipantRequest.Builder("author").role(PullRequestRole.AUTHOR).build()));

        assertEquals("The PR list view should be shown", modelAndView.getViewName(), PullRequestController.LIST_VIEW);
        assertNotNull("A repository should be available in the model", modelAndView.getModel().get(REPOSITORY_KEY));
        assertSame("The PR page should be available in the model", page, modelAndView.getModel().get("pullRequestPage"));
        assertTrue("The repositoryHasPullRequests flag should be true", (Boolean) modelAndView.getModel().get("repositoryHasPullRequests"));

        // When there are no PRs in the results we have no choice but to fall back on the service
        verify(refService).resolveRef(argThat(resolveRefRequest(repo, BRANCH1)));
        assertNull("The target branch for the PRs should not be available in the model", modelAndView.getModel().get("selectedTargetBranch"));
    }

    @Test
    public void testListPullRequestsNoPullRequestsInRepository() {
        PageImpl<PullRequest> page = new PageImpl<>(new PageRequestImpl(0, 100), Collections.emptySet(), true);
        when(pullRequestService.search(any(PullRequestSearchRequest.class), any(PageRequest.class))).thenReturn(page);
        when(pullRequestService.count(any(PullRequestSearchRequest.class))).thenReturn(0L);

        ModelAndView modelAndView = controller.listPullRequestsView(PROJECT_KEY, REPO_SLUG, PullRequestState.OPEN.name(),
                PullRequestDirection.INCOMING.name(), BRANCH1, 0, 100, PullRequestOrder.NEWEST.name(), null, false);

        verify(pullRequestService).search(
                refEq(new PullRequestSearchRequest.Builder().toRepositoryId(repo.getId()).state(PullRequestState.OPEN)
                        .toRefId(BRANCH1).order(PullRequestOrder.NEWEST).build()),
                any(PageRequest.class));
        verify(pullRequestService).count(refEq(new PullRequestSearchRequest.Builder().toRepositoryId(repo.getId()).build()));

        assertEquals("The PR list view should be shown", modelAndView.getViewName(), PullRequestController.LIST_VIEW);
        assertNotNull("A repository should be available in the model", modelAndView.getModel().get(REPOSITORY_KEY));
        assertSame("The PR page should be available in the model", page, modelAndView.getModel().get("pullRequestPage"));
        assertFalse("The repositoryHasPullRequests flag should be false", (Boolean) modelAndView.getModel().get("repositoryHasPullRequests"));
    }

    @Test(expected = NoSuchEntityException.class)
    public void testListPullRequestsWhenNoSuchProject() {
        whenProjectFindByKeyThrowNoSuchEntity();

        controller.listPullRequestsView(PROJECT_KEY, REPO_SLUG, PullRequestState.OPEN.name(), PullRequestDirection.INCOMING.name(),
                BRANCH1, 0, 100, PullRequestOrder.NEWEST.name(), null, false);
    }

    @Test(expected = NoSuchEntityException.class)
    public void testListPullRequestsWhenNoSuchRepository() {
        whenRepoFindBySlugThrowNoSuchEntity();

        controller.listPullRequestsView(PROJECT_KEY, REPO_SLUG, PullRequestState.OPEN.name(), PullRequestDirection.INCOMING.name(),
                BRANCH1, 0, 100, PullRequestOrder.NEWEST.name(), null, false);
    }

    @Test
    public void testPostingToCreatePullRequest() {
        when(repositoryService.isEmpty(same(repo))).thenReturn(false);
        when(refService.getDefaultBranch(same(repo))).thenReturn(branch);

        InternalPullRequest pullRequest = newPullRequestWithAuthor();

        when(pullRequestService.create(eq(PULL_REQUEST_TITLE), eq(PULL_REQUEST_DESCRIPTION), anySetOf(String.class), same(fromRepo), eq(BRANCH1), same(repo), eq(BRANCH2))).thenReturn(pullRequest);

        ModelAndView modelAndView = controller.createPullRequest(webRequest, PROJECT_KEY, REPO_SLUG, new PullRequestForm(FROM_REPO_ID, BRANCH1, BRANCH2, PULL_REQUEST_TITLE, PULL_REQUEST_DESCRIPTION, ""), errors);

        verify(pullRequestService).create(eq(PULL_REQUEST_TITLE), eq(PULL_REQUEST_DESCRIPTION), anySetOf(String.class), same(fromRepo), eq(BRANCH1), same(repo), eq(BRANCH2));

        View view = modelAndView.getView();
        assertTrue("There should be a redirect after a successful POST to create a PR", view instanceof RedirectView);
        assertEquals("The redirect should be to the url for the PR", PULL_REQUEST_FAKE_URL, ((RedirectView) view).getUrl());
    }

    @Test
    public void testPostingToCreatePullRequestWhenContraintValidationErrors() {
        when(pullRequestService.create(anyString(), anyString(), anySetOf(String.class), any(Repository.class), anyString(), any(Repository.class), anyString()))
                .thenThrow(new ConstraintViolationException("", Collections.emptySet()));

        ModelAndView modelAndView = controller.createPullRequest(webRequest, PROJECT_KEY, REPO_SLUG, new PullRequestForm(FROM_REPO_ID, BRANCH1, BRANCH2, PULL_REQUEST_TITLE, PULL_REQUEST_DESCRIPTION, ""), errors);

        checkCreateViewShownWithErrors(modelAndView);
    }

    @Test(expected = NoSuchEntityException.class)
    public void testPostingToCreatePullRequestWhenNoSuchProject() {
        whenProjectFindByKeyThrowNoSuchEntity();

        controller.createPullRequest(webRequest, PROJECT_KEY, REPO_SLUG, new PullRequestForm(FROM_REPO_ID, BRANCH1, BRANCH2, PULL_REQUEST_TITLE, PULL_REQUEST_DESCRIPTION, ""), errors);
    }

    @Test(expected = NoSuchEntityException.class)
    public void testPostingToCreatePullRequestWhenNoSuchRepository() {
        whenRepoFindBySlugThrowNoSuchEntity();

        controller.createPullRequest(webRequest, PROJECT_KEY, REPO_SLUG, new PullRequestForm(FROM_REPO_ID, BRANCH1, BRANCH2, PULL_REQUEST_TITLE, PULL_REQUEST_DESCRIPTION, ""), errors);
    }

    @Test
    public void testPostingToCreatePullRequestWhenRepoEmpty() {
        when(repositoryService.isEmpty(same(repo))).thenReturn(true);
        when(refService.getDefaultBranch(same(repo))).thenReturn(branch);

        ModelAndView modelAndView = controller.createPullRequest(webRequest, PROJECT_KEY, REPO_SLUG, new PullRequestForm(FROM_REPO_ID, BRANCH1, BRANCH2, PULL_REQUEST_TITLE, PULL_REQUEST_DESCRIPTION, ""), errors);

        verify(pullRequestService, never()).create(anyString(), anyString(), anySetOf(String.class), any(Repository.class), anyString(), any(Repository.class), anyString());

        assertEquals("The empty repo view should be shown", PullRequestController.EMPTY_REPO_VIEW, modelAndView.getViewName());
        assertSame("The repository should be available in the model", repo, modelAndView.getModel().get(REPOSITORY_KEY));
        assertFalse("There should be an indication that the repo is ready", (Boolean) modelAndView.getModel().get(NOT_INITIALISED_KEY));
    }

    @Test
    public void testPostingToCreatePullRequestWhenRepoInitialising() {
        repo = new InternalRepository.Builder(repo).state(Repository.State.INITIALISING).build();

        when(repositoryService.getBySlug(eq(PROJECT_KEY), eq(REPO_SLUG))).thenReturn(repo);
        when(repositoryService.isEmpty(same(repo))).thenReturn(false);
        when(refService.getDefaultBranch(same(repo))).thenReturn(branch);

        ModelAndView modelAndView = controller.createPullRequest(webRequest, PROJECT_KEY, REPO_SLUG, new PullRequestForm(FROM_REPO_ID, BRANCH1, BRANCH2, PULL_REQUEST_TITLE, PULL_REQUEST_DESCRIPTION, ""), errors);

        verify(pullRequestService, never()).create(anyString(), anyString(), anySetOf(String.class), any(Repository.class), anyString(), any(Repository.class), anyString());

        assertEquals("The empty repo view should be shown", PullRequestController.EMPTY_REPO_VIEW, modelAndView.getViewName());
        assertSame("The repository should be available in the model", repo, modelAndView.getModel().get(REPOSITORY_KEY));
        assertTrue("There should be an indication that the repo is not ready", (Boolean) modelAndView.getModel().get(NOT_INITIALISED_KEY));
    }

    @Test
    public void testPostingToCreatePullRequestWhenServiceException() {
        when(pullRequestService.create(anyString(), anyString(), anySetOf(String.class), any(Repository.class), anyString(), any(Repository.class), anyString()))
                .thenThrow(new IllegalPullRequestStateException(new KeyedMessage("", "", "")));

        ModelAndView modelAndView = controller.createPullRequest(webRequest, PROJECT_KEY, REPO_SLUG, new PullRequestForm(FROM_REPO_ID, BRANCH1, BRANCH2, PULL_REQUEST_TITLE, PULL_REQUEST_DESCRIPTION, ""), errors);

        checkCreateViewShownWithErrors(modelAndView);
    }

    @Test
    public void testPostingToCreatePullRequestWhenValidationErrors() {
        when(errors.hasErrors()).thenReturn(true);

        ModelAndView modelAndView = controller.createPullRequest(webRequest, PROJECT_KEY, REPO_SLUG, new PullRequestForm(FROM_REPO_ID, BRANCH1, BRANCH2, PULL_REQUEST_TITLE, PULL_REQUEST_DESCRIPTION, ""), errors);

        checkCreateViewShownWithErrors(modelAndView);
    }

    @Test(expected = NoSuchEntityException.class)
    public void testViewPullRequestDiffWhenNoSuchProject() throws SoyException {
        whenProjectFindByKeyThrowNoSuchEntity();
        controller.viewPullRequestDiff(PROJECT_KEY, REPO_SLUG, 0L);
    }

    @Test
    public void testViewPullRequestOverview() throws SoyException {
        InternalPullRequest pullRequest = newPullRequestWithAuthor();

        when(pullRequestService.getById(eq(0), eq(0L))).thenReturn(pullRequest);

        ModelAndView modelAndView = controller.viewPullRequestOverview(PROJECT_KEY, REPO_SLUG, 0L);

        assertEquals("The PR view should be shown", PullRequestController.VIEW, modelAndView.getViewName());
        assertNotNull("A repository should be available in the model", modelAndView.getModel().get(REPOSITORY_KEY));
        assertSame("The PR should be available in the model", pullRequest, modelAndView.getModel().get(PULL_REQUEST_KEY));
    }

    @Test
    public void testUnwatch() throws SoyException {
        when(pullRequestService.unwatch(eq(0), eq(0L))).thenReturn(true);

        RedirectAttributes redirectAttributes = mock(RedirectAttributes.class);
        ModelAndView modelAndView = controller.unwatch(PROJECT_KEY, REPO_SLUG, 0L, redirectAttributes);

        assertTrue("There should be a redirect to the activity URL when hitting a PR URL", modelAndView.getView() instanceof RedirectView);
        assertEquals("The redirect should be to the url for the PR's activity", PULL_REQUEST_OVERVIEW_FAKE_URL, ((RedirectView) modelAndView.getView()).getUrl());
        verify(redirectAttributes).addFlashAttribute(eq("unwatched"), eq(Boolean.TRUE));
    }

    @Test(expected = NoSuchEntityException.class)
    public void testViewPullRequestOverviewWhenNoSuchProject() throws SoyException {
        whenProjectFindByKeyThrowNoSuchEntity();

        controller.viewPullRequestOverview(PROJECT_KEY, REPO_SLUG, 0L);
    }

    @Test(expected = NoSuchEntityException.class)
    public void testViewPullRequestOverviewWhenNoSuchPullRequest() throws SoyException {
        whenPullRequestFindByRepositoryAndIdThrowNoSuchEntity();

        controller.viewPullRequestOverview(PROJECT_KEY, REPO_SLUG, 0L);
    }

    @Test(expected = NoSuchEntityException.class)
    public void testViewPullRequestOverviewWhenNoSuchRepository() throws SoyException {
        whenRepoFindBySlugThrowNoSuchEntity();

        controller.viewPullRequestOverview(PROJECT_KEY, REPO_SLUG, 0L);
    }


    @Test
    public void testViewPullRequestCommits() throws SoyException {
        InternalPullRequest pullRequest = newPullRequestWithAuthor();

        when(pullRequestService.getById(eq(0), eq(0L))).thenReturn(pullRequest);

        ModelAndView modelAndView = controller.viewPullRequestCommits(PROJECT_KEY, REPO_SLUG, 0L, 0, 100, false);
        assertEquals("The PR view view should be shown", PullRequestController.VIEW, modelAndView.getViewName());
        assertNotNull("A repository should be available in the model", modelAndView.getModel().get(REPOSITORY_KEY));
        assertSame("The PR should be available in the model", pullRequest, modelAndView.getModel().get(PULL_REQUEST_KEY));
        assertDiscover(modelAndView, EnumSet.noneOf(DiscoverableFeature.class));
    }

    @Test
    public void testViewPullRequestCommitsIfUserHasNotSeenCommitReviewFeatureDiscovery() throws SoyException {
        when(discoveryService.getIfUndiscovered(EnumSet.of(DiscoverableFeature.NEEDS_WORK, DiscoverableFeature.COMMIT_REVIEW)))
                .thenReturn(EnumSet.of(DiscoverableFeature.COMMIT_REVIEW));
        InternalPullRequest pullRequest = newPullRequestWithAuthor();

        when(pullRequestService.getById(eq(0), eq(0L))).thenReturn(pullRequest);

        ModelAndView modelAndView = controller.viewPullRequestCommits(PROJECT_KEY, REPO_SLUG, 0L, 0, 100, false);
        assertEquals("The PR view view should be shown", PullRequestController.VIEW, modelAndView.getViewName());
        assertNotNull("A repository should be available in the model", modelAndView.getModel().get(REPOSITORY_KEY));
        assertSame("The PR should be available in the model", pullRequest, modelAndView.getModel().get(PULL_REQUEST_KEY));

        assertDiscover(modelAndView, EnumSet.of(DiscoverableFeature.COMMIT_REVIEW));
    }

    @Test
    public void testViewPullRequestCommitsIfUserHasNotSeenNeedsWorkFeatureDiscovery() throws SoyException {
        when(discoveryService.getIfUndiscovered(EnumSet.of(DiscoverableFeature.NEEDS_WORK, DiscoverableFeature.COMMIT_REVIEW)))
                .thenReturn(EnumSet.of(DiscoverableFeature.NEEDS_WORK));
        InternalPullRequest pullRequest = newPullRequestWithAuthor();

        when(pullRequestService.getById(eq(0), eq(0L))).thenReturn(pullRequest);

        ModelAndView modelAndView = controller.viewPullRequestCommits(PROJECT_KEY, REPO_SLUG, 0L, 0, 100, false);
        assertEquals("The PR view view should be shown", PullRequestController.VIEW, modelAndView.getViewName());
        assertNotNull("A repository should be available in the model", modelAndView.getModel().get(REPOSITORY_KEY));
        assertSame("The PR should be available in the model", pullRequest, modelAndView.getModel().get(PULL_REQUEST_KEY));

        assertDiscover(modelAndView, EnumSet.of(DiscoverableFeature.NEEDS_WORK));
    }

    @Test
    public void testViewPullRequestCommitsIfUserHasNotSeenCommitReviewAndNeedsWorkFeatureDiscovery() throws SoyException {
        when(discoveryService.getIfUndiscovered(EnumSet.of(DiscoverableFeature.NEEDS_WORK, DiscoverableFeature.COMMIT_REVIEW)))
                .thenReturn(EnumSet.of(DiscoverableFeature.NEEDS_WORK, DiscoverableFeature.COMMIT_REVIEW));
        InternalPullRequest pullRequest = newPullRequestWithAuthor();

        when(pullRequestService.getById(eq(0), eq(0L))).thenReturn(pullRequest);

        ModelAndView modelAndView = controller.viewPullRequestCommits(PROJECT_KEY, REPO_SLUG, 0L, 0, 100, false);
        assertEquals("The PR view view should be shown", PullRequestController.VIEW, modelAndView.getViewName());
        assertNotNull("A repository should be available in the model", modelAndView.getModel().get(REPOSITORY_KEY));
        assertSame("The PR should be available in the model", pullRequest, modelAndView.getModel().get(PULL_REQUEST_KEY));

        assertDiscover(modelAndView, EnumSet.of(DiscoverableFeature.COMMIT_REVIEW,
                DiscoverableFeature.NEEDS_WORK));
    }

    @Test(expected = NoSuchEntityException.class)
    public void testViewPullRequestCommitsWhenNoSuchProject() throws SoyException {
        whenProjectFindByKeyThrowNoSuchEntity();

        controller.viewPullRequestDiff(PROJECT_KEY, REPO_SLUG, 0L);
    }

    @Test(expected = NoSuchEntityException.class)
    public void testViewPullRequestCommitsWhenNoSuchPullRequest() throws SoyException {
        whenPullRequestFindByRepositoryAndIdThrowNoSuchEntity();

        controller.viewPullRequestDiff(PROJECT_KEY, REPO_SLUG, 0L);
    }

    @Test(expected = NoSuchEntityException.class)
    public void testViewPullRequestCommitsWhenNoSuchRepository() throws SoyException {
        whenRepoFindBySlugThrowNoSuchEntity();

        controller.viewPullRequestDiff(PROJECT_KEY, REPO_SLUG, 0L);
    }

    @Test
    public void testViewPullRequestCommit() throws SoyException {
        SimpleCommit commit = mock(SimpleCommit.class);
        InternalPullRequest pullRequest = newPullRequestWithAuthor();

        when(commitService.getCommit(any(CommitRequest.class))).thenReturn(commit);
        when(pullRequestService.getById(eq(0), eq(0L))).thenReturn(pullRequest);
        ModelAndView modelAndView = controller.viewPullRequestCommitOrCommitRange(PROJECT_KEY, REPO_SLUG, 0L, "commit", "");

        assertEquals("The PR view view should be shown", PullRequestController.VIEW, modelAndView.getViewName());
        assertNotNull("A repository should be available in the model", modelAndView.getModel().get(REPOSITORY_KEY));
        assertSame("The PR should be available in the model", pullRequest, modelAndView.getModel().get(PULL_REQUEST_KEY));
        assertEquals("The commit should be present in the model", commit, modelAndView.getModel().get(COMMIT_KEY));
        assertDiscover(modelAndView, EnumSet.noneOf(DiscoverableFeature.class));
    }

    @Test
    public void testViewPullRequestCommitRangeCompleteUrl() throws SoyException {
        SimpleCommit commit = mock(SimpleCommit.class);
        PullRequestParticipant participant = mock(PullRequestParticipant.class);
        InternalPullRequest pullRequest = newPullRequestWithParticipant(participant);

        commitRangeSetup(commit, pullRequest, participant);
        when(darkFeatureManager.isFeatureEnabledForAllUsers(ITERATIVE_REVIEW_DARK_FEATURE_KEY)).thenReturn(true);

        ModelAndView modelAndView = controller.viewPullRequestCommitOrCommitRange(PROJECT_KEY, REPO_SLUG, 0L, "until-commit", "since-commit");

        assertEquals("The PR view view should be shown", PullRequestController.VIEW, modelAndView.getViewName());
        assertNotNull("A repository should be available in the model", modelAndView.getModel().get(REPOSITORY_KEY));
        assertSame("The PR should be available in the model", pullRequest, modelAndView.getModel().get(PULL_REQUEST_KEY));
        assertEquals("Since commit id should be present in the model", "since-commit", modelAndView.getModel().get(SINCE_COMMIT_ID_KEY));
        assertEquals("Until commit id should be present in the model", "until-commit", modelAndView.getModel().get(UNTIL_COMMIT_ID_KEY));
        assertDiscover(modelAndView, EnumSet.noneOf(DiscoverableFeature.class));
    }

    @Test(expected = NoSuchEntityException.class)
    public void testViewPullRequestCommitRangeDarkFeatureDisabled() {
        SimpleCommit commit = mock(SimpleCommit.class);
        PullRequestParticipant participant = mock(PullRequestParticipant.class);
        InternalPullRequest pullRequest = newPullRequestWithParticipant(participant);

        commitRangeSetup(commit, pullRequest, participant);
        when(darkFeatureManager.isFeatureEnabledForAllUsers(ITERATIVE_REVIEW_DARK_FEATURE_KEY)).thenReturn(false);

        controller.viewPullRequestCommitOrCommitRange(PROJECT_KEY, REPO_SLUG, 0L, "until-commit", "since-commit");
    }

    @Test
    public void testViewPullRequestDiff() throws SoyException {
        InternalPullRequest pullRequest = newPullRequestWithAuthor();

        when(pullRequestService.getById(eq(0), eq(0L))).thenReturn(pullRequest);

        ModelAndView modelAndView = controller.viewPullRequestDiff(PROJECT_KEY, REPO_SLUG, 0L);

        assertEquals("The PR view view should be shown", PullRequestController.VIEW, modelAndView.getViewName());
        assertNotNull("A repository should be available in the model", modelAndView.getModel().get(REPOSITORY_KEY));
        assertSame("The PR should be available in the model", pullRequest, modelAndView.getModel().get(PULL_REQUEST_KEY));

        assertDiscover(modelAndView, EnumSet.noneOf(DiscoverableFeature.class));
    }

    @Test
    public void testViewPullRequestDiffIfUserHasNotSeenCommitReviewFeatureDiscovery() throws SoyException {
        when(discoveryService.getIfUndiscovered(EnumSet.of(DiscoverableFeature.NEEDS_WORK, DiscoverableFeature.COMMIT_REVIEW)))
                .thenReturn(EnumSet.of(DiscoverableFeature.COMMIT_REVIEW));

        InternalPullRequest pullRequest = newPullRequestWithAuthor();

        when(pullRequestService.getById(eq(0), eq(0L))).thenReturn(pullRequest);

        ModelAndView modelAndView = controller.viewPullRequestDiff(PROJECT_KEY, REPO_SLUG, 0L);

        assertEquals("The PR view view should be shown", PullRequestController.VIEW, modelAndView.getViewName());
        assertNotNull("A repository should be available in the model", modelAndView.getModel().get(REPOSITORY_KEY));
        assertSame("The PR should be available in the model", pullRequest, modelAndView.getModel().get(PULL_REQUEST_KEY));

        assertDiscover(modelAndView, EnumSet.of(DiscoverableFeature.COMMIT_REVIEW));
    }

    @Test
    public void testViewPullRequestDiffIfUserHasNotSeenNeedsWorkFeatureDiscovery() throws SoyException {
        when(discoveryService.getIfUndiscovered(EnumSet.of(DiscoverableFeature.NEEDS_WORK, DiscoverableFeature.COMMIT_REVIEW)))
                .thenReturn(EnumSet.of(DiscoverableFeature.NEEDS_WORK));

        InternalPullRequest pullRequest = newPullRequestWithAuthor();

        when(pullRequestService.getById(eq(0), eq(0L))).thenReturn(pullRequest);

        ModelAndView modelAndView = controller.viewPullRequestDiff(PROJECT_KEY, REPO_SLUG, 0L);

        assertEquals("The PR view view should be shown", PullRequestController.VIEW, modelAndView.getViewName());
        assertNotNull("A repository should be available in the model", modelAndView.getModel().get(REPOSITORY_KEY));
        assertSame("The PR should be available in the model", pullRequest, modelAndView.getModel().get(PULL_REQUEST_KEY));

        assertDiscover(modelAndView, EnumSet.of(DiscoverableFeature.NEEDS_WORK));
    }

    @Test
    public void testViewPullRequestDiffIfUserHasNotSeenCommitReviewAndNeedsWorkFeatureDiscovery() throws SoyException {
        when(discoveryService.getIfUndiscovered(EnumSet.of(DiscoverableFeature.NEEDS_WORK, DiscoverableFeature.COMMIT_REVIEW)))
                .thenReturn(EnumSet.of(DiscoverableFeature.NEEDS_WORK, DiscoverableFeature.COMMIT_REVIEW));

        InternalPullRequest pullRequest = newPullRequestWithAuthor();

        when(pullRequestService.getById(eq(0), eq(0L))).thenReturn(pullRequest);

        ModelAndView modelAndView = controller.viewPullRequestDiff(PROJECT_KEY, REPO_SLUG, 0L);

        assertEquals("The PR view view should be shown", PullRequestController.VIEW, modelAndView.getViewName());
        assertNotNull("A repository should be available in the model", modelAndView.getModel().get(REPOSITORY_KEY));
        assertSame("The PR should be available in the model", pullRequest, modelAndView.getModel().get(PULL_REQUEST_KEY));

        assertDiscover(modelAndView, EnumSet.of(DiscoverableFeature.COMMIT_REVIEW, DiscoverableFeature.NEEDS_WORK));
    }

    @Test(expected = NoSuchEntityException.class)
    public void testViewPullRequestDiffWhenNoSuchPullRequest() throws SoyException {
        whenPullRequestFindByRepositoryAndIdThrowNoSuchEntity();
        controller.viewPullRequestDiff(PROJECT_KEY, REPO_SLUG, 0L);
    }

    @Test(expected = NoSuchEntityException.class)
    public void testViewPullRequestDiffWhenNoSuchRepository() throws SoyException {
        whenRepoFindBySlugThrowNoSuchEntity();

        controller.viewPullRequestDiff(PROJECT_KEY, REPO_SLUG, 0L);
    }

    @Test
    public void testViewPullRequestRedirectsToActivityUrl() {
        InternalPullRequest pullRequest = newPullRequestWithAuthor();
        when(pullRequestService.getById(eq(0), eq(pullRequest.getId()))).thenReturn(pullRequest);

        ModelAndView modelAndView = controller.viewPullRequest(PROJECT_KEY, REPO_SLUG, 0L);

        View view = modelAndView.getView();
        assertTrue("There should be a redirect to the activity URL when hitting a PR URL", view instanceof RedirectView);
        assertEquals("The redirect should be to the url for the PR's activity", PULL_REQUEST_OVERVIEW_FAKE_URL, ((RedirectView) view).getUrl());
    }

    @Test(expected = NoSuchEntityException.class)
    public void testVisitingCreatePullRequestPageWhenNoSuchProject() {
        whenProjectFindByKeyThrowNoSuchEntity();

        controller.createPullRequestView(PROJECT_KEY, REPO_SLUG, null, null, -1, null, null, null);
    }

    @Test(expected = NoSuchEntityException.class)
    public void testVisitingCreatePullRequestPageWhenNoSuchRepository() {
        whenRepoFindBySlugThrowNoSuchEntity();

        controller.createPullRequestView(PROJECT_KEY, REPO_SLUG, null, null, -1, null, null, null);
    }

    @Test
    public void testVisitingCreatePullRequestPageWithEmptyRepo() {
        when(repositoryService.isEmpty(same(repo))).thenReturn(true);

        ModelAndView modelAndView = controller.createPullRequestView(PROJECT_KEY, REPO_SLUG, null, null, -1, null, null, null);

        assertEquals("The empty repo view should be shown", PullRequestController.EMPTY_REPO_VIEW, modelAndView.getViewName());
        assertSame("The repository should be available in the model", repo, modelAndView.getModel().get(REPOSITORY_KEY));
        assertFalse("There should be an indication that the repo is ready", (Boolean) modelAndView.getModel().get(NOT_INITIALISED_KEY));
    }

    @Test
    public void testVisitingCreatePullRequestPageWithInitialisingRepo() {
        repo = new InternalRepository.Builder(repo).state(Repository.State.INITIALISING).build();

        when(repositoryService.getBySlug(eq(PROJECT_KEY), eq(REPO_SLUG))).thenReturn(repo);
        when(repositoryService.isEmpty(same(repo))).thenReturn(false);
        when(refService.getDefaultBranch(same(repo))).thenReturn(branch);

        ModelAndView modelAndView = controller.createPullRequestView(PROJECT_KEY, REPO_SLUG, null, null, -1, null, null, null);

        assertEquals("The empty repo view should be shown", PullRequestController.EMPTY_REPO_VIEW, modelAndView.getViewName());
        assertSame("The repository should be available in the model", repo, modelAndView.getModel().get(REPOSITORY_KEY));
        assertTrue("There should be an indication that the repo isn't ready", (Boolean) modelAndView.getModel().get(NOT_INITIALISED_KEY));
    }

    @Test
    public void testVisitingCreatePullRequestPageWithSourceAndTarget() {
        when(repositoryService.isEmpty(same(repo))).thenReturn(false);
        when(refService.getDefaultBranch(same(repo))).thenReturn(branch);

        ModelAndView modelAndView = controller.createPullRequestView(PROJECT_KEY, REPO_SLUG, BRANCH1, BRANCH2, -1, null, null, null);

        assertEquals("The create PR view should be shown", PullRequestController.CREATE_VIEW, modelAndView.getViewName());
        assertEquals("The source branch should be passed back to the view", fromBranch, modelAndView.getModel().get(FROM_BRANCH_KEY));
        assertEquals("The target branch should be passed back to the view", toBranch, modelAndView.getModel().get(TO_BRANCH_KEY));
        assertNotNull("A repository should be available in the model", modelAndView.getModel().get(REPOSITORY_KEY));
    }

    @Test
    public void testVisitingCreatePullRequestPageNoFork() {
        when(repositoryService.isEmpty(same(repo))).thenReturn(false);
        when(refService.resolveRef(argThat(resolveRefRequest(repo, null)))).thenReturn(branch);

        ModelAndView modelAndView = controller.createPullRequestView(PROJECT_KEY, REPO_SLUG, BRANCH1, null, -1, null, null, null);

        assertEquals("The create PR view should be shown", PullRequestController.CREATE_VIEW, modelAndView.getViewName());
        assertEquals("The source branch should be passed back to the view", fromBranch, modelAndView.getModel().get(FROM_BRANCH_KEY));
        assertEquals("The target branch should be passed back to the view", branch, modelAndView.getModel().get(TO_BRANCH_KEY));
        assertNotNull("A repository should be available in the model", modelAndView.getModel().get(REPOSITORY_KEY));
        assertEquals("Source repository should be fork", repo, modelAndView.getModel().get(FROM_REPOSITORY_KEY));
        assertEquals("Target repository should be fork", repo, modelAndView.getModel().get(REPOSITORY_KEY));
    }

    @Test
    public void testCreatePullRequestPageFromPersonalFork() {
        InternalNormalUser user = mock(InternalNormalUser.class);
        when(user.getId()).thenReturn(42);
        when(user.getName()).thenReturn("user");

        InternalProject personalProject = new InternalPersonalProject.Builder().id(0).owner(user).build();
        Repository fork = new InternalRepository.Builder().id(0).project(personalProject).name(REPO_SLUG).state(Repository.State.AVAILABLE).origin(repo).build();

        when(permissionService.hasRepositoryPermission(same(repo), same(Permission.REPO_READ))).thenReturn(true);
        when(repositoryService.getBySlug(eq(personalProject.getKey()), eq(REPO_SLUG))).thenReturn(fork);
        when(repositoryService.isEmpty(same(fork))).thenReturn(false);
        when(refService.resolveRef(any(ResolveRefRequest.class))).thenReturn(branch);
        when(refService.resolveRef(argThat(resolveRefRequest(fork, BRANCH1)))).thenReturn(fromBranch);

        ModelAndView modelAndView = controller.createPullRequestView(personalProject.getKey(), REPO_SLUG, BRANCH1, null, -1, null, null, null);

        assertEquals("The create PR view should be shown", PullRequestController.CREATE_VIEW, modelAndView.getViewName());
        assertEquals("The source branch should be passed back to the view", fromBranch, modelAndView.getModel().get(FROM_BRANCH_KEY));
        assertEquals("The target branch should be passed back to the view", branch, modelAndView.getModel().get(TO_BRANCH_KEY));
        assertSame("Source repository should be fork", fork, modelAndView.getModel().get(FROM_REPOSITORY_KEY));
        assertSame("Target repository should be origin", repo, modelAndView.getModel().get(REPOSITORY_KEY));
    }

    @Test
    public void testCreatePullRequestPageFromPersonalForkWithoutPermissionsForOrigin() {
        InternalNormalUser user = mock(InternalNormalUser.class);
        when(user.getId()).thenReturn(42);
        when(user.getName()).thenReturn("user");

        InternalProject personalProject = new InternalPersonalProject.Builder().id(0).owner(user).build();
        Repository fork = new InternalRepository.Builder().id(0).project(personalProject).name(REPO_SLUG).state(Repository.State.AVAILABLE).origin(repo).build();

        when(repositoryService.getBySlug(eq(personalProject.getKey()), eq(REPO_SLUG))).thenReturn(fork);
        when(repositoryService.isEmpty(same(fork))).thenReturn(false);
        when(refService.resolveRef(any(ResolveRefRequest.class))).thenReturn(branch);
        when(refService.resolveRef(argThat(resolveRefRequest(fork, BRANCH1)))).thenReturn(fromBranch);

        ModelAndView modelAndView = controller.createPullRequestView(personalProject.getKey(), REPO_SLUG, BRANCH1, null, -1, null, null, null);

        assertEquals("The create PR view should be shown", PullRequestController.CREATE_VIEW, modelAndView.getViewName());
        assertEquals("The source branch should be passed back to the view", fromBranch, modelAndView.getModel().get(FROM_BRANCH_KEY));
        assertEquals("The target branch should be passed back to the view", branch, modelAndView.getModel().get(TO_BRANCH_KEY));
        //When the user doesn't have access to the upstream repository, the system should
        //not default the pull request target to it
        assertSame("Source repository should be fork", fork, modelAndView.getModel().get(FROM_REPOSITORY_KEY));
        assertSame("Target repository should be fork", fork, modelAndView.getModel().get(REPOSITORY_KEY));
    }

    @Test
    public void testCreatePullRequestPageFromPersonalForkWithoutScmCrossRepositorySupport() {
        reset(scmService); //Will now return false

        InternalNormalUser user = mock(InternalNormalUser.class);
        when(user.getId()).thenReturn(42);
        when(user.getName()).thenReturn("user");

        InternalProject personalProject = new InternalPersonalProject.Builder().id(0).owner(user).build();
        Repository fork = new InternalRepository.Builder().id(0).project(personalProject).name(REPO_SLUG).state(Repository.State.AVAILABLE).origin(repo).build();

        when(repositoryService.getBySlug(eq(personalProject.getKey()), eq(REPO_SLUG))).thenReturn(fork);
        when(repositoryService.isEmpty(same(fork))).thenReturn(false);
        when(refService.resolveRef(any(ResolveRefRequest.class))).thenReturn(branch);
        when(refService.resolveRef(argThat(resolveRefRequest(fork, BRANCH1)))).thenReturn(fromBranch);

        ModelAndView modelAndView = controller.createPullRequestView(personalProject.getKey(), REPO_SLUG, BRANCH1, null, -1, null, null, null);

        assertEquals("The create PR view should be shown", PullRequestController.CREATE_VIEW, modelAndView.getViewName());
        assertEquals("The source branch should be passed back to the view", fromBranch, modelAndView.getModel().get(FROM_BRANCH_KEY));
        assertEquals("The target branch should be passed back to the view", branch, modelAndView.getModel().get(TO_BRANCH_KEY));
        //When the SCM doesn't support cross-repository pull requests it should _not_
        //default the pull request target to the upstream repository
        assertSame("Source repository should be fork", fork, modelAndView.getModel().get(FROM_REPOSITORY_KEY));
        assertSame("Target repository should be fork", fork, modelAndView.getModel().get(REPOSITORY_KEY));
    }

    @Test
    public void testViewCreatePullRequestPageWithTargetRepoParam() {
        when(repositoryService.isEmpty(same(repo))).thenReturn(false);
        when(refService.getDefaultBranch(same(repo))).thenReturn(branch);

        Repository targetRepo = new InternalRepository.Builder().id(TARGET_REPO_ID).project(repo.getProject()).name(TARGET_REPO_SLUG).state(Repository.State.AVAILABLE).hierarchyId(MAIN_HIERARCHY).build();

        when(refService.resolveRef(argThat(resolveRefRequest(targetRepo, BRANCH2)))).thenReturn(toBranch);
        when(repositoryService.getById(TARGET_REPO_ID)).thenReturn(targetRepo);

        ModelAndView modelAndView = controller.createPullRequestView(PROJECT_KEY, REPO_SLUG, BRANCH1, BRANCH2, TARGET_REPO_ID, null, null, null);

        assertEquals("The create PR view should be shown", PullRequestController.CREATE_VIEW, modelAndView.getViewName());
        assertEquals("The source branch should be passed back to the view", fromBranch, modelAndView.getModel().get(FROM_BRANCH_KEY));
        assertEquals("The target branch should be passed back to the view", toBranch, modelAndView.getModel().get(TO_BRANCH_KEY));
        assertEquals("Source repository should be the repo in the path", repo, modelAndView.getModel().get(FROM_REPOSITORY_KEY));
        assertEquals("Target repository should be the targeted repo", targetRepo, modelAndView.getModel().get(REPOSITORY_KEY));
    }

    @Test
    public void testViewCreatePullRequestPageWithInvalidTargetRepoParam() {
        when(repositoryService.isEmpty(same(repo))).thenReturn(false);
        when(refService.getDefaultBranch(same(repo))).thenReturn(branch);

        ModelAndView modelAndView = controller.createPullRequestView(PROJECT_KEY, REPO_SLUG, BRANCH1, BRANCH2, 99, null, null, null);

        assertEquals("The create PR view should be shown", PullRequestController.CREATE_VIEW, modelAndView.getViewName());
        assertEquals("The source branch should be passed back to the view", fromBranch, modelAndView.getModel().get(FROM_BRANCH_KEY));
        assertEquals("The target branch should be passed back to the view", toBranch, modelAndView.getModel().get(TO_BRANCH_KEY));
        assertEquals("Source repository should be the repo in the path", repo, modelAndView.getModel().get(FROM_REPOSITORY_KEY));
        assertEquals("Target repository should be the repo in the path", repo, modelAndView.getModel().get(REPOSITORY_KEY));
    }

    @Test
    public void testViewCreatePullRequestPageWithParamForUnrelatedTargetRepo() {
        when(repositoryService.isEmpty(same(repo))).thenReturn(false);
        when(refService.getDefaultBranch(same(repo))).thenReturn(branch);

        Repository targetRepo = new InternalRepository.Builder().id(TARGET_REPO_ID).project(repo.getProject()).name(TARGET_REPO_SLUG).state(Repository.State.AVAILABLE).hierarchyId("heirarchy2").build();

        when(repositoryService.getById(TARGET_REPO_ID)).thenReturn(targetRepo);

        ModelAndView modelAndView = controller.createPullRequestView(PROJECT_KEY, REPO_SLUG, BRANCH1, BRANCH2, TARGET_REPO_ID, null, null, null);

        assertEquals("The create PR view should be shown", PullRequestController.CREATE_VIEW, modelAndView.getViewName());
        assertEquals("The source branch should be passed back to the view", fromBranch, modelAndView.getModel().get(FROM_BRANCH_KEY));
        assertEquals("The target branch should be passed back to the view", toBranch, modelAndView.getModel().get(TO_BRANCH_KEY));
        assertEquals("Source repository should be the repo in the path", repo, modelAndView.getModel().get(FROM_REPOSITORY_KEY));
        assertEquals("Target repository should be the repo in the path", repo, modelAndView.getModel().get(REPOSITORY_KEY));
    }

    @Test(expected = NoSuchCommitException.class)
    public void testViewCreatePullRequestPageWithInvalidSourceBranch() {
        try {
            controller.createPullRequestView(PROJECT_KEY, REPO_SLUG, NOT_FOUND_BRANCH, BRANCH2, TARGET_REPO_ID, null, null, null);
        } finally {
            verify(refService).resolveRef(argThat(resolveRefRequest(repo, NOT_FOUND_BRANCH)));
        }
    }

    @Test(expected = NoSuchCommitException.class)
    public void testViewCreatePullRequestPageWithInvalidTargetBranch() {
        try {
            controller.createPullRequestView(PROJECT_KEY, REPO_SLUG, BRANCH1, NOT_FOUND_BRANCH, TARGET_REPO_ID, null, null, null);
        } finally {
            verify(refService).resolveRef(argThat(resolveRefRequest(repo, NOT_FOUND_BRANCH)));
        }
    }

    @Test
    public void testViewPullRequest() {
        InternalPullRequest pullRequest = newPullRequestWithAuthor();

        when(pullRequestService.getById(eq(0), eq(0L))).thenReturn(pullRequest);

        ModelAndView modelAndView = controller.viewPullRequestOverview(PROJECT_KEY, REPO_SLUG, 0L);

        assertEquals("The PR view should be shown", PullRequestController.VIEW, modelAndView.getViewName());
        assertDiscover(modelAndView, EnumSet.noneOf(DiscoverableFeature.class));
    }

    @Test
    public void testViewPullRequestIfUserHasNotSeenCommitReviewFeatureDiscovery() {
        when(discoveryService.getIfUndiscovered(EnumSet.of(DiscoverableFeature.NEEDS_WORK, DiscoverableFeature.COMMIT_REVIEW)))
                .thenReturn(EnumSet.of(DiscoverableFeature.COMMIT_REVIEW));
        InternalPullRequest pullRequest = newPullRequestWithAuthor();

        when(pullRequestService.getById(eq(0), eq(0L))).thenReturn(pullRequest);

        ModelAndView modelAndView = controller.viewPullRequestOverview(PROJECT_KEY, REPO_SLUG, 0L);
        assertEquals("The PR view should be shown", PullRequestController.VIEW, modelAndView.getViewName());
        assertDiscover(modelAndView, EnumSet.of(DiscoverableFeature.COMMIT_REVIEW));
    }

    @Test
    public void testViewPullRequestIfUserHasNotSeenNeedsWorkFeatureDiscovery() {
        when(discoveryService.getIfUndiscovered(EnumSet.of(DiscoverableFeature.NEEDS_WORK, DiscoverableFeature.COMMIT_REVIEW)))
                .thenReturn(EnumSet.of(DiscoverableFeature.NEEDS_WORK));
        InternalPullRequest pullRequest = newPullRequestWithAuthor();

        when(pullRequestService.getById(eq(0), eq(0L))).thenReturn(pullRequest);

        ModelAndView modelAndView = controller.viewPullRequestOverview(PROJECT_KEY, REPO_SLUG, 0L);
        assertEquals("The PR view should be shown", PullRequestController.VIEW, modelAndView.getViewName());
        assertDiscover(modelAndView, EnumSet.of(DiscoverableFeature.NEEDS_WORK));
    }

    @Test
    public void testViewPullRequestIfUserHasNotSeenCommitReviewAndNeedsWorkFeatureDiscovery() {
        when(discoveryService.getIfUndiscovered(EnumSet.of(DiscoverableFeature.NEEDS_WORK, DiscoverableFeature.COMMIT_REVIEW)))
                .thenReturn(EnumSet.of(DiscoverableFeature.NEEDS_WORK, DiscoverableFeature.COMMIT_REVIEW));
        InternalPullRequest pullRequest = newPullRequestWithAuthor();

        when(pullRequestService.getById(eq(0), eq(0L))).thenReturn(pullRequest);

        ModelAndView modelAndView = controller.viewPullRequestOverview(PROJECT_KEY, REPO_SLUG, 0L);
        assertEquals("The PR view should be shown", PullRequestController.VIEW, modelAndView.getViewName());
        assertDiscover(modelAndView, EnumSet.of(DiscoverableFeature.COMMIT_REVIEW, DiscoverableFeature.NEEDS_WORK));
    }

    private void assertDiscover(ModelAndView modelAndView, Set<DiscoverableFeature> features) {
        assertEquals(!features.contains(DiscoverableFeature.COMMIT_REVIEW),
                modelAndView.getModelMap().get("seenCommitReview"));
        assertEquals(!features.contains(DiscoverableFeature.NEEDS_WORK),
                modelAndView.getModelMap().get("seenNeedsWork"));
        verify(discoveryService).setDiscovered(eq(features));
    }

    private void checkCreateViewShownWithErrors(ModelAndView modelAndView) {
        assertEquals("The create PR view should be shown", PullRequestController.CREATE_VIEW, modelAndView.getViewName());
        assertNotNull("there should be some field errors", modelAndView.getModel().get(FIELD_ERRORS_KEY));
        assertNotNull("There should be some form errors", modelAndView.getModel().get(FORM_ERRORS_KEY));
        assertEquals("The source branch should be passed back to the view", fromBranch, modelAndView.getModel().get(FROM_BRANCH_KEY));
        assertEquals("The target branch should be passed back to the view", toBranch, modelAndView.getModel().get(TO_BRANCH_KEY));
        assertEquals("The PR title should be passed back to the view", PULL_REQUEST_TITLE, modelAndView.getModel().get(PULL_REQUEST_TITLE));
        assertEquals("The PR description should be passed back to the view", PULL_REQUEST_DESCRIPTION, modelAndView.getModel().get(PULL_REQUEST_DESCRIPTION));
    }

    private void commitRangeSetup(Commit commit, InternalPullRequest pullRequest, PullRequestParticipant participant) {
        when(commitService.getCommit(any(CommitRequest.class))).thenReturn(commit);
        when(participant.getRole()).thenReturn(PullRequestRole.PARTICIPANT);
        when(participant.getUser()).thenReturn(currentUser);
        when(pullRequestService.getById(eq(0), eq(0L))).thenReturn(pullRequest);
    }

    private void whenProjectFindByKeyThrowNoSuchEntity() {
        when(repositoryService.getBySlug(anyString(), anyString())).thenReturn(null);
        when(projectService.getByKey(anyString())).thenThrow(new NoSuchEntityException(new KeyedMessage("", "", "")));
    }

    private OngoingStubbing<PullRequest> whenPullRequestFindByRepositoryAndIdThrowNoSuchEntity() {
        return when(pullRequestService.getById(anyInt(), anyInt())).thenThrow(new NoSuchEntityException(new KeyedMessage("", "", "")));
    }

    private void whenRepoFindBySlugThrowNoSuchEntity() {
        when(repositoryService.getBySlug(anyString(), anyString())).thenThrow(new NoSuchEntityException(new KeyedMessage("", "", "")));
    }

    private InternalPullRequest newPullRequestWithParticipant(PullRequestParticipant participant) {
        InternalPullRequest pr = new InternalPullRequest.Builder().globalId(0L).scopedId(1L).build();

        return new InternalPullRequest.Builder(pr)
                .fromRef(new InternalPullRequestRef.Builder().id("ref")
                        .hash("from-commit")
                        .repository(mock(InternalRepository.class))
                        .build())
                .toRef(new InternalPullRequestRef.Builder().id("ref")
                        .hash("to-commit")
                        .repository(mock(InternalRepository.class))
                        .build())
                .participants(participant, new InternalPullRequestParticipant.Builder(pr).id(0L)
                        .role(PullRequestRole.AUTHOR)
                        .user(new InternalNormalUser.Builder().id(0).name("user").build())
                        .build())
                .build();
    }

    private InternalPullRequest newPullRequestWithAuthor() {
        InternalPullRequest pr = new InternalPullRequest.Builder().globalId(0L).scopedId(1L).build();

        return new InternalPullRequest.Builder(pr)
                .fromRef(new InternalPullRequestRef.Builder().id("ref")
                        .hash("from-commit")
                        .repository(mock(InternalRepository.class))
                        .build())
                .toRef(new InternalPullRequestRef.Builder().id("ref")
                        .hash("to-commit")
                        .repository(mock(InternalRepository.class))
                        .build())
                .participant(new InternalPullRequestParticipant.Builder(pr).id(0L)
                        .role(PullRequestRole.AUTHOR)
                        .user(new InternalNormalUser.Builder().id(0).name("user").build())
                        .build())
                .build();
    }
}
