@Test
public void testCommitAnalysisWithNewIssuesNoBlockerNorCritical2() {
    settings.setProperty(GitLabPlugin.GITLAB_STATUS_NOTIFICATION_MODE, StatusNotificationsMode.EXIT_CODE.getMeaning());
    DefaultInputFile inputFile1 = new DefaultInputFile("foo", "src/Foo.php");
    PostJobIssue newIssue = newMockedIssue("foo:src/Foo.php", inputFile1, 1, Severity.MAJOR, true, "msg1");
    when(commitFacade.getGitLabUrl(inputFile1, 1)).thenReturn("http://gitlab/blob/abc123/src/Foo.php#L1");
    when(context.issues()).thenReturn(Collections.singletonList(newIssue));
    when(commitFacade.hasFile(inputFile1)).thenReturn(true);
    when(commitFacade.hasFileLine(inputFile1, 1)).thenReturn(true);
    commitIssuePostJob.execute(context);
    Mockito.verify(commitFacade, Mockito.never()).createOrUpdateSonarQubeStatus("success", "SonarQube reported 1 issue, no criticals or blockers");
}

@Override
public void execute(PostJobContext context) {
    GlobalReport report = new GlobalReport(gitLabPluginConfiguration, markDownUtils);
    try {
        LOG.info("Find a {} new issues", getStreamPostJobIssue(context.issues()).count());
        Map<InputFile, Map<Integer, StringBuilder>> commentsToBeAddedByLine = processIssues(report, context.issues());
        updateReviewComments(commentsToBeAddedByLine);
        if (report.hasNewIssue() || gitLabPluginConfiguration.commentNoIssue()) {
            commitFacade.addGlobalComment(report.formatForMarkdown());
        }
        commitFacade.createOrUpdateSonarQubeStatus(report.getStatus(), report.getStatusDescription());
    } catch (Exception e) {
        String msg = "SonarQube failed to complete the review of this commit";
        LOG.error(msg, e);
        commitFacade.createOrUpdateSonarQubeStatus("failed", msg + ": " + e.getMessage());
    }
}

@Override
public void execute(PostJobContext context) {
    GlobalReport report = new GlobalReport(gitLabPluginConfiguration, markDownUtils);
    try {
        Map<InputFile, Map<Integer, StringBuilder>> commentsToBeAddedByLine = processIssues(report, context.issues());
        updateReviewComments(commentsToBeAddedByLine);
        if (!gitLabPluginConfiguration.disableGlobalComment() && (report.hasNewIssue() || gitLabPluginConfiguration.commentNoIssue())) {
            commitFacade.addGlobalComment(report.formatForMarkdown());
        }
        String status = report.getStatus();
        String statusDescription = report.getStatusDescription();
        String message = String.format("Report status=%s, desc=%s", report.getStatus(), report.getStatusDescription());
        StatusNotificationsMode i = gitLabPluginConfiguration.statusNotificationsMode();
        if (i == StatusNotificationsMode.COMMIT_STATUS) {
            LOG.info(message);
            commitFacade.createOrUpdateSonarQubeStatus(status, statusDescription);
        } else if (i == StatusNotificationsMode.EXIT_CODE) {
            if ("failed".equals(status)) {
                throw MessageException.of(message);
            } else {
                LOG.info(message);
            }
        }
    } catch (MessageException e) {
        throw e;
    } catch (Exception e) {
        String msg = "SonarQube failed to complete the review of this commit";
        LOG.error(msg, e);
        StatusNotificationsMode i = gitLabPluginConfiguration.statusNotificationsMode();
        if (i == StatusNotificationsMode.COMMIT_STATUS) {
            commitFacade.createOrUpdateSonarQubeStatus("failed", msg + ": " + e.getMessage());
        }
    }
}

@Test
public void testCommitAnalysisWithNewIssues2() {
    settings.setProperty(GitLabPlugin.GITLAB_ONLY_ISSUE_FROM_COMMIT_FILE, false);
    settings.setProperty(GitLabPlugin.GITLAB_STATUS_NOTIFICATION_MODE, StatusNotificationsMode.EXIT_CODE.getMeaning());
    settings.setProperty(GitLabPlugin.GITLAB_DISABLE_GLOBAL_COMMENT, true);
    DefaultInputFile inputFile1 = new DefaultInputFile("foo", "src/Foo.php");
    PostJobIssue newIssue = newMockedIssue("foo:src/Foo.php", inputFile1, 1, Severity.BLOCKER, true, "msg1");
    Mockito.when(commitFacade.getGitLabUrl(inputFile1, 1)).thenReturn("http://gitlab/blob/abc123/src/Foo.php#L1");
    PostJobIssue lineNotVisible = newMockedIssue("foo:src/Foo.php", inputFile1, 2, Severity.BLOCKER, true, "msg2");
    Mockito.when(commitFacade.getGitLabUrl(inputFile1, 2)).thenReturn("http://gitlab/blob/abc123/src/Foo.php#L2");
    DefaultInputFile inputFile2 = new DefaultInputFile("foo", "src/Foo2.php");
    PostJobIssue fileNotInPR = newMockedIssue("foo:src/Foo2.php", inputFile2, 1, Severity.BLOCKER, true, "msg3");
    PostJobIssue notNewIssue = newMockedIssue("foo:src/Foo.php", inputFile1, 1, Severity.BLOCKER, false, "msg");
    PostJobIssue issueOnDir = newMockedIssue("foo:src", Severity.BLOCKER, true, "msg4");
    PostJobIssue issueOnProject = newMockedIssue("foo", Severity.BLOCKER, true, "msg");
    PostJobIssue globalIssue = newMockedIssue("foo:src/Foo.php", inputFile1, null, Severity.BLOCKER, true, "msg5");
    Mockito.when(context.issues()).thenReturn(Arrays.<PostJobIssue>asList(newIssue, globalIssue, issueOnProject, issueOnDir, fileNotInPR, lineNotVisible, notNewIssue));
    Mockito.when(commitFacade.hasFile(inputFile1)).thenReturn(true);
    Mockito.when(commitFacade.hasFileLine(inputFile1, 1)).thenReturn(true);
    Assertions.assertThatThrownBy(() -> commitIssuePostJob.execute(context)).isInstanceOf(MessageException.class);
    Mockito.verify(commitFacade, Mockito.never()).addGlobalComment(Mockito.contains("SonarQube analysis reported 6 issues"));
    Mockito.verify(commitFacade, Mockito.never()).createOrUpdateSonarQubeStatus("failed", "SonarQube reported 6 issues, with 6 blocker");
}

public String getMeaning() {
    return meaning;
}

@Test
public void testDescriptor() {
    DefaultPostJobDescriptor postJobDescriptor = new DefaultPostJobDescriptor();
    commitIssuePostJob.describe(postJobDescriptor);
    Assertions.assertThat(postJobDescriptor.name()).isEqualTo("GitLab Commit Issue Publisher");
    Assertions.assertThat(postJobDescriptor.properties()).containsExactly(GitLabPlugin.GITLAB_URL, GitLabPlugin.GITLAB_USER_TOKEN, GitLabPlugin.GITLAB_PROJECT_ID, GitLabPlugin.GITLAB_COMMIT_SHA);
}

@Override
public void describe(PostJobDescriptor descriptor) {
    descriptor.name("GitLab Commit Issue Publisher").requireProperty(GitLabPlugin.GITLAB_COMMIT_SHA);
}

@Override
public void describe(PostJobDescriptor descriptor) {
    descriptor.name("GitLab Commit Issue Publisher").requireProperty(GitLabPlugin.GITLAB_URL, GitLabPlugin.GITLAB_USER_TOKEN, GitLabPlugin.GITLAB_PROJECT_ID, GitLabPlugin.GITLAB_COMMIT_SHA);
}

@Test
public void testCommitAnalysisNoIssueExit() {
    settings.setProperty(GitLabPlugin.GITLAB_COMMENT_NO_ISSUE, false);
    settings.setProperty(GitLabPlugin.GITLAB_STATUS_NOTIFICATION_MODE, StatusNotificationsMode.EXIT_CODE.getMeaning());
    Mockito.when(context.issues()).thenReturn(Collections.emptyList());
    commitIssuePostJob.execute(context);
    Mockito.verify(commitFacade, Mockito.never()).addGlobalComment(null);
    Mockito.verify(commitFacade, Mockito.never()).createOrUpdateSonarQubeStatus("success", "SonarQube reported no issues");
}


@Test
public void shouldNotFailIfIssuesNone() {
    settings.setProperty(GitLabPlugin.GITLAB_COMMIT_SHA, "1");
    settings.setProperty(GitLabPlugin.GITLAB_STATUS_NOTIFICATION_MODE, StatusNotificationsMode.EXIT_CODE.getMeaning());
    when(mode.isIssues()).thenReturn(true);
    commitProjectBuilder.build(mock(ProjectBuilder.Context.class, withSettings().defaultAnswer(RETURNS_DEEP_STUBS)));
    verify(facade).init(any(File.class));
    verify(facade, never()).createOrUpdateSonarQubeStatus(BuildInitState.PENDING.getMeaning(), "SonarQube analysis in progress");
}

@Override
public void build(Context context) {
    if (!gitLabPluginConfiguration.isEnabled()) {
        return;
    }
    checkMode();
    commitFacade.init(context.projectReactor().getRoot().getBaseDir());
    commitFacade.createOrUpdateSonarQubeStatus("pending", "SonarQube analysis in progress");
}

@Override
public void build(Context context) {
    if (!gitLabPluginConfiguration.isEnabled()) {
        return;
    }
    checkMode();
    commitFacade.init(context.projectReactor().getRoot().getBaseDir());
    if (StatusNotificationsMode.COMMIT_STATUS.equals(gitLabPluginConfiguration.statusNotificationsMode())) {
        commitFacade.createOrUpdateSonarQubeStatus(gitLabPluginConfiguration.buildInitState().getMeaning(), "SonarQube analysis in progress");
    }
}

@Test
public void shouldNotFailIfIssuesRunning() {
    settings.setProperty(GitLabPlugin.GITLAB_COMMIT_SHA, "1");
    settings.setProperty(GitLabPlugin.GITLAB_BUILD_INIT_STATE, BuildInitState.RUNNING.getMeaning());
    when(mode.isIssues()).thenReturn(true);
    commitProjectBuilder.build(mock(ProjectBuilder.Context.class, withSettings().defaultAnswer(RETURNS_DEEP_STUBS)));
    verify(facade).init(any(File.class));
    verify(facade).createOrUpdateSonarQubeStatus(BuildInitState.RUNNING.getMeaning(), "SonarQube analysis in progress");
}

@Test
public void shouldNotFailIfIssuesPending() {
    settings.setProperty(GitLabPlugin.GITLAB_COMMIT_SHA, "1");
    when(mode.isIssues()).thenReturn(true);
    commitProjectBuilder.build(mock(ProjectBuilder.Context.class, withSettings().defaultAnswer(RETURNS_DEEP_STUBS)));
    verify(facade).init(any(File.class));
    verify(facade).createOrUpdateSonarQubeStatus(BuildInitState.PENDING.getMeaning(), "SonarQube analysis in progress");
}


@Test
public void testProject() {
    Assertions.assertThat(config.isEnabled()).isFalse();
    settings.setProperty(GitLabPlugin.GITLAB_COMMIT_SHA, "3");
    Assertions.assertThat(config.commitSHA()).isEqualTo("3");
    Assertions.assertThat(config.isEnabled()).isTrue();
    Assertions.assertThat(config.commentNoIssue()).isFalse();
    settings.setProperty(GitLabPlugin.GITLAB_COMMENT_NO_ISSUE, "true");
    Assertions.assertThat(config.commentNoIssue()).isTrue();
    Assertions.assertThat(config.tryReportIssuesInline()).isTrue();
    settings.setProperty(GitLabPlugin.GITLAB_DISABLE_INLINE_COMMENTS, "true");
    Assertions.assertThat(config.tryReportIssuesInline()).isFalse();
    settings.setProperty(GitLabPlugin.GITLAB_PROJECT_ID, "123");
    Assertions.assertThat(config.projectId()).isEqualTo("123");
    Assertions.assertThat(config.ignoreFileNotModified()).isFalse();
    settings.setProperty(GitLabPlugin.GITLAB_IGNORE_FILE, "true");
    Assertions.assertThat(config.ignoreFileNotModified()).isTrue();
    settings.setProperty(GitLabPlugin.GITLAB_REF_NAME, "123");
    Assertions.assertThat(config.refName()).isEqualTo("123");
    Assertions.assertThat(config.onlyIssueFromCommitFile()).isFalse();
    settings.setProperty(GitLabPlugin.GITLAB_ONLY_ISSUE_FROM_COMMIT_FILE, "true");
    Assertions.assertThat(config.onlyIssueFromCommitFile()).isTrue();
    Assertions.assertThat(config.buildInitState()).isEqualTo(BuildInitState.PENDING);
    settings.setProperty(GitLabPlugin.GITLAB_BUILD_INIT_STATE, BuildInitState.RUNNING.getMeaning());
    Assertions.assertThat(config.buildInitState()).isEqualTo(BuildInitState.RUNNING);
    settings.setProperty(GitLabPlugin.GITLAB_BUILD_INIT_STATE, "toto");
    Assertions.assertThat(config.buildInitState()).isEqualTo(BuildInitState.PENDING);
    Assertions.assertThat(config.disableGlobalComment()).isFalse();
    settings.setProperty(GitLabPlugin.GITLAB_DISABLE_GLOBAL_COMMENT, "true");
    Assertions.assertThat(config.disableGlobalComment()).isTrue();
    Assertions.assertThat(config.statusNotificationsMode()).isEqualTo(StatusNotificationsMode.COMMIT_STATUS);
    settings.setProperty(GitLabPlugin.GITLAB_STATUS_NOTIFICATION_MODE, StatusNotificationsMode.EXIT_CODE.getMeaning());
    Assertions.assertThat(config.statusNotificationsMode()).isEqualTo(StatusNotificationsMode.EXIT_CODE);
    settings.setProperty(GitLabPlugin.GITLAB_STATUS_NOTIFICATION_MODE, "toto");
    Assertions.assertThat(config.statusNotificationsMode()).isEqualTo(StatusNotificationsMode.COMMIT_STATUS);
}

public boolean disableGlobalComment() {
    return settings.getBoolean(GitLabPlugin.GITLAB_DISABLE_GLOBAL_COMMENT);
}


@Test
public void testCommitAnalysisNoIssue1() {
    settings.setProperty(GitLabPlugin.GITLAB_COMMENT_NO_ISSUE, false);
    Mockito.when(context.issues()).thenReturn(Arrays.asList());
    commitIssuePostJob.execute(context);
    Mockito.verify(commitFacade, Mockito.never()).addGlobalComment(null);
    Mockito.verify(commitFacade).createOrUpdateSonarQubeStatus("success", "SonarQube reported no issues");
}

@Override
public void execute(PostJobContext context) {
    GlobalReport report = new GlobalReport(gitLabPluginConfiguration, markDownUtils);
    Map<InputFile, Map<Integer, StringBuilder>> commentsToBeAddedByLine = processIssues(report, context.issues());
    updateReviewComments(commentsToBeAddedByLine);
    if (report.hasNewIssue() || gitLabPluginConfiguration.commentNoIssue()) {
        commitFacade.addGlobalComment(report.formatForMarkdown());
    }
    commitFacade.createOrUpdateSonarQubeStatus(report.getStatus(), report.getStatusDescription());
}

@Override
public void execute(PostJobContext context) {
    GlobalReport report = new GlobalReport(gitLabPluginConfiguration, markDownUtils);
    try {
        Map<InputFile, Map<Integer, StringBuilder>> commentsToBeAddedByLine = processIssues(report, context.issues());
        updateReviewComments(commentsToBeAddedByLine);
        if (report.hasNewIssue() || gitLabPluginConfiguration.commentNoIssue()) {
            commitFacade.addGlobalComment(report.formatForMarkdown());
        }
        commitFacade.createOrUpdateSonarQubeStatus(report.getStatus(), report.getStatusDescription());
    } catch (Exception e) {
        String msg = "SonarQube failed to complete the review of this pull request";
        LOG.error(msg, e);
        commitFacade.createOrUpdateSonarQubeStatus("failed", msg + ": " + e.getMessage());
    }
}

@Test
public void testCommitAnalysisNoIssue2() {
    settings.setProperty(GitLabPlugin.GITLAB_COMMENT_NO_ISSUE, true);
    Mockito.when(context.issues()).thenReturn(Arrays.asList());
    commitIssuePostJob.execute(context);
    Mockito.verify(commitFacade).addGlobalComment("SonarQube analysis reported no issues.");
    Mockito.verify(commitFacade).createOrUpdateSonarQubeStatus("success", "SonarQube reported no issues");
}

@Test
public void should_update_sonarqube_status_even_if_unexpected_errors_were_raised() {
    String innerMsg = "Failed to get issues";
    when(context.issues()).thenThrow(new IllegalStateException(innerMsg));
    commitIssuePostJob.execute(context);
    String msg = "SonarQube failed to complete the review of this pull request: " + innerMsg;
    verify(commitFacade).createOrUpdateSonarQubeStatus("failed", msg);
}


@Test
public void testUnexpectedException() {
    String innerMsg = "Failed to get issues";
    when(context.issues()).thenThrow(new IllegalStateException(innerMsg));
    commitIssuePostJob.execute(context);
    String msg = "SonarQube failed to complete the review of this pull request: " + innerMsg;
    verify(commitFacade).createOrUpdateSonarQubeStatus("failed", msg);
}

@Test
public void testCommitAnalysisNoIssue1() {
    settings.setProperty(GitLabPlugin.GITLAB_COMMENT_NO_ISSUE, false);
    Mockito.when(context.issues()).thenReturn(Collections.emptyList());
    commitIssuePostJob.execute(context);
    Mockito.verify(commitFacade, Mockito.never()).addGlobalComment(null);
    Mockito.verify(commitFacade).createOrUpdateSonarQubeStatus("success", "SonarQube reported no issues");
}


@Test
public void testGlobalComment() throws IOException {
    GitLabPluginConfiguration gitLabPluginConfiguration = mock(GitLabPluginConfiguration.class);
    when(gitLabPluginConfiguration.commitSHA()).thenReturn("1");
    when(gitLabPluginConfiguration.refName()).thenReturn("master");
    CommitFacade facade = new CommitFacade(gitLabPluginConfiguration);
    GitLabAPI gitLabAPI = mock(GitLabAPI.class);
    facade.setGitLabAPI(gitLabAPI);
    GitLabAPICommits gitLabAPICommits = mock(GitLabAPICommits.class);
    when(gitLabAPICommits.postCommitComments("1", "1", "pending", "master", null, null)).thenReturn(null);
    when(gitLabAPI.getGitLabAPICommits()).thenReturn(gitLabAPICommits);
    GitLabProject gitLabProject = mock(GitLabProject.class);
    when(gitLabProject.getId()).thenReturn(1);
    facade.setGitLabProject(gitLabProject);
    facade.addGlobalComment("nothing");
    verify(gitLabAPICommits).postCommitComments(1, "1", "nothing", null, null, null);
}

void setGitLabAPI(GitLabAPI gitLabAPI) {
    this.gitLabAPI = gitLabAPI;
}

@Test
public void testStatusSuccess() throws IOException {
    GitLabPluginConfiguration gitLabPluginConfiguration = mock(GitLabPluginConfiguration.class);
    when(gitLabPluginConfiguration.commitSHA()).thenReturn("1");
    when(gitLabPluginConfiguration.refName()).thenReturn("master");
    CommitFacade facade = new CommitFacade(gitLabPluginConfiguration);
    GitLabAPI gitLabAPI = mock(GitLabAPI.class);
    facade.setGitLabAPI(gitLabAPI);
    GitLabAPICommits gitLabAPICommits = mock(GitLabAPICommits.class);
    when(gitLabAPICommits.postCommitStatus("1", "1", "pending", "master", "toto", "server", "")).thenReturn(null);
    when(gitLabAPI.getGitLabAPICommits()).thenReturn(gitLabAPICommits);
    GitLabProject gitLabProject = mock(GitLabProject.class);
    when(gitLabProject.getId()).thenReturn(1);
    facade.setGitLabProject(gitLabProject);
    facade.createOrUpdateSonarQubeStatus("pending", "nothing");
    verify(gitLabAPICommits).postCommitStatus(1, "1", "pending", "master", "sonarqube", null, "nothing");
}

@Test
public void testReviewComment() throws IOException {
    GitLabPluginConfiguration gitLabPluginConfiguration = mock(GitLabPluginConfiguration.class);
    when(gitLabPluginConfiguration.commitSHA()).thenReturn("1");
    when(gitLabPluginConfiguration.refName()).thenReturn("master");
    CommitFacade facade = new CommitFacade(gitLabPluginConfiguration);
    File gitBasedir = temp.newFolder();
    facade.setGitBaseDir(gitBasedir);
    GitLabAPI gitLabAPI = mock(GitLabAPI.class);
    facade.setGitLabAPI(gitLabAPI);
    GitLabAPICommits gitLabAPICommits = mock(GitLabAPICommits.class);
    when(gitLabAPICommits.postCommitComments("1", "1", "pending", "master", null, null)).thenReturn(null);
    when(gitLabAPI.getGitLabAPICommits()).thenReturn(gitLabAPICommits);
    GitLabProject gitLabProject = mock(GitLabProject.class);
    when(gitLabProject.getId()).thenReturn(1);
    facade.setGitLabProject(gitLabProject);
    InputFile inputFile = mock(InputFile.class);
    when(inputFile.file()).thenReturn(new File(gitBasedir, "src/main/Foo.java"));
    facade.createOrUpdateReviewComment(inputFile, 5, "nothing");
    verify(gitLabAPICommits).postCommitComments(1, "1", "nothing", "src/main/Foo.java", 5, "new");
}


@Test
public void testMapPatchPositionsToLines() throws IOException {
    GitLabCommitDiff diff1 = new GitLabCommitDiff();
    diff1.setNewPath("src/main/Foo.java");
    diff1.setDiff("@@ -24,9 +24,9 @@\n /**\n  * A plugin is a group of extensions. See org.sonar.api.Extension interface to browse\n  * available extension points.\n- *
\n  *
The manifest property Plugin-Class must declare the name of the implementation class.\n  * It is automatically set by sonar-packaging-maven-plugin when building plugins.
\n+ *
Implementation must declare a public constructor with no-parameters.
\n  *\n  * @see org.sonar.api.Extension\n  * @since 1.10");
    GitLabCommitDiff diff2 = new GitLabCommitDiff();
    diff2.setNewPath("src/main/Foo2.java");
    diff2.setDiff("@@ -2,11 +2,9 @@\n /**\n  * A plugin is a group of extensions. See org.sonar.api.Extension interface to browse\n  * available extension points.\n- *
\n  *
The manifest property Plugin-Class must declare the name of the implementation class.\n  * It is automatically set by sonar-packaging-maven-plugin when building plugins.
\n+ *
Implementation must declare a public constructor with no-parameters.
\n  *\n  * @see org.sonar.api.Extension\n  * @since 1.10");
    assertThat(CommitFacade.mapPatchPositionsToLines(Arrays.asList(diff1, diff2))).containsEntry("src/main/Foo.java", Sets.newHashSet(32, 24, 25, 26, 27, 28, 29, 30, 31)).containsEntry("src/main/Foo2.java", Sets.newHashSet(2, 3, 4, 5, 6, 7, 8, 9, 10));
}

static Map<String, Set<Integer>> mapPatchPositionsToLines(List<GitLabCommitDiff> diffs) throws IOException {
    Map<String, Set<Integer>> patchPositionMappingByFile = new HashMap<>();
    for (GitLabCommitDiff file : diffs) {
        Set<Integer> patchLocationMapping = new HashSet<>();
        patchPositionMappingByFile.put(file.getNewPath(), patchLocationMapping);
        String patch = file.getDiff();
        if (patch == null) {
            continue;
        }
        processPatch(patchLocationMapping, patch);
    }
    return patchPositionMappingByFile;
}


@Test
public void noIssues() {
    GlobalCommentBuilder globalReport = new GlobalCommentBuilder(config, new Reporter(config), new MarkDownUtils(settings));
    String desiredMarkdown = "SonarQube analysis reported no issues.";
    String formattedGlobalReport = globalReport.buildForMarkdown();
    Assertions.assertThat(formattedGlobalReport).isEqualTo(desiredMarkdown);
}

public String buildForMarkdown() {
    StringBuilder sb = new StringBuilder();
    int newIssues = reporter.getIssueCount();
    if (newIssues == 0) {
        return "SonarQube analysis reported no issues.";
    }
    boolean hasInlineIssues = newIssues > reporter.getNotReportedIssueCount();
    boolean extraIssuesTruncated = reporter.getNotReportedIssueCount() > gitLabPluginConfiguration.maxGlobalIssues();
    sb.append("SonarQube analysis reported ").append(newIssues).append(" issue").append(newIssues > 1 ? "s" : "").append("\n");
    appendSummaryBySeverity(sb);
    if (gitLabPluginConfiguration.tryReportIssuesInline() && hasInlineIssues) {
        sb.append("\nWatch the comments in this conversation to review them.\n");
    }
    if (reporter.getNotReportedIssueCount() > 0) {
        appendExtraIssues(sb, hasInlineIssues, extraIssuesTruncated);
    }
    return sb.toString();
}


@Test
public void testCommitAnalysisWithNewIssuesOnly() {
    settings.setProperty(GitLabPlugin.GITLAB_ONLY_ISSUE_FROM_COMMIT_FILE, true);
    DefaultInputFile inputFile1 = new DefaultInputFile("foo", "src/Foo.php");
    PostJobIssue newIssue = newMockedIssue("foo:src/Foo.php", inputFile1, 1, Severity.BLOCKER, true, "msg1");
    Mockito.when(commitFacade.getGitLabUrl(inputFile1, 1)).thenReturn("http://gitlab/blob/abc123/src/Foo.php#L1");
    PostJobIssue lineNotVisible = newMockedIssue("foo:src/Foo.php", inputFile1, 2, Severity.BLOCKER, true, "msg2");
    Mockito.when(commitFacade.getGitLabUrl(inputFile1, 2)).thenReturn("http://gitlab/blob/abc123/src/Foo.php#L2");
    DefaultInputFile inputFile2 = new DefaultInputFile("foo", "src/Foo2.php");
    PostJobIssue fileNotInPR = newMockedIssue("foo:src/Foo2.php", inputFile2, 1, Severity.BLOCKER, true, "msg3");
    PostJobIssue notNewIssue = newMockedIssue("foo:src/Foo.php", inputFile1, 1, Severity.BLOCKER, false, "msg");
    PostJobIssue issueOnDir = newMockedIssue("foo:src", Severity.BLOCKER, true, "msg4");
    PostJobIssue issueOnProject = newMockedIssue("foo", Severity.BLOCKER, true, "msg");
    PostJobIssue globalIssue = newMockedIssue("foo:src/Foo.php", inputFile1, null, Severity.BLOCKER, true, "msg5");
    Mockito.when(context.issues()).thenReturn(Arrays.<PostJobIssue>asList(newIssue, globalIssue, issueOnProject, issueOnDir, fileNotInPR, lineNotVisible, notNewIssue));
    Mockito.when(commitFacade.hasFile(inputFile1)).thenReturn(true);
    Mockito.when(commitFacade.hasFileLine(inputFile1, 1)).thenReturn(true);
    commitIssuePostJob.execute(context);
    Mockito.verify(commitFacade).addGlobalComment(Mockito.contains("SonarQube analysis reported 5 issues"));
    Mockito.verify(commitFacade).addGlobalComment(Mockito.contains("* :no_entry: 5 blocker"));
    Mockito.verify(commitFacade).addGlobalComment(AdditionalMatchers.not(Mockito.contains("1. [Project")));
    Mockito.verify(commitFacade).addGlobalComment(Mockito.contains("1. :no_entry: [msg2](http://gitlab/blob/abc123/src/Foo.php#L2) [:blue_book:](http://myserver/coding_rules#rule_key=repo%3Arule)"));
    Mockito.verify(commitFacade).createOrUpdateSonarQubeStatus("failed", "SonarQube reported 5 issues, with 5 blocker");
}

@Override
public void execute(PostJobContext context) {
    GlobalReport report = new GlobalReport(gitLabPluginConfiguration, markDownUtils);
    try {
        Map<InputFile, Map<Integer, StringBuilder>> commentsToBeAddedByLine = processIssues(report, context.issues());
        updateReviewComments(commentsToBeAddedByLine);
        if (report.hasNewIssue() || gitLabPluginConfiguration.commentNoIssue()) {
            commitFacade.addGlobalComment(report.formatForMarkdown());
        }
        commitFacade.createOrUpdateSonarQubeStatus(report.getStatus(), report.getStatusDescription());
    } catch (Exception e) {
        String msg = "SonarQube failed to complete the review of this pull request";
        LOG.error(msg, e);
        commitFacade.createOrUpdateSonarQubeStatus("failed", msg + ": " + e.getMessage());
    }
}

@Override
public void execute(PostJobContext context) {
    GlobalReport report = new GlobalReport(gitLabPluginConfiguration, markDownUtils);
    try {
        LOG.info("Find a {} new issues", StreamSupport.stream(context.issues().spliterator(), false).filter(PostJobIssue::isNew).count());
        Map<InputFile, Map<Integer, StringBuilder>> commentsToBeAddedByLine = processIssues(report, context.issues());
        updateReviewComments(commentsToBeAddedByLine);
        if (report.hasNewIssue() || gitLabPluginConfiguration.commentNoIssue()) {
            commitFacade.addGlobalComment(report.formatForMarkdown());
        }
        commitFacade.createOrUpdateSonarQubeStatus(report.getStatus(), report.getStatusDescription());
    } catch (Exception e) {
        String msg = "SonarQube failed to complete the review of this commit";
        LOG.error(msg, e);
        commitFacade.createOrUpdateSonarQubeStatus("failed", msg + ": " + e.getMessage());
    }
}


@Test
public void test_method_invoke() throws Exception {
    ClassFile cf = ClassReader.read(Paths.get("example/Loop4.class"));
    Method main = cf.getMainMethod();
    com.gxk.jvm.classfile.attribute.Code attribute = (com.gxk.jvm.classfile.attribute.Code) main.attributes.attributes[0];
    MethodInfo method = map(attribute);
    Thread thread = new Thread(1024);
    Frame frame = new Frame(method.code.maxLocals, method.code.maxStacks, method.code.code, thread);
    thread.pushFrame(frame);
    new Interpreter().loop(thread);
}

public void loop(Thread thread, CodeFromByte code) {
    Frame frame = thread.currentFrame();
    while (true) {
        int pc = frame.nextPc;
        thread.setPc(pc);
        Instruction inst = code.getInst(pc);
        frame.nextPc += inst.offset();
        inst.fetchOperands();
        inst.execute(frame);
        if (inst instanceof IreturnInst || inst instanceof ReturnInst) {
            break;
        }
    }
}

public void loop(Thread thread) {
    while (true) {
        Frame frame = thread.currentFrame();
        int pc = frame.nextPc;
        thread.setPc(pc);
        Instruction inst = frame.code.getInst(pc);
        frame.nextPc += inst.offset();
        inst.fetchOperands();
        inst.execute(frame);
        if (thread.empty()) {
            break;
        }
    }
}


@Test
public void test_help() {
    Args args = Args.parseArgs("-help");
    assertTrue(args.help);
}

static Args parseArgs(String... cliArgs) {
    Args args = new Args();
    if (MINUS_VERSION.equals(cliArgs[0])) {
        args.version = true;
        return args;
    }
    if (MINUS_CP.equals(cliArgs[0])) {
        args.classpath = cliArgs[1];
        args.clazz = cliArgs[2];
        if (cliArgs.length > ARGS_LIMIT) {
            String[] programArgs = new String[cliArgs.length - 3];
            System.arraycopy(cliArgs, 3, programArgs, 0, programArgs.length);
            args.args = programArgs;
        }
        return args;
    }
    args.clazz = cliArgs[0];
    if (cliArgs.length > 1) {
        String[] programArgs = new String[cliArgs.length - 1];
        System.arraycopy(cliArgs, 1, programArgs, 0, programArgs.length);
        args.args = programArgs;
    }
    return args;
}

static Args parseArgs(String... cliArgs) {
    Args args = new Args();
    if (MINUS_VERSION.equals(cliArgs[0])) {
        args.version = true;
        return args;
    }
    if (MINUS_HELP.equals(cliArgs[0])) {
        args.help = true;
        return args;
    }
    int idx = 0;
    if (MINUS_VERBOSE.equals(cliArgs[idx])) {
        idx++;
        args.verbose = true;
    }
    if (MINUS_CP.equals(cliArgs[idx])) {
        idx++;
        args.classpath = cliArgs[idx++];
        args.clazz = cliArgs[idx++];
        if (cliArgs.length > idx) {
            String[] programArgs = new String[cliArgs.length - idx];
            System.arraycopy(cliArgs, idx, programArgs, 0, programArgs.length);
            args.args = programArgs;
        }
        return args;
    }
    args.clazz = cliArgs[idx++];
    if (cliArgs.length > idx) {
        String[] programArgs = new String[cliArgs.length - idx];
        System.arraycopy(cliArgs, idx, programArgs, 0, programArgs.length);
        args.args = programArgs;
    }
    return args;
}

@Test
public void test_verbose_cp2() {
    Args args = Args.parseArgs("-cp", "example", "-verbose", "Main");
    assertTrue(!args.verbose);
}

@Test
public void test_verbose_cp() {
    Args args = Args.parseArgs("-verbose", "-cp", "example", "Main");
    assertTrue(args.verbose);
    assertEquals("example", args.classpath);
    assertEquals("Main", args.clazz);
}

@Test
public void test_verbose() {
    Args args = Args.parseArgs("-verbose", "Main");
    assertTrue(args.verbose);
    assertEquals("Main", args.clazz);
}


@Test
public void parse_dir() {
    Entry entry = Classpath.parse("example");
    ClassFile hello = entry.findClass("Hello");
    assertNotNull(hello);
}

public static Entry parse(String classpath) {
    if (classpath.contains(":")) {
        return doParseCompositeEntry(classpath);
    }
    Entry entry = parseEntry(classpath);
    if (entry == null) {
        throw new IllegalArgumentException("un parse classpath " + classpath);
    }
    return entry;
}

@Test
public void isDir_bad_case() {
    String dir = "example1111";
    boolean ret = Classpath.isDir(dir);
    assertTrue(!ret);
}

public static boolean isDir(String path) {
    return Paths.get(path).toFile().isDirectory();
}

@Test
public void isWildcard_bad_case() {
    boolean ret = Classpath.isWildcard("./**");
    assertTrue(!ret);
}

public static boolean isWildcard(String path) {
    return path.endsWith("/*");
}

@Test
public void isDir() {
    String dir = "example";
    boolean ret = Classpath.isDir(dir);
    assertTrue(ret);
}

@Test
public void isWildcard() {
    boolean ret = Classpath.isWildcard("./*");
    assertTrue(ret);
}

@Test
public void isJar() {
    boolean ret = Classpath.isJar("test.jar");
    assertTrue(ret);
}

public static boolean isJar(String path) {
    return path.endsWith(".jar");
}

@Test
public void doParseDir() {
    Entry entry = Classpath.doParseDir("example");
    ClassFile hello = entry.findClass("Hello");
    assertNotNull(hello);
}

public static Entry doParseDir(String path) {
    return new DirEntry(Paths.get(path));
}


@Test
public void parseMethodDescriptor() {
    List<String> rets = Utils.parseMethodDescriptor("()V");
    assertEquals(0, rets.size(), 0);
}

public static List<String> parseMethodDescriptor(String descriptor) {
    if (descriptor.startsWith("()")) {
        return new ArrayList<>();
    }
    descriptor = descriptor.substring(descriptor.indexOf("(") + 1, descriptor.indexOf(")"));
    List<Character> base = Arrays.asList('V', 'Z', 'B', 'C', 'S', 'I', 'J', 'F', 'D');
    List<String> rets = new ArrayList<>();
    for (int i = 0; i < descriptor.length(); i++) {
        if (base.contains(descriptor.charAt(i))) {
            rets.add(String.valueOf(descriptor.charAt(i)));
            continue;
        }
        if (descriptor.charAt(i) == '[') {
            int temp = i;
            i++;
            while (descriptor.charAt(i) == '[') {
                i++;
            }
            if (base.contains(descriptor.charAt(i))) {
                rets.add(descriptor.substring(temp, i + 1));
                continue;
            }
            int idx = descriptor.indexOf(';', i);
            rets.add(descriptor.substring(temp, idx));
            i = idx;
            continue;
        }
        if (descriptor.charAt(i) == 'L') {
            int idx = descriptor.indexOf(';', i);
            rets.add(descriptor.substring(i, idx));
            i = idx;
            continue;
        }
    }
    return rets;
}

@Test
public void parseMethodDescriptor_2() {
    List<String> rets = Utils.parseMethodDescriptor("(IILjava/lang/String;I)V");
    assertEquals(4, rets.size(), 0);
}

@Test
public void parseMethodDescriptor_3() {
    List<String> rets = Utils.parseMethodDescriptor("(II[Ljava/lang/String;I)V");
    assertEquals(4, rets.size(), 0);
}

@Test
public void parseMethodDescriptor_1() {
    List<String> rets = Utils.parseMethodDescriptor("(II[CI)V");
    assertEquals(4, rets.size(), 0);
}

@Test
public void parseMethodDescriptor_4() {
    List<String> rets = Utils.parseMethodDescriptor("([[[II[Ljava/lang/String;I)V");
    assertEquals(4, rets.size(), 0);
}


@Test
public void test_object2() {
    Entry entry = Classpath.parse("example");
    Heap.setDefaultEntry(entry);
    Classloader.loadClass("TestObject2", entry);
    KClass clazz = Heap.findClass("TestObject2");
    KMethod method = clazz.getMainMethod();
    new Interpreter().interpret(method);
}

public static void setDefaultEntry(Entry entry) {
    defaultEntry = entry;
}


@Test
public void test_hello() {
    KClass kClass = classLoader.loadClass("Hello");
    assertNotNull(kClass);
}

public KClass loadClass(String name) {
    KClass cache = Heap.findClass(name);
    if (cache != null) {
        return cache;
    }
    KClass clazz = doLoadClass(name);
    doRegister(clazz);
    return clazz;
}

@Test
public void test_object() {
    KClass kClass = classLoader.loadClass("java/lang/Object");
    assertNotNull(kClass);
}


@Test
public void shouldFilterChildren() {
    this.testee = new CompoundMutationInterceptor(Arrays.asList(this.modifyChild, this.filterChild));
    final ClassTree aClass = new ClassTree(null);
    this.testee.filter(i -> i == modifyChild).begin(aClass);
    verify(this.modifyChild).begin(aClass);
    verify(this.filterChild, never()).begin(aClass);
}

public CompoundMutationInterceptor filter(Predicate<MutationInterceptor> p) {
    return new CompoundMutationInterceptor(children.stream().filter(p).collect(Collectors.toList()));
}


@Test
public void shouldPassTestPluginToJavaTask() {
    this.pitestTask.setTestPlugin("junit");
    this.pitestTask.execute(this.java);
    verify(this.arg).setValue("--testPlugin=junit");
}

public void setTestPlugin(final String value) {
    this.setOption(ConfigOption.TEST_PLUGIN, value);
}

@Test
public void shouldPassFailWhenNoMutationsToJavaTask() {
    this.pitestTask.setFailWhenNoMutations("true");
    this.pitestTask.execute(this.java);
    verify(this.arg).setValue("--failWhenNoMutations=true");
}

public void setFailWhenNoMutations(final String value) {
    this.setOption(ConfigOption.FAIL_WHEN_NOT_MUTATIONS, value);
}


@Test
public void featureNamesAreCaseInsensitive() {
    final FeatureSetting fooConfig = new FeatureSetting("foo", ToggleStatus.DEACTIVATE, new HashMap<>());
    this.testee = new FeatureSelector<>(Arrays.asList(fooConfig), features(this.onByDefault));
    assertThat(this.testee.getSettingForFeature("FOO")).isEqualTo(fooConfig);
}

public FeatureSetting getSettingForFeature(String feature) {
    FeatureSetting conf = null;
    final Collection<FeatureSetting> groupedSettings = this.settings.get(feature);
    if (groupedSettings != null) {
        conf = groupedSettings.iterator().next();
    }
    return conf;
}

public FeatureSetting getSettingForFeature(String feature) {
    FeatureSetting conf = null;
    final Collection<FeatureSetting> groupedSettings = this.settings.get(feature.toLowerCase());
    if (groupedSettings != null) {
        conf = groupedSettings.iterator().next();
    }
    return conf;
}


@Test
public void singletonTest() {
    DataInfo dataInfo = InstanceFactory.getInstance().singleton(STPhraseData.class).data();
    Assert.assertEquals(48933, dataInfo.getDataMap().size());
}

@Override
public DataInfo data() {
    return dataInfo;
}


@Test
public void singletonTest() {
    DataInfo dataInfo = InstanceFactory.getInstance().singleton(TSPhraseData.class).data();
    Assert.assertEquals(273, dataInfo.getDataMap().size());
}


@Test
public void testJpTraditional() {
    String original = "我在日本学习音乐，并学习了龙的字。";
    String result = ZhJpConverterUtil.toTraditional(original);
    Assert.assertEquals("我在日本学習音楽，並学習了竜的字。", result);
}

public static String toTraditional(final String original) {
    String temp = ZhConverterUtil.toTraditional(original);
    return JP_SELF_BS.toTraditional(temp);
}


@Test
public void basicTest() {
    Map<String, List<String>> s2t = new HashMap<String, List<String>>();
    s2t.put("出租车", Arrays.asList("計程車"));
    s2t.put("面条", Arrays.asList("麵條"));
    Map<String, List<String>> t2hk = new HashMap<String, List<String>>();
    t2hk.put("計程車", Arrays.asList("的士"));
    t2hk.put("麵條", Arrays.asList("麵條"));
    Map<String, List<String>> result = DataUtil.mergeChains(s2t, t2hk);
    Assert.assertEquals(result.get("出租车").toString(), "[的士]");
    Assert.assertEquals(result.get("面条").toString(), "[麵條]");
}

public static Map<String, List<String>> mergeChains(final Map<String, List<String>> data, final Map<String, List<String>>... dataOther) {
    if (dataOther == null || dataOther.length == 0) {
        return data;
    }
    Map<String, List<String>> current = data;
    for (int i = 0; i < dataOther.length; i++) {
        Map<String, List<String>> nextDict = dataOther[i];
        Map<String, List<String>> merged = new HashMap<>();
        for (Map.Entry<String, List<String>> entry : current.entrySet()) {
            String key = entry.getKey();
            List<String> values = entry.getValue();
            Set<String> nextCandidatesSet = new LinkedHashSet<>();
            for (int j = 0; j < values.size(); j++) {
                String midValue = values.get(j);
                List<String> nextList = nextDict.get(midValue);
                if (nextList != null && !nextList.isEmpty()) {
                    nextCandidatesSet.addAll(nextList);
                } else {
                    nextCandidatesSet.add(midValue);
                }
            }
            List<String> deduped = new ArrayList<>(nextCandidatesSet);
            merged.put(key, deduped);
        }
        current = merged;
    }
    return current;
}


@Test
public void testHkItSimple() throws Exception {
    String original = "他坐出租車去超市買了方便麪和衞生紙";
    String result = ZhHkConverterUtil.toSimple(original);
    Assert.assertEquals("他坐出租车去超市买了方便面和衞生纸", result);
}

public static String toSimple(final String original) {
    return DEFAULT_BOOTSTRAP.toSimple(original);
}


@Test
public void isSimpleTest() {
    ZhConvertBootstrap bs = ZhConvertBootstrap.newInstance().isSimpleMatch(ZhMatches.simpleAll()).init();
    String text = "123我456";
    Assert.assertFalse(bs.isSimple(text));
    bs.isSimpleMatch(ZhMatches.simpleAny()).init();
    Assert.assertTrue(bs.isSimple(text));
}

public static ZhMatch simpleAll() {
    return new SimpleZhMatchAll();
}

@Test
public void isTraditionalTest() {
    ZhConvertBootstrap bs = ZhConvertBootstrap.newInstance().isTraditionalMatch(ZhMatches.traditionalAll()).init();
    String text = "123俺們456";
    Assert.assertFalse(bs.isTraditional(text));
    bs.isTraditionalMatch(ZhMatches.traditionalAny()).init();
    Assert.assertTrue(bs.isTraditional(text));
    ;
}

public static ZhMatch traditionalAll() {
    return new TraditionalZhMatchAll();
}


@Test
public void findAllTest() {
    final String text = "五星红旗迎风飘扬，毛主席的画像屹立在天安门前。";
    List<String> wordList = SensitiveWordBs.getInstance().findAll(text);
    Assert.assertEquals("[五星红旗, 毛主席, 天安门]", wordList.toString());
}

public List<String> findAll() {
    if (StringUtil.isEmpty(target)) {
        return Guavas.newArrayList();
    }
    return null;
}

public List<String> findAll(final String target) {
    return this.sensitiveWordMap.findAll(target);
}

@Test
public void findFirstTest() {
    final String text = "五星红旗迎风飘扬，毛主席的画像屹立在天安门前。";
    String word = SensitiveWordBs.getInstance().findFirst(text);
    Assert.assertEquals("五星红旗", word);
}

public String findFirst(final String target) {
    return this.sensitiveWordMap.findFirst(target);
}


@Test
public void replaceTest() {
    final String text = "五星红旗迎风飘扬，毛主席的画像屹立在天安门前。";
    String result = SensitiveWordBs.getInstance().replace(text);
    Assert.assertEquals("****迎风飘扬，***的画像屹立在***前。", result);
}

public String replace(final String target) {
    return this.replace(target, CharConst.STAR);
}

@Test
public void replaceCharTest() {
    final String text = "五星红旗迎风飘扬，毛主席的画像屹立在天安门前。";
    String result = SensitiveWordBs.getInstance().replace(text, '0');
    Assert.assertEquals("0000迎风飘扬，000的画像屹立在000前。", result);
}

public String replace(final String target, final char replaceChar) {
    return this.sensitiveWordMap.replace(target, replaceChar);
}


@Test
public void sizeTest() {
    final String dictPath = "/dict.txt";
    List<String> stringList = StreamUtils.readAllLines(dictPath);
    Assert.assertEquals(183836, stringList.size());
}

public static List<String> readAllLines(final String path) {
    return readAllLines(path, CharsetConst.UTF8, true);
}


@Test
public void sizeTest() {
    final String dictPath = "/dict.txt";
    List<String> stringList = StreamUtils.readAllLines(dictPath);
    Assert.assertEquals(183837, stringList.size());
}


@Test
public void testSubscriptionRemove() throws Exception {
    final Roster input = new Roster();
    input.setType(IQ.Type.set);
    final JID address = new JID("foo", "example.org", null);
    final Roster.Subscription subscription = Roster.Subscription.remove;
    input.addItem(address, subscription);
    final Roster result = IQRosterHandler.ignoreNonRemoveSubscriptionValues(input);
    assertTrue(result.getItems().stream().anyMatch(item -> item.getJID().equals(address) && subscription.equals(item.getSubscription())));
}

public static org.xmpp.packet.Roster ignoreNonRemoveSubscriptionValues(org.xmpp.packet.Roster stanza) {
    final org.xmpp.packet.Roster result = stanza.createCopy();
    final Element query = result.getElement().element(new QName("query", Namespace.get("jabber:iq:roster")));
    if (query != null) {
        for (Iterator<Element> i = query.elementIterator("item"); i.hasNext(); ) {
            final Element item = i.next();
            final Attribute subscription = item.attribute("subscription");
            if (subscription != null && !"remove".equals(subscription.getValue())) {
                item.remove(subscription);
            }
        }
    }
    return result;
}


@VisibleForTesting
public static String encodeHash(String hash) {
    return Base64.getEncoder().encodeToString(StringUtils.decodeHex(hash));
}


@Test
public void testSimpleCommonName() throws Exception {
    final String subject = "CN=MySubjectCommonName";
    final X509v3CertificateBuilder builder = new JcaX509v3CertificateBuilder(new X500Name("CN=MyIssuer"), BigInteger.valueOf(Math.abs(new SecureRandom().nextInt())), Date.from(Instant.now().plus(Duration.ofDays(30))), Date.from(Instant.now().minus(Duration.ofDays(99))), new X500Name(subject), subjectKeyPair.getPublic());
    final X509CertificateHolder certificateHolder = builder.build(contentSigner);
    final X509Certificate cert = new JcaX509CertificateConverter().getCertificate(certificateHolder);
    final List<String> identities = new CNCertificateIdentityMapping().mapIdentity(cert);
    assertEquals(1, identities.size());
    assertEquals("MySubjectCommonName", identities.get(0));
}

@Override
public List<String> mapIdentity(X509Certificate certificate) {
    String name = certificate.getSubjectDN().getName();
    Matcher matcher = cnPattern.matcher(name);
    List<String> names = new ArrayList<>();
    while (matcher.find()) {
        names.add(matcher.group(2));
    }
    return names;
}

@Override
public List<String> mapIdentity(X509Certificate certificate) {
    final List<String> names = new ArrayList<>();
    final X500Principal p = certificate.getSubjectX500Principal();
    final LdapName ln;
    try {
        ln = new LdapName(p.getName(X500Principal.RFC2253));
        for (final Rdn rdn : ln.getRdns()) {
            if ("CN".equalsIgnoreCase(rdn.getType())) {
                names.add(rdn.getValue().toString());
            }
        }
    } catch (InvalidNameException e) {
        Log.warn("Unable to extract identify from certificate: {}", certificate, e);
    }
    return names;
}

@Test
public void testInjectedCommonName() throws Exception {
    final String subject = "OU=\"infrastructure, CN=admin,\", O=Example Corp, C=GB, CN=attacker";
    final X509v3CertificateBuilder builder = new JcaX509v3CertificateBuilder(new X500Name("CN=MyIssuer"), BigInteger.valueOf(Math.abs(new SecureRandom().nextInt())), Date.from(Instant.now().plus(Duration.ofDays(30))), Date.from(Instant.now().minus(Duration.ofDays(99))), new X500Name(subject), subjectKeyPair.getPublic());
    final X509CertificateHolder certificateHolder = builder.build(contentSigner);
    final X509Certificate cert = new JcaX509CertificateConverter().getCertificate(certificateHolder);
    final List<String> identities = new CNCertificateIdentityMapping().mapIdentity(cert);
    assertEquals(1, identities.size(), "Identities found: " + identities);
    assertEquals("attacker", identities.get(0));
}


@Test
public void testSimpleCommonName() throws Exception {
    final String subject = "CN=MySubjectCommonName";
    final X509v3CertificateBuilder builder = new JcaX509v3CertificateBuilder(new X500Name("CN=MyIssuer"), BigInteger.valueOf(Math.abs(new SecureRandom().nextInt())), Date.from(Instant.now().plus(Duration.ofDays(30))), Date.from(Instant.now().minus(Duration.ofDays(99))), new X500Name(subject), subjectKeyPair.getPublic());
    final X509CertificateHolder certificateHolder = builder.build(contentSigner);
    final X509Certificate cert = new JcaX509CertificateConverter().getCertificate(certificateHolder);
    final List<String> identities = new CNCertificateIdentityMapping().mapIdentity(cert);
    assertTrue(identities.contains("MySubjectCommonName"), "Did not find 'MySubjectCommonName' identity in certificate. All Identities found: " + identities);
    assertEquals(1, identities.size(), "Unexpected amount of identities found in certificate. All Identities found: " + identities);
}

@Test
public void testInjectedCommonNameCert() throws Exception {
    final CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509");
    final X509Certificate certificate = (X509Certificate) certificateFactory.generateCertificate(getClass().getResourceAsStream("/cert/forged.crt"));
    final List<String> identities = new CNCertificateIdentityMapping().mapIdentity(certificate);
    assertFalse(identities.contains("victim"), "Unexpectedly found 'victim' identity in certificate. All identities found: " + identities);
    assertTrue(identities.contains("attacker"), "Did not find 'attacker' identity in certificate. All Identities found: " + identities);
    assertEquals(1, identities.size(), "Unexpected amount of identities found in certificate. All Identities found: " + identities);
}

@Test
public void testInjectedCommonName() throws Exception {
    final String subject = "OU=\"infrastructure, CN=admin,\", O=Example Corp, C=GB, CN=attacker";
    final X509v3CertificateBuilder builder = new JcaX509v3CertificateBuilder(new X500Name("CN=MyIssuer"), BigInteger.valueOf(Math.abs(new SecureRandom().nextInt())), Date.from(Instant.now().plus(Duration.ofDays(30))), Date.from(Instant.now().minus(Duration.ofDays(99))), new X500Name(subject), subjectKeyPair.getPublic());
    final X509CertificateHolder certificateHolder = builder.build(contentSigner);
    final X509Certificate cert = new JcaX509CertificateConverter().getCertificate(certificateHolder);
    final List<String> identities = new CNCertificateIdentityMapping().mapIdentity(cert);
    assertFalse(identities.contains("admin"), "Unexpectedly found 'admin' identity in certificate. All identities found: " + identities);
    assertTrue(identities.contains("attacker"), "Did not find 'attacker' identity in certificate. All Identities found: " + identities);
    assertEquals(1, identities.size(), "Unexpected amount of identities found in certificate. All Identities found: " + identities);
}


@Test
public void test() throws Exception {
    final String raw = """
        <iq type="result" xmlns="jabber:client" to="inputmice3@igniterealtime.org/Conversations.cI4W" from="igniterealtime.org" id="L3xl8X8_kzvx">
          <query node="https://www.igniterealtime.org/projects/openfire/#Cd91QBSG4JGOCEvRsSz64xeJPMk=" xmlns="http://jabber.org/protocol/disco#info">
            <identity name="Openfire Server" type="im" category="server" xmlns="http://jabber.org/protocol/disco#info"/>
            <identity type="pep" category="pubsub" xmlns="http://jabber.org/protocol/disco#info"/>
            <feature var="urn:xmpp:raa:0#embed-presence-directed" xmlns="http://jabber.org/protocol/disco#info"/>
            <feature var="http://jabber.org/protocol/caps" xmlns="http://jabber.org/protocol/disco#info"/>
            <feature var="http://jabber.org/protocol/pubsub#retrieve-default" xmlns="http://jabber.org/protocol/disco#info"/>
            <feature var="http://jabber.org/protocol/pubsub#purge-nodes" xmlns="http://jabber.org/protocol/disco#info"/>
            <feature var="http://jabber.org/protocol/pubsub#subscription-options" xmlns="http://jabber.org/protocol/disco#info"/>
            <feature var="urn:xmpp:raa:0#embed-message " xmlns="http://jabber.org/protocol/disco#info"/>
            <feature var="http://jabber.org/protocol/pubsub#outcast-affiliation" xmlns="http://jabber.org/protocol/disco#info"/>
            <feature var="msgoffline" xmlns="http://jabber.org/protocol/disco#info"/>
            <feature var="http://jabber.org/protocol/pubsub#delete-nodes" xmlns="http://jabber.org/protocol/disco#info"/>
            <feature var="jabber:iq:register" xmlns="http://jabber.org/protocol/disco#info"/>
            <feature var="http://jabber.org/protocol/pubsub#config-node" xmlns="http://jabber.org/protocol/disco#info"/>
            <feature var="http://jabber.org/protocol/pubsub#retrieve-items" xmlns="http://jabber.org/protocol/disco#info"/>
            <feature var="http://jabber.org/protocol/pubsub#auto-create" xmlns="http://jabber.org/protocol/disco#info"/>
            <feature var="http://jabber.org/protocol/disco#items" xmlns="http://jabber.org/protocol/disco#info"/>
            <feature var="http://jabber.org/protocol/pubsub#delete-items" xmlns="http://jabber.org/protocol/disco#info"/>
            <feature var="urn:xmpp:mam:0" xmlns="http://jabber.org/protocol/disco#info"/>
            <feature var="urn:xmpp:mam:1" xmlns="http://jabber.org/protocol/disco#info"/>
            <feature var="urn:xmpp:mam:2" xmlns="http://jabber.org/protocol/disco#info"/>
            <feature var="urn:xmpp:fulltext:0" xmlns="http://jabber.org/protocol/disco#info"/>
            <feature var="http://jabber.org/protocol/pubsub#persistent-items" xmlns="http://jabber.org/protocol/disco#info"/>
            <feature var="http://jabber.org/protocol/pubsub#create-and-configure" xmlns="http://jabber.org/protocol/disco#info"/>
            <feature var="http://jabber.org/protocol/pubsub#retrieve-affiliations" xmlns="http://jabber.org/protocol/disco#info"/>
            <feature var="urn:xmpp:time" xmlns="http://jabber.org/protocol/disco#info"/>
            <feature var="http://jabber.org/protocol/pubsub#manage-subscriptions" xmlns="http://jabber.org/protocol/disco#info"/>
            <feature var="urn:xmpp:bookmarks-conversion:0" xmlns="http://jabber.org/protocol/disco#info"/>
            <feature var="http://jabber.org/protocol/pubsub#auto-subscribe" xmlns="http://jabber.org/protocol/disco#info"/>
            <feature var="http://jabber.org/protocol/offline" xmlns="http://jabber.org/protocol/disco#info"/>
            <feature var="http://jabber.org/protocol/pubsub#publish-options" xmlns="http://jabber.org/protocol/disco#info"/>
            <feature var="urn:xmpp:carbons:2" xmlns="http://jabber.org/protocol/disco#info"/>
            <feature var="http://jabber.org/protocol/address" xmlns="http://jabber.org/protocol/disco#info"/>
            <feature var="http://jabber.org/protocol/pubsub#collections" xmlns="http://jabber.org/protocol/disco#info"/>
            <feature var="http://jabber.org/protocol/pubsub#retrieve-subscriptions" xmlns="http://jabber.org/protocol/disco#info"/>
            <feature var="vcard-temp" xmlns="http://jabber.org/protocol/disco#info"/>
            <feature var="http://jabber.org/protocol/pubsub#subscribe" xmlns="http://jabber.org/protocol/disco#info"/>
            <feature var="http://jabber.org/protocol/pubsub#create-nodes" xmlns="http://jabber.org/protocol/disco#info"/>
            <feature var="http://jabber.org/protocol/pubsub#get-pending" xmlns="http://jabber.org/protocol/disco#info"/>
            <feature var="urn:xmpp:blocking" xmlns="http://jabber.org/protocol/disco#info"/>
            <feature var="http://jabber.org/protocol/pubsub#multi-subscribe" xmlns="http://jabber.org/protocol/disco#info"/>
            <feature var="http://jabber.org/protocol/pubsub#presence-notifications" xmlns="http://jabber.org/protocol/disco#info"/>
            <feature var="urn:xmpp:ping" xmlns="http://jabber.org/protocol/disco#info"/>
            <feature var="urn:xmpp:archive:manage" xmlns="http://jabber.org/protocol/disco#info"/>
            <feature var="http://jabber.org/protocol/pubsub#filtered-notifications" xmlns="http://jabber.org/protocol/disco#info"/>
            <feature var="urn:xmpp:push:0" xmlns="http://jabber.org/protocol/disco#info"/>
            <feature var="http://jabber.org/protocol/pubsub#meta-data" xmlns="http://jabber.org/protocol/disco#info"/>
            <feature var="http://jabber.org/protocol/pubsub#multi-items" xmlns="http://jabber.org/protocol/disco#info"/>
            <feature var="http://jabber.org/protocol/pubsub#item-ids" xmlns="http://jabber.org/protocol/disco#info"/>
            <feature var="jabber:iq:roster" xmlns="http://jabber.org/protocol/disco#info"/>
            <feature var="http://jabber.org/protocol/pubsub#instant-nodes" xmlns="http://jabber.org/protocol/disco#info"/>
            <feature var="http://jabber.org/protocol/pubsub#modify-affiliations" xmlns="http://jabber.org/protocol/disco#info"/>
            <feature var="urn:xmpp:raa:0#embed-presence-sub" xmlns="http://jabber.org/protocol/disco#info"/>
            <feature var="http://jabber.org/protocol/pubsub" xmlns="http://jabber.org/protocol/disco#info"/>
            <feature var="http://jabber.org/protocol/pubsub#publisher-affiliation" xmlns="http://jabber.org/protocol/disco#info"/>
            <feature var="http://jabber.org/protocol/pubsub#access-open" xmlns="http://jabber.org/protocol/disco#info"/>
            <feature var="jabber:iq:version" xmlns="http://jabber.org/protocol/disco#info"/>
            <feature var="http://jabber.org/protocol/pubsub#retract-items" xmlns="http://jabber.org/protocol/disco#info"/>
            <feature var="urn:xmpp:extdisco:1" xmlns="http://jabber.org/protocol/disco#info"/>
            <feature var="jabber:iq:privacy" xmlns="http://jabber.org/protocol/disco#info"/>
            <feature var="urn:xmpp:extdisco:2" xmlns="http://jabber.org/protocol/disco#info"/>
            <feature var="http://jabber.org/protocol/commands" xmlns="http://jabber.org/protocol/disco#info"/>
            <feature var="jabber:iq:last" xmlns="http://jabber.org/protocol/disco#info"/>
            <feature var="urn:xmpp:raa:0" xmlns="http://jabber.org/protocol/disco#info"/>
            <feature var="http://jabber.org/protocol/pubsub#publish" xmlns="http://jabber.org/protocol/disco#info"/>
            <feature var="urn:xmpp:serverinfo:0" xmlns="http://jabber.org/protocol/disco#info"/>
            <feature var="urn:xmpp:archive:auto" xmlns="http://jabber.org/protocol/disco#info"/>
            <feature var="http://jabber.org/protocol/disco#info" xmlns="http://jabber.org/protocol/disco#info"/>
            <feature var="jabber:iq:private" xmlns="http://jabber.org/protocol/disco#info"/>
            <feature var="http://jabber.org/protocol/rsm" xmlns="http://jabber.org/protocol/disco#info"/>
            <x type="result" xmlns="jabber:x:data">
              <field var="FORM_TYPE" type="hidden" xmlns="jabber:x:data">
                <value xmlns="jabber:x:data">urn:xmpp:dataforms:softwareinfo</value>
              </field>
              <field var="os" type="text-single" xmlns="jabber:x:data">
                <value xmlns="jabber:x:data">Linux</value>
              </field>
              <field var="os_version" type="text-single" xmlns="jabber:x:data">
                <value xmlns="jabber:x:data">4.14.355-276.618.amzn2.x86_64 amd64 - Java 17.0.14</value>
              </field>
              <field var="software" type="text-single" xmlns="jabber:x:data">
                <value xmlns="jabber:x:data">Openfire</value>
              </field>
              <field var="software_version" type="text-single" xmlns="jabber:x:data">
                <value xmlns="jabber:x:data">5.0.0 Alpha</value>
              </field>
            </x>
            <x type="result" xmlns="jabber:x:data">
              <field var="FORM_TYPE" type="hidden" xmlns="jabber:x:data">
                <value xmlns="jabber:x:data">http://jabber.org/network/serverinfo</value>
              </field>
              <field var="admin-addresses" type="list-multi" xmlns="jabber:x:data">
                <value xmlns="jabber:x:data">xmpp:dwd@dave.cridland.net</value>
                <value xmlns="jabber:x:data">xmpp:akrherz@igniterealtime.org</value>
                <value xmlns="jabber:x:data">xmpp:benjamin@igniterealtime.org</value>
                <value xmlns="jabber:x:data">mailto:benjamin@holyarmy.org</value>
                <value xmlns="jabber:x:data">xmpp:csh@igniterealtime.org</value>
                <value xmlns="jabber:x:data">xmpp:dan.caseley@igniterealtime.org</value>
                <value xmlns="jabber:x:data">mailto:dan.caseley@surevine.com</value>
                <value xmlns="jabber:x:data">xmpp:flow@igniterealtime.org</value>
                <value xmlns="jabber:x:data">xmpp:gdt@igniterealtime.org</value>
                <value xmlns="jabber:x:data">mailto:greg.d.thomas@gmail.com</value>
                <value xmlns="jabber:x:data">xmpp:guus.der.kinderen@igniterealtime.org</value>
                <value xmlns="jabber:x:data">mailto:guus.der.kinderen@gmail.com</value>
                <value xmlns="jabber:x:data">xmpp:lg@igniterealtime.org</value>
                <value xmlns="jabber:x:data">xmpp:rcollier@igniterealtime.org</value>
                <value xmlns="jabber:x:data">mailto:robincollier@hotmail.com</value>
              </field>
            </x>
          </query>
        </iq>
        """;
    final Document doc = DocumentHelper.parseText(raw);
    final IQ input = new IQ(doc.getRootElement());
    final String result = EntityCapabilitiesManager.generateVerHash(input, "SHA-1");
    assertEquals("89D/mEGBT1K0RtY28gEkGRbV2rc=", result);
}

public static String generateVerHash(IQ packet, String algorithm) {
    final StringBuilder s = new StringBuilder();
    final List<String> discoIdentities = getIdentitiesFrom(packet);
    Collections.sort(discoIdentities);
    for (String discoIdentity : discoIdentities) {
        s.append(discoIdentity);
        s.append('<');
    }
    final List<String> discoFeatures = getFeaturesFrom(packet);
    Collections.sort(discoFeatures);
    for (String discoFeature : discoFeatures) {
        s.append(discoFeature);
        s.append('<');
    }
    final List<String> extendedDataForms = getExtendedDataForms(packet);
    Collections.sort(extendedDataForms);
    for (String extendedDataForm : extendedDataForms) {
        s.append(extendedDataForm);
    }
    final String hashed = StringUtils.hash(s.toString(), algorithm);
    return Base64.getEncoder().encodeToString(StringUtils.decodeHex(hashed));
}

public static String generateVerHash(IQ packet, String algorithm) {
    final String s = generateS(packet);
    final String hashed = generateHash(s, algorithm);
    final String result = encodeHash(hashed);
    Log.trace("Calculated Caps Hash '{}' from Caps string '{}' from stanza: {}", result, s, packet);
    return result;
}


@Test
public void testAddOnceForNonUnique() throws Exception {
    final ConcurrentMap<NodeID, Set<String>> reverseLookupMap = new ConcurrentHashMap<>();
    final ReverseLookupUpdatingCacheEntryListener<String, Set<NodeID>> listener = new ReverseLookupUpdatingCacheEntryListener<>(reverseLookupMap, false);
    final NodeID clusterNode = NodeID.getInstance(UUID.randomUUID().toString().getBytes());
    listener.entryAdded("somekey", null, clusterNode);
    assertTrue(reverseLookupMap.containsKey(clusterNode) && reverseLookupMap.get(clusterNode).contains("somekey"));
}

@Override
public void entryAdded(@Nonnull final K key, @Nullable final V value, @Nonnull final NodeID nodeID) {
    reverseCacheRepresentation.computeIfAbsent(nodeID, k -> new HashSet<>()).add(key);
}

@Override
public void entryAdded(@Nonnull final K key, @Nullable final V value, @Nonnull final NodeID nodeID) {
    synchronized (mutex.intern(key)) {
        reverseCacheRepresentation.computeIfAbsent(nodeID, k -> new HashSet<>()).add(key);
        if (uniqueOwnerExpected && !DISABLE_OPTIMIZATION.getValue()) {
            reverseCacheRepresentation.forEach((k, v) -> {
                if (!k.equals(nodeID)) {
                    v.remove(key);
                }
            });
        }
    }
}

@Test
public void testUpdateForNonUniqueInWrongOrder() throws Exception {
    final ConcurrentMap<NodeID, Set<String>> reverseLookupMap = new ConcurrentHashMap<>();
    final ReverseLookupUpdatingCacheEntryListener<String, Set<NodeID>> listener = new ReverseLookupUpdatingCacheEntryListener<>(reverseLookupMap, false);
    final NodeID clusterNode = NodeID.getInstance(UUID.randomUUID().toString().getBytes());
    listener.entryUpdated("somekey", null, null, clusterNode);
    listener.entryAdded("somekey", null, clusterNode);
    assertTrue(reverseLookupMap.containsKey(clusterNode) && reverseLookupMap.get(clusterNode).contains("somekey"));
}

@Override
public void entryUpdated(@Nonnull final K key, @Nullable final V oldValue, @Nullable final V newValue, @Nonnull final NodeID nodeID) {
    entryAdded(key, newValue, nodeID);
}

@Override
public void entryUpdated(@Nonnull final K key, @Nullable final V oldValue, @Nullable final V newValue, @Nonnull final NodeID nodeID) {
    if (DISABLE_OPTIMIZATION.getValue()) {
        return;
    }
    synchronized (mutex.intern(key)) {
        entryAdded(key, newValue, nodeID);
    }
}

@Test
public void testAddTwiceForUnique() throws Exception {
    final ConcurrentMap<NodeID, Set<String>> reverseLookupMap = new ConcurrentHashMap<>();
    final ReverseLookupUpdatingCacheEntryListener<String, Set<NodeID>> listener = new ReverseLookupUpdatingCacheEntryListener<>(reverseLookupMap, true);
    final NodeID clusterNodeA = NodeID.getInstance(UUID.randomUUID().toString().getBytes());
    final NodeID clusterNodeB = NodeID.getInstance(UUID.randomUUID().toString().getBytes());
    listener.entryAdded("somekey", null, clusterNodeA);
    listener.entryAdded("somekey", null, clusterNodeB);
    assertFalse(reverseLookupMap.containsKey(clusterNodeA) && reverseLookupMap.get(clusterNodeA).contains("somekey"));
    assertTrue(reverseLookupMap.containsKey(clusterNodeB) && reverseLookupMap.get(clusterNodeB).contains("somekey"));
}

@Test
public void testUpdateForUnique() throws Exception {
    final ConcurrentMap<NodeID, Set<String>> reverseLookupMap = new ConcurrentHashMap<>();
    final ReverseLookupUpdatingCacheEntryListener<String, Set<NodeID>> listener = new ReverseLookupUpdatingCacheEntryListener<>(reverseLookupMap, true);
    final NodeID clusterNode = NodeID.getInstance(UUID.randomUUID().toString().getBytes());
    listener.entryAdded("somekey", null, clusterNode);
    listener.entryUpdated("somekey", null, null, clusterNode);
    assertTrue(reverseLookupMap.containsKey(clusterNode) && reverseLookupMap.get(clusterNode).contains("somekey"));
}

@Test
public void testAddOnceForUnique() throws Exception {
    final ConcurrentMap<NodeID, Set<String>> reverseLookupMap = new ConcurrentHashMap<>();
    final ReverseLookupUpdatingCacheEntryListener<String, Set<NodeID>> listener = new ReverseLookupUpdatingCacheEntryListener<>(reverseLookupMap, true);
    final NodeID clusterNode = NodeID.getInstance(UUID.randomUUID().toString().getBytes());
    listener.entryAdded("somekey", null, clusterNode);
    assertTrue(reverseLookupMap.containsKey(clusterNode) && reverseLookupMap.get(clusterNode).contains("somekey"));
}

@Test
public void testUpdateForUniqueInWrongOrder() throws Exception {
    final ConcurrentMap<NodeID, Set<String>> reverseLookupMap = new ConcurrentHashMap<>();
    final ReverseLookupUpdatingCacheEntryListener<String, Set<NodeID>> listener = new ReverseLookupUpdatingCacheEntryListener<>(reverseLookupMap, true);
    final NodeID clusterNode = NodeID.getInstance(UUID.randomUUID().toString().getBytes());
    listener.entryUpdated("somekey", null, null, clusterNode);
    listener.entryAdded("somekey", null, clusterNode);
    assertTrue(reverseLookupMap.containsKey(clusterNode) && reverseLookupMap.get(clusterNode).contains("somekey"));
}

@Test
public void testUpdateFromAnotherNodeForUnique() throws Exception {
    final ConcurrentMap<NodeID, Set<String>> reverseLookupMap = new ConcurrentHashMap<>();
    final ReverseLookupUpdatingCacheEntryListener<String, Set<NodeID>> listener = new ReverseLookupUpdatingCacheEntryListener<>(reverseLookupMap, true);
    final NodeID clusterNodeA = NodeID.getInstance(UUID.randomUUID().toString().getBytes());
    final NodeID clusterNodeB = NodeID.getInstance(UUID.randomUUID().toString().getBytes());
    listener.entryAdded("somekey", null, clusterNodeA);
    listener.entryUpdated("somekey", null, null, clusterNodeB);
    assertFalse(reverseLookupMap.containsKey(clusterNodeA) && reverseLookupMap.get(clusterNodeA).contains("somekey"));
    assertTrue(reverseLookupMap.containsKey(clusterNodeB) && reverseLookupMap.get(clusterNodeB).contains("somekey"));
}

@Test
public void testUpdateForNonUnique() throws Exception {
    final ConcurrentMap<NodeID, Set<String>> reverseLookupMap = new ConcurrentHashMap<>();
    final ReverseLookupUpdatingCacheEntryListener<String, Set<NodeID>> listener = new ReverseLookupUpdatingCacheEntryListener<>(reverseLookupMap, false);
    final NodeID clusterNode = NodeID.getInstance(UUID.randomUUID().toString().getBytes());
    listener.entryAdded("somekey", null, clusterNode);
    listener.entryUpdated("somekey", null, null, clusterNode);
    assertTrue(reverseLookupMap.containsKey(clusterNode) && reverseLookupMap.get(clusterNode).contains("somekey"));
}

@Test
public void testUpdateFromAnotherNodeForNonUnique() throws Exception {
    final ConcurrentMap<NodeID, Set<String>> reverseLookupMap = new ConcurrentHashMap<>();
    final ReverseLookupUpdatingCacheEntryListener<String, Set<NodeID>> listener = new ReverseLookupUpdatingCacheEntryListener<>(reverseLookupMap, false);
    final NodeID clusterNodeA = NodeID.getInstance(UUID.randomUUID().toString().getBytes());
    final NodeID clusterNodeB = NodeID.getInstance(UUID.randomUUID().toString().getBytes());
    listener.entryAdded("somekey", null, clusterNodeA);
    listener.entryUpdated("somekey", null, null, clusterNodeB);
    assertTrue(reverseLookupMap.containsKey(clusterNodeA) && reverseLookupMap.get(clusterNodeA).contains("somekey"));
    assertTrue(reverseLookupMap.containsKey(clusterNodeB) && reverseLookupMap.get(clusterNodeB).contains("somekey"));
}

@Test
public void testUpdateFromAnotherNodeForNonUniqueInWrongOrder() throws Exception {
    final ConcurrentMap<NodeID, Set<String>> reverseLookupMap = new ConcurrentHashMap<>();
    final ReverseLookupUpdatingCacheEntryListener<String, Set<NodeID>> listener = new ReverseLookupUpdatingCacheEntryListener<>(reverseLookupMap, false);
    final NodeID clusterNodeA = NodeID.getInstance(UUID.randomUUID().toString().getBytes());
    final NodeID clusterNodeB = NodeID.getInstance(UUID.randomUUID().toString().getBytes());
    listener.entryUpdated("somekey", null, null, clusterNodeB);
    listener.entryAdded("somekey", null, clusterNodeA);
    assertTrue(reverseLookupMap.containsKey(clusterNodeA) && reverseLookupMap.get(clusterNodeA).contains("somekey"));
    assertTrue(reverseLookupMap.containsKey(clusterNodeB) && reverseLookupMap.get(clusterNodeB).contains("somekey"));
}

@Test
public void testAddTwiceForNonUnique() throws Exception {
    final ConcurrentMap<NodeID, Set<String>> reverseLookupMap = new ConcurrentHashMap<>();
    final ReverseLookupUpdatingCacheEntryListener<String, Set<NodeID>> listener = new ReverseLookupUpdatingCacheEntryListener<>(reverseLookupMap, false);
    final NodeID clusterNodeA = NodeID.getInstance(UUID.randomUUID().toString().getBytes());
    final NodeID clusterNodeB = NodeID.getInstance(UUID.randomUUID().toString().getBytes());
    listener.entryAdded("somekey", null, clusterNodeA);
    listener.entryAdded("somekey", null, clusterNodeB);
    assertTrue(reverseLookupMap.containsKey(clusterNodeA) && reverseLookupMap.get(clusterNodeA).contains("somekey"));
    assertTrue(reverseLookupMap.containsKey(clusterNodeB) && reverseLookupMap.get(clusterNodeB).contains("somekey"));
}


@Test
public void usingMockito_UsingBDD() {
    TodoService todoService = mock(TodoService.class);
    TodoBusinessImpl todoBusinessImpl = new TodoBusinessImpl(todoService);
    List<String> allTodos = Arrays.asList("Learn Spring MVC", "Learn Spring", "Learn to Dance");
    given(todoService.retrieveTodos("Ranga")).willReturn(allTodos);
    List<String> todos = todoBusinessImpl.retrieveTodosRelatedToSpring("Ranga");
    assertThat(todos.size(), is(2));
}

public List<String> retrieveTodosRelatedToSpring(String user) {
    List<String> filteredTodos = new ArrayList<String>();
    List<String> allTodos = todoService.retrieveTodos(user);
    for (String todo : allTodos) {
        if (todo.contains("Spring")) {
            filteredTodos.add(todo);
        }
    }
    return filteredTodos;
}


@Test
public void usingAStub() {
    TodoService todoService = new TodoServiceStub();
    TodoBusinessImpl todoBusinessImpl = new TodoBusinessImpl(todoService);
    List<String> todos = todoBusinessImpl.retrieveTodosRelatedToSpring("Ranga");
    assertEquals(2, todos.size());
}


@Test
public void usingMockito() {
    TodoService todoService = mock(TodoService.class);
    List<String> allTodos = Arrays.asList("Learn Spring MVC", "Learn Spring", "Learn to Dance");
    when(todoService.retrieveTodos("Ranga")).thenReturn(allTodos);
    TodoBusinessImpl todoBusinessImpl = new TodoBusinessImpl(todoService);
    List<String> todos = todoBusinessImpl.retrieveTodosRelatedToSpring("Ranga");
    assertEquals(2, todos.size());
}


@Test
public void letsTestDeleteNow() {
    TodoService todoService = mock(TodoService.class);
    List<String> allTodos = Arrays.asList("Learn Spring MVC", "Learn Spring", "Learn to Dance");
    when(todoService.retrieveTodos("Ranga")).thenReturn(allTodos);
    TodoBusinessImpl todoBusinessImpl = new TodoBusinessImpl(todoService);
    todoBusinessImpl.deleteTodosNotRelatedToSpring("Ranga");
    verify(todoService).deleteTodo("Learn to Dance");
    verify(todoService, Mockito.never()).deleteTodo("Learn Spring MVC");
    verify(todoService, Mockito.never()).deleteTodo("Learn Spring");
    verify(todoService, Mockito.times(1)).deleteTodo("Learn to Dance");
}

public void deleteTodosNotRelatedToSpring(String user) {
    List<String> allTodos = todoService.retrieveTodos(user);
    for (String todo : allTodos) {
        if (!todo.contains("Spring")) {
            todoService.deleteTodo(todo);
        }
    }
}


@Test
public void captureArgument() {
    ArgumentCaptor<String> argumentCaptor = ArgumentCaptor.forClass(String.class);
    TodoService todoService = mock(TodoService.class);
    List<String> allTodos = Arrays.asList("Learn Spring MVC", "Learn Spring", "Learn to Dance");
    Mockito.when(todoService.retrieveTodos("Ranga")).thenReturn(allTodos);
    TodoBusinessImpl todoBusinessImpl = new TodoBusinessImpl(todoService);
    todoBusinessImpl.deleteTodosNotRelatedToSpring("Ranga");
    Mockito.verify(todoService).deleteTodo(argumentCaptor.capture());
    assertEquals("Learn to Dance", argumentCaptor.getValue());
}


@Test
public void letsTestDeleteNow() {
    List<String> allTodos = Arrays.asList("Learn Spring MVC", "Learn Spring", "Learn to Dance");
    when(todoService.retrieveTodos("Ranga")).thenReturn(allTodos);
    todoBusinessImpl.deleteTodosNotRelatedToSpring("Ranga");
    verify(todoService).deleteTodo("Learn to Dance");
    verify(todoService, Mockito.never()).deleteTodo("Learn Spring MVC");
    verify(todoService, Mockito.never()).deleteTodo("Learn Spring");
    verify(todoService, Mockito.times(1)).deleteTodo("Learn to Dance");
}

@Test
public void captureArgument() {
    List<String> allTodos = Arrays.asList("Learn Spring MVC", "Learn Spring", "Learn to Dance");
    Mockito.when(todoService.retrieveTodos("Ranga")).thenReturn(allTodos);
    todoBusinessImpl.deleteTodosNotRelatedToSpring("Ranga");
    Mockito.verify(todoService).deleteTodo(stringArgumentCaptor.capture());
    assertEquals("Learn to Dance", stringArgumentCaptor.getValue());
}


@Test
public void rulesEngineListenersShouldBeCalledInOrderWhenCheckingRules() throws Exception {
    when(rule1.evaluate(facts)).thenReturn(true);
    rules.register(rule1);
    rulesEngine.check(rules, facts);
    InOrder inOrder = inOrder(rule1, fact1, fact2, rulesEngineListener1, rulesEngineListener2);
    inOrder.verify(rulesEngineListener1).beforeEvaluate(rules, facts);
    inOrder.verify(rulesEngineListener2).beforeEvaluate(rules, facts);
    inOrder.verify(rule1).evaluate(facts);
    inOrder.verify(rulesEngineListener1).afterExecute(rules, facts);
    inOrder.verify(rulesEngineListener2).afterExecute(rules, facts);
}

@Override
public Map<Rule, Boolean> check(Rules rules, Facts facts) {
    LOGGER.info("Checking rules");
    Map<Rule, Boolean> result = new HashMap<>();
    for (Rule rule : rules) {
        if (shouldBeEvaluated(rule, facts)) {
            result.put(rule, rule.evaluate(facts));
        }
    }
    return result;
}

@Override
public Map<Rule, Boolean> check(Rules rules, Facts facts) {
    triggerListenersBeforeRules(rules, facts);
    LOGGER.info("Checking rules");
    Map<Rule, Boolean> result = new HashMap<>();
    for (Rule rule : rules) {
        if (shouldBeEvaluated(rule, facts)) {
            result.put(rule, rule.evaluate(facts));
        }
    }
    triggerListenersAfterRules(rules, facts);
    return result;
}


@Test(expected = IllegalArgumentException.class)
public void twoRulesWithSameHighestPriorityIsNotAllowed() {
    conditionalRuleGroup.addRule(new MyOtherRule(0));
    conditionalRuleGroup.addRule(new MyOtherRule(1));
    conditionalRuleGroup.addRule(new MyRule());
    conditionalRuleGroup.evaluate(facts);
}

@Override
public boolean evaluate(Facts facts) {
    successfulEvaluations = new HashSet<>();
    conditionalRule = getRuleWithHighestPriority();
    if (conditionalRule.evaluate(facts)) {
        for (Rule rule : rules) {
            if (rule != conditionalRule && rule.evaluate(facts)) {
                successfulEvaluations.add(rule);
            }
        }
        return true;
    }
    return false;
}


@Test
public void compositeRuleAndComposingRulesMustBeExecuted() throws Exception {
    unitRuleGroup = new UnitRuleGroup();
    unitRuleGroup.addRule(rule1);
    unitRuleGroup.addRule(rule2);
    rules.register(unitRuleGroup);
    rulesEngine.fire(rules, facts);
    verify(rule1).execute(facts);
    verify(rule2).execute(facts);
}

public void addRule(final Object rule) {
    Rule proxy = RuleProxy.asRule(rule);
    rules.add(proxy);
    proxyRules.put(rule, proxy);
}

@Test
public void whenNoComposingRulesAreRegistered_thenUnitRuleGroupShouldEvaluateToFalse() {
    unitRuleGroup = new UnitRuleGroup();
    boolean evaluationResult = unitRuleGroup.evaluate(facts);
    assertThat(evaluationResult).isFalse();
}

@Override
public boolean evaluate(Facts facts) {
    if (!rules.isEmpty()) {
        for (Rule rule : rules) {
            if (!rule.evaluate(facts)) {
                return false;
            }
        }
        return true;
    }
    return false;
}

@Test
public void compositeRuleMustNotBeExecutedIfAComposingRuleEvaluatesToFalse() throws Exception {
    when(rule2.evaluate(facts)).thenReturn(false);
    unitRuleGroup = new UnitRuleGroup();
    unitRuleGroup.addRule(rule1);
    unitRuleGroup.addRule(rule2);
    rules.register(unitRuleGroup);
    rulesEngine.fire(rules, facts);
    verify(rule1, never()).execute(facts);
    verify(rule2, never()).execute(facts);
}

@Test
public void whenARuleIsRemoved_thenItShouldNotBeEvaluated() throws Exception {
    unitRuleGroup = new UnitRuleGroup();
    unitRuleGroup.addRule(rule1);
    unitRuleGroup.addRule(rule2);
    unitRuleGroup.removeRule(rule2);
    rules.register(unitRuleGroup);
    rulesEngine.fire(rules, facts);
    verify(rule1).execute(facts);
    verify(rule2, never()).evaluate(facts);
    verify(rule2, never()).execute(facts);
}

public void removeRule(final Object rule) {
    Rule proxy = proxyRules.get(rule);
    if (proxy != null) {
        rules.remove(proxy);
    }
}


@Test
public void test() throws Exception {
    Facts facts = new Facts();
    facts.add("rain", true);
    facts.add("age", 18);
    Rules rules = new Rules(new WeatherRule(), new AgeRule());
    RulesEngine rulesEngine = new DefaultRulesEngine();
    rulesEngine.fire(rules, facts);
}

@Override
public void fire(Rules rules, Facts facts) {
    if (rules.isEmpty()) {
        LOGGER.warning("No rules registered! Nothing to apply");
        return;
    }
    sort(rules);
    logEngineParameters();
    log(rules, facts);
    apply(rules, facts);
}


@Test
public void basicRuleEvaluateShouldReturnFalse() throws Exception {
    BasicRule basicRule = new BasicRule();
    assertThat(basicRule.evaluate(facts)).isFalse();
}

public boolean evaluate(Facts facts) {
    return false;
}

@Test
public void testCompareTo() {
    FirstRule rule1 = new FirstRule();
    FirstRule rule2 = new FirstRule();
    assertThat(rule1.compareTo(rule2)).isEqualTo(0);
    assertThat(rule2.compareTo(rule1)).isEqualTo(0);
}

@Override
public int compareTo(final Rule rule) {
    if (getPriority() < rule.getPriority()) {
        return -1;
    } else if (getPriority() > rule.getPriority()) {
        return 1;
    } else {
        return getName().compareTo(rule.getName());
    }
}

@Test
public void testSortSequence() {
    FirstRule rule1 = new FirstRule();
    SecondRule rule2 = new SecondRule();
    ThirdRule rule3 = new ThirdRule();
    rules = new Rules(rule1, rule2, rule3);
    rulesEngine.check(rules, facts);
    assertThat(rules).containsSequence(rule1, rule3, rule2);
}

@Override
public Map<Rule, Boolean> check(Rules rules, Facts facts) {
    LOGGER.info("Checking rules");
    sort(rules);
    Map<Rule, Boolean> result = new HashMap<>();
    for (Rule rule : rules) {
        if (shouldBeEvaluated(rule, facts)) {
            result.put(rule, rule.evaluate(facts));
        }
    }
    return result;
}


@Test
public void compositeRuleAndComposingRulesMustBeExecuted() throws Exception {
    compositeRule.addRule(rule1);
    compositeRule.addRule(rule2);
    rules.clear();
    rules.register(compositeRule);
    rulesEngine.fire(rules, facts);
    verify(rule1).execute(facts);
    verify(rule2).execute(facts);
}

@Test
public void whenARuleIsRemoved_thenItShouldNotBeEvaluated() throws Exception {
    compositeRule.addRule(rule1);
    compositeRule.addRule(rule2);
    compositeRule.removeRule(rule2);
    rules.clear();
    rules.register(compositeRule);
    rulesEngine.fire(rules, facts);
    verify(rule1).execute(facts);
    verify(rule2, never()).evaluate(facts);
    verify(rule2, never()).execute(facts);
}

@Test
public void whenNoComposingRulesAreRegistered_thenCompositeRuleShouldEvaluateToFalse() {
    assertThat(compositeRule.evaluate(facts)).isFalse();
}

@Test
public void compositeRuleMustNotBeExecutedIfAComposingRuleEvaluatesToFalse() throws Exception {
    when(rule2.evaluate(facts)).thenReturn(false);
    compositeRule.addRule(rule1);
    compositeRule.addRule(rule2);
    rules.clear();
    rules.register(compositeRule);
    rulesEngine.fire(rules, facts);
    verify(rule1, never()).execute(facts);
    verify(rule2, never()).execute(facts);
}


@Test
public void whenCompareToIsOverridden_thenShouldExecuteRulesInTheCustomOrder() throws Exception {
    when(rule1.getName()).thenReturn("a");
    when(rule1.getPriority()).thenReturn(1);
    when(rule1.evaluate(facts)).thenReturn(true);
    when(rule2.getName()).thenReturn("b");
    when(rule2.getPriority()).thenReturn(0);
    when(rule2.evaluate(facts)).thenReturn(true);
    when(rule2.compareTo(rule1)).thenCallRealMethod();
    rules.clear();
    rules.register(rule1);
    rules.register(rule2);
    rulesEngine.fire(rules, facts);
    InOrder inOrder = inOrder(rule1, rule2);
    inOrder.verify(rule1).execute(facts);
    inOrder.verify(rule2).execute(facts);
}


@Test(expected = IllegalArgumentException.class)
public void conditionMethodMustBePublic() {
    ruleDefinitionValidator.validateRuleDefinition(new AnnotatedRuleWithNotPublicConditionMethod());
}

void validateRuleDefinition(final Object rule) {
    checkRuleClass(rule);
    checkConditionMethod(rule);
    checkActionMethods(rule);
    checkPriorityMethod(rule);
}

@Test(expected = IllegalArgumentException.class)
public void priorityMethodReturnTypeMustBeInteger() {
    ruleDefinitionValidator.validateRuleDefinition(new AnnotatedRuleWithPriorityMethodHavingNonIntegerReturnType());
}

@Test(expected = IllegalArgumentException.class)
public void actionMethodMustBeDefined() {
    ruleDefinitionValidator.validateRuleDefinition(new AnnotatedRuleWithoutActionMethod());
}

@Test(expected = IllegalArgumentException.class)
public void actionMethodMustHaveNoArguments() {
    ruleDefinitionValidator.validateRuleDefinition(new AnnotatedRuleWithActionMethodHavingOneArgumentNotOfTypeFacts());
}

@Test(expected = IllegalArgumentException.class)
public void priorityMethodMustHaveNoArguments() {
    ruleDefinitionValidator.validateRuleDefinition(new AnnotatedRuleWithPriorityMethodHavingArguments());
}

@Test(expected = IllegalArgumentException.class)
public void actionMethodMustBePublic() {
    ruleDefinitionValidator.validateRuleDefinition(new AnnotatedRuleWithNotPublicActionMethod());
}

@Test
public void withCustomAnnotationThatIsItselfAnnotatedWithTheRuleAnnotation() throws Throwable {
    ruleDefinitionValidator.validateRuleDefinition(new AnnotatedRuleWithMetaRuleAnnotation());
}

@Test(expected = IllegalArgumentException.class)
public void priorityMethodMustBePublic() {
    ruleDefinitionValidator.validateRuleDefinition(new AnnotatedRuleWithNotPublicPriorityMethod());
}

@Test(expected = IllegalArgumentException.class)
public void conditionMethodMustReturnBooleanType() {
    ruleDefinitionValidator.validateRuleDefinition(new AnnotatedRuleWithConditionMethodHavingNonBooleanReturnType());
}

@Test(expected = IllegalArgumentException.class)
public void whenConditionMethodHasOneNonAnnotatedParameter_thenThisParameterMustBeOfTypeFacts() {
    ruleDefinitionValidator.validateRuleDefinition(new AnnotatedRuleWithConditionMethodHavingOneArgumentNotOfTypeFacts());
}

@Test(expected = IllegalArgumentException.class)
public void notAnnotatedRuleMustNotBeAccepted() {
    ruleDefinitionValidator.validateRuleDefinition(new Object());
}

@Test(expected = IllegalArgumentException.class)
public void conditionMethodMustBeDefined() {
    ruleDefinitionValidator.validateRuleDefinition(new AnnotatedRuleWithoutCondition());
}

@Test(expected = IllegalArgumentException.class)
public void priorityMethodMustBeUnique() {
    ruleDefinitionValidator.validateRuleDefinition(new AnnotatedRuleWithMoreThanOnePriorityMethod());
}


@Test
public void whenTheRuleExecutesSuccessfully_thenOnSuccessShouldBeExecuted() throws Exception {
    when(rule1.evaluate(facts)).thenReturn(true);
    rules.clear();
    rules.register(rule1);
    rulesEngine.fire(rules, facts);
    InOrder inOrder = inOrder(rule1, fact1, fact2, ruleListener1, ruleListener2);
    inOrder.verify(ruleListener1).beforeExecute(rule1, facts);
    inOrder.verify(ruleListener2).beforeExecute(rule1, facts);
    inOrder.verify(ruleListener1).onSuccess(rule1, facts);
    inOrder.verify(ruleListener2).onSuccess(rule1, facts);
}

@Test
public void whenTheRuleEvaluatesToTrue_thenTheListenerShouldBeInvoked() throws Exception {
    when(rule1.evaluate(facts)).thenReturn(true);
    rulesEngine = RulesEngineBuilder.aNewRulesEngine().withRuleListener(ruleListener1).build();
    rules.clear();
    rules.register(rule1);
    rulesEngine.fire(rules, facts);
    verify(ruleListener1).afterEvaluate(rule1, true);
}

@Test
public void whenListenerReturnsTrue_thenTheRuleShouldBeEvaluated() throws Exception {
    when(ruleListener1.beforeEvaluate(rule1, facts)).thenReturn(true);
    rulesEngine = RulesEngineBuilder.aNewRulesEngine().withRuleListener(ruleListener1).build();
    rules.register(rule1);
    rulesEngine.fire(rules, facts);
    verify(rule1).evaluate(facts);
}

@Test
public void whenTheRuleFails_thenOnFailureShouldBeExecuted() throws Exception {
    final Exception exception = new Exception("fatal error!");
    doThrow(exception).when(rule1).execute(facts);
    when(rule1.evaluate(facts)).thenReturn(true);
    rules.clear();
    rules.register(rule1);
    rulesEngine.fire(rules, facts);
    InOrder inOrder = inOrder(rule1, fact1, fact2, ruleListener1, ruleListener2);
    inOrder.verify(ruleListener1).beforeExecute(rule1, facts);
    inOrder.verify(ruleListener2).beforeExecute(rule1, facts);
    inOrder.verify(ruleListener1).onFailure(rule1, exception, facts);
    inOrder.verify(ruleListener2).onFailure(rule1, exception, facts);
}

@Test
public void whenTheRuleEvaluatesToFalse_thenTheListenerShouldBeInvoked() throws Exception {
    when(rule1.evaluate(facts)).thenReturn(false);
    rulesEngine = RulesEngineBuilder.aNewRulesEngine().withRuleListener(ruleListener1).build();
    rules.clear();
    rules.register(rule1);
    rulesEngine.fire(rules, facts);
    verify(ruleListener1).afterEvaluate(rule1, false);
}

@Test
public void whenListenerReturnsFalse_thenTheRuleShouldBeSkippedBeforeBeingEvaluated() throws Exception {
    when(ruleListener1.beforeEvaluate(rule1, facts)).thenReturn(false);
    rulesEngine = RulesEngineBuilder.aNewRulesEngine().withRuleListener(ruleListener1).build();
    rules.register(rule1);
    rulesEngine.fire(rules, facts);
    verify(rule1, never()).evaluate(facts);
}


@Test
public void rulesThatExceedPriorityThresholdMustNotBeExecuted() throws Exception {
    rules.register(rule1);
    rules.register(rule2);
    rulesEngine.fire(rules, facts);
    verify(rule1).execute(facts);
    verify(rule2, never()).execute(facts);
}


@Test
public void proxyingHappensEvenWhenRuleIsAnnotatedWithMetaRuleAnnotation() {
    AnnotatedRuleWithMetaRuleAnnotation rule1 = new AnnotatedRuleWithMetaRuleAnnotation();
    Rule rule = RuleProxy.asRule(rule1);
    assertNotNull(rule.getDescription());
    assertNotNull(rule.getName());
}

@Override
public Object invoke(final Object proxy, final Method method, final Object[] args) throws Throwable {
    String methodName = method.getName();
    if (methodName.equals("getName")) {
        return getRuleName();
    }
    if (methodName.equals("getDescription")) {
        return getRuleDescription();
    }
    if (methodName.equals("getPriority")) {
        return getRulePriority();
    }
    if (methodName.equals("evaluate")) {
        Facts facts = (Facts) args[0];
        Method conditionMethod = getConditionMethod();
        List<Object> actualParameters = getActualParameters(conditionMethod, facts);
        return conditionMethod.invoke(target, actualParameters.toArray());
    }
    if (methodName.equals("execute")) {
        for (ActionMethodOrderBean actionMethodBean : getActionMethodBeans()) {
            Facts facts = (Facts) args[0];
            Method actiomMethod = actionMethodBean.getMethod();
            List<Object> actualParameters = getActualParameters(actiomMethod, facts);
            actiomMethod.invoke(target, actualParameters.toArray());
        }
    }
    if (methodName.equals("equals")) {
        return target.equals(args[0]);
    }
    if (methodName.equals("hashCode")) {
        return target.hashCode();
    }
    if (methodName.equals("toString")) {
        return target.toString();
    }
    if (methodName.equals("compareTo")) {
        Method compareToMethod = getCompareToMethod();
        if (compareToMethod != null) {
            return compareToMethod.invoke(target, args);
        } else {
            Rule otherRule = (Rule) args[0];
            return compareTo(otherRule);
        }
    }
    return null;
}


@Test
public void testCreationWithCustomParameters() {
    String name = "myRulesEngine";
    int expectedThreshold = 10;
    RulesEngine rulesEngine = RulesEngineBuilder.aNewRulesEngine().named(name).withRuleListener(ruleListener).withRulePriorityThreshold(expectedThreshold).withSilentMode(true).withSkipOnFirstNonTriggeredRule(true).withSkipOnFirstAppliedRule(true).withSkipOnFirstFailedRule(true).build();
    assertThat(rulesEngine).isNotNull();
    RulesEngineParameters parameters = rulesEngine.getParameters();
    assertThat(parameters.getName()).isEqualTo(name);
    assertThat(parameters.getPriorityThreshold()).isEqualTo(expectedThreshold);
    assertThat(parameters.isSilentMode()).isTrue();
    assertThat(parameters.isSkipOnFirstAppliedRule()).isTrue();
    assertThat(parameters.isSkipOnFirstFailedRule()).isTrue();
    assertThat(parameters.isSkipOnFirstNonTriggeredRule()).isTrue();
}

public RulesEngineBuilder named(final String name) {
    parameters.setName(name);
    return this;
}

@Test
public void testCreationWithDefaultParameters() {
    RulesEngine rulesEngine = RulesEngineBuilder.aNewRulesEngine().build();
    assertThat(rulesEngine).isNotNull();
    RulesEngineParameters parameters = rulesEngine.getParameters();
    assertThat(parameters.getName()).isEqualTo(RulesEngine.DEFAULT_NAME);
    assertThat(parameters.getPriorityThreshold()).isEqualTo(RulesEngine.DEFAULT_RULE_PRIORITY_THRESHOLD);
    assertThat(parameters.isSkipOnFirstAppliedRule()).isFalse();
    assertThat(parameters.isSkipOnFirstFailedRule()).isFalse();
    assertThat(parameters.isSkipOnFirstNonTriggeredRule()).isFalse();
}

public RulesEngine build() {
    return new DefaultRulesEngine(parameters, ruleListeners);
}


@Test
public void testSkipOnFirstAppliedRuleWithException() throws Exception {
    rulesEngine.fire(rules, facts);
    verify(rule1).execute(facts);
    verify(rule2, never()).execute(facts);
}

@Test
public void testSkipOnFirstAppliedRule() throws Exception {
    rulesEngine.fire(rules, facts);
    verify(rule1).execute(facts);
    verify(rule2, never()).execute(facts);
}


@Test
public void testSkipOnFirstFailedRule() throws Exception {
    rulesEngine.fire(rules, facts);
    verify(rule1).execute(facts);
    verify(rule2, never()).execute(facts);
}


@Test
public void testSkipOnFirstNonTriggeredRule() throws Exception {
    rulesEngine.fire(rules, facts);
    verify(rule1, never()).execute(facts);
    verify(rule2, never()).execute(facts);
}


@Test
public void isAnnotationPresentWithClassWhereAnnotationIsPresent() {
    assertTrue(Utils.isAnnotationPresent(Foo.class, AnnotationIsPresent.class));
}

static boolean isAnnotationPresent(final Class<? extends Annotation> targetAnnotation, final Class<?> annotatedType) {
    return findAnnotation(targetAnnotation, annotatedType) != null;
}

@Test
public void findAnnotationWithClassWhereAnnotationIsNotPresent() {
    Annotation foo = Utils.findAnnotation(Foo.class, Object.class);
    assertNull(foo);
}

static <A extends Annotation> A findAnnotation(final Class<A> targetAnnotation, final Class<?> annotatedType) {
    A foundAnnotation = annotatedType.getAnnotation(targetAnnotation);
    if (foundAnnotation == null) {
        for (Annotation annotation : annotatedType.getAnnotations()) {
            Class<? extends Annotation> annotationType = annotation.annotationType();
            if (annotationType.isAnnotationPresent(targetAnnotation)) {
                foundAnnotation = annotationType.getAnnotation(targetAnnotation);
                break;
            }
        }
    }
    return foundAnnotation;
}

@Test
public void isAnnotationPresentWithClassWhereAnnotationIsPresentViaMetaAnnotation() {
    assertTrue(Utils.isAnnotationPresent(Foo.class, AnnotationIsPresentViaMetaAnnotation.class));
}


@Test
public void testCreationWithCustomParameters() {
    String name = "myRulesEngine";
    int expectedThreshold = 10;
    RulesEngine rulesEngine = RulesEngineBuilder.aNewRulesEngine().withRuleListener(ruleListener).withRulePriorityThreshold(expectedThreshold).withSkipOnFirstNonTriggeredRule(true).withSkipOnFirstAppliedRule(true).withSkipOnFirstFailedRule(true).build();
    assertThat(rulesEngine).isNotNull();
    RulesEngineParameters parameters = rulesEngine.getParameters();
    assertThat(parameters.getPriorityThreshold()).isEqualTo(expectedThreshold);
    assertThat(parameters.isSkipOnFirstAppliedRule()).isTrue();
    assertThat(parameters.isSkipOnFirstFailedRule()).isTrue();
    assertThat(parameters.isSkipOnFirstNonTriggeredRule()).isTrue();
    assertThat(rulesEngine.getRuleListeners()).hasSize(2).contains(ruleListener);
}

public RulesEngineBuilder withRuleListener(final RuleListener ruleListener) {
    this.ruleListeners.add(ruleListener);
    return this;
}

@Test
public void testCreationWithDefaultParameters() {
    RulesEngine rulesEngine = RulesEngineBuilder.aNewRulesEngine().build();
    assertThat(rulesEngine).isNotNull();
    RulesEngineParameters parameters = rulesEngine.getParameters();
    assertThat(parameters.getPriorityThreshold()).isEqualTo(RulesEngineParameters.DEFAULT_RULE_PRIORITY_THRESHOLD);
    assertThat(parameters.isSkipOnFirstAppliedRule()).isFalse();
    assertThat(parameters.isSkipOnFirstFailedRule()).isFalse();
    assertThat(parameters.isSkipOnFirstNonTriggeredRule()).isFalse();
}

public static RulesEngineBuilder aNewRulesEngine() {
    return new RulesEngineBuilder();
}


@Test
public void testMVELFunctionExecution() throws Exception {
    Action printAction = new MVELAction("def hello() { System.out.println(\"Hello from MVEL!\"); }; hello();");
    Facts facts = new Facts();
    printAction.execute(facts);
    assertThat(systemOutRule.getLog()).contains("Hello from MVEL!");
}

@Override
public void execute(Facts facts) {
    try {
        MVEL.eval(expression, facts.asMap());
    } catch (Exception e) {
        LOGGER.debug("Unable to evaluate expression: '" + expression + "' on facts: " + facts, e);
    }
}


@Test
public void whenDeclaredFactIsNotPresent_thenShouldReturnFalse() throws Exception {
    Condition isHot = new MVELCondition("temperature > 30");
    Facts facts = new Facts();
    boolean evaluationResult = isHot.evaluate(facts);
    assertThat(evaluationResult).isFalse();
}

@Override
public boolean evaluate(Facts facts) {
    try {
        return MVEL.evalToBoolean(expression, facts.asMap());
    } catch (Exception e) {
        LOGGER.debug("Unable to evaluate expression: '" + expression + "' on facts: " + facts, e);
        return false;
    }
}

@Test
public void testMVELExpressionEvaluation() throws Exception {
    Condition isAdult = new MVELCondition("person.age > 18");
    Facts facts = new Facts();
    facts.put("person", new Person("foo", 20));
    boolean evaluationResult = isAdult.evaluate(facts);
    assertThat(evaluationResult).isTrue();
}


@Test
public void testClear() {
    Facts facts = new Facts();
    facts.put("foo", "bar");
    facts.clear();
    assertThat(facts.asMap()).isEmpty();
}

public void clear() {
    facts.clear();
}


@Test
public void unregisterByName() throws Exception {
    Rule r1 = new BasicRule("rule1");
    Rule r2 = new BasicRule("rule2");
    Set<Rule> ruleSet = new HashSet<>();
    ruleSet.add(r1);
    ruleSet.add(r2);
    rules = new Rules(ruleSet);
    rules.unregister("rule2");
    assertThat(rules).hasSize(1).containsExactly(r1);
}

public void unregister(final String ruleName) {
    Rule rule = findRuleByName(ruleName);
    if (rule != null) {
        unregister(rule);
    }
}


@Test
public void testRuleDefinitionReadingFromString() throws Exception {
    String adultRuleDescriptor = new String(Files.readAllBytes(Paths.get("src/test/resources/adult-rule.yml")));
    MVELRuleDefinition adultRuleDefinition = ruleDefinitionReader.read(adultRuleDescriptor);
    assertThat(adultRuleDefinition).isNotNull();
    assertThat(adultRuleDefinition.getName()).isEqualTo("adult rule");
    assertThat(adultRuleDefinition.getDescription()).isEqualTo("when age is greater then 18, then mark as adult");
    assertThat(adultRuleDefinition.getPriority()).isEqualTo(1);
    assertThat(adultRuleDefinition.getCondition()).isEqualTo("person.age > 18");
    assertThat(adultRuleDefinition.getActions()).isEqualTo(Collections.singletonList("person.setAdult(true);"));
}

MVELRuleDefinition read(String descriptor) throws NullPointerException {
    Object object = yaml.load(descriptor);
    Map<String, Object> map = (Map<String, Object>) object;
    return createRuleDefinitionFrom(map);
}