package org.kohsuke.github;

import org.awaitility.Awaitility;
import org.junit.Before;
import org.junit.Test;
import org.kohsuke.github.GHWorkflowJob.Step;
import org.kohsuke.github.GHWorkflowRun.Conclusion;
import org.kohsuke.github.GHWorkflowRun.Status;
import org.kohsuke.github.function.InputStreamFunction;

import java.io.IOException;
import java.time.Duration;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.Scanner;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;

import static org.hamcrest.Matchers.*;

public class GHWorkflowRunTest extends AbstractGitHubWireMockTest {

    private static final String REPO_NAME = "hub4j-test-org/GHWorkflowRunTest";
    private static final String MAIN_BRANCH = "main";
    private static final String SECOND_BRANCH = "second-branch";

    private static final String FAST_WORKFLOW_PATH = "fast-workflow.yml";
    private static final String FAST_WORKFLOW_NAME = "Fast workflow";

    private static final String SLOW_WORKFLOW_PATH = "slow-workflow.yml";
    private static final String SLOW_WORKFLOW_NAME = "Slow workflow";

    private static final String ARTIFACTS_WORKFLOW_PATH = "artifacts-workflow.yml";
    private static final String ARTIFACTS_WORKFLOW_NAME = "Artifacts workflow";

    private static final String MULTI_JOBS_WORKFLOW_PATH = "multi-jobs-workflow.yml";
    private static final String MULTI_JOBS_WORKFLOW_NAME = "Multi jobs workflow";
    private static final String RUN_A_ONE_LINE_SCRIPT_STEP_NAME = "Run a one-line script";

    private GHRepository repo;

    @Before
    public void setUp() throws Exception {
        repo = gitHub.getRepository(REPO_NAME);
    }

    @Test
    public void testManualRunAndBasicInformation() throws IOException {
        GHWorkflow workflow = repo.getWorkflow(FAST_WORKFLOW_PATH);

        long latestPreexistingWorkflowRunId = getLatestPreexistingWorkflowRunId();

        workflow.dispatch(MAIN_BRANCH);

        await((nonRecordingRepo) -> getWorkflowRun(nonRecordingRepo,
                FAST_WORKFLOW_NAME,
                MAIN_BRANCH,
                Status.COMPLETED,
                latestPreexistingWorkflowRunId).isPresent());

        GHWorkflowRun workflowRun = getWorkflowRun(FAST_WORKFLOW_NAME,
                MAIN_BRANCH,
                Status.COMPLETED,
                latestPreexistingWorkflowRunId).orElseThrow(
                        () -> new IllegalStateException("We must have a valid workflow run starting from here"));

        assertThat(workflowRun.getWorkflowId(), equalTo(workflow.getId()));
        assertThat(workflowRun.getId(), notNullValue());
        assertThat(workflowRun.getNodeId(), notNullValue());
        assertThat(workflowRun.getRepository().getFullName(), equalTo(REPO_NAME));
        assertThat(workflowRun.getUrl().getPath(), containsString("/actions/runs/"));
        assertThat(workflowRun.getHtmlUrl().getPath(), containsString("/actions/runs/"));
        assertThat(workflowRun.getJobsUrl().getPath(), endsWith("/jobs"));
        assertThat(workflowRun.getLogsUrl().getPath(), endsWith("/logs"));
        assertThat(workflowRun.getCheckSuiteUrl().getPath(), containsString("/check-suites/"));
        assertThat(workflowRun.getArtifactsUrl().getPath(), endsWith("/artifacts"));
        assertThat(workflowRun.getCancelUrl().getPath(), endsWith("/cancel"));
        assertThat(workflowRun.getRerunUrl().getPath(), endsWith("/rerun"));
        assertThat(workflowRun.getWorkflowUrl().getPath(), containsString("/actions/workflows/"));
        assertThat(workflowRun.getHeadBranch(), equalTo(MAIN_BRANCH));
        assertThat(workflowRun.getHeadCommit().getId(), notNullValue());
        assertThat(workflowRun.getHeadCommit().getTreeId(), notNullValue());
        assertThat(workflowRun.getHeadCommit().getMessage(), notNullValue());
        assertThat(workflowRun.getHeadCommit().getTimestamp(), notNullValue());
        assertThat(workflowRun.getHeadCommit().getAuthor().getEmail(), notNullValue());
        assertThat(workflowRun.getHeadCommit().getCommitter().getEmail(), notNullValue());
        assertThat(workflowRun.getEvent(), equalTo(GHEvent.WORKFLOW_DISPATCH));
        assertThat(workflowRun.getStatus(), equalTo(Status.COMPLETED));
        assertThat(workflowRun.getConclusion(), equalTo(Conclusion.SUCCESS));
        assertThat(workflowRun.getHeadSha(), notNullValue());
    }

    @Test
    public void testCancelAndRerun() throws IOException {
        GHWorkflow workflow = repo.getWorkflow(SLOW_WORKFLOW_PATH);

        long latestPreexistingWorkflowRunId = getLatestPreexistingWorkflowRunId();

        workflow.dispatch(MAIN_BRANCH);

        // now that we have triggered the workflow run, we will wait until it's in progress and then cancel it
        await((nonRecordingRepo) -> getWorkflowRun(nonRecordingRepo,
                SLOW_WORKFLOW_NAME,
                MAIN_BRANCH,
                Status.IN_PROGRESS,
                latestPreexistingWorkflowRunId).isPresent());

        GHWorkflowRun workflowRun = getWorkflowRun(SLOW_WORKFLOW_NAME,
                MAIN_BRANCH,
                Status.IN_PROGRESS,
                latestPreexistingWorkflowRunId).orElseThrow(
                        () -> new IllegalStateException("We must have a valid workflow run starting from here"));

        assertThat(workflowRun.getId(), notNullValue());

        workflowRun.cancel();
        long cancelledWorkflowRunId = workflowRun.getId();

        // let's wait until it's completed
        await((nonRecordingRepo) -> getWorkflowRunStatus(nonRecordingRepo, cancelledWorkflowRunId) == Status.COMPLETED);

        // let's check that it has been properly cancelled
        workflowRun = repo.getWorkflowRun(cancelledWorkflowRunId);
        assertThat(workflowRun.getConclusion(), equalTo(Conclusion.CANCELLED));

        // now let's rerun it
        workflowRun.rerun();

        // let's check that it has been rerun
        await((nonRecordingRepo) -> getWorkflowRunStatus(nonRecordingRepo,
                cancelledWorkflowRunId) == Status.IN_PROGRESS);

        // cancel it again
        workflowRun.cancel();
    }

    @Test
    public void testDelete() throws IOException {
        GHWorkflow workflow = repo.getWorkflow(FAST_WORKFLOW_PATH);

        long latestPreexistingWorkflowRunId = getLatestPreexistingWorkflowRunId();

        workflow.dispatch(MAIN_BRANCH);

        await((nonRecordingRepo) -> getWorkflowRun(nonRecordingRepo,
                FAST_WORKFLOW_NAME,
                MAIN_BRANCH,
                Status.COMPLETED,
                latestPreexistingWorkflowRunId).isPresent());

        GHWorkflowRun workflowRunToDelete = getWorkflowRun(FAST_WORKFLOW_NAME,
                MAIN_BRANCH,
                Status.COMPLETED,
                latestPreexistingWorkflowRunId).orElseThrow(
                        () -> new IllegalStateException("We must have a valid workflow run starting from here"));

        assertThat(workflowRunToDelete.getId(), notNullValue());

        workflowRunToDelete.delete();

        try {
            repo.getWorkflowRun(workflowRunToDelete.getId());
            fail("The workflow " + workflowRunToDelete.getId() + " should have been deleted.");
        } catch (GHFileNotFoundException e) {
            // success
        }
    }

    @Test
    public void testSearchOnBranch() throws IOException {
        GHWorkflow workflow = repo.getWorkflow(FAST_WORKFLOW_PATH);

        long latestPreexistingWorkflowRunId = getLatestPreexistingWorkflowRunId();

        workflow.dispatch(SECOND_BRANCH);

        await((nonRecordingRepo) -> getWorkflowRun(nonRecordingRepo,
                FAST_WORKFLOW_NAME,
                SECOND_BRANCH,
                Status.COMPLETED,
                latestPreexistingWorkflowRunId).isPresent());

        GHWorkflowRun workflowRun = getWorkflowRun(FAST_WORKFLOW_NAME,
                SECOND_BRANCH,
                Status.COMPLETED,
                latestPreexistingWorkflowRunId).orElseThrow(
                        () -> new IllegalStateException("We must have a valid workflow run starting from here"));

        assertThat(workflowRun.getWorkflowId(), equalTo(workflow.getId()));
        assertThat(workflowRun.getHeadBranch(), equalTo(SECOND_BRANCH));
        assertThat(workflowRun.getEvent(), equalTo(GHEvent.WORKFLOW_DISPATCH));
        assertThat(workflowRun.getStatus(), equalTo(Status.COMPLETED));
        assertThat(workflowRun.getConclusion(), equalTo(Conclusion.SUCCESS));
    }

    @Test
    public void testLogs() throws IOException {
        GHWorkflow workflow = repo.getWorkflow(FAST_WORKFLOW_PATH);

        long latestPreexistingWorkflowRunId = getLatestPreexistingWorkflowRunId();

        workflow.dispatch(MAIN_BRANCH);

        await((nonRecordingRepo) -> getWorkflowRun(nonRecordingRepo,
                FAST_WORKFLOW_NAME,
                MAIN_BRANCH,
                Status.COMPLETED,
                latestPreexistingWorkflowRunId).isPresent());

        GHWorkflowRun workflowRun = getWorkflowRun(FAST_WORKFLOW_NAME,
                MAIN_BRANCH,
                Status.COMPLETED,
                latestPreexistingWorkflowRunId).orElseThrow(
                        () -> new IllegalStateException("We must have a valid workflow run starting from here"));

        List<String> logsArchiveEntries = new ArrayList<>();
        String fullLogContent = workflowRun
                .downloadLogs(getLogArchiveInputStreamFunction("1_build.txt", logsArchiveEntries));

        assertThat(logsArchiveEntries, hasItems("1_build.txt", "build/9_Complete job.txt"));
        assertThat(fullLogContent, containsString("Hello, world!"));

        workflowRun.deleteLogs();

        try {
            workflowRun.downloadLogs((is) -> "");
            fail("Downloading logs should not be possible as they were deleted");
        } catch (GHFileNotFoundException e) {
            assertThat(e.getMessage(), containsString("Not Found"));
        }
    }

    @SuppressWarnings("resource")
    @Test
    public void testArtifacts() throws IOException {
        GHWorkflow workflow = repo.getWorkflow(ARTIFACTS_WORKFLOW_PATH);

        long latestPreexistingWorkflowRunId = getLatestPreexistingWorkflowRunId();

        workflow.dispatch(MAIN_BRANCH);

        await((nonRecordingRepo) -> getWorkflowRun(nonRecordingRepo,
                ARTIFACTS_WORKFLOW_NAME,
                MAIN_BRANCH,
                Status.COMPLETED,
                latestPreexistingWorkflowRunId).isPresent());

        GHWorkflowRun workflowRun = getWorkflowRun(ARTIFACTS_WORKFLOW_NAME,
                MAIN_BRANCH,
                Status.COMPLETED,
                latestPreexistingWorkflowRunId).orElseThrow(
                        () -> new IllegalStateException("We must have a valid workflow run starting from here"));

        List<GHArtifact> artifacts = new ArrayList<>(workflowRun.listArtifacts().toList());
        artifacts.sort((a1, a2) -> a1.getName().compareTo(a2.getName()));

        assertThat(artifacts.size(), is(2));

        // Test properties
        checkArtifactProperties(artifacts.get(0), "artifact1");
        checkArtifactProperties(artifacts.get(1), "artifact2");

        // Test download
        String artifactContent = artifacts.get(0).download((is) -> {
            try (ZipInputStream zis = new ZipInputStream(is)) {
                StringBuilder sb = new StringBuilder();

                ZipEntry ze = zis.getNextEntry();
                assertThat(ze.getName(), is("artifact1.txt"));

                // the scanner has to be kept open to avoid closing zis
                Scanner scanner = new Scanner(zis);
                while (scanner.hasNextLine()) {
                    sb.append(scanner.nextLine());
                }

                return sb.toString();
            }
        });

        assertThat(artifactContent, is("artifact1"));

        // Test GHRepository#getArtifact(long) as we are sure we have artifacts around
        GHArtifact artifactById = repo.getArtifact(artifacts.get(0).getId());
        checkArtifactProperties(artifactById, "artifact1");

        artifactById = repo.getArtifact(artifacts.get(1).getId());
        checkArtifactProperties(artifactById, "artifact2");

        // Test GHRepository#listArtifacts() as we are sure we have artifacts around
        List<GHArtifact> artifactsFromRepo = new ArrayList<>(
                repo.listArtifacts().withPageSize(2).iterator().nextPage());
        artifactsFromRepo.sort((a1, a2) -> a1.getName().compareTo(a2.getName()));

        // We have at least the two artifacts we just added
        assertThat(artifactsFromRepo.size(), is(2));

        // Test properties
        checkArtifactProperties(artifactsFromRepo.get(0), "artifact1");
        checkArtifactProperties(artifactsFromRepo.get(1), "artifact2");

        // Now let's test the delete() method
        GHArtifact artifact1 = artifacts.get(0);
        artifact1.delete();

        try {
            repo.getArtifact(artifact1.getId());
            fail("Getting the artifact should fail as it was deleted");
        } catch (GHFileNotFoundException e) {
            assertThat(e.getMessage(), containsString("Not Found"));
        }
    }

    @Test
    public void testJobs() throws IOException {
        GHWorkflow workflow = repo.getWorkflow(MULTI_JOBS_WORKFLOW_PATH);

        long latestPreexistingWorkflowRunId = getLatestPreexistingWorkflowRunId();

        workflow.dispatch(MAIN_BRANCH);

        await((nonRecordingRepo) -> getWorkflowRun(nonRecordingRepo,
                MULTI_JOBS_WORKFLOW_NAME,
                MAIN_BRANCH,
                Status.COMPLETED,
                latestPreexistingWorkflowRunId).isPresent());

        GHWorkflowRun workflowRun = getWorkflowRun(MULTI_JOBS_WORKFLOW_NAME,
                MAIN_BRANCH,
                Status.COMPLETED,
                latestPreexistingWorkflowRunId).orElseThrow(
                        () -> new IllegalStateException("We must have a valid workflow run starting from here"));

        List<GHWorkflowJob> jobs = workflowRun.listJobs()
                .toList()
                .stream()
                .sorted((j1, j2) -> j1.getName().compareTo(j2.getName()))
                .collect(Collectors.toList());

        assertThat(jobs.size(), is(2));

        GHWorkflowJob job1 = jobs.get(0);
        checkJobProperties(workflowRun.getId(), job1, "job1");
        String fullLogContent = job1.downloadLogs(getLogTextInputStreamFunction());
        assertThat(fullLogContent, containsString("Hello from job1!"));

        GHWorkflowJob job2 = jobs.get(1);
        checkJobProperties(workflowRun.getId(), job2, "job2");
        fullLogContent = job2.downloadLogs(getLogTextInputStreamFunction());
        assertThat(fullLogContent, containsString("Hello from job2!"));

        // while we have a job around, test GHRepository#getWorkflowJob(id)
        GHWorkflowJob job1ById = repo.getWorkflowJob(job1.getId());
        checkJobProperties(workflowRun.getId(), job1ById, "job1");

        // Also test listAllJobs() works correctly
        List<GHWorkflowJob> allJobs = workflowRun.listAllJobs().withPageSize(10).iterator().nextPage();
        assertThat(allJobs.size(), greaterThanOrEqualTo(2));
    }

    private void await(Function<GHRepository, Boolean> condition) throws IOException {
        if (!mockGitHub.isUseProxy()) {
            return;
        }

        GHRepository nonRecordingRepo = getNonRecordingGitHub().getRepository(REPO_NAME);

        Awaitility.await().pollInterval(Duration.ofSeconds(5)).atMost(Duration.ofSeconds(60)).until(() -> {
            return condition.apply(nonRecordingRepo);
        });
    }

    private long getLatestPreexistingWorkflowRunId() {
        return repo.queryWorkflowRuns().list().withPageSize(1).iterator().next().getId();
    }

    private static Optional<GHWorkflowRun> getWorkflowRun(GHRepository repository,
            String workflowName,
            String branch,
            Status status,
            long latestPreexistingWorkflowRunId) {
        List<GHWorkflowRun> workflowRuns = repository.queryWorkflowRuns()
                .branch(branch)
                .status(status)
                .event(GHEvent.WORKFLOW_DISPATCH)
                .list()
                .withPageSize(20)
                .iterator()
                .nextPage();

        for (GHWorkflowRun workflowRun : workflowRuns) {
            if (workflowRun.getName().equals(workflowName) && workflowRun.getId() > latestPreexistingWorkflowRunId) {
                return Optional.of(workflowRun);
            }
        }
        return Optional.empty();
    }

    private Optional<GHWorkflowRun> getWorkflowRun(String workflowName,
            String branch,
            Status status,
            long latestPreexistingWorkflowRunId) {
        return getWorkflowRun(this.repo, workflowName, branch, status, latestPreexistingWorkflowRunId);
    }

    private static Status getWorkflowRunStatus(GHRepository repository, long workflowRunId) {
        try {
            return repository.getWorkflowRun(workflowRunId).getStatus();
        } catch (IOException e) {
            throw new IllegalStateException("Unable to get workflow run status", e);
        }
    }

    @SuppressWarnings("resource")
    private static InputStreamFunction<String> getLogArchiveInputStreamFunction(String mainLogFileName,
            List<String> logsArchiveEntries) {
        return (is) -> {
            try (ZipInputStream zis = new ZipInputStream(is)) {
                StringBuilder sb = new StringBuilder();

                ZipEntry ze;
                while ((ze = zis.getNextEntry()) != null) {
                    logsArchiveEntries.add(ze.getName());
                    if (mainLogFileName.equals(ze.getName())) {
                        // the scanner has to be kept open to avoid closing zis
                        Scanner scanner = new Scanner(zis);
                        while (scanner.hasNextLine()) {
                            sb.append(scanner.nextLine()).append("\n");
                        }
                    }
                }

                return sb.toString();
            }
        };
    }

    @SuppressWarnings("resource")
    private static InputStreamFunction<String> getLogTextInputStreamFunction() {
        return (is) -> {
            StringBuilder sb = new StringBuilder();
            Scanner scanner = new Scanner(is);
            while (scanner.hasNextLine()) {
                sb.append(scanner.nextLine()).append("\n");
            }
            return sb.toString();
        };
    }

    private static void checkArtifactProperties(GHArtifact artifact, String artifactName) throws IOException {
        assertThat(artifact.getId(), notNullValue());
        assertThat(artifact.getNodeId(), notNullValue());
        assertThat(artifact.getRepository().getFullName(), equalTo(REPO_NAME));
        assertThat(artifact.getName(), is(artifactName));
        assertThat(artifact.getArchiveDownloadUrl().getPath(), containsString("actions/artifacts"));
        assertThat(artifact.getCreatedAt(), notNullValue());
        assertThat(artifact.getUpdatedAt(), notNullValue());
        assertThat(artifact.getExpiresAt(), notNullValue());
        assertThat(artifact.getSizeInBytes(), greaterThan(0L));
        assertThat(artifact.isExpired(), is(false));
    }

    private static void checkJobProperties(long workflowRunId, GHWorkflowJob job, String jobName) throws IOException {
        assertThat(job.getId(), notNullValue());
        assertThat(job.getNodeId(), notNullValue());
        assertThat(job.getRepository().getFullName(), equalTo(REPO_NAME));
        assertThat(job.getName(), is(jobName));
        assertThat(job.getStartedAt(), notNullValue());
        assertThat(job.getCompletedAt(), notNullValue());
        assertThat(job.getHeadSha(), notNullValue());
        assertThat(job.getStatus(), is(Status.COMPLETED));
        assertThat(job.getConclusion(), is(Conclusion.SUCCESS));
        assertThat(job.getRunId(), is(workflowRunId));
        assertThat(job.getUrl().getPath(), containsString("/actions/jobs/"));
        assertThat(job.getHtmlUrl().getPath(), containsString("/runs/" + job.getId()));
        assertThat(job.getCheckRunUrl().getPath(), containsString("/check-runs/"));

        // we only test the step we have control over, the others are added by GitHub
        Optional<Step> step = job.getSteps()
                .stream()
                .filter(s -> RUN_A_ONE_LINE_SCRIPT_STEP_NAME.equals(s.getName()))
                .findFirst();
        if (!step.isPresent()) {
            fail("Unable to find " + RUN_A_ONE_LINE_SCRIPT_STEP_NAME + " step");
        }

        checkStepProperties(step.get(), RUN_A_ONE_LINE_SCRIPT_STEP_NAME, 2);
    }

    private static void checkStepProperties(Step step, String name, int number) {
        assertThat(step.getName(), is(name));
        assertThat(step.getNumber(), is(number));
        assertThat(step.getStatus(), is(Status.COMPLETED));
        assertThat(step.getConclusion(), is(Conclusion.SUCCESS));
        assertThat(step.getStartedAt(), notNullValue());
        assertThat(step.getCompletedAt(), notNullValue());
    }
}
