// Copyright 2018-2026 the Deno authors. MIT license.
import * as path from "@std/path";
import { Buffer } from "node:buffer";
import * as fs from "node:fs/promises";
import { assert, assertEquals, assertRejects } from "@std/assert";

const moduleDir = path.dirname(path.fromFileUrl(import.meta.url));
const testData = path.resolve(moduleDir, "testdata", "hello.txt");
const decoder = new TextDecoder();

Deno.test("readFileSuccess", async function () {
  const fileHandle = await fs.open(testData);
  const data = await fileHandle.readFile();

  assert(data instanceof Uint8Array);
  assertEquals(decoder.decode(data as Uint8Array), "hello world");

  await fileHandle.close();
});

Deno.test("read", async function () {
  const fileHandle = await fs.open(testData);
  const byteLength = "hello world".length;

  const buf = new Buffer(byteLength);
  await fileHandle.read(buf, 0, byteLength, 0);

  assertEquals(decoder.decode(buf as Uint8Array), "hello world");

  await fileHandle.close();
});

Deno.test("read specify opt", async function () {
  const fileHandle = await fs.open(testData);
  const byteLength = "hello world".length;

  const opt = {
    buffer: new Buffer(byteLength),
    offset: 6,
    length: 5,
    position: 6,
  };
  let res = await fileHandle.read(opt);

  assertEquals(res.bytesRead, 5);
  assertEquals(
    new TextDecoder().decode(res.buffer.subarray(6) as Uint8Array),
    "world",
  );

  const opt2 = {
    buffer: new Buffer(byteLength),
    length: 5,
    position: 0,
  };
  res = await fileHandle.read(opt2);

  assertEquals(res.bytesRead, 5);
  assertEquals(
    decoder.decode(res.buffer.subarray(0, 5) as Uint8Array),
    "hello",
  );

  await fileHandle.close();
});

Deno.test("[node/fs filehandle.write] Write from Buffer", async function () {
  const tempFile: string = await Deno.makeTempFile();
  const fileHandle = await fs.open(tempFile, "a+");

  const buffer = Buffer.from("hello world");
  const res = await fileHandle.write(buffer, 0, 5, 0);

  const data = Deno.readFileSync(tempFile);
  await Deno.remove(tempFile);
  await fileHandle.close();

  assertEquals(res.bytesWritten, 5);
  assertEquals(decoder.decode(data), "hello");
});

Deno.test("[node/fs filehandle.write] Write from string", async function () {
  const tempFile: string = await Deno.makeTempFile();
  const fileHandle = await fs.open(tempFile, "a+");

  const str = "hello world";
  const res = await fileHandle.write(str);

  const data = Deno.readFileSync(tempFile);
  await Deno.remove(tempFile);
  await fileHandle.close();

  assertEquals(res.bytesWritten, 11);
  assertEquals(decoder.decode(data), "hello world");
});

Deno.test("[node/fs filehandle.stat] Get file status", async function () {
  const fileHandle = await fs.open(testData);
  const stat = await fileHandle.stat();

  assertEquals(stat.isFile(), true);
  assertEquals(stat.size, "hello world".length);

  await fileHandle.close();
});

Deno.test("[node/fs filehandle.writeFile] Write to file", async function () {
  const tempFile: string = await Deno.makeTempFile();
  const fileHandle = await fs.open(tempFile, "w");

  const str = "hello world";
  await fileHandle.writeFile(str);

  const data = Deno.readFileSync(tempFile);
  await Deno.remove(tempFile);
  await fileHandle.close();

  assertEquals(decoder.decode(data), "hello world");
});

Deno.test(
  "[node/fs filehandle.writev] Write array of buffers to file",
  async function () {
    const tempFile: string = await Deno.makeTempFile();
    const fileHandle = await fs.open(tempFile, "w");

    const buffer1 = Buffer.from("hello ");
    const buffer2 = Buffer.from("world");
    const res = await fileHandle.writev([buffer1, buffer2]);

    const data = Deno.readFileSync(tempFile);
    await Deno.remove(tempFile);
    await fileHandle.close();

    assertEquals(res.bytesWritten, 11);
    assertEquals(decoder.decode(data), "hello world");
  },
);

Deno.test(
  "[node/fs filehandle.writev] Write array of buffers to file with position",
  async function () {
    const tempFile: string = await Deno.makeTempFile();
    const fileHandle = await fs.open(tempFile, "w");

    const buffer1 = Buffer.from("hello ");
    const buffer2 = Buffer.from("world");
    await fileHandle.writev([buffer1, buffer2], 0);
    const buffer3 = Buffer.from("lorem ipsum");
    await fileHandle.writev([buffer3], 6);

    const data = Deno.readFileSync(tempFile);
    await Deno.remove(tempFile);
    await fileHandle.close();

    assertEquals(decoder.decode(data), "hello lorem ipsum");
  },
);

Deno.test(
  "[node/fs filehandle.readv] Read into array of buffers from file",
  async function () {
    const fileHandle = await fs.open(testData);

    const buffer1 = Buffer.alloc(5);
    const buffer2 = Buffer.alloc(6);
    const res = await fileHandle.readv([buffer1, buffer2]);

    await fileHandle.close();

    assertEquals(res.bytesRead, 11);
    assertEquals(decoder.decode(buffer1), "hello");
    assertEquals(decoder.decode(buffer2), " world");
  },
);

Deno.test(
  "[node/fs filehandle.readv] Read into array of buffers with position",
  async function () {
    const fileHandle = await fs.open(testData);

    const buffer1 = Buffer.alloc(5);
    const res = await fileHandle.readv([buffer1], 6);

    await fileHandle.close();

    assertEquals(res.bytesRead, 5);
    assertEquals(decoder.decode(buffer1), "world");
  },
);

Deno.test(
  "[node/fs filehandle.truncate] Truncate file with length",
  async function () {
    const tempFile: string = await Deno.makeTempFile();
    const fileHandle = await fs.open(tempFile, "w+");

    await fileHandle.writeFile("hello world");

    await fileHandle.truncate(5);

    const data = Deno.readFileSync(tempFile);
    await Deno.remove(tempFile);
    await fileHandle.close();

    assertEquals(decoder.decode(data), "hello");
  },
);

Deno.test(
  "[node/fs filehandle.truncate] Truncate file without length",
  async function () {
    const tempFile: string = await Deno.makeTempFile();
    const fileHandle = await fs.open(tempFile, "w+");

    await fileHandle.writeFile("hello world");

    await fileHandle.truncate();

    const data = Deno.readFileSync(tempFile);
    await Deno.remove(tempFile);
    await fileHandle.close();

    assertEquals(decoder.decode(data), "");
  },
);

Deno.test(
  "[node/fs filehandle.truncate] Truncate file with extension",
  async function () {
    const tempFile: string = await Deno.makeTempFile();
    const fileHandle = await fs.open(tempFile, "w+");

    await fileHandle.writeFile("hi");

    await fileHandle.truncate(5);

    const data = Deno.readFileSync(tempFile);
    await Deno.remove(tempFile);
    await fileHandle.close();

    const expected = new Uint8Array(5);
    expected.set(new TextEncoder().encode("hi"));

    assertEquals(data, expected);
    assertEquals(data.length, 5);
    assertEquals(decoder.decode(data.subarray(0, 2)), "hi");
    // Verify null bytes
    assertEquals(data[2], 0);
    assertEquals(data[3], 0);
    assertEquals(data[4], 0);
  },
);

Deno.test(
  "[node/fs filehandle.truncate] Truncate file with negative length",
  async function () {
    const tempFile: string = await Deno.makeTempFile();
    const fileHandle = await fs.open(tempFile, "w+");

    await fileHandle.writeFile("hello world");

    await fileHandle.truncate(-1);

    const data = Deno.readFileSync(tempFile);
    await Deno.remove(tempFile);
    await fileHandle.close();

    assertEquals(decoder.decode(data), "");
    assertEquals(data.length, 0);
  },
);

Deno.test({
  name: "[node/fs filehandle.chmod] Change the permissions of the file",
  ignore: Deno.build.os === "windows",
  async fn() {
    const fileHandle = await fs.open(testData);

    const readOnly = 0o444;
    await fileHandle.chmod(readOnly.toString(8));
    assertEquals(Deno.statSync(testData).mode! & 0o777, readOnly);

    const readWrite = 0o666;
    await fileHandle.chmod(readWrite.toString(8));
    assertEquals(Deno.statSync(testData).mode! & 0o777, readWrite);

    await fileHandle.close();
  },
});

Deno.test({
  name:
    "[node/fs filehandle.utimes] Change the file system timestamps of the file",
  async fn() {
    const fileHandle = await fs.open(testData, "a");

    // Use whole-second timestamps to avoid sub-millisecond rounding
    // differences between JS Date and filesystem timestamp resolution.
    const now = Math.floor(Date.now() / 1000) * 1000;
    const atime = new Date(now);
    const mtime = new Date(0);

    await fileHandle.utimes(atime, mtime);
    assertEquals(Deno.statSync(testData).atime!.getTime(), atime.getTime());
    assertEquals(Deno.statSync(testData).mtime!.getTime(), mtime.getTime());

    await fileHandle.close();
  },
});

Deno.test({
  name: "[node/fs filehandle.chown] Change owner of the file",
  ignore: Deno.build.os === "windows",
  async fn() {
    const fileHandle = await fs.open(testData);

    const nobodyUid = 65534;
    const nobodyGid = 65534;

    await assertRejects(
      async () => await fileHandle.chown(nobodyUid, nobodyGid),
      Deno.errors.PermissionDenied,
      "Operation not permitted",
    );

    const realUid = Deno.uid() || 1000;
    const realGid = Deno.gid() || 1000;

    await fileHandle.chown(realUid, realGid);

    assertEquals(Deno.statSync(testData).uid, realUid);
    assertEquals(Deno.statSync(testData).gid, realGid);

    await fileHandle.close();
  },
});

Deno.test({
  name:
    "[node/fs filehandle.sync] Request that all data for the open file descriptor is flushed to the storage device",
  async fn() {
    const fileHandle = await fs.open(testData, "r+");

    await fileHandle.datasync();
    await fileHandle.sync();
    const buf = Buffer.from("hello world");
    await fileHandle.write(buf);
    const ret = await fileHandle.read(Buffer.alloc(11), 0, 11, 0);
    assertEquals(ret.bytesRead, 11);
    assertEquals(ret.buffer, buf);
    await fileHandle.close();
  },
});

Deno.test(
  "[node/fs filehandle.createReadStream] Create a read stream",
  async function () {
    const fileHandle = await fs.open(testData);
    const stream = fileHandle.createReadStream();
    const fileSize = (await fileHandle.stat()).size;

    assertEquals(stream.bytesRead, 0);
    assertEquals(stream.readable, true);

    let bytesRead = 0;

    stream.on("open", () => assertEquals(stream.bytesRead, 0));

    stream.on("data", (data) => {
      assertEquals(data instanceof Buffer, true);
      assertEquals((data as Buffer).byteOffset % 8, 0);
      bytesRead += data.length;
      assertEquals(stream.bytesRead, bytesRead);
    });

    stream.on("end", () => {
      assertEquals(stream.bytesRead, fileSize);
      assertEquals(bytesRead, fileSize);
    });

    // Wait for the 'close' event so that the test won't finish prematurely
    // note: stream automatically closes fd once all the data is read
    await new Promise<void>((resolve) => {
      stream.on("close", resolve);
    });
  },
);

Deno.test(
  "[node/fs filehandle.createWriteStream] Create a write stream",
  async function () {
    const tempFile: string = await Deno.makeTempFile();
    try {
      const fileHandle = await fs.open(tempFile, "w");
      const stream = fileHandle.createWriteStream();
      const { promise, resolve } = Promise.withResolvers<void>();
      stream.on("close", resolve);
      stream.end("a\n", "utf8");
      await promise;
      assertEquals(await Deno.readTextFile(tempFile), "a\n");
    } finally {
      await Deno.remove(tempFile);
    }
  },
);

Deno.test(
  "[node/fs filehandle.readableWebStream] Create a readable web stream",
  async function () {
    const fileHandle = await fs.open(testData);
    const webStream = fileHandle.readableWebStream();

    assert(webStream instanceof ReadableStream);
    assertEquals(
      Deno.inspect(webStream),
      'ReadableStream { locked: false, state: "readable", supportsBYOB: true }',
    );

    const reader = webStream.getReader();
    const chunks: Uint8Array[] = [];

    while (true) {
      const { done, value } = await reader.read();
      if (done) break;
      chunks.push(value);
    }

    const result = new Uint8Array(
      chunks.reduce((acc, chunk) => acc + chunk.length, 0),
    );
    let offset = 0;
    for (const chunk of chunks) {
      result.set(chunk, offset);
      offset += chunk.length;
    }

    assertEquals(decoder.decode(result), "hello world");

    await fileHandle.close();
  },
);

Deno.test(
  "[node/fs filehandle.readableWebStream] With autoClose option",
  async function () {
    const fileHandle = await fs.open(testData);
    const { promise: closePromise, resolve: closeResolve } = Promise
      .withResolvers<void>();
    fileHandle.once("close", closeResolve);

    const webStream = fileHandle.readableWebStream({ autoClose: true });

    assert(webStream instanceof ReadableStream);

    const reader = webStream.getReader();
    const chunks: Uint8Array[] = [];

    while (true) {
      const { done, value } = await reader.read();
      if (done) break;
      chunks.push(value);
    }

    const result = new Uint8Array(
      chunks.reduce((acc, chunk) => acc + chunk.length, 0),
    );
    let offset = 0;
    for (const chunk of chunks) {
      result.set(chunk, offset);
      offset += chunk.length;
    }

    assertEquals(decoder.decode(result), "hello world");

    // Wait for autoClose to complete by waiting for the close event
    await closePromise;

    // File should be closed
    assertEquals(fileHandle.fd, -1);
  },
);

Deno.test(
  "[node/fs filehandle.readableWebStream] Throws when called twice",
  async function () {
    const fileHandle = await fs.open(testData);

    // First call should succeed
    const stream = fileHandle.readableWebStream();

    // Second call should throw
    let threw = false;
    try {
      fileHandle.readableWebStream();
    } catch (e) {
      threw = true;
      assert(e instanceof Error);
      assertEquals((e as NodeJS.ErrnoException).code, "ERR_INVALID_STATE");
    }

    assertEquals(threw, true);

    // Cancel the stream to avoid resource leak
    await stream.cancel();
    await fileHandle.close();
  },
);

Deno.test(
  "[node/fs filehandle.readableWebStream] cancel without autoClose keeps fd open",
  async function () {
    const fileHandle = await fs.open(testData);
    const stream = fileHandle.readableWebStream({ autoClose: false });

    // Cancel the stream
    await stream.cancel();

    // fd should still be valid (not -1)
    assert(fileHandle.fd !== -1);

    // FileHandle should still be usable
    const stat = await fileHandle.stat();
    assertEquals(stat.isFile(), true);

    await fileHandle.close();
  },
);

Deno.test(
  "[node/fs filehandle.readableWebStream] cancel with autoClose closes fd",
  async function () {
    const fileHandle = await fs.open(testData);
    const { promise: closePromise, resolve: closeResolve } = Promise
      .withResolvers<void>();
    fileHandle.once("close", closeResolve);

    const stream = fileHandle.readableWebStream({ autoClose: true });

    // Cancel the stream - this should trigger autoClose
    await stream.cancel();

    // Wait for close event
    await closePromise;

    // fd should be -1
    assertEquals(fileHandle.fd, -1);
  },
);

Deno.test(
  "[node/fs filehandle.readableWebStream] close FileHandle while reader is active",
  async function () {
    const fileHandle = await fs.open(testData);
    const stream = fileHandle.readableWebStream();
    const reader = stream.getReader({ mode: "byob" });

    // Close the FileHandle while reader is active
    await fileHandle.close();

    // fd should be -1
    assertEquals(fileHandle.fd, -1);

    // Reader should eventually get done or error
    const buffer = new ArrayBuffer(1024);
    try {
      const result = await reader.read(new Uint8Array(buffer));
      // If no error, should be done
      assertEquals(result.done, true);
    } catch {
      // Error is also acceptable (FileHandle was closed)
    }
  },
);
