package it.com.atlassian.stash.pull;

import com.atlassian.bitbucket.permission.Permission;
import com.atlassian.bitbucket.pull.PullRequestState;
import com.atlassian.bitbucket.test.AnalyticsTestHelper;
import com.atlassian.bitbucket.test.BuildStatusTestHelper;
import com.atlassian.bitbucket.test.PullRequestTestHelper;
import com.atlassian.pageobjects.elements.query.Poller;
import com.atlassian.webdriver.bitbucket.element.*;
import com.atlassian.webdriver.bitbucket.element.activity.*;
import com.atlassian.webdriver.bitbucket.page.*;
import com.google.common.collect.ImmutableMap;
import it.com.atlassian.stash.rules.WebStorageCleanUpRule;
import org.apache.commons.lang3.StringEscapeUtils;
import org.hamcrest.Matcher;
import org.junit.After;
import org.junit.Before;
import org.junit.Ignore;
import org.junit.Test;
import org.junit.rules.TestRule;

import java.util.List;
import java.util.Map;

import static com.atlassian.bitbucket.permission.Permission.PROJECT_WRITE;
import static com.atlassian.bitbucket.test.TestRuleOrderingSyntax.outerRules;
import static com.atlassian.pageobjects.elements.query.Conditions.forSupplier;
import static com.atlassian.pageobjects.elements.query.Poller.*;
import static com.atlassian.webdriver.bitbucket.element.Comment.EditCommentForm;
import static com.atlassian.webdriver.bitbucket.element.Comment.ReplyCommentForm;
import static org.hamcrest.Matchers.*;
import static org.junit.Assert.*;

@SuppressWarnings({"ConstantConditions", "unchecked"})
public class PullRequestViewTest extends BasePullRequestFuncTest {

    private static final String SOURCE_COMMIT = "ff851cdafd5c9bda5b19d151c5f638f13c2b2f6e";
    private static final String SOURCE_BRANCH = BRANCH_THAT_HAS_SAME_FILE_MODIFIED_AND_MOVED_SRC;
    private static final String TARGET_BRANCH = BRANCH_THAT_HAS_SAME_FILE_MODIFIED_AND_MOVED_TRGT;
    private static final String BRANCH_THAT_HAS_SAME_FILE_MODIFIED_AND_MOVED_SRC_HASH = "ff851cdafd5c9bda5b19d151c5f638f13c2b2f6e";
    private static final String BRANCH_THAT_HAS_SAME_FILE_MODIFIED_AND_MOVED_TRGT_HASH = "ca9868121d19677bfc9d93e7ed6fd30b11106974";

    @Before
    public void setup() {
        AnalyticsTestHelper.enableEventLogging();
    }

    @After
    public void tearDown() {
        AnalyticsTestHelper.disableEventLogging();
    }

    @Test
    public void testActivityPagination() {
        testContext.projectPermission(PROJECT_KEY, AUTHOR_1, PROJECT_WRITE);
        PullRequestTestHelper pullRequestTestHelper = createPullRequest(
                new PullRequestBuilder(SOURCE_BRANCH, TARGET_BRANCH).build());
        long pullRequestId = pullRequestTestHelper.getId();

        // Create a full page of general comments
        int pageSize = PullRequestOverviewPage.PAGE_SIZE;
        for (int i = 0; i < pageSize; i++) {
            pullRequestTestHelper.addGeneralComment("general comment " + i);
        }

        // Add an inline comment - to test activity with at least one diff view loaded
        PullRequestDiffPage diffPage = loginAsAdmin(PullRequestEffectiveDiffPage.class, PROJECT_KEY, REPO_SLUG, pullRequestId);
        UnifiedDiffFileContent.Line line = diffPage.getFileContent().getDiffLines().get(0);
        String inlineComment = "inline comment here";
        diffPage.addComment(line, inlineComment);

        PullRequestOverviewPage overviewPage = diffPage.switchToOverview();
        List<? extends ActivityItem> activityItems = overviewPage.getActivityItems();
        assertEquals("There should be " + pageSize + " activity items", pageSize, activityItems.size());
        DiffLineCommentActivity diffComment = (DiffLineCommentActivity) activityItems.get(0);
        assertEquals(inlineComment, diffComment.getFileContent().getComments().get(0).getCommentText());
        overviewPage.loadNextPage();

        activityItems = overviewPage.getActivityItems();
        // Additional 2 activity items: inline comment and pull request open
        assertEquals("There should be " + (pageSize + 2) + " activity items", pageSize + 2, activityItems.size());
    }

    @Test
    public void testAddGeneralComment() {
        PullRequestOverviewPage pullRequestPage = createPullRequestAndLogin(SOURCE_BRANCH, TARGET_BRANCH);
        GeneralCommentForm generalCommentForm = pullRequestPage.getGeneralCommentForm();

        // submit empty comment
        generalCommentForm.expand();
        generalCommentForm.submitExpectingErrors();
        assertTrue("Should have error when submitting empty comment", generalCommentForm.hasErrors());

        String commentText = "Func test comment test data <input type='text'>alert('666')</input>";
        generalCommentForm.setText(commentText);
        Comment comment = generalCommentForm.submit();
        assertEquals("Comment text should be same as entered", "Func test comment test data <input type='text'>alert('666')</input>", comment.getCommentText());
        assertEquals("Comment author should be user who submitted form", AUTHOR_1_DISPLAY_NAME, comment.getAuthor());
        assertFalse("Errors should be removed", generalCommentForm.hasErrors());
    }

    @Test
    public void testAddSelfAsReviewer() {
        PullRequestOverviewPage page = loginAs(REVIEWER_1, REVIEWER_1, PullRequestOverviewPage.class,
                PROJECT_KEY, REPO_SLUG, createPullRequest(new PullRequestBuilder(SOURCE_BRANCH, TARGET_BRANCH).build())
                        .getId());

        List<Participant> reviewers = page.getReviewers();
        assertThat("Pull request should have no reviewers", reviewers, is(empty()));
        assertThat("Add self as a reviewer button should be present", page.isAddSelfButtonPresent(), is(true));

        page.getAddSelfButton().clickButton();

        reviewers = page.getReviewers();
        assertThat("Add self as a reviewer button should not be present", page.isAddSelfButtonPresent(), is(false));
        assertThat("Pull request should have one reviewer", reviewers.size(), is(1));
        assertThat("Pull request should have " + REVIEWER_1 + " as a reviewer", reviewers.get(0).getUsername(),
                is(REVIEWER_1));

        ReviewerActivityItem activity = getFirstReviewerItemMatching(page, REVIEWER_1);
        assertThat("Activity should be for adding self as a reviewer", activity.getText(),
                containsString("joined as a reviewer"));
        assertThat("Activity should not contain details of multiple reviewers", activity.hasDetail(), is(false));
        assertThat("Activity username should be ", activity.getAvatar().getUsername(), is(REVIEWER_1));
        assertThat("Activity timestamp should be 'A moment ago'", activity.getTimePretty(), is("A moment ago"));
    }

    @Test
    public void testAddSelfAsReviewerAsAuthor() {
        PullRequestOverviewPage page = createPullRequestAndLogin(SOURCE_BRANCH, TARGET_BRANCH);

        assertThat("Pull request should have no reviewers", page.getReviewers(), is(empty()));
        assertThat("Add self as a reviewer button should not be present for author", page.isAddSelfButtonPresent(),
                is(false));
    }

    @Test
    public void testDeepLinkReplyToComment() {
        PullRequestOverviewPage pullRequestPage = createPullRequestAndLogin(SOURCE_BRANCH, TARGET_BRANCH);
        // Add a comment to the page and save it
        String commentText = "test comment";
        GeneralCommentForm generalCommentForm = pullRequestPage.getGeneralCommentForm();
        generalCommentForm.expand();
        generalCommentForm.setText(commentText);
        Comment comment = generalCommentForm.submit();

        // Build target URL params and navigate
        PullRequestOverviewPage newPage = BITBUCKET.visit(PullRequestOverviewPage.class, pullRequestPage.getProjectKey(),
                pullRequestPage.getSlug(), pullRequestPage.getPullRequestId(), comment.getCommentId(), "reply");

        // Assert comment is rendered, reply box is present below comment and visible
        Comment renderedComment = newPage.getGeneralCommentAt(0);
        assertEquals("Comment text should be the same as entered", commentText, renderedComment.getCommentText());
        ReplyCommentForm replyCommentForm = renderedComment.getReplyCommentForm();
        assertTrue("Reply comment form should be visible", replyCommentForm.isVisible());

        // Assert analytics plugin receives event and has the whitelist
        Map<String, Object> event = AnalyticsTestHelper.findWhitelistedEvent("stash.client.comment.actions.reply");
        assertThat(event, hasKey("properties"));
        assertThat(event, hasEntry(equalTo("properties"), (Matcher) hasKey("pullRequest.id")));
        assertThat(event, hasEntry(equalTo("properties"), (Matcher) hasKey("repository.id")));
        assertThat(event, hasEntry(equalTo("properties"), (Matcher) hasKey("comment.id")));
        assertThat(event, hasEntry(equalTo("properties"), (Matcher) hasKey("userRole")));
    }

    @Test
    public void testDecline() {
        PullRequestOverviewPage pullRequestPage = createPullRequestAndLogin(SOURCE_BRANCH, TARGET_BRANCH, PROJECT_WRITE);
        PullRequestDeclineDialog<PullRequestOverviewPage> declineDialog = pullRequestPage.getDeclineButton().clickButton();
        pullRequestPage = declineDialog.decline();
        assertFalse("Decline button still visible", pullRequestPage.getDeclineButton().isPresent());
        assertFalse("Merge button still visible", pullRequestPage.getMergeButton().isPresent());
        pullRequestPage = pullRequestPage.getReopenButton().clickButton();
        assertTrue("Merge button not visible", pullRequestPage.getMergeButton().isVisible());
    }

    @Test
    public void testDefaultActivities() {
        PullRequestOverviewPage pullRequestPage = createPullRequestAndLogin(SOURCE_BRANCH, TARGET_BRANCH);
        assertEquals(1, pullRequestPage.getActivityItems().size());
        StateActivityItem activityItem = (StateActivityItem) pullRequestPage.getActivityItems().get(0);
        assertEquals("OPENED", activityItem.getState());
        assertEquals(AUTHOR_1, activityItem.getAvatar().getUsername());
    }

    @Test
    public void testEdit() {
        PullRequestOverviewPage pullRequestPage = createPullRequestAndLogin(SOURCE_BRANCH, TARGET_BRANCH, PROJECT_WRITE);

        PullRequestEditDialog<PullRequestOverviewPage> editDialog = pullRequestPage.getEditButton().clickButton();
        PullRequestDetails pullRequestDetails = editDialog.getDetails();
        pullRequestDetails.setDescription("# New description <script>alert('xss');</script>");

        assertEquals("<h1>New description &lt;script&gt;alert('xss');&lt;/script&gt;</h1>\n",
                pullRequestDetails.togglePreview().getPreviewHtml());
        assertFalse(pullRequestDetails.togglePreview().isPreviewVisible());
        PullRequestOverviewPage page = editDialog.save();

        //The UPDATED activity will be created in the database, but should be filtered when loading the stream.
        //That means the lead activity should still be from when the pull request was opened.
        page.waitUntilActivityItemsLoaded();
        StateActivityItem updateActivity = (StateActivityItem) page.getActivityItems().get(0);
        assertEquals("OPENED", updateActivity.getState());

        assertEquals("<h1>New description &lt;script&gt;alert('xss');&lt;/script&gt;</h1>\n",
                page.getPullRequestDescriptionAsHtml());
    }

    @Test
    public void testEditDestinationBranch() {
        PullRequestOverviewPage pullRequestPage = createPullRequestAndLogin(SOURCE_BRANCH, TARGET_BRANCH, PROJECT_WRITE);
        PullRequestEditDialog<PullRequestOverviewPage> editDialog = pullRequestPage.getEditButton().clickButton();
        PullRequestDetails pullRequestDetails = editDialog.getDetails();
        String newTargetId = "refs/heads/" + BRANCH_THAT_HAS_SAME_FILE_MODIFIED_AND_MOVED_TRGT;
        pullRequestDetails.getToBranchSelector().open()
                .filterBy(BRANCH_THAT_HAS_SAME_FILE_MODIFIED_AND_MOVED_TRGT).selectItemById(newTargetId);

        PullRequestOverviewPage page = editDialog.save();

        assertEquals(BRANCH_THAT_HAS_SAME_FILE_MODIFIED_AND_MOVED_TRGT, page.getTargetBranch());
    }

    @Test
    public void testEditNotAllowedOnMergedPullRequest() {
        String sourceBranch = "test_edit_on_merged_pr_src";
        String targetBranch = "test_edit_on_merged_pr_target";

        testContext.branch(PROJECT_KEY, REPO_SLUG, sourceBranch, BRANCH_THAT_HAS_SAME_FILE_MODIFIED_AND_MOVED_SRC_HASH)
                .branch(PROJECT_KEY, REPO_SLUG, targetBranch, BRANCH_THAT_HAS_SAME_FILE_MODIFIED_AND_MOVED_TRGT_HASH);

        PullRequestTestHelper pullRequest = createPullRequest(
                new PullRequestBuilder(sourceBranch, targetBranch).build(),
                PullRequestState.MERGED
        );

        PullRequestOverviewPage pullRequestPage = loginAs(AUTHOR_1, AUTHOR_1, PullRequestOverviewPage.class, pullRequest.getProjectKey(),
                pullRequest.getRepoSlug(), pullRequest.getId());
        waitUntilFalse("Edit button still present", pullRequestPage.getEditButton().timed().isPresent());
        pullRequestPage.editWithShortcut();
        assertFalse("Edit dialog shown", pullRequestPage.getEditDialog().isVisible());
    }

    @Test
    public void testEditReviewers() {
        PullRequestDetails details = createPullRequestAndLogin(SOURCE_BRANCH, TARGET_BRANCH)
                .getEditButton().clickButton()
                .getDetails();

        details.addReviewer(REVIEWER_1);
        waitUntil(details.getReviewerUsernames(), contains(REVIEWER_1));

        // make sure non-licensed and non-repo-read users cannot be selected
        UserSelect reviewerSelect = details.getReviewerSelect();
        reviewerSelect.type(REVIEWER_UNLICENSED);
        waitUntilTrue("Unexpected options: " + reviewerSelect.getOptions().now(), reviewerSelect.hasNoOptions());
        reviewerSelect.clearFilter();

        reviewerSelect.type(REVIEWER_NO_REPOSITORY_ACCESS);
        waitUntilTrue("Unexpected options: " + reviewerSelect.getOptions().now(), reviewerSelect.hasNoOptions());
        reviewerSelect.clearFilter();

        details.removeReviewer(REVIEWER_1);
        waitUntil(details.getReviewerSelect().getSelectedOptions(), emptyIterable());
    }

    @Test
    public void testEditVisibleOnDeclinedPullRequest() {
        PullRequestOverviewPage pullRequestPage = createPullRequestAndLogin(SOURCE_BRANCH, TARGET_BRANCH, PROJECT_WRITE);
        PullRequestDeclineDialog<PullRequestOverviewPage> declineDialog = pullRequestPage.getDeclineButton().clickButton();
        pullRequestPage = declineDialog.decline();
        assertTrue("Edit button still present", pullRequestPage.getEditButton().isPresent());

        pullRequestPage.editWithShortcut();
        assertTrue("Edit dialog shown", pullRequestPage.getEditDialog().isVisible());
    }

    @Test
    public void testMerge() {
        PullRequestOverviewPage pullRequestPage =
                createPullRequestAndLogin(BRANCH_THAT_HAS_SAME_FILE_MODIFIED_AND_MOVED_SRC_MERGE,
                        BRANCH_THAT_HAS_SAME_FILE_MODIFIED_AND_MOVED_TRGT_MERGE, Permission.PROJECT_WRITE);
        pullRequestPage = pullRequestPage.getMergeButton().clickButton().confirmMerge();
        waitUntilFalse("Decline button still visible", pullRequestPage.getDeclineButton().timed().isPresent());
        assertFalse("Merge button still visible", pullRequestPage.getMergeButton().isPresent());
    }

    @Test
    public void testPreviewGeneralComment() {
        PullRequestOverviewPage pullRequestPage = createPullRequestAndLogin(SOURCE_BRANCH, TARGET_BRANCH);
        CommentForm generalCommentForm = pullRequestPage.getGeneralCommentForm();

        // fill a general comment
        String commentText = "General comment";
        generalCommentForm.setText(commentText);

        // try to preview it
        generalCommentForm.togglePreview();
        assertTrue("comment preview is not shown", generalCommentForm.isPreviewVisible());
        assertEquals("<p>General comment</p>\n", generalCommentForm.getPreviewContent());
        generalCommentForm.togglePreview();
        assertFalse("comment preview should not be visible", generalCommentForm.isPreviewVisible());

        // check the comment can be correctly submitted after toggling on and off the preview
        Comment comment = generalCommentForm.submit();
        assertEquals("Comment text should be same as entered", commentText, comment.getCommentText());
        assertEquals("Comment author should be user who submitted form", AUTHOR_1_DISPLAY_NAME, comment.getAuthor());
    }

    @Test
    public void testPullRequestBuildStatus() {
        BuildStatusTestHelper.addBuildStatus(SOURCE_COMMIT, "TEST-1", BuildStatusTestHelper.State.SUCCESSFUL);
        PullRequestOverviewPage pullRequestPage = createPullRequestAndLogin(SOURCE_BRANCH, TARGET_BRANCH);
        assertTrue("Build status should be present", pullRequestPage.hasBuildStatus());
        assertEquals("Build status dialog has a successful build",
                BuildStatusTestHelper.State.SUCCESSFUL, pullRequestPage.openBuildStatus()
                        .getBuildStatusLines()
                        .get(0)
                        .getStatus());
    }

    @Test
    public void testPullRequestCommitsPagination() {
        PullRequestOverviewPage overviewPage = createPullRequestAndLogin("101-additional-commits", TARGET_BRANCH);
        PullRequestCommitsPage commitsPage = overviewPage.switchToCommits();
        List<CommitList.CommitListEntry> commits = commitsPage.getCommitList().getRows();
        assertEquals("First page should be loaded", 50, commits.size());
        assertEquals("Commit 101 to many_commits.txt", commits.get(0).getMessage());
        assertEquals("Commit 52 to many_commits.txt", commits.get(49).getMessage());

        commitsPage.getCommitList().loadNextPage();
        commits = commitsPage.getCommitList().getRows();
        assertEquals("Second page should be loaded", 100, commits.size());
        assertEquals("Commit 2 to many_commits.txt", commits.get(99).getMessage());

        commitsPage.getCommitList().loadNextPage();
        commits = commitsPage.getCommitList().getRows();
        assertEquals("Third page should be loaded", 101, commits.size());
        assertEquals("Commit 1 to many_commits.txt", commits.get(100).getMessage());
    }

    @Test
    @Ignore("Flaky, cause of screen resolution: BSERVDEV-12109")
    public void testPullRequestMetadata() {
        testContext.projectPermission(PROJECT_KEY, AUTHOR_1, Permission.PROJECT_READ);

        String title = "<script src='http://ha.ckers.org/xss.js'></script>";
        String description = "# a description <script>alert('xss')</script>";
        PullRequestTestHelper pullRequest = createPullRequest(new PullRequestBuilder(SOURCE_BRANCH, TARGET_BRANCH)
                .title(title)
                .description(description)
                .build());

        PullRequestOverviewPage pullRequestPage = loginAsAdmin(PullRequestOverviewPage.class,
                PROJECT_KEY, REPO_SLUG, pullRequest.getId());
        assertEquals("Author of pull request should match", AUTHOR_1_DISPLAY_NAME, pullRequestPage.getPullRequestAuthor());
        assertEquals("Title of pull request should match", title, pullRequestPage.getPullRequestTitle());
        assertEquals("Description of pull request should match",
                "<h1>a description &lt;script&gt;alert('xss')&lt;/script&gt;</h1>\n", pullRequestPage.getPullRequestDescriptionAsHtml());
        assertEquals("Source branch of pull request should match", SOURCE_BRANCH, pullRequestPage.getSourceBranch());
        assertEquals("Target branch of pull request should match", TARGET_BRANCH, pullRequestPage.getTargetBranch());
    }

    @Test
    public void testPullRequestTabNotVisibleToAnonUser() {
        String repoName = "publicRepository";
        testContext.repository(PROJECT_KEY, repoName, true);
        FileBrowserPage overview = anonymous(FileBrowserPage.class, PROJECT_KEY, repoName);
        assertNull(overview.getItems().getItem("Pull requests"));
        // Check we are on the login page
        try {
            anonymous(PullRequestListPageAnonymous.class, PROJECT_KEY, repoName, "").isHere();
            fail("Anonymous user should be caught by the login page");
        } catch (AssertionError e) {
            // Nothing to do here.
        }
    }

    @Test
    public void testReadPermission() {
        testContext.user("testReadPermission", "testReadPermission")
                .projectPermission(PROJECT_KEY, AUTHOR_1, Permission.PROJECT_READ)
                .projectPermission(PROJECT_KEY, "testReadPermission", Permission.PROJECT_READ);
        PullRequestTestHelper pullRequest = createPullRequest(new PullRequestBuilder(SOURCE_BRANCH, TARGET_BRANCH)
                .reviewer(REVIEWER_1)
                .build());

        PullRequestOverviewPage pullRequestPage = loginAs("testReadPermission", "testReadPermission",
                PullRequestOverviewPage.class, PROJECT_KEY, REPO_SLUG, pullRequest.getId());

        assertFalse("Decline button visible", pullRequestPage.getDeclineButton().isPresent());
        assertFalse("Merge button visible", pullRequestPage.getMergeButton().isPresent());
        assertFalse("Edit button visible", pullRequestPage.canEdit());
    }

    @Test
    public void testRemoveSelfAsAReviewer() {
        PullRequestOverviewPage page = loginAs(REVIEWER_1, REVIEWER_1, PullRequestOverviewPage.class,
                PROJECT_KEY, REPO_SLUG, createPullRequest(new PullRequestBuilder(SOURCE_BRANCH, TARGET_BRANCH)
                        .reviewer(REVIEWER_1).build()).getId());

        List<Participant> reviewers = page.getReviewers();
        assertThat("Pull request should have one reviewer", reviewers.size(), is(1));
        assertThat("Pull request should have " + REVIEWER_1 + " as a reviewer", reviewers.get(0).getUsername(),
                is(REVIEWER_1));
        assertThat("Add self as a reviewer button should not be present", page.isAddSelfButtonPresent(), is(false));

        page.getRemoveSelfDialog().confirm();

        reviewers = page.getReviewers();
        assertThat("Pull request should have no reviewers", reviewers, is(empty()));
        assertThat("Add self as a reviewer button should be present", page.isAddSelfButtonPresent(), is(true));

        ReviewerActivityItem activity = getFirstReviewerItemMatching(page, REVIEWER_1);
        assertThat("Activity should be for removing self as a reviewer", activity.getText(),
                containsString("is no longer a reviewer"));
        assertThat("Activity should not contain details of multiple reviewers", activity.hasDetail(), is(false));
        assertThat("Activity username should be ", activity.getAvatar().getUsername(), is(REVIEWER_1));
        assertThat("Activity timestamp should be 'A moment ago'", activity.getTimePretty(), is("A moment ago"));
    }

    @Test
    public void testReplyEditAndDeleteForGeneralComment() {
        String commentText = "Func test comment test data <input type='text'>alert('666')</input>";
        testReplyEditAndDelete(commentText, "Func test comment test data <input type='text'>alert('666')</input>", "<p>%s</p>\n");
    }

    @Test
    public void testReplyEditAndDeleteMarkupForGeneralComment() {
        testReplyEditAndDelete("# header", "header", "<h1>%s</h1>\n");
    }

    @Test
    public void testWatchAndUnwatch() {
        PullRequestOverviewPage pullRequestPage = createPullRequestAndLogin(SOURCE_BRANCH, TARGET_BRANCH);
        assertTrue(pullRequestPage.currentUserIsWatching());

        pullRequestPage.unwatchPullRequest();
        assertFalse(pullRequestPage.currentUserIsWatching());

        pullRequestPage.watchPullRequest();
        assertTrue(pullRequestPage.currentUserIsWatching());
    }

    @Test
    public void testWatchForAddAndRemoveReviewer() {
        PullRequestTestHelper pullRequest = createPullRequest(new PullRequestBuilder(SOURCE_BRANCH, TARGET_BRANCH)
                .build());

        PullRequestOverviewPage pullRequestPage = loginAs(REVIEWER_1, REVIEWER_1, PullRequestOverviewPage.class,
                PROJECT_KEY, REPO_SLUG, pullRequest.getId());
        assertFalse("Not watching PR before being added as a reviewer", pullRequestPage.currentUserIsWatching());

        loginAs(AUTHOR_1, AUTHOR_1, PullRequestOverviewPage.class, PROJECT_KEY, REPO_SLUG, pullRequest.getId());
        PullRequestEditDialog editDialog = pullRequestPage.getEditButton().clickButton();
        PullRequestDetails details = editDialog.getDetails();
        details.addReviewer(REVIEWER_1); // commenter
        details.addReviewer(REVIEWER_2); // accidental add (no interaction with PR)
        waitUntil(details.getReviewerUsernames(), contains(REVIEWER_1, REVIEWER_2));
        editDialog.save();

        loginAs(REVIEWER_1, REVIEWER_1, PullRequestOverviewPage.class, PROJECT_KEY, REPO_SLUG, pullRequest.getId());
        assertTrue("Watching PR after being added as a reviewer", pullRequestPage.currentUserIsWatching());

        // REVIEWER_1 adds a comment
        GeneralCommentForm generalCommentForm = pullRequestPage.getGeneralCommentForm();
        generalCommentForm.expand();
        generalCommentForm.setText("comment");
        generalCommentForm.submit();

        // remove both reviewers
        loginAs(AUTHOR_1, AUTHOR_1, PullRequestOverviewPage.class, PROJECT_KEY, REPO_SLUG, pullRequest.getId());
        editDialog = pullRequestPage.getEditButton().clickButton();
        details = editDialog.getDetails();
        details.removeReviewer(REVIEWER_1);
        details.removeReviewer(REVIEWER_2);
        editDialog.save();

        loginAs(REVIEWER_1, REVIEWER_1, PullRequestOverviewPage.class, PROJECT_KEY, REPO_SLUG, pullRequest.getId());
        assertTrue("Watching PR after commenting " +
                "and being removed as a reviewer", pullRequestPage.currentUserIsWatching());

        loginAs(REVIEWER_2, REVIEWER_2, PullRequestOverviewPage.class, PROJECT_KEY, REPO_SLUG, pullRequest.getId());
        assertFalse("Not watching PR after no activity " +
                "and being removed as a reviewer", pullRequestPage.currentUserIsWatching());
    }

    @Test
    public void testWatchForAddAndRemoveSelfAsReviewer() {
        PullRequestOverviewPage page = loginAs(REVIEWER_1, REVIEWER_1, PullRequestOverviewPage.class,
                PROJECT_KEY, REPO_SLUG, createPullRequest(new PullRequestBuilder(SOURCE_BRANCH, TARGET_BRANCH).build())
                        .getId());

        assertFalse("Not watching PR before adding self", page.currentUserIsWatching());
        page.getAddSelfButton().clickButton();
        assertTrue("Watch PR after adding self as reviewer", page.currentUserIsWatching());

        page.getRemoveSelfDialog().confirm();
        assertTrue("Watching PR after default removing self", page.currentUserIsWatching());
    }

    @Test
    public void testWatchForAddGeneralComment() {
        PullRequestOverviewPage pullRequestPage = loginAs(REVIEWER_1, REVIEWER_1, PullRequestOverviewPage.class,
                PROJECT_KEY, REPO_SLUG, createPullRequest(new PullRequestBuilder(SOURCE_BRANCH, TARGET_BRANCH).build())
                        .getId());

        GeneralCommentForm generalCommentForm = pullRequestPage.getGeneralCommentForm();
        assertFalse("Not watching PR before commenting", pullRequestPage.currentUserIsWatching());
        generalCommentForm.expand();
        String commentText = "Func test comment test data <input type='text'>alert('666')</input>";
        generalCommentForm.setText(commentText);
        generalCommentForm.submit();
        assertTrue("Watch on PR comment", pullRequestPage.currentUserIsWatching());

        // After an explicit unwatch a user isn't automatically made a watcher again
        pullRequestPage.unwatchPullRequest();
        generalCommentForm.setText(commentText);
        generalCommentForm.submit();
        assertFalse("Don't watch PR on comment after explicit unwatch", pullRequestPage.currentUserIsWatching());
    }

    @Test
    public void testWatchForCommentReply() {
        PullRequestTestHelper pullRequest = createPullRequest(new PullRequestBuilder(SOURCE_BRANCH, TARGET_BRANCH)
                .build());

        PullRequestOverviewPage pullRequestPage = loginAs(AUTHOR_1, AUTHOR_1, PullRequestOverviewPage.class,
                PROJECT_KEY, REPO_SLUG, pullRequest.getId());
        CommentForm generalCommentForm = pullRequestPage.getGeneralCommentForm();
        generalCommentForm.setText("Comment");
        generalCommentForm.submit();

        loginAs(REVIEWER_1, REVIEWER_1, PullRequestOverviewPage.class,
                PROJECT_KEY, REPO_SLUG, pullRequest.getId());
        assertFalse("Not watching PR before replying to comment", pullRequestPage.currentUserIsWatching());
        ReplyCommentForm replyForm = ((GeneralCommentActivity) pullRequestPage.getActivityItems().get(0)).getComment().reply();
        replyForm.setText("Reply");
        replyForm.submit();
        assertTrue("Watch PR on comment reply", pullRequestPage.currentUserIsWatching());

        // After an explicit unwatch a user isn't automatically made a watcher again
        pullRequestPage.unwatchPullRequest();
        replyForm = ((GeneralCommentActivity) pullRequestPage.getActivityItems().get(0)).getComment().reply();
        replyForm.setText("Reply 2");
        replyForm.submit();
        assertFalse("Don't watch PR on comment reply after explicit unwatch", pullRequestPage.currentUserIsWatching());
    }

    @Override
    protected List<TestRule> getRetryRuleChain() {
        return outerRules(new WebStorageCleanUpRule()).around(super.getRetryRuleChain());
    }

    private static ReviewerActivityItem getFirstReviewerItemMatching(PullRequestOverviewPage pullRequestPage, String expected) {
        Poller.waitUntilTrue(forSupplier(() -> {
            ReviewerActivityItem activity = (ReviewerActivityItem) pullRequestPage.getActivityItems().get(0);
            return expected.equals(activity.getAvatar().getUsername());
        }));
        return (ReviewerActivityItem) pullRequestPage.getActivityItems().get(0);
    }

    private void testReplyEditAndDelete(String commentText, String expected, String expectedHtml) {
        PullRequestOverviewPage pullRequestPage = createPullRequestAndLogin(SOURCE_BRANCH, TARGET_BRANCH);
        CommentForm generalCommentForm = pullRequestPage.getGeneralCommentForm();
        generalCommentForm.setText(commentText);
        assertEquals(expected, generalCommentForm.submit().getCommentText());
        assertEquals("Comment activity missing", 2, pullRequestPage.getActivityItems().size());

        Comment commentActivity = ((GeneralCommentActivity) pullRequestPage.getActivityItems().get(0)).getComment();
        assertEquals(expected, commentActivity.getCommentText());
        assertEquals(String.format(expectedHtml, StringEscapeUtils.escapeHtml4(expected)), commentActivity.getCommentHtml());

        ReplyCommentForm replyForm = commentActivity.reply();
        String reply = "reply";
        replyForm.setText(commentText + reply);
        assertEquals(expected + reply, replyForm.submit().getCommentText());
        assertEquals("Reply added top-level comment", 2, pullRequestPage.getActivityItems().size());
        Comment replyActivity = commentActivity.getReplies().get(0);
        assertEquals(expected + reply, replyActivity.getCommentText());

        String reply2 = "Another reply";
        EditCommentForm edit = replyActivity.edit();
        assertEquals(expected + reply2, edit.setText(commentText + reply2).submit().getCommentText());

        replyActivity.delete().confirmDelete();
        assertEquals("Comment not deleted", 0, commentActivity.getReplies().size());
    }

    // We want to check that we redirect to the login page
    public static class PullRequestListPageAnonymous extends PullRequestListPage {

        public PullRequestListPageAnonymous(String projectKey, String slug, String state) {
            super(projectKey, slug, ImmutableMap.of("state", state));
        }

        @Override
        public void doWait() {
            pageBinder.bind(BitbucketLoginPage.class).doWait();
        }
    }
}
