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

import * as actionsCache from "@actions/cache";
import test from "ava";
import * as sinon from "sinon";

import {
  getRecordingLogger,
  LoggedMessage,
  mockCodeQLVersion,
  setupTests,
} from "../testing-utils";
import { DiskUsage, withTmpDir } from "../util";

import { getCacheKey, shouldSkipOverlayAnalysis } from "./status";

setupTests(test);

function makeDiskUsage(totalGiB: number): DiskUsage {
  return {
    numTotalBytes: totalGiB * 1024 * 1024 * 1024,
    numAvailableBytes: 0,
  };
}

test("getCacheKey incorporates language, CodeQL version, and disk space", async (t) => {
  const codeql = mockCodeQLVersion("2.20.0");
  t.is(
    await getCacheKey(codeql, ["javascript"], makeDiskUsage(50)),
    "codeql-overlay-status-javascript-2.20.0-runner-50GB",
  );
  t.is(
    await getCacheKey(codeql, ["python"], makeDiskUsage(50)),
    "codeql-overlay-status-python-2.20.0-runner-50GB",
  );
  t.is(
    await getCacheKey(
      mockCodeQLVersion("2.21.0"),
      ["javascript"],
      makeDiskUsage(50),
    ),
    "codeql-overlay-status-javascript-2.21.0-runner-50GB",
  );
  t.is(
    await getCacheKey(codeql, ["javascript"], makeDiskUsage(100)),
    "codeql-overlay-status-javascript-2.20.0-runner-100GB",
  );
});

test("getCacheKey sorts and joins multiple languages", async (t) => {
  const codeql = mockCodeQLVersion("2.20.0");
  t.is(
    await getCacheKey(codeql, ["python", "javascript"], makeDiskUsage(50)),
    "codeql-overlay-status-javascript+python-2.20.0-runner-50GB",
  );
  t.is(
    await getCacheKey(codeql, ["javascript", "python"], makeDiskUsage(50)),
    "codeql-overlay-status-javascript+python-2.20.0-runner-50GB",
  );
});

test("getCacheKey rounds disk space down to nearest 10 GiB", async (t) => {
  const codeql = mockCodeQLVersion("2.20.0");
  t.is(
    await getCacheKey(codeql, ["javascript"], makeDiskUsage(14)),
    "codeql-overlay-status-javascript-2.20.0-runner-10GB",
  );
  t.is(
    await getCacheKey(codeql, ["javascript"], makeDiskUsage(19)),
    "codeql-overlay-status-javascript-2.20.0-runner-10GB",
  );
});

test.serial(
  "shouldSkipOverlayAnalysis returns false when no cached status exists",
  async (t) => {
    await withTmpDir(async (tmpDir) => {
      process.env["RUNNER_TEMP"] = tmpDir;
      const codeql = mockCodeQLVersion("2.20.0");
      const messages: LoggedMessage[] = [];
      const logger = getRecordingLogger(messages);

      sinon.stub(actionsCache, "restoreCache").resolves(undefined);

      const result = await shouldSkipOverlayAnalysis(
        codeql,
        ["javascript"],
        makeDiskUsage(50),
        logger,
      );

      t.false(result);
      t.true(
        messages.some(
          (m) =>
            m.type === "debug" &&
            typeof m.message === "string" &&
            m.message.includes("No overlay status found in Actions cache."),
        ),
      );
    });
  },
);

test.serial(
  "shouldSkipOverlayAnalysis returns true when cached status indicates failed build",
  async (t) => {
    await withTmpDir(async (tmpDir) => {
      process.env["RUNNER_TEMP"] = tmpDir;
      const codeql = mockCodeQLVersion("2.20.0");
      const messages: LoggedMessage[] = [];
      const logger = getRecordingLogger(messages);

      const status = {
        attemptedToBuildOverlayBaseDatabase: true,
        builtOverlayBaseDatabase: false,
      };

      // Stub restoreCache to write the status file and return a key
      sinon.stub(actionsCache, "restoreCache").callsFake(async (paths) => {
        const statusFile = paths[0];
        await fs.promises.mkdir(path.dirname(statusFile), { recursive: true });
        await fs.promises.writeFile(statusFile, JSON.stringify(status));
        return "found-key";
      });

      const result = await shouldSkipOverlayAnalysis(
        codeql,
        ["javascript"],
        makeDiskUsage(50),
        logger,
      );

      t.true(result);
    });
  },
);

test.serial(
  "shouldSkipOverlayAnalysis returns false when cached status indicates successful build",
  async (t) => {
    await withTmpDir(async (tmpDir) => {
      process.env["RUNNER_TEMP"] = tmpDir;
      const codeql = mockCodeQLVersion("2.20.0");
      const messages: LoggedMessage[] = [];
      const logger = getRecordingLogger(messages);

      const status = {
        attemptedToBuildOverlayBaseDatabase: true,
        builtOverlayBaseDatabase: true,
      };

      sinon.stub(actionsCache, "restoreCache").callsFake(async (paths) => {
        const statusFile = paths[0];
        await fs.promises.mkdir(path.dirname(statusFile), { recursive: true });
        await fs.promises.writeFile(statusFile, JSON.stringify(status));
        return "found-key";
      });

      const result = await shouldSkipOverlayAnalysis(
        codeql,
        ["javascript"],
        makeDiskUsage(50),
        logger,
      );

      t.false(result);
      t.true(
        messages.some(
          (m) =>
            m.type === "debug" &&
            typeof m.message === "string" &&
            m.message.includes(
              "Cached overlay status does not indicate a previous unsuccessful attempt",
            ),
        ),
      );
    });
  },
);
