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

/// <reference lib="deno.ns" />
import {
  assert,
  assertEquals,
  assertRejects,
  assertThrows,
  fail,
} from "@std/assert";
import { Buffer } from "node:buffer";
import { join } from "node:path";
import * as path from "node:path";
import { fileURLToPath } from "node:url";
import { tmpdir } from "node:os";
import {
  accessSync,
  appendFile,
  appendFileSync,
  chmod,
  chmodSync,
  chown,
  chownSync,
  close,
  closeSync,
  constants,
  copyFileSync,
  createReadStream,
  createWriteStream,
  existsSync,
  fchmod,
  fchmodSync,
  fchown,
  fchownSync,
  fdatasync,
  fdatasyncSync,
  fsync,
  fsyncSync,
  ftruncate,
  ftruncateSync,
  futimes,
  futimesSync,
  globSync,
  link,
  linkSync,
  lstatSync,
  mkdirSync,
  mkdtempSync,
  openAsBlob,
  opendirSync,
  openSync,
  promises,
  promises as fsPromises,
  readFileSync,
  readSync,
  rmSync,
  Stats,
  statSync,
  unlink,
  unlinkSync,
  watch,
  writeFileSync,
} from "node:fs";
import { readFile } from "node:fs/promises";
import {
  constants as fsPromiseConstants,
  copyFile,
  cp,
  FileHandle,
  lchown,
  lutimes,
  open,
  stat,
  statfs,
  writeFile,
} from "node:fs/promises";
import { fromFileUrl } from "@std/path";
import process from "node:process";
import { setTimeout as setTimeoutPromise } from "node:timers/promises";
import { assertCallbackErrorUncaught } from "./_test_utils.ts";
import { pathToAbsoluteFileUrl } from "../unit/test_util.ts";

Deno.test(
  "[node/fs writeFileSync] write file without option",
  () => {
    const data = "Hello";
    const filename = mkdtempSync(join(tmpdir(), "foo-")) + "/test.txt";

    writeFileSync(filename, data);
    const dataRead = readFileSync(filename, "utf8");

    assert(dataRead === "Hello");
  },
);

Deno.test(
  "[node/fs writeFileSync] write file with option ASCII",
  () => {
    const data = "Hello";
    const filename = mkdtempSync(join(tmpdir(), "foo-")) + "/test.txt";

    writeFileSync(filename, data, { encoding: "ascii" });
    const dataRead = readFileSync(filename, "utf8");

    assert(dataRead === "Hello");
  },
);

Deno.test(
  "[node/fs writeFileSync] overwrite existing file with default 'w' flag",
  () => {
    const filename = mkdtempSync(join(tmpdir(), "foo-")) + "/test.txt";

    // Create a file with initial content
    writeFileSync(filename, "initial content");
    assertEquals(readFileSync(filename, "utf8"), "initial content");

    // Overwrite the same file — should NOT throw EEXIST
    writeFileSync(filename, "new content");
    assertEquals(readFileSync(filename, "utf8"), "new content");
  },
);

Deno.test(
  "[node/fs writeFileSync] 'wx' flag fails with EEXIST on existing file",
  () => {
    const filename = mkdtempSync(join(tmpdir(), "foo-")) + "/test.txt";

    // Create a file first
    writeFileSync(filename, "original");

    // Writing with 'wx' should fail since file exists
    assertThrows(
      () => writeFileSync(filename, "overwrite", { flag: "wx" }),
      Error,
    );

    // Original content should be preserved (no truncation/corruption)
    assertEquals(readFileSync(filename, "utf8"), "original");
  },
);

Deno.test(
  "[node/fs writeFileSync] 'wx' flag succeeds for new file",
  () => {
    const filename = mkdtempSync(join(tmpdir(), "foo-")) + "/new.txt";

    // 'wx' on a new file should succeed
    writeFileSync(filename, "exclusive write", { flag: "wx" });
    assertEquals(readFileSync(filename, "utf8"), "exclusive write");
  },
);

Deno.test(
  "[node/fs writeFileSync] repeated overwrites preserve content on failure",
  () => {
    const filename = mkdtempSync(join(tmpdir(), "foo-")) + "/config.json";

    // Simulate a long-running app that repeatedly overwrites a config file
    for (let i = 0; i < 50; i++) {
      const content = JSON.stringify({ version: i, data: "x".repeat(100) });
      writeFileSync(filename, content);
      assertEquals(readFileSync(filename, "utf8"), content);
    }

    // Verify file is never corrupted to 0 bytes
    const stat = Deno.statSync(filename);
    assert(stat.size > 0, "File should not be empty after repeated writes");
  },
);

Deno.test(
  "[node/fs existsSync] path",
  { permissions: { read: true } },
  () => {
    assert(existsSync("tests/testdata/assets/fixture.json"));
  },
);

Deno.test(
  "[node/fs existsSync] url",
  { permissions: { read: true } },
  () => {
    assert(existsSync(
      pathToAbsoluteFileUrl("tests/testdata/assets/fixture.json"),
    ));
  },
);

Deno.test(
  "[node/fs existsSync] no permission",
  { permissions: { read: false } },
  () => {
    assertThrows(() => {
      existsSync("tests/testdata/assets/fixture.json");
    }, Deno.errors.NotCapable);
  },
);

Deno.test(
  "[node/fs existsSync] not exists",
  { permissions: { read: true } },
  () => {
    assert(!existsSync("bad_filename"));
  },
);

Deno.test(
  "[node/fs/promises constants] is the same as from node:fs",
  () => {
    assertEquals(constants, fsPromiseConstants);
    assertEquals(constants, promises.constants);
  },
);

Deno.test(
  "[node/fs statSync] instanceof fs.Stats",
  () => {
    const stat = statSync("tests/testdata/assets/fixture.json");
    assert(stat);
    assert(stat instanceof Stats);
  },
);

Deno.test(
  "[node/fs statSync] throw error with path information",
  () => {
    const file = "non-exist-file";
    const fileUrl = new URL(file, import.meta.url);

    assertThrows(() => {
      statSync(file);
    }, "Error: ENOENT: no such file or directory, stat 'non-exist-file'");

    assertThrows(() => {
      statSync(fileUrl);
    }, `Error: ENOENT: no such file or directory, stat '${fileUrl.pathname}'`);
  },
);

Deno.test(
  "[node/fs/promises stat] throw error with path information",
  async () => {
    const file = "non-exist-file";
    const fileUrl = new URL(file, import.meta.url);

    try {
      await stat(file);
    } catch (error: unknown) {
      assertEquals(
        `${error}`,
        "Error: ENOENT: no such file or directory, stat 'non-exist-file'",
      );
    }

    try {
      await stat(fileUrl);
    } catch (error: unknown) {
      assertEquals(
        `${error}`,
        `Error: ENOENT: no such file or directory, stat '${
          fileURLToPath(fileUrl)
        }'`,
      );
    }
  },
);

Deno.test(
  "[node/fs/promises statfs] export statfs function",
  async () => {
    await statfs(import.meta.filename!);
  },
);

Deno.test(
  "[node/fs/promises cp] copy file",
  async () => {
    const src = mkdtempSync(join(tmpdir(), "foo-")) + "/test.txt";
    const dest = mkdtempSync(join(tmpdir(), "foo-")) + "/test.txt";
    writeFileSync(src, "Hello");

    await cp(src, dest);

    const dataRead = readFileSync(dest, "utf8");
    assert(dataRead === "Hello");
  },
);

// TODO(kt3k): Delete this test case, and instead enable the compat case
// `test/parallel/test-fs-writestream-open-write.js`, when we update
// `tests/node_compat/runner/suite`.
Deno.test("[node/fs createWriteStream", async () => {
  const { promise, resolve, reject } = Promise.withResolvers<void>();
  const tempDir = await Deno.makeTempDir();
  const file = join(tempDir, "file.txt");
  try {
    const w = createWriteStream(file);

    w.on("open", () => {
      w.write("hello, ");

      process.nextTick(() => {
        w.write("world");
        w.end();
      });
    });

    w.on("close", async () => {
      try {
        assertEquals(await Deno.readTextFile(file), "hello, world");
        resolve();
      } catch (e) {
        reject(e);
      }
    });
    await promise;
  } finally {
    await Deno.remove(tempDir, { recursive: true });
  }
});

Deno.test("[node/fs] FileHandle.appendFile", async () => {
  const tempDir = await Deno.makeTempDir();
  const filePath = join(tempDir, "test_append.txt");
  const initialContent = "Hello, ";
  const appendContent = "World!";
  const expectedContent = "Hello, World!";

  try {
    await Deno.writeTextFile(filePath, initialContent);
    const fileHandle = await fsPromises.open(filePath, "a+");
    try {
      await fileHandle.appendFile(appendContent);
      const content = await Deno.readTextFile(filePath);
      assertEquals(content, expectedContent);
      const binaryData = new Uint8Array([65, 66, 67]);
      await fileHandle.appendFile(binaryData);
      const finalContent = await Deno.readFile(filePath);
      const expectedBinary = new TextEncoder().encode(expectedContent);
      const expectedFinal = new Uint8Array([...expectedBinary, ...binaryData]);
      assertEquals(finalContent, expectedFinal);
    } finally {
      await fileHandle.close().catch(() => {});
    }
  } finally {
    await Deno.remove(tempDir, { recursive: true }).catch(() => {});
  }
});

Deno.test(
  "[node/fs lstatSync] supports throwIfNoEntry option",
  () => {
    const result = lstatSync("non-existing-path", { throwIfNoEntry: false });
    assertEquals(result, undefined);
  },
);

// Test for https://github.com/denoland/deno/issues/23707
Deno.test(
  "[node/fs/promises read] respect position argument",
  async () => {
    const file = mkdtempSync(join(tmpdir(), "foo-")) + "/test.bin";
    await writeFile(file, "");

    const res: number[] = [];
    let fd: FileHandle | undefined;
    try {
      fd = await open(file, "r+");

      for (let i = 0; i <= 5; i++) {
        const buffer = new Uint8Array([i]);
        await fd.write(buffer, 0, 1, i + 10);
      }

      for (let i = 10; i <= 15; i++) {
        const buffer = new Uint8Array(1);
        await fd.read(buffer, 0, 1, i);
        res.push(Number(buffer.toString()));
      }
    } finally {
      await fd?.close();
    }

    assertEquals(res, [0, 1, 2, 3, 4, 5]);
  },
);

Deno.test("[node/fs] readSync works", () => {
  const fd = openSync("tests/testdata/assets/hello.txt", "r");
  const buf = new Uint8Array(256);
  const bytesRead = readSync(fd!, buf);
  assertEquals(bytesRead, 12);
  closeSync(fd!);
});

Deno.test("[node/fs] copyFile COPYFILE_EXCL works", async () => {
  const dir = mkdtempSync(join(tmpdir(), "foo-"));
  const src = join(dir, "src.txt");
  const dest = join(dir, "dest.txt");
  await writeFile(src, "");
  await copyFile(src, dest, fsPromiseConstants.COPYFILE_EXCL);
  assert(existsSync(dest));
  await assertRejects(() =>
    copyFile(src, dest, fsPromiseConstants.COPYFILE_EXCL)
  );
  const dest2 = join(dir, "dest2.txt");
  copyFileSync(src, dest2, fsPromiseConstants.COPYFILE_EXCL);
  assert(existsSync(dest2));
  assertThrows(() =>
    copyFileSync(src, dest2, fsPromiseConstants.COPYFILE_EXCL)
  );
});

Deno.test("[node/fs] statSync throws ENOENT for invalid path containing colon in it", () => {
  // deno-lint-ignore no-explicit-any
  const err: any = assertThrows(() => {
    // Note: Deno.stat throws ERROR_INVALID_NAME (os error 123) instead of
    // ERROR_FILE_NOT_FOUND (os error 2) on windows. This case checks that
    // ERROR_INVALID_NAME is mapped to ENOENT correctly on node compat layer.
    statSync("jsr:@std/assert");
  });
  assertEquals(err.code, "ENOENT");
});

Deno.test("[node/fs] readFile aborted with signal", async () => {
  const src = mkdtempSync(join(tmpdir(), "foo-")) + "/test.txt";
  await writeFile(src, "Hello");
  const signal = AbortSignal.abort();
  await assertRejects(
    () => readFile(src, { signal }),
    DOMException,
    "The signal has been aborted",
  );
});

async function execCmd(cmd: string) {
  const dec = new TextDecoder();
  const [bin, ...args] = cmd.split(" ");
  const command = new Deno.Command(bin, { args });
  const { code, stdout, stderr } = await command.output();
  if (code !== 0) {
    throw new Error(
      `Command failed with code ${code}: ${cmd} - ${dec.decode(stderr)}`,
    );
  }
  return dec.decode(stdout).trim();
}

Deno.test("[node/fs] fchown and fchownSync", {
  ignore: Deno.build.os === "windows",
}, async () => {
  const file = mkdtempSync(join(tmpdir(), "foo-")) + "/test.txt";
  await writeFile(file, "Hello");
  const uid = await execCmd("id -u");
  const gid = await execCmd("id -g");
  const fd = openSync(file, "r+");
  // Changing the owner of a file to the current user is not an error.
  await new Promise<void>((resolve) =>
    fchown(fd, +uid, +gid, (err) => {
      assertEquals(err, null);
      resolve();
    })
  );
  fchownSync(fd, +uid, +gid);
  // Changing the owner of a file to root is an error.
  await assertRejects(() =>
    new Promise<void>((resolve, reject) =>
      fchown(fd, 0, 0, (err) => {
        if (err) {
          reject(err);
        } else {
          resolve();
        }
      })
    )
  );
  assertThrows(() => {
    fchownSync(fd, 0, 0);
  });
  closeSync(fd);
});

Deno.test("[node/fs] fchmod works", {
  ignore: Deno.build.os === "windows",
}, async () => {
  // Prepare
  const tempFile = await Deno.makeTempFile();
  const originalFileMode = (await Deno.lstat(tempFile)).mode;
  const fd = openSync(tempFile, "r+");
  // Execute
  await new Promise<void>((resolve, reject) => {
    fchmod(fd, 0o777, (err) => {
      if (err) {
        reject(err);
      } else {
        resolve();
      }
    });
  })
    // Assert
    .then(() => {
      const newFileMode = Deno.lstatSync(tempFile).mode;
      assert(newFileMode && originalFileMode);
      assert(newFileMode === 33279 && newFileMode > originalFileMode);
    }, (error) => {
      fail(error);
    })
    .finally(() => {
      closeSync(fd);
      Deno.removeSync(tempFile);
    });
});

Deno.test("[node/fs] fchmodSync works", {
  ignore: Deno.build.os === "windows",
}, () => {
  // Prepare
  const tempFile = Deno.makeTempFileSync();
  const originalFileMode = Deno.lstatSync(tempFile).mode;
  const fd = openSync(tempFile, "r+");
  // Execute
  fchmodSync(fd, 0o777);
  // Assert
  const newFileMode = Deno.lstatSync(tempFile).mode;
  assert(newFileMode && originalFileMode);
  assert(newFileMode === 33279 && newFileMode > originalFileMode);
  closeSync(fd);
  Deno.removeSync(tempFile);
});

Deno.test("[node/fs/promises] lchown works", {
  ignore: Deno.build.os === "windows",
}, async () => {
  const tempFile = Deno.makeTempFileSync();
  const symlinkPath = tempFile + "-link";
  Deno.symlinkSync(tempFile, symlinkPath);
  const uid = await execCmd("id -u");
  const gid = await execCmd("id -g");

  await lchown(symlinkPath, +uid, +gid);

  Deno.removeSync(tempFile);
  Deno.removeSync(symlinkPath);
});

Deno.test("[node/fs/promises] lutimes works", {
  ignore: Deno.build.os === "windows",
}, async () => {
  const tempFile = Deno.makeTempFileSync();
  const symlinkPath = tempFile + "-link";
  Deno.symlinkSync(tempFile, symlinkPath);

  const date = new Date("1970-01-01T00:00:00Z");
  await lutimes(symlinkPath, date, date);

  const stats = Deno.lstatSync(symlinkPath);
  assertEquals((stats.atime as Date).getTime(), date.getTime());
  assertEquals((stats.mtime as Date).getTime(), date.getTime());

  Deno.removeSync(tempFile);
  Deno.removeSync(symlinkPath);
});

Deno.test("[node/fs] constants are correct across platforms", () => {
  assert(constants.R_OK === 4);
  // Check a handful of constants with different values across platforms
  if (Deno.build.os === "darwin") {
    assert(constants.UV_FS_O_FILEMAP === 0);
    assert(constants.O_CREAT === 0x200);
    assert(constants.O_DIRECT === undefined);
    assert(constants.O_NOATIME === undefined);
    assert(constants.O_SYMLINK === 0x200000);
  }
  if (Deno.build.os === "linux") {
    assert(constants.UV_FS_O_FILEMAP === 0);
    assert(constants.O_CREAT === 0x40);
    assert(constants.O_DIRECT !== undefined); // O_DIRECT has different values between architectures
    assert(constants.O_NOATIME === 0x40000);
    assert(constants.O_SYMLINK === undefined);
  }
  if (Deno.build.os === "windows") {
    assert(constants.UV_FS_O_FILEMAP === 0x20000000);
    assert(constants.O_CREAT === 0x100);
    assert(constants.O_DIRECT === undefined);
    assert(constants.O_NOATIME === undefined);
    assert(constants.O_SYMLINK === undefined);
  }
});

Deno.test(
  "[node/fs openAsBlob] returns a Blob with file contents",
  async () => {
    const filename = mkdtempSync(join(tmpdir(), "foo-")) + "/test.txt";
    const data = "Hello, openAsBlob!";
    writeFileSync(filename, data);

    const blob = await openAsBlob(filename);
    assertEquals(blob instanceof Blob, true);
    assertEquals(await blob.text(), data);
    assertEquals(blob.type, "");
  },
);

Deno.test(
  "[node/fs openAsBlob] respects type option",
  async () => {
    const filename = mkdtempSync(join(tmpdir(), "foo-")) + "/test.txt";
    writeFileSync(filename, "content");

    const blob = await openAsBlob(filename, { type: "text/plain" });
    assertEquals(blob.type, "text/plain");
    assertEquals(await blob.text(), "content");
  },
);

Deno.test(
  "[node/fs openAsBlob] structuredClone throws DataCloneError",
  async () => {
    const filename = mkdtempSync(join(tmpdir(), "foo-")) + "/test.txt";
    writeFileSync(filename, "content");

    const blob = await openAsBlob(filename);
    const err = assertThrows(
      () => structuredClone(blob),
      DOMException,
      "Invalid state: File-backed Blobs are not cloneable",
    );
    assertEquals(err.name, "DataCloneError");

    const sliceErr = assertThrows(
      () => structuredClone(blob.slice(0, 1)),
      DOMException,
      "Invalid state: File-backed Blobs are not cloneable",
    );
    assertEquals(sliceErr.name, "DataCloneError");
  },
);

Deno.test(
  "[node/fs openAsBlob] rejects for non-existent file",
  async () => {
    await assertRejects(
      () => openAsBlob("/non/existent/file.txt"),
    );
  },
);

Deno.test(
  "[node/fs.access] Uses the owner permission when the user is the owner",
  { ignore: Deno.build.os === "windows" },
  async () => {
    const file = await Deno.makeTempFile();
    try {
      await Deno.chmod(file, 0o600);
      await promises.access(file, constants.R_OK);
      await promises.access(file, constants.W_OK);
      await assertRejects(async () => {
        await promises.access(file, constants.X_OK);
      });
    } finally {
      await Deno.remove(file);
    }
  },
);

Deno.test(
  "[node/fs.access] doesn't reject on windows",
  { ignore: Deno.build.os !== "windows" },
  async () => {
    const file = await Deno.makeTempFile();
    try {
      await promises.access(file, constants.R_OK);
      await promises.access(file, constants.W_OK);
      await promises.access(file, constants.X_OK);
      await promises.access(file, constants.F_OK);
    } finally {
      await Deno.remove(file);
    }
  },
);

Deno.test(
  "[node/fs.accessSync] Uses the owner permission when the user is the owner",
  { ignore: Deno.build.os === "windows" },
  () => {
    const file = Deno.makeTempFileSync();
    try {
      Deno.chmodSync(file, 0o600);
      accessSync(file, constants.R_OK);
      accessSync(file, constants.W_OK);
      assertThrows(() => {
        accessSync(file, constants.X_OK);
      });
    } finally {
      Deno.removeSync(file);
    }
  },
);

Deno.test(
  "[node/fs.accessSync] doesn't throw on windows",
  { ignore: Deno.build.os !== "windows" },
  () => {
    const file = Deno.makeTempFileSync();
    try {
      accessSync(file, constants.R_OK);
      accessSync(file, constants.W_OK);
      accessSync(file, constants.X_OK);
      accessSync(file, constants.F_OK);
    } finally {
      Deno.removeSync(file);
    }
  },
);

// ==========
// appendFile tests (from _fs_appendFile_test.ts)
// ==========

const appendFileDecoder = new TextDecoder("utf-8");

Deno.test({
  name: "[node/fs.appendFile] No callback Fn results in Error",
  fn() {
    assertThrows(
      () => {
        // @ts-expect-error Argument of type 'string' is not assignable to parameter of type 'NoParamCallback'
        appendFile("some/path", "some data", "utf8");
      },
      Error,
      "The \"cb\" argument must be of type function. Received type string ('utf8')",
    );
  },
});

Deno.test({
  name: "[node/fs.appendFile] Unsupported encoding results in error()",
  fn() {
    assertThrows(
      () => {
        // @ts-expect-error Type '"made-up-encoding"' is not assignable to type
        appendFile("some/path", "some data", "made-up-encoding", () => {});
      },
      Error,
      "The argument 'encoding' is invalid encoding. Received 'made-up-encoding'",
    );
    assertThrows(
      () => {
        appendFile(
          "some/path",
          "some data",
          // @ts-expect-error Type '"made-up-encoding"' is not assignable to type
          { encoding: "made-up-encoding" },
          () => {},
        );
      },
      Error,
      "The argument 'encoding' is invalid encoding. Received 'made-up-encoding'",
    );
    assertThrows(
      // @ts-expect-error Type '"made-up-encoding"' is not assignable to type
      () => appendFileSync("some/path", "some data", "made-up-encoding"),
      Error,
      "The argument 'encoding' is invalid encoding. Received 'made-up-encoding'",
    );
    assertThrows(
      () =>
        appendFileSync("some/path", "some data", {
          // @ts-expect-error Type '"made-up-encoding"' is not assignable to type
          encoding: "made-up-encoding",
        }),
      Error,
      "The argument 'encoding' is invalid encoding. Received 'made-up-encoding'",
    );
  },
});

Deno.test({
  name: "[node/fs.appendFile] Async: Data is written to passed in rid",
  // TODO(bartlomieju): this test is broken in Deno 2, because `file.rid` is undefined.
  // The fs APIs should be rewritten to use actual FDs, not RIDs
  ignore: true,
  async fn() {
    const tempFile: string = await Deno.makeTempFile();
    using file = await Deno.open(tempFile, {
      create: true,
      write: true,
      read: true,
    });
    await new Promise<void>((resolve, reject) => {
      // @ts-ignore (iuioiua) `file.rid` should no longer be needed once FDs are used
      appendFile(file.rid, "hello world", (err) => {
        if (err) reject();
        else resolve();
      });
    })
      .then(async () => {
        const data = await Deno.readFile(tempFile);
        assertEquals(appendFileDecoder.decode(data), "hello world");
      }, () => {
        fail("No error expected");
      })
      .finally(async () => {
        await Deno.remove(tempFile);
      });
  },
});

Deno.test({
  name: "[node/fs.appendFile] Async: Data is written to passed in file path",
  async fn() {
    await new Promise<void>((resolve, reject) => {
      appendFile("_fs_appendFile_test_file.txt", "hello world", (err) => {
        if (err) reject(err);
        else resolve();
      });
    })
      .then(async () => {
        const data = await Deno.readFile("_fs_appendFile_test_file.txt");
        assertEquals(appendFileDecoder.decode(data), "hello world");
      }, (err) => {
        fail("No error was expected: " + err);
      })
      .finally(async () => {
        await Deno.remove("_fs_appendFile_test_file.txt");
      });
  },
});

Deno.test({
  name: "[node/fs.appendFile] Async: Data is written to passed in URL",
  async fn() {
    const fileURL = new URL("_fs_appendFile_test_file.txt", import.meta.url);
    await new Promise<void>((resolve, reject) => {
      appendFile(fileURL, "hello world", (err) => {
        if (err) reject(err);
        else resolve();
      });
    })
      .then(async () => {
        const data = await Deno.readFile(fromFileUrl(fileURL));
        assertEquals(appendFileDecoder.decode(data), "hello world");
      }, (err) => {
        fail("No error was expected: " + err);
      })
      .finally(async () => {
        await Deno.remove(fromFileUrl(fileURL));
      });
  },
});

Deno.test({
  name:
    "[node/fs.appendFile] Async: Callback is made with error if attempting to append data to an existing file with 'ax' flag",
  async fn() {
    const tempFile: string = await Deno.makeTempFile();
    await new Promise<void>((resolve, reject) => {
      appendFile(tempFile, "hello world", { flag: "ax" }, (err) => {
        if (err) reject(err);
        else resolve();
      });
    })
      .then(() => fail("Expected error to be thrown"))
      .catch(() => {})
      .finally(async () => {
        await Deno.remove(tempFile);
      });
  },
});

Deno.test({
  name: "[node/fs.appendFileSync] Sync: Data is written to passed in rid",
  // TODO(bartlomieju): this test is broken in Deno 2, because `file.rid` is undefined.
  // The fs APIs should be rewritten to use actual FDs, not RIDs
  ignore: true,
  fn() {
    const tempFile: string = Deno.makeTempFileSync();
    using file = Deno.openSync(tempFile, {
      create: true,
      write: true,
      read: true,
    });
    // @ts-ignore (iuioiua) `file.rid` should no longer be needed once FDs are used
    appendFileSync(file.rid, "hello world");
    const data = Deno.readFileSync(tempFile);
    assertEquals(appendFileDecoder.decode(data), "hello world");
    Deno.removeSync(tempFile);
  },
});

Deno.test({
  name: "[node/fs.appendFileSync] Sync: Data is written to passed in file path",
  fn() {
    appendFileSync("_fs_appendFile_test_file_sync.txt", "hello world");
    const data = Deno.readFileSync("_fs_appendFile_test_file_sync.txt");
    assertEquals(appendFileDecoder.decode(data), "hello world");
    Deno.removeSync("_fs_appendFile_test_file_sync.txt");
  },
});

Deno.test({
  name:
    "[node/fs.appendFileSync] Sync: error thrown if attempting to append data to an existing file with 'ax' flag",
  fn() {
    const tempFile: string = Deno.makeTempFileSync();
    assertThrows(
      () => appendFileSync(tempFile, "hello world", { flag: "ax" }),
      Error,
      "",
    );
    Deno.removeSync(tempFile);
  },
});

Deno.test({
  name:
    "[node/fs.appendFileSync] Sync: Data is written in Uint8Array to passed in file path",
  fn() {
    const testData = new TextEncoder().encode("hello world");
    appendFileSync("_fs_appendFile_test_file_sync.txt", testData);
    const data = Deno.readFileSync("_fs_appendFile_test_file_sync.txt");
    assertEquals(data, testData);
    Deno.removeSync("_fs_appendFile_test_file_sync.txt");
  },
});

Deno.test({
  name:
    "[node/fs.appendFile] Async: Data is written in Uint8Array to passed in file path",
  async fn() {
    const testData = new TextEncoder().encode("hello world");
    await new Promise<void>((resolve, reject) => {
      appendFile("_fs_appendFile_test_file.txt", testData, (err) => {
        if (err) reject(err);
        else resolve();
      });
    })
      .then(async () => {
        const data = await Deno.readFile("_fs_appendFile_test_file.txt");
        assertEquals(data, testData);
      }, (err) => {
        fail("No error was expected: " + err);
      })
      .finally(async () => {
        await Deno.remove("_fs_appendFile_test_file.txt");
      });
  },
});

Deno.test("[node/fs.appendFile] appendFile callback isn't called twice if error is thrown", async () => {
  const tempFile = await Deno.makeTempFile();
  const importUrl = new URL("node:fs", import.meta.url);
  await assertCallbackErrorUncaught({
    prelude: `import { appendFile } from ${JSON.stringify(importUrl)}`,
    invocation: `appendFile(${JSON.stringify(tempFile)}, "hello world", `,
    async cleanup() {
      await Deno.remove(tempFile);
    },
  });
});

// ==========
// chmod tests (from _fs_chmod_test.ts)
// ==========

let chmodModeAsync: number;
let chmodModeSync: number;
// On Windows chmod is only able to manipulate write permission
if (Deno.build.os === "windows") {
  chmodModeAsync = 0o444; // read-only
  chmodModeSync = 0o666; // read-write
} else {
  chmodModeAsync = 0o777;
  chmodModeSync = 0o644;
}

Deno.test({
  name: "[node/fs.chmod] ASYNC: Permissions are changed",
  async fn() {
    const tempFile: string = await Deno.makeTempFile();
    await new Promise<void>((resolve, reject) => {
      chmod(tempFile, chmodModeAsync, (err) => {
        if (err) reject(err);
        else resolve();
      });
    })
      .then(() => {
        const fileMode = Deno.lstatSync(tempFile).mode as number;
        assertEquals(fileMode & 0o777, chmodModeAsync);
      }, (error) => {
        fail(error);
      })
      .finally(() => {
        Deno.removeSync(tempFile);
      });
  },
});

Deno.test({
  name: "[node/fs.chmod] ASYNC: don't swallow NotFoundError (Windows)",
  ignore: Deno.build.os !== "windows",
  async fn() {
    await assertRejects(async () => {
      await new Promise<void>((resolve, reject) => {
        chmod("./__non_existent_file__", 0o777, (err) => {
          if (err) reject(err);
          else resolve();
        });
      });
    });
  },
});

Deno.test({
  name: "[node/fs.chmodSync] SYNC: Permissions are changed",
  fn() {
    const tempFile: string = Deno.makeTempFileSync();
    try {
      chmodSync(tempFile, chmodModeSync.toString(8));

      const fileMode = Deno.lstatSync(tempFile).mode as number;
      assertEquals(fileMode & 0o777, chmodModeSync);
    } finally {
      Deno.removeSync(tempFile);
    }
  },
});

Deno.test({
  name: "[node/fs.chmodSync] SYNC: don't swallow NotFoundError (Windows)",
  ignore: Deno.build.os !== "windows",
  fn() {
    assertThrows(() => {
      chmodSync("./__non_existent_file__", "777");
    });
  },
});

Deno.test({
  name: "[node/fs.chmod] chmod callback isn't called twice if error is thrown",
  async fn() {
    const tempFile = await Deno.makeTempFile();
    const importUrl = new URL("node:fs", import.meta.url);
    await assertCallbackErrorUncaught({
      prelude: `import { chmod } from ${JSON.stringify(importUrl)}`,
      invocation: `chmod(${JSON.stringify(tempFile)}, 0o777, `,
      async cleanup() {
        await Deno.remove(tempFile);
      },
    });
  },
});

// ==========
// chown tests (from _fs_chown_test.ts)
// ==========

const chownIgnore = Deno.build.os === "windows";

Deno.test({
  ignore: chownIgnore,
  name:
    "[node/fs.chown] ASYNC: setting existing uid/gid works as expected (non-Windows)",
  async fn() {
    const tempFile: string = await Deno.makeTempFile();
    const originalUserId: number | null = (await Deno.lstat(tempFile)).uid;
    const originalGroupId: number | null = (await Deno.lstat(tempFile)).gid;
    await new Promise<void>((resolve, reject) => {
      chown(tempFile, originalUserId!, originalGroupId!, (err) => {
        if (err) reject(err);
        else resolve();
      });
    })
      .then(() => {
        const newUserId: number | null = Deno.lstatSync(tempFile).uid;
        const newGroupId: number | null = Deno.lstatSync(tempFile).gid;
        assertEquals(newUserId, originalUserId);
        assertEquals(newGroupId, originalGroupId);
      }, () => {
        fail();
      })
      .finally(() => {
        Deno.removeSync(tempFile);
      });
  },
});

Deno.test({
  ignore: chownIgnore,
  name:
    "[node/fs.chownSync] SYNC: setting existing uid/gid works as expected (non-Windows)",
  fn() {
    const tempFile: string = Deno.makeTempFileSync();
    const originalUserId: number | null = Deno.lstatSync(tempFile).uid;
    const originalGroupId: number | null = Deno.lstatSync(tempFile).gid;
    chownSync(tempFile, originalUserId!, originalGroupId!);

    const newUserId: number | null = Deno.lstatSync(tempFile).uid;
    const newGroupId: number | null = Deno.lstatSync(tempFile).gid;
    assertEquals(newUserId, originalUserId);
    assertEquals(newGroupId, originalGroupId);
    Deno.removeSync(tempFile);
  },
});

Deno.test({
  name: "[node/fs.chown] chown callback isn't called twice if error is thrown",
  ignore: Deno.build.os === "windows",
  async fn() {
    const tempFile = await Deno.makeTempFile();
    const { uid, gid } = await Deno.lstat(tempFile);
    const importUrl = new URL("node:fs", import.meta.url);
    await assertCallbackErrorUncaught({
      prelude: `import { chown } from ${JSON.stringify(importUrl)}`,
      invocation: `chown(${JSON.stringify(tempFile)}, ${uid}, ${gid}, `,
      async cleanup() {
        await Deno.remove(tempFile);
      },
    });
  },
});

// ==========
// close tests (from _fs_close_test.ts)
// ==========

Deno.test({
  name: "[node/fs.close] ASYNC: File is closed",
  // TODO(bartlomieju): this test is broken in Deno 2, because `file.rid` is undefined.
  // The fs APIs should be rewritten to use actual FDs, not RIDs
  ignore: true,
  async fn() {
    const tempFile: string = await Deno.makeTempFile();
    const file: Deno.FsFile = await Deno.open(tempFile);

    await new Promise<void>((resolve, reject) => {
      // @ts-ignore (iuioiua) `file.rid` should no longer be needed once FDs are used
      close(file.rid, (err) => {
        if (err !== null) reject();
        else resolve();
      });
    })
      .catch(() => fail("No error expected"))
      .finally(async () => {
        await Deno.remove(tempFile);
      });
  },
});

Deno.test({
  name: "[node/fs.close] ASYNC: Invalid fd",
  fn() {
    assertThrows(() => {
      close(-1, (_err) => {});
    }, RangeError);
  },
});

Deno.test({
  name: "[node/fs.close] close callback should be asynchronous",
  // TODO(bartlomieju): this test is broken in Deno 2, because `file.rid` is undefined.
  // The fs APIs should be rewritten to use actual FDs, not RIDs
  ignore: true,
  async fn() {
    const tempFile: string = Deno.makeTempFileSync();
    const file: Deno.FsFile = Deno.openSync(tempFile);

    let foo: string;
    const promise = new Promise<void>((resolve) => {
      // @ts-ignore (iuioiua) `file.rid` should no longer be needed once FDs are used
      close(file.rid, () => {
        assert(foo === "bar");
        resolve();
      });
      foo = "bar";
    });

    await promise;
    Deno.removeSync(tempFile);
  },
});

Deno.test({
  name: "[node/fs.closeSync] SYNC: File is closed",
  // TODO(bartlomieju): this test is broken in Deno 2, because `file.rid` is undefined.
  // The fs APIs should be rewritten to use actual FDs, not RIDs
  ignore: true,
  fn() {
    const tempFile: string = Deno.makeTempFileSync();
    const file: Deno.FsFile = Deno.openSync(tempFile);

    // @ts-ignore (iuioiua) `file.rid` should no longer be needed once FDs are used
    closeSync(file.rid);
    Deno.removeSync(tempFile);
  },
});

Deno.test({
  name: "[node/fs.closeSync] SYNC: Invalid fd",
  fn() {
    assertThrows(() => closeSync(-1));
  },
});

Deno.test({
  name: "[node/fs.close] close callback isn't called twice if error is thrown",
  // TODO(bartlomieju): this test is broken in Deno 2, because `file.rid` is undefined.
  // The fs APIs should be rewritten to use actual FDs, not RIDs
  ignore: true,
}, async () => {
  const tempFile = await Deno.makeTempFile();
  const importUrl = new URL("node:fs", import.meta.url);
  await assertCallbackErrorUncaught({
    prelude: `
    import { close } from ${JSON.stringify(importUrl)};

    const file = await Deno.open(${JSON.stringify(tempFile)});
    `,
    invocation: "close(file.rid, ",
    async cleanup() {
      await Deno.remove(tempFile);
    },
  });
});

Deno.test({
  name: "[node/fs.close] close with default callback if none is provided",
}, async () => {
  const tempFile = await Deno.makeTempFile();
  const rid = openSync(tempFile, "r");
  close(rid);
  await setTimeoutPromise(1000);
  assertThrows(() => {
    closeSync(rid), Deno.errors.BadResource;
  });
  await Deno.remove(tempFile);
});

// Regression test for https://github.com/denoland/deno/issues/33712
// `fs.close` used to defer via `setTimeout(0)`, which Deno's test sanitizer
// flagged as a leaked timer when a stream auto-closed at end-of-stream.
// `createReadStream(...).on("end", ...)` triggers exactly that path.
Deno.test({
  name: "[node/fs.close] createReadStream auto-close doesn't leak a timer",
  async fn() {
    const tempFile = await Deno.makeTempFile();
    try {
      await Deno.writeTextFile(tempFile, "hello");
      await new Promise<void>((resolve, reject) => {
        const stream = createReadStream(tempFile);
        stream.on("data", () => {});
        stream.on("end", () => resolve());
        stream.on("error", reject);
      });
    } finally {
      await Deno.remove(tempFile);
    }
  },
});

// ==========
// fsync tests (from _fs_fsync_test.ts)
// ==========

Deno.test({
  name:
    "[node/fs.fsync] ASYNC: flush any pending data of the given file stream to disk",
  // TODO(bartlomieju): this test is broken in Deno 2, because `file.rid` is undefined.
  // The fs APIs should be rewritten to use actual FDs, not RIDs
  ignore: true,
  async fn() {
    const filePath = await Deno.makeTempFile();
    using file = await Deno.open(filePath, {
      read: true,
      write: true,
      create: true,
    });
    const size = 64;
    await file.truncate(size);

    await new Promise<void>((resolve, reject) => {
      // @ts-ignore (iuioiua) `file.rid` should no longer be needed once FDs are used
      fsync(file.rid, (err: Error | null) => {
        if (err !== null) reject();
        else resolve();
      });
    })
      .then(
        async () => {
          assertEquals((await Deno.stat(filePath)).size, size);
        },
        () => {
          fail("No error expected");
        },
      )
      .finally(async () => {
        await Deno.remove(filePath);
      });
  },
});

Deno.test({
  name:
    "[node/fs.fsyncSync] SYNC: flush any pending data the given file stream to disk",
  // TODO(bartlomieju): this test is broken in Deno 2, because `file.rid` is undefined.
  // The fs APIs should be rewritten to use actual FDs, not RIDs
  ignore: true,
  fn() {
    const filePath = Deno.makeTempFileSync();
    using file = Deno.openSync(filePath, {
      read: true,
      write: true,
      create: true,
    });
    const size = 64;
    file.truncateSync(size);

    try {
      // @ts-ignore (iuioiua) `file.rid` should no longer be needed once FDs are used
      fsyncSync(file.rid);
      assertEquals(Deno.statSync(filePath).size, size);
    } finally {
      Deno.removeSync(filePath);
    }
  },
});

// ==========
// fdatasync tests (from _fs_fdatasync_test.ts)
// ==========

Deno.test({
  name:
    "[node/fs.fdatasync] ASYNC: flush any pending data operations of the given file stream to disk",
  // TODO(bartlomieju): this test is broken in Deno 2, because `file.rid` is undefined.
  // The fs APIs should be rewritten to use actual FDs, not RIDs
  ignore: true,
  async fn() {
    const filePath = await Deno.makeTempFile();
    using file = await Deno.open(filePath, {
      read: true,
      write: true,
      create: true,
    });
    const data = new Uint8Array(64);
    await file.write(data);

    await new Promise<void>((resolve, reject) => {
      // @ts-ignore (iuioiua) `file.rid` should no longer be needed once FDs are used
      fdatasync(file.rid, (err: Error | null) => {
        if (err !== null) reject();
        else resolve();
      });
    })
      .then(
        async () => {
          assertEquals(await Deno.readFile(filePath), data);
        },
        () => {
          fail("No error expected");
        },
      )
      .finally(async () => {
        await Deno.remove(filePath);
      });
  },
});

Deno.test({
  name:
    "[node/fs.fdatasyncSync] SYNC: flush any pending data operations of the given file stream to disk.",
  // TODO(bartlomieju): this test is broken in Deno 2, because `file.rid` is undefined.
  // The fs APIs should be rewritten to use actual FDs, not RIDs
  ignore: true,
  fn() {
    const filePath = Deno.makeTempFileSync();
    using file = Deno.openSync(filePath, {
      read: true,
      write: true,
      create: true,
    });
    const data = new Uint8Array(64);
    file.writeSync(data);

    try {
      // @ts-ignore (iuioiua) `file.rid` should no longer be needed once FDs are used
      fdatasyncSync(file.rid);
      assertEquals(Deno.readFileSync(filePath), data);
    } finally {
      Deno.removeSync(filePath);
    }
  },
});

// ==========
// link tests (from _fs_link_test.ts)
// ==========

Deno.test({
  name: "[node/fs.link] ASYNC: hard linking files works as expected",
  async fn() {
    const tempFile: string = await Deno.makeTempFile();
    const linkedFile: string = tempFile + ".link";
    await new Promise<void>((res, rej) => {
      link(tempFile, linkedFile, (err) => {
        if (err) rej(err);
        else res();
      });
    })
      .then(() => {
        assertEquals(Deno.statSync(tempFile), Deno.statSync(linkedFile));
      }, () => {
        fail("Expected to succeed");
      })
      .finally(() => {
        Deno.removeSync(tempFile);
        Deno.removeSync(linkedFile);
      });
  },
});

Deno.test({
  name: "[node/fs.link] ASYNC: hard linking files passes error to callback",
  async fn() {
    let failed = false;
    await new Promise<void>((res, rej) => {
      link("no-such-file", "no-such-file", (err) => {
        if (err) rej(err);
        else res();
      });
    })
      .then(() => {
        fail("Expected to succeed");
      }, (err) => {
        assert(err);
        failed = true;
      });
    assert(failed);
  },
});

Deno.test({
  name: "[node/fs.linkSync] SYNC: hard linking files works as expected",
  fn() {
    const tempFile: string = Deno.makeTempFileSync();
    const linkedFile: string = tempFile + ".link";
    linkSync(tempFile, linkedFile);

    assertEquals(Deno.statSync(tempFile), Deno.statSync(linkedFile));
    Deno.removeSync(tempFile);
    Deno.removeSync(linkedFile);
  },
});

Deno.test("[node/fs.link] link callback isn't called twice if error is thrown", async () => {
  const tempDir = await Deno.makeTempDir();
  const tempFile = path.join(tempDir, "file.txt");
  const linkFile = path.join(tempDir, "link.txt");
  await Deno.writeTextFile(tempFile, "hello world");
  const importUrl = new URL("node:fs", import.meta.url);
  await assertCallbackErrorUncaught({
    prelude: `import { link } from ${JSON.stringify(importUrl)}`,
    invocation: `link(${JSON.stringify(tempFile)},
                      ${JSON.stringify(linkFile)}, `,
    async cleanup() {
      await Deno.remove(tempDir, { recursive: true });
    },
  });
});

Deno.test("[node/fs.link] link accepts Buffer", async () => {
  const tempDir = await Deno.makeTempDir();
  const tempFile = path.join(tempDir, "file.txt");
  const linkedFile = path.join(tempDir, "file.link");
  const tempFileBuffer = Buffer.from(tempFile, "utf8");
  const linkedFileBuffer = Buffer.from(linkedFile, "utf8");
  await Deno.writeTextFile(tempFile, "hello world");

  await new Promise<void>((resolve, reject) => {
    link(tempFileBuffer, linkedFileBuffer, (err) => {
      if (err) reject(err);
      else resolve();
    });
  })
    .then(() => {
      assertEquals(Deno.statSync(tempFile), Deno.statSync(linkedFile));
    }, () => {
      fail("Expected to succeed");
    })
    .finally(() => {
      Deno.removeSync(tempFile);
      Deno.removeSync(linkedFile);
      Deno.removeSync(tempDir);
    });
});

Deno.test("[node/fs.linkSync] linkSync accepts Buffer", () => {
  const tempDir = Deno.makeTempDirSync();
  const tempFile = path.join(tempDir, "file.txt");
  const linkedFile = path.join(tempDir, "file.link");
  const tempFileBuffer = Buffer.from(tempFile, "utf8");
  const linkedFileBuffer = Buffer.from(linkedFile, "utf8");
  Deno.writeTextFileSync(tempFile, "hello world");

  linkSync(tempFileBuffer, linkedFileBuffer);
  assertEquals(Deno.statSync(tempFile), Deno.statSync(linkedFile));

  Deno.removeSync(linkedFile);
  Deno.removeSync(tempFile);
  Deno.removeSync(tempDir);
});

// ==========
// unlink tests (from _fs_unlink_test.ts)
// ==========

Deno.test({
  name: "[node/fs.unlink] ASYNC: deleting a file",
  async fn() {
    const file = Deno.makeTempFileSync();
    await new Promise<void>((resolve, reject) => {
      unlink(file, (err) => {
        if (err) reject(err);
        resolve();
      });
    })
      .then(() => assertEquals(existsSync(file), false), () => fail())
      .finally(() => {
        if (existsSync(file)) Deno.removeSync(file);
      });
  },
});

Deno.test({
  name: "[node/fs.unlinkSync] SYNC: Test deleting a file",
  fn() {
    const file = Deno.makeTempFileSync();
    unlinkSync(file);
    assertEquals(existsSync(file), false);
  },
});

Deno.test("[node/fs.unlink] unlink callback isn't called twice if error is thrown", async () => {
  const tempFile = await Deno.makeTempFile();
  const importUrl = new URL("node:fs", import.meta.url);
  await assertCallbackErrorUncaught({
    prelude: `import { unlink } from ${JSON.stringify(importUrl)}`,
    invocation: `unlink(${JSON.stringify(tempFile)}, `,
  });
});

Deno.test("[node/fs.unlink] unlink accepts Buffer path", async () => {
  const file = Deno.makeTempFileSync();
  const bufferPath = Buffer.from(file, "utf-8");
  await new Promise<void>((resolve, reject) => {
    unlink(bufferPath, (err) => {
      if (err) reject(err);
      resolve();
    });
  })
    .then(() => assertEquals(existsSync(file), false), () => fail());
});

Deno.test("[node/fs.unlinkSync] unlinkSync accepts Buffer path", () => {
  const file = Deno.makeTempFileSync();
  const bufferPath = Buffer.from(file, "utf-8");
  unlinkSync(bufferPath);
  assertEquals(existsSync(file), false);
});

Deno.test("[node/fs.unlink] unlink: convert Deno error to Node.js error", async () => {
  const dir = Deno.makeTempDirSync();
  const unlinkPath = join(dir, "non_existent_file");

  await new Promise<void>((resolve, reject) => {
    unlink(unlinkPath, (err) => {
      if (err) reject(err);
      resolve();
    });
  })
    .then(() => fail(), (err) => {
      assertEquals(err.code, "ENOENT");
      assertEquals(err.syscall, "unlink");
      assertEquals(err.path, unlinkPath);
    });
});

Deno.test("[node/fs.unlinkSync] unlinkSync: convert Deno error to Node.js error", () => {
  const dir = Deno.makeTempDirSync();
  const unlinkPath = join(dir, "non_existent_file");

  try {
    unlinkSync(unlinkPath);
    fail();
  } catch (err) {
    // deno-lint-ignore no-explicit-any
    assertEquals((err as any).code, "ENOENT");
    // deno-lint-ignore no-explicit-any
    assertEquals((err as any).syscall, "unlink");
    // deno-lint-ignore no-explicit-any
    assertEquals((err as any).path, unlinkPath);
  }
});

// -- ftruncate --

Deno.test({
  name: "[node/fs ftruncate] no callback function results in Error",
  fn() {
    assertThrows(
      () => {
        // @ts-expect-error Argument of type 'number' is not assignable to parameter of type 'NoParamCallback'
        ftruncate(123, 0);
      },
      Error,
      "No callback function supplied",
    );
  },
});

Deno.test({
  name: "[node/fs ftruncate] truncate entire file contents",
  // TODO(bartlomieju): this test is broken in Deno 2, because `file.rid` is undefined.
  // The fs APIs should be rewritten to use actual FDs, not RIDs
  ignore: true,
  async fn() {
    const filePath = Deno.makeTempFileSync();
    await Deno.writeTextFile(filePath, "hello world");
    using file = await Deno.open(filePath, {
      read: true,
      write: true,
      create: true,
    });

    await new Promise<void>((resolve, reject) => {
      // @ts-ignore (iuioiua) `file.rid` should no longer be needed once FDs are used
      ftruncate(file.rid, (err: Error | null) => {
        if (err !== null) reject();
        else resolve();
      });
    })
      .then(
        () => {
          const fileInfo: Deno.FileInfo = Deno.lstatSync(filePath);
          assertEquals(fileInfo.size, 0);
        },
        () => {
          fail("No error expected");
        },
      )
      .finally(() => {
        Deno.removeSync(filePath);
      });
  },
});

Deno.test({
  name: "[node/fs ftruncate] truncate file to a size of precisely len bytes",
  // TODO(bartlomieju): this test is broken in Deno 2, because `file.rid` is undefined.
  // The fs APIs should be rewritten to use actual FDs, not RIDs
  ignore: true,
  async fn() {
    const filePath = Deno.makeTempFileSync();
    await Deno.writeTextFile(filePath, "hello world");
    using file = await Deno.open(filePath, {
      read: true,
      write: true,
      create: true,
    });

    await new Promise<void>((resolve, reject) => {
      // @ts-ignore (iuioiua) `file.rid` should no longer be needed once FDs are used
      ftruncate(file.rid, 3, (err: Error | null) => {
        if (err !== null) reject();
        else resolve();
      });
    })
      .then(
        () => {
          const fileInfo: Deno.FileInfo = Deno.lstatSync(filePath);
          assertEquals(fileInfo.size, 3);
        },
        () => {
          fail("No error expected");
        },
      )
      .finally(() => {
        Deno.removeSync(filePath);
      });
  },
});

Deno.test({
  name: "[node/fs ftruncateSync] truncate entire file contents",
  // TODO(bartlomieju): this test is broken in Deno 2, because `file.rid` is undefined.
  // The fs APIs should be rewritten to use actual FDs, not RIDs
  ignore: true,
  fn() {
    const filePath = Deno.makeTempFileSync();
    Deno.writeFileSync(filePath, new TextEncoder().encode("hello world"));
    using file = Deno.openSync(filePath, {
      read: true,
      write: true,
      create: true,
    });

    try {
      // @ts-ignore (iuioiua) `file.rid` should no longer be needed once FDs are used
      ftruncateSync(file.rid);
      const fileInfo: Deno.FileInfo = Deno.lstatSync(filePath);
      assertEquals(fileInfo.size, 0);
    } finally {
      Deno.removeSync(filePath);
    }
  },
});

Deno.test({
  name:
    "[node/fs ftruncateSync] truncate file to a size of precisely len bytes",
  // TODO(bartlomieju): this test is broken in Deno 2, because `file.rid` is undefined.
  // The fs APIs should be rewritten to use actual FDs, not RIDs
  ignore: true,
  fn() {
    const filePath = Deno.makeTempFileSync();
    Deno.writeFileSync(filePath, new TextEncoder().encode("hello world"));
    using file = Deno.openSync(filePath, {
      read: true,
      write: true,
      create: true,
    });

    try {
      // @ts-ignore (iuioiua) `file.rid` should no longer be needed once FDs are used
      ftruncateSync(file.rid, 3);
      const fileInfo: Deno.FileInfo = Deno.lstatSync(filePath);
      assertEquals(fileInfo.size, 3);
    } finally {
      Deno.removeSync(filePath);
    }
  },
});

// -- futimes --

const _randomDate = new Date(Date.now() + 1000);

Deno.test({
  name: "[node/fs futimes] change file timestamps",
  // TODO(bartlomieju): this test is broken in Deno 2, because `file.rid` is undefined.
  // The fs APIs should be rewritten to use actual FDs, not RIDs
  ignore: true,
  async fn() {
    const filePath = Deno.makeTempFileSync();
    using file = await Deno.open(filePath, { create: true, write: true });

    await new Promise<void>((resolve, reject) => {
      // @ts-ignore (iuioiua) `file.rid` should no longer be needed once FDs are used
      futimes(file.rid, _randomDate, _randomDate, (err: Error | null) => {
        if (err !== null) reject();
        else resolve();
      });
    })
      .then(
        () => {
          const fileInfo: Deno.FileInfo = Deno.lstatSync(filePath);
          assertEquals(fileInfo.mtime, _randomDate);
          assertEquals(fileInfo.atime, _randomDate);
        },
        () => {
          fail("No error expected");
        },
      )
      .finally(() => {
        Deno.removeSync(filePath);
      });
  },
});

Deno.test({
  name: "[node/fs futimes] should throw error if atime is infinity",
  fn() {
    assertThrows(
      () => {
        futimes(123, Infinity, 0, (_err: Error | null) => {});
      },
      Error,
      "invalid atime, must not be infinity or NaN",
    );
  },
});

Deno.test({
  name: "[node/fs futimes] should throw error if atime is NaN",
  fn() {
    assertThrows(
      () => {
        futimes(123, "some string", 0, (_err: Error | null) => {});
      },
      Error,
      "invalid atime, must not be infinity or NaN",
    );
  },
});

Deno.test({
  name: "[node/fs futimesSync] change file timestamps",
  // TODO(bartlomieju): this test is broken in Deno 2, because `file.rid` is undefined.
  // The fs APIs should be rewritten to use actual FDs, not RIDs
  ignore: true,
  fn() {
    const filePath = Deno.makeTempFileSync();
    using file = Deno.openSync(filePath, { create: true, write: true });

    try {
      // @ts-ignore (iuioiua) `file.rid` should no longer be needed once FDs are used
      futimesSync(file.rid, _randomDate, _randomDate);

      const fileInfo: Deno.FileInfo = Deno.lstatSync(filePath);

      assertEquals(fileInfo.mtime, _randomDate);
      assertEquals(fileInfo.atime, _randomDate);
    } finally {
      Deno.removeSync(filePath);
    }
  },
});

Deno.test({
  name: "[node/fs futimesSync] should throw error if atime is NaN",
  fn() {
    assertThrows(
      () => {
        futimesSync(123, "some string", 0);
      },
      Error,
      "invalid atime, must not be infinity or NaN",
    );
  },
});

Deno.test({
  name: "[node/fs futimesSync] should throw error if atime is Infinity",
  fn() {
    assertThrows(
      () => {
        futimesSync(123, Infinity, 0);
      },
      Error,
      "invalid atime, must not be infinity or NaN",
    );
  },
});

Deno.test({
  name:
    "[node/fs/promises FileHandle.readLines] close after readLines should not throw",
  async fn() {
    const tempFile = Deno.makeTempFileSync();
    Deno.writeTextFileSync(tempFile, "line one\nline two\nline three\n");
    try {
      const fd = await open(tempFile, "r");
      fd.readLines();
      await fd.close();
    } finally {
      Deno.removeSync(tempFile);
    }
  },
});

Deno.test({
  name:
    "[node/fs/promises FileHandle.readLines] should iterate lines correctly",
  async fn() {
    const tempFile = Deno.makeTempFileSync();
    Deno.writeTextFileSync(tempFile, "line one\nline two\nline three\n");
    try {
      const fd = await open(tempFile, "r");
      const lines: string[] = [];
      for await (const line of fd.readLines()) {
        lines.push(line);
      }
      assertEquals(lines, ["line one", "line two", "line three"]);
      await fd.close();
    } finally {
      Deno.removeSync(tempFile);
    }
  },
});

Deno.test(
  "[node/fs Dir] Dir is disposable via Symbol.dispose",
  { permissions: { read: true } },
  () => {
    using dir = opendirSync(".");
    void dir;
  },
);

Deno.test(
  "[node/fs Dir] Dir is async-disposable via Symbol.asyncDispose",
  { permissions: { read: true } },
  async () => {
    await using dir = await fsPromises.opendir(".");
    void dir;
  },
);

Deno.test(
  "[node/fs globSync] **/../* must not skip siblings due to readdir ordering",
  { permissions: { read: true, write: true, env: true } },
  () => {
    // Regression test: the ".." handler in glob can queue the same path
    // multiple times. With the LIFO queue, a duplicate entry may be processed
    // first, adding it to the cache. When the parent directory later iterates
    // its children and encounters this cached entry, the old code used `return`
    // which aborted processing of ALL remaining siblings. The fix changes it to
    // `continue` on the outer loop so only the cached child is skipped.
    const tmp = mkdtempSync(join(tmpdir(), "glob-dotdot-"));
    try {
      // Build fixture tree:
      //   a/b/c/d   (nested dirs)
      //   a/c/d/c/  (nested dirs - child "c" of a/c/d triggers ".." back to
      //              a/c, creating a duplicate queue entry for a/c)
      //   a/x       (file)
      //   a/z       (file)
      const a = join(tmp, "a");
      mkdirSync(join(a, "b", "c", "d"), { recursive: true });
      mkdirSync(join(a, "c", "d", "c"), { recursive: true });
      writeFileSync(join(a, "x"), "");
      writeFileSync(join(a, "z"), "");

      const results = globSync("a/**/../*", { cwd: tmp }).sort();

      // Every entry in "a/" must appear regardless of readdir ordering
      const sep = join("a", "b").includes("\\") ? "\\" : "/";
      for (const expected of ["a/b", "a/c", "a/x", "a/z"]) {
        const normalized = expected.replaceAll("/", sep);
        assert(
          results.includes(normalized),
          `Expected "${normalized}" in glob results, got: ${
            JSON.stringify(results)
          }`,
        );
      }
    } finally {
      rmSync(tmp, { recursive: true, force: true });
    }
  },
);

// Regression test for #30873: read_dir used to silently drop entries
// whose file names were not valid UTF-8, which made globSync (and
// readdirSync) invisibly skip those files. Lossy decoding via
// to_string_lossy keeps the file in the listing (matching Node's
// default utf8 behavior of substituting U+FFFD for invalid bytes).
Deno.test({
  name: "[node/fs globSync] surfaces files with non-UTF-8 names",
  // Restricted to Linux: macOS APFS/HFS+ normalizes/rejects non-UTF-8
  // names, and Windows file names are UTF-16. The behavior under test
  // is platform-independent, but the fixture is only buildable on Linux.
  ignore: Deno.build.os !== "linux",
  permissions: { read: true, write: true, env: true, run: true },
  async fn() {
    const tmp = mkdtempSync(join(tmpdir(), "glob-non-utf8-"));
    try {
      // Create a file whose name is the single non-UTF-8 byte 0xE9.
      // We use bash + printf so the raw byte reaches the kernel as-is;
      // passing a Buffer path through Deno's fs APIs would lossily
      // decode it to U+FFFD before the file was created.
      const cmd = new Deno.Command("/bin/bash", {
        args: ["-c", `touch "$(printf '\\xe9')"`],
        cwd: tmp,
      });
      const { code } = await cmd.output();
      assertEquals(code, 0);

      const results = globSync("*", { cwd: tmp });
      assertEquals(results.length, 1);
      // The non-UTF-8 byte is surfaced via Unicode replacement (U+FFFD).
      assertEquals(results[0], "�");
    } finally {
      rmSync(tmp, { recursive: true, force: true });
    }
  },
});

Deno.test({
  name: "[node/fs] watch recursive returns relative path for nested files",
  ignore: Deno.build.os === "windows",
  async fn() {
    const tmp = Deno.makeTempDirSync();
    const subdir = join(tmp, "sub");
    mkdirSync(subdir, { recursive: true });

    try {
      const filenames: string[] = [];
      const { promise, resolve } = Promise.withResolvers<void>();

      const watcher = watch(
        tmp,
        { recursive: true },
        (_event: string, filename: string | null) => {
          if (filename) filenames.push(filename);
          if (filenames.length >= 1) resolve();
        },
      );

      // Small delay to let the watcher start
      await new Promise((r) => setTimeout(r, 100));
      writeFileSync(join(subdir, "test.txt"), "hello");

      await promise;
      watcher.close();

      // macOS FSEvents only delivers directory-granularity events for
      // recursive watches, so the filename may be "sub" instead of
      // "sub/test.txt". Both are correct relative paths.
      const hasRelative = filenames.some((f) => f.startsWith("sub"));
      assert(
        hasRelative,
        `Expected relative path starting with "sub", got: ${filenames}`,
      );
    } finally {
      rmSync(tmp, { recursive: true, force: true });
    }
  },
});

// Regression test for #34396: fs.watch on a non-existent path used to
// throw a raw `Deno.errors.NotFound` synchronously, which left chokidar/
// vite (and any other EventEmitter-style consumer) unable to recover from
// transient races (e.g. an editor's atomic-save temp file getting renamed
// away before the inotify watch is added). The error should instead be
// delivered as a Node-style ENOENT on the watcher's 'error' event.
Deno.test({
  name: "[node/fs] watch surfaces NotFound as 'error' event with ENOENT",
  async fn() {
    const missing = join(
      tmpdir(),
      `deno-watch-missing-${Date.now()}-${Math.random()}`,
    );

    const { promise, resolve, reject } = Promise.withResolvers<Error>();
    let watcher: ReturnType<typeof watch>;
    try {
      watcher = watch(missing, () => {});
    } catch (e) {
      reject(
        new Error(
          `watch threw synchronously instead of emitting 'error': ${e}`,
        ),
      );
      return;
    }
    watcher.on("error", resolve);
    const timeoutId = setTimeout(
      () => reject(new Error("watcher never emitted 'error'")),
      5000,
    );

    try {
      const err = await promise as NodeJS.ErrnoException;
      assertEquals(err.code, "ENOENT");
      assertEquals(err.syscall, "watch");
      assertEquals(err.path, missing);
    } finally {
      clearTimeout(timeoutId);
      watcher.close();
    }
  },
});

Deno.test(
  {
    name: "[node/fs] readFile on non-terminating source respects AbortSignal",
    ignore: Deno.build.os === "windows",
  },
  async () => {
    const controller = new AbortController();
    setTimeout(() => controller.abort(), 500);

    await assertRejects(
      () =>
        readFile("/dev/urandom", {
          encoding: "utf8",
          signal: controller.signal,
        }),
      Error,
      "abort",
    );
  },
);

// Regression test for #34335: `paths.map(fs.promises.unlink)` passes
// `(elem, index, array)` to unlink. The promisify wrapper used to forward all
// three to callback-style `fs.unlink`, which then read `index` as the callback
// and rejected. Promisified `fs.promises.*` methods must slice extra args.
Deno.test(
  "[node/fs] promises methods drop extra positional args (Array#map use)",
  async () => {
    const tmp = mkdtempSync(join(tmpdir(), "fs-arity-"));
    try {
      const a = join(tmp, "a");
      const b = join(tmp, "b");
      writeFileSync(a, "");
      writeFileSync(b, "");
      await Promise.all([a, b].map(promises.unlink));
      assertEquals(existsSync(a), false);
      assertEquals(existsSync(b), false);
    } finally {
      rmSync(tmp, { recursive: true, force: true });
    }
  },
);
