// Copyright 2018-2026 the Deno authors. MIT license.
import {
  assert,
  assertEquals,
  AssertionError,
  assertIsError,
  assertThrows,
  pathToAbsoluteFileUrl,
} from "./test_util.ts";

function assertMissing(path: string) {
  let caughtErr = false;
  let info;
  try {
    info = Deno.lstatSync(path);
  } catch (e) {
    caughtErr = true;
    assert(e instanceof Deno.errors.NotFound);
  }
  assert(caughtErr);
  assertEquals(info, undefined);
}

function assertFile(path: string) {
  const info = Deno.lstatSync(path);
  assert(info.isFile);
}

function assertDirectory(path: string, mode?: number) {
  const info = Deno.lstatSync(path);
  assert(info.isDirectory);
  if (Deno.build.os !== "windows" && mode !== undefined) {
    assertEquals(info.mode! & 0o777, mode & ~Deno.umask());
  }
}

Deno.test(
  { permissions: { read: true, write: true } },
  function renameSyncSuccess() {
    const testDir = Deno.makeTempDirSync();
    const oldpath = testDir + "/oldpath";
    const newpath = testDir + "/newpath";
    Deno.mkdirSync(oldpath);
    Deno.renameSync(oldpath, newpath);
    assertDirectory(newpath);
    assertMissing(oldpath);
  },
);

Deno.test(
  { permissions: { read: true, write: true } },
  function renameSyncWithURL() {
    const testDir = Deno.makeTempDirSync();
    const oldpath = testDir + "/oldpath";
    const newpath = testDir + "/newpath";
    Deno.mkdirSync(oldpath);
    Deno.renameSync(
      pathToAbsoluteFileUrl(oldpath),
      pathToAbsoluteFileUrl(newpath),
    );
    assertDirectory(newpath);
    assertMissing(oldpath);
  },
);

Deno.test(
  { permissions: { read: false, write: true } },
  function renameSyncReadPerm() {
    assertThrows(() => {
      const oldpath = "/oldbaddir";
      const newpath = "/newbaddir";
      Deno.renameSync(oldpath, newpath);
    }, Deno.errors.NotCapable);
  },
);

Deno.test(
  { permissions: { read: true, write: false } },
  function renameSyncWritePerm() {
    assertThrows(() => {
      const oldpath = "/oldbaddir";
      const newpath = "/newbaddir";
      Deno.renameSync(oldpath, newpath);
    }, Deno.errors.NotCapable);
  },
);

Deno.test(
  { permissions: { read: true, write: true } },
  async function renameSuccess() {
    const testDir = Deno.makeTempDirSync();
    const oldpath = testDir + "/oldpath";
    const newpath = testDir + "/newpath";
    Deno.mkdirSync(oldpath);
    await Deno.rename(oldpath, newpath);
    assertDirectory(newpath);
    assertMissing(oldpath);
  },
);

Deno.test(
  { permissions: { read: true, write: true } },
  async function renameWithURL() {
    const testDir = Deno.makeTempDirSync();
    const oldpath = testDir + "/oldpath";
    const newpath = testDir + "/newpath";
    Deno.mkdirSync(oldpath);
    await Deno.rename(
      pathToAbsoluteFileUrl(oldpath),
      pathToAbsoluteFileUrl(newpath),
    );
    assertDirectory(newpath);
    assertMissing(oldpath);
  },
);

function readFileString(filename: string): string {
  const dataRead = Deno.readFileSync(filename);
  const dec = new TextDecoder("utf-8");
  return dec.decode(dataRead);
}

function writeFileString(filename: string, s: string) {
  const enc = new TextEncoder();
  const data = enc.encode(s);
  Deno.writeFileSync(filename, data, { mode: 0o666 });
}

Deno.test(
  {
    ignore: Deno.build.os === "windows",
    permissions: { read: true, write: true },
  },
  function renameSyncErrorsUnix() {
    const testDir = Deno.makeTempDirSync();
    const oldfile = testDir + "/oldfile";
    const olddir = testDir + "/olddir";
    const emptydir = testDir + "/empty";
    const fulldir = testDir + "/dir";
    const file = fulldir + "/file";
    writeFileString(oldfile, "Hello");
    Deno.mkdirSync(olddir);
    Deno.mkdirSync(emptydir);
    Deno.mkdirSync(fulldir);
    writeFileString(file, "world");

    assertThrows(
      () => {
        Deno.renameSync(oldfile, emptydir);
      },
      Error,
      "Is a directory",
    );
    try {
      assertThrows(
        () => {
          Deno.renameSync(olddir, fulldir);
        },
        Error,
        "Directory not empty",
      );
    } catch (e) {
      // rename syscall may also return EEXIST, e.g. with XFS
      assertIsError(
        e,
        AssertionError,
        `Expected error message to include "Directory not empty", but got "File exists`,
      );
    }
    assertThrows(
      () => {
        Deno.renameSync(olddir, file);
      },
      Error,
      "Not a directory",
    );
    assertThrows(
      () => {
        Deno.renameSync(olddir, file);
      },
      Error,
      `rename '${olddir}' -> '${file}'`,
    );

    const fileLink = testDir + "/fileLink";
    const dirLink = testDir + "/dirLink";
    const danglingLink = testDir + "/danglingLink";
    Deno.symlinkSync(file, fileLink);
    Deno.symlinkSync(emptydir, dirLink);
    Deno.symlinkSync(testDir + "/nonexistent", danglingLink);

    assertThrows(
      () => {
        Deno.renameSync(olddir, fileLink);
      },
      Error,
      "Not a directory",
    );
    assertThrows(
      () => {
        Deno.renameSync(olddir, dirLink);
      },
      Error,
      "Not a directory",
    );
    assertThrows(
      () => {
        Deno.renameSync(olddir, danglingLink);
      },
      Error,
      "Not a directory",
    );

    // should succeed on Unix
    Deno.renameSync(olddir, emptydir);
    Deno.renameSync(oldfile, dirLink);
    Deno.renameSync(dirLink, danglingLink);
    assertFile(danglingLink);
    assertEquals("Hello", readFileString(danglingLink));
  },
);

Deno.test(
  {
    ignore: Deno.build.os !== "windows",
    permissions: { read: true, write: true },
  },
  function renameSyncErrorsWin() {
    const testDir = Deno.makeTempDirSync();
    const oldfile = testDir + "/oldfile";
    const olddir = testDir + "/olddir";
    const emptydir = testDir + "/empty";
    const fulldir = testDir + "/dir";
    const file = fulldir + "/file";
    writeFileString(oldfile, "Hello");
    Deno.mkdirSync(olddir);
    Deno.mkdirSync(emptydir);
    Deno.mkdirSync(fulldir);
    writeFileString(file, "world");

    assertThrows(
      () => {
        Deno.renameSync(oldfile, emptydir);
      },
      Deno.errors.PermissionDenied,
      "Access is denied",
    );
    assertThrows(
      () => {
        Deno.renameSync(olddir, fulldir);
      },
      Error,
      "The directory is not empty",
    );

    // should succeed on Windows
    Deno.renameSync(olddir, emptydir);
    assertDirectory(emptydir);
  },
);
