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

import com.atlassian.bitbucket.permission.Permission;
import com.atlassian.bitbucket.permission.PermissionService;
import com.atlassian.bitbucket.user.ApplicationUser;
import com.atlassian.bitbucket.user.DummySecurityService;
import com.atlassian.bitbucket.comment.Comment;
import com.atlassian.bitbucket.user.SecurityService;
import com.atlassian.bitbucket.user.UserService;
import com.atlassian.stash.experimental.pull.ExperimentalPullRequestService;
import com.atlassian.stash.internal.comment.InternalComment;
import com.atlassian.stash.internal.notification.batch.dao.UserNotification;
import com.atlassian.stash.internal.notification.batch.dao.UserNotificationBuilder;
import com.atlassian.stash.internal.notification.custom.pull.CustomPullRequestNotificationRendererAccessor;
import com.atlassian.stash.internal.notification.mention.MentionHelper;
import com.atlassian.stash.internal.pull.InternalPullRequest;
import com.atlassian.stash.internal.pull.InternalPullRequestActivity;
import com.atlassian.stash.internal.pull.InternalPullRequestCommentActivity;
import com.atlassian.bitbucket.mail.MailMessage;
import com.atlassian.bitbucket.mail.MailService;
import com.atlassian.bitbucket.pull.PullRequest;
import com.atlassian.bitbucket.pull.PullRequestActivity;
import com.atlassian.bitbucket.pull.PullRequestActivitySearchRequest;
import com.atlassian.bitbucket.pull.PullRequestCommentActivity;
import com.atlassian.bitbucket.repository.Repository;
import com.atlassian.bitbucket.util.Page;
import com.atlassian.bitbucket.util.PageImpl;
import com.atlassian.bitbucket.util.PageRequest;
import com.atlassian.bitbucket.watcher.Watcher;
import com.google.common.collect.ImmutableListMultimap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import com.google.common.collect.ListMultimap;
import org.hamcrest.Matcher;
import org.hamcrest.Matchers;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.*;
import org.mockito.runners.MockitoJUnitRunner;

import java.util.*;

import static org.hamcrest.CoreMatchers.*;
import static org.hamcrest.Matchers.hasSize;
import static org.junit.Assert.*;
import static org.mockito.Mockito.any;
import static org.mockito.Mockito.*;

@RunWith(MockitoJUnitRunner.class)
public class PullRequestBatchSenderTest {

    @Mock
    private CustomPullRequestNotificationRendererAccessor rendererAccessor;
    @Mock
    private ExperimentalPullRequestService pullRequestService;
    @Mock
    private MailService mailService;
    @Mock
    private MentionHelper mentionHelper;
    @Mock
    private PullRequestActivityRenderer renderer;
    @Mock
    private PermissionService permissionService;
    @Spy
    private SecurityService securityService = new DummySecurityService();
    @Mock
    private UserService userService;

    private PullRequestBatchDataLoader batchDataLoader;
    private PullRequestBatchSender batchSender;

    private List<UserNotification> sentNotifications = new ArrayList<>();

    @Before
    public void setup() {
        batchDataLoader = new PullRequestBatchDataLoader(mentionHelper, pullRequestService, securityService, userService);
        batchSender = new PullRequestBatchSender(mailService, permissionService, rendererAccessor, renderer, batchDataLoader);
        when(permissionService.hasRepositoryPermission(any(ApplicationUser.class), any(Repository.class), eq(Permission.REPO_READ)))
                .thenReturn(true);
        when(pullRequestService.searchActivities(any(PullRequestActivitySearchRequest.class), any(PageRequest.class)))
                .thenReturn(page());
        when(mailService.isHostConfigured()).thenReturn(true);
    }

    @Test
    public void testCheckBatchIdEncodeDecode() {
        PullRequestBatchId batchId = PullRequestBatchId.of(1, 2);
        PullRequestBatchId decodedBatchId = PullRequestBatchId.decode(batchId.encode());
        assertThat(decodedBatchId.getRepositoryId(), is(batchId.getRepositoryId()));
        assertThat(decodedBatchId.getPullRequestId(), is(batchId.getPullRequestId()));
    }

    @Test
    public void testBatchIdInvalid() {
        PullRequestBatchId decodedBatchId = PullRequestBatchId.decode("invalidharrharr");
        assertThat(decodedBatchId, nullValue());
    }

    @Test(expected = IllegalStateException.class)
    public void testBatchIdInvalidThrows() {
        batchSender.send("invalildharrharr", ImmutableListMultimap.of(user(1, "user"), activityNotification(1)), sentNotifications);
    }

    @Test
    public void testNoActivities() {
        PullRequestBatchId batchId = PullRequestBatchId.of(1, 2);
        UserNotification notification = activityNotification(11);
        when(pullRequestService.getActivitiesById(1, 2L, ImmutableSet.of(11L))).thenReturn(ImmutableSet.<PullRequestActivity>of());
        when(pullRequestService.searchActivities(any(PullRequestActivitySearchRequest.class), any(PageRequest.class)))
                .thenReturn(page());
        InternalPullRequest pullRequest = pullRequest(batchId, user(99, "username"));
        when(batchDataLoader.getPullRequest(batchId)).thenReturn(pullRequest);

        ApplicationUser user = mock(ApplicationUser.class);
        send(batchId, ImmutableListMultimap.of(user, notification));

        verifyNotSent(notification);
    }

    @Test
    public void testSendActivities() {
        PullRequestBatchId batchId = PullRequestBatchId.of(1, 2);
        UserNotification notification1 = activityNotification(11);
        UserNotification notification2 = activityNotification(12);
        ApplicationUser user = user(1, "user1");
        InternalPullRequest pr = pullRequest(batchId, user);
        Set<PullRequestActivity> activities = ImmutableSet.of(activity(pr, 11), activity(pr, 12));
        when(pullRequestService.getById(1, 2L)).thenReturn(pr);
        when(pullRequestService.getActivitiesById(1, 2L, ImmutableSet.of(11L, 12L))).thenReturn(activities);

        MailMessage mailMessage = mockRender(user, pr, activities,
                Collections.<Long>emptySet(), Collections.<ToBranchUpdate>emptySet());

        send(batchId, ImmutableListMultimap.of(user, notification1, user, notification2));

        verifySent(mailMessage, notification1, notification2);
    }

    @Test
    public void testSendComments() {
        PullRequestBatchId batchId = PullRequestBatchId.of(1, 2);
        long rootCommentId = 11;
        long comment1Id = 12;
        long comment2Id = 13;
        InternalComment comment1 = comment(comment1Id, "comment 1");
        InternalComment comment2 = comment(comment2Id, "comment 2");
        InternalComment rootComment = comment(rootCommentId, "root comment", comment1, comment2);

        when(pullRequestService.getRootCommentId(1, 2L, comment1Id)).thenReturn(rootCommentId);
        when(pullRequestService.getRootCommentId(1, 2L, comment2Id)).thenReturn(rootCommentId);

        ApplicationUser user = user(1, "user1");
        InternalPullRequest pr = pullRequest(batchId, user);

        PullRequestActivity commentActivity = commentActivity(pr, rootComment);

        when(pullRequestService.getById(1, 2L)).thenReturn(pr);
        ArgumentCaptor<PullRequestActivitySearchRequest> searchRequestCaptor = ArgumentCaptor.forClass(PullRequestActivitySearchRequest.class);
        when(pullRequestService.searchActivities(searchRequestCaptor.capture(), any(PageRequest.class)))
                .thenReturn(page(commentActivity));

        MailMessage mailMessage = mockRender(user, pr, ImmutableSet.of(commentActivity),
                ImmutableSet.of(comment1Id, comment2Id), Collections.<ToBranchUpdate>emptySet());

        UserNotification notification1 = commentNotification(comment1Id);
        UserNotification notification2 = commentNotification(comment2Id);
        send(batchId, ImmutableListMultimap.of(user, notification1, user, notification2));

        assertThat(searchRequestCaptor.getValue().getCommentIds(), hasItem(rootCommentId));

        verifySent(mailMessage, notification1, notification2);
    }

    @Test
    public void testSendToBranchUpdates() {
        PullRequestBatchId batchId = PullRequestBatchId.of(1, 2);

        int updaterId = 1;
        ApplicationUser updater = user(updaterId, "updateruser");
        String toBranch = "to-branch";
        Date date = new Date();
        UserNotification notification = toBranchUpdateNotification(updaterId, toBranch, date);

        ApplicationUser user = user(2, "user");
        InternalPullRequest pr = pullRequest(batchId, user);

        when(pullRequestService.getById(1, 2L)).thenReturn(pr);
        doReturn(ImmutableSet.of(updater)).when(userService).getUsersById(ImmutableSet.of(updaterId), true);

        ArgumentCaptor<Set> captor = ArgumentCaptor.forClass(Set.class);
        MailMessage mailMessage = mock(MailMessage.class);
        when(renderer.render(eq(user), eq(pr), any(Iterable.class), eq(ImmutableSet.<Long>of()), captor.capture(),
                anyCollection()))
            .thenReturn(ImmutableSet.of(mailMessage));

        send(batchId, ImmutableListMultimap.of(user, notification));

        verifySent(mailMessage, notification);

        ToBranchUpdate toBranchUpdate = (ToBranchUpdate) Iterables.getOnlyElement(captor.getValue());
        assertThat(toBranchUpdate.getUser(), is(updater));
        assertThat(toBranchUpdate.getToBranch(), is(toBranch));
        assertThat(toBranchUpdate.getDate(), is(date));
    }

    @Test
    public void testNonWatcherMentionedInComment() {
        String mentionedUserName = "mentioneduser";
        ApplicationUser mentionedUser = user(1, mentionedUserName);

        PullRequestBatchId batchId = PullRequestBatchId.of(1, 2);
        long commentId = 11;
        String commentText = "@" + mentionedUserName + " what do you think?";
        InternalComment comment = comment(commentId, commentText);

        when(pullRequestService.getRootCommentId(1, 2L, commentId)).thenReturn(commentId);

        InternalPullRequest pr = pullRequest(batchId); // no watchers
        PullRequestActivity commentActivity = commentActivity(pr, comment);

        when(pullRequestService.getById(1, 2L)).thenReturn(pr);
        when(pullRequestService.searchActivities(any(PullRequestActivitySearchRequest.class), any(PageRequest.class)))
                .thenReturn(page(commentActivity));
        when(mentionHelper.getMentionedUsers(commentText)).thenReturn(ImmutableSet.of(mentionedUserName));

        MailMessage mailMessage = mockRender(mentionedUser, pr, ImmutableSet.of(commentActivity),
                ImmutableSet.of(commentId), Collections.<ToBranchUpdate>emptySet());

        UserNotification notification = commentNotification(commentId);
        send(batchId, ImmutableListMultimap.of(mentionedUser, notification));

        verifySent(mailMessage, notification);
    }

    @Test
    public void testUserNotWatchingDoesNotReceiveNotification() {
        PullRequestBatchId batchId = PullRequestBatchId.of(1, 2);
        UserNotification notification = activityNotification(11);
        ApplicationUser user = user(1, "user");
        InternalPullRequest pr = pullRequest(batchId); // no watchers
        Set<PullRequestActivity> activities = ImmutableSet.of(activity(pr, 11));
        when(pullRequestService.getById(1, 2L)).thenReturn(pr);
        when(pullRequestService.getActivitiesById(1, 2L, ImmutableSet.of(11L, 12L))).thenReturn(activities);

        send(batchId, ImmutableListMultimap.of(user, notification));

        verifyNotSent(notification);
    }

    @Test
    public void testUserWithoutReadPermissionToRepositoryDoesNotReceiveNotification() {
        PullRequestBatchId batchId = PullRequestBatchId.of(1, 2);
        UserNotification notification = activityNotification(11);
        ApplicationUser user = user(1, "user");
        InternalPullRequest pr = pullRequest(batchId);
        Set<PullRequestActivity> activities = ImmutableSet.of(activity(pr, 11));
        when(pullRequestService.getById(1, 2L)).thenReturn(pr);
        when(pullRequestService.getActivitiesById(1, 2L, ImmutableSet.of(11L, 12L))).thenReturn(activities);
        when(pullRequestService.searchActivities(any(PullRequestActivitySearchRequest.class), any(PageRequest.class)))
                .thenReturn(page());

        when(permissionService.hasRepositoryPermission(user, pr.getToRef().getRepository(), Permission.REPO_READ))
                .thenReturn(false);
        send(batchId, ImmutableListMultimap.of(user, notification));

        verifyNotSent(notification);
    }

    @Test
    public void testUnconfiguredMailServer() {
        when(mailService.isHostConfigured()).thenReturn(false);

        PullRequestBatchId batchId = PullRequestBatchId.of(1, 2);
        UserNotification notification = activityNotification(11);
        ApplicationUser user = user(1, "user1");

        send(batchId, ImmutableListMultimap.of(user, notification));

        verifyNotSent();
    }

    private static Page page(PullRequestActivity... activities) {
        return new PageImpl<>(mock(PageRequest.class), Arrays.asList(activities), true);
    }

    private static PullRequestActivity activity(InternalPullRequest pullRequest, long activityId) {
        return new InternalPullRequestActivity.Builder(pullRequest).id(activityId).build();
    }

    private static PullRequestCommentActivity commentActivity(InternalPullRequest pullRequest, InternalComment comment) {
        return new InternalPullRequestCommentActivity.Builder(pullRequest).comment(comment).build();
    }

    private static InternalComment comment(long id, String text, InternalComment... childComments) {
        InternalComment comment = mock(InternalComment.class);
        when(comment.getId()).thenReturn(id);
        when(comment.getText()).thenReturn(text);
        when(comment.getComments()).thenReturn(Arrays.<Comment>asList(childComments));
        return comment;
    }

    private static InternalPullRequest pullRequest(PullRequestBatchId batchId, ApplicationUser... watcherUsers) {
        InternalPullRequest pullRequest = mock(InternalPullRequest.class, Mockito.RETURNS_DEEP_STUBS);
        when(pullRequest.getToRef().getRepository().getId()).thenReturn(batchId.getRepositoryId());
        when(pullRequest.getId()).thenReturn(batchId.getPullRequestId());
        Set<Watcher> watchers = new HashSet<>();
        for (ApplicationUser watcherUser : watcherUsers) {
            Watcher watcher = mock(Watcher.class);
            when(watcher.getUser()).thenReturn(watcherUser);
            watchers.add(watcher);
        }
        doReturn(watchers).when(pullRequest).getWatchers();
        return pullRequest;
    }

    private static UserNotification activityNotification(long activityId) {
        return new UserNotificationBuilder().data("{\"activity\": " + activityId + "}").build();
    }

    private static UserNotification commentNotification(long commentId) {
        return new UserNotificationBuilder().data("{\"comment\": " + commentId + "}").build();
    }

    private static UserNotification toBranchUpdateNotification(int userId, String toBranch, Date date) {
        String data = "{\"userId\":" + userId + ",\"toBranch\":\"" + toBranch + "\",\"date\":" + date.getTime() + "}";
        return new UserNotificationBuilder().data(data).build();
    }

    private static ApplicationUser user(int id, String name) {
        ApplicationUser user = mock(ApplicationUser.class);
        when(user.getId()).thenReturn(id);
        when(user.getName()).thenReturn(name);
        return user;
    }

    private MailMessage mockRender(ApplicationUser user, PullRequest pr, Collection<PullRequestActivity> activities,
                                   Set<Long> commentIds, Set<ToBranchUpdate> branchUpdates) {
        MailMessage mailMessage = mock(MailMessage.class);
        Matcher<Iterable<PullRequestActivity>> itemsMatcher = Matchers.hasItems(activities.toArray(new PullRequestActivity[activities.size()]));
        Matcher<Iterable<PullRequestActivity>> sizeMatcher = Matchers.iterableWithSize(activities.size());
        when(renderer.render(eq(user), eq(pr), argThat(Matchers.allOf(itemsMatcher, sizeMatcher)), eq(commentIds),
                eq(branchUpdates), anyCollection())).thenReturn(ImmutableSet.of(mailMessage));
        return mailMessage;
    }

    private void send(PullRequestBatchId batchId, ListMultimap<ApplicationUser, UserNotification> notifications) {
        batchSender.send(batchId.encode(), notifications, sentNotifications);
    }

    private void verifySent(MailMessage message, UserNotification... expectedProcessedNotifications) {
        verify(mailService).submit(message);
        assertThat(sentNotifications, hasSize(expectedProcessedNotifications.length));
        assertThat(sentNotifications, hasItems(expectedProcessedNotifications));
    }

    private void verifyNotSent(UserNotification... expectedProcessedNotifications) {
        verifyZeroInteractions(renderer);
        verify(mailService, never()).submit(any(MailMessage.class));
        assertThat(sentNotifications, hasSize(expectedProcessedNotifications.length));
        assertThat(sentNotifications, hasItems(expectedProcessedNotifications));
    }
}
