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

import { defineToolcraft } from "../../schema/define-toolcraft";
import type { ToolcraftSourceAssetCoordinator } from "../../source-assets/source-asset-coordinator";
import { ToolcraftRoot } from "../app-shell/toolcraft-root";
import { ToolcraftSourceAssetCoordinatorContext } from "../app-shell/toolcraft-source-asset-context";
import { useToolcraft } from "../app-shell/use-toolcraft";
import { CanvasShell } from "./canvas-shell";
import {
  CanvasStateProbe,
  createSchema,
  renderCanvasShell,
} from "./canvas-shell-test-utils";

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

function CanvasHistoryProbe() {
  const { state } = useToolcraft();

  return <span data-testid="history-count">{state.history.undo.length}</span>;
}

describe("CanvasShell upload routing", () => {
  it("submits one complete direct-canvas image batch to the shared coordinator", () => {
    const importBatch = vi.fn<ToolcraftSourceAssetCoordinator["importBatch"]>(
      async () => ({
        assetIds: [] as const,
        kind: "committed" as const,
      }),
    );
    const idleOperation = Object.freeze({
      phase: "idle" as const,
      target: "toolcraft.canvas.image",
    });
    const coordinator: ToolcraftSourceAssetCoordinator = {
      cancelTarget: vi.fn(),
      clearPresentationFeedback: vi.fn(),
      dispose: vi.fn(async () => undefined),
      getOperation: () => idleOperation,
      getPresentation: vi.fn(() => null),
      hydrateBinaryMedia: vi.fn(async () => undefined),
      hydrateModels: vi.fn(async () => undefined),
      importBatch,
      repairModel: vi.fn(async () => ({ kind: "cancelled" as const })),
      reportPresentationFeedback: vi.fn(),
      subscribe: () => () => undefined,
    };

    render(
      <ToolcraftRoot schema={createSchema()}>
        <ToolcraftSourceAssetCoordinatorContext.Provider value={coordinator}>
          <div style={{ height: 640, position: "relative", width: 640 }}>
            <CanvasShell />
          </div>
        </ToolcraftSourceAssetCoordinatorContext.Provider>
      </ToolcraftRoot>,
    );
    const canvas = screen.getByRole("application", { name: "Canvas viewport" });
    const files = [
      new File(["one"], "one.png", { type: "image/png" }),
      new File(["two"], "two.png", { type: "image/png" }),
    ];

    fireEvent.drop(canvas, {
      clientX: 120,
      clientY: 80,
      dataTransfer: { files },
    });

    expect(importBatch).toHaveBeenCalledTimes(1);
    const [batch, control] = importBatch.mock.calls[0] ?? [];
    expect(batch).toMatchObject({ files, origin: "canvas" });
    expect(batch?.files).toHaveLength(2);
    expect(batch?.files.map((file) => file.name)).toEqual([
      "one.png",
      "two.png",
    ]);
    expect(batch).not.toHaveProperty("target");
    expect(control).toBeUndefined();
  });

  it("replaces prior direct-canvas media with one complete atomic batch", async () => {
    renderCanvasShell({
      afterCanvas: (
        <>
          <CanvasStateProbe />
          <CanvasHistoryProbe />
        </>
      ),
      schema: createSchema(),
    });

    const canvas = screen.getByRole("application", { name: "Canvas viewport" });
    const files = [
      new File(["one"], "material-one.png", { type: "image/png" }),
      new File(["two"], "material-two.png", { type: "image/png" }),
    ];

    fireEvent.drop(canvas, {
      clientX: 0,
      clientY: 0,
      dataTransfer: { files },
    });

    await waitFor(() => {
      expect(screen.getByTestId("media-count").textContent).toBe("2");
    });

    expect(screen.getByTestId("history-count").textContent).toBe("1");
    expect(screen.getByTestId("media-size").textContent).toBe("300,200");
    expect(screen.getByTestId("media-targets").textContent).toBe("none,none");
    expect(
      screen.getByRole("button", { name: "Select material-one.png" }),
    ).toBeTruthy();
    expect(
      screen.getByRole("button", { name: "Select material-two.png" }),
    ).toBeTruthy();

    const replacementFiles = [
      new File(["three"], "replacement-one.png", { type: "image/png" }),
      new File(["four"], "replacement-two.png", { type: "image/png" }),
    ];

    fireEvent.drop(canvas, {
      clientX: 0,
      clientY: 0,
      dataTransfer: { files: replacementFiles },
    });

    await waitFor(() => {
      expect(
        screen.getByRole("button", { name: "Select replacement-two.png" }),
      ).toBeTruthy();
    });

    expect(screen.getByTestId("media-count").textContent).toBe("2");
    expect(screen.getByTestId("history-count").textContent).toBe("2");
    expect(screen.getByTestId("media-targets").textContent).toBe("none,none");
    expect(
      screen.getByRole("button", { name: "Select replacement-one.png" }),
    ).toBeTruthy();
    expect(
      screen.queryByRole("button", { name: "Select material-one.png" }),
    ).toBeNull();
    expect(
      screen.queryByRole("button", { name: "Select material-two.png" }),
    ).toBeNull();
  });

  it("appends complete direct-canvas batches when layers are enabled", async () => {
    const schema = defineToolcraft({
      canvas: {
        enabled: true,
        size: { width: 300, height: 200, unit: "px" },
        upload: true,
      },
      panels: { layers: true },
    });
    renderCanvasShell({
      afterCanvas: (
        <>
          <CanvasStateProbe />
          <CanvasHistoryProbe />
        </>
      ),
      schema,
    });

    const canvas = screen.getByRole("application", { name: "Canvas viewport" });
    const firstBatch = [
      new File(["one"], "layer-one.png", { type: "image/png" }),
      new File(["two"], "layer-two.png", { type: "image/png" }),
    ];
    const secondBatch = [
      new File(["three"], "layer-three.png", { type: "image/png" }),
      new File(["four"], "layer-four.png", { type: "image/png" }),
    ];

    fireEvent.drop(canvas, {
      clientX: 0,
      clientY: 0,
      dataTransfer: { files: firstBatch },
    });
    await waitFor(() => {
      expect(screen.getByTestId("media-count").textContent).toBe("2");
    });

    fireEvent.drop(canvas, {
      clientX: 0,
      clientY: 0,
      dataTransfer: { files: secondBatch },
    });
    await waitFor(() => {
      expect(screen.getByTestId("media-count").textContent).toBe("4");
    });

    expect(screen.getByTestId("history-count").textContent).toBe("2");
    for (const file of [...firstBatch, ...secondBatch]) {
      expect(
        screen.getByRole("button", { name: `Select ${file.name}` }),
      ).toBeTruthy();
    }
  });

  it("announces ambiguous canvas upload targets", async () => {
    const schema = defineToolcraft({
      canvas: {
        enabled: true,
        size: { width: 300, height: 200, unit: "px" },
        upload: true,
      },
      panels: {
        controls: {
          sections: [
            {
              controls: {
                primary: {
                  label: "Primary image",
                  target: "source.primary",
                  type: "fileDrop",
                },
                secondary: {
                  label: "Secondary image",
                  target: "source.secondary",
                  type: "fileDrop",
                },
              },
              title: "Sources",
            },
          ],
          title: "Controls",
        },
      },
    });
    renderCanvasShell({ schema });
    const canvas = screen.getByRole("application", { name: "Canvas viewport" });

    fireEvent.drop(canvas, {
      clientX: 0,
      clientY: 0,
      dataTransfer: {
        files: [new File(["image"], "ambiguous.png", { type: "image/png" })],
      },
    });

    await waitFor(() => {
      expect(screen.getByRole("alert").textContent).toContain(
        "more than one equally specific upload target",
      );
    });
  });

  it("routes a mixed canvas batch to one generic file fallback target", async () => {
    const schema = defineToolcraft({
      canvas: {
        enabled: true,
        size: { width: 300, height: 200, unit: "px" },
        upload: true,
      },
      panels: {
        controls: {
          sections: [
            {
              controls: {
                files: {
                  assetKind: "file",
                  label: "Files",
                  multiple: true,
                  target: "source.files",
                  type: "fileDrop",
                },
                image: {
                  label: "Image",
                  target: "source.image",
                  type: "fileDrop",
                },
              },
              title: "Sources",
            },
          ],
          title: "Controls",
        },
      },
    });

    renderCanvasShell({
      afterCanvas: <CanvasStateProbe />,
      schema,
    });

    const canvas = screen.getByRole("application", { name: "Canvas viewport" });
    const imageFile = new File(["image"], "material.png", { type: "image/png" });
    const textFile = new File(["notes"], "notes.txt", { type: "text/plain" });

    fireEvent.drop(canvas, {
      clientX: 0,
      clientY: 0,
      dataTransfer: {
        files: [imageFile, textFile],
      },
    });

    await waitFor(() => {
      expect(screen.getByTestId("media-count").textContent).toBe("2");
    });

    expect(screen.getByTestId("media-targets").textContent?.split(",")).toEqual([
      "source.files",
      "source.files",
    ]);
    expect(screen.getByTestId("media-kinds").textContent?.split(",")).toEqual([
      "file",
      "file",
    ]);
    expect(screen.queryByRole("button", { name: "Select material.png" })).toBeNull();
    expect(screen.queryByRole("button", { name: "Select notes.txt" })).toBeNull();
  });
});
