import * as fs from "fs";
import * as path from "path";

import * as github from "@actions/github";
import { HTTPError } from "@actions/tool-cache";
import test from "ava";
import * as sinon from "sinon";

import * as analyses from "./analyses";
import { AnalysisKind, CodeQuality, CodeScanning } from "./analyses";
import * as api from "./api-client";
import * as diffUtils from "./diff-informed-analysis-utils";
import { getRunnerLogger, Logger } from "./logging";
import * as sarif from "./sarif";
import { setupTests } from "./testing-utils";
import * as uploadLib from "./upload-lib";
import { UploadPayload } from "./upload-lib/types";
import { GitHubVariant, initializeEnvironment, withTmpDir } from "./util";

setupTests(test);

test.beforeEach(() => {
  initializeEnvironment("1.2.3");
});

test.serial("validateSarifFileSchema - valid", (t) => {
  const inputFile = `${__dirname}/../src/testdata/valid-sarif.sarif`;
  t.notThrows(() =>
    uploadLib.validateSarifFileSchema(
      uploadLib.readSarifFileOrThrow(inputFile),
      inputFile,
      getRunnerLogger(true),
    ),
  );
});

test.serial("validateSarifFileSchema - invalid", (t) => {
  const inputFile = `${__dirname}/../src/testdata/invalid-sarif.sarif`;
  t.throws(() =>
    uploadLib.validateSarifFileSchema(
      uploadLib.readSarifFileOrThrow(inputFile),
      inputFile,
      getRunnerLogger(true),
    ),
  );
});

test.serial(
  "validate correct payload used for push, PR merge commit, and PR head",
  async (t) => {
    process.env["GITHUB_EVENT_NAME"] = "push";
    const pushPayload: any = uploadLib.buildPayload(
      "commit",
      "refs/heads/master",
      "key",
      undefined,
      "",
      1234,
      1,
      "/opt/src",
      undefined,
      ["CodeQL", "eslint"],
      "mergeBaseCommit",
    );
    // Not triggered by a pull request
    t.falsy(pushPayload.base_ref);
    t.falsy(pushPayload.base_sha);

    process.env["GITHUB_EVENT_NAME"] = "pull_request";
    process.env["GITHUB_SHA"] = "commit";
    process.env["GITHUB_BASE_REF"] = "master";
    process.env["GITHUB_EVENT_PATH"] =
      `${__dirname}/../src/testdata/pull_request.json`;
    const prMergePayload: any = uploadLib.buildPayload(
      "commit",
      "refs/pull/123/merge",
      "key",
      undefined,
      "",
      1234,
      1,
      "/opt/src",
      undefined,
      ["CodeQL", "eslint"],
      "mergeBaseCommit",
    );
    // Uploads for a merge commit use the merge base
    t.deepEqual(prMergePayload.base_ref, "refs/heads/master");
    t.deepEqual(prMergePayload.base_sha, "mergeBaseCommit");

    const prHeadPayload: any = uploadLib.buildPayload(
      "headCommit",
      "refs/pull/123/head",
      "key",
      undefined,
      "",
      1234,
      1,
      "/opt/src",
      undefined,
      ["CodeQL", "eslint"],
      "mergeBaseCommit",
    );
    // Uploads for the head use the PR base
    t.deepEqual(prHeadPayload.base_ref, "refs/heads/master");
    t.deepEqual(
      prHeadPayload.base_sha,
      "f95f852bd8fca8fcc58a9a2d6c842781e32a215e",
    );
  },
);

test.serial("finding SARIF files", async (t) => {
  await withTmpDir(async (tmpDir) => {
    // include a couple of sarif files
    fs.writeFileSync(path.join(tmpDir, "a.sarif"), "");
    fs.writeFileSync(path.join(tmpDir, "b.sarif"), "");

    // other random files shouldn't be returned
    fs.writeFileSync(path.join(tmpDir, "c.foo"), "");

    // we should recursively look in subdirectories
    fs.mkdirSync(path.join(tmpDir, "dir1"));
    fs.writeFileSync(path.join(tmpDir, "dir1", "d.sarif"), "");
    fs.mkdirSync(path.join(tmpDir, "dir1", "dir2"));
    fs.writeFileSync(path.join(tmpDir, "dir1", "dir2", "e.sarif"), "");

    // we should ignore symlinks
    fs.mkdirSync(path.join(tmpDir, "dir3"));
    fs.symlinkSync(tmpDir, path.join(tmpDir, "dir3", "symlink1"), "dir");
    fs.symlinkSync(
      path.join(tmpDir, "a.sarif"),
      path.join(tmpDir, "dir3", "symlink2.sarif"),
      "file",
    );

    // add some non-Code Scanning files that should be ignored, unless we look for them specifically
    for (const analysisKind of analyses.supportedAnalysisKinds) {
      if (analysisKind === AnalysisKind.CodeScanning) continue;

      const analysis = analyses.getAnalysisConfig(analysisKind);

      fs.writeFileSync(path.join(tmpDir, `a${analysis.sarifExtension}`), "");
      fs.writeFileSync(
        path.join(tmpDir, "dir1", `b${analysis.sarifExtension}`),
        "",
      );
    }

    const expectedSarifFiles: Partial<Record<AnalysisKind, string[]>> = {};
    expectedSarifFiles[AnalysisKind.CodeScanning] = [
      path.join(tmpDir, "a.sarif"),
      path.join(tmpDir, "b.sarif"),
      path.join(tmpDir, "dir1", "d.sarif"),
      path.join(tmpDir, "dir1", "dir2", "e.sarif"),
    ];
    const sarifFiles = uploadLib.findSarifFilesInDir(
      tmpDir,
      CodeScanning.sarifPredicate,
    );

    t.deepEqual(sarifFiles, expectedSarifFiles[AnalysisKind.CodeScanning]);

    for (const analysisKind of analyses.supportedAnalysisKinds) {
      if (analysisKind === AnalysisKind.CodeScanning) continue;

      const analysis = analyses.getAnalysisConfig(analysisKind);

      expectedSarifFiles[analysisKind] = [
        path.join(tmpDir, `a${analysis.sarifExtension}`),
        path.join(tmpDir, "dir1", `b${analysis.sarifExtension}`),
      ];
      const foundSarifFiles = uploadLib.findSarifFilesInDir(
        tmpDir,
        analysis.sarifPredicate,
      );

      t.deepEqual(foundSarifFiles, expectedSarifFiles[analysisKind]);
    }

    const groupedSarifFiles = await uploadLib.getGroupedSarifFilePaths(
      getRunnerLogger(true),
      tmpDir,
    );

    t.not(groupedSarifFiles, undefined);
    for (const analysisKind of analyses.supportedAnalysisKinds) {
      t.not(groupedSarifFiles[analysisKind], undefined);
      t.deepEqual(
        groupedSarifFiles[analysisKind],
        expectedSarifFiles[analysisKind],
      );
    }
  });
});

test.serial("getGroupedSarifFilePaths - Risk Assessment files", async (t) => {
  await withTmpDir(async (tmpDir) => {
    const sarifPath = path.join(tmpDir, "a.csra.sarif");
    fs.writeFileSync(sarifPath, "");

    const groupedSarifFiles = await uploadLib.getGroupedSarifFilePaths(
      getRunnerLogger(true),
      sarifPath,
    );

    t.not(groupedSarifFiles, undefined);
    t.is(groupedSarifFiles[AnalysisKind.CodeScanning], undefined);
    t.is(groupedSarifFiles[AnalysisKind.CodeQuality], undefined);
    t.not(groupedSarifFiles[AnalysisKind.RiskAssessment], undefined);
    t.deepEqual(groupedSarifFiles[AnalysisKind.RiskAssessment], [sarifPath]);
  });
});

test.serial("getGroupedSarifFilePaths - Code Quality file", async (t) => {
  await withTmpDir(async (tmpDir) => {
    const sarifPath = path.join(tmpDir, "a.quality.sarif");
    fs.writeFileSync(sarifPath, "");

    const groupedSarifFiles = await uploadLib.getGroupedSarifFilePaths(
      getRunnerLogger(true),
      sarifPath,
    );

    t.not(groupedSarifFiles, undefined);
    t.is(groupedSarifFiles[AnalysisKind.CodeScanning], undefined);
    t.not(groupedSarifFiles[AnalysisKind.CodeQuality], undefined);
    t.is(groupedSarifFiles[AnalysisKind.RiskAssessment], undefined);
    t.deepEqual(groupedSarifFiles[AnalysisKind.CodeQuality], [sarifPath]);
  });
});

test.serial("getGroupedSarifFilePaths - Code Scanning file", async (t) => {
  await withTmpDir(async (tmpDir) => {
    const sarifPath = path.join(tmpDir, "a.sarif");
    fs.writeFileSync(sarifPath, "");

    const groupedSarifFiles = await uploadLib.getGroupedSarifFilePaths(
      getRunnerLogger(true),
      sarifPath,
    );

    t.not(groupedSarifFiles, undefined);
    t.not(groupedSarifFiles[AnalysisKind.CodeScanning], undefined);
    t.is(groupedSarifFiles[AnalysisKind.CodeQuality], undefined);
    t.is(groupedSarifFiles[AnalysisKind.RiskAssessment], undefined);
    t.deepEqual(groupedSarifFiles[AnalysisKind.CodeScanning], [sarifPath]);
  });
});

test.serial("getGroupedSarifFilePaths - Other file", async (t) => {
  await withTmpDir(async (tmpDir) => {
    const sarifPath = path.join(tmpDir, "a.json");
    fs.writeFileSync(sarifPath, "");

    const groupedSarifFiles = await uploadLib.getGroupedSarifFilePaths(
      getRunnerLogger(true),
      sarifPath,
    );

    t.not(groupedSarifFiles, undefined);
    t.not(groupedSarifFiles[AnalysisKind.CodeScanning], undefined);
    t.is(groupedSarifFiles[AnalysisKind.CodeQuality], undefined);
    t.is(groupedSarifFiles[AnalysisKind.RiskAssessment], undefined);
    t.deepEqual(groupedSarifFiles[AnalysisKind.CodeScanning], [sarifPath]);
  });
});

test.serial("populateRunAutomationDetails", (t) => {
  const tool = { driver: { name: "test tool" } };
  let sarifLog: sarif.Log = {
    version: "2.1.0",
    runs: [{ tool }],
  };
  const analysisKey = ".github/workflows/codeql-analysis.yml:analyze";

  let expectedSarif: sarif.Log = {
    version: "2.1.0",
    runs: [
      { tool, automationDetails: { id: "language:javascript/os:linux/" } },
    ],
  };

  // Category has priority over analysis_key/environment
  let modifiedSarif = uploadLib.populateRunAutomationDetails(
    sarifLog,
    "language:javascript/os:linux",
    analysisKey,
    '{"language": "other", "os": "other"}',
  );
  t.deepEqual(modifiedSarif, expectedSarif);

  // It doesn't matter if the category has a slash at the end or not
  modifiedSarif = uploadLib.populateRunAutomationDetails(
    sarifLog,
    "language:javascript/os:linux/",
    analysisKey,
    "",
  );
  t.deepEqual(modifiedSarif, expectedSarif);

  // check that the automation details doesn't get overwritten
  sarifLog = {
    version: "2.1.0",
    runs: [{ tool, automationDetails: { id: "my_id" } }],
  };
  expectedSarif = {
    version: "2.1.0",
    runs: [{ tool, automationDetails: { id: "my_id" } }],
  };
  modifiedSarif = uploadLib.populateRunAutomationDetails(
    sarifLog,
    undefined,
    analysisKey,
    '{"os": "linux", "language": "javascript"}',
  );
  t.deepEqual(modifiedSarif, expectedSarif);

  // check multiple runs
  sarifLog = {
    version: "2.1.0",
    runs: [{ tool, automationDetails: { id: "my_id" } }, { tool }],
  };
  expectedSarif = {
    version: "2.1.0",
    runs: [
      { tool, automationDetails: { id: "my_id" } },
      {
        tool,
        automationDetails: {
          id: ".github/workflows/codeql-analysis.yml:analyze/language:javascript/os:linux/",
        },
      },
    ],
  };
  modifiedSarif = uploadLib.populateRunAutomationDetails(
    sarifLog,
    undefined,
    analysisKey,
    '{"os": "linux", "language": "javascript"}',
  );
  t.deepEqual(modifiedSarif, expectedSarif);
});

test.serial("validateUniqueCategory when empty", (t) => {
  t.notThrows(() =>
    uploadLib.validateUniqueCategory(
      createMockSarif(),
      CodeScanning.sentinelPrefix,
    ),
  );
  t.throws(() =>
    uploadLib.validateUniqueCategory(
      createMockSarif(),
      CodeScanning.sentinelPrefix,
    ),
  );
});

test.serial("validateUniqueCategory for automation details id", (t) => {
  t.notThrows(() =>
    uploadLib.validateUniqueCategory(
      createMockSarif("abc"),
      CodeScanning.sentinelPrefix,
    ),
  );
  t.throws(() =>
    uploadLib.validateUniqueCategory(
      createMockSarif("abc"),
      CodeScanning.sentinelPrefix,
    ),
  );
  t.throws(() =>
    uploadLib.validateUniqueCategory(
      createMockSarif("AbC"),
      CodeScanning.sentinelPrefix,
    ),
  );

  t.notThrows(() =>
    uploadLib.validateUniqueCategory(
      createMockSarif("def"),
      CodeScanning.sentinelPrefix,
    ),
  );
  t.throws(() =>
    uploadLib.validateUniqueCategory(
      createMockSarif("def"),
      CodeScanning.sentinelPrefix,
    ),
  );

  // Our category sanitization is not perfect. Here are some examples
  // of where we see false clashes
  t.notThrows(() =>
    uploadLib.validateUniqueCategory(
      createMockSarif("abc/def"),
      CodeScanning.sentinelPrefix,
    ),
  );
  t.throws(() =>
    uploadLib.validateUniqueCategory(
      createMockSarif("abc@def"),
      CodeScanning.sentinelPrefix,
    ),
  );
  t.throws(() =>
    uploadLib.validateUniqueCategory(
      createMockSarif("abc_def"),
      CodeScanning.sentinelPrefix,
    ),
  );
  t.throws(() =>
    uploadLib.validateUniqueCategory(
      createMockSarif("abc def"),
      CodeScanning.sentinelPrefix,
    ),
  );

  // this one is fine
  t.notThrows(() =>
    uploadLib.validateUniqueCategory(
      createMockSarif("abc_ def"),
      CodeScanning.sentinelPrefix,
    ),
  );
});

test.serial("validateUniqueCategory for tool name", (t) => {
  t.notThrows(() =>
    uploadLib.validateUniqueCategory(
      createMockSarif(undefined, "abc"),
      CodeScanning.sentinelPrefix,
    ),
  );
  t.throws(() =>
    uploadLib.validateUniqueCategory(
      createMockSarif(undefined, "abc"),
      CodeScanning.sentinelPrefix,
    ),
  );
  t.throws(() =>
    uploadLib.validateUniqueCategory(
      createMockSarif(undefined, "AbC"),
      CodeScanning.sentinelPrefix,
    ),
  );

  t.notThrows(() =>
    uploadLib.validateUniqueCategory(
      createMockSarif(undefined, "def"),
      CodeScanning.sentinelPrefix,
    ),
  );
  t.throws(() =>
    uploadLib.validateUniqueCategory(
      createMockSarif(undefined, "def"),
      CodeScanning.sentinelPrefix,
    ),
  );

  // Our category sanitization is not perfect. Here are some examples
  // of where we see false clashes
  t.notThrows(() =>
    uploadLib.validateUniqueCategory(
      createMockSarif(undefined, "abc/def"),
      CodeScanning.sentinelPrefix,
    ),
  );
  t.throws(() =>
    uploadLib.validateUniqueCategory(
      createMockSarif(undefined, "abc@def"),
      CodeScanning.sentinelPrefix,
    ),
  );
  t.throws(() =>
    uploadLib.validateUniqueCategory(
      createMockSarif(undefined, "abc_def"),
      CodeScanning.sentinelPrefix,
    ),
  );
  t.throws(() =>
    uploadLib.validateUniqueCategory(
      createMockSarif(undefined, "abc def"),
      CodeScanning.sentinelPrefix,
    ),
  );

  // this one is fine
  t.notThrows(() =>
    uploadLib.validateUniqueCategory(
      createMockSarif("abc_ def"),
      CodeScanning.sentinelPrefix,
    ),
  );
});

test.serial(
  "validateUniqueCategory for automation details id and tool name",
  (t) => {
    t.notThrows(() =>
      uploadLib.validateUniqueCategory(
        createMockSarif("abc", "abc"),
        CodeScanning.sentinelPrefix,
      ),
    );
    t.throws(() =>
      uploadLib.validateUniqueCategory(
        createMockSarif("abc", "abc"),
        CodeScanning.sentinelPrefix,
      ),
    );

    t.notThrows(() =>
      uploadLib.validateUniqueCategory(
        createMockSarif("abc_", "def"),
        CodeScanning.sentinelPrefix,
      ),
    );
    t.throws(() =>
      uploadLib.validateUniqueCategory(
        createMockSarif("abc_", "def"),
        CodeScanning.sentinelPrefix,
      ),
    );

    t.notThrows(() =>
      uploadLib.validateUniqueCategory(
        createMockSarif("ghi", "_jkl"),
        CodeScanning.sentinelPrefix,
      ),
    );
    t.throws(() =>
      uploadLib.validateUniqueCategory(
        createMockSarif("ghi", "_jkl"),
        CodeScanning.sentinelPrefix,
      ),
    );

    // Our category sanitization is not perfect. Here are some examples
    // of where we see false clashes because we replace some characters
    // with `_` in `sanitize`.
    t.notThrows(() =>
      uploadLib.validateUniqueCategory(
        createMockSarif("abc", "def__"),
        CodeScanning.sentinelPrefix,
      ),
    );
    t.throws(() =>
      uploadLib.validateUniqueCategory(
        createMockSarif("abc_def", "_"),
        CodeScanning.sentinelPrefix,
      ),
    );

    t.notThrows(() =>
      uploadLib.validateUniqueCategory(
        createMockSarif("mno_", "pqr"),
        CodeScanning.sentinelPrefix,
      ),
    );
    t.throws(() =>
      uploadLib.validateUniqueCategory(
        createMockSarif("mno", "_pqr"),
        CodeScanning.sentinelPrefix,
      ),
    );
  },
);

test.serial("validateUniqueCategory for multiple runs", (t) => {
  const sarif1 = createMockSarif("abc", "def");
  const sarif2 = createMockSarif("ghi", "jkl");

  // duplicate categories are allowed within the same sarif file
  const multiSarif: sarif.Log = {
    version: "2.1.0",
    runs: [sarif1.runs[0], sarif1.runs[0], sarif2.runs[0]],
  };
  t.notThrows(() =>
    uploadLib.validateUniqueCategory(multiSarif, CodeScanning.sentinelPrefix),
  );

  // should throw if there are duplicate categories in separate validations
  t.throws(() =>
    uploadLib.validateUniqueCategory(sarif1, CodeScanning.sentinelPrefix),
  );
  t.throws(() =>
    uploadLib.validateUniqueCategory(sarif2, CodeScanning.sentinelPrefix),
  );
});

test.serial("validateUniqueCategory with different prefixes", (t) => {
  t.notThrows(() =>
    uploadLib.validateUniqueCategory(
      createMockSarif(),
      CodeScanning.sentinelPrefix,
    ),
  );
  t.notThrows(() =>
    uploadLib.validateUniqueCategory(
      createMockSarif(),
      CodeQuality.sentinelPrefix,
    ),
  );
});

test.serial("accept results with invalid artifactLocation.uri value", (t) => {
  const loggedMessages: string[] = [];
  const mockLogger = {
    info: (message: string) => {
      loggedMessages.push(message);
    },
  } as Logger;

  const sarifFile = `${__dirname}/../src/testdata/with-invalid-uri.sarif`;
  uploadLib.validateSarifFileSchema(
    uploadLib.readSarifFileOrThrow(sarifFile),
    sarifFile,
    mockLogger,
  );

  t.deepEqual(loggedMessages.length, 3);
  t.deepEqual(
    loggedMessages[1],
    "Warning: 'not a valid URI' is not a valid URI in 'instance.runs[0].tool.driver.rules[0].helpUri'.",
    "Warning: 'not a valid URI' is not a valid URI in 'instance.runs[0].results[0].locations[0].physicalLocation.artifactLocation.uri'.",
  );
});

test.serial(
  "shouldShowCombineSarifFilesDeprecationWarning when on dotcom",
  async (t) => {
    t.true(
      await uploadLib.shouldShowCombineSarifFilesDeprecationWarning([
        createMockSarif("abc", "def"),
        createMockSarif("abc", "def"),
      ]),
    );
  },
);

test.serial(
  "shouldShowCombineSarifFilesDeprecationWarning with only 1 run",
  async (t) => {
    t.false(
      await uploadLib.shouldShowCombineSarifFilesDeprecationWarning([
        createMockSarif("abc", "def"),
      ]),
    );
  },
);

test.serial(
  "shouldShowCombineSarifFilesDeprecationWarning with distinct categories",
  async (t) => {
    t.false(
      await uploadLib.shouldShowCombineSarifFilesDeprecationWarning([
        createMockSarif("abc", "def"),
        createMockSarif("def", "def"),
      ]),
    );
  },
);

test.serial(
  "shouldShowCombineSarifFilesDeprecationWarning with distinct tools",
  async (t) => {
    t.false(
      await uploadLib.shouldShowCombineSarifFilesDeprecationWarning([
        createMockSarif("abc", "abc"),
        createMockSarif("abc", "def"),
      ]),
    );
  },
);

test.serial(
  "shouldShowCombineSarifFilesDeprecationWarning when environment variable is already set",
  async (t) => {
    process.env["CODEQL_MERGE_SARIF_DEPRECATION_WARNING"] = "true";

    t.false(
      await uploadLib.shouldShowCombineSarifFilesDeprecationWarning([
        createMockSarif("abc", "def"),
        createMockSarif("abc", "def"),
      ]),
    );
  },
);

test.serial("throwIfCombineSarifFilesDisabled when on dotcom", async (t) => {
  await t.throwsAsync(
    uploadLib.throwIfCombineSarifFilesDisabled(
      [createMockSarif("abc", "def"), createMockSarif("abc", "def")],
      {
        type: GitHubVariant.DOTCOM,
      },
    ),
    {
      message:
        /The CodeQL Action does not support uploading multiple SARIF runs with the same category/,
    },
  );
});

test.serial("throwIfCombineSarifFilesDisabled when on GHES 3.14", async (t) => {
  await t.notThrowsAsync(
    uploadLib.throwIfCombineSarifFilesDisabled(
      [createMockSarif("abc", "def"), createMockSarif("abc", "def")],
      {
        type: GitHubVariant.GHES,
        version: "3.14.0",
      },
    ),
  );
});

test.serial("throwIfCombineSarifFilesDisabled when on GHES 3.17", async (t) => {
  await t.notThrowsAsync(
    uploadLib.throwIfCombineSarifFilesDisabled(
      [createMockSarif("abc", "def"), createMockSarif("abc", "def")],
      {
        type: GitHubVariant.GHES,
        version: "3.17.0",
      },
    ),
  );
});

test.serial(
  "throwIfCombineSarifFilesDisabled when on GHES 3.18 pre",
  async (t) => {
    await t.throwsAsync(
      uploadLib.throwIfCombineSarifFilesDisabled(
        [createMockSarif("abc", "def"), createMockSarif("abc", "def")],
        {
          type: GitHubVariant.GHES,
          version: "3.18.0.pre1",
        },
      ),
      {
        message:
          /The CodeQL Action does not support uploading multiple SARIF runs with the same category/,
      },
    );
  },
);

test.serial(
  "throwIfCombineSarifFilesDisabled when on GHES 3.18 alpha",
  async (t) => {
    await t.throwsAsync(
      uploadLib.throwIfCombineSarifFilesDisabled(
        [createMockSarif("abc", "def"), createMockSarif("abc", "def")],
        {
          type: GitHubVariant.GHES,
          version: "3.18.0-alpha.1",
        },
      ),
      {
        message:
          /The CodeQL Action does not support uploading multiple SARIF runs with the same category/,
      },
    );
  },
);

test.serial("throwIfCombineSarifFilesDisabled when on GHES 3.18", async (t) => {
  await t.throwsAsync(
    uploadLib.throwIfCombineSarifFilesDisabled(
      [createMockSarif("abc", "def"), createMockSarif("abc", "def")],
      {
        type: GitHubVariant.GHES,
        version: "3.18.0",
      },
    ),
    {
      message:
        /The CodeQL Action does not support uploading multiple SARIF runs with the same category/,
    },
  );
});

test.serial(
  "throwIfCombineSarifFilesDisabled with an invalid GHES version",
  async (t) => {
    await t.notThrowsAsync(
      uploadLib.throwIfCombineSarifFilesDisabled(
        [createMockSarif("abc", "def"), createMockSarif("abc", "def")],
        {
          type: GitHubVariant.GHES,
          version: "foobar",
        },
      ),
    );
  },
);

test.serial("throwIfCombineSarifFilesDisabled with only 1 run", async (t) => {
  await t.notThrowsAsync(
    uploadLib.throwIfCombineSarifFilesDisabled(
      [createMockSarif("abc", "def")],
      {
        type: GitHubVariant.DOTCOM,
      },
    ),
  );
});

test.serial(
  "throwIfCombineSarifFilesDisabled with distinct categories",
  async (t) => {
    await t.notThrowsAsync(
      uploadLib.throwIfCombineSarifFilesDisabled(
        [createMockSarif("abc", "def"), createMockSarif("def", "def")],
        {
          type: GitHubVariant.DOTCOM,
        },
      ),
    );
  },
);

test.serial(
  "throwIfCombineSarifFilesDisabled with distinct tools",
  async (t) => {
    await t.notThrowsAsync(
      uploadLib.throwIfCombineSarifFilesDisabled(
        [createMockSarif("abc", "abc"), createMockSarif("abc", "def")],
        {
          type: GitHubVariant.DOTCOM,
        },
      ),
    );
  },
);

test.serial(
  "shouldConsiderConfigurationError correctly detects configuration errors",
  (t) => {
    const error1 = [
      "CodeQL analyses from advanced configurations cannot be processed when the default setup is enabled",
    ];
    t.true(uploadLib.shouldConsiderConfigurationError(error1));

    const error2 = [
      "rejecting delivery as the repository has too many logical alerts",
    ];
    t.true(uploadLib.shouldConsiderConfigurationError(error2));

    // We fail cases where we get > 1 error messages back
    const error3 = [
      "rejecting delivery as the repository has too many alerts",
      "extra error message",
    ];
    t.false(uploadLib.shouldConsiderConfigurationError(error3));
  },
);

test.serial(
  "shouldConsiderInvalidRequest returns correct recognises processing errors",
  (t) => {
    const error1 = [
      "rejecting SARIF",
      "an invalid URI was provided as a SARIF location",
    ];
    t.true(uploadLib.shouldConsiderInvalidRequest(error1));

    const error2 = [
      "locationFromSarifResult: expected artifact location",
      "an invalid URI was provided as a SARIF location",
    ];
    t.true(uploadLib.shouldConsiderInvalidRequest(error2));

    // We expect ALL errors to be of processing errors, for the outcome to be classified as
    // an invalid SARIF upload error.
    const error3 = [
      "could not convert rules: invalid security severity value, is not a number",
      "an unknown error occurred",
    ];
    t.false(uploadLib.shouldConsiderInvalidRequest(error3));
  },
);

function createMockSarif(id?: string, tool?: string): sarif.Log {
  return {
    version: "2.1.0",
    runs: [
      {
        automationDetails: {
          id,
        },
        tool: {
          driver: {
            name: tool || "test tool",
          },
        },
      },
    ],
  };
}

function uploadPayloadFixtures(analysis: analyses.AnalysisConfig) {
  const mockData = {
    payload: {
      commit_oid: "abc123",
      ref: "ref",
      sarif: "base64data",
      workflow_run_id: 1,
      workflow_run_attempt: 1,
      checkout_uri: "uri",
      tool_names: ["codeql"],
    } satisfies UploadPayload,
    owner: "test-owner",
    repo: "test-repo",
    response: {
      status: 200,
      data: { id: "uploaded-sarif-id" },
      headers: {},
      url: analysis.target,
    },
  };
  const client = github.getOctokit("123");
  sinon.stub(api, "getApiClient").value(() => client);
  const requestStub = sinon.stub(client, "request");

  const upload = async () =>
    uploadLib.uploadPayload(
      mockData.payload,
      {
        owner: mockData.owner,
        repo: mockData.repo,
      },
      getRunnerLogger(true),
      analysis,
    );

  return {
    upload,
    requestStub,
    mockData,
  };
}

for (const analysisKind of analyses.supportedAnalysisKinds) {
  const analysis = analyses.getAnalysisConfig(analysisKind);

  test.serial(
    `uploadPayload on ${analysis.name} uploads successfully`,
    async (t) => {
      const { upload, requestStub, mockData } = uploadPayloadFixtures(analysis);
      requestStub
        .withArgs(analysis.target, {
          owner: mockData.owner,
          repo: mockData.repo,
          data: mockData.payload,
        })
        .onFirstCall()
        .returns(Promise.resolve(mockData.response));
      const result = await upload();
      t.is(result, mockData.response.data.id);
      t.true(requestStub.calledOnce);
    },
  );

  for (const envVar of [
    "CODEQL_ACTION_SKIP_SARIF_UPLOAD",
    "CODEQL_ACTION_TEST_MODE",
  ]) {
    test.serial(
      `uploadPayload on ${analysis.name} skips upload when ${envVar} is set`,
      async (t) => {
        const { upload, requestStub, mockData } =
          uploadPayloadFixtures(analysis);
        await withTmpDir(async (tmpDir) => {
          process.env.RUNNER_TEMP = tmpDir;
          process.env[envVar] = "true";
          const result = await upload();
          t.is(result, "dummy-sarif-id");
          t.false(requestStub.called);

          const payloadFile = path.join(
            tmpDir,
            `payload-${analysis.kind}.json`,
          );
          t.true(fs.existsSync(payloadFile));

          const savedPayload = JSON.parse(fs.readFileSync(payloadFile, "utf8"));
          t.deepEqual(savedPayload, mockData.payload);
        });
      },
    );
  }

  test.serial(
    `uploadPayload on ${analysis.name} wraps request errors using wrapApiConfigurationError`,
    async (t) => {
      const { upload, requestStub } = uploadPayloadFixtures(analysis);
      const wrapApiConfigurationErrorStub = sinon.stub(
        api,
        "wrapApiConfigurationError",
      );
      const originalError = new HTTPError(404);
      const wrappedError = new Error("Wrapped error message");
      requestStub.rejects(originalError);
      wrapApiConfigurationErrorStub
        .withArgs(originalError)
        .returns(wrappedError);
      await t.throwsAsync(upload, {
        is: wrappedError,
      });
    },
  );
}

function runFilterAlertsByDiffRange(
  input: Partial<sarif.Log>,
  diffRanges: diffUtils.DiffThunkRange[],
): Partial<sarif.Log> {
  sinon.stub(diffUtils, "readDiffRangesJsonFile").returns(diffRanges);
  return uploadLib.filterAlertsByDiffRange(getRunnerLogger(true), input);
}

test.serial(
  "filterAlertsByDiffRange filters out alerts outside diff-range",
  (t) => {
    const input = sarif.readSarifFile(
      `${__dirname}/../src/testdata/valid-sarif.sarif`,
    );
    const actualOutput = runFilterAlertsByDiffRange(input, [
      {
        path: "main.js",
        startLine: 1,
        endLine: 3,
      },
    ]);

    const expectedOutput = sarif.readSarifFile(
      `${__dirname}/../src/testdata/valid-sarif-diff-filtered.sarif`,
    );

    t.deepEqual(actualOutput, expectedOutput);
  },
);
