// @vitest-environment happy-dom

import React, { act } from "react";
import { createRoot } from "react-dom/client";
import { describe, expect, it, vi } from "vitest";
import { Window } from "happy-dom";
import {
  DomEditOverlay,
  filterNestedDomEditGroupItems,
  focusDomEditOverlayElement,
  hasDomEditRotationChanged,
  resolveDomEditCoordinateScale,
  resolveDomEditGroupOverlayRect,
  resolveDomEditResizeGesture,
  resolveDomEditRotationGesture,
} from "./DomEditOverlay";
import type { DomEditSelection } from "./domEditing";

// React 19 warns unless the test environment opts into act().
globalThis.IS_REACT_ACT_ENVIRONMENT = true;

vi.mock("./useDomEditOverlayGestures", () => ({
  createDomEditOverlayGestureHandlers: () => ({
    startGesture: () => true,
    startGroupDrag: () => {},
    onPointerMove: () => {},
    onPointerUp: () => {},
    clearPointerState: () => {},
  }),
}));

vi.mock("./useDomEditOverlayRects", async () => {
  const React = await import("react");
  const { rectsEqual } = await import("./domEditOverlayGeometry");

  return {
    useDomEditOverlayRects: () => {
      const [overlayRect, setOverlayRectState] = React.useState(null);
      const overlayRectRef = React.useRef(null);
      const [groupOverlayItems, setGroupOverlayItemsState] = React.useState([]);
      const groupOverlayItemsRef = React.useRef([]);

      const setOverlayRect = (next: unknown) => {
        if (rectsEqual(overlayRectRef.current, next)) return;
        overlayRectRef.current = next;
        setOverlayRectState(next);
      };

      const setGroupOverlayItems = (next: unknown[]) => {
        groupOverlayItemsRef.current = next;
        setGroupOverlayItemsState(next);
      };

      return {
        overlayRect,
        overlayRectRef,
        setOverlayRect,
        hoverRect: null,
        hoverRectRef: { current: null },
        setHoverRect: () => {},
        groupOverlayItems,
        groupOverlayItemsRef,
        setGroupOverlayItems,
      };
    },
  };
});

vi.mock("./domEditOverlayGeometry", async () => {
  const actual = await vi.importActual<typeof import("./domEditOverlayGeometry")>(
    "./domEditOverlayGeometry",
  );

  return {
    ...actual,
    toOverlayRect: () => ({
      left: 24,
      top: 36,
      width: 180,
      height: 72,
      editScaleX: 1,
      editScaleY: 1,
    }),
  };
});

describe("focusDomEditOverlayElement", () => {
  it("focuses the canvas overlay without scrolling", () => {
    const calls: Array<FocusOptions | undefined> = [];
    focusDomEditOverlayElement({
      focus: (options?: FocusOptions) => calls.push(options),
    });

    expect(calls).toEqual([{ preventScroll: true }]);
  });
});

describe("DomEditOverlay", () => {
  it("renders selected bounds right after clicking a movable selection", () => {
    const host = document.createElement("div");
    document.body.append(host);
    const root = createRoot(host);
    const selection: DomEditSelection = {
      element: document.createElement("div"),
      id: "hero-title",
      selector: ".hero-title",
      selectorIndex: 0,
      sourceFile: "index.html",
      tagName: "div",
      label: "Hero Title",
      textContent: "Hello",
      textFields: [],
      capabilities: {
        canEditText: true,
        canEditLayout: true,
        canMove: true,
        canApplyManualOffset: true,
        canApplyManualSize: false,
        canApplyManualRotation: false,
        canAdjustOpacity: true,
        canAdjustFill: true,
        canAdjustBorderRadius: true,
        canAdjustStroke: true,
        canAdjustShadow: true,
        canAdjustZIndex: true,
      },
      computedStyle: {
        display: "block",
        position: "absolute",
      },
    };

    let currentSelection: DomEditSelection | null = null;
    const iframeRef = { current: document.createElement("iframe") as HTMLIFrameElement | null };
    const originalPointerCapture = HTMLDivElement.prototype.setPointerCapture;
    HTMLDivElement.prototype.setPointerCapture = () => {};

    function Harness() {
      const [selected, setSelected] = React.useState<DomEditSelection | null>(null);
      currentSelection = selected;

      return React.createElement(DomEditOverlay, {
        iframeRef,
        activeCompositionPath: null,
        selection: selected,
        hoverSelection: null,
        groupSelections: [],
        onCanvasMouseDown: () => {},
        onCanvasPointerMove: () => selection,
        onCanvasPointerLeave: () => {},
        onSelectionChange: (next: DomEditSelection) => setSelected(next),
        onBlockedMove: () => {},
        onPathOffsetCommit: () => {},
        onGroupPathOffsetCommit: () => {},
        onBoxSizeCommit: () => {},
        onRotationCommit: () => {},
      });
    }

    act(() => {
      root.render(React.createElement(Harness));
    });

    const overlay = host.querySelector('[aria-label="Composition canvas"]') as HTMLDivElement;
    expect(overlay).toBeTruthy();

    act(() => {
      overlay.dispatchEvent(
        new PointerEvent("pointerdown", {
          bubbles: true,
          button: 0,
          clientX: 120,
          clientY: 80,
        }),
      );
    });

    expect(currentSelection).toBe(selection);
    expect(host.querySelector('[data-dom-edit-selection-box="true"]')).toBeTruthy();

    act(() => {
      root.unmount();
    });
    HTMLDivElement.prototype.setPointerCapture = originalPointerCapture;
    host.remove();
  });
});

describe("resolveDomEditCoordinateScale", () => {
  it("uses the top-level preview scale when no source boundary dimensions are available", () => {
    expect(
      resolveDomEditCoordinateScale({
        rootScaleX: 0.5,
        rootScaleY: 0.5,
      }),
    ).toEqual({
      scaleX: 0.5,
      scaleY: 0.5,
    });
  });

  it("converts source-local pixels through a scaled nested composition host", () => {
    expect(
      resolveDomEditCoordinateScale({
        rootScaleX: 0.5,
        rootScaleY: 0.5,
        sourceRectWidth: 960,
        sourceRectHeight: 540,
        sourceWidth: 1920,
        sourceHeight: 1080,
      }),
    ).toEqual({
      scaleX: 0.25,
      scaleY: 0.25,
    });
  });
});

describe("resolveDomEditGroupOverlayRect", () => {
  it("returns a bounding box that contains every selected element", () => {
    expect(
      resolveDomEditGroupOverlayRect([
        { left: 40, top: 30, width: 80, height: 50, editScaleX: 1, editScaleY: 1 },
        { left: 150, top: 10, width: 30, height: 120, editScaleX: 0.5, editScaleY: 0.5 },
        { left: 20, top: 90, width: 50, height: 20, editScaleX: 2, editScaleY: 2 },
      ]),
    ).toEqual({
      left: 20,
      top: 10,
      width: 160,
      height: 120,
      editScaleX: 1,
      editScaleY: 1,
    });
  });

  it("returns null for an empty group", () => {
    expect(resolveDomEditGroupOverlayRect([])).toBeNull();
  });
});

describe("filterNestedDomEditGroupItems", () => {
  it("keeps top-level selected elements so descendants are not moved twice", () => {
    const window = new Window();
    const parent = window.document.createElement("div");
    const child = window.document.createElement("div");
    const sibling = window.document.createElement("div");
    parent.append(child);

    expect(
      filterNestedDomEditGroupItems([
        { key: "parent", element: parent },
        { key: "child", element: child },
        { key: "sibling", element: sibling },
      ]).map((item) => item.key),
    ).toEqual(["parent", "sibling"]);
  });
});

describe("resolveDomEditResizeGesture", () => {
  it("resizes width and height independently by default", () => {
    expect(
      resolveDomEditResizeGesture({
        originWidth: 240,
        originHeight: 120,
        actualWidth: 240,
        actualHeight: 120,
        scaleX: 1,
        scaleY: 1,
        dx: 30,
        dy: 12,
        uniform: false,
      }),
    ).toEqual({
      overlayWidth: 270,
      overlayHeight: 132,
      width: 270,
      height: 132,
    });
  });

  it("snaps width and height to the same value when Shift is held", () => {
    expect(
      resolveDomEditResizeGesture({
        originWidth: 240,
        originHeight: 120,
        actualWidth: 240,
        actualHeight: 120,
        scaleX: 1,
        scaleY: 1,
        dx: 30,
        dy: 12,
        uniform: true,
      }),
    ).toEqual({
      overlayWidth: 270,
      overlayHeight: 270,
      width: 270,
      height: 270,
    });
  });

  it("uses the dominant pointer delta for uniform shrink", () => {
    expect(
      resolveDomEditResizeGesture({
        originWidth: 300,
        originHeight: 180,
        actualWidth: 300,
        actualHeight: 180,
        scaleX: 1,
        scaleY: 1,
        dx: 8,
        dy: -40,
        uniform: true,
      }),
    ).toMatchObject({
      width: 260,
      height: 260,
    });
  });

  it("writes source-local dimensions when the edited source is scaled down in master view", () => {
    expect(
      resolveDomEditResizeGesture({
        originWidth: 100,
        originHeight: 50,
        actualWidth: 400,
        actualHeight: 200,
        scaleX: 0.25,
        scaleY: 0.25,
        dx: 25,
        dy: 10,
        uniform: false,
      }),
    ).toEqual({
      overlayWidth: 125,
      overlayHeight: 60,
      width: 500,
      height: 240,
    });
  });
});

describe("resolveDomEditRotationGesture", () => {
  it("rotates by the pointer angle around the element center", () => {
    expect(
      resolveDomEditRotationGesture({
        centerX: 0,
        centerY: 0,
        startX: 0,
        startY: -10,
        currentX: 10,
        currentY: 0,
        actualAngle: 5,
        snap: false,
      }),
    ).toEqual({ angle: 95 });
  });

  it("uses the shortest delta across the 180 degree boundary", () => {
    expect(
      resolveDomEditRotationGesture({
        centerX: 0,
        centerY: 0,
        startX: -10,
        startY: 1.76,
        currentX: -10,
        currentY: -1.76,
        actualAngle: 0,
        snap: false,
      }).angle,
    ).toBeCloseTo(20, 1);
  });

  it("snaps to 15 degree increments when requested", () => {
    expect(
      resolveDomEditRotationGesture({
        centerX: 0,
        centerY: 0,
        startX: 10,
        startY: 0,
        currentX: 10,
        currentY: 3.25,
        actualAngle: 0,
        snap: true,
      }),
    ).toEqual({ angle: 15 });
  });

  it("allows small pointer movements when the rounded angle changes", () => {
    const nextRotation = resolveDomEditRotationGesture({
      centerX: 0,
      centerY: 0,
      startX: 0,
      startY: -40,
      currentX: 1,
      currentY: -40,
      actualAngle: 0,
      snap: false,
    });

    expect(nextRotation.angle).toBe(1.4);
    expect(hasDomEditRotationChanged(0, nextRotation.angle)).toBe(true);
    expect(hasDomEditRotationChanged(0, 0)).toBe(false);
  });
});
