import * as filepath from "path";

import * as core from "@actions/core";
import * as toolcache from "@actions/tool-cache";
import test, { ExecutionContext } from "ava";
import sinon from "sinon";

import * as apiClient from "./api-client";
import * as defaults from "./defaults.json";
import { setUpFeatureFlagTests } from "./feature-flags/testing-util";
import { UnvalidatedObject, validateSchema } from "./json";
import { makeFromSchema } from "./json/testing-util";
import { BuiltInLanguage } from "./languages";
import { getRunnerLogger, Logger } from "./logging";
import * as startProxyExports from "./start-proxy";
import * as statusReport from "./status-report";
import {
  assertNotLogged,
  checkExpectedLogMessages,
  createFeatures,
  makeMacro,
  makeTestToken,
  RecordingLogger,
  setupTests,
  withRecordingLoggerAsync,
} from "./testing-utils";
import {
  ConfigurationError,
  GitHubVariant,
  GitHubVersion,
  withTmpDir,
} from "./util";

setupTests(test);

const sendFailedStatusReportTest = makeMacro({
  exec: async (
    t: ExecutionContext<unknown>,
    err: Error,
    expectedMessage: string,
    expectedStatus: statusReport.ActionStatus = "failure",
  ) => {
    const now = new Date();

    // Override core.setFailed to avoid it setting the program's exit code
    sinon.stub(core, "setFailed").returns();

    const createStatusReportBase = sinon.stub(
      statusReport,
      "createStatusReportBase",
    );
    createStatusReportBase.resolves(undefined);

    await withRecordingLoggerAsync(async (logger) => {
      await startProxyExports.sendFailedStatusReport(
        logger,
        now,
        undefined,
        err,
      );

      // Check that the stub has been called exactly once, with the expected arguments,
      // but not with the message from the error.
      sinon.assert.calledOnceWithExactly(
        createStatusReportBase,
        statusReport.ActionName.StartProxy,
        expectedStatus,
        now,
        sinon.match.any,
        sinon.match.any,
        sinon.match.any,
        expectedMessage,
      );
      t.false(
        createStatusReportBase.calledWith(
          statusReport.ActionName.StartProxy,
          expectedStatus,
          now,
          sinon.match.any,
          sinon.match.any,
          sinon.match.any,
          sinon.match((msg: string) => msg.includes(err.message)),
        ),
        "createStatusReportBase was called with the error message",
      );
    });
  },

  title: (providedTitle = "") => `sendFailedStatusReport - ${providedTitle}`,
});

sendFailedStatusReportTest.serial(
  "reports generic error message for non-StartProxyError error",
  new Error("Something went wrong today"),
  "Error from start-proxy Action omitted (Error).",
);

sendFailedStatusReportTest.serial(
  "reports generic error message for non-StartProxyError error with safe error message",
  new Error(
    startProxyExports.getStartProxyErrorMessage(
      startProxyExports.StartProxyErrorType.DownloadFailed,
    ),
  ),
  "Error from start-proxy Action omitted (Error).",
);

sendFailedStatusReportTest.serial(
  "reports generic error message for ConfigurationError error",
  new ConfigurationError("Something went wrong today"),
  "Error from start-proxy Action omitted (ConfigurationError).",
  "user-error",
);

const toEncodedJSON = (data: any) =>
  Buffer.from(JSON.stringify(data)).toString("base64");

const mixedCredentials = [
  { type: "npm_registry", host: "npm.pkg.github.com", token: "abc" },
  { type: "maven_repository", host: "maven.pkg.github.com", token: "def" },
  { type: "nuget_feed", host: "nuget.pkg.github.com", token: "ghi" },
  { type: "goproxy_server", host: "goproxy.example.com", token: "jkl" },
  { type: "git_source", host: "github.com/github", token: "mno" },
];

test("getCredentials prefers registriesCredentials over registrySecrets", async (t) => {
  const registryCredentials = Buffer.from(
    JSON.stringify([
      { type: "npm_registry", host: "npm.pkg.github.com", token: "abc" },
    ]),
  ).toString("base64");
  const registrySecrets = JSON.stringify([
    { type: "npm_registry", host: "registry.npmjs.org", token: "def" },
  ]);

  const credentials = startProxyExports.getCredentials(
    getRunnerLogger(true),
    registrySecrets,
    registryCredentials,
    undefined,
  );
  t.is(credentials.length, 1);
  t.is(credentials[0].host, "npm.pkg.github.com");
});

test("getCredentials throws an error when configurations are not an array", async (t) => {
  const registryCredentials = Buffer.from(
    JSON.stringify({ type: "npm_registry", token: "abc" }),
  ).toString("base64");

  t.throws(
    () =>
      startProxyExports.getCredentials(
        getRunnerLogger(true),
        undefined,
        registryCredentials,
        undefined,
      ),
    {
      message:
        "Expected credentials data to be an array of configurations, but it is not.",
    },
  );
});

test("getCredentials throws error when credential is not an object", async (t) => {
  const testCredentials = [["foo"], [null]].map(toEncodedJSON);

  for (const testCredential of testCredentials) {
    t.throws(
      () =>
        startProxyExports.getCredentials(
          getRunnerLogger(true),
          undefined,
          testCredential,
          undefined,
        ),
      {
        message: "Invalid credentials - must be an object",
      },
    );
  }
});

test("getCredentials throws error when credential is missing type", async (t) => {
  const testCredentials = [[{ token: "abc", url: "https://localhost" }]].map(
    toEncodedJSON,
  );

  for (const testCredential of testCredentials) {
    t.throws(
      () =>
        startProxyExports.getCredentials(
          getRunnerLogger(true),
          undefined,
          testCredential,
          undefined,
        ),
      {
        message: "Invalid credentials - must have a type",
      },
    );
  }
});

test("getCredentials throws error when credential missing host and url", async (t) => {
  const testCredentials = [
    [{ type: "npm_registry", token: "abc" }],
    [{ type: "npm_registry", token: "abc", host: null }],
    [{ type: "npm_registry", token: "abc", url: null }],
  ].map(toEncodedJSON);

  for (const testCredential of testCredentials) {
    t.throws(
      () =>
        startProxyExports.getCredentials(
          getRunnerLogger(true),
          undefined,
          testCredential,
          undefined,
        ),
      {
        message: "Invalid credentials - must specify host or url",
      },
    );
  }
});

test("getCredentials filters by language when specified", async (t) => {
  const credentials = startProxyExports.getCredentials(
    getRunnerLogger(true),
    undefined,
    toEncodedJSON(mixedCredentials),
    BuiltInLanguage.java,
  );
  t.is(credentials.length, 1);
  t.is(credentials[0].type, "maven_repository");
});

test("getCredentials returns all for a language when specified", async (t) => {
  const credentials = startProxyExports.getCredentials(
    getRunnerLogger(true),
    undefined,
    toEncodedJSON(mixedCredentials),
    BuiltInLanguage.go,
  );
  t.is(credentials.length, 2);

  const credentialsTypes = credentials.map((c) => c.type);
  t.assert(credentialsTypes.includes("goproxy_server"));
  t.assert(credentialsTypes.includes("git_source"));
});

test("getCredentials returns all goproxy_servers for Go when specified", async (t) => {
  const multipleGoproxyServers = [
    { type: "goproxy_server", host: "goproxy1.example.com", token: "token1" },
    { type: "goproxy_server", host: "goproxy2.example.com", token: "token2" },
    { type: "git_source", host: "github.com/github", token: "mno" },
  ];

  const credentials = startProxyExports.getCredentials(
    getRunnerLogger(true),
    undefined,
    toEncodedJSON(multipleGoproxyServers),
    BuiltInLanguage.go,
  );
  t.is(credentials.length, 3);

  const goproxyServers = credentials.filter((c) => c.type === "goproxy_server");
  t.is(goproxyServers.length, 2);
  t.assert(goproxyServers.some((c) => c.host === "goproxy1.example.com"));
  t.assert(goproxyServers.some((c) => c.host === "goproxy2.example.com"));
});

test("getCredentials returns all maven_repositories for Java when specified", async (t) => {
  const multipleMavenRepositories = [
    {
      type: "maven_repository",
      host: "maven1.pkg.github.com",
      token: "token1",
    },
    {
      type: "maven_repository",
      host: "maven2.pkg.github.com",
      token: "token2",
    },
    { type: "git_source", host: "github.com/github", token: "mno" },
  ];

  const credentials = startProxyExports.getCredentials(
    getRunnerLogger(true),
    undefined,
    toEncodedJSON(multipleMavenRepositories),
    BuiltInLanguage.java,
  );
  t.is(credentials.length, 2);

  const mavenRepositories = credentials.filter(
    (c) => c.type === "maven_repository",
  );
  t.assert(mavenRepositories.some((c) => c.host === "maven1.pkg.github.com"));
  t.assert(mavenRepositories.some((c) => c.host === "maven2.pkg.github.com"));
});

test("getCredentials returns all credentials when no language specified", async (t) => {
  const credentialsInput = toEncodedJSON(mixedCredentials);

  const credentials = startProxyExports.getCredentials(
    getRunnerLogger(true),
    undefined,
    credentialsInput,
    undefined,
  );
  t.is(credentials.length, mixedCredentials.length);
});

test("getCredentials throws an error when non-printable characters are used", async (t) => {
  const invalidCredentials: startProxyExports.RawCredential[] = [
    { type: "nuget_feed", host: "1nuget.pkg.github.com", token: "abc\u0000" }, // Non-printable character in token
    { type: "nuget_feed", host: "2nuget.pkg.github.com\u0001" }, // Non-printable character in host
    {
      type: "nuget_feed",
      host: "3nuget.pkg.github.com",
      password: "ghi\u0002",
    }, // Non-printable character in password
    {
      type: "nuget_feed",
      host: "4nuget.pkg.github.com",
      token: "ghi\x00",
    }, // Non-printable character in token
  ];

  for (const invalidCredential of invalidCredentials) {
    const credentialsInput = toEncodedJSON([invalidCredential]);

    t.throws(
      () =>
        startProxyExports.getCredentials(
          getRunnerLogger(true),
          undefined,
          credentialsInput,
          undefined,
        ),
      {
        message:
          "Invalid credentials - fields must contain only printable characters",
      },
    );
  }
});

for (const oidcSchemaInfo of startProxyExports.oidcSchemas) {
  test(`getCredentials throws when non-printable characters are used (${oidcSchemaInfo.name} OIDC)`, (t) => {
    const validCredential = makeFromSchema(true, oidcSchemaInfo.schema);
    for (const key of Object.keys(validCredential)) {
      const invalidAuthConfig = {
        ...validCredential,
        [key]: "123\x00",
      };
      const invalidCredential: startProxyExports.RawCredential = {
        type: "nuget_feed",
        host: `${key}.nuget.pkg.github.com`,
        ...invalidAuthConfig,
      };
      const credentialsInput = toEncodedJSON([invalidCredential]);

      t.throws(
        () =>
          startProxyExports.getCredentials(
            getRunnerLogger(true),
            undefined,
            credentialsInput,
            undefined,
          ),
        {
          message:
            "Invalid credentials - fields must contain only printable characters",
        },
      );
    }
  });
}

test("getCredentials accepts OIDC configurations", (t) => {
  const oidcConfigurations = startProxyExports.oidcSchemas.map(
    (schemaInfo) => ({
      type: "nuget_feed",
      host: `${schemaInfo.name.toLowerCase()}.pkg.github.com`,
      ...makeFromSchema(true, schemaInfo.schema),
    }),
  );

  const credentials = startProxyExports.getCredentials(
    getRunnerLogger(true),
    undefined,
    toEncodedJSON(oidcConfigurations),
    BuiltInLanguage.csharp,
  );
  t.is(credentials.length, startProxyExports.oidcSchemas.length);

  t.assert(credentials.every((c) => c.type === "nuget_feed"));

  for (const oidcSchemaInfo of startProxyExports.oidcSchemas) {
    t.assert(
      credentials.some((c) =>
        validateSchema(
          oidcSchemaInfo.schema,
          c as unknown as UnvalidatedObject<any>,
        ),
      ),
    );
  }
});

const getCredentialsMacro = makeMacro({
  exec: async (
    t: ExecutionContext<unknown>,
    credentials: startProxyExports.RawCredential[],
    checkAccepted: (
      t: ExecutionContext<unknown>,
      logger: RecordingLogger,
      results: startProxyExports.Credential[],
    ) => void,
  ) => {
    const logger = new RecordingLogger();
    const credentialsString = toEncodedJSON(credentials);

    const results = startProxyExports.getCredentials(
      logger,
      undefined,
      credentialsString,
      undefined,
    );

    checkAccepted(t, logger, results);
  },

  title: (providedTitle = "") => `getCredentials - ${providedTitle}`,
});

getCredentialsMacro(
  "warns for PAT-like password without a username",
  [
    {
      type: "git_server",
      host: "https://github.com/",
      password: `ghp_${makeTestToken()}`,
    },
  ],
  (t, logger, results) => {
    // The configurations should be accepted, despite the likely problem.
    t.assert(results);
    t.is(results.length, 1);
    t.is(results[0].type, "git_server");
    t.is(results[0].host, "https://github.com/");

    if (startProxyExports.hasUsernameAndPassword(results[0])) {
      t.assert(results[0].password?.startsWith("ghp_"));
    } else {
      t.fail("Expected a `UsernamePassword`-based credential.");
    }

    // A warning should have been logged.
    checkExpectedLogMessages(t, logger.messages, [
      "using a GitHub Personal Access Token (PAT), but no username was provided",
    ]);
  },
);

getCredentialsMacro(
  "no warning for PAT-like password with a username",
  [
    {
      type: "git_server",
      host: "https://github.com/",
      username: "someone",
      password: `ghp_${makeTestToken()}`,
    },
  ],
  (t, logger, results) => {
    // The configurations should be accepted, despite the likely problem.
    t.assert(results);
    t.is(results.length, 1);
    t.is(results[0].type, "git_server");
    t.is(results[0].host, "https://github.com/");

    if (startProxyExports.hasUsernameAndPassword(results[0])) {
      t.assert(results[0].password?.startsWith("ghp_"));
    } else {
      t.fail("Expected a `UsernamePassword`-based credential.");
    }

    assertNotLogged(
      t,
      logger,
      "using a GitHub Personal Access Token (PAT), but no username was provided",
    );
  },
);

getCredentialsMacro(
  "warns for PAT-like token without a username",
  [
    {
      type: "git_server",
      host: "https://github.com/",
      token: `ghp_${makeTestToken()}`,
    },
  ],
  (t, logger, results) => {
    // The configurations should be accepted, despite the likely problem.
    t.assert(results);
    t.is(results.length, 1);
    t.is(results[0].type, "git_server");
    t.is(results[0].host, "https://github.com/");

    if (startProxyExports.isToken(results[0])) {
      t.assert(results[0].token?.startsWith("ghp_"));
    } else {
      t.fail("Expected a `Token`-based credential.");
    }

    // A warning should have been logged.
    checkExpectedLogMessages(t, logger.messages, [
      "using a GitHub Personal Access Token (PAT), but no username was provided",
    ]);
  },
);

getCredentialsMacro(
  "no warning for PAT-like token with a username",
  [
    {
      type: "git_server",
      host: "https://github.com/",
      username: "someone",
      token: `ghp_${makeTestToken()}`,
    },
  ],
  (t, logger, results) => {
    // The configurations should be accepted, despite the likely problem.
    t.assert(results);
    t.is(results.length, 1);
    t.is(results[0].type, "git_server");
    t.is(results[0].host, "https://github.com/");

    if (startProxyExports.isToken(results[0])) {
      t.assert(results[0].token?.startsWith("ghp_"));
    } else {
      t.fail("Expected a `Token`-based credential.");
    }

    assertNotLogged(
      t,
      logger,
      "using a GitHub Personal Access Token (PAT), but no username was provided",
    );
  },
);

test("getCredentials validates 'replaces-base' correctly", async (t) => {
  // Valid cases.
  const credentialsInput = toEncodedJSON([
    {
      type: "maven_repository",
      host: "maven1.pkg.github.com",
      token: "abc",
      "replaces-base": false,
    },
    {
      type: "maven_repository",
      host: "maven2.pkg.github.com",
      token: "def",
      "replaces-base": true,
    },
    {
      type: "maven_repository",
      host: "maven3.pkg.github.com",
      token: "ghi",
    },
  ]);

  const credentials = startProxyExports.getCredentials(
    getRunnerLogger(true),
    undefined,
    credentialsInput,
    BuiltInLanguage.java,
    false,
  );

  t.is(credentials.length, 3);
  t.true(credentials.some((c) => c["replaces-base"] === true));
  t.true(credentials.some((c) => c["replaces-base"] === false));
  t.true(credentials.some((c) => c["replaces-base"] === undefined));

  // Invalid cases.
  const baseInvalid = {
    type: "maven_repository",
    host: "maven4.pkg.github.com",
    token: "jkl",
  };
  t.throws(() =>
    startProxyExports.getCredentials(
      getRunnerLogger(true),
      undefined,
      toEncodedJSON([{ ...baseInvalid, "replaces-base": null }]),
      BuiltInLanguage.actions,
      false,
    ),
  );
  t.throws(() =>
    startProxyExports.getCredentials(
      getRunnerLogger(true),
      undefined,
      toEncodedJSON([{ ...baseInvalid, "replaces-base": 123 }]),
      BuiltInLanguage.actions,
      false,
    ),
  );
  t.throws(() =>
    startProxyExports.getCredentials(
      getRunnerLogger(true),
      undefined,
      toEncodedJSON([{ ...baseInvalid, "replaces-base": "true" }]),
      BuiltInLanguage.actions,
      false,
    ),
  );
});

test("getCredentials returns all credentials for Actions when using LANGUAGE_TO_REGISTRY_TYPE", async (t) => {
  const credentialsInput = toEncodedJSON(mixedCredentials);

  const credentials = startProxyExports.getCredentials(
    getRunnerLogger(true),
    undefined,
    credentialsInput,
    BuiltInLanguage.actions,
    false,
  );
  t.is(credentials.length, mixedCredentials.length);
});

test("getCredentials returns no credentials for Actions when using NEW_LANGUAGE_TO_REGISTRY_TYPE", async (t) => {
  const credentialsInput = toEncodedJSON(mixedCredentials);

  const credentials = startProxyExports.getCredentials(
    getRunnerLogger(true),
    undefined,
    credentialsInput,
    BuiltInLanguage.actions,
    true,
  );
  t.deepEqual(credentials, []);
});

function mockGetApiClient(endpoints: any) {
  return (
    sinon
      .stub(apiClient, "getApiClient")
      // eslint-disable-next-line @typescript-eslint/no-unsafe-argument
      .returns({ rest: endpoints } as any)
  );
}

type ReleaseAssets = Array<{ name: string; url?: string }>;

function mockGetReleaseByTag(assets?: ReleaseAssets) {
  const getReleaseByTag =
    assets === undefined
      ? sinon.stub().rejects()
      : sinon.stub().resolves({
          status: 200,
          data: { assets },
          headers: {},
          url: "GET /repos/:owner/:repo/releases/tags/:tag",
        });

  return mockGetApiClient({ repos: { getReleaseByTag } });
}

function mockOfflineFeatures(tempDir: string, logger: Logger) {
  // Using GHES ensures that we are using `OfflineFeatures`.
  const gitHubVersion = {
    type: GitHubVariant.GHES,
    version: "3.0.0",
  };
  sinon.stub(apiClient, "getGitHubVersion").resolves(gitHubVersion);

  return setUpFeatureFlagTests(tempDir, logger, gitHubVersion);
}

test.serial(
  "getDownloadUrl returns fallback when `getReleaseByVersion` rejects",
  async (t) => {
    const logger = new RecordingLogger();
    mockGetReleaseByTag();

    await withTmpDir(async (tempDir) => {
      const features = mockOfflineFeatures(tempDir, logger);
      const info = await startProxyExports.getDownloadUrl(
        getRunnerLogger(true),
        features,
      );

      t.is(info.version, startProxyExports.UPDATEJOB_PROXY_VERSION);
      t.is(
        info.url,
        startProxyExports.getFallbackUrl(startProxyExports.getProxyPackage()),
      );
    });
  },
);

test.serial(
  "getDownloadUrl returns fallback when there's no matching release asset",
  async (t) => {
    const logger = new RecordingLogger();
    const testAssets = [[], [{ name: "foo" }]];

    await withTmpDir(async (tempDir) => {
      const features = mockOfflineFeatures(tempDir, logger);

      for (const assets of testAssets) {
        const stub = mockGetReleaseByTag(assets);
        const info = await startProxyExports.getDownloadUrl(
          getRunnerLogger(true),
          features,
        );

        t.is(info.version, startProxyExports.UPDATEJOB_PROXY_VERSION);
        t.is(
          info.url,
          startProxyExports.getFallbackUrl(startProxyExports.getProxyPackage()),
        );

        stub.restore();
      }
    });
  },
);

test.serial("getDownloadUrl returns matching release asset", async (t) => {
  const logger = new RecordingLogger();
  const assets = [
    { name: "foo", url: "other-url" },
    { name: startProxyExports.getProxyPackage(), url: "url-we-want" },
  ];
  mockGetReleaseByTag(assets);

  await withTmpDir(async (tempDir) => {
    const features = mockOfflineFeatures(tempDir, logger);
    const info = await startProxyExports.getDownloadUrl(
      getRunnerLogger(true),
      features,
    );

    t.is(info.version, defaults.cliVersion);
    t.is(info.url, "url-we-want");
  });
});

test.serial(
  "getSafeErrorMessage - returns actual message for `StartProxyError`",
  (t) => {
    const error = new startProxyExports.StartProxyError(
      startProxyExports.StartProxyErrorType.DownloadFailed,
    );
    t.is(
      startProxyExports.getSafeErrorMessage(error),
      startProxyExports.getStartProxyErrorMessage(error.errorType),
    );
  },
);

test.serial(
  "getSafeErrorMessage - does not return message for arbitrary errors",
  (t) => {
    const error = new Error(
      startProxyExports.getStartProxyErrorMessage(
        startProxyExports.StartProxyErrorType.DownloadFailed,
      ),
    );

    const message = startProxyExports.getSafeErrorMessage(error);

    t.not(message, error.message);
    t.assert(message.startsWith("Error from start-proxy Action omitted"));
    t.assert(message.includes(error.name));
  },
);

const wrapFailureTest = makeMacro({
  exec: async (
    t: ExecutionContext<unknown>,
    setup: () => void,
    fn: (logger: Logger) => Promise<void>,
  ) => {
    await withRecordingLoggerAsync(async (logger) => {
      setup();

      await t.throwsAsync(fn(logger), {
        instanceOf: startProxyExports.StartProxyError,
      });
    });
  },
  title: (providedTitle) => `${providedTitle} - wraps errors on failure`,
});

test.serial("downloadProxy - returns file path on success", async (t) => {
  await withRecordingLoggerAsync(async (logger) => {
    const testPath = "/some/path";
    sinon.stub(toolcache, "downloadTool").resolves(testPath);

    const result = await startProxyExports.downloadProxy(
      logger,
      "url",
      undefined,
    );
    t.is(result, testPath);
  });
});

wrapFailureTest.serial(
  "downloadProxy",
  () => {
    sinon.stub(toolcache, "downloadTool").throws();
  },
  async (logger) => {
    await startProxyExports.downloadProxy(logger, "url", undefined);
  },
);

test.serial("extractProxy - returns file path on success", async (t) => {
  await withRecordingLoggerAsync(async (logger) => {
    const testPath = "/some/path";
    sinon.stub(toolcache, "extractTar").resolves(testPath);

    const result = await startProxyExports.extractProxy(logger, "/other/path");
    t.is(result, testPath);
  });
});

wrapFailureTest.serial(
  "extractProxy",
  () => {
    sinon.stub(toolcache, "extractTar").throws();
  },
  async (logger) => {
    await startProxyExports.extractProxy(logger, "path");
  },
);

test.serial("cacheProxy - returns file path on success", async (t) => {
  await withRecordingLoggerAsync(async (logger) => {
    const testPath = "/some/path";
    sinon.stub(toolcache, "cacheDir").resolves(testPath);

    const result = await startProxyExports.cacheProxy(
      logger,
      "/other/path",
      "proxy",
      "1.0",
    );
    t.is(result, testPath);
  });
});

wrapFailureTest.serial(
  "cacheProxy",
  () => {
    sinon.stub(toolcache, "cacheDir").throws();
  },
  async (logger) => {
    await startProxyExports.cacheProxy(logger, "/other/path", "proxy", "1.0");
  },
);

test.serial(
  "getProxyBinaryPath - returns path from tool cache if available",
  async (t) => {
    const logger = new RecordingLogger();
    mockGetReleaseByTag();

    await withTmpDir(async (tempDir) => {
      const toolcachePath = "/path/to/proxy/dir";
      sinon.stub(toolcache, "find").returns(toolcachePath);

      const features = mockOfflineFeatures(tempDir, logger);
      const path = await startProxyExports.getProxyBinaryPath(logger, features);

      t.assert(path);
      t.is(
        path,
        filepath.join(toolcachePath, startProxyExports.getProxyFilename()),
      );
    });
  },
);

test.serial(
  "getProxyBinaryPath - downloads proxy if not in cache",
  async (t) => {
    const logger = new RecordingLogger();
    const downloadUrl = "url-we-want";
    mockGetReleaseByTag([
      { name: startProxyExports.getProxyPackage(), url: downloadUrl },
    ]);

    const toolcachePath = "/path/to/proxy/dir";
    const find = sinon.stub(toolcache, "find").returns("");
    const getApiDetails = sinon.stub(apiClient, "getApiDetails").returns({
      auth: "",
      url: "",
      apiURL: "",
    });
    const getAuthorizationHeaderFor = sinon
      .stub(apiClient, "getAuthorizationHeaderFor")
      .returns(undefined);
    const archivePath = "/path/to/archive";
    const downloadTool = sinon
      .stub(toolcache, "downloadTool")
      .resolves(archivePath);
    const extractedPath = "/path/to/extracted";
    const extractTar = sinon
      .stub(toolcache, "extractTar")
      .resolves(extractedPath);
    const cacheDir = sinon.stub(toolcache, "cacheDir").resolves(toolcachePath);

    const path = await startProxyExports.getProxyBinaryPath(
      logger,
      createFeatures([]),
    );

    t.assert(find.calledOnce);
    t.assert(getApiDetails.calledOnce);
    t.assert(getAuthorizationHeaderFor.calledOnce);
    t.assert(downloadTool.calledOnceWith(downloadUrl));
    t.assert(extractTar.calledOnceWith(archivePath));
    t.assert(cacheDir.calledOnceWith(extractedPath));
    t.assert(path);
    t.is(
      path,
      filepath.join(toolcachePath, startProxyExports.getProxyFilename()),
    );

    checkExpectedLogMessages(t, logger.messages, [
      `Found '${startProxyExports.getProxyPackage()}' in release '${defaults.bundleVersion}' at '${downloadUrl}'`,
    ]);
  },
);

test.serial(
  "getProxyBinaryPath - downloads proxy based on features if not in cache",
  async (t) => {
    const logger = new RecordingLogger();
    const expectedTag = "codeql-bundle-v2.20.1";
    const expectedParams = {
      owner: "github",
      repo: "codeql-action",
      tag: expectedTag,
    };
    const downloadUrl = "url-we-want";
    const assets = [
      {
        name: startProxyExports.getProxyPackage(),
        url: downloadUrl,
      },
    ];

    const getReleaseByTag = sinon.stub();
    getReleaseByTag.withArgs(sinon.match(expectedParams)).resolves({
      status: 200,
      data: { assets },
      headers: {},
      url: "GET /repos/:owner/:repo/releases/tags/:tag",
    });
    mockGetApiClient({ repos: { getReleaseByTag } });

    await withTmpDir(async (tempDir) => {
      const toolcachePath = "/path/to/proxy/dir";
      const find = sinon.stub(toolcache, "find").returns("");
      const getApiDetails = sinon.stub(apiClient, "getApiDetails").returns({
        auth: "",
        url: "",
        apiURL: "",
      });
      const getAuthorizationHeaderFor = sinon
        .stub(apiClient, "getAuthorizationHeaderFor")
        .returns(undefined);
      const archivePath = "/path/to/archive";
      const downloadTool = sinon
        .stub(toolcache, "downloadTool")
        .resolves(archivePath);
      const extractedPath = "/path/to/extracted";
      const extractTar = sinon
        .stub(toolcache, "extractTar")
        .resolves(extractedPath);
      const cacheDir = sinon
        .stub(toolcache, "cacheDir")
        .resolves(toolcachePath);

      const gitHubVersion: GitHubVersion = {
        type: GitHubVariant.DOTCOM,
      };
      sinon.stub(apiClient, "getGitHubVersion").resolves(gitHubVersion);

      const features = setUpFeatureFlagTests(tempDir, logger, gitHubVersion);
      sinon.stub(features, "getValue").callsFake(async (_feature, _codeql) => {
        return true;
      });
      const getDefaultCliVersion = sinon
        .stub(features, "getEnabledDefaultCliVersions")
        .resolves({
          enabledVersions: [{ cliVersion: "2.20.1", tagName: expectedTag }],
        });
      const path = await startProxyExports.getProxyBinaryPath(logger, features);

      t.assert(getDefaultCliVersion.calledOnce);
      sinon.assert.calledOnceWithMatch(
        getReleaseByTag,
        sinon.match(expectedParams),
      );
      t.assert(find.calledOnce);
      t.assert(getApiDetails.calledOnce);
      t.assert(getAuthorizationHeaderFor.calledOnce);
      t.assert(downloadTool.calledOnceWith(downloadUrl));
      t.assert(extractTar.calledOnceWith(archivePath));
      t.assert(cacheDir.calledOnceWith(extractedPath));

      t.assert(path);
      t.is(
        path,
        filepath.join(toolcachePath, startProxyExports.getProxyFilename()),
      );
    });

    checkExpectedLogMessages(t, logger.messages, [
      `Found '${startProxyExports.getProxyPackage()}' in release '${expectedTag}' at '${downloadUrl}'`,
    ]);
  },
);
