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

import com.atlassian.bitbucket.hamcrest.EmailMatcher;
import com.atlassian.bitbucket.permission.Permission;
import com.atlassian.bitbucket.test.PermissionsTestHelper;
import com.atlassian.bitbucket.test.PullRequestTestHelper;
import com.atlassian.bitbucket.test.UserTestHelper;
import com.atlassian.webdriver.bitbucket.util.DateUtils;
import it.com.atlassian.stash.TimeZoneTestHelper;
import org.junit.Before;
import org.junit.Test;

import javax.mail.internet.MimeMessage;
import java.time.ZoneId;
import java.time.ZoneOffset;
import java.util.Optional;

import static com.atlassian.bitbucket.hamcrest.RegexMatcher.matchesRegex;
import static com.atlassian.bitbucket.hamcrest.StringContainsCaseInsensitive.containsCaseInsensitiveString;
import static com.atlassian.bitbucket.test.PullRequestRestTestHelper.getPullRequestViewURL;
import static java.lang.String.format;
import static org.apache.commons.lang3.StringEscapeUtils.escapeHtml4;
import static org.apache.commons.lang3.StringUtils.repeat;
import static org.hamcrest.Matchers.*;

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

    private static final String ADMIN_USERNAME = "admin";
    private static final String ADMIN_MAIL_ADDRESS = "admin@example.com";

    @Before
    public void setupUserNotificationPrefs() {
        // Ensure all actors email preferences are "immediate" to make sure tests work once batching is the default
        UserTestHelper.setNotificationSendMode(COMMENT_AUTHOR_USERNAME, UserTestHelper.SEND_MODE_IMMEDIATE);
        UserTestHelper.setNotificationSendMode(MENTION_USERNAME, UserTestHelper.SEND_MODE_IMMEDIATE);
        UserTestHelper.setNotificationSendMode(AUTHOR_1, AUTHOR_1, UserTestHelper.SEND_MODE_IMMEDIATE);
    }

    @Test
    public void testDecliningPullRequestSendsNotificationToWatchers() {
        addWatcher(pullRequestId, COMMENT_AUTHOR_USERNAME);

        declinePullRequest(pullRequestId);

        smtpServer.waitForEmail(new EmailMatcher()
                                        .addressedTo(COMMENT_AUTHOR_EMAIL_ADDRESS)
                                        .withBody(allOf(containsString(escapeHtml4(AUTHOR_1_DISPLAY_NAME)),
                                                        containsCaseInsensitiveString("declined"))));
    }

    @Test
    public void testEmailsAreThreaded() throws Exception {
        pullRequestId = createPullRequest(new PullRequestBuilder(
                BRANCH_THAT_HAS_SAME_FILE_MODIFIED_AND_MOVED_SRC, BRANCH_THAT_HAS_SAME_FILE_MODIFIED_AND_MOVED_TRGT)
                .description("this is a pull request description")
                .reviewer(MENTION_USERNAME)
                .build()).getId();

        MimeMessage firstEmail = smtpServer.waitForEmail(new EmailMatcher()
                .addressedTo(MENTION_USER_EMAIL)
                .withBodyAsText(containsString("this is a pull request description")));

        String inReplyToHeader = firstEmail.getHeader(IN_REPLY_TO_HEADER)[0];
        String referencesHeader = firstEmail.getHeader(REFERENCES_HEADER)[0];

        long commentId = commentOnPullRequest(pullRequestId, COMMENT_AUTHOR_USERNAME, "this is a comment");
        smtpServer.waitForEmail(new EmailMatcher()
                .addressedTo(MENTION_USER_EMAIL)
                .withHeader(IN_REPLY_TO_HEADER, inReplyToHeader)
                .withHeader(REFERENCES_HEADER, referencesHeader)
                .withBodyAsText(containsString("this is a comment")));

        replyToPullRequestComment(pullRequestId, COMMENT_AUTHOR_USERNAME, commentId, "i like to reply to my own comments");
        smtpServer.waitForEmail(new EmailMatcher()
                .addressedTo(MENTION_USER_EMAIL)
                .withHeader(IN_REPLY_TO_HEADER, inReplyToHeader)
                .withHeader(REFERENCES_HEADER, referencesHeader)
                .withBodyAsText(containsString("i like to reply to my own comments")));

        commentOnFileInPullRequest(pullRequestId, COMMENT_AUTHOR_USERNAME, "fakeFileName.txt", 123, "i'm commenting on a file");
        smtpServer.waitForEmail(new EmailMatcher()
                .addressedTo(MENTION_USER_EMAIL)
                .withHeader(IN_REPLY_TO_HEADER, inReplyToHeader)
                .withHeader(REFERENCES_HEADER, referencesHeader)
                .withBodyAsText(containsString("i'm commenting on a file")));

        declinePullRequest(pullRequestId);
        smtpServer.waitForEmail(new EmailMatcher()
                .addressedTo(MENTION_USER_EMAIL)
                .withHeader(IN_REPLY_TO_HEADER, inReplyToHeader)
                .withHeader(REFERENCES_HEADER, referencesHeader)
                .withBodyAsText(containsCaseInsensitiveString("declined")));

        reopenPullRequest(pullRequestId);
        smtpServer.waitForEmail(new EmailMatcher()
                .addressedTo(MENTION_USER_EMAIL)
                .withHeader(IN_REPLY_TO_HEADER, inReplyToHeader)
                .withHeader(REFERENCES_HEADER, referencesHeader)
                .withBodyAsText(containsCaseInsensitiveString("reopened")));
    }

    @Test
    public void testMergingPullRequestSendsNotification() {
        //upgrade pull request creator to write permission - use the class test context as we don't want it cleaned up
        PermissionsTestHelper.setProjectPermissionForUser(PROJECT_KEY, AUTHOR_1, Permission.PROJECT_WRITE.name());

        try {
            pullRequestId = createPullRequest(new PullRequestBuilder(
                    BRANCH_THAT_HAS_SAME_FILE_MODIFIED_AND_MOVED_SRC_MERGE,
                    BRANCH_THAT_HAS_SAME_FILE_MODIFIED_AND_MOVED_TRGT_MERGE).build()).getId();

            addWatcher(pullRequestId, COMMENT_AUTHOR_USERNAME);

            mergePullRequest(pullRequestId);

            smtpServer.waitForEmail(new EmailMatcher()
                                            .addressedTo(COMMENT_AUTHOR_EMAIL_ADDRESS)
                                            .withBody(allOf(containsString(escapeHtml4(AUTHOR_1_DISPLAY_NAME)),
                                                            containsCaseInsensitiveString("merged"))));
        } finally {
            //restore read permission
            PermissionsTestHelper.setProjectPermissionForUser(PROJECT_KEY, AUTHOR_1, Permission.PROJECT_READ.name());
        }
    }

    @Test
    public void testPullRequestCommentAndReplyAreTruncatedWithEllipses() {
        String commentText = repeat("A", 32000);
        String replyText = repeat("B", 32000);

        int maximumCommentDisplayLength = 2045;
        String expectedTruncatedComment = commentText.substring(0, maximumCommentDisplayLength) + "...";
        String expectedTruncatedReply = replyText.substring(0, maximumCommentDisplayLength) + "...";

        long commentId = commentOnPullRequest(pullRequestId, COMMENT_AUTHOR_USERNAME, commentText);

        smtpServer.waitForEmail(new EmailMatcher()
                .addressedTo(AUTHOR_1_EMAIL_ADDRESS)
                .withBody(containsString(expectedTruncatedComment)));

        replyToPullRequestComment(pullRequestId, COMMENT_AUTHOR_USERNAME, commentId, replyText);

        smtpServer.waitForEmail(new EmailMatcher()
                .addressedTo(AUTHOR_1_EMAIL_ADDRESS)
                .withBody(allOf(containsString(expectedTruncatedComment),
                        containsString(expectedTruncatedReply))));
    }

    @Test
    public void testPullRequestCommentMentionOfWatchingUserNotifiesMentionedWatchingUser() {
        commentOnPullRequest(pullRequestId, COMMENT_AUTHOR_USERNAME, format("A comment @%s", AUTHOR_1));

        smtpServer.waitForEmail(new EmailMatcher()
                .addressedTo(AUTHOR_1_EMAIL_ADDRESS)
                .withBody(allOf(containsMention("A comment @%s", AUTHOR_1_DISPLAY_NAME),
                        containsString(escapeHtml4(COMMENT_AUTHOR_DISPLAY_NAME)),
                        containsCaseInsensitiveString("mentioned"))));

        // May want a negative test to prove that we haven't sent the watcher an email saying there is a comment.
        // However, that can be quite tricky to write.  Need some way to ensure that "no more emails" isn't because
        // mail delivery is just slow.
    }

    @Test
    public void testPullRequestCommentMentionOnNonWatchingUserNotifiesNonWatchingMentionedUserAndWatchers() {
        commentOnPullRequest(pullRequestId, COMMENT_AUTHOR_USERNAME, format("A comment @%s", MENTION_USERNAME));

        smtpServer.waitForEmail(new EmailMatcher()
                .addressedTo(MENTION_USER_EMAIL)
                .withBody(allOf(containsMention("A comment @%s", MENTION_DISPLAY_NAME),
                        containsString(escapeHtml4(COMMENT_AUTHOR_DISPLAY_NAME)),
                        containsString(escapeHtml4(MENTION_DISPLAY_NAME)))));

        smtpServer.waitForEmail(new EmailMatcher()
                .addressedTo(AUTHOR_1_EMAIL_ADDRESS)
                .withBody(allOf(containsMention("A comment @%s", MENTION_DISPLAY_NAME),
                        containsString(escapeHtml4(COMMENT_AUTHOR_DISPLAY_NAME)),
                        containsString(escapeHtml4(MENTION_DISPLAY_NAME)))));
    }

    @Test
    public void testPullRequestCommentSendsNotificationWithLotsOfDetailsToCommentUserAndWatcher() {
        commentOnPullRequest(pullRequestId, COMMENT_AUTHOR_USERNAME, "A comment");

        smtpServer.waitForEmail(new EmailMatcher()
                .addressedTo(AUTHOR_1_EMAIL_ADDRESS)
                .withSubject(allOf(containsString(PROJECT_KEY),
                        containsString(REPO_SLUG)))
                .withBodyAsText(containsString("A comment"))
                .withBody(allOf(containsString(getPullRequestViewURL(PROJECT_KEY, REPO_SLUG, pullRequestId)),
                        containsString(getUnwatchUrl(PROJECT_KEY, REPO_SLUG, pullRequestId)),
                        containsString(getNotificationSettingsUrl()),
                        containsString(escapeHtml4(COMMENT_AUTHOR_DISPLAY_NAME)),
                        containsString("gravatar.com/avatar/" + COMMENT_AUTHOR_EMAIL_MD5 + ".jpg"))));
    }

    @Test
    public void testPullRequestCommentSendsNotificationWithTransformedMarkdownCommentContent() {
        commentOnPullRequest(pullRequestId, COMMENT_AUTHOR_USERNAME, "# A heading comment");

        smtpServer.waitForEmail(new EmailMatcher()
                .addressedTo(AUTHOR_1_EMAIL_ADDRESS)
                .withBody(matchesRegex("<h1.*?>A heading comment</h1>")));
    }

    @Test
    public void testPullRequestCommentReplySendsNotificationWithOriginalCommentAndReply() {
        String commentText = "Beer and Pretzels -- Breakfast of Champions.";
        String replyText = "A reply";

        long commentId = commentOnPullRequest(pullRequestId, COMMENT_AUTHOR_USERNAME, commentText);

        smtpServer.waitForEmail(new EmailMatcher()
                .addressedTo(AUTHOR_1_EMAIL_ADDRESS)
                .withBody(containsString(commentText)));

        replyToPullRequestComment(pullRequestId, COMMENT_AUTHOR_USERNAME, commentId, replyText);

        smtpServer.waitForEmail(new EmailMatcher()
                .addressedTo(AUTHOR_1_EMAIL_ADDRESS)
                .withBody(allOf(containsString(commentText),
                        containsString(replyText))));
    }

    @Test
    public void testPullRequestAddingReviewerSendsNotificationWithCommitList() {
        TimeZoneTestHelper.withServerTimeZone(Optional.empty(), this::checkAddedReviewerEmail);
    }

    @Test
    public void testPullRequestAddingReviewerSendsNotificationWithCommitListInYesterdayServerTz() {
        TimeZoneTestHelper.withServerTimeZone(Optional.of(ZoneOffset.MIN), this::checkAddedReviewerEmail);
    }

    @Test
    public void testPullRequestAddingReviewerSendsNotificationWithCommitListInTodayServerTz() {
        TimeZoneTestHelper.withServerTimeZone(Optional.of(ZoneOffset.MAX), this::checkAddedReviewerEmail);
    }

    @Test
    public void testPullRequestAddingReviewerSendsNotificationWithCommitListForUsersInDifferentTz() {
        ZoneOffset serverTz = ZoneOffset.MIN;
        ZoneOffset userTz = ZoneOffset.MAX;

        PullRequestTestHelper pullRequest = testContext.pullRequest(new PullRequestBuilder(
                BRANCH_THAT_HAS_SAME_FILE_MODIFIED_AND_MOVED_SRC, BRANCH_THAT_HAS_SAME_FILE_MODIFIED_AND_MOVED_TRGT)
                        .description("# Description")
                        .build());

        TimeZoneTestHelper.withServerAndUserTimeZone(Optional.of(serverTz), Optional.of(userTz), (ignored) -> {
            pullRequest.addReviewers(COMMENT_AUTHOR_USERNAME, ADMIN_USERNAME);

            String headCommit = "2012-08-08T10:20:12+1000";
            smtpServer.waitForEmail(new EmailMatcher()
                    .addressedTo(ADMIN_MAIL_ADDRESS)
                    .withBody(allOf(containsString("added you as a reviewer"),
                            containsString(DateUtils.DATE_TIME.withTimeZone(userTz).formatTimestamp(headCommit)))));

            smtpServer.waitForEmail(new EmailMatcher()
                    .addressedTo(COMMENT_AUTHOR_EMAIL_ADDRESS)
                    .withBody(allOf(containsString("added you as a reviewer"),
                            containsString(DateUtils.DATE_TIME.withTimeZone(serverTz).formatTimestamp(headCommit)))));

            return null;
        });
    }

    @Test
    public void testPullRequestDiffCommentContainsCommentAndFileDetails() {
        String fileName = "Monthly_416.bak";
        String fileNameWithBrokenDomainInference = "Monthly_416<span>.</span>bak";
        int lineNumber = 666;
        String commentText = "This is a line comment!";

        commentOnFileInPullRequest(pullRequestId, COMMENT_AUTHOR_USERNAME, fileName, lineNumber, commentText);

        smtpServer.waitForEmail(new EmailMatcher()
                .addressedTo(AUTHOR_1_EMAIL_ADDRESS)
                .withBody(allOf(containsString(commentText),
                        not(containsString(fileName)),
                        containsString(fileNameWithBrokenDomainInference),
                        containsString("of <strong>" + fileNameWithBrokenDomainInference),
                        containsString(String.valueOf(lineNumber)),
                        containsString(escapeHtml4(COMMENT_AUTHOR_DISPLAY_NAME)))));
    }

    @Test
    public void testPullRequestMentionInInitialDescriptionSendsNotificationToMentionedUser() {
        PullRequestTestHelper pullRequest = createPullRequest(new PullRequestBuilder(
                BRANCH_THAT_HAS_SAME_FILE_MODIFIED_AND_MOVED_SRC, BRANCH_THAT_HAS_SAME_FILE_MODIFIED_AND_MOVED_TRGT)
                .description("Mentioning @" + MENTION_USERNAME)
                .build());

        pullRequestId = pullRequest.getId();

        smtpServer.waitForEmail(new EmailMatcher()
                .addressedTo(MENTION_USER_EMAIL)
                .withBody(allOf(containsMention("Mentioning @%s", MENTION_DISPLAY_NAME),
                        containsString(escapeHtml4(AUTHOR_1_DISPLAY_NAME)),
                        containsCaseInsensitiveString("mentioned"))));
    }

    @Test
    public void testPullRequestMentionInUpdatedDescriptionSendsNotificationToMentionedUser() {
        PullRequestTestHelper pullRequest = createPullRequest(new PullRequestBuilder(
                BRANCH_THAT_HAS_SAME_FILE_MODIFIED_AND_MOVED_SRC, BRANCH_THAT_HAS_SAME_FILE_MODIFIED_AND_MOVED_TRGT)
                .build());
        pullRequest.setDescription(format("Updating description to mention @%s", MENTION_USERNAME));

        smtpServer.waitForEmail(new EmailMatcher()
                .addressedTo(MENTION_USER_EMAIL)
                .withBody(allOf(containsMention("Updating description to mention @%s", MENTION_DISPLAY_NAME),
                        containsString(escapeHtml4(AUTHOR_1_DISPLAY_NAME)),
                        containsCaseInsensitiveString("mentioned"))));
    }

    @Test
    public void testReopeningPullRequestSendsNotificationToWatchers() {
        addWatcher(pullRequestId, COMMENT_AUTHOR_USERNAME);

        declinePullRequest(pullRequestId);

        reopenPullRequest(pullRequestId);

        smtpServer.waitForEmail(new EmailMatcher()
                .addressedTo(COMMENT_AUTHOR_EMAIL_ADDRESS)
                .withBody(allOf(containsString(escapeHtml4(AUTHOR_1_DISPLAY_NAME)),
                        containsCaseInsensitiveString("reopened"))));
    }

    private Void checkAddedReviewerEmail(Optional<? extends ZoneId> zoneId) {

        PullRequestTestHelper pullRequest = createPullRequest(new PullRequestBuilder(
                BRANCH_THAT_HAS_SAME_FILE_MODIFIED_AND_MOVED_SRC, BRANCH_THAT_HAS_SAME_FILE_MODIFIED_AND_MOVED_TRGT)
                .description("# Description")
                .build());

        pullRequest.addReviewers(COMMENT_AUTHOR_USERNAME);
        pullRequestId = pullRequest.getId();

        smtpServer.waitForEmail(new EmailMatcher()
                .addressedTo(COMMENT_AUTHOR_EMAIL_ADDRESS)
                .withBody(allOf(containsString(escapeHtml4(AUTHOR_1_DISPLAY_NAME)),
                        containsString("reviewer"),
                        matchesRegex("<h1.*?>Description</h1>"),
                        containsString("commits-table"),
                        containsString("Rowland Rempel DDS"),
                        containsString("ff851cdafd5c9bda5b19d151c5f638f13c2b2f6e"),
                        matchesRegex("<a.+>ff851cdafd5</a>"),
                        containsString(DateUtils.DATE_TIME.withTimeZone(zoneId).formatTimestamp("2012-08-08T10:20:12+1000")),
                        containsString("Modifying file on source branch"),
                        not(containsString(getNotificationSettingsUrl())) // adding reviewer is not batchable
                )));
        return null;
    }
}
