/**
 * @vitest-environment jsdom
 */
import { act } from "react";
import { createRoot, type Root } from "react-dom/client";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { ComposerPrimitiveAttachmentDropzone } from "./ComposerAttachmentDropzone";

const { addAttachment, threadCapabilities, threadState } = vi.hoisted(() => ({
  addAttachment: vi.fn<(file: File) => Promise<void>>(),
  threadCapabilities: { attachments: true },
  threadState: { isDisabled: false },
}));

(globalThis as Record<string, unknown>).IS_REACT_ACT_ENVIRONMENT = true;

vi.mock("@assistant-ui/store", async (importOriginal) => {
  const actual = await importOriginal<typeof import("@assistant-ui/store")>();
  return {
    ...actual,
    useAui: () => ({
      composer: {
        addAttachment,
      },
      thread: {
        getState: () => ({ capabilities: threadCapabilities }),
      },
    }),
    useAuiState: (
      selector: (state: { thread: typeof threadState }) => unknown,
    ) => selector({ thread: threadState }),
  };
});

const createDropEvent = (files: File[], types: string[] = ["Files"]) => {
  const event = new Event("drop", { bubbles: true, cancelable: true });
  Object.defineProperty(event, "dataTransfer", {
    value: { files, types },
    configurable: true,
  });
  return event;
};

const createDragEvent = (type: string, types: string[]) => {
  const event = new Event(type, { bubbles: true, cancelable: true });
  Object.defineProperty(event, "dataTransfer", {
    value: { files: [], types },
    configurable: true,
  });
  return event;
};

describe("ComposerPrimitiveAttachmentDropzone", () => {
  let container: HTMLDivElement;
  let root: Root;

  beforeEach(async () => {
    addAttachment.mockReset();
    threadCapabilities.attachments = true;
    threadState.isDisabled = false;
    container = document.createElement("div");
    document.body.appendChild(container);
    root = createRoot(container);

    await act(async () => {
      root.render(
        <ComposerPrimitiveAttachmentDropzone data-testid="dropzone">
          <div>dropzone</div>
        </ComposerPrimitiveAttachmentDropzone>,
      );
    });
  });

  afterEach(async () => {
    await act(async () => {
      root.unmount();
    });
    container.remove();
    vi.restoreAllMocks();
  });

  it("composes a render element and keeps the drag props attached", async () => {
    await act(async () => {
      root.render(
        <ComposerPrimitiveAttachmentDropzone
          data-testid="dropzone"
          render={<section className="child" />}
          className="parent"
        >
          <div>outer</div>
        </ComposerPrimitiveAttachmentDropzone>,
      );
    });

    const dropzone = container.querySelector(
      "section[data-testid='dropzone']",
    ) as HTMLElement;
    expect(dropzone).not.toBeNull();
    expect(dropzone.textContent).toBe("outer");
    expect(dropzone.className).toContain("parent");
    expect(dropzone.className).toContain("child");

    await act(async () => {
      dropzone.dispatchEvent(createDragEvent("dragenter", ["Files"]));
    });

    expect(dropzone.getAttribute("data-dragging")).toBe("true");
  });

  it("falls back to the render element's own children", async () => {
    await act(async () => {
      root.render(
        <ComposerPrimitiveAttachmentDropzone
          data-testid="dropzone"
          render={<section>fallback</section>}
        />,
      );
    });

    expect(
      container.querySelector("section[data-testid='dropzone']")?.textContent,
    ).toBe("fallback");
  });

  it("starts all dropped attachments before awaiting completion", async () => {
    const resolvers: Array<() => void> = [];
    addAttachment.mockImplementation(
      () =>
        new Promise<void>((resolve) => {
          resolvers.push(resolve);
        }),
    );

    const dropzone = container.querySelector("[data-testid='dropzone']");
    expect(dropzone).not.toBeNull();

    const files = [
      new File(["a"], "first.txt", { type: "text/plain" }),
      new File(["b"], "second.txt", { type: "text/plain" }),
      new File(["c"], "third.txt", { type: "text/plain" }),
    ];

    await act(async () => {
      dropzone!.dispatchEvent(createDropEvent(files));
      await Promise.resolve();
    });

    expect(addAttachment).toHaveBeenCalledTimes(3);
    expect(addAttachment.mock.calls.map(([file]) => file.name)).toEqual([
      "first.txt",
      "second.txt",
      "third.txt",
    ]);

    for (const resolve of resolvers) resolve();

    await act(async () => {
      await Promise.resolve();
    });
  });

  it("continues processing other files when one attachment fails without console.error", async () => {
    const error = new Error("upload failed");
    const errorSpy = vi.spyOn(console, "error").mockImplementation(() => {});

    addAttachment
      .mockRejectedValueOnce(error)
      .mockResolvedValueOnce(undefined)
      .mockResolvedValueOnce(undefined);

    const dropzone = container.querySelector("[data-testid='dropzone']");
    expect(dropzone).not.toBeNull();

    const files = [
      new File(["a"], "first.txt", { type: "text/plain" }),
      new File(["b"], "second.txt", { type: "text/plain" }),
      new File(["c"], "third.txt", { type: "text/plain" }),
    ];

    await act(async () => {
      dropzone!.dispatchEvent(createDropEvent(files));
      await Promise.resolve();
      await Promise.resolve();
    });

    expect(addAttachment).toHaveBeenCalledTimes(3);
    expect(errorSpy).not.toHaveBeenCalled();
    errorSpy.mockRestore();
  });

  it("highlights on file drags", async () => {
    const dropzone = container.querySelector("[data-testid='dropzone']");
    expect(dropzone).not.toBeNull();

    const event = createDragEvent("dragenter", ["Files"]);
    await act(async () => {
      dropzone!.dispatchEvent(event);
    });

    expect(dropzone!.getAttribute("data-dragging")).toBe("true");
    expect(event.defaultPrevented).toBe(true);
  });

  it("clears the drag highlight when disabled during a drag", async () => {
    const dropzone = container.querySelector("[data-testid='dropzone']");
    expect(dropzone).not.toBeNull();

    await act(async () => {
      dropzone!.dispatchEvent(createDragEvent("dragenter", ["Files"]));
    });
    expect(dropzone!.getAttribute("data-dragging")).toBe("true");

    await act(async () => {
      root.render(
        <ComposerPrimitiveAttachmentDropzone data-testid="dropzone" disabled />,
      );
    });

    expect(dropzone!.hasAttribute("data-dragging")).toBe(false);

    await act(async () => {
      root.render(
        <ComposerPrimitiveAttachmentDropzone data-testid="dropzone" />,
      );
    });

    expect(dropzone!.hasAttribute("data-dragging")).toBe(false);

    await act(async () => {
      dropzone!.dispatchEvent(createDragEvent("dragenter", ["Files"]));
    });
    expect(dropzone!.getAttribute("data-dragging")).toBe("true");
  });

  it("prevents a disabled file drop from navigating away without adding attachments", async () => {
    await act(async () => {
      root.render(
        <ComposerPrimitiveAttachmentDropzone data-testid="dropzone" disabled />,
      );
    });
    const dropzone = container.querySelector("[data-testid='dropzone']")!;
    const over = createDragEvent("dragover", ["Files"]);
    const drop = createDropEvent([
      new File(["file"], "photo.png", { type: "image/png" }),
    ]);

    await act(async () => {
      dropzone.dispatchEvent(over);
      dropzone.dispatchEvent(drop);
    });

    expect(over.defaultPrevented).toBe(true);
    expect(drop.defaultPrevented).toBe(true);
    expect(addAttachment).not.toHaveBeenCalled();
    expect(dropzone.hasAttribute("data-dragging")).toBe(false);
  });

  it("blocks file drops when the thread is disabled", async () => {
    threadState.isDisabled = true;
    await act(async () => {
      root.render(
        <ComposerPrimitiveAttachmentDropzone data-testid="dropzone">
          <div>dropzone</div>
        </ComposerPrimitiveAttachmentDropzone>,
      );
    });
    const dropzone = container.querySelector("[data-testid='dropzone']")!;
    const over = createDragEvent("dragover", ["Files"]);
    const drop = createDropEvent([
      new File(["file"], "photo.png", { type: "image/png" }),
    ]);

    await act(async () => {
      dropzone.dispatchEvent(over);
      dropzone.dispatchEvent(drop);
    });

    expect(over.defaultPrevented).toBe(true);
    expect(drop.defaultPrevented).toBe(true);
    expect(addAttachment).not.toHaveBeenCalled();
    expect(dropzone.hasAttribute("data-dragging")).toBe(false);
  });

  it("ignores non-file drags", async () => {
    const dropzone = container.querySelector("[data-testid='dropzone']");
    expect(dropzone).not.toBeNull();

    const enter = createDragEvent("dragenter", ["text/plain"]);
    const over = createDragEvent("dragover", ["text/plain"]);
    await act(async () => {
      dropzone!.dispatchEvent(enter);
      dropzone!.dispatchEvent(over);
    });

    expect(dropzone!.hasAttribute("data-dragging")).toBe(false);
    expect(enter.defaultPrevented).toBe(false);
    expect(over.defaultPrevented).toBe(false);
  });

  it("claims file drags without highlighting when the runtime does not support attachments", async () => {
    threadCapabilities.attachments = false;
    const dropzone = container.querySelector("[data-testid='dropzone']");
    expect(dropzone).not.toBeNull();

    const enter = createDragEvent("dragenter", ["Files"]);
    const drop = createDropEvent([
      new File(["a"], "first.txt", { type: "text/plain" }),
    ]);
    await act(async () => {
      dropzone!.dispatchEvent(enter);
      dropzone!.dispatchEvent(drop);
    });

    expect(dropzone!.hasAttribute("data-dragging")).toBe(false);
    expect(enter.defaultPrevented).toBe(true);
    expect((enter as unknown as DragEvent).dataTransfer!.dropEffect).toBe(
      "none",
    );
    expect(drop.defaultPrevented).toBe(true);
    expect(addAttachment).not.toHaveBeenCalled();
  });

  it("clears the highlight and keeps a file drop claimed when it yields no files", async () => {
    const dropzone = container.querySelector("[data-testid='dropzone']");
    expect(dropzone).not.toBeNull();

    await act(async () => {
      dropzone!.dispatchEvent(createDragEvent("dragenter", ["Files"]));
    });
    expect(dropzone!.getAttribute("data-dragging")).toBe("true");

    const drop = createDropEvent([], ["Files"]);
    await act(async () => {
      dropzone!.dispatchEvent(drop);
    });

    expect(dropzone!.hasAttribute("data-dragging")).toBe(false);
    expect(drop.defaultPrevented).toBe(true);
    expect(addAttachment).not.toHaveBeenCalled();
  });

  it("lets non-file drops pass through", async () => {
    const dropzone = container.querySelector("[data-testid='dropzone']");
    expect(dropzone).not.toBeNull();

    const drop = createDropEvent([], []);
    await act(async () => {
      dropzone!.dispatchEvent(drop);
    });

    expect(drop.defaultPrevented).toBe(false);
    expect(addAttachment).not.toHaveBeenCalled();
  });
});
