import { resolveDroppedFiles } from "../src/components/resolveDroppedFiles";

type FileDropItem = Omit<DataTransferItem, "getAsFileSystemHandle" | "webkitGetAsEntry"> & {
  getAsFileSystemHandle?: () => Promise<unknown>;
  webkitGetAsEntry?: () => unknown;
};

function assert(condition: unknown, message: string): asserts condition {
  if (!condition) throw new Error(message);
}

function assertFiles(actual: File[], expected: File[], message: string): void {
  assert(actual.length === expected.length, `${message}: expected ${expected.length} file(s), got ${actual.length}`);
  expected.forEach((file, index) => {
    assert(actual[index] === file, `${message}: file ${index + 1} did not preserve object identity`);
  });
}

function transfer(files: File[] = [], items: FileDropItem[] = []): Pick<DataTransfer, "files" | "items"> {
  return {
    files: files as unknown as FileList,
    items: items as unknown as DataTransferItemList
  };
}

function item(overrides: Partial<FileDropItem> = {}): FileDropItem {
  return {
    kind: "file",
    type: "application/octet-stream",
    getAsFile: () => null,
    getAsString: () => undefined,
    ...overrides
  } as FileDropItem;
}

function namedFile(name: string): File {
  return new File([name], name, { type: "text/plain" });
}

async function* handles(...entries: unknown[]): AsyncIterable<unknown> {
  for (const entry of entries) yield entry;
}

async function run(): Promise<void> {
  const direct = namedFile("direct.txt");
  let directItemCalls = 0;
  const directResult = await resolveDroppedFiles(transfer([direct], [item({
    getAsFile: () => {
      directItemCalls += 1;
      return direct;
    }
  })]));
  assertFiles(directResult, [direct], "native DataTransfer.files takes precedence");
  assert(directItemCalls === 0, "native files must not also be resolved from items");

  const itemA = namedFile("item-a.txt");
  const itemB = namedFile("item-b.txt");
  let itemHandleCalls = 0;
  const itemResult = await resolveDroppedFiles(transfer([], [
    item({
      getAsFile: () => itemA,
      getAsFileSystemHandle: async () => {
        itemHandleCalls += 1;
        return { kind: "file", getFile: async () => itemA };
      }
    }),
    item({ getAsFile: () => itemB }),
    item({ kind: "string", getAsFile: () => null })
  ]));
  assertFiles(itemResult, [itemA, itemB], "DataTransferItem.getAsFile fallback resolves file items");
  assert(itemHandleCalls === 0, "item files must not also be resolved from file-system handles");

  const topLevelFile = namedFile("top-level.txt");
  const rootFile = namedFile("root.txt");
  const nestedFile = namedFile("nested.txt");
  const deepFile = namedFile("deep.txt");
  const deepDirectory = {
    kind: "directory",
    values: () => handles({ kind: "file", getFile: async () => deepFile })
  };
  const nestedDirectory = {
    kind: "directory",
    values: () => handles(
      { kind: "file", getFile: async () => nestedFile },
      deepDirectory
    )
  };
  const rootDirectory = {
    kind: "directory",
    values: () => handles(
      { kind: "file", getFile: async () => rootFile },
      nestedDirectory
    )
  };
  const handleResult = await resolveDroppedFiles(transfer([], [
    item({
      getAsFileSystemHandle: async () => ({
        kind: "file",
        getFile: async () => topLevelFile
      })
    }),
    item({ getAsFileSystemHandle: async () => rootDirectory })
  ]));
  assertFiles(
    handleResult,
    [topLevelFile, rootFile, nestedFile, deepFile],
    "File System Access file handles resolve and directory handles recurse in enumeration order"
  );

  const legacy = namedFile("legacy.txt");
  const legacyResult = await resolveDroppedFiles(transfer([], [item({
    getAsFileSystemHandle: async () => {
      throw new Error("file-system handles unavailable");
    },
    webkitGetAsEntry: () => ({
      isFile: true,
      isDirectory: false,
      file: (resolve: (file: File) => void) => resolve(legacy)
    })
  })]));
  assertFiles(legacyResult, [legacy], "legacy webkit file entry remains a final fallback");

  const emptyResult = await resolveDroppedFiles(transfer([], [
    item({ kind: "string" }),
    item({
      getAsFileSystemHandle: async () => ({ kind: "directory" }),
      webkitGetAsEntry: () => ({ isFile: false, isDirectory: true })
    })
  ]));
  assertFiles(emptyResult, [], "empty and non-readable directory paths resolve to an empty drop");

  let unreadableRejected = false;
  try {
    await resolveDroppedFiles(transfer([], [item({
      getAsFileSystemHandle: async () => ({
        kind: "file",
        getFile: async () => {
          throw new Error("permission denied");
        }
      })
    })]));
  } catch (error) {
    unreadableRejected = error instanceof Error && error.message === "permission denied";
  }
  assert(unreadableRejected, "an unreadable file handle rejects so the component can report the unreadable reason");
}

void run().catch((error) => {
  throw error;
});
