import path from "path";

import test from "ava";
import * as sinon from "sinon";

import * as actionsUtil from "./actions-util";
import {
  AnalysisKind,
  CodeScanning,
  compatibilityMatrix,
  RiskAssessment,
  getAnalysisConfig,
  getAnalysisKinds,
  parseAnalysisKinds,
  supportedAnalysisKinds,
} from "./analyses";
import { EnvVar } from "./environment";
import { getRunnerLogger } from "./logging";
import {
  createFeatures,
  RecordingLogger,
  setupBaseActionsVars,
  setupTests,
} from "./testing-utils";
import { AssessmentPayload } from "./upload-lib/types";
import { ConfigurationError } from "./util";

setupTests(test);

test("All known analysis kinds can be parsed successfully", async (t) => {
  for (const analysisKind of supportedAnalysisKinds) {
    t.deepEqual(await parseAnalysisKinds(analysisKind), [analysisKind]);
  }
});

test("Parsing analysis kinds returns unique results", async (t) => {
  const analysisKinds = await parseAnalysisKinds(
    "code-scanning,code-quality,code-scanning",
  );
  t.deepEqual(analysisKinds, [
    AnalysisKind.CodeScanning,
    AnalysisKind.CodeQuality,
  ]);
});

test("Parsing an unknown analysis kind fails with a configuration error", async (t) => {
  await t.throwsAsync(parseAnalysisKinds("code-scanning,foo"), {
    instanceOf: ConfigurationError,
  });
});

test("Parsing analysis kinds requires at least one analysis kind", async (t) => {
  await t.throwsAsync(parseAnalysisKinds(","), {
    instanceOf: ConfigurationError,
  });
});

test.serial(
  "getAnalysisKinds - returns expected analysis kinds for `analysis-kinds` input",
  async (t) => {
    process.env[EnvVar.TEST_MODE] = "true";
    const features = createFeatures([]);
    const requiredInputStub = sinon.stub(actionsUtil, "getRequiredInput");
    requiredInputStub
      .withArgs("analysis-kinds")
      .returns("code-scanning,code-quality");
    const result = await getAnalysisKinds(
      getRunnerLogger(true),
      features,
      true,
    );
    t.assert(result.includes(AnalysisKind.CodeScanning));
    t.assert(result.includes(AnalysisKind.CodeQuality));
  },
);

test.serial(
  "getAnalysisKinds - only use `code-scanning` for multiple analysis kinds outside of test mode",
  async (t) => {
    setupBaseActionsVars();
    process.env[EnvVar.TEST_MODE] = "false";
    const features = createFeatures([]);
    const logger = new RecordingLogger();
    const requiredInputStub = sinon.stub(actionsUtil, "getRequiredInput");
    requiredInputStub
      .withArgs("analysis-kinds")
      .returns("code-scanning,code-quality");
    const result = await getAnalysisKinds(logger, features, true);
    t.deepEqual(result, [AnalysisKind.CodeScanning]);
    t.assert(
      logger.hasMessage(
        "Continuing with only `analysis-kinds: code-scanning`.",
      ),
    );
  },
);

test.serial(
  "getAnalysisKinds - logs error for non-default `analysis-kinds` in custom workflow",
  async (t) => {
    setupBaseActionsVars({ GITHUB_EVENT_NAME: "push" });
    process.env[EnvVar.TEST_MODE] = "false";
    const features = createFeatures([]);
    const logger = new RecordingLogger();
    const requiredInputStub = sinon.stub(actionsUtil, "getRequiredInput");
    requiredInputStub.withArgs("analysis-kinds").returns("code-quality");
    const result = await getAnalysisKinds(logger, features, true);
    t.deepEqual(result, [AnalysisKind.CodeQuality]);
    t.assert(
      logger.hasMessage(
        "An analysis kind other than `code-scanning` was specified in a custom workflow.",
      ),
    );
  },
);

test.serial(
  "getAnalysisKinds - no error for non-default `analysis-kinds` in managed workflow",
  async (t) => {
    setupBaseActionsVars({ GITHUB_EVENT_NAME: "dynamic" });
    process.env[EnvVar.TEST_MODE] = "false";
    const features = createFeatures([]);
    const logger = new RecordingLogger();
    const requiredInputStub = sinon.stub(actionsUtil, "getRequiredInput");
    requiredInputStub.withArgs("analysis-kinds").returns("code-quality");
    const result = await getAnalysisKinds(logger, features, true);
    t.deepEqual(result, [AnalysisKind.CodeQuality]);
    t.deepEqual(logger.messages, []);
  },
);

test.serial(
  "getAnalysisKinds - includes `code-quality` when deprecated `quality-queries` input is used",
  async (t) => {
    process.env[EnvVar.TEST_MODE] = "true";
    const features = createFeatures([]);
    const requiredInputStub = sinon.stub(actionsUtil, "getRequiredInput");
    requiredInputStub.withArgs("analysis-kinds").returns("code-scanning");
    const optionalInputStub = sinon.stub(actionsUtil, "getOptionalInput");
    optionalInputStub.withArgs("quality-queries").returns("code-quality");
    const result = await getAnalysisKinds(
      getRunnerLogger(true),
      features,
      true,
    );
    t.assert(result.includes(AnalysisKind.CodeScanning));
    t.assert(result.includes(AnalysisKind.CodeQuality));
  },
);

test.serial(
  "getAnalysisKinds - throws if `analysis-kinds` input is invalid",
  async (t) => {
    const features = createFeatures([]);
    const requiredInputStub = sinon.stub(actionsUtil, "getRequiredInput");
    requiredInputStub.withArgs("analysis-kinds").returns("no-such-thing");
    await t.throwsAsync(
      getAnalysisKinds(getRunnerLogger(true), features, true),
    );
  },
);

// Test the compatibility matrix by looping through all analysis kinds.
const analysisKinds = Object.values(AnalysisKind);
for (let i = 0; i < analysisKinds.length; i++) {
  const analysisKind = analysisKinds[i];

  for (let j = i + 1; j < analysisKinds.length; j++) {
    const otherAnalysis = analysisKinds[j];

    if (analysisKind === otherAnalysis) continue;
    if (compatibilityMatrix[analysisKind].has(otherAnalysis)) {
      test.serial(
        `getAnalysisKinds - allows ${analysisKind} with ${otherAnalysis}`,
        async (t) => {
          setupBaseActionsVars();
          process.env[EnvVar.TEST_MODE] = "true";
          const features = createFeatures([]);
          const requiredInputStub = sinon.stub(actionsUtil, "getRequiredInput");
          requiredInputStub
            .withArgs("analysis-kinds")
            .returns([analysisKind, otherAnalysis].join(","));
          const result = await getAnalysisKinds(
            getRunnerLogger(true),
            features,
            true,
          );
          t.is(result.length, 2);
        },
      );
    } else {
      test.serial(
        `getAnalysisKinds - throws if ${analysisKind} is enabled with ${otherAnalysis}`,
        async (t) => {
          setupBaseActionsVars();
          const features = createFeatures([]);
          const requiredInputStub = sinon.stub(actionsUtil, "getRequiredInput");
          requiredInputStub
            .withArgs("analysis-kinds")
            .returns([analysisKind, otherAnalysis].join(","));
          await t.throwsAsync(
            getAnalysisKinds(getRunnerLogger(true), features, true),
            {
              instanceOf: ConfigurationError,
              message: `${analysisKind} and ${otherAnalysis} cannot be enabled at the same time`,
            },
          );
        },
      );
    }
  }
}

test("Code Scanning configuration does not accept other SARIF extensions", (t) => {
  for (const analysisKind of supportedAnalysisKinds) {
    if (analysisKind === AnalysisKind.CodeScanning) continue;

    const analysis = getAnalysisConfig(analysisKind);
    const sarifPath = path.join("path", "to", `file${analysis.sarifExtension}`);

    // The Code Scanning configuration's `sarifPredicate` should not accept a path which
    // ends in a different configuration's `sarifExtension`.
    t.false(CodeScanning.sarifPredicate(sarifPath));
  }
});

test.serial(
  "Risk Assessment configuration transforms SARIF upload payload",
  (t) => {
    process.env[EnvVar.RISK_ASSESSMENT_ID] = "1";
    const payload = RiskAssessment.transformPayload({
      commit_oid: "abc",
      sarif: "sarif",
      ref: "ref",
      workflow_run_attempt: 1,
      workflow_run_id: 1,
      checkout_uri: "uri",
      tool_names: [],
    }) as AssessmentPayload;

    const expected: AssessmentPayload = { sarif: "sarif", assessment_id: 1 };
    t.deepEqual(expected, payload);
  },
);

test.serial(
  "Risk Assessment configuration throws for negative assessment IDs",
  (t) => {
    process.env[EnvVar.RISK_ASSESSMENT_ID] = "-1";
    t.throws(
      () =>
        RiskAssessment.transformPayload({
          commit_oid: "abc",
          sarif: "sarif",
          ref: "ref",
          workflow_run_attempt: 1,
          workflow_run_id: 1,
          checkout_uri: "uri",
          tool_names: [],
        }),
      {
        instanceOf: Error,
        message: (msg) =>
          msg.startsWith(`${EnvVar.RISK_ASSESSMENT_ID} must not be negative: `),
      },
    );
  },
);

test.serial("Risk Assessment configuration throws for invalid IDs", (t) => {
  process.env[EnvVar.RISK_ASSESSMENT_ID] = "foo";
  t.throws(
    () =>
      RiskAssessment.transformPayload({
        commit_oid: "abc",
        sarif: "sarif",
        ref: "ref",
        workflow_run_attempt: 1,
        workflow_run_id: 1,
        checkout_uri: "uri",
        tool_names: [],
      }),
    {
      instanceOf: Error,
      message: (msg) =>
        msg.startsWith(`${EnvVar.RISK_ASSESSMENT_ID} must not be NaN: `),
    },
  );
});
