/**
 *
 * @author Jeff Pearce (GitHub jeffpearce)
 */
package org.jenkinsci.plugins.githubautostatus.integration;


import hudson.model.Result;
import jenkins.model.CauseOfInterruption;
import org.jenkinsci.plugins.githubautostatus.BuildStatusAction;
import org.jenkinsci.plugins.githubautostatus.model.BuildStage;
import org.jenkinsci.plugins.workflow.cps.CpsFlowDefinition;
import org.jenkinsci.plugins.workflow.job.WorkflowJob;
import org.jenkinsci.plugins.workflow.job.WorkflowRun;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.jvnet.hudson.test.JenkinsRule;
import org.mockito.junit.MockitoJUnit;
import org.mockito.junit.MockitoRule;
import org.mockito.quality.Strictness;

import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.*;

public class DeclarativePipelineTest {
    @Rule
    public JenkinsRule r = new JenkinsRule();

    @Rule
    public MockitoRule rule = MockitoJUnit.rule().strictness(Strictness.STRICT_STUBS);


    @Before
    public void setUp() {
    }

    /**
     * Verifies a simple pipeline that succeeds sends the correct notifications
     * @throws Exception
     */
    @Test
    public void testSuccess() throws Exception {

        WorkflowJob p = r.jenkins.createProject(WorkflowJob.class, "p");
        p.setDefinition(new CpsFlowDefinition(
                "pipeline {\n" +
                        "    agent any\n" +
                        "    stages {\n" +
                        "        stage('The stage') {\n" +
                        "            steps {\n" +
                        "                echo 'hello'\n" +
                        "                echo 'goodbye'\n" +
                        "            }\n" +
                        "        }\n" +
                        "    }\n" +
                        "}",
                true));

        WorkflowRun b = p.scheduleBuild2(0).waitForStart();
        BuildStatusAction buildStatus = mock(BuildStatusAction.class);
        b.addOrReplaceAction(buildStatus);
        r.waitForCompletion(b);
        Thread.sleep(500);

        verify(buildStatus, times(1)).updateBuildStatusForStage(eq("The stage"), eq(BuildStage.State.CompletedSuccess), anyLong());
        verify(buildStatus, times(1)).updateBuildStatusForJob(eq(BuildStage.State.CompletedSuccess), any());

        verify(buildStatus, times(1)).updateBuildStatusForStage(any(), any(), anyLong());
        verify(buildStatus, times(1)).updateBuildStatusForJob(any(), any());
    }

    /**
     * Verifies notifications are sent for nested stages
     * @throws Exception
     */
    @Test
    public void testNested() throws Exception {

        WorkflowJob p = r.jenkins.createProject(WorkflowJob.class, "p");
        p.setDefinition(new CpsFlowDefinition(
                "pipeline {\n" +
                        "agent any\n" +
                        "stages {\n" +
                            "stage('Parallel') {\n" +
                                "parallel {\n" +
                                    "stage ('Stage A') {\n" +
                                        "steps {\n" +
                                            "sh 'echo hello'\n" +
                                        "}\n" +
                                    "}\n" +
                                    "stage ('Stage B') {\n" +
                                        "steps {\n" +
                                            "sh 'echo hello'\n" +
                                        "}\n" +
                                    "}\n" +
                                "}\n" +
                            "}\n" +
                        "}\n" +
                    "}",
                true));

        WorkflowRun b = p.scheduleBuild2(0).waitForStart();
        BuildStatusAction buildStatus = mock(BuildStatusAction.class);
        b.addOrReplaceAction(buildStatus);
        r.waitForCompletion(b);
        Thread.sleep(500);

        verify(buildStatus, times(1)).updateBuildStatusForStage(eq("Parallel"), eq(BuildStage.State.CompletedSuccess), anyLong());
        verify(buildStatus, times(1)).updateBuildStatusForStage(eq("Stage A"), eq(BuildStage.State.CompletedSuccess), anyLong());
        verify(buildStatus, times(1)).updateBuildStatusForStage(eq("Stage B"), eq(BuildStage.State.CompletedSuccess), anyLong());

        verify(buildStatus, times(3)).updateBuildStatusForStage(any(), any(), anyLong());
        verify(buildStatus, times(1)).updateBuildStatusForJob(any(), any());
    }

    /**
     * Verifies a simple pipeline that fails sends the correct notifications
     * @throws Exception
     */
    @Test
    public void testFail() throws Exception {

        WorkflowJob p = r.jenkins.createProject(WorkflowJob.class, "p");
        p.setDefinition(new CpsFlowDefinition(
                "pipeline {\n" +
                        "    agent any\n" +
                        "    stages {\n" +
                        "        stage('The stage') {\n" +
                        "            steps {\n" +
                        "                echo 'hello'\n" +
                        "                error('fail')\n" +
                        "            }\n" +
                        "        }\n" +
                        "    }\n" +
                        "}",
                true));

        WorkflowRun b = p.scheduleBuild2(0).waitForStart();
        BuildStatusAction buildStatus = mock(BuildStatusAction.class);
        b.addOrReplaceAction(buildStatus);
        r.waitForCompletion(b);
        Thread.sleep(500);

        verify(buildStatus, times(1)).updateBuildStatusForStage(eq("The stage"), eq(BuildStage.State.CompletedError), anyLong());
        verify(buildStatus, times(1)).updateBuildStatusForJob(eq(BuildStage.State.CompletedError), any());

        verify(buildStatus, atMost(1)).updateBuildStatusForStage(any(), any(), anyLong());
        verify(buildStatus, atMost(1)).updateBuildStatusForJob(any(), any());
    }

    /**
     * Verifies stages with caught errors are reported as success
     * @throws Exception
     */
    @Test
    public void testCaughtException() throws Exception {

        WorkflowJob p = r.jenkins.createProject(WorkflowJob.class, "p");
        p.setDefinition(new CpsFlowDefinition(
                "pipeline {\n" +
                        "    agent any\n" +
                        "    stages {\n" +
                        "        stage('The stage') {\n" +
                        "            steps {\n" +
                        "                script {\n" +
                        "                    try {\n" +
                        "                        sh 'exit 1'\n" +
                        "                    } catch (Exception ignored) {}\n" +
                        "                }\n" +
                        "            }\n" +
                        "        }\n" +
                        "    }\n" +
                        "}",
                true));

        WorkflowRun b = p.scheduleBuild2(0).waitForStart();
        BuildStatusAction buildStatus = mock(BuildStatusAction.class);
        b.addOrReplaceAction(buildStatus);
        r.waitForCompletion(b);
        Thread.sleep(500);

        verify(buildStatus, times(1)).updateBuildStatusForStage(eq("The stage"), eq(BuildStage.State.CompletedSuccess), anyLong());
        verify(buildStatus, times(1)).updateBuildStatusForJob(eq(BuildStage.State.CompletedSuccess), any());

        verify(buildStatus, times(1)).updateBuildStatusForStage(any(), any(), anyLong());
        verify(buildStatus, times(1)).updateBuildStatusForJob(any(), any());
    }
    /**
     * Verifies stage status can be reported correctly when set to FAILED in catchError
     * @throws Exception
     */
    @Test
    public void testCaughtExceptionSetStageFail() throws Exception {

        WorkflowJob p = r.jenkins.createProject(WorkflowJob.class, "p");
        p.setDefinition(new CpsFlowDefinition(
                "pipeline {\n" +
                        "    agent any\n" +
                        "    stages {\n" +
                        "        stage('Before stage') {\n" +
                        "            steps {\n" +
                        "                echo 'hello'\n" +
                        "            }\n" +
                        "        }\n" +
                        "        stage('Error stage') {\n" +
                        "            steps {\n" +
                        "                echo 'before step'\n" +
                        "                catchError(buildResult: 'SUCCESS', stageResult: 'FAILURE') {\n" +
                        "                    error 'fail on purpose'\n" +
                        "                }\n" +
                        "                echo 'after step'\n" +
                        "            }\n" +
                        "        }\n" +
                        "        stage('After stage') {\n" +
                        "            steps {\n" +
                        "                echo 'goodbye'\n" +
                        "            }\n" +
                        "        }\n" +
                        "    }\n" +
                        "}",
                true));

        WorkflowRun b = p.scheduleBuild2(0).waitForStart();
        BuildStatusAction buildStatus = mock(BuildStatusAction.class);
        b.addOrReplaceAction(buildStatus);
        r.waitForCompletion(b);
        Thread.sleep(500);

        verify(buildStatus, times(1)).updateBuildStatusForStage(eq("Before stage"), eq(BuildStage.State.CompletedSuccess), anyLong());
        verify(buildStatus, times(1)).updateBuildStatusForStage(eq("Error stage"), eq(BuildStage.State.CompletedError), anyLong());
        verify(buildStatus, times(1)).updateBuildStatusForStage(eq("After stage"), eq(BuildStage.State.CompletedSuccess), anyLong());
        verify(buildStatus, times(1)).updateBuildStatusForJob(eq(BuildStage.State.CompletedSuccess), any());

        verify(buildStatus, times(3)).updateBuildStatusForStage(any(), any(), anyLong());
        verify(buildStatus, times(1)).updateBuildStatusForJob(any(), any());
    }

    /**
     * Verifies stage status can be reported correctly when set to ABORTED in catchError
     * @throws Exception
     */
    @Test
    public void testCaughtExceptionSetStageAbort() throws Exception {

        WorkflowJob p = r.jenkins.createProject(WorkflowJob.class, "p");
        p.setDefinition(new CpsFlowDefinition(
                "pipeline {\n" +
                        "    agent any\n" +
                        "    stages {\n" +
                        "        stage('Error stage') {\n" +
                        "            steps {\n" +
                        "                catchError(buildResult: 'SUCCESS', stageResult: 'ABORTED') {\n" +
                        "                    error 'fail on purpose'\n" +
                        "                }\n" +
                        "            }\n" +
                        "        }\n" +
                        "    }\n" +
                        "}",
                true));

        WorkflowRun b = p.scheduleBuild2(0).waitForStart();
        BuildStatusAction buildStatus = mock(BuildStatusAction.class);
        b.addOrReplaceAction(buildStatus);
        r.waitForCompletion(b);
        Thread.sleep(500);

        verify(buildStatus, times(1)).updateBuildStatusForStage(eq("Error stage"), eq(BuildStage.State.Aborted), anyLong());
        verify(buildStatus, times(1)).updateBuildStatusForJob(eq(BuildStage.State.CompletedSuccess), any());

        verify(buildStatus, times(1)).updateBuildStatusForStage(any(), any(), anyLong());
        verify(buildStatus, times(1)).updateBuildStatusForJob(any(), any());
    }

    /**
     * Verifies an aborted stage is reported correctly
     * @throws Exception
     */
    @Test
    public void testAbort() throws Exception {

        WorkflowJob p = r.jenkins.createProject(WorkflowJob.class, "p");
        p.setDefinition(new CpsFlowDefinition(
                "pipeline {\n" +
                        "    agent any\n" +
                        "    stages {\n" +
                        "        stage('Error stage') {\n" +
                        "            steps {\n" +
                        "                sleep time: 5, unit: 'MINUTES'\n" +
                        "            }\n" +
                        "        }\n" +
                        "    }\n" +
                        "}",
                true));

        WorkflowRun b = p.scheduleBuild2(0).waitForStart();
        BuildStatusAction buildStatus = mock(BuildStatusAction.class);
        b.addOrReplaceAction(buildStatus);

        WorkflowRun runningBuild = null;
        while (runningBuild == null) {
            runningBuild = p.getBuildByNumber(1);
            Thread.sleep(10);
        }
        Thread.sleep(5000);
        runningBuild.getExecution().interrupt(Result.ABORTED, new CauseOfInterruption.UserInterruption("xxxx"));

        r.waitForCompletion(b);
        Thread.sleep(500);

        verify(buildStatus, times(1)).updateBuildStatusForStage(eq("Error stage"), eq(BuildStage.State.Aborted), anyLong());
        verify(buildStatus, times(1)).updateBuildStatusForJob(eq(BuildStage.State.Aborted), any());

        verify(buildStatus, times(1)).updateBuildStatusForStage(any(), any(), anyLong());
        verify(buildStatus, times(1)).updateBuildStatusForJob(any(), any());
    }
}