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

import { BigIntStats, Stats } from "node:fs";
import {
  assert,
  assertEquals,
  assertNotStrictEquals,
  assertStrictEquals,
  assertStringIncludes,
} from "@std/assert";

/** Asserts that an error thrown in a callback will not be wrongly caught. */
export async function assertCallbackErrorUncaught(
  { prelude, invocation, cleanup }: {
    /** Any code which needs to run before the actual invocation (notably, any import statements). */
    prelude?: string;
    /**
     * The start of the invocation of the function, e.g. `open("foo.txt", `.
     * The callback will be added after it.
     */
    invocation: string;
    /** Called after the subprocess is finished but before running the assertions, e.g. to clean up created files. */
    cleanup?: () => Promise<void> | void;
  },
) {
  // Since the error has to be uncaught, and that will kill the Deno process,
  // the only way to test this is to spawn a subprocess.
  const p = new Deno.Command(Deno.execPath(), {
    args: [
      "eval",
      `${prelude ?? ""}
  
        ${invocation}(err) => {
          // If the bug is present and the callback is called again with an error,
          // don't throw another error, so if the subprocess fails we know it had the correct behaviour.
          if (!err) throw new Error("success");
        });`,
    ],
    stderr: "piped",
  });
  const { stderr, success } = await p.output();
  const error = new TextDecoder().decode(stderr);
  await cleanup?.();
  assert(!success);
  assertStringIncludes(error, "Error: success");
}

const numberFields = [
  "dev",
  "mode",
  "nlink",
  "uid",
  "gid",
  "rdev",
  "blksize",
  "ino",
  "size",
  "blocks",
  "atimeMs",
  "mtimeMs",
  "ctimeMs",
  "birthtimeMs",
];

const bigintFields = [
  ...numberFields,
  "atimeNs",
  "mtimeNs",
  "ctimeNs",
  "birthtimeNs",
];

const dateFields = [
  "atime",
  "mtime",
  "ctime",
  "birthtime",
];

export function assertStats(actual: Stats, expected: Deno.FileInfo) {
  [...numberFields, ...dateFields].forEach(function (k) {
    assert(k in actual, `${k} should be in Stats`);
    assertNotStrictEquals(
      actual[k as keyof Stats],
      undefined,
      `${k} should not be undefined`,
    );
    assertNotStrictEquals(
      actual[k as keyof Stats],
      null,
      `${k} should not be null`,
    );
  });

  numberFields.forEach((k) => {
    assertStrictEquals(
      typeof actual[k as keyof Stats],
      "number",
      `${k} should be a number`,
    );
  });

  dateFields.forEach((k) => {
    assert(actual[k as keyof Stats] instanceof Date, `${k} should be a Date`);
  });

  // Some properties from Deno.FileInfo may be null,
  // while node:fs Stats always has number / Date properties.
  // So we only check properties that are not null in Deno.FileInfo.
  assertEquals(actual.dev, expected.dev);
  if (expected.gid) assertEquals(actual.gid, expected.gid, "Stats.gid");
  assertEquals(actual.size, expected.size, "Stats.size");
  if (expected.blksize) {
    assertEquals(actual.blksize, expected.blksize, "Stats.blksize");
  }
  if (expected.blocks) {
    assertEquals(actual.blocks, expected.blocks, "Stats.blocks");
  }
  if (expected.ino) assertEquals(actual.ino, expected.ino, "Stats.ino");
  if (expected.mode) assertEquals(actual.mode, expected.mode, "Stats.mode");
  if (expected.nlink) assertEquals(actual.nlink, expected.nlink, "Stats.nlink");
  if (expected.rdev) assertEquals(actual.rdev, expected.rdev, "Stats.rdev");
  if (expected.uid) assertEquals(actual.uid, expected.uid, "Stats.uid");
  if (expected.atime?.getTime()) {
    assertEquals(
      actual.atime.getTime(),
      expected.atime.getTime(),
      "Stats.atime",
    );
    assertEquals(actual.atimeMs, expected.atime.getTime(), "Stats.atimeMs");
  }
  if (expected.mtime?.getTime()) {
    assertEquals(
      actual.mtime.getTime(),
      expected.mtime.getTime(),
      "Stats.mtime",
    );
    assertEquals(actual.mtimeMs, expected.mtime.getTime(), "Stats.mtimeMs");
  }
  if (expected.birthtime?.getTime()) {
    assertEquals(
      actual.birthtime.getTime(),
      expected.birthtime.getTime(),
      "Stats.birthtime",
    );
    assertEquals(
      actual.birthtimeMs,
      expected.birthtime.getTime(),
      "Stats.birthtimeMs",
    );
  }
  if (expected.ctime?.getTime()) {
    assertEquals(
      actual.ctime.getTime(),
      expected.ctime.getTime(),
      "Stats.ctime",
    );
    assertEquals(actual.ctimeMs, expected.ctime.getTime(), "Stats.ctimeMs");
  }
  assertEquals(actual.isFile(), expected.isFile, "Stats.isFile");
  assertEquals(actual.isDirectory(), expected.isDirectory, "Stats.isDirectory");
  assertEquals(
    actual.isSymbolicLink(),
    expected.isSymlink,
    "Stats.isSymbolicLink",
  );
  assertEquals(
    actual.isBlockDevice(),
    Deno.build.os === "windows" ? false : expected.isBlockDevice,
    "Stats.isBlockDevice",
  );
  assertEquals(
    actual.isFIFO(),
    Deno.build.os === "windows" ? false : expected.isFifo,
    "Stats.isFIFO",
  );
  assertEquals(
    actual.isCharacterDevice(),
    Deno.build.os === "windows" ? false : expected.isCharDevice,
    "Stats.isCharacterDevice",
  );
  assertEquals(
    actual.isSocket(),
    Deno.build.os === "windows" ? false : expected.isSocket,
    "Stats.isSocket",
  );
}

function toBigInt(num?: number | null) {
  if (num === undefined || num === null) return null;
  return BigInt(num);
}

export function assertStatsBigInt(
  actual: BigIntStats,
  expected: Deno.FileInfo,
) {
  [...bigintFields, ...dateFields].forEach(function (k) {
    assert(k in actual, `${k} should be in BigIntStats`);
    assertNotStrictEquals(
      actual[k as keyof BigIntStats],
      undefined,
      `${k} should not be undefined`,
    );
    assertNotStrictEquals(
      actual[k as keyof BigIntStats],
      null,
      `${k} should not be null`,
    );
  });

  bigintFields.forEach((k) => {
    assertStrictEquals(
      typeof actual[k as keyof BigIntStats],
      "bigint",
      `${k} should be a bigint`,
    );
  });

  dateFields.forEach((k) => {
    assert(
      actual[k as keyof BigIntStats] instanceof Date,
      `${k} should be a Date`,
    );
  });

  // Some properties from Deno.FileInfo may be null,
  // while node:fs BigIntStats always has bigint / Date properties.
  // So we only check properties that are not null in Deno.FileInfo.
  assertEquals(actual.dev, toBigInt(expected.dev), "BigIntStats.dev");
  if (expected.gid) {
    assertEquals(actual.gid, toBigInt(expected.gid), "BigIntStats.gid");
  }
  assertEquals(actual.size, toBigInt(expected.size), "BigIntStats.size");
  if (expected.blksize) {
    assertEquals(
      actual.blksize,
      toBigInt(expected.blksize),
      "BigIntStats.blksize",
    );
  }
  if (expected.blocks) {
    assertEquals(
      actual.blocks,
      toBigInt(expected.blocks),
      "BigIntStats.blocks",
    );
  }
  if (expected.ino) {
    assertEquals(actual.ino, toBigInt(expected.ino), "BigIntStats.ino");
  }
  if (expected.mode) {
    assertEquals(actual.mode, toBigInt(expected.mode), "BigIntStats.mode");
  }
  if (expected.nlink) {
    assertEquals(actual.nlink, toBigInt(expected.nlink), "BigIntStats.nlink");
  }
  if (expected.rdev) {
    assertEquals(actual.rdev, toBigInt(expected.rdev), "BigIntStats.rdev");
  }
  if (expected.uid) {
    assertEquals(actual.uid, toBigInt(expected.uid), "BigIntStats.uid");
  }
  if (expected.atime?.getTime()) {
    assertEquals(
      actual.atime.getTime(),
      expected.atime.getTime(),
      "BigIntStats.atime",
    );
    assertEquals(
      actual.atimeMs,
      toBigInt(expected.atime.getTime()),
      "BigIntStats.atimeMs",
    );
    assertEquals(
      actual.atimeNs,
      toBigInt(expected.atime.getTime()) as bigint * 1000000n,
      "BigIntStats.atimeNs",
    );
  }
  if (expected.mtime?.getTime()) {
    assertEquals(
      actual.mtime.getTime(),
      expected.mtime.getTime(),
      "BigIntStats.mtime",
    );
    assertEquals(
      actual.mtimeMs,
      toBigInt(expected.mtime.getTime()),
      "BigIntStats.mtimeMs",
    );
    assertEquals(
      actual.mtimeNs,
      toBigInt(expected.mtime.getTime()) as bigint * 1000000n,
      "BigIntStats.mtimeNs",
    );
  }
  if (expected.birthtime?.getTime()) {
    assertEquals(
      actual.birthtime.getTime(),
      expected.birthtime.getTime(),
      "BigIntStats.birthtime",
    );
    assertEquals(
      actual.birthtimeMs,
      toBigInt(expected.birthtime.getTime()),
      "BigIntStats.birthtimeMs",
    );
    assertEquals(
      actual.birthtimeNs,
      toBigInt(expected.birthtime.getTime()) as bigint * 1000000n,
      "BigIntStats.birthtimeNs",
    );
  }
  if (expected.ctime?.getTime()) {
    assertEquals(
      actual.ctime.getTime(),
      expected.ctime.getTime(),
      "BigIntStats.ctime",
    );
    assertEquals(
      actual.ctimeMs,
      toBigInt(expected.ctime.getTime()),
      "BigIntStats.ctimeMs",
    );
    assertEquals(
      actual.ctimeNs,
      toBigInt(expected.ctime.getTime()) as bigint * 1000000n,
      "BigIntStats.ctimeNs",
    );
  }
  assertEquals(
    actual.isBlockDevice(),
    Deno.build.os === "windows" ? false : expected.isBlockDevice,
    "BigIntStats.isBlockDevice",
  );
  assertEquals(
    actual.isFIFO(),
    Deno.build.os === "windows" ? false : expected.isFifo,
    "BigIntStats.isFIFO",
  );
  assertEquals(
    actual.isCharacterDevice(),
    Deno.build.os === "windows" ? false : expected.isCharDevice,
    "BigIntStats.isCharacterDevice",
  );
  assertEquals(
    actual.isSocket(),
    Deno.build.os === "windows" ? false : expected.isSocket,
    "BigIntStats.isSocket",
  );
}
