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

import { defineToolcraft } from "../../schema/define-toolcraft";
import type { ToolcraftExternalStore } from "../../state/toolcraft-external-store";
import { useToolcraftStore } from "../app-shell/toolcraft-store-context";

import {
  CanvasStateProbe,
  createSchema,
  mockCanvasRect,
  renderCanvasShell,
} from "./canvas-shell-test-utils";

afterEach(() => {
  vi.useRealTimers();
  cleanup();
  window.localStorage.clear();
  vi.restoreAllMocks();
});

function CaptureStore({
  capture,
}: {
  capture: (store: ToolcraftExternalStore) => void;
}): null {
  capture(useToolcraftStore());
  return null;
}

function createGestureEvent(
  type: "gesturechange" | "gestureend" | "gesturestart",
  properties: { clientX?: number; clientY?: number; scale?: number } = {},
): Event {
  const event = new Event(type, { bubbles: true, cancelable: true });
  Object.defineProperties(
    event,
    Object.fromEntries(
      Object.entries(properties).map(([key, value]) => [
        key,
        { configurable: true, value },
      ]),
    ),
  );
  return event;
}

describe("CanvasShell viewport interactions", () => {
  it("updates runtime canvas offset while panning", () => {
    let store: ToolcraftExternalStore | undefined;
    renderCanvasShell({
      afterCanvas: (
        <>
          <CaptureStore capture={(value) => { store = value; }} />
          <CanvasStateProbe />
        </>
      ),
    });

    const canvas = screen.getByRole("application", { name: "Canvas viewport" });

    fireEvent.pointerDown(canvas, { button: 0, clientX: 10, clientY: 20, pointerId: 1 });
    fireEvent.pointerMove(canvas, { clientX: 25, clientY: 45, pointerId: 1 });

    expect(store?.getState().canvas.offset).toEqual({ x: 15, y: 25 });
    expect(store?.getCommittedState().canvas.offset).toEqual({ x: 0, y: 0 });

    const commitTransient = vi.spyOn(store!, "commitTransient");
    fireEvent.pointerUp(canvas, { pointerId: 1 });

    expect(screen.getByTestId("canvas-offset").textContent).toBe("15,25");
    expect(store?.getCommittedState().canvas.offset).toEqual({ x: 15, y: 25 });
    expect(commitTransient).toHaveBeenCalledTimes(1);
    expect(commitTransient).toHaveBeenCalledWith("viewport");
  });

  it("accumulates wheel movement from the latest effective viewport and commits after idle", () => {
    vi.useFakeTimers();
    let store: ToolcraftExternalStore | undefined;
    renderCanvasShell({
      afterCanvas: <CaptureStore capture={(value) => { store = value; }} />,
    });

    const canvas = screen.getByRole("application", { name: "Canvas viewport" });
    const dispatchWheel = () =>
      canvas.dispatchEvent(
        new WheelEvent("wheel", {
          bubbles: true,
          cancelable: true,
          deltaX: 8,
          deltaY: -12,
        }),
      );

    expect(dispatchWheel()).toBe(false);
    expect(dispatchWheel()).toBe(false);
    expect(store?.getState().canvas.offset).toEqual({ x: -16, y: 24 });
    expect(store?.getCommittedState().canvas.offset).toEqual({ x: 0, y: 0 });

    act(() => {
      vi.advanceTimersByTime(119);
    });
    expect(store?.getCommittedState().canvas.offset).toEqual({ x: 0, y: 0 });

    act(() => {
      vi.advanceTimersByTime(1);
    });
    expect(store?.getCommittedState().canvas.offset).toEqual({ x: -16, y: 24 });
  });

  it("commits a pending wheel viewport when the canvas unmounts", () => {
    vi.useFakeTimers();
    let store: ToolcraftExternalStore | undefined;
    const view = renderCanvasShell({
      afterCanvas: <CaptureStore capture={(value) => { store = value; }} />,
    });
    const canvas = screen.getByRole("application", { name: "Canvas viewport" });

    canvas.dispatchEvent(
      new WheelEvent("wheel", {
        bubbles: true,
        cancelable: true,
        deltaY: 20,
      }),
    );

    expect(store?.getCommittedState().canvas.offset).toEqual({ x: 0, y: 0 });
    view.unmount();
    expect(store?.getCommittedState().canvas.offset).toEqual({ x: 0, y: -20 });
  });

  it("uses the active transient viewport when positioning a dropped image", async () => {
    let store: ToolcraftExternalStore | undefined;
    renderCanvasShell({
      afterCanvas: <CaptureStore capture={(value) => { store = value; }} />,
    });
    const canvas = screen.getByRole("application", { name: "Canvas viewport" });
    mockCanvasRect(canvas);
    act(() => {
      store?.dispatchTransient({
        offset: { x: 100, y: -50 },
        type: "canvas.setViewport",
        zoom: 200,
      });
    });

    const dropEvent = createEvent.drop(canvas, {
      dataTransfer: {
        files: [new File(["image"], "positioned.png", { type: "image/png" })],
      },
    });
    Object.defineProperties(dropEvent, {
      clientX: { value: 500 },
      clientY: { value: 400 },
    });
    fireEvent(canvas, dropEvent);

    await waitFor(() => {
      expect(store?.getState().mediaAssets).toHaveLength(1);
    });
    const mediaAsset = store?.getState().mediaAssets[0];
    expect(
      mediaAsset?.assetKind === "image" ? mediaAsset.position : undefined,
    ).toEqual({ x: 0, y: 75 });
  });

  it("pans the canvas from a two finger wheel gesture", async () => {
    renderCanvasShell({
      afterCanvas: <CanvasStateProbe />,
    });

    const canvas = screen.getByRole("application", { name: "Canvas viewport" });
    const wheelEvent = new WheelEvent("wheel", {
      bubbles: true,
      cancelable: true,
      deltaX: 8,
      deltaY: -12,
    });

    expect(canvas.dispatchEvent(wheelEvent)).toBe(false);
    expect(wheelEvent.defaultPrevented).toBe(true);
    await waitFor(() => {
      expect(screen.getByTestId("canvas-offset").textContent).toBe("-8,12");
    });
    expect(screen.getByTestId("canvas-zoom").textContent).toBe("100");
  });

  it("zooms the canvas world from a trackpad pinch around the pointer", async () => {
    renderCanvasShell({
      afterCanvas: <CanvasStateProbe />,
    });

    const canvas = screen.getByRole("application", { name: "Canvas viewport" });
    const world = canvas.querySelector("[data-toolcraft-canvas-world]") as HTMLElement;
    mockCanvasRect(canvas);

    expect(world.style.transform).toBe("translate(-50%, -50%) translate(0px, 0px) scale(1)");

    const wheelEvent = new WheelEvent("wheel", {
      bubbles: true,
      cancelable: true,
      clientX: 600,
      clientY: 300,
      ctrlKey: true,
      deltaY: -100,
    });

    expect(canvas.dispatchEvent(wheelEvent)).toBe(false);
    expect(wheelEvent.defaultPrevented).toBe(true);
    await waitFor(() => {
      expect(screen.getByTestId("canvas-zoom").textContent).toBe("150");
    });
    expect(world.style.transform).toContain("translate(-100px");
    expect(world.style.transform).toContain("scale(1.5)");
  });

  it("prevents panel pinch gestures without moving the canvas viewport", async () => {
    renderCanvasShell({
      afterCanvas: (
        <>
          <button data-testid="panel-overlay" type="button">
            Panel overlay
          </button>
          <CanvasStateProbe />
        </>
      ),
      frameProps: {
        "data-slot": "toolcraft-runtime-app",
      } as React.HTMLAttributes<HTMLDivElement>,
    });

    const canvas = screen.getByRole("application", { name: "Canvas viewport" });
    const overlay = screen.getByTestId("panel-overlay");
    mockCanvasRect(canvas);

    const wheelEvent = new WheelEvent("wheel", {
      bubbles: true,
      cancelable: true,
      clientX: 600,
      clientY: 300,
      ctrlKey: true,
      deltaY: -100,
    });

    expect(overlay.dispatchEvent(wheelEvent)).toBe(false);
    expect(wheelEvent.defaultPrevented).toBe(true);
    expect(screen.getByTestId("canvas-zoom").textContent).toBe("100");
    expect(screen.getByTestId("canvas-offset").textContent).toBe("0,0");
    expect(overlay.closest("[data-toolcraft-canvas-world]")).toBeNull();

    const gestureStart = createGestureEvent("gesturestart", { scale: 1 });
    const gestureChange = createGestureEvent("gesturechange", { scale: 1.5 });
    const gestureEnd = createGestureEvent("gestureend");

    expect(overlay.dispatchEvent(gestureStart)).toBe(false);
    expect(overlay.dispatchEvent(gestureChange)).toBe(false);
    expect(overlay.dispatchEvent(gestureEnd)).toBe(false);
    expect(gestureStart.defaultPrevented).toBe(true);
    expect(gestureChange.defaultPrevented).toBe(true);
    expect(gestureEnd.defaultPrevented).toBe(true);
    expect(screen.getByTestId("canvas-zoom").textContent).toBe("100");
    expect(screen.getByTestId("canvas-offset").textContent).toBe("0,0");

    const scrollEvent = new WheelEvent("wheel", {
      bubbles: true,
      cancelable: true,
      deltaY: 80,
    });

    expect(overlay.dispatchEvent(scrollEvent)).toBe(true);
    expect(scrollEvent.defaultPrevented).toBe(false);
    expect(screen.getByTestId("canvas-zoom").textContent).toBe("100");
    expect(screen.getByTestId("canvas-offset").textContent).toBe("0,0");
  });

  it("prevents browser pinch zoom when canvas zoom is already clamped", () => {
    renderCanvasShell({
      afterCanvas: <CanvasStateProbe />,
      initialState: {
        canvas: {
          offset: { x: 0, y: 0 },
          size: { height: 200, unit: "px", width: 300 },
          zoom: 400,
        },
      },
      schema: createSchema(),
    });

    const canvas = screen.getByRole("application", { name: "Canvas viewport" });
    mockCanvasRect(canvas);

    const wheelEvent = new WheelEvent("wheel", {
      bubbles: true,
      cancelable: true,
      clientX: 600,
      clientY: 300,
      ctrlKey: true,
      deltaY: -100,
    });

    expect(canvas.dispatchEvent(wheelEvent)).toBe(false);
    expect(wheelEvent.defaultPrevented).toBe(true);
    expect(screen.getByTestId("canvas-zoom").textContent).toBe("400");
    expect(screen.getByTestId("canvas-offset").textContent).toBe("0,0");
  });

  it("treats clamp continuation and reverse as one persisted wheel boundary", () => {
    vi.useFakeTimers();
    const setItem = vi.spyOn(Storage.prototype, "setItem");
    const schema = defineToolcraft({
      canvas: { enabled: true },
      panels: {},
      persistence: {
        include: ["canvas", "panels"],
        key: "toolcraft:wheel-clamp:state:v1",
        storage: "localStorage",
        version: 1,
      },
    });
    let store: ToolcraftExternalStore | undefined;
    renderCanvasShell({
      afterCanvas: <CaptureStore capture={(value) => { store = value; }} />,
      initialState: {
        canvas: {
          offset: { x: 0, y: 0 },
          size: { height: 200, unit: "px", width: 300 },
          zoom: 350,
        },
      },
      schema,
    });
    const canvas = screen.getByRole("application", { name: "Canvas viewport" });
    mockCanvasRect(canvas);
    let committedCanvas = store!.getCommittedState().canvas;
    let committedBoundaries = 0;
    const unsubscribe = store!.subscribe(() => {
      const nextCanvas = store!.getCommittedState().canvas;

      if (nextCanvas !== committedCanvas) {
        committedCanvas = nextCanvas;
        committedBoundaries += 1;
      }
    });
    const pinch = (deltaY: number): void => {
      canvas.dispatchEvent(
        new WheelEvent("wheel", {
          bubbles: true,
          cancelable: true,
          clientX: 400,
          clientY: 300,
          ctrlKey: true,
          deltaY,
        }),
      );
    };

    act(() => {
      vi.advanceTimersByTime(120);
    });
    setItem.mockClear();

    pinch(-100);
    act(() => {
      vi.advanceTimersByTime(100);
    });
    pinch(-100);
    act(() => {
      vi.advanceTimersByTime(30);
    });

    expect(store?.getCommittedState().canvas.zoom).toBe(350);
    expect(committedBoundaries).toBe(0);

    pinch(50);
    expect(store?.getState().canvas.zoom).toBe(375);
    expect(store?.getCommittedState().canvas.zoom).toBe(350);

    act(() => {
      vi.advanceTimersByTime(119);
    });
    expect(committedBoundaries).toBe(0);
    expect(setItem).not.toHaveBeenCalled();

    act(() => {
      vi.advanceTimersByTime(1);
    });
    expect(store?.getCommittedState().canvas.zoom).toBe(375);
    expect(committedBoundaries).toBe(1);

    act(() => {
      vi.advanceTimersByTime(120);
    });
    expect(setItem).toHaveBeenCalledTimes(1);
    expect(
      JSON.parse(
        window.localStorage.getItem("toolcraft:wheel-clamp:state:v1") ?? "{}",
      ),
    ).toMatchObject({ state: { canvas: { zoom: 375 } } });
    unsubscribe();
  });
});
