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

import {
  assert,
  assertEquals,
  assertRejects,
  assertThrows,
} from "./test_util.ts";
import { copy } from "@std/io/copy";

// Note tests for Deno.FsFile.setRaw is in integration tests.

Deno.test(function filesStdioFileDescriptors() {
  // @ts-ignore `Deno.stdin.rid` was soft-removed in Deno 2.
  assertEquals(Deno.stdin.rid, 0);
  // @ts-ignore `Deno.stdout.rid` was soft-removed in Deno 2.
  assertEquals(Deno.stdout.rid, 1);
  // @ts-ignore `Deno.stderr.rid` was soft-removed in Deno 2.
  assertEquals(Deno.stderr.rid, 2);
});

Deno.test(
  { permissions: { read: true } },
  async function filesCopyToStdout() {
    const filename = "tests/testdata/assets/fixture.json";
    using file = await Deno.open(filename);
    assert(file instanceof Deno.FsFile);
    const bytesWritten = await copy(file, Deno.stdout);
    const fileSize = Deno.statSync(filename).size;
    assertEquals(bytesWritten, fileSize);
  },
);

Deno.test(
  {
    permissions: { read: true, write: true, sys: ["umask"] },
  },
  function openSyncMode() {
    const path = Deno.makeTempDirSync() + "/test_openSync.txt";
    using _file = Deno.openSync(path, {
      write: true,
      createNew: true,
      mode: 0o626,
    });
    const pathInfo = Deno.statSync(path);
    if (Deno.build.os !== "windows") {
      assertEquals(pathInfo.mode! & 0o777, 0o626 & ~Deno.umask());
    }
  },
);

Deno.test(
  {
    permissions: { read: true, write: true, sys: ["umask"] },
  },
  async function openMode() {
    const path = (await Deno.makeTempDir()) + "/test_open.txt";
    using _file = await Deno.open(path, {
      write: true,
      createNew: true,
      mode: 0o626,
    });
    const pathInfo = Deno.statSync(path);
    if (Deno.build.os !== "windows") {
      assertEquals(pathInfo.mode! & 0o777, 0o626 & ~Deno.umask());
    }
  },
);

Deno.test(
  {
    permissions: { read: true, write: true, sys: ["umask"] },
  },
  function openSyncUrl() {
    const tempDir = Deno.makeTempDirSync();
    const fileUrl = new URL(
      `file://${
        Deno.build.os === "windows" ? "/" : ""
      }${tempDir}/test_open.txt`,
    );
    using _file = Deno.openSync(fileUrl, {
      write: true,
      createNew: true,
      mode: 0o626,
    });
    const pathInfo = Deno.statSync(fileUrl);
    if (Deno.build.os !== "windows") {
      assertEquals(pathInfo.mode! & 0o777, 0o626 & ~Deno.umask());
    }

    Deno.removeSync(tempDir, { recursive: true });
  },
);

Deno.test(
  {
    permissions: { read: true, write: true, sys: ["umask"] },
  },
  async function openUrl() {
    const tempDir = await Deno.makeTempDir();
    const fileUrl = new URL(
      `file://${
        Deno.build.os === "windows" ? "/" : ""
      }${tempDir}/test_open.txt`,
    );
    using _file = await Deno.open(fileUrl, {
      write: true,
      createNew: true,
      mode: 0o626,
    });
    const pathInfo = Deno.statSync(fileUrl);
    if (Deno.build.os !== "windows") {
      assertEquals(pathInfo.mode! & 0o777, 0o626 & ~Deno.umask());
    }

    Deno.removeSync(tempDir, { recursive: true });
  },
);

Deno.test(
  { permissions: { write: false } },
  async function writePermFailure() {
    const filename = "tests/hello.txt";
    const openOptions: Deno.OpenOptions[] = [{ write: true }, { append: true }];
    for (const options of openOptions) {
      await assertRejects(async () => {
        await Deno.open(filename, options);
      }, Deno.errors.NotCapable);
    }
  },
);

Deno.test(async function openOptions() {
  const filename = "tests/testdata/assets/fixture.json";
  await assertRejects(
    async () => {
      await Deno.open(filename, { write: false });
    },
    Error,
    "'options' requires at least one option to be true",
  );

  await assertRejects(
    async () => {
      await Deno.open(filename, { truncate: true, write: false });
    },
    Error,
    "'truncate' option requires 'write' to be true",
  );

  await assertRejects(
    async () => {
      await Deno.open(filename, { create: true, write: false });
    },
    Error,
    "'create' or 'createNew' options require 'write' or 'append' to be true",
  );

  await assertRejects(
    async () => {
      await Deno.open(filename, { createNew: true, append: false });
    },
    Error,
    "'create' or 'createNew' options require 'write' or 'append' to be true",
  );
});

Deno.test({ permissions: { read: false } }, async function readPermFailure() {
  await assertRejects(async () => {
    await Deno.open("package.json", { read: true });
  }, Deno.errors.NotCapable);
});

Deno.test(
  { permissions: { write: true } },
  async function writeNullBufferFailure() {
    const tempDir = Deno.makeTempDirSync();
    const filename = tempDir + "hello.txt";
    const w = {
      write: true,
      truncate: true,
      create: true,
    };
    using file = await Deno.open(filename, w);

    // writing null should throw an error
    await assertRejects(
      async () => {
        // deno-lint-ignore no-explicit-any
        await file.write(null as any);
      },
    ); // TODO(bartlomieju): Check error kind when dispatch_minimal pipes errors properly
    await Deno.remove(tempDir, { recursive: true });
  },
);

Deno.test(
  { permissions: { write: true, read: true } },
  async function readNullBufferFailure() {
    const tempDir = Deno.makeTempDirSync();
    const filename = tempDir + "hello.txt";
    using file = await Deno.open(filename, {
      read: true,
      write: true,
      truncate: true,
      create: true,
    });

    // reading into an empty buffer should return 0 immediately
    const bytesRead = await file.read(new Uint8Array(0));
    assert(bytesRead === 0);

    // reading file into null buffer should throw an error
    await assertRejects(async () => {
      // deno-lint-ignore no-explicit-any
      await file.read(null as any);
    }, TypeError);
    // TODO(bartlomieju): Check error kind when dispatch_minimal pipes errors properly

    await Deno.remove(tempDir, { recursive: true });
  },
);

Deno.test(
  { permissions: { write: false, read: false } },
  async function readWritePermFailure() {
    const filename = "tests/hello.txt";
    await assertRejects(async () => {
      await Deno.open(filename, { read: true });
    }, Deno.errors.NotCapable);
  },
);

Deno.test(
  { permissions: { write: true, read: true } },
  async function openNotFound() {
    await assertRejects(
      async () => {
        await Deno.open("bad_file_name");
      },
      Deno.errors.NotFound,
      `open 'bad_file_name'`,
    );
  },
);

Deno.test(
  { permissions: { write: true, read: true } },
  function openSyncNotFound() {
    assertThrows(
      () => {
        Deno.openSync("bad_file_name");
      },
      Deno.errors.NotFound,
      `open 'bad_file_name'`,
    );
  },
);

Deno.test(
  { permissions: { read: true, write: true } },
  async function createFile() {
    const tempDir = await Deno.makeTempDir();
    const filename = tempDir + "/test.txt";
    const f = await Deno.create(filename);
    let fileInfo = Deno.statSync(filename);
    assert(fileInfo.isFile);
    assert(fileInfo.size === 0);
    const enc = new TextEncoder();
    const data = enc.encode("Hello");
    await f.write(data);
    fileInfo = Deno.statSync(filename);
    assert(fileInfo.size === 5);
    f.close();

    // TODO(bartlomieju): test different modes
    await Deno.remove(tempDir, { recursive: true });
  },
);

Deno.test(
  { permissions: { read: true, write: true } },
  async function createFileWithUrl() {
    const tempDir = await Deno.makeTempDir();
    const fileUrl = new URL(
      `file://${Deno.build.os === "windows" ? "/" : ""}${tempDir}/test.txt`,
    );
    const f = await Deno.create(fileUrl);
    let fileInfo = Deno.statSync(fileUrl);
    assert(fileInfo.isFile);
    assert(fileInfo.size === 0);
    const enc = new TextEncoder();
    const data = enc.encode("Hello");
    await f.write(data);
    fileInfo = Deno.statSync(fileUrl);
    assert(fileInfo.size === 5);
    f.close();

    await Deno.remove(tempDir, { recursive: true });
  },
);

Deno.test(
  { permissions: { read: true, write: true } },
  async function createSyncFile() {
    const tempDir = await Deno.makeTempDir();
    const filename = tempDir + "/test.txt";
    const f = Deno.createSync(filename);
    let fileInfo = Deno.statSync(filename);
    assert(fileInfo.isFile);
    assert(fileInfo.size === 0);
    const enc = new TextEncoder();
    const data = enc.encode("Hello");
    await f.write(data);
    fileInfo = Deno.statSync(filename);
    assert(fileInfo.size === 5);
    f.close();

    // TODO(bartlomieju): test different modes
    await Deno.remove(tempDir, { recursive: true });
  },
);

Deno.test(
  { permissions: { read: true, write: true } },
  async function createSyncFileWithUrl() {
    const tempDir = await Deno.makeTempDir();
    const fileUrl = new URL(
      `file://${Deno.build.os === "windows" ? "/" : ""}${tempDir}/test.txt`,
    );
    const f = Deno.createSync(fileUrl);
    let fileInfo = Deno.statSync(fileUrl);
    assert(fileInfo.isFile);
    assert(fileInfo.size === 0);
    const enc = new TextEncoder();
    const data = enc.encode("Hello");
    await f.write(data);
    fileInfo = Deno.statSync(fileUrl);
    assert(fileInfo.size === 5);
    f.close();

    await Deno.remove(tempDir, { recursive: true });
  },
);

Deno.test(
  { permissions: { read: true, write: true } },
  async function openModeWrite() {
    const tempDir = Deno.makeTempDirSync();
    const encoder = new TextEncoder();
    const filename = tempDir + "hello.txt";
    const data = encoder.encode("Hello world!\n");
    let file = await Deno.open(filename, {
      create: true,
      write: true,
      truncate: true,
    });
    // assert file was created
    let fileInfo = Deno.statSync(filename);
    assert(fileInfo.isFile);
    assertEquals(fileInfo.size, 0);
    // write some data
    await file.write(data);
    fileInfo = Deno.statSync(filename);
    assertEquals(fileInfo.size, 13);
    // assert we can't read from file
    let thrown = false;
    try {
      const buf = new Uint8Array(20);
      await file.read(buf);
    } catch (_e) {
      thrown = true;
    } finally {
      assert(thrown, "'w' mode shouldn't allow to read file");
    }
    file.close();
    // assert that existing file is truncated on open
    file = await Deno.open(filename, {
      write: true,
      truncate: true,
    });
    file.close();
    const fileSize = Deno.statSync(filename).size;
    assertEquals(fileSize, 0);
    await Deno.remove(tempDir, { recursive: true });
  },
);

Deno.test(
  { permissions: { read: true, write: true } },
  async function openModeWriteRead() {
    const tempDir = Deno.makeTempDirSync();
    const encoder = new TextEncoder();
    const filename = tempDir + "hello.txt";
    const data = encoder.encode("Hello world!\n");

    using file = await Deno.open(filename, {
      write: true,
      truncate: true,
      create: true,
      read: true,
    });
    const seekPosition = 0;
    // assert file was created
    let fileInfo = Deno.statSync(filename);
    assert(fileInfo.isFile);
    assertEquals(fileInfo.size, 0);
    // write some data
    await file.write(data);
    fileInfo = Deno.statSync(filename);
    assertEquals(fileInfo.size, 13);

    const buf = new Uint8Array(20);
    // seeking from beginning of a file
    const cursorPosition = await file.seek(seekPosition, Deno.SeekMode.Start);
    assertEquals(seekPosition, cursorPosition);
    const result = await file.read(buf);
    assertEquals(result, 13);

    await Deno.remove(tempDir, { recursive: true });
  },
);

Deno.test({ permissions: { read: true } }, async function seekStart() {
  const filename = "tests/testdata/assets/hello.txt";
  using file = await Deno.open(filename);
  const seekPosition = 6;
  // Deliberately move 1 step forward
  await file.read(new Uint8Array(1)); // "H"
  // Skipping "Hello "
  // seeking from beginning of a file plus seekPosition
  const cursorPosition = await file.seek(seekPosition, Deno.SeekMode.Start);
  assertEquals(seekPosition, cursorPosition);
  const buf = new Uint8Array(6);
  await file.read(buf);
  const decoded = new TextDecoder().decode(buf);
  assertEquals(decoded, "world!");
});

Deno.test({ permissions: { read: true } }, async function seekStartBigInt() {
  const filename = "tests/testdata/assets/hello.txt";
  using file = await Deno.open(filename);
  const seekPosition = 6n;
  // Deliberately move 1 step forward
  await file.read(new Uint8Array(1)); // "H"
  // Skipping "Hello "
  // seeking from beginning of a file plus seekPosition
  const cursorPosition = await file.seek(seekPosition, Deno.SeekMode.Start);
  assertEquals(seekPosition, BigInt(cursorPosition));
  const buf = new Uint8Array(6);
  await file.read(buf);
  const decoded = new TextDecoder().decode(buf);
  assertEquals(decoded, "world!");
});

Deno.test({ permissions: { read: true } }, function seekSyncStart() {
  const filename = "tests/testdata/assets/hello.txt";
  using file = Deno.openSync(filename);
  const seekPosition = 6;
  // Deliberately move 1 step forward
  file.readSync(new Uint8Array(1)); // "H"
  // Skipping "Hello "
  // seeking from beginning of a file plus seekPosition
  const cursorPosition = file.seekSync(seekPosition, Deno.SeekMode.Start);
  assertEquals(seekPosition, cursorPosition);
  const buf = new Uint8Array(6);
  file.readSync(buf);
  const decoded = new TextDecoder().decode(buf);
  assertEquals(decoded, "world!");
});

Deno.test({ permissions: { read: true } }, async function seekCurrent() {
  const filename = "tests/testdata/assets/hello.txt";
  using file = await Deno.open(filename);
  // Deliberately move 1 step forward
  await file.read(new Uint8Array(1)); // "H"
  // Skipping "ello "
  const seekPosition = 5;
  // seekPosition is relative to current cursor position after read
  const cursorPosition = await file.seek(seekPosition, Deno.SeekMode.Current);
  assertEquals(seekPosition + 1, cursorPosition);
  const buf = new Uint8Array(6);
  await file.read(buf);
  const decoded = new TextDecoder().decode(buf);
  assertEquals(decoded, "world!");
});

Deno.test({ permissions: { read: true } }, function seekSyncCurrent() {
  const filename = "tests/testdata/assets/hello.txt";
  using file = Deno.openSync(filename);
  // Deliberately move 1 step forward
  file.readSync(new Uint8Array(1)); // "H"
  // Skipping "ello "
  const seekPosition = 5;
  // seekPosition is relative to current cursor position after read
  const cursorPosition = file.seekSync(seekPosition, Deno.SeekMode.Current);
  assertEquals(seekPosition + 1, cursorPosition);
  const buf = new Uint8Array(6);
  file.readSync(buf);
  const decoded = new TextDecoder().decode(buf);
  assertEquals(decoded, "world!");
});

Deno.test({ permissions: { read: true } }, async function seekEnd() {
  const filename = "tests/testdata/assets/hello.txt";
  using file = await Deno.open(filename);
  const seekPosition = -6;
  // seek from end of file that has 12 chars, 12 - 6  = 6
  const cursorPosition = await file.seek(seekPosition, Deno.SeekMode.End);
  assertEquals(6, cursorPosition);
  const buf = new Uint8Array(6);
  await file.read(buf);
  const decoded = new TextDecoder().decode(buf);
  assertEquals(decoded, "world!");
});

Deno.test({ permissions: { read: true } }, function seekSyncEnd() {
  const filename = "tests/testdata/assets/hello.txt";
  using file = Deno.openSync(filename);
  const seekPosition = -6;
  // seek from end of file that has 12 chars, 12 - 6  = 6
  const cursorPosition = file.seekSync(seekPosition, Deno.SeekMode.End);
  assertEquals(6, cursorPosition);
  const buf = new Uint8Array(6);
  file.readSync(buf);
  const decoded = new TextDecoder().decode(buf);
  assertEquals(decoded, "world!");
});

Deno.test({ permissions: { read: true } }, async function seekMode() {
  const filename = "tests/testdata/assets/hello.txt";
  using file = await Deno.open(filename);
  await assertRejects(
    async () => {
      await file.seek(1, -1 as unknown as Deno.SeekMode);
    },
    TypeError,
    "Invalid seek mode",
  );

  // We should still be able to read the file
  // since it is still open.
  const buf = new Uint8Array(1);
  await file.read(buf); // "H"
  assertEquals(new TextDecoder().decode(buf), "H");
});

Deno.test(
  { permissions: { read: true, write: true } },
  function fileTruncateSyncSuccess() {
    const filename = Deno.makeTempDirSync() + "/test_fileTruncateSync.txt";
    using file = Deno.openSync(filename, {
      create: true,
      read: true,
      write: true,
    });

    file.truncateSync(20);
    assertEquals(Deno.readFileSync(filename).byteLength, 20);
    file.truncateSync(5);
    assertEquals(Deno.readFileSync(filename).byteLength, 5);
    file.truncateSync(-5);
    assertEquals(Deno.readFileSync(filename).byteLength, 0);

    Deno.removeSync(filename);
  },
);

Deno.test(
  { permissions: { read: true, write: true } },
  async function fileTruncateSuccess() {
    const filename = Deno.makeTempDirSync() + "/test_fileTruncate.txt";
    using file = await Deno.open(filename, {
      create: true,
      read: true,
      write: true,
    });

    await file.truncate(20);
    assertEquals((await Deno.readFile(filename)).byteLength, 20);
    await file.truncate(5);
    assertEquals((await Deno.readFile(filename)).byteLength, 5);
    await file.truncate(-5);
    assertEquals((await Deno.readFile(filename)).byteLength, 0);

    await Deno.remove(filename);
  },
);

Deno.test({ permissions: { read: true } }, function fileStatSyncSuccess() {
  using file = Deno.openSync("README.md");
  const fileInfo = file.statSync();
  assert(fileInfo.isFile);
  assert(!fileInfo.isSymlink);
  assert(!fileInfo.isDirectory);
  assert(fileInfo.size);
  assert(fileInfo.atime);
  assert(fileInfo.mtime);
  // The `birthtime` field is not available on Linux before kernel version 4.11.
  assert(fileInfo.birthtime || Deno.build.os === "linux");
});

Deno.test(async function fileStatSuccess() {
  using file = await Deno.open("README.md");
  const fileInfo = await file.stat();
  assert(fileInfo.isFile);
  assert(!fileInfo.isSymlink);
  assert(!fileInfo.isDirectory);
  assert(fileInfo.size);
  assert(fileInfo.atime);
  assert(fileInfo.mtime);
  // The `birthtime` field is not available on Linux before kernel version 4.11.
  assert(fileInfo.birthtime || Deno.build.os === "linux");
});

Deno.test({ permissions: { read: true } }, async function readableStream() {
  const filename = "tests/testdata/assets/hello.txt";
  const file = await Deno.open(filename);
  assert(file.readable instanceof ReadableStream);
  const chunks = [];
  for await (const chunk of file.readable) {
    chunks.push(chunk);
  }
  assertEquals(chunks.length, 1);
  assertEquals(chunks[0].byteLength, 12);
});

Deno.test(
  {
    permissions: { read: true },
    sanitizeOps: true,
    sanitizeResources: true,
  },
  async function readableStreamTakesOwnership() {
    // Consuming `file.readable` to completion takes ownership of the file and
    // closes it automatically, so the resource and op sanitizers should not
    // report a leak even though `file` is never closed manually.
    const filename = "tests/testdata/assets/hello.txt";
    const file = await Deno.open(filename);
    const chunks = await Array.fromAsync(file.readable);
    assertEquals(chunks.length, 1);
  },
);

Deno.test(
  { permissions: { read: true } },
  async function readableStreamClosedResourceErrorMessage() {
    // Regression test for https://github.com/denoland/deno/issues/34958 —
    // closing the file (e.g. via a `using` declaration or an explicit
    // `.close()`) while its `.readable` stream is still being consumed should
    // surface an actionable error rather than a terse "Bad resource ID".
    const filename = "tests/testdata/assets/hello.txt";
    const file = await Deno.open(filename);
    const reader = file.readable.getReader();
    file.close();
    await assertRejects(
      () => reader.read(),
      Error,
      "The stream's underlying resource was closed or consumed",
    );
  },
);

Deno.test(
  { permissions: { read: true, write: true } },
  async function writableStreamClosedResourceErrorMessage() {
    // Regression test for https://github.com/denoland/deno/issues/34958 —
    // the writable counterpart of the case above.
    const tempFile = await Deno.makeTempFile();
    try {
      const file = await Deno.open(tempFile, { write: true });
      const writer = file.writable.getWriter();
      file.close();
      // The file's writable stream buffers small writes (64 KiB), so write a
      // chunk large enough to be flushed to the underlying resource directly.
      // The flush errors the stream; the error surfaces on `writer.closed`.
      await assertRejects(
        async () => {
          await writer.write(new Uint8Array(128 * 1024)).catch(() => {});
          await writer.closed;
        },
        Error,
        "The stream's underlying resource was closed or consumed",
      );
    } finally {
      await Deno.remove(tempFile);
    }
  },
);

Deno.test(
  { permissions: { read: true } },
  async function readableStreamTextEncoderPipe() {
    const filename = "tests/testdata/assets/hello.txt";
    const file = await Deno.open(filename);
    const readable = file.readable.pipeThrough(new TextDecoderStream());
    const chunks = [];
    for await (const chunk of readable) {
      chunks.push(chunk);
    }
    assertEquals(chunks.length, 1);
    assertEquals(chunks[0].length, 12);
  },
);

Deno.test(
  {
    permissions: { read: true, write: true, run: true },
    ignore: Deno.build.os === "windows",
  },
  async function readableStreamCancelOnFifo() {
    // Regression test for https://github.com/denoland/deno/issues/21186
    // — a `read` on a FIFO can block indefinitely waiting for data, and
    // previously cancelling the wrapping `ReadableStream` would not
    // unblock the in-flight `op_read`. As a result, the runtime would
    // hang on exit even after JS had finished. We verify the fix by
    // running a subprocess that opens a FIFO, starts a read, cancels
    // it, and is expected to exit promptly. Without the fix the
    // subprocess would hang until killed.
    const tempDir = await Deno.makeTempDir();
    const fifo = `${tempDir}/fifo`;
    try {
      const mkfifo = new Deno.Command("mkfifo", { args: [fifo] }).outputSync();
      if (mkfifo.code !== 0) {
        throw new Error(
          `mkfifo failed: ${new TextDecoder().decode(mkfifo.stderr)}`,
        );
      }
      const script = `
        const fifo = ${JSON.stringify(fifo)};
        // O_RDWR avoids the POSIX FIFO open-blocks-on-peer semantics.
        const file = await Deno.open(fifo, { read: true, write: true });
        const r = file.readable.getReader();
        const reading = r.read();
        await new Promise((res) => setTimeout(res, 50));
        await r.cancel();
        await reading;
      `;
      const out = await new Deno.Command(Deno.execPath(), {
        args: ["eval", script],
        stdout: "null",
        stderr: "piped",
        signal: AbortSignal.timeout(10_000),
      }).output();
      if (out.signal !== null) {
        throw new Error(
          "subprocess hung after stream cancellation — read was not cancelled",
        );
      }
      assertEquals(
        out.code,
        0,
        `subprocess failed: ${new TextDecoder().decode(out.stderr)}`,
      );
    } finally {
      await Deno.remove(tempDir, { recursive: true });
    }
  },
);

Deno.test(
  { permissions: { read: true, write: true } },
  async function writableStream() {
    const path = await Deno.makeTempFile();
    const file = await Deno.open(path, { write: true });
    assert(file.writable instanceof WritableStream);
    const readable = new ReadableStream({
      start(controller) {
        controller.enqueue(new TextEncoder().encode("hello "));
        controller.enqueue(new TextEncoder().encode("world!"));
        controller.close();
      },
    });
    await readable.pipeTo(file.writable);
    const res = await Deno.readTextFile(path);
    assertEquals(res, "hello world!");
  },
);

Deno.test(
  { permissions: { read: true, write: true } },
  async function readTextFileNonUtf8() {
    const path = await Deno.makeTempFile();
    using file = await Deno.open(path, { write: true });
    await file.write(new TextEncoder().encode("hello "));
    await file.write(new Uint8Array([0xC0]));

    const res = await Deno.readTextFile(path);
    const resSync = Deno.readTextFileSync(path);
    assertEquals(res, resSync);
    assertEquals(res, "hello \uFFFD");
  },
);

Deno.test(
  { permissions: { read: true } },
  async function fsFileExplicitResourceManagement() {
    let file2: Deno.FsFile;

    {
      using file = await Deno.open("tests/testdata/assets/hello.txt");
      file2 = file;

      const stat = file.statSync();
      assert(stat.isFile);
    }

    assertThrows(() => file2.statSync(), Deno.errors.BadResource);
  },
);

Deno.test(
  { permissions: { read: true } },
  async function fsFileExplicitResourceManagementManualClose() {
    using file = await Deno.open("tests/testdata/assets/hello.txt");
    file.close();
    assertThrows(() => file.statSync(), Deno.errors.BadResource); // definitely closed
    // calling [Symbol.dispose] after manual close is a no-op
  },
);

Deno.test(
  { permissions: { read: true, write: true } },
  function fsFileDatasyncSyncSuccess() {
    const filename = Deno.makeTempDirSync() + "/test_fdatasyncSync.txt";
    const file = Deno.openSync(filename, {
      read: true,
      write: true,
      create: true,
    });
    const data = new Uint8Array(64);
    file.writeSync(data);
    file.syncDataSync();
    assertEquals(Deno.readFileSync(filename), data);
    file.close();
    Deno.removeSync(filename);
  },
);

Deno.test(
  { permissions: { read: true, write: true } },
  async function fsFileDatasyncSuccess() {
    const filename = (await Deno.makeTempDir()) + "/test_fdatasync.txt";
    const file = await Deno.open(filename, {
      read: true,
      write: true,
      create: true,
    });
    const data = new Uint8Array(64);
    await file.write(data);
    await file.syncData();
    assertEquals(await Deno.readFile(filename), data);
    file.close();
    await Deno.remove(filename);
  },
);

Deno.test(
  { permissions: { read: true, write: true } },
  function fsFileSyncSyncSuccess() {
    const filename = Deno.makeTempDirSync() + "/test_fsyncSync.txt";
    const file = Deno.openSync(filename, {
      read: true,
      write: true,
      create: true,
    });
    const size = 64;
    file.truncateSync(size);
    file.syncSync();
    assertEquals(file.statSync().size, size);
    file.close();
    Deno.removeSync(filename);
  },
);

Deno.test(
  { permissions: { read: true, write: true } },
  async function fsFileSyncSuccess() {
    const filename = (await Deno.makeTempDir()) + "/test_fsync.txt";
    const file = await Deno.open(filename, {
      read: true,
      write: true,
      create: true,
    });
    const size = 64;
    await file.truncate(size);
    await file.sync();
    assertEquals((await file.stat()).size, size);
    file.close();
    await Deno.remove(filename);
  },
);

Deno.test({ permissions: { read: true } }, function fsFileIsTerminal() {
  // CI not under TTY, so cannot test stdin/stdout/stderr.
  using file = Deno.openSync("tests/testdata/assets/hello.txt");
  assert(!file.isTerminal());
});

Deno.test(
  { permissions: { read: true, run: true } },
  async function fsFileLockFileSync() {
    await runFlockTests({ sync: true });
  },
);

Deno.test(
  { permissions: { read: true, run: true } },
  async function fsFileLockFileAsync() {
    await runFlockTests({ sync: false });
  },
);

Deno.test(
  { permissions: { read: true, write: true } },
  function fsFileTryLockSync() {
    const path = Deno.makeTempFileSync();
    try {
      using file1 = Deno.openSync(path, { read: true, write: true });
      using file2 = Deno.openSync(path, { read: true, write: true });

      // First lock should succeed
      assertEquals(file1.tryLockSync(true), true);

      // Second exclusive lock should fail (returns false, not block)
      assertEquals(file2.tryLockSync(true), false);

      // Shared lock should also fail when exclusive is held
      assertEquals(file2.tryLockSync(false), false);

      // Unlock first file
      file1.unlockSync();

      // Now second file should be able to lock
      assertEquals(file2.tryLockSync(true), true);
      file2.unlockSync();
    } finally {
      Deno.removeSync(path);
    }
  },
);

Deno.test(
  { permissions: { read: true, write: true } },
  async function fsFileTryLockAsync() {
    const path = await Deno.makeTempFile();
    try {
      using file1 = await Deno.open(path, { read: true, write: true });
      using file2 = await Deno.open(path, { read: true, write: true });

      // First lock should succeed
      assertEquals(await file1.tryLock(true), true);

      // Second exclusive lock should fail (returns false, not block)
      assertEquals(await file2.tryLock(true), false);

      // Shared lock should also fail when exclusive is held
      assertEquals(await file2.tryLock(false), false);

      // Unlock first file
      await file1.unlock();

      // Now second file should be able to lock
      assertEquals(await file2.tryLock(true), true);
      await file2.unlock();
    } finally {
      await Deno.remove(path);
    }
  },
);

Deno.test(
  { permissions: { read: true, write: true } },
  function fsFileTryLockSharedSync() {
    const path = Deno.makeTempFileSync();
    try {
      using file1 = Deno.openSync(path, { read: true, write: true });
      using file2 = Deno.openSync(path, { read: true, write: true });

      // First shared lock should succeed
      assertEquals(file1.tryLockSync(false), true);

      // Second shared lock should also succeed
      assertEquals(file2.tryLockSync(false), true);

      // Exclusive lock should fail when shared locks are held
      using file3 = Deno.openSync(path, { read: true, write: true });
      assertEquals(file3.tryLockSync(true), false);

      file1.unlockSync();
      file2.unlockSync();
    } finally {
      Deno.removeSync(path);
    }
  },
);

async function runFlockTests(opts: { sync: boolean }) {
  assertEquals(
    await checkFirstBlocksSecond({
      firstExclusive: true,
      secondExclusive: false,
      sync: opts.sync,
    }),
    true,
    "exclusive blocks shared",
  );
  assertEquals(
    await checkFirstBlocksSecond({
      firstExclusive: false,
      secondExclusive: true,
      sync: opts.sync,
    }),
    true,
    "shared blocks exclusive",
  );
  assertEquals(
    await checkFirstBlocksSecond({
      firstExclusive: true,
      secondExclusive: true,
      sync: opts.sync,
    }),
    true,
    "exclusive blocks exclusive",
  );
  assertEquals(
    await checkFirstBlocksSecond({
      firstExclusive: false,
      secondExclusive: false,
      sync: opts.sync,
      // need to wait for both to enter the lock to prevent the case where the
      // first process enters and exits the lock before the second even enters
      waitBothEnteredLock: true,
    }),
    false,
    "shared does not block shared",
  );
}

async function checkFirstBlocksSecond(opts: {
  firstExclusive: boolean;
  secondExclusive: boolean;
  sync: boolean;
  waitBothEnteredLock?: boolean;
}) {
  const firstProcess = runFlockTestProcess({
    exclusive: opts.firstExclusive,
    sync: opts.sync,
  });
  const secondProcess = runFlockTestProcess({
    exclusive: opts.secondExclusive,
    sync: opts.sync,
  });
  try {
    const sleep = (time: number) => new Promise((r) => setTimeout(r, time));

    await Promise.all([
      firstProcess.waitStartup(),
      secondProcess.waitStartup(),
    ]);

    await firstProcess.enterLock();
    await firstProcess.waitEnterLock();

    await secondProcess.enterLock();
    await sleep(100);

    if (!opts.waitBothEnteredLock) {
      await firstProcess.exitLock();
    }

    await secondProcess.waitEnterLock();

    if (opts.waitBothEnteredLock) {
      await firstProcess.exitLock();
    }

    await secondProcess.exitLock();

    // collect the final output
    const firstPsTimes = await firstProcess.getTimes();
    const secondPsTimes = await secondProcess.getTimes();
    return firstPsTimes.exitTime < secondPsTimes.enterTime;
  } finally {
    await firstProcess.close();
    await secondProcess.close();
  }
}

function runFlockTestProcess(opts: { exclusive: boolean; sync: boolean }) {
  const path = "tests/testdata/assets/lock_target.txt";
  const scriptText = `
    const file = Deno.openSync("${path}");

    // ready signal
    Deno.stdout.writeSync(new Uint8Array(1));
    // wait for enter lock signal
    Deno.stdin.readSync(new Uint8Array(1));

    // entering signal
    Deno.stdout.writeSync(new Uint8Array(1));
    // lock and record the entry time
    ${
    opts.sync
      ? `file.lockSync(${opts.exclusive ? "true" : "false"});`
      : `await file.lock(${opts.exclusive ? "true" : "false"});`
  }
    const enterTime = new Date().getTime();
    // entered signal
    Deno.stdout.writeSync(new Uint8Array(1));

    // wait for exit lock signal
    Deno.stdin.readSync(new Uint8Array(1));

    // record the exit time and wait a little bit before releasing
    // the lock so that the enter time of the next process doesn't
    // occur at the same time as this exit time
    const exitTime = new Date().getTime();
    await new Promise(resolve => setTimeout(resolve, 100));

    // release the lock
    ${opts.sync ? "file.unlockSync();" : "await file.unlock();"}

    // exited signal
    Deno.stdout.writeSync(new Uint8Array(1));

    // output the enter and exit time
    console.log(JSON.stringify({ enterTime, exitTime }));
`;

  const process = new Deno.Command(Deno.execPath(), {
    args: ["eval", scriptText],
    stdin: "piped",
    stdout: "piped",
    stderr: "null",
  }).spawn();

  const waitSignal = async () => {
    const reader = process.stdout.getReader({ mode: "byob" });
    await reader.read(new Uint8Array(1));
    reader.releaseLock();
  };
  const signal = async () => {
    const writer = process.stdin.getWriter();
    await writer.write(new Uint8Array(1));
    writer.releaseLock();
  };

  return {
    async waitStartup() {
      await waitSignal();
    },
    async enterLock() {
      await signal();
      await waitSignal(); // entering signal
    },
    async waitEnterLock() {
      await waitSignal();
    },
    async exitLock() {
      await signal();
      await waitSignal();
    },
    getTimes: async () => {
      const { stdout } = await process.output();
      const text = new TextDecoder().decode(stdout);
      return JSON.parse(text) as {
        enterTime: number;
        exitTime: number;
      };
    },
    close: async () => {
      await process.status;
      await process.stdin.close();
    },
  };
}
