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

vi.mock("motion/react", async (importOriginal) => {
  const actual = await importOriginal<typeof import("motion/react")>();
  type TestDragInfo = { velocity: { x: number; y: number } };
  type TestMotionDivProps = Omit<
    React.HTMLAttributes<HTMLDivElement>,
    "onDragEnd" | "style"
  > & {
    animate?: unknown;
    drag?: unknown;
    dragControls?: unknown;
    dragElastic?: unknown;
    dragListener?: unknown;
    dragMomentum?: unknown;
    dragTransition?: unknown;
    initial?: unknown;
    onDragEnd?: (event: Event, info: TestDragInfo) => void;
    onDragStart?: () => void;
    style?: React.CSSProperties & {
      x?: { set: (value: number) => void };
      y?: { set: (value: number) => void };
    };
    transition?: unknown;
  };
  const MotionDiv = React.forwardRef<HTMLDivElement, TestMotionDivProps>(
    function MotionDiv(
      {
        animate: _animate,
        children,
        drag: _drag,
        dragControls: _dragControls,
        dragElastic: _dragElastic,
        dragListener: _dragListener,
        dragMomentum: _dragMomentum,
        dragTransition: _dragTransition,
        initial: _initial,
        onDragEnd,
        onDragStart,
        style,
        transition: _transition,
        ...props
      },
      ref,
    ) {
      return (
        <div
          {...props}
          onDragEnd={(event) => {
            const detail: unknown = Reflect.get(event.nativeEvent, "detail");

            if (
              typeof detail !== "object" ||
              detail === null ||
              !("x" in detail) ||
              typeof detail.x !== "number" ||
              !("y" in detail) ||
              typeof detail.y !== "number"
            ) {
              throw new Error("Expected a numeric test drag position.");
            }

            style?.x?.set(detail.x);
            style?.y?.set(detail.y);
            onDragEnd?.(event.nativeEvent, {
              velocity: { x: 0, y: 0 },
            });
          }}
          onDragStart={onDragStart}
          ref={ref}
        >
          {children}
        </div>
      );
    },
  );

  return {
    ...actual,
    motion: { ...actual.motion, div: MotionDiv },
  };
});

import { defineToolcraft } from "../../schema/define-toolcraft";
import { ToolcraftRoot } from "../app-shell/toolcraft-root";
import { useToolcraft } from "../app-shell/use-toolcraft";
import { ToolcraftPanelHost } from "./panel-host";

const schema = defineToolcraft({ canvas: { enabled: true }, panels: {} });

function ToolbarPlacementProbe(): React.JSX.Element {
  const { state } = useToolcraft();
  const { offset, snapEdge } = state.panels.toolbar;

  return (
    <span data-testid="toolbar-placement">
      {offset.x},{offset.y},{snapEdge ?? "free"}
    </span>
  );
}

function renderToolbar(offset = { x: 0, y: 0 }) {
  return render(
    <ToolcraftRoot
      initialState={{ panels: { toolbar: { offset } } }}
      schema={schema}
    >
      <ToolbarPlacementProbe />
      <ToolcraftPanelHost panelType="toolbar">
        <div>Toolbar</div>
      </ToolcraftPanelHost>
    </ToolcraftRoot>,
  );
}

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

describe("ToolcraftPanelHost runtime placement", () => {
  it("commits the final free drag position through runtime panel state", async () => {
    const { container } = renderToolbar();
    const host = container.querySelector<HTMLElement>(
      "[data-slot='toolcraft-runtime-panel-host']",
    );

    if (!host) {
      throw new Error("Expected the toolbar panel host to render.");
    }
    vi.spyOn(host, "getBoundingClientRect").mockReturnValue({
      bottom: 300,
      height: 100,
      left: 200,
      right: 300,
      top: 200,
      width: 100,
      x: 200,
      y: 200,
      toJSON: () => ({}),
    });

    fireEvent(
      host,
      new CustomEvent("dragend", {
        bubbles: true,
        detail: { x: 40, y: 80 },
      }),
    );

    await waitFor(() => {
      expect(screen.getByTestId("toolbar-placement").textContent).toBe(
        "40,80,free",
      );
    });
  });

  it("clamps restored runtime placement after a viewport resize", async () => {
    Object.defineProperty(window, "innerWidth", {
      configurable: true,
      value: 800,
    });
    Object.defineProperty(window, "innerHeight", {
      configurable: true,
      value: 500,
    });
    const { container } = renderToolbar({ x: 900, y: -200 });
    const host = container.querySelector<HTMLElement>(
      "[data-slot='toolcraft-runtime-panel-host']",
    );

    if (!host) {
      throw new Error("Expected the toolbar panel host to render.");
    }
    vi.spyOn(host, "getBoundingClientRect").mockReturnValue({
      bottom: -50,
      height: 100,
      left: 1000,
      right: 1200,
      top: -150,
      width: 200,
      x: 1000,
      y: -150,
      toJSON: () => ({}),
    });

    fireEvent(window, new Event("resize"));

    await waitFor(() => {
      expect(screen.getByTestId("toolbar-placement").textContent).toBe(
        "490,-40,free",
      );
    });
  });

  it("keeps the CSS-anchored default placement during viewport resize", async () => {
    const { container } = renderToolbar();
    const host = container.querySelector<HTMLElement>(
      "[data-slot='toolcraft-runtime-panel-host']",
    );

    if (!host) {
      throw new Error("Expected the toolbar panel host to render.");
    }
    vi.spyOn(host, "getBoundingClientRect").mockReturnValue({
      bottom: 900,
      height: 100,
      left: 1_500,
      right: 1_700,
      top: 800,
      width: 200,
      x: 1_500,
      y: 800,
      toJSON: () => ({}),
    });

    fireEvent(window, new Event("resize"));

    await waitFor(() => {
      expect(screen.getByTestId("toolbar-placement").textContent).toBe(
        "0,0,free",
      );
    });
  });
});
