package com.atlassian.stash.internal.repository.sync.auto;

import com.atlassian.stash.internal.repository.sync.BulkRefSyncRequest;
import com.atlassian.stash.internal.repository.sync.InternalRefSyncService;
import com.atlassian.stash.internal.repository.sync.RefSyncTestUtils;
import com.atlassian.bitbucket.pull.PullRequest;
import com.atlassian.bitbucket.pull.PullRequestSearchRequest;
import com.atlassian.bitbucket.pull.PullRequestService;
import com.atlassian.bitbucket.pull.PullRequestState;
import com.atlassian.bitbucket.repository.RefChange;
import com.atlassian.bitbucket.repository.Repository;
import com.atlassian.bitbucket.repository.RepositoryService;
import com.atlassian.bitbucket.user.DummySecurityService;
import com.atlassian.bitbucket.user.SecurityService;
import com.atlassian.bitbucket.user.ApplicationUser;
import com.atlassian.bitbucket.user.UserService;
import com.atlassian.bitbucket.util.Page;
import com.atlassian.bitbucket.util.PageRequest;
import com.atlassian.bitbucket.util.PageUtils;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.*;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.runners.MockitoJUnitRunner;
import org.mockito.stubbing.Answer;

import java.util.List;

import static com.atlassian.stash.internal.repository.sync.RefSyncTestUtils.add;
import static org.hamcrest.Matchers.*;
import static org.junit.Assert.*;
import static org.mockito.Mockito.*;
import static org.mockito.Mockito.isA;

@RunWith(MockitoJUnitRunner.class)
public class AutoRefSyncProcessorTest {

    @InjectMocks
    private AutoRefSyncProcessor processor;
    @Mock(answer = Answers.RETURNS_DEEP_STUBS)
    private PullRequestService pullRequestService;
    @Mock
    private RepositoryService repositoryService;
    @Spy
    private SecurityService securityService = new DummySecurityService();
    @Mock
    private InternalRefSyncService syncService;
    @Mock
    private UserService userService;

    @Test
    public void testProcess() {
        Repository repository = mock(Repository.class, "repository");
        Repository upstream = mock(Repository.class, "upstream");
        ApplicationUser user = mock(ApplicationUser.class);

        AutoRefSyncRequest first = mock(AutoRefSyncRequest.class);
        when(first.getOperation()).thenReturn(AutoRefSyncOperation.REVIEW);
        when(first.getRefChanges()).thenReturn(changesFor("refs/heads/review", "2f744cba6b8087868233eea2e4deef0cc580f00b"));
        when(first.getRepositoryId()).thenReturn(100);
        when(first.getUpstreamId()).thenReturn(10);
        when(first.getUserId()).thenReturn(1); //This user won't be found

        AutoRefSyncRequest second = mock(AutoRefSyncRequest.class);
        when(second.getOperation()).thenReturn(AutoRefSyncOperation.SYNCHRONIZE);
        when(second.getRefChanges()).thenReturn(changesFor("refs/heads/sync", "fc234c1c2bfd3343f830eb680cc9f0fe90ac0123"));
        when(second.getRepositoryId()).thenReturn(100);
        when(second.getUpstreamId()).thenReturn(10);
        when(second.getUserId()).thenReturn(2);

        when(repositoryService.getById(eq(10))).thenReturn(upstream);
        when(repositoryService.getById(eq(100))).thenReturn(repository);
        when(userService.getUserById(eq(2))).thenReturn(user); //Verify that

        processor.process("1", ImmutableList.of(first, second));

        ArgumentCaptor<BulkRefSyncRequest> requestCaptor = ArgumentCaptor.forClass(BulkRefSyncRequest.class);

        InOrder ordered = inOrder(repositoryService, securityService, syncService, userService);
        ordered.verify(userService).getUserById(eq(1));
        ordered.verify(userService).getUserById(eq(2));
        ordered.verify(securityService).impersonating(same(user), isA(String.class));
        ordered.verify(repositoryService).getById(eq(100));
        ordered.verify(repositoryService).getById(eq(10));
        ordered.verify(syncService).synchronize(requestCaptor.capture()); //Synchronize should always be done first
        ordered.verify(syncService).updateStatus(requestCaptor.capture()); //Review should always be done last

        List<BulkRefSyncRequest> requests = requestCaptor.getAllValues();
        assertEquals(2, requests.size());

        BulkRefSyncRequest synchronize = requests.get(0);
        assertEquals(1, synchronize.getRefChanges().size());
        assertEquals("refs/heads/sync", Iterables.getOnlyElement(synchronize.getRefChanges()).getRef().getId());
        assertSame(repository, synchronize.getRepository());
        assertSame(upstream, synchronize.getUpstream());
        assertSame(user, synchronize.getUser());

        BulkRefSyncRequest review = requests.get(1);
        assertEquals(1, review.getRefChanges().size());
        assertEquals("refs/heads/review", Iterables.getOnlyElement(review.getRefChanges()).getRef().getId());
        assertSame(repository, review.getRepository());
        assertSame(upstream, review.getUpstream());
        assertSame(user, review.getUser());
    }

    @Test
    public void testProcessDropsRequestedWhenRepositoryNotFound() {
        ApplicationUser user = mock(ApplicationUser.class);

        AutoRefSyncRequest request = mock(AutoRefSyncRequest.class);
        when(request.getRepositoryId()).thenReturn(100);
        when(request.getUserId()).thenReturn(1);

        when(userService.getUserById(eq(1))).thenReturn(user);

        processor.process("1", Lists.newArrayList(request));

        InOrder ordered = inOrder(repositoryService, securityService, userService);
        ordered.verify(userService).getUserById(eq(1));
        ordered.verify(securityService).impersonating(same(user), isA(String.class));
        ordered.verify(repositoryService).getById(eq(100));
        verifyZeroInteractions(syncService);
    }

    @Test
    public void testProcessDropsRequestedWhenUserNotFound() {
        AutoRefSyncRequest request = mock(AutoRefSyncRequest.class);
        when(request.getUserId()).thenReturn(1);

        processor.process("1", Lists.newArrayList(request));

        verify(userService).getUserById(eq(1));
        verifyZeroInteractions(repositoryService, securityService, syncService);
    }

    @Test
    public void testProcessDropsRequestedWhenUpstreamNotFound() {
        Repository repository = mock(Repository.class);
        ApplicationUser user = mock(ApplicationUser.class);

        AutoRefSyncRequest request = mock(AutoRefSyncRequest.class);
        when(request.getRepositoryId()).thenReturn(100);
        when(request.getUpstreamId()).thenReturn(10);
        when(request.getUserId()).thenReturn(1);

        when(repositoryService.getById(eq(100))).thenReturn(repository);
        when(userService.getUserById(eq(1))).thenReturn(user);

        processor.process("1", Lists.newArrayList(request));

        InOrder ordered = inOrder(repositoryService, securityService, userService);
        ordered.verify(userService).getUserById(eq(1));
        ordered.verify(securityService).impersonating(same(user), isA(String.class));
        ordered.verify(repositoryService).getById(eq(100));
        ordered.verify(repositoryService).getById(eq(10));
        verifyZeroInteractions(syncService);
    }

    @Test
    public void testProcessHandlesExceptions() {
        Repository repository = mock(Repository.class, "repository");
        Repository upstream = mock(Repository.class, "upstream");
        ApplicationUser user = mock(ApplicationUser.class);

        AutoRefSyncRequest first = mock(AutoRefSyncRequest.class);
        when(first.getOperation()).thenReturn(AutoRefSyncOperation.REVIEW);
        when(first.getRefChanges()).thenReturn(changesFor("refs/heads/review", "2f744cba6b8087868233eea2e4deef0cc580f00b"));
        when(first.getRepositoryId()).thenReturn(100);
        when(first.getUpstreamId()).thenReturn(10);
        when(first.getUserId()).thenReturn(1); //This user won't be found

        AutoRefSyncRequest second = mock(AutoRefSyncRequest.class);
        when(second.getOperation()).thenReturn(AutoRefSyncOperation.SYNCHRONIZE);
        when(second.getRefChanges()).thenReturn(changesFor("refs/heads/sync", "fc234c1c2bfd3343f830eb680cc9f0fe90ac0123"));
        when(second.getRepositoryId()).thenReturn(100);
        when(second.getUpstreamId()).thenReturn(10);
        when(second.getUserId()).thenReturn(2);

        when(repositoryService.getById(eq(10))).thenReturn(upstream);
        when(repositoryService.getById(eq(100))).thenReturn(repository);
        when(userService.getUserById(eq(2))).thenReturn(user); //Verify that
        doThrow(RuntimeException.class).when(syncService).synchronize(isA(BulkRefSyncRequest.class));
        doThrow(RuntimeException.class).when(syncService).updateStatus(isA(BulkRefSyncRequest.class));

        processor.process("1", ImmutableList.of(first, second));

        InOrder ordered = inOrder(repositoryService, securityService, syncService, userService);
        ordered.verify(userService).getUserById(eq(1));
        ordered.verify(userService).getUserById(eq(2));
        ordered.verify(securityService).impersonating(same(user), isA(String.class));
        ordered.verify(repositoryService).getById(eq(100));
        ordered.verify(repositoryService).getById(eq(10));
        ordered.verify(syncService).synchronize(isA(BulkRefSyncRequest.class));
        ordered.verify(syncService).updateStatus(isA(BulkRefSyncRequest.class));
    }

    @Test
    public void testProcessIgnoresDeletesWithOpenPullRequest() {
        Repository repository = mock(Repository.class, "repository");
        when(repository.getId()).thenReturn(100);
        when(repositoryService.getById(eq(100))).thenReturn(repository);

        Repository upstream = mock(Repository.class, "upstream");
        when(upstream.getId()).thenReturn(10);
        when(repositoryService.getById(eq(10))).thenReturn(upstream);

        RefChange update = RefSyncTestUtils.update("refs/heads/updated", "2f744cba6b8087868233eea2e4deef0cc580f00b",
                "fc234c1c2bfd3343f830eb680cc9f0fe90ac0123");
        RefChange delete1 = RefSyncTestUtils.delete("refs/heads/delete1", "2f744cba6b8087868233eea2e4deef0cc580f00b");
        RefChange delete2 = RefSyncTestUtils.delete("refs/heads/delete2", "2f744cba6b8087868233eea2e4deef0cc580f00b");
        AutoRefSyncRequest request = RefSyncTestUtils.requestFor(repository, upstream, update, delete1, delete2);

        // We don't really care about matching the user id for this test
        ApplicationUser user = mock(ApplicationUser.class);
        when(userService.getUserById(anyInt())).thenReturn(user);

        final PullRequest pullRequest = mock(PullRequest.class, Answers.RETURNS_DEEP_STUBS.get());
        when(pullRequest.getFromRef().getId()).thenReturn("refs/heads/delete2");
        when(pullRequestService.search(isA(PullRequestSearchRequest.class), isA(PageRequest.class)))
                .thenAnswer(new Answer<Page<PullRequest>>() {
                    @Override
                    public Page<PullRequest> answer(InvocationOnMock invocation) throws Throwable {
                        return PageUtils.createPage(ImmutableList.of(pullRequest),
                                (PageRequest) invocation.getArguments()[1]);
                    }
                });

        processor.process("1", ImmutableList.of(request));

        ArgumentCaptor<PullRequestSearchRequest> searchRequestCaptor = ArgumentCaptor.forClass(PullRequestSearchRequest.class);
        verify(pullRequestService).search(searchRequestCaptor.capture(), isA(PageRequest.class));
        PullRequestSearchRequest searchRequest = searchRequestCaptor.getValue();
        assertThat(searchRequest.getFromRefIds(), containsInAnyOrder("refs/heads/delete1", "refs/heads/delete2"));
        assertEquals(PullRequestState.OPEN, searchRequest.getState());
        assertEquals(new Integer(repository.getId()), searchRequest.getFromRepositoryId());

        ArgumentCaptor<BulkRefSyncRequest> bulkRequestCaptor = ArgumentCaptor.forClass(BulkRefSyncRequest.class);
        verify(syncService).synchronize(bulkRequestCaptor.capture());
        BulkRefSyncRequest bulkRequest = bulkRequestCaptor.getValue();
        assertThat(bulkRequest.getRefChanges(), hasItem(update));
        assertThat(bulkRequest.getRefChanges(), hasItem(delete1));
        assertThat(bulkRequest.getRefChanges(), not(hasItem(delete2)));
    }

    private static List<RefChange> changesFor(String refId, String toHash) {
        return ImmutableList.of(add(refId, toHash));
    }
}