// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
import { partition } from "@std/collections/partition";
import { join } from "@std/path";
import * as JSONC from "@std/jsonc";
import { walk } from "@std/fs/walk";
import { relative } from "@std/path/posix/relative";

/**
 * The test suite matches the folders inside the `test` folder inside the
 * node repo
 *
 * Each test suite contains a list of files (which can be paths
 * or a regex to match) that will be pulled from the node repo
 */
type TestSuites = Record<string, string[]>;

interface Config {
  /** Ignored files won't regenerated by the update script */
  ignore: TestSuites;
  /**
   * The files that will be run by the test suite
   *
   * The files to be generated with the update script must be listed here as well,
   * but they won't be regenerated if they are listed in the `ignore` configuration
   */
  tests: TestSuites;
  windowsIgnore: TestSuites;
  darwinIgnore: TestSuites;
}

export const config: Config = JSONC.parse(
  await Deno.readTextFile(new URL("./config.jsonc", import.meta.url)),
) as unknown as Config;

export const ignoreList = Object.entries(config.ignore).reduce(
  (total: RegExp[], [suite, paths]) => {
    paths.forEach((path) => total.push(new RegExp(join(suite, path))));
    return total;
  },
  [/package\.json/],
);

export function getPathsFromTestSuites(suites: TestSuites): string[] {
  const testPaths: string[] = [];
  for (const [dir, paths] of Object.entries(suites)) {
    if (
      ["parallel", "internet", "pummel", "sequential", "pseudo-tty"].includes(
        dir,
      )
    ) {
      for (const path of paths) {
        testPaths.push(join(dir, path));
      }
    }
  }
  return testPaths;
}

const PARALLEL_PATTERN = /^parallel[\/\\]/;

export function partitionParallelTestPaths(
  testPaths: string[],
): { parallel: string[]; sequential: string[] } {
  const partitions = partition(testPaths, (p) => !!p.match(PARALLEL_PATTERN));
  return { parallel: partitions[0], sequential: partitions[1] };
}

export const NODE_IGNORED_TEST_DIRS = [
  "addons",
  "async-hooks",
  "cctest",
  "common",
  "doctool",
  "embedding",
  "fixtures",
  "fuzzers",
  "js-native-api",
  "node-api",
  "overlapped-checker",
  "report",
  "testpy",
  "tick-processor",
  "tools",
  "v8-updates",
  "wasi",
  "wpt",
];

export const VENDORED_NODE_TEST = new URL(
  "./runner/suite/test/",
  import.meta.url,
);
export const NODE_COMPAT_TEST_DEST_URL = new URL(
  "./test/",
  import.meta.url,
);

export async function getNodeTests(): Promise<string[]> {
  const paths: string[] = [];
  const rootPath = VENDORED_NODE_TEST.href.slice(7);
  for await (
    const item of walk(VENDORED_NODE_TEST, { exts: [".js"] })
  ) {
    const path = relative(rootPath, item.path);
    if (NODE_IGNORED_TEST_DIRS.every((dir) => !path.startsWith(dir))) {
      paths.push(path);
    }
  }

  return paths.sort();
}

export async function getDenoTests() {
  const paths: string[] = [];
  const rootPath = NODE_COMPAT_TEST_DEST_URL.href.slice(7);
  for await (
    const item of walk(NODE_COMPAT_TEST_DEST_URL, { exts: [".js"] })
  ) {
    const path = relative(rootPath, item.path);
    paths.push(path);
  }

  return paths.sort();
}

let testSerialId = 0;
const cwd = new URL(".", import.meta.url);

export async function runNodeCompatTestCase(
  testCase: string,
  signal?: AbortSignal,
) {
  const v8Flags = ["--stack-size=4000"];
  const testSource = await Deno.readTextFile(testCase);
  const envVars: Record<string, string> = {};
  const knownGlobals: string[] = [];
  parseFlags(testSource).forEach((flag) => {
    switch (flag) {
      case "--expose_externalize_string":
        v8Flags.push("--expose-externalize-string");
        knownGlobals.push("createExternalizableString");
        break;
      case "--expose-gc":
        v8Flags.push("--expose-gc");
        knownGlobals.push("gc");
        break;
      default:
        break;
    }
  });
  if (knownGlobals.length > 0) {
    envVars["NODE_TEST_KNOWN_GLOBALS"] = knownGlobals.join(",");
  }
  // TODO(nathanwhit): once we match node's behavior on executing
  // `node:test` tests when we run a file, we can remove this
  const usesNodeTest = testSource.includes("node:test");
  const args = [
    usesNodeTest ? "test" : "run",
    "-A",
    "--quiet",
    "--unstable-unsafe-proto",
    "--unstable-bare-node-builtins",
    "--unstable-fs",
    "--v8-flags=" + v8Flags.join(),
  ];
  if (usesNodeTest) {
    // deno test typechecks by default + we want to pass script args
    args.push("--no-check", "runner.ts", "--", testCase);
  } else {
    args.push("runner.ts", testCase);
  }

  // Pipe stdout in order to output each test result as Deno.test output
  // That way the tests will respect the `--quiet` option when provided
  return new Deno.Command(Deno.execPath(), {
    args,
    env: {
      TEST_SERIAL_ID: String(testSerialId++),
      ...envVars,
    },
    cwd,
    stdout: "piped",
    stderr: "piped",
    signal,
  }).spawn();
}

/** Parses the special "Flags:"" syntax in Node.js test files */
function parseFlags(source: string): string[] {
  const line = /^\/\/ Flags: (.+)$/um.exec(source);
  if (line == null) return [];
  return line[1].split(" ");
}