// @vitest-environment jsdom

import { cleanup, fireEvent, render, screen } from "@testing-library/react";
import { afterEach, describe, expect, it, vi } from "vitest";

import { FileDropControl } from "./file-drop-control";
import type { FileDropPresentation } from "./file-drop-types";

afterEach(() => {
  cleanup();
});

const modelPresentation = {
  accept: ".gltf,.glb",
  allowsFileBatch: true,
  allowsFolderSelection: true,
  emptyDescription: "or drop its local dependencies",
  emptyTitle: "Upload model",
  items: [],
  presenter: "model-item",
  secondaryActions: [],
} satisfies FileDropPresentation<"model">;

describe("FileDropControl folder selection", () => {
  it("exposes typed presentation feedback for browser evidence", () => {
    render(
      <FileDropControl
        onFilesSelect={vi.fn()}
        presentation={{
          ...modelPresentation,
          feedback: {
            code: "missing-appearance-resource",
            message: "A referenced texture is unavailable.",
          },
        }}
      />,
    );

    const alert = screen.getByRole("alert");
    expect(alert.textContent).toBe("A referenced texture is unavailable.");
    expect(alert.getAttribute("data-file-drop-feedback-code")).toBe("missing-appearance-resource");
  });

  it("preserves a selected model folder as one physical file batch", () => {
    const onFilesSelect = vi.fn();
    const onFolderSelect = vi.fn();
    const files = [
      new File([new Uint8Array([1])], "scene.gltf"),
      new File([new Uint8Array([2])], "mesh.bin"),
    ];
    Object.defineProperty(files[0], "webkitRelativePath", {
      value: "robot/scene.gltf",
    });
    Object.defineProperty(files[1], "webkitRelativePath", {
      value: "robot/mesh.bin",
    });

    render(
      <FileDropControl
        onFilesSelect={onFilesSelect}
        onFolderSelect={onFolderSelect}
        presentation={modelPresentation}
      />,
    );

    const folderInput = screen.getByLabelText("Upload model folder") as HTMLInputElement;
    fireEvent.change(folderInput, { target: { files } });

    expect(folderInput.hasAttribute("webkitdirectory")).toBe(true);
    expect(folderInput.hasAttribute("directory")).toBe(true);
    expect(folderInput.accept).toBe("");
    expect(onFolderSelect).toHaveBeenCalledWith(files);
    expect(onFilesSelect).not.toHaveBeenCalled();
    expect(files.map((file) => file.webkitRelativePath)).toEqual([
      "robot/scene.gltf",
      "robot/mesh.bin",
    ]);
  });

  it("does not expose folder selection for an ordinary file presentation", () => {
    render(
      <FileDropControl
        onFilesSelect={vi.fn()}
        presentation={{
          ...modelPresentation,
          allowsFolderSelection: false,
          emptyTitle: "Upload file",
          presenter: "file-list",
        }}
      />,
    );

    expect(screen.queryByRole("button", { name: "Choose folder" })).toBeNull();
    expect(screen.queryByLabelText("Upload file folder")).toBeNull();
  });
});

describe("FileDropControl collection actions", () => {
  const filePresentation = {
    accept: "image/*",
    allowsFileBatch: true,
    allowsFolderSelection: false,
    emptyDescription: "or drop bump maps",
    emptyTitle: "Upload bump maps",
    items: [],
    presenter: "file-list",
    secondaryActions: [],
  } satisfies FileDropPresentation<"file">;

  it("adds an empty upload slot before selecting another file", () => {
    const onFilesSelect = vi.fn();

    render(
      <FileDropControl
        collectionActions={{
          addLabel: "Add bump map",
          itemLabel: "bump map",
          name: "Bump maps",
          removeLabel: "Remove bump map",
        }}
        onFilesSelect={onFilesSelect}
        presentation={{
          ...filePresentation,
          items: [{
            assetKind: "file",
            fileName: "broad.png",
            id: "bump-broad",
          }],
        }}
      />,
    );

    fireEvent.click(screen.getByRole("button", { name: "Add bump map" }));
    expect(onFilesSelect).not.toHaveBeenCalled();
    expect(
      (screen.getByRole("button", { name: "Add bump map" }) as HTMLButtonElement)
        .disabled,
    ).toBe(true);

    const input = screen.getByLabelText("Upload bump map 2") as HTMLInputElement;
    const file = new File([new Uint8Array([1])], "grain.png", {
      type: "image/png",
    });
    fireEvent.change(input, { target: { files: [file] } });

    expect(onFilesSelect).toHaveBeenCalledWith([file]);
  });

  it("removes the final item and suppresses duplicate list cardinality actions", () => {
    const onItemRemove = vi.fn();
    const finalItem = {
      assetKind: "file" as const,
      fileName: "fine.png",
      id: "bump-fine",
    };

    render(
      <FileDropControl
        collectionActions={{
          addLabel: "Add bump map",
          itemLabel: "bump map",
          name: "Bump maps",
          removeLabel: "Remove bump map",
        }}
        onFilesSelect={vi.fn()}
        onItemRemove={onItemRemove}
        presentation={{
          ...filePresentation,
          items: [
            {
              assetKind: "file",
              fileName: "broad.png",
              id: "bump-broad",
            },
            finalItem,
          ],
        }}
      />,
    );

    expect(screen.queryByRole("button", { name: "Add a new file" })).toBeNull();
    expect(screen.queryByRole("button", { name: "Remove broad.png" })).toBeNull();
    expect(screen.queryByRole("button", { name: "Remove fine.png" })).toBeNull();

    fireEvent.click(screen.getByRole("button", { name: "Remove bump map" }));
    expect(onItemRemove).toHaveBeenCalledWith(finalItem, 1);
  });

  it("removes a pending final slot before deleting an attached file", () => {
    const onItemRemove = vi.fn();

    render(
      <FileDropControl
        collectionActions={{
          addLabel: "Add bump map",
          itemLabel: "bump map",
          name: "Bump maps",
          removeLabel: "Remove bump map",
        }}
        onFilesSelect={vi.fn()}
        onItemRemove={onItemRemove}
        presentation={{
          ...filePresentation,
          items: [{
            assetKind: "file",
            fileName: "broad.png",
            id: "bump-broad",
          }],
        }}
      />,
    );

    fireEvent.click(screen.getByRole("button", { name: "Add bump map" }));
    expect(screen.getByLabelText("Upload bump map 2")).toBeTruthy();

    fireEvent.click(screen.getByRole("button", { name: "Remove bump map" }));
    expect(screen.queryByLabelText("Upload bump map 2")).toBeNull();
    expect(onItemRemove).not.toHaveBeenCalled();
  });

  it("renders item-owned settings only under an attached upload", () => {
    const renderItemContent = vi.fn((item: { id: string }) => (
      <div>Settings for {item.id}</div>
    ));

    render(
      <FileDropControl
        collectionActions={{
          addLabel: "Add bump map",
          itemLabel: "bump map",
          name: "Bump maps",
          renderItemContent,
        }}
        onFilesSelect={vi.fn()}
        presentation={{
          ...filePresentation,
          items: [
            {
              assetKind: "file",
              fileName: "broad.png",
              id: "bump-broad",
            },
          ],
        }}
      />,
    );

    expect(screen.getByText("Settings for bump-broad")).toBeTruthy();
    fireEvent.click(screen.getByRole("button", { name: "Add bump map" }));
    expect(screen.getByLabelText("Upload bump map 2")).toBeTruthy();
    expect(renderItemContent).toHaveBeenCalled();
    expect(
      renderItemContent.mock.calls.every(([item]) => item.id === "bump-broad"),
    ).toBe(true);
  });

  it("separates attached logical file groups with one between-group line", () => {
    const { container } = render(
      <FileDropControl
        collectionActions={{
          itemLabel: "bump map",
          name: "Bump maps",
          renderItemContent: (item) => <div>Settings for {item.id}</div>,
        }}
        onFilesSelect={vi.fn()}
        onItemRemove={vi.fn()}
        presentation={{
          ...filePresentation,
          items: [
            { assetKind: "file", fileName: "broad.png", id: "broad" },
            { assetKind: "file", fileName: "fine.png", id: "fine" },
          ],
        }}
      />,
    );
    const groups = container.querySelectorAll(
      '[data-slot="collection-item-group"]',
    );

    expect(groups).toHaveLength(2);
    expect(groups[0]?.className).not.toContain("border-t");
    expect(groups[1]?.className).toContain("border-t");
    expect(container.textContent).not.toMatch(/Item 1|Item 2/u);
  });
});
