package it.com.atlassian.stash.pull.notification;

import com.atlassian.bitbucket.async.AsyncTestUtils;
import com.atlassian.bitbucket.hamcrest.EmailMatcher;
import com.atlassian.bitbucket.pull.PullRequestParticipantStatus;
import com.atlassian.bitbucket.smtp.SmtpServer;
import com.atlassian.bitbucket.test.*;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;

import javax.mail.MessagingException;
import java.util.concurrent.TimeUnit;

import static java.lang.String.format;
import static org.hamcrest.Matchers.allOf;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.hasSize;
import static org.junit.Assert.assertThat;

/**
 * @see PullRequestImmediateNotificationTest
 */
public class PullRequestBatchNotificationTest extends BasePullRequestNotificationTest {

    private static final long DEFAULT_WAIT_FOR_EXTRA_NOTIFICATIONS = TimeUnit.SECONDS.toMillis(5);
    private static final long DEFAULT_WAIT_FOR_NOTIFICATIONS_TIMEOUT = SmtpServer.DEFAULT_TIMEOUT_IN_MS;

    @Before
    public void setupUsers() {
        UserTestHelper.setNotificationSendMode(COMMENT_AUTHOR_USERNAME, UserTestHelper.SEND_MODE_BATCHED);
        UserTestHelper.setNotificationSendMode(MENTION_USERNAME, UserTestHelper.SEND_MODE_BATCHED);
        UserTestHelper.setNotificationSendMode(AUTHOR_1, AUTHOR_1, UserTestHelper.SEND_MODE_BATCHED);
    }

    @After
    @Override
    public void stopSmtpServer() throws Exception {
        /*
         * We need to decline here so that we can make sure we get the DECLINED
         * notification before ending the test so as not to affect other tests.
         *
         * We decline as the admin user so we can verify the PR creator got the
         * message.
         */
        declinePullRequest(pullRequestId, DefaultFuncTestData.getAdminUser(), DefaultFuncTestData.getAdminPassword());
        pullRequestId = -1L; // so that later cleanup methods don't try and decline
        BatchNotificationTestHelper.flushBatchNotificationQueue();
        smtpServer.waitForEmail(new EmailMatcher()
                .addressedTo(AUTHOR_1_EMAIL_ADDRESS)
                .withBody(containsString("DECLINED"))
        );
        super.stopSmtpServer();
    }

    @Test
    public void testAddingReviewerSendsImmediately() throws Exception {
        getPullRequestTestHelper(AUTHOR_1, AUTHOR_1, pullRequestId).addReviewers(COMMENT_AUTHOR_USERNAME);
        // email should be immediate even though users are batched
        smtpServer.waitForEmail(new EmailMatcher()
                        .addressedTo(COMMENT_AUTHOR_EMAIL_ADDRESS)
                        .withBody(containsString("added you as a reviewer"))
        );
    }

    @Test
    public void testEditingAndDeletingCommentNotifications() throws Exception {
        AsyncTestUtils.assertWaitForChangedBy(2, BatchNotificationTestHelper::getBatchNotificationCount, () -> {
            long deletedCommentId = commentOnPullRequest(pullRequestId, COMMENT_AUTHOR_USERNAME, "Deleted comment");
            long editedCommentId = commentOnPullRequest(pullRequestId, COMMENT_AUTHOR_USERNAME, format("Edited comment for %s", MENTION_USERNAME));
            editCommentOnPullRequest(pullRequestId, editedCommentId, COMMENT_AUTHOR_USERNAME, "Edited comment with no mention");
            deleteCommentOnPullRequest(pullRequestId, deletedCommentId, COMMENT_AUTHOR_USERNAME);
            return null;
        }, DEFAULT_WAIT_FOR_NOTIFICATIONS_TIMEOUT);

        BatchNotificationTestHelper.flushBatchNotificationQueue();

        smtpServer.waitForEmail(new EmailMatcher()
                .addressedTo(AUTHOR_1_EMAIL_ADDRESS)
                .withBody(allOf(containsString("1 Comment"),
                        containsString("Edited comment with no mention")))
        );

        // there should only be one email received - for the edit comment by the author,
        // mentioned user should not get an email
        assertThatNoMoreEmailsAreReceivedThan(1);
    }

    @Test
    public void testSimpleMultipleBatchActivities() throws Exception {
        // we timestamp the file in case this test gets Retried to ensure we're
        // adding a unique file to the repo each time
        final long timestamp = System.nanoTime();
        String fileNameWithBrokenDomainInference = "Monthly_" + timestamp + "<span>.</span>bak";
        String pathNameWithSpaces = "some / sub / directory";

        /*
         * The 6 notifications are:
         * - comment to PR author
         * - approval to PR author
         * - commit pushed to comment author
         * - commit pushed to PR author (pushed by admin)
         * - file comment to PR author
         * - file comment to admin as they're now a watcher
         */
        AsyncTestUtils.assertWaitForChangedBy(7, BatchNotificationTestHelper::getBatchNotificationCount, () -> {
            String pathName = "some/sub/directory";
            String fileName = "Monthly_" + timestamp + ".bak";
            int lineNumber = 1;
            String commentText = "This is a line comment!";

            commentOnPullRequest(pullRequestId, COMMENT_AUTHOR_USERNAME, "A comment");
            PullRequestTestHelper pr = getPullRequestTestHelper(COMMENT_AUTHOR_USERNAME, COMMENT_AUTHOR_USERNAME, pullRequestId);
            pr.setStatusFor(COMMENT_AUTHOR_USERNAME, PullRequestParticipantStatus.APPROVED);

            RepositoryTestHelper.pushCommit(DefaultFuncTestData.getBaseURL(), DefaultFuncTestData.getAdminUser(), DefaultFuncTestData.getAdminPassword(),
                    PROJECT_KEY, REPO_SLUG, PR_BRANCH,
                    AUTHOR_1_DISPLAY_NAME, AUTHOR_1_EMAIL_ADDRESS,
                    pathName, fileName, "Contents", "Added a test file");

            commentOnFileInPullRequest(pullRequestId, COMMENT_AUTHOR_USERNAME, pathName + "/" + fileName, lineNumber, commentText);
            return null;
        }, DEFAULT_WAIT_FOR_NOTIFICATIONS_TIMEOUT);

        BatchNotificationTestHelper.flushBatchNotificationQueue();

        smtpServer.waitForEmail(new EmailMatcher()
                .addressedTo(AUTHOR_1_EMAIL_ADDRESS)
                .withBody(allOf(containsString("A comment"),
                        containsString("line comment"),
                        containsString("1 Approval, 1 Push, 2 Comments"),
                        containsString("APPROVED"),
                        containsString("joined as a reviewer"),
                        containsString("Added a test file"),
                        containsString(pathNameWithSpaces),
                        containsString(fileNameWithBrokenDomainInference),
                        containsString("Don't want to receive batch emails anymore")))
        );
    }

    @Test
    public void testTwoPRs() throws Exception {
        AsyncTestUtils.assertWaitForChangedBy(2, BatchNotificationTestHelper::getBatchNotificationCount, () -> {
            commentOnPullRequest(pullRequestId, COMMENT_AUTHOR_USERNAME, "New comment");

            PullRequestTestHelper otherPullRequest = createPullRequest(new PullRequestBuilder(
                    BRANCH_THAT_HAS_FILE_PERMISSIONS_CHANGED_SRC, BRANCH_THAT_HAS_FILE_PERMISSIONS_CHANGED_TRGT)
                    .title("PR2")
                    .build());

            commentOnPullRequest(otherPullRequest.getId(), COMMENT_AUTHOR_USERNAME, "Other comment");

            return null;
        }, DEFAULT_WAIT_FOR_NOTIFICATIONS_TIMEOUT);

        BatchNotificationTestHelper.flushBatchNotificationQueue();

        // As long as both emails arrive within the timeout then regardless of order
        // both of these waits should work as expected
        smtpServer.waitForEmail(new EmailMatcher()
                .withSubject(containsString(OPEN_PULL_REQUEST_TITLE))
                .addressedTo(AUTHOR_1_EMAIL_ADDRESS)
                .withBody(allOf(
                        containsString("New comment"),
                        containsString("1 Comment")
                ))
        );

        smtpServer.waitForEmail(new EmailMatcher()
                .withSubject(containsString("PR2"))
                .addressedTo(AUTHOR_1_EMAIL_ADDRESS)
                .withBody(allOf(
                        containsString("Other comment"),
                        containsString("1 Comment")
                ))
        );
    }

    @Test
    public void testUnwatchStopsNotifications() throws Exception {
        addWatcher(pullRequestId, MENTION_USERNAME);

        AsyncTestUtils.assertWaitForChangedBy(2, BatchNotificationTestHelper::getBatchNotificationCount, () -> {
            commentOnPullRequest(pullRequestId, COMMENT_AUTHOR_USERNAME, "This is a comment.");
            return null;
        }, DEFAULT_WAIT_FOR_NOTIFICATIONS_TIMEOUT);

        BatchNotificationTestHelper.flushBatchNotificationQueue();

        smtpServer.waitForEmail(new EmailMatcher()
            .addressedTo(MENTION_USER_EMAIL)
            .withBody(allOf(containsString("This is a comment."),
                    containsString("1 Comment")))
        );

        /*
         * The approach for checking whether an email WASN'T sent is thus:
         *
         * - we remove the watcher, make a comment on the PR and flush the notification queue,
         * - we assert that the PR author receives a notification about the added comment,
         * - we assert that no other notification is by waiting for a small timeout at
          *  the end of the test.
         */
        removeWatcher(pullRequestId, MENTION_USERNAME);

        AsyncTestUtils.assertWaitForChangedBy(1, BatchNotificationTestHelper::getBatchNotificationCount, () -> {
            commentOnPullRequest(pullRequestId, COMMENT_AUTHOR_USERNAME, "This is another comment.");
            return null;
        }, DEFAULT_WAIT_FOR_NOTIFICATIONS_TIMEOUT);

        BatchNotificationTestHelper.flushBatchNotificationQueue();

        smtpServer.waitForEmail(new EmailMatcher()
            .addressedTo(AUTHOR_1_EMAIL_ADDRESS)
            .withBody(containsString("This is another comment.")));

        /*
         *  Check that no more than 3 notifications were received:
         *
         * - 2 comment notifications for the PR creator and the watcher (corresponding to the initial comment);
         * - 1 comment notification for the PR creator (corresponding to the replied comment) since the watcher
         *   was removed.
         */
        assertThatNoMoreEmailsAreReceivedThan(3);
    }

    private void assertThatNoMoreEmailsAreReceivedThan(int expectedTotal) throws InterruptedException, MessagingException {
        // ensure no extra email was sent
        Thread.sleep(DEFAULT_WAIT_FOR_EXTRA_NOTIFICATIONS);
        assertThat("the SMTP server received more notifications than expected",
                smtpServer.getReceivedEmails(), hasSize(expectedTotal));
    }
}
