import { expect, it } from "bun:test";
import { join, resolve } from "path";

function resolveFrom(from) {
  return specifier => import.meta.resolveSync(specifier, from);
}

it("#imports", async () => {
  const baz = import.meta.resolveSync("#foo", join(import.meta.resolveSync("package-json-imports/baz"), "../"));
  expect(baz).toBe(resolve(import.meta.dir, "node_modules/package-json-imports/foo/private-foo.js"));

  const subpath = import.meta.resolveSync("#foo/bar", join(import.meta.resolveSync("package-json-imports/baz"), "../"));
  expect(subpath).toBe(resolve(import.meta.dir, "node_modules/package-json-imports/foo/private-foo.js"));

  const react = import.meta.resolveSync(
    "#internal-react",
    join(import.meta.resolveSync("package-json-imports/baz"), "../"),
  );
  expect(react).toBe(resolve(import.meta.dir, "../../../../node_modules/react/index.js"));

  // Check that #foo is not resolved to the package.json file.
  try {
    import.meta.resolveSync("#foo");
    throw new Error("Test failed");
  } catch (exception) {
    expect(exception instanceof ResolveMessage).toBe(true);
    expect(exception.referrer).toBe(import.meta.path);
    expect(exception.name).toBe("ResolveMessage");
  }

  // Chcek that package-json-imports/#foo doesn't work
  try {
    import.meta.resolveSync("package-json-imports/#foo");
    throw new Error("Test failed");
  } catch (exception) {
    expect(exception instanceof ResolveMessage).toBe(true);
    expect(exception.referrer).toBe(import.meta.path);
    expect(exception.name).toBe("ResolveMessage");
  }
});

it("#imports with wildcard", async () => {
  const run = resolveFrom(resolve(import.meta.dir + "/node_modules/package-json-imports/package.json"));

  const wildcard = resolve(import.meta.dir + "/node_modules/package-json-imports/foo/wildcard.js");
  expect(run("#foo/wildcard.js")).toBe(wildcard);
  expect(run("#foo/extensionless/wildcard")).toBe(wildcard);
});

it("import.meta.resolveSync", async () => {
  expect(import.meta.resolveSync("./resolve-test.js")).toBe(import.meta.path);

  expect(import.meta.resolveSync("./resolve-test.js", import.meta.path)).toBe(import.meta.path);

  expect(
    // optional second param can be any path, including a dir
    import.meta.resolveSync("./resolve/resolve-test.js", join(import.meta.path, "../")),
  ).toBe(import.meta.path);

  // can be a package path
  expect(import.meta.resolveSync("react", import.meta.path).length > 0).toBe(true);

  // file extensions are optional
  expect(import.meta.resolveSync("./resolve-test")).toBe(import.meta.path);

  // works with tsconfig.json "paths"
  expect(import.meta.resolveSync("foo/bar")).toBe(join(import.meta.path, "../baz.js"));
  expect(import.meta.resolveSync("@faasjs/baz")).toBe(join(import.meta.path, "../baz.js"));
  expect(import.meta.resolveSync("@faasjs/bar")).toBe(join(import.meta.path, "../bar/src/index.js"));
  expect(import.meta.resolveSync("@faasjs/larger/bar")).toBe(join(import.meta.path, "../bar/larger-index.js"));

  // works with package.json "exports"
  expect(import.meta.resolveSync("package-json-exports/baz")).toBe(
    join(import.meta.path, "../node_modules/package-json-exports/foo/bar.js"),
  );

  // if they never exported /package.json, allow reading from it too
  expect(import.meta.resolveSync("package-json-exports/package.json")).toBe(
    join(import.meta.path, "../node_modules/package-json-exports/package.json"),
  );

  // if an unnecessary ".js" extension was added, try against /baz
  expect(import.meta.resolveSync("package-json-exports/baz.js")).toBe(
    join(import.meta.path, "../node_modules/package-json-exports/foo/bar.js"),
  );

  // works with TypeScript compiler edgecases like:
  // - If the file ends with .js and it doesn't exist, try again with .ts and .tsx
  expect(import.meta.resolveSync("./resolve-typescript-file.js")).toBe(
    join(import.meta.path, "../resolve-typescript-file.tsx"),
  );
  expect(import.meta.resolveSync("./resolve-typescript-file.tsx")).toBe(
    join(import.meta.path, "../resolve-typescript-file.tsx"),
  );

  // throws a ResolveMessage on failure
  try {
    import.meta.resolveSync("THIS FILE DOESNT EXIST");
    throw new Error("Test failed");
  } catch (exception) {
    expect(exception instanceof ResolveMessage).toBe(true);
    expect(exception.referrer).toBe(import.meta.path);
    expect(exception.name).toBe("ResolveMessage");
  }
});

// the slightly lower level API, which doesn't prefill the second param
// and expects a directory instead of a filepath
it("Bun.resolve", async () => {
  expect(await Bun.resolve("./resolve-test.js", import.meta.dir)).toBe(import.meta.path);
});

// synchronous
it("Bun.resolveSync", () => {
  expect(Bun.resolveSync("./resolve-test.js", import.meta.dir)).toBe(import.meta.path);
});

it("dynamic import of file: URL with 4 slashes doesn't trigger ASAN", async () => {
  expect(await import(`file://` + `//a.js`).catch(e => e)).toBeInstanceOf(BuildMessage);
});

it("require of file: URL with 4 slashes doesn't trigger ASAN", async () => {
  expect(() => import.meta.require(`file://` + `//a.js`)).toBeInstanceOf(BuildMessage);
});

it("self-referencing imports works", async () => {
  const baz = import.meta.resolveSync("package-json-exports/baz");
  const namespace = import.meta.resolveSync("package-json-exports/references-baz");
  Loader.registry.delete(baz);
  Loader.registry.delete(namespace);
  var a = await import(baz);
  var b = await import(namespace);
  expect(a.bar).toBe(1);
  expect(b.bar).toBe(1);

  Loader.registry.delete(baz);
  Loader.registry.delete(namespace);
  var a = await import("package-json-exports/baz");
  var b = await import("package-json-exports/references-baz");
  expect(a.bar).toBe(1);
  expect(b.bar).toBe(1);

  Loader.registry.delete(baz);
  Loader.registry.delete(namespace);
  var a = import.meta.require("package-json-exports/baz");
  var b = import.meta.require("package-json-exports/references-baz");
  expect(a.bar).toBe(1);
  expect(b.bar).toBe(1);

  Loader.registry.delete(baz);
  Loader.registry.delete(namespace);
  var a = import.meta.require(baz);
  var b = import.meta.require(namespace);
  expect(a.bar).toBe(1);
  expect(b.bar).toBe(1);

  // test that file:// works
  Loader.registry.delete(baz);
  Loader.registry.delete(namespace);
  var a = import.meta.require("file://" + baz);
  var b = import.meta.require("file://" + namespace);
  expect(a.bar).toBe(1);
  expect(b.bar).toBe(1);
});
