// Copyright 2018-2026 the Deno authors. MIT license.
import {
  assert,
  assertEquals,
  assertMatch,
  assertRejects,
  assertThrows,
} from "@std/assert";
import { fromFileUrl, join, joinGlobs, normalize, relative } from "@std/path";
import {
  expandGlob,
  type ExpandGlobOptions,
  expandGlobSync,
} from "./expand_glob.ts";

async function expandGlobArray(
  globString: string,
  options: ExpandGlobOptions,
  { forceRoot = "" } = {},
): Promise<string[]> {
  const paths = await Array.fromAsync(
    expandGlob(globString, options),
    ({ path }) => path,
  );
  paths.sort();
  const root = normalize(forceRoot || options.root || Deno.cwd());
  for (const path of paths) {
    assert(path.startsWith(root));
  }
  const relativePaths = paths.map(
    (path: string): string => relative(root, path) || ".",
  );
  relativePaths.sort();
  return relativePaths;
}

function expandGlobSyncArray(
  globString: string,
  options: ExpandGlobOptions,
  { forceRoot = "" } = {},
): string[] {
  const pathsSync = [...expandGlobSync(globString, options)].map(
    ({ path }): string => path,
  );
  pathsSync.sort();
  const root = normalize(forceRoot || options.root || Deno.cwd());
  for (const path of pathsSync) {
    assert(path.startsWith(root));
  }
  const relativePaths = pathsSync.map(
    (path: string): string => relative(root, path) || ".",
  );
  relativePaths.sort();
  return relativePaths;
}

const EG_OPTIONS: ExpandGlobOptions = {
  root: fromFileUrl(new URL(join("testdata", "glob"), import.meta.url)),
  includeDirs: true,
  extended: false,
};

Deno.test("expandGlob() with wildcard input returns all test data", async function () {
  const options = EG_OPTIONS;
  assertEquals(await expandGlobArray("*", options), [
    "a[b]c",
    "abc",
    "abcdef",
    "abcdefghi",
    "link",
    "subdir",
  ]);
});

Deno.test("expandGlobSync() with wildcard input returns all test data", function () {
  const options = EG_OPTIONS;
  assertEquals(expandGlobSyncArray("*", options), [
    "a[b]c",
    "abc",
    "abcdef",
    "abcdefghi",
    "link",
    "subdir",
  ]);
});

Deno.test("expandGlob() excludes items in `exclude` option", async function () {
  const options = { ...EG_OPTIONS, exclude: ["abc"] };
  assertEquals(await expandGlobArray("*", options), [
    "a[b]c",
    "abcdef",
    "abcdefghi",
    "link",
    "subdir",
  ]);
  assertEquals(expandGlobSyncArray("*", options), [
    "a[b]c",
    "abcdef",
    "abcdefghi",
    "link",
    "subdir",
  ]);
});

Deno.test("expandGlob() returns empty array if path doesn't exist", async function () {
  assertEquals(await expandGlobArray("nonexistent", EG_OPTIONS), []);
  assertEquals(expandGlobSyncArray("nonexistent", EG_OPTIONS), []);
});

Deno.test(
  "expandGlob() throws permission error if the runtime doesn't have read permission",
  { permissions: {} },
  async function () {
    {
      await assertRejects(
        async () => {
          await expandGlobArray("*", EG_OPTIONS);
        },
        // deno-lint-ignore no-explicit-any
        (Deno as any).errors.NotCapable ?? Deno.errors.PermissionDenied,
        "run again with the --allow-read flag",
      );
    }

    {
      assertThrows(
        () => {
          expandGlobSyncArray("*", EG_OPTIONS);
        },
        // deno-lint-ignore no-explicit-any
        (Deno as any).errors.NotCapable ?? Deno.errors.PermissionDenied,
        "run again with the --allow-read flag",
      );
    }
  },
);

Deno.test("expandGlob() with */ input returns subdirs", async function () {
  const options = EG_OPTIONS;
  assertEquals(await expandGlobArray("*/", options), [
    "a[b]c",
    "subdir",
  ]);
});

Deno.test("expandGlobSync() with */ input returns subdirs", function () {
  const options = EG_OPTIONS;
  assertEquals(expandGlobSyncArray("*/", options), [
    "a[b]c",
    "subdir",
  ]);
});

Deno.test("expandGlob() with subdir/../* input expands parent", async function () {
  const options = EG_OPTIONS;
  assertEquals(await expandGlobArray("subdir/../*", options), [
    "a[b]c",
    "abc",
    "abcdef",
    "abcdefghi",
    "link",
    "subdir",
  ]);
});

Deno.test("expandGlobSync() with subdir/../* input expands parent", function () {
  const options = EG_OPTIONS;
  assertEquals(expandGlobSyncArray("subdir/../*", options), [
    "a[b]c",
    "abc",
    "abcdef",
    "abcdefghi",
    "link",
    "subdir",
  ]);
});

Deno.test("expandGlob() accepts extended option set as true", async function () {
  const options = { ...EG_OPTIONS, extended: true };
  assertEquals(await expandGlobArray("abc?(def|ghi)", options), [
    "abc",
    "abcdef",
  ]);
  assertEquals(await expandGlobArray("abc*(def|ghi)", options), [
    "abc",
    "abcdef",
    "abcdefghi",
  ]);
  assertEquals(await expandGlobArray("abc+(def|ghi)", options), [
    "abcdef",
    "abcdefghi",
  ]);
  assertEquals(await expandGlobArray("abc@(def)", options), ["abcdef"]);
  assertEquals(await expandGlobArray("abc@(def|ghi)", options), ["abcdef"]);
  assertEquals(await expandGlobArray("abc{def}", options), ["abcdef"]);
  assertEquals(await expandGlobArray("abc{def,ghi}", options), ["abcdef"]);
  assertEquals(await expandGlobArray("abc!(def|ghi)", options), ["abc"]);
});

Deno.test("expandGlobSync() accepts extended option set as true", function () {
  const options = { ...EG_OPTIONS, extended: true };
  assertEquals(expandGlobSyncArray("abc?(def|ghi)", options), [
    "abc",
    "abcdef",
  ]);
  assertEquals(expandGlobSyncArray("abc*(def|ghi)", options), [
    "abc",
    "abcdef",
    "abcdefghi",
  ]);
  assertEquals(expandGlobSyncArray("abc+(def|ghi)", options), [
    "abcdef",
    "abcdefghi",
  ]);
  assertEquals(expandGlobSyncArray("abc@(def)", options), ["abcdef"]);
  assertEquals(expandGlobSyncArray("abc@(def|ghi)", options), ["abcdef"]);
  assertEquals(expandGlobSyncArray("abc{def}", options), ["abcdef"]);
  assertEquals(expandGlobSyncArray("abc{def,ghi}", options), ["abcdef"]);
  assertEquals(expandGlobSyncArray("abc!(def|ghi)", options), ["abc"]);
});

Deno.test("expandGlob() with globstar returns all dirs", async function () {
  const options = { ...EG_OPTIONS };
  assertEquals(
    await expandGlobArray("**/abc", options),
    ["abc", join("subdir", "abc")],
  );
});

Deno.test("expandGlobSync() with globstar returns all dirs", function () {
  const options = { ...EG_OPTIONS };
  assertEquals(
    expandGlobSyncArray("**/abc", options),
    ["abc", join("subdir", "abc")],
  );
});

Deno.test("expandGlob() with globstar parent returns all dirs", async function () {
  const options = { ...EG_OPTIONS, globstar: true };
  assertEquals(
    await expandGlobArray(joinGlobs(["subdir", "**", ".."], options), options),
    ["."],
  );
});

Deno.test("expandGlobSync() with globstar parent returns all dirs", function () {
  const options = { ...EG_OPTIONS, globstar: true };
  assertEquals(
    expandGlobSyncArray(joinGlobs(["subdir", "**", ".."], options), options),
    ["."],
  );
});

Deno.test("expandGlob() with globstar parent and globstar option set to false returns current dir", async function () {
  const options = { ...EG_OPTIONS, globstar: false };
  assertEquals(await expandGlobArray("**", options), [
    ".",
    "a[b]c",
    "abc",
    "abcdef",
    "abcdefghi",
    "link",
    "subdir",
  ]);
});

Deno.test("expandGlobSync() with globstar parent and globstar option set to false returns current dir", function () {
  const options = { ...EG_OPTIONS, globstar: false };
  assertEquals(expandGlobSyncArray("**", options), [
    ".",
    "a[b]c",
    "abc",
    "abcdef",
    "abcdefghi",
    "link",
    "subdir",
  ]);
});

Deno.test("expandGlob() accepts includeDirs option set to false", async function () {
  const options = { ...EG_OPTIONS, includeDirs: false };
  assertEquals(await expandGlobArray("subdir", options), []);
});

Deno.test("expandGlobSync() accepts includeDirs option set to false", function () {
  const options = { ...EG_OPTIONS, includeDirs: false };
  assertEquals(expandGlobSyncArray("subdir", options), []);
});

Deno.test(
  "expandGlob() throws permission error without fs permissions",
  async function () {
    const exampleUrl = new URL("testdata/expand_wildcard.js", import.meta.url);
    const command = new Deno.Command(Deno.execPath(), {
      args: [
        "run",
        "--quiet",
        "--no-lock",
        exampleUrl.toString(),
      ],
    });
    const { code, success, stdout, stderr } = await command.output();
    const decoder = new TextDecoder();
    assert(!success);
    assertEquals(code, 1);
    assertEquals(decoder.decode(stdout), "");
    assertMatch(
      decoder.decode(stderr),
      /(NotCapable|PermissionDenied)/,
    );
  },
);

Deno.test("expandGlob() returns single entry when root is not glob", async function () {
  const options = { ...EG_OPTIONS, root: join(EG_OPTIONS.root!, "a[b]c") };
  assertEquals(await expandGlobArray("*", options), ["foo"]);
});

Deno.test("expandGlobSync() returns single entry when root is not glob", function () {
  const options = { ...EG_OPTIONS, root: join(EG_OPTIONS.root!, "a[b]c") };
  assertEquals(expandGlobSyncArray("*", options), ["foo"]);
});

Deno.test("expandGlob() accepts followSymlinks option set to true", async function () {
  const options = {
    ...EG_OPTIONS,
    root: join(EG_OPTIONS.root!, "link"),
    followSymlinks: true,
  };
  assertEquals(await expandGlobArray("*", options), ["abc"]);
});

Deno.test("expandGlobSync() accepts followSymlinks option set to true", function () {
  const options = {
    ...EG_OPTIONS,
    root: join(EG_OPTIONS.root!, "link"),
    followSymlinks: true,
  };
  assertEquals(expandGlobSyncArray("*", options), ["abc"]);
});

Deno.test("expandGlob() accepts followSymlinks option set to true with canonicalize", async function () {
  const options = {
    ...EG_OPTIONS,
    root: join(EG_OPTIONS.root!, "."),
    followSymlinks: true,
  };
  assertEquals(
    await expandGlobArray("**/abc", options),
    ["abc", join("subdir", "abc")],
  );
});

Deno.test("expandGlobSync() accepts followSymlinks option set to true with canonicalize", function () {
  const options = {
    ...EG_OPTIONS,
    root: join(EG_OPTIONS.root!, "."),
    followSymlinks: true,
  };
  assertEquals(
    expandGlobSyncArray("**/abc", options),
    ["abc", join("subdir", "abc")],
  );
});

Deno.test("expandGlob() accepts followSymlinks option set to true without canonicalize", async function () {
  const options = {
    ...EG_OPTIONS,
    root: join(EG_OPTIONS.root!, "."),
    followSymlinks: true,
    canonicalize: false,
  };
  assertEquals(
    await expandGlobArray("**/abc", options),
    ["abc", join("link", "abc"), join("subdir", "abc")],
  );
});

Deno.test("expandGlobSync() accepts followSymlinks option set to true without canonicalize", function () {
  const options = {
    ...EG_OPTIONS,
    root: join(EG_OPTIONS.root!, "."),
    followSymlinks: true,
    canonicalize: false,
  };
  assertEquals(
    expandGlobSyncArray("**/abc", options),
    ["abc", join("link", "abc"), join("subdir", "abc")],
  );
});

Deno.test(
  "expandGlob() does not require read permissions when root path is specified",
  {
    permissions: { read: [EG_OPTIONS.root!] },
  },
  async function () {
    const options = { root: EG_OPTIONS.root! };
    assertEquals(await expandGlobArray("abc", options), ["abc"]);
  },
);

Deno.test(
  "expandGlobSync() does not require read permissions when root path is specified",
  {
    permissions: { read: [EG_OPTIONS.root!] },
  },
  function () {
    const options = { root: EG_OPTIONS.root! };
    assertEquals(expandGlobSyncArray("abc", options), ["abc"]);
  },
);

Deno.test(
  "expandGlob() does not require read permissions when an absolute glob is specified",
  {
    permissions: { read: [EG_OPTIONS.root!] },
  },
  async function () {
    assertEquals(
      await expandGlobArray(`${EG_OPTIONS.root!}/abc`, {}, {
        forceRoot: EG_OPTIONS.root!,
      }),
      ["abc"],
    );
  },
);

Deno.test(
  "expandGlobSync() does not require read permissions when an absolute glob is specified",
  {
    permissions: { read: [EG_OPTIONS.root!] },
  },
  function () {
    assertEquals(
      expandGlobSyncArray(`${EG_OPTIONS.root!}/abc`, {}, {
        forceRoot: EG_OPTIONS.root!,
      }),
      ["abc"],
    );
  },
);

const escapeChar = Deno.build.os === "windows" ? "`" : "\\";

Deno.test("expandGlob() finds directory with escaped brackets", async function () {
  assertEquals(
    await expandGlobArray(`a${escapeChar}[b${escapeChar}]c`, EG_OPTIONS),
    ["a[b]c"],
  );
  assertEquals(
    await expandGlobArray(`a${escapeChar}[b${escapeChar}]c/fo[o]`, EG_OPTIONS),
    [join("a[b]c", "foo")],
  );
});

Deno.test("expandGlobSync() finds directory with escaped brackets", function () {
  assertEquals(
    expandGlobSyncArray(`a${escapeChar}[b${escapeChar}]c`, EG_OPTIONS),
    ["a[b]c"],
  );
  assertEquals(
    expandGlobSyncArray(`a${escapeChar}[b${escapeChar}]c/fo[o]`, EG_OPTIONS),
    [join("a[b]c", "foo")],
  );
});
