@Override
  @CheckForNull
  public EmailMessage format(Notification notif) {
    if (!(notif instanceof ChangesOnMyIssuesNotification)) {
      return null;
    }

    ChangesOnMyIssuesNotification notification = (ChangesOnMyIssuesNotification) notif;

    if (notification.getChange() instanceof AnalysisChange) {
      checkState(!notification.getChangedIssues().isEmpty(), "changedIssues can't be empty");
      return formatAnalysisNotification(notification.getChangedIssues().keySet().iterator().next(), notification);
    }
    return formatMultiProject(notification);
  }
@Test
  public void formats_returns_html_message_with_multiple_links_by_rule_of_groups_of_up_to_40_issues_and_hotspots_when_user_change() {
    Project project1 = newProject("1");
    Project project2 = newProject("V");
    Project project2Branch = newBranch("V", "AB");
    Rule rule1 = newRule("1", randomRuleTypeHotspotExcluded());
    Rule rule2 = newRule("a", randomRuleTypeHotspotExcluded());

    Rule hotspot1 = newSecurityHotspotRule("h1");
    Rule hotspot2 = newSecurityHotspotRule("h2");

    String status = randomValidStatus();
    String host = randomAlphabetic(15);
    List<ChangedIssue> changedIssues = Stream.of(
      IntStream.range(0, 39).mapToObj(i -> newChangedIssue("39_" + i, status, project1, rule1)),
      IntStream.range(0, 40).mapToObj(i -> newChangedIssue("40_" + i, status, project1, rule2)),
      IntStream.range(0, 81).mapToObj(i -> newChangedIssue("1-40_41-80_1_" + i, status, project2, rule2)),
      IntStream.range(0, 6).mapToObj(i -> newChangedIssue("6_" + i, status, project2Branch, rule1)),

      IntStream.range(0, 39).mapToObj(i -> newChangedIssue("39_" + i, STATUS_REVIEWED, project1, hotspot1)),
      IntStream.range(0, 40).mapToObj(i -> newChangedIssue("40_" + i, STATUS_REVIEWED, project1, hotspot2)),
      IntStream.range(0, 81).mapToObj(i -> newChangedIssue("1-40_41-80_1_" + i, STATUS_TO_REVIEW, project2, hotspot2)),
      IntStream.range(0, 6).mapToObj(i -> newChangedIssue("6_" + i, STATUS_TO_REVIEW, project2Branch, hotspot1)))
      .flatMap(t -> t)
      .collect(toList());
    Collections.shuffle(changedIssues);
    UserChange userChange = newUserChange();
    when(emailSettings.getServerBaseURL()).thenReturn(host);

    EmailMessage emailMessage = underTest.format(new ChangesOnMyIssuesNotification(userChange, ImmutableSet.copyOf(changedIssues)));

    HtmlFragmentAssert.assertThat(emailMessage.getMessage())
      .hasParagraph().hasParagraph() // skip header
      .hasParagraph(project1.getProjectName())
      .hasList()
      .withItemTexts(
        "Rule " + rule1.getName() + " - See all 39 issues",
        "Rule " + rule2.getName() + " - See all 40 issues")
      .withLink("See all 39 issues",
        host + "/project/issues?id=" + project1.getKey()
          + "&issues=" + IntStream.range(0, 39).mapToObj(i -> "39_" + i).sorted().collect(joining("%2C")))
      .withLink("See all 40 issues",
        host + "/project/issues?id=" + project1.getKey()
          + "&issues=" + IntStream.range(0, 40).mapToObj(i -> "40_" + i).sorted().collect(joining("%2C")))
      .hasEmptyParagraph()
      .hasList()
      .withItemTexts(
        "Rule " + hotspot1.getName() + " - See all 39 hotspots",
        "Rule " + hotspot2.getName() + " - See all 40 hotspots")
      .withLink("See all 39 hotspots",
        host + "/security_hotspots?id=" + project1.getKey()
          + "&hotspots=" + IntStream.range(0, 39).mapToObj(i -> "39_" + i).sorted().collect(joining("%2C")))
      .withLink("See all 40 hotspots",
        host + "/security_hotspots?id=" + project1.getKey()
          + "&hotspots=" + IntStream.range(0, 40).mapToObj(i -> "40_" + i).sorted().collect(joining("%2C")))
      .hasParagraph(project2.getProjectName())
      .hasList(
        "Rule " + rule2.getName() + " - See issues 1-40 41-80 81")
      .withLink("1-40",
        host + "/project/issues?id=" + project2.getKey()
          + "&issues=" + IntStream.range(0, 81).mapToObj(i -> "1-40_41-80_1_" + i).sorted().limit(40).collect(joining("%2C")))
      .withLink("41-80",
        host + "/project/issues?id=" + project2.getKey()
          + "&issues=" + IntStream.range(0, 81).mapToObj(i -> "1-40_41-80_1_" + i).sorted().skip(40).limit(40).collect(joining("%2C")))
      .withLink("81",
        host + "/project/issues?id=" + project2.getKey()
          + "&issues=" + "1-40_41-80_1_9" + "&open=" + "1-40_41-80_1_9")
      .hasEmptyParagraph()
      .hasList("Rule " + hotspot2.getName() + " - See hotspots 1-40 41-80 81")
      .withLink("1-40",
        host + "/security_hotspots?id=" + project2.getKey()
          + "&hotspots=" + IntStream.range(0, 81).mapToObj(i -> "1-40_41-80_1_" + i).sorted().limit(40).collect(joining("%2C")))
      .withLink("41-80",
        host + "/security_hotspots?id=" + project2.getKey()
          + "&hotspots=" + IntStream.range(0, 81).mapToObj(i -> "1-40_41-80_1_" + i).sorted().skip(40).limit(40).collect(joining("%2C")))
      .withLink("81",
        host + "/security_hotspots?id=" + project2.getKey()
          + "&hotspots=" + "1-40_41-80_1_9")
      .hasParagraph(project2Branch.getProjectName() + ", " + project2Branch.getBranchName().get())
      .hasList(
        "Rule " + rule1.getName() + " - See all 6 issues")
      .withLink("See all 6 issues",
        host + "/project/issues?id=" + project2Branch.getKey() + "&branch=" + project2Branch.getBranchName().get()
          + "&issues=" + IntStream.range(0, 6).mapToObj(i -> "6_" + i).sorted().collect(joining("%2C")))
      .hasEmptyParagraph()
      .hasList("Rule " + hotspot1.getName() + " - See all 6 hotspots")
      .withLink("See all 6 hotspots",
        host + "/security_hotspots?id=" + project2Branch.getKey() + "&branch=" + project2Branch.getBranchName().get()
          + "&hotspots=" + IntStream.range(0, 6).mapToObj(i -> "6_" + i).sorted().collect(joining("%2C")))
      .hasParagraph().hasParagraph() // skip footer
      .noMoreBlock();
  }