import { cleanup, fireEvent, render, screen } from "@testing-library/react";
import {
  FileDrop,
  type FileDropPresentation,
  type FileDropPreview,
} from "@repo/ui";
import { afterEach, describe, expect, it, vi } from "vitest";

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

const modelItemPresentation = {
  accept: ".glb",
  allowsFileBatch: true,
  allowsFolderSelection: true,
  emptyDescription: "or drop its local dependencies",
  emptyTitle: "Upload model",
  items: [
    {
      assetKind: "model",
      fileName: "scene.unknown",
      id: "model-1",
    },
  ],
  presenter: "model-item",
  secondaryActions: [
    {
      ariaLabel: "Repair model geometry",
      icon: "wand-sparkles",
      label: "Fix model",
      state: { loading: true, loadingAriaLabel: "Optimizing model" },
      value: "model.fix",
    },
  ],
} satisfies FileDropPresentation<"model", never, never>;

describe("FileDrop presentation", () => {
  it("submits one complete physical file batch for one previewless model item", () => {
    const onFilesSelect = vi.fn();
    const files = [
      new File([new Uint8Array([1])], "scene.unknown"),
      new File([new Uint8Array([2])], "mesh.bin"),
    ];

    render(
      <FileDrop
        onFilesSelect={onFilesSelect}
        presentation={modelItemPresentation}
      />,
    );

    expect(screen.getByText("scene.unknown")).toBeTruthy();
    expect(screen.queryByRole("img")).toBeNull();

    const input = screen.getByLabelText("Upload model") as HTMLInputElement;

    expect(input.multiple).toBe(true);
    const clickSpy = vi.spyOn(input, "click");
    fireEvent.click(screen.getByText("scene.unknown"));
    expect(clickSpy).toHaveBeenCalledTimes(1);
    fireEvent.change(input, { target: { files } });

    expect(onFilesSelect).toHaveBeenCalledTimes(1);
    expect(onFilesSelect).toHaveBeenCalledWith(files);
  });

  it("routes secondary action presentation through ActionsControl", () => {
    const onAction = vi.fn();
    const { container } = render(
      <FileDrop
        onAction={onAction}
        onFilesSelect={vi.fn()}
        presentation={modelItemPresentation}
      />,
    );
    const actionLayout = container.querySelector(
      '[data-slot="actions-control-buttons"]',
    );
    const loadingAction = screen.getByRole("button", { name: "Optimizing model" });

    expect(actionLayout?.getAttribute("data-actions-columns")).toBe("1");
    expect(actionLayout?.className).toContain("w-1/2");
    expect((loadingAction as HTMLButtonElement).disabled).toBe(true);
    expect(loadingAction.querySelector('[data-slot="button-loader"]')).toBeTruthy();
    expect(screen.queryByText("File actions")).toBeNull();
    expect(onAction).not.toHaveBeenCalled();
  });

  it("keeps physical batch allowance separate from presented collection cardinality", () => {
    render(
      <FileDrop
        onFilesSelect={vi.fn()}
        presentation={{
          ...modelItemPresentation,
          allowsFileBatch: false,
          items: [
            { assetKind: "file", fileName: "one.txt", id: "file-1" },
            { assetKind: "file", fileName: "two.txt", id: "file-2" },
          ],
          presenter: "file-list",
          secondaryActions: [],
        }}
      />,
    );

    expect((screen.getByLabelText("Upload model") as HTMLInputElement).multiple).toBe(
      false,
    );
    expect(screen.getByText("one.txt")).toBeTruthy();
    expect(screen.getByText("two.txt")).toBeTruthy();
  });

  it("submits the complete drop once even when native batch picking is disabled", () => {
    const onFilesSelect = vi.fn();
    const files = [
      new File([new Uint8Array([1])], "one.txt"),
      new File([new Uint8Array([2])], "two.txt"),
    ];
    render(
      <FileDrop
        onFilesSelect={onFilesSelect}
        presentation={{
          ...modelItemPresentation,
          allowsFileBatch: false,
          items: [],
          presenter: "file-list",
          secondaryActions: [],
        }}
      />,
    );
    const dropTarget = screen.getByRole("group", {
      name: "Upload model. or drop its local dependencies",
    });

    fireEvent.drop(dropTarget, {
      dataTransfer: { files },
    });

    expect(onFilesSelect).toHaveBeenCalledTimes(1);
    expect(onFilesSelect).toHaveBeenCalledWith(files);
  });

  it("routes an enabled secondary action through ActionsControl", () => {
    const onAction = vi.fn();
    render(
      <FileDrop
        onAction={onAction}
        onFilesSelect={vi.fn()}
        presentation={{
          ...modelItemPresentation,
          secondaryActions: [
            {
              ariaLabel: "Inspect model",
              icon: "check",
              label: "Inspect",
              value: "model.inspect",
            },
          ],
        }}
      />,
    );

    fireEvent.click(screen.getByRole("button", { name: "Inspect model" }));

    expect(onAction).toHaveBeenCalledTimes(1);
    expect(onAction).toHaveBeenCalledWith("model.inspect");
  });

  it("disables custom actions that have no callback instead of allowing no-ops", () => {
    render(
      <FileDrop
        onFilesSelect={vi.fn()}
        presentation={{
          ...modelItemPresentation,
          secondaryActions: [
            {
              ariaLabel: "Inspect model",
              label: "Inspect",
              value: "model.inspect",
            },
          ],
        }}
      />,
    );

    expect(
      (screen.getByRole("button", { name: "Inspect model" }) as HTMLButtonElement)
        .disabled,
    ).toBe(true);
  });

  it("does not let parent keyboard handling steal nested add-file actions", () => {
    render(
      <FileDrop
        onFilesSelect={vi.fn()}
        presentation={{
          ...modelItemPresentation,
          items: [{ assetKind: "file", fileName: "one.txt", id: "file-1" }],
          presenter: "file-list",
          secondaryActions: [],
        }}
      />,
    );
    const input = screen.getByLabelText("Upload model") as HTMLInputElement;
    const clickSpy = vi.spyOn(input, "click");
    const addButton = screen.getByRole("button", { name: "Add a new file" });

    fireEvent.keyDown(addButton, { key: "Enter" });
    expect(clickSpy).not.toHaveBeenCalled();
    fireEvent.click(addButton);
    expect(clickSpy).toHaveBeenCalledTimes(1);
  });

  it("uses an announced native button for multi-image selection", () => {
    const onPreviewTransform = vi.fn();
    const previews: FileDropPreview[] = [
      {
        alt: "First image",
        fileName: "first.png",
        id: "image-1",
        src: "data:image/png;base64,b25l",
      },
      {
        alt: "Second image",
        fileName: "second.png",
        id: "image-2",
        src: "data:image/png;base64,dHdv",
      },
    ];

    render(
      <FileDrop
        multiple
        onPreviewTransform={onPreviewTransform}
        previews={previews}
      />,
    );

    const selectFirst = screen.getByRole("button", { name: "Select First image" });
    expect(selectFirst.tagName).toBe("BUTTON");
    fireEvent.click(selectFirst);

    expect(selectFirst.getAttribute("aria-pressed")).toBe("true");
    fireEvent.click(screen.getByRole("button", { name: "90° Right" }));
    expect(onPreviewTransform).toHaveBeenCalledWith(previews[0], "rotate-right");
  });

  it("reports original presentation indices when non-renderable image items are hidden", () => {
    const onItemRemove = vi.fn();
    const onItemSelect = vi.fn();
    const hiddenItem = {
      assetKind: "image" as const,
      fileName: "missing.png",
      id: "image-hidden",
    };
    const visibleItem = {
      alt: "Visible image",
      assetKind: "image" as const,
      fileName: "visible.png",
      id: "image-visible",
      previewSrc: "data:image/png;base64,dmlzaWJsZQ==",
    };

    render(
      <FileDrop
        onFilesSelect={vi.fn()}
        onItemRemove={onItemRemove}
        onItemSelect={onItemSelect}
        presentation={{
          accept: "image/png",
          allowsFileBatch: true,
          allowsFolderSelection: false,
          emptyDescription: "or drag it onto the canvas",
          emptyTitle: "Upload images",
          items: [hiddenItem, visibleItem],
          presenter: "image-grid",
          secondaryActions: [],
        }}
      />,
    );

    fireEvent.click(screen.getByRole("button", { name: "Select Visible image" }));
    fireEvent.click(screen.getByRole("button", { name: "Remove Visible image" }));

    expect(onItemSelect).toHaveBeenCalledWith(visibleItem, 1);
    expect(onItemRemove).toHaveBeenCalledWith(visibleItem, 1);
  });

  it("keeps image selection separate from the keyboard reorder handle", () => {
    const onItemSelect = vi.fn();
    const items = [
      {
        alt: "First image",
        assetKind: "image" as const,
        fileName: "first.png",
        id: "image-1",
        previewSrc: "data:image/png;base64,b25l",
      },
      {
        alt: "Second image",
        assetKind: "image" as const,
        fileName: "second.png",
        id: "image-2",
        previewSrc: "data:image/png;base64,dHdv",
      },
    ];

    render(
      <FileDrop
        onFilesSelect={vi.fn()}
        onItemSelect={onItemSelect}
        onItemsReorder={vi.fn()}
        presentation={{
          accept: "image/png",
          allowsFileBatch: true,
          allowsFolderSelection: false,
          emptyDescription: "or drag it onto the canvas",
          emptyTitle: "Upload images",
          items,
          presenter: "image-grid",
          secondaryActions: [],
        }}
      />,
    );

    const selectButton = screen.getByRole("button", { name: "Select First image" });
    const reorderButton = screen.getByRole("button", { name: "Reorder First image" });

    expect(selectButton).not.toBe(reorderButton);
    expect(selectButton.getAttribute("aria-roledescription")).toBeNull();
    expect(reorderButton.getAttribute("aria-roledescription")).toBe("sortable");
    fireEvent.click(selectButton);
    expect(onItemSelect).toHaveBeenCalledWith(items[0], 0);
  });

  it("rejects collection reorder callbacks for non-collection presenters", () => {
    expect(() =>
      render(
        <FileDrop
          onFilesSelect={vi.fn()}
          onItemsReorder={vi.fn()}
          presentation={modelItemPresentation}
        />,
      ),
    ).toThrow(
      'FileDrop presenter "model-item" does not support item reordering.',
    );
  });

  it("never nests one interactive button inside another", () => {
    const { container } = render(
      <FileDrop
        onClear={vi.fn()}
        onFilesSelect={vi.fn()}
        presentation={{
          accept: "image/png",
          allowsFileBatch: false,
          allowsFolderSelection: false,
          emptyDescription: "or drag it onto the canvas",
          emptyTitle: "Upload image",
          items: [
            {
              alt: "Source image",
              assetKind: "image",
              fileName: "source.png",
              id: "image-1",
              previewSrc: "data:image/png;base64,c291cmNl",
            },
          ],
          presenter: "single-image",
          secondaryActions: [],
        }}
      />,
    );

    for (const button of container.querySelectorAll("button")) {
      expect(button.querySelector("button")).toBeNull();
    }
  });

  it("treats image presentations without preview sources as empty", () => {
    render(
      <FileDrop
        onFilesSelect={vi.fn()}
        presentation={{
          accept: "image/png",
          allowsFileBatch: false,
          allowsFolderSelection: false,
          emptyDescription: "or drag it onto the canvas",
          emptyTitle: "Upload image",
          items: [
            { assetKind: "image", fileName: "missing.png", id: "image-1" },
          ],
          presenter: "single-image",
          secondaryActions: [],
        }}
      />,
    );

    expect(screen.getByText("Upload image")).toBeTruthy();
    expect(screen.queryByRole("img")).toBeNull();
  });

  it("preserves the legacy empty-source empty state", () => {
    render(
      <FileDrop
        preview={{
          alt: "Missing image",
          fileName: "missing.png",
          id: "image-1",
          src: "",
        }}
      />,
    );

    expect(screen.getByText("Click to upload an image")).toBeTruthy();
    expect(screen.getByRole("group", { name: "Browse image file" })).toBeTruthy();
    expect(screen.queryByRole("img")).toBeNull();
  });

  it("adapts legacy image props without losing preview metadata or actions", () => {
    const onClear = vi.fn();
    const onPreviewTransform = vi.fn();
    const preview: FileDropPreview = {
      alt: "Source image",
      fileName: "source.png",
      id: "image-1",
      size: { height: 80, width: 120 },
      src: "data:image/png;base64,c291cmNl",
      transform: { flipHorizontal: true, rotationDeg: 90 },
    };

    render(
      <FileDrop
        accept="PNG, JPEG"
        onClear={onClear}
        onPreviewTransform={onPreviewTransform}
        preview={preview}
      />,
    );

    const image = screen.getByRole("img", { name: "Source image" });
    const input = screen.getByLabelText("Click to upload an image") as HTMLInputElement;

    expect(image.getAttribute("src")).toBe(preview.src);
    expect(image.getAttribute("height")).toBe("80");
    expect(image.getAttribute("width")).toBe("120");
    expect(image.getAttribute("style")).toContain("rotate(90deg)");
    expect(image.getAttribute("style")).toContain("scale(-1, 1)");
    expect(input.accept).toBe(".png,image/png,.jpg,.jpeg,image/jpeg");

    fireEvent.click(screen.getByRole("button", { name: "90° Right" }));
    fireEvent.click(screen.getByRole("button", { name: "Remove Source image" }));

    expect(onPreviewTransform).toHaveBeenCalledWith(preview, "rotate-right");
    expect(onClear).toHaveBeenCalledTimes(1);
  });
});
