// Copyright 2018-2026 the Deno authors. MIT license.
/// FLAGS

import { parseArgs } from "@std/cli/parse-args";
import { checkCiHash, join, resolve, ROOT_PATH } from "../../../tools/util.js";

export const {
  all,
  json,
  wptreport,
  quiet,
  release,
  rebuild,
  ["--"]: rest,
  ["auto-config"]: autoConfig,
  ["inspect-brk"]: inspectBrk,
  ["no-ignore"]: noIgnore,
  binary,
} = parseArgs(Deno.args, {
  "--": true,
  boolean: [
    "all",
    "quiet",
    "release",
    "no-interactive",
    "inspect-brk",
    "no-ignore",
  ],
  string: ["json", "wptreport", "binary"],
});

export function denoBinary() {
  if (binary) {
    return resolve(binary);
  }
  const ext = Deno.build.os === "windows" ? ".exe" : "";
  return join(
    ROOT_PATH,
    `./target/${release ? "release" : "debug"}/deno${ext}`,
  );
}

/// CI SKIP CACHE

async function getWptSubmoduleHash(): Promise<string> {
  const gitResult = await new Deno.Command("git", {
    args: ["rev-parse", "HEAD"],
    cwd: join(ROOT_PATH, "tests", "wpt", "suite"),
    stdout: "piped",
    stderr: "piped",
  }).output();
  return new TextDecoder().decode(gitResult.stdout).trim();
}

export function checkWptCiHash() {
  return checkCiHash("wpt", async (hasher) => {
    const hashDenoBinaryTask = hasher.hashFile(denoBinary());
    const submoduleHash = await getWptSubmoduleHash();
    await hashDenoBinaryTask;
    hasher.writeSync(submoduleHash);
  });
}

/// WPT TEST MANIFEST

export interface Manifest {
  items: {
    testharness: ManifestFolder;
  };
}
export interface ManifestFolder {
  [key: string]: ManifestFolder | ManifestTest;
}
export type ManifestTest = [
  hash: string,
  ...variations: ManifestTestVariation[],
];
export type ManifestTestVariation = [
  path: string,
  options: ManifestTestOptions,
];
export interface ManifestTestOptions {
  // deno-lint-ignore camelcase
  script_metadata: [string, string][];
  timeout?: string;
}

const MANIFEST_PATH = join(ROOT_PATH, "./tests/wpt/runner/manifest.json");

export async function updateManifest() {
  const status = await runPy(
    [
      "wpt",
      "manifest",
      "--tests-root",
      ".",
      "-p",
      MANIFEST_PATH,
      ...(rebuild ? ["--rebuild"] : []),
    ],
    {},
  ).status;
  assert(status.success, "updating wpt manifest should succeed");
}

export function getManifest(): Manifest {
  const manifestText = Deno.readTextFileSync(MANIFEST_PATH);
  return JSON.parse(manifestText);
}

/// WPT TEST EXPECTATIONS

export const EXPECTATIONS_DIR = join(
  ROOT_PATH,
  "./tests/wpt/runner/expectations",
);

export interface TestExpectation {
  ignore?: boolean;
  expectedFailures?: string[];
  flaky?: boolean;
}

export interface Expectation {
  [key: string]: Expectation | boolean | TestExpectation;
}

export function getExpectation(): Expectation {
  const expectation: Expectation = {};
  for (const entry of Deno.readDirSync(EXPECTATIONS_DIR)) {
    if (
      !entry.isFile || !entry.name.endsWith(".json") ||
      entry.name === "schema.json"
    ) continue;
    const suiteName = entry.name.slice(0, -".json".length);
    const text = Deno.readTextFileSync(join(EXPECTATIONS_DIR, entry.name));
    expectation[suiteName] = JSON.parse(text);
  }
  return expectation;
}

export function saveExpectation(
  expectation: Expectation,
  path?: string,
) {
  if (path) {
    // Write single merged file (used for tmp diff in `run` command)
    Deno.writeTextFileSync(
      path,
      JSON.stringify(expectation, undefined, "  ") + "\n",
    );
    return;
  }

  // Write each top-level key to its own file in the expectations directory
  const existingFiles = new Set<string>();
  for (const entry of Deno.readDirSync(EXPECTATIONS_DIR)) {
    if (entry.isFile && entry.name.endsWith(".json")) {
      existingFiles.add(entry.name);
    }
  }

  const writtenFiles = new Set<string>();
  for (const [key, value] of Object.entries(expectation)) {
    const fileName = `${key}.json`;
    writtenFiles.add(fileName);
    Deno.writeTextFileSync(
      join(EXPECTATIONS_DIR, fileName),
      JSON.stringify(value, undefined, "  ") + "\n",
    );
  }

  // Remove files for keys no longer present (but never remove schema.json)
  for (const fileName of existingFiles) {
    if (!writtenFiles.has(fileName) && fileName !== "schema.json") {
      Deno.removeSync(join(EXPECTATIONS_DIR, fileName));
    }
  }
}

export function getExpectFailForCase(
  expectation: boolean | TestExpectation,
  caseName: string,
): boolean {
  if (noIgnore) return false;
  if (typeof expectation === "boolean") {
    return !expectation;
  }
  return expectation.expectedFailures?.includes(caseName) ?? false;
}

/// UTILS

class AssertionError extends Error {
  override name = "AssertionError";
  constructor(message: string) {
    super(message);
  }
}

export function assert(condition: unknown, message: string): asserts condition {
  if (!condition) {
    throw new AssertionError(message);
  }
}

export function runPy<T extends Omit<Deno.CommandOptions, "cwd">>(
  args: string[],
  options: T,
): Deno.ChildProcess {
  const cmd = Deno.build.os == "windows" ? "python.exe" : "python3";
  return new Deno.Command(cmd, {
    args,
    stdout: "inherit",
    stderr: "inherit",
    ...options,
    cwd: join(ROOT_PATH, "./tests/wpt/suite/"),
  }).spawn();
}

export async function runGitDiff(args: string[]): Promise<void> {
  await new Deno.Command("git", {
    args: ["diff", ...args],
    stdout: "inherit",
    stderr: "inherit",
    cwd: ROOT_PATH,
  }).output();
}

export async function checkPy3Available() {
  const { success, stdout } = await runPy(["--version"], {
    stdout: "piped",
  }).output();
  assert(success, "failed to run python --version");
  const output = new TextDecoder().decode(stdout);
  assert(
    output.includes("Python 3.11"),
    `The ${
      Deno.build.os == "windows" ? "python.exe" : "python3"
    } in your path is not Python 3.11.x. See https://github.com/web-platform-tests/wpt/issues/44427 for more details.`,
  );
}

export async function cargoBuild() {
  if (binary) return;
  const { success } = await new Deno.Command("cargo", {
    args: ["build", ...(release ? ["--release"] : [])],
    cwd: ROOT_PATH,
    stdout: "inherit",
    stderr: "inherit",
  }).output();
  assert(success, "cargo build failed");
}

export function escapeLoneSurrogates(input: string): string;
export function escapeLoneSurrogates(input: string | null): string | null;
export function escapeLoneSurrogates(input: string | null): string | null {
  if (input === null) return null;
  return input.replace(
    /[\uD800-\uDFFF]/gu,
    (match) => `U+${match.charCodeAt(0).toString(16)}`,
  );
}

/// WPTREPORT

export async function generateRunInfo(): Promise<unknown> {
  const oses: Record<string, string> = {
    "windows": "win",
    "darwin": "mac",
    "linux": "linux",
    "freebsd": "freebsd",
    "openbsd": "openbsd",
  };
  const proc = await new Deno.Command("git", {
    args: ["rev-parse", "HEAD"],
    cwd: join(ROOT_PATH, "tests", "wpt", "suite"),
    stderr: "inherit",
  }).output();
  const revision = (new TextDecoder().decode(proc.stdout)).trim();
  const proc2 = await new Deno.Command(denoBinary(), {
    args: ["eval", "console.log(JSON.stringify(Deno.version))"],
    cwd: join(ROOT_PATH, "tests", "wpt", "suite"),
  }).output();
  const version = JSON.parse(new TextDecoder().decode(proc2.stdout));
  const runInfo = {
    "os": oses[Deno.build.os],
    "processor": Deno.build.arch,
    "version": "unknown",
    "os_version": "unknown",
    "bits": 64,
    "has_sandbox": true,
    "webrender": false,
    "automation": false,
    "linux_distro": "unknown",
    "revision": revision,
    "python_version": 3,
    "product": "deno",
    "debug": false,
    "browser_version": version.deno,
    "browser_channel": version.deno.includes("+") ? "canary" : "stable",
    "verify": false,
    "wasm": false,
    "headless": true,
  };
  return runInfo;
}
