package com.atlassian.stash.internal.notification.pull.activity;

import com.atlassian.bitbucket.comment.Comment;
import com.atlassian.bitbucket.commit.Commit;
import com.atlassian.bitbucket.hamcrest.PageMatchers;
import com.atlassian.bitbucket.pull.*;
import com.atlassian.bitbucket.user.ApplicationUser;
import com.atlassian.bitbucket.util.Page;
import com.atlassian.stash.internal.notification.pull.handlers.PullRequestNotificationHelper;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Iterables;
import org.hamcrest.Matcher;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.Spy;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.runners.MockitoJUnitRunner;
import org.mockito.stubbing.Answer;

import java.util.*;

import static com.google.common.collect.Iterables.limit;
import static com.google.common.collect.Iterables.toArray;
import static java.util.Collections.singleton;
import static org.hamcrest.Matchers.*;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertThat;
import static org.mockito.Mockito.*;

@RunWith(MockitoJUnitRunner.class)
public class PullRequestActivityRenderContextTest {

    private static final String FILE = "file";
    private static final String PATH = "path/to/";

    @Mock
    private PullRequestNotificationHelper notificationHelper;

    @Mock
    private ApplicationUser user;

    @Mock
    private PullRequest pullRequest;

    @Mock
    private Page<Commit> commitPage;

    @Spy
    @SuppressWarnings("MismatchedQueryAndUpdateOfCollection") //Used by PullRequestActivityRenderContext constructor
    private Set<Long> commentIds = new HashSet<>();

    @InjectMocks
    private PullRequestActivityRenderContext context;

    private final Date now = new Date();

    @Test
    public void testAddWorkflowActivity() throws Exception {
        PullRequestActivity activity = getMockActivity(PullRequestActivity.class);
        when(activity.getAction()).thenReturn(PullRequestAction.APPROVED);
        when(activity.getId()).thenReturn(321L);

        context.add(activity);

        Map<String, Object> item = expectSingleItem();
        assertEquals(ImmutableMap.of("date", now,
            "type", BatchEmailActivityType.WORKFLOW,
            "user", user,
            "action", PullRequestAction.APPROVED.toString(),
            "activityId", 321L), item);
    }

    @Test
    public void testAddMergeActivity() throws Exception {
        PullRequestMergeActivity activity = getMockActivity(PullRequestMergeActivity.class);
        when(activity.getAction()).thenReturn(PullRequestAction.MERGED);
        when(activity.getId()).thenReturn(321L);

        context.add(activity);

        Map<String, Object> item = expectSingleItem();
        assertEquals(ImmutableMap.of("date", now,
                "type", BatchEmailActivityType.WORKFLOW,
                "user", user,
                "action", PullRequestAction.MERGED.toString(),
                "activityId", 321L), item);
    }

    @Test
    public void testAddUnwantedWorkflowActivity() throws Exception {
        PullRequestActivity activity = getMockActivity(PullRequestActivity.class);
        when(activity.getAction()).thenReturn(PullRequestAction.COMMENTED);
        context.add(activity);
        assertThat(context.getSortedItems(), is(empty()));
    }

    @Test
    public void testAddRescopeActivity() throws Exception {
        PullRequestRescopeActivity activity = getMockActivity(PullRequestRescopeActivity.class);
        when(activity.getAction()).thenReturn(PullRequestAction.RESCOPED);
        List<Commit> added = ImmutableList.of(getMockCommit("314"), getMockCommit("159"), getMockCommit("265"));
        List<Commit> removed = ImmutableList.of(getMockCommit("271"), getMockCommit("828"), getMockCommit("182"), getMockCommit("845"));
        when(activity.getAdded().getTotal()).thenReturn(added.size());
        when(activity.getAdded().getCommits()).thenReturn(added);
        when(activity.getRemoved().getTotal()).thenReturn(removed.size());
        when(activity.getRemoved().getCommits()).thenReturn(removed);

        context.add(activity);

        Map<String, Object> item = expectSingleItem();

        assertThat(item, hasEntry("date", (Object) now));
        assertThat(item, hasEntry("type", (Object) BatchEmailActivityType.RESCOPE));
        assertThat(item, hasEntry("user", (Object) user));
        assertThat(item, hasEntry("authors", (Object) 7));
        assertThat(item, hasEntry(equalTo("commits"), (Matcher) PageMatchers.hasSize(3)));
        assertThat(item, hasEntry(equalTo("commits"), (Matcher) PageMatchers.isLastPage()));
        assertThat(item, hasEntry(equalTo("commits"),
                (Matcher) PageMatchers.isPageOf(toArray(added, Commit.class))));
        assertThat(item, hasEntry(equalTo("commitsRemoved"), (Matcher) PageMatchers.hasSize(2)));
        assertThat(item, hasEntry(equalTo("commitsRemoved"), (Matcher) not(PageMatchers.isLastPage())));
        assertThat(item, hasEntry(equalTo("commitsRemoved"),
                (Matcher) PageMatchers.isPageOf(toArray(limit(removed, 2), Commit.class))));
        assertEquals(1, context.getCounts().get(PullRequestAction.RESCOPED.toString().toLowerCase()).longValue());
    }

    @Test
    public void testAddReviewersUpdatedActivity() throws Exception {
        Set<ApplicationUser> addedReviewers = singleton(mock(ApplicationUser.class));
        Set<ApplicationUser> removedReviewers = singleton(mock(ApplicationUser.class));
        PullRequestReviewersUpdatedActivity activity = getMockActivity(PullRequestReviewersUpdatedActivity.class);
        when(activity.getAction()).thenReturn(PullRequestAction.UPDATED);
        when(activity.getAddedReviewers()).thenReturn(addedReviewers);
        when(activity.getRemovedReviewers()).thenReturn(removedReviewers);

        context.add(activity);

        Map<String, Object> item = expectSingleItem();

        assertThat(item, hasEntry("date", (Object) now));
        assertThat(item, hasEntry("type", (Object) BatchEmailActivityType.REVIEWERS_UPDATED));
        assertThat(item, hasEntry("user", (Object) user));
        assertThat(item, hasEntry(equalTo("addedReviewers"), equalTo(addedReviewers)));
        assertThat(item, hasEntry(equalTo("removedReviewers"), equalTo(removedReviewers)));
        assertEquals(1, context.getCounts().get(PullRequestAction.UPDATED.toString().toLowerCase()).longValue());
    }

    @Test
    public void testAddSimpleCommentActivity() throws Exception {
        PullRequestCommentActivity activity = getMockActivity(PullRequestCommentActivity.class);
        Comment comment = getMockComment(activity, 1);
        commentIds.add(comment.getId());
        when(activity.getCommentAnchor()).thenReturn(null);
        context.add(activity);
        assertEquals(1, context.getCounts().get(PullRequestAction.COMMENTED.toString().toLowerCase()).longValue());

        Map<String, Object> item = expectSingleItem();

        ImmutableMap<String, Object> expectedItem = ImmutableMap.<String, Object>builder()
                .put("type", BatchEmailActivityType.COMMENT_THREAD)
                .put("date", comment.getCreatedDate())
                .put("comment", comment)
                .put("context", false)
                .put("replies", Collections.emptySet())
                .build();
        assertEquals(expectedItem, item);
    }

    @Test
    public void testAddSimpleAnchoredCommentActivity() throws Exception {
        PullRequestCommentActivity firstActivity = getMockActivity(PullRequestCommentActivity.class);
        commentIds.add(getMockComment(firstActivity, 1).getId());

        PullRequestDiffCommentAnchor firstActivityAnchor = mock(PullRequestDiffCommentAnchor.class);
        when(firstActivity.getCommentAnchor()).thenReturn(firstActivityAnchor);
        when(firstActivityAnchor.getPath()).thenReturn(PATH + FILE);
        when(firstActivityAnchor.getFromHash()).thenReturn("commit_hash_001");
        when(firstActivityAnchor.getToHash()).thenReturn("commit_hash_002");
        when(firstActivityAnchor.getDiffType()).thenReturn(PullRequestCommentAnchorDiffType.COMMIT);

        PullRequestCommentActivity secondActivity = getMockActivity(PullRequestCommentActivity.class);
        commentIds.add(getMockComment(secondActivity, 2).getId());

        PullRequestDiffCommentAnchor secondActivityAnchor = mock(PullRequestDiffCommentAnchor.class);
        when(secondActivity.getCommentAnchor()).thenReturn(secondActivityAnchor);
        when(secondActivityAnchor.getPath()).thenReturn(PATH + FILE);
        when(secondActivityAnchor.getFromHash()).thenReturn("commit_hash_003");
        when(secondActivityAnchor.getToHash()).thenReturn("commit_hash_004");
        when(secondActivityAnchor.getDiffType()).thenReturn(PullRequestCommentAnchorDiffType.COMMIT);

        context.add(firstActivity);
        context.add(secondActivity);

        List<Map<String, Object>> items = context.getSortedItems();
        assertThat(items, hasSize(2));

        Map<String, Object> item = items.get(0);
        assertThat(item, hasEntry("type", BatchEmailActivityType.FILE));
        assertThat(item, hasEntry("path", PATH));
        assertThat(item, hasEntry("name", FILE));
        assertThat(item, hasEntry("fromHash", "commit_hash_001"));
        assertThat(item, hasEntry("toHash", "commit_hash_002"));
        assertThat(item, hasEntry("diffType", PullRequestCommentAnchorDiffType.COMMIT));
        assertThat(item, hasEntry(equalTo("lines"), (Matcher) hasSize(1)));

        item = items.get(1);
        assertThat(item, hasEntry("type", BatchEmailActivityType.FILE));
        assertThat(item, hasEntry("path", PATH));
        assertThat(item, hasEntry("name", FILE));
        assertThat(item, hasEntry("fromHash", "commit_hash_003"));
        assertThat(item, hasEntry("toHash", "commit_hash_004"));
        assertThat(item, hasEntry("diffType", PullRequestCommentAnchorDiffType.COMMIT));
        assertThat(item, hasEntry(equalTo("lines"), (Matcher) hasSize(1)));
    }

    @Test
    public void testAddSimpleAnchoredCommentActivityOnSameCommitAndDifferentFile() throws Exception {
        PullRequestCommentActivity firstActivity = getMockActivity(PullRequestCommentActivity.class);
        commentIds.add(getMockComment(firstActivity, 1).getId());

        PullRequestDiffCommentAnchor firstActivityAnchor = mock(PullRequestDiffCommentAnchor.class);
        when(firstActivity.getCommentAnchor()).thenReturn(firstActivityAnchor);
        when(firstActivityAnchor.getPath()).thenReturn(PATH + FILE + "001");
        when(firstActivityAnchor.getFromHash()).thenReturn("commit_hash_001");
        when(firstActivityAnchor.getToHash()).thenReturn("commit_hash_002");
        when(firstActivityAnchor.getDiffType()).thenReturn(PullRequestCommentAnchorDiffType.COMMIT);

        PullRequestCommentActivity secondActivity = getMockActivity(PullRequestCommentActivity.class);
        commentIds.add(getMockComment(secondActivity, 2).getId());

        PullRequestDiffCommentAnchor secondActivityAnchor = mock(PullRequestDiffCommentAnchor.class);
        when(secondActivity.getCommentAnchor()).thenReturn(secondActivityAnchor);
        when(secondActivityAnchor.getPath()).thenReturn(PATH + FILE + "002");
        when(secondActivityAnchor.getFromHash()).thenReturn("commit_hash_001");
        when(secondActivityAnchor.getToHash()).thenReturn("commit_hash_002");
        when(secondActivityAnchor.getDiffType()).thenReturn(PullRequestCommentAnchorDiffType.COMMIT);

        context.add(firstActivity);
        context.add(secondActivity);

        List<Map<String, Object>> items = context.getSortedItems();
        assertThat(items, hasSize(2));

        Map<String, Object> item = items.get(0);
        assertThat(item, hasEntry("type", BatchEmailActivityType.FILE));
        assertThat(item, hasEntry("path", PATH));
        assertThat(item, hasEntry("name", FILE + "001"));
        assertThat(item, hasEntry("fromHash", "commit_hash_001"));
        assertThat(item, hasEntry("toHash", "commit_hash_002"));
        assertThat(item, hasEntry("diffType", PullRequestCommentAnchorDiffType.COMMIT));
        assertThat(item, hasEntry(equalTo("lines"), (Matcher) hasSize(1)));

        item = items.get(1);
        assertThat(item, hasEntry("type", BatchEmailActivityType.FILE));
        assertThat(item, hasEntry("path", PATH));
        assertThat(item, hasEntry("name", FILE + "002"));
        assertThat(item, hasEntry("fromHash", "commit_hash_001"));
        assertThat(item, hasEntry("toHash", "commit_hash_002"));
        assertThat(item, hasEntry("diffType", PullRequestCommentAnchorDiffType.COMMIT));
        assertThat(item, hasEntry(equalTo("lines"), (Matcher) hasSize(1)));
    }

    @Test
    @SuppressWarnings("unchecked")
    public void testAddCommentHierarchy() {
        PullRequestCommentActivity activity = getMockActivity(PullRequestCommentActivity.class);
        when(activity.getCommentAnchor()).thenReturn(null);

        Comment comment = getMockComment(activity, 1);
        Comment comment1 = getMockComment(2);
        Comment comment1a = getMockComment(3);
        Comment comment1b = getMockComment(4);
        Comment comment1bx = getMockComment(5);
        Comment comment1c = getMockComment(6);
        when(comment.getComments()).thenReturn(ImmutableList.of(comment1));
        when(comment1.getComments()).thenReturn(ImmutableList.of(comment1a, comment1b, comment1c));
        when(comment1b.getComments()).thenReturn(ImmutableList.of(comment1bx));
        commentIds.addAll(Arrays.asList(comment1a.getId(), comment1b.getId(), comment1bx.getId()));

        context.add(activity);

        Map<String, Object> item = expectSingleItem();
        assertThat(item, hasEntry("context", (Object) true));
        assertThat(item, hasEntry("comment", (Object) comment1));
        assertThat(item, hasKey("replies"));
        Collection<CommentThread> replies = (Collection<CommentThread>) item.get("replies");
        assertThat(replies, hasSize(2));
        CommentThread thread1 = Iterables.get(replies, 0);
        CommentThread thread2 = Iterables.get(replies, 1);
        assertEquals(comment1a, thread1.getComment());
        assertEquals(comment1b, thread2.getComment());
        assertThat(thread1.getReplies(), hasSize(0));
        assertThat(thread2.getReplies(), hasSize(1));
        assertEquals(comment1bx, Iterables.get(thread2.getReplies(), 0).getComment());
    }

    @Test
    public void testAddBranchUpdate() {
        ToBranchUpdate toBranchUpdate = new ToBranchUpdate(mock(ApplicationUser.class), "to-branch", new Date());

        context.add(toBranchUpdate);

        Map<String, Object> item = expectSingleItem();
        assertThat(item, hasEntry("type", (Object) BatchEmailActivityType.TO_BRANCH_UPDATE));
        assertThat(item, hasEntry("user", (Object) toBranchUpdate.getUser()));
        assertThat(item, hasEntry("toBranch", (Object) toBranchUpdate.getToBranch()));
        assertThat(item, hasEntry("date", (Object) toBranchUpdate.getDate()));
    }

    private Map<String, Object> expectSingleItem() {
        List<Map<String, Object>> items = context.getSortedItems();
        assertThat(items, hasSize(1));
        return items.get(0);
    }

    private Commit getMockCommit(String id) {
        Commit commit = mock(Commit.class, RETURNS_DEEP_STUBS);
        when(commit.getId()).thenReturn(id);
        return commit;
    }

    private Comment getMockComment(PullRequestCommentActivity activity, long commentId) {
        Comment comment = getMockComment(commentId);
        when(activity.getComment()).thenReturn(comment);
        return comment;
    }

    private Comment getMockComment(long commentId) {
        Comment comment = mock(Comment.class, "comment " + commentId);
        when(comment.getId()).thenReturn(commentId);
        when(comment.getCreatedDate()).thenReturn(new Date(commentId));
        return comment;
    }

    private <T extends PullRequestActivity> T getMockActivity(final Class<T> pullRequestActivityClass) {
        T mockActivity = mock(pullRequestActivityClass, RETURNS_DEEP_STUBS);
        when(mockActivity.getCreatedDate()).thenReturn(now);
        when(mockActivity.getUser()).thenReturn(user);
        when(mockActivity.getPullRequest()).thenReturn(pullRequest);
        doAnswer(new Answer<Void>() {
            @Override
            public Void answer(InvocationOnMock invocationOnMock) {
                PullRequestActivityVisitor visitor = (PullRequestActivityVisitor) invocationOnMock.getArguments()[0];
                if (pullRequestActivityClass == PullRequestActivity.class) {
                    visitor.visit((PullRequestActivity) invocationOnMock.getMock());
                } else if (pullRequestActivityClass == PullRequestCommentActivity.class) {
                    visitor.visit((PullRequestCommentActivity) invocationOnMock.getMock());
                } else if (pullRequestActivityClass == PullRequestMergeActivity.class) {
                    visitor.visit((PullRequestMergeActivity) invocationOnMock.getMock());
                } else if (pullRequestActivityClass == PullRequestRescopeActivity.class) {
                    visitor.visit((PullRequestRescopeActivity) invocationOnMock.getMock());
                } else if (pullRequestActivityClass == PullRequestReviewersUpdatedActivity.class) {
                    visitor.visit((PullRequestReviewersUpdatedActivity) invocationOnMock.getMock());
                }
                return null;
            }
        }).when(mockActivity).accept(notNull(PullRequestActivityVisitor.class));
        return mockActivity;
    }

}
