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

import test, { ExecutionContext } from "ava";
import * as sinon from "sinon";

import { AnalysisKind, getAnalysisConfig } from "./analyses";
import { getRunnerLogger } from "./logging";
import { createFeatures, makeMacro, setupTests } from "./testing-utils";
import { UploadResult } from "./upload-lib";
import * as uploadLib from "./upload-lib";
import { postProcessAndUploadSarif } from "./upload-sarif";
import * as util from "./util";

setupTests(test);

interface UploadSarifExpectedResult {
  uploadResult?: UploadResult;
  expectedFiles?: string[];
}

function mockPostProcessSarifFiles() {
  const postProcessSarifFiles = sinon.stub(uploadLib, "postProcessSarifFiles");

  for (const analysisKind of Object.values(AnalysisKind)) {
    const analysisConfig = getAnalysisConfig(analysisKind);
    postProcessSarifFiles
      .withArgs(
        sinon.match.any,
        sinon.match.any,
        sinon.match.any,
        sinon.match.any,
        sinon.match.any,
        analysisConfig,
      )
      .resolves({
        sarif: { version: "2.1.0", runs: [] },
        analysisKey: "",
        environment: "",
      });
  }

  return postProcessSarifFiles;
}

const postProcessAndUploadSarifMacro = makeMacro({
  exec: async (
    t: ExecutionContext<unknown>,
    sarifFiles: string[],
    sarifPath: (tempDir: string) => string = (tempDir) => tempDir,
    expectedResult: Partial<Record<AnalysisKind, UploadSarifExpectedResult>>,
  ) => {
    await util.withTmpDir(async (tempDir) => {
      const logger = getRunnerLogger(true);
      const testPath = sarifPath(tempDir);
      const features = createFeatures([]);

      const toFullPath = (filename: string) => path.join(tempDir, filename);

      const postProcessSarifFiles = mockPostProcessSarifFiles();
      const uploadPostProcessedFiles = sinon.stub(
        uploadLib,
        "uploadPostProcessedFiles",
      );

      for (const analysisKind of Object.values(AnalysisKind)) {
        const analysisConfig = getAnalysisConfig(analysisKind);
        uploadPostProcessedFiles
          .withArgs(logger, sinon.match.any, analysisConfig, sinon.match.any)
          .resolves(expectedResult[analysisKind]?.uploadResult);
      }

      const fullSarifPaths = sarifFiles.map(toFullPath);
      for (const sarifFile of fullSarifPaths) {
        fs.writeFileSync(sarifFile, "");
      }

      const actual = await postProcessAndUploadSarif(
        logger,
        features,
        "always",
        "",
        testPath,
      );

      for (const analysisKind of Object.values(AnalysisKind)) {
        const analysisKindResult = expectedResult[analysisKind];
        if (analysisKindResult) {
          // We are expecting a result for this analysis kind, check that we have it.
          t.deepEqual(actual[analysisKind], analysisKindResult.uploadResult);
          // Additionally, check that the mocked `postProcessSarifFiles` was called with only the file paths
          // that we expected it to be called with.
          t.assert(
            postProcessSarifFiles.calledWith(
              logger,
              features,
              sinon.match.any,
              analysisKindResult.expectedFiles?.map(toFullPath) ??
                fullSarifPaths,
              sinon.match.any,
              getAnalysisConfig(analysisKind),
            ),
          );
        } else {
          // Otherwise, we are not expecting a result for this analysis kind. However, note that `undefined`
          // is also returned by our mocked `uploadProcessedFiles` when there is no expected result for this
          // analysis kind.
          t.is(actual[analysisKind], undefined);
          // Therefore, we also check that the mocked `uploadProcessedFiles` was not called for this analysis kind.
          t.assert(
            !uploadPostProcessedFiles.calledWith(
              logger,
              sinon.match.any,
              getAnalysisConfig(analysisKind),
              sinon.match.any,
            ),
            `uploadProcessedFiles was called for ${analysisKind}, but should not have been.`,
          );
        }
      }
    });
  },
  title: (providedTitle = "") => `processAndUploadSarif - ${providedTitle}`,
});

postProcessAndUploadSarifMacro.serial(
  "SARIF file",
  ["test.sarif"],
  (tempDir) => path.join(tempDir, "test.sarif"),
  {
    "code-scanning": {
      uploadResult: {
        statusReport: {},
        sarifID: "code-scanning-sarif",
      },
    },
  },
);

postProcessAndUploadSarifMacro.serial(
  "JSON file",
  ["test.json"],
  (tempDir) => path.join(tempDir, "test.json"),
  {
    "code-scanning": {
      uploadResult: {
        statusReport: {},
        sarifID: "code-scanning-sarif",
      },
    },
  },
);

postProcessAndUploadSarifMacro.serial(
  "Code Scanning files",
  ["test.json", "test.sarif"],
  undefined,
  {
    "code-scanning": {
      uploadResult: {
        statusReport: {},
        sarifID: "code-scanning-sarif",
      },
      expectedFiles: ["test.sarif"],
    },
  },
);

postProcessAndUploadSarifMacro.serial(
  "Code Quality file",
  ["test.quality.sarif"],
  (tempDir) => path.join(tempDir, "test.quality.sarif"),
  {
    "code-quality": {
      uploadResult: {
        statusReport: {},
        sarifID: "code-quality-sarif",
      },
    },
  },
);

postProcessAndUploadSarifMacro.serial(
  "Mixed files",
  ["test.sarif", "test.quality.sarif"],
  undefined,
  {
    "code-scanning": {
      uploadResult: {
        statusReport: {},
        sarifID: "code-scanning-sarif",
      },
      expectedFiles: ["test.sarif"],
    },
    "code-quality": {
      uploadResult: {
        statusReport: {},
        sarifID: "code-quality-sarif",
      },
      expectedFiles: ["test.quality.sarif"],
    },
  },
);

test.serial(
  "postProcessAndUploadSarif doesn't upload if upload is disabled",
  async (t) => {
    await util.withTmpDir(async (tempDir) => {
      const logger = getRunnerLogger(true);
      const features = createFeatures([]);

      const toFullPath = (filename: string) => path.join(tempDir, filename);

      const postProcessSarifFiles = mockPostProcessSarifFiles();
      const uploadPostProcessedFiles = sinon.stub(
        uploadLib,
        "uploadPostProcessedFiles",
      );

      fs.writeFileSync(toFullPath("test.sarif"), "");
      fs.writeFileSync(toFullPath("test.quality.sarif"), "");

      const actual = await postProcessAndUploadSarif(
        logger,
        features,
        "never",
        "",
        tempDir,
      );

      t.truthy(actual);
      t.assert(postProcessSarifFiles.calledTwice);
      t.assert(uploadPostProcessedFiles.notCalled);
    });
  },
);

test.serial(
  "postProcessAndUploadSarif writes post-processed SARIF files if output directory is provided",
  async (t) => {
    await util.withTmpDir(async (tempDir) => {
      const logger = getRunnerLogger(true);
      const features = createFeatures([]);

      const toFullPath = (filename: string) => path.join(tempDir, filename);

      const postProcessSarifFiles = mockPostProcessSarifFiles();

      fs.writeFileSync(toFullPath("test.sarif"), "");
      fs.writeFileSync(toFullPath("test.quality.sarif"), "");

      const postProcessedOutPath = path.join(tempDir, "post-processed");
      const actual = await postProcessAndUploadSarif(
        logger,
        features,
        "never",
        "",
        tempDir,
        "",
        postProcessedOutPath,
      );

      t.truthy(actual);
      t.assert(postProcessSarifFiles.calledTwice);
      t.assert(fs.existsSync(path.join(postProcessedOutPath, "upload.sarif")));
      t.assert(
        fs.existsSync(path.join(postProcessedOutPath, "upload.quality.sarif")),
      );
    });
  },
);
