// @vitest-environment jsdom

import { act, useEffect, useRef, useState } from "react";
import { createRoot, type Root } from "react-dom/client";
import type { Edge as RFEdge, Node as RFNode } from "@xyflow/react";
import { afterEach, describe, expect, it, vi } from "vitest";

import type { Id } from "@/convex/_generated/dataModel";

const mocks = vi.hoisted(() => ({
  enqueueCanvasSyncOp: vi.fn(async () => ({ replacedIds: [] as string[] })),
  countCanvasSyncOps: vi.fn(async () => 0),
  listCanvasSyncOps: vi.fn(async () => []),
  mutationMocks: new Map<unknown, ReturnType<typeof vi.fn>>(),
}));

vi.mock("@/convex/_generated/api", () => ({
  api: {
    nodes: {
      move: "nodes.move",
      resize: "nodes.resize",
      updateData: "nodes.updateData",
      create: "nodes.create",
      createWithEdgeFromSource: "nodes.createWithEdgeFromSource",
      createWithEdgeToTarget: "nodes.createWithEdgeToTarget",
      createWithEdgeSplit: "nodes.createWithEdgeSplit",
      batchRemove: "nodes.batchRemove",
      splitEdgeAtExistingNode: "nodes.splitEdgeAtExistingNode",
    },
    edges: {
      create: "edges.create",
      remove: "edges.remove",
    },
  },
}));

vi.mock("convex/react", () => ({
  useConvexConnectionState: () => ({ isWebSocketConnected: true }),
  useMutation: (key: unknown) => {
    let mutation = mocks.mutationMocks.get(key);
    if (!mutation) {
      mutation = vi.fn(async () => undefined);
      Object.assign(mutation, {
        withOptimisticUpdate: () => mutation,
      });
      mocks.mutationMocks.set(key, mutation);
    }
    return mutation;
  },
}));

vi.mock("@/lib/canvas-op-queue", () => ({
  ackCanvasSyncOp: vi.fn(async () => undefined),
  countCanvasSyncOps: mocks.countCanvasSyncOps,
  dropCanvasSyncOpsByClientRequestIds: vi.fn(async () => []),
  dropCanvasSyncOpsByEdgeIds: vi.fn(async () => []),
  dropCanvasSyncOpsByNodeIds: vi.fn(async () => []),
  dropExpiredCanvasSyncOps: vi.fn(async () => []),
  enqueueCanvasSyncOp: mocks.enqueueCanvasSyncOp,
  listCanvasSyncOps: mocks.listCanvasSyncOps,
  markCanvasSyncOpFailed: vi.fn(async () => undefined),
  remapCanvasSyncNodeId: vi.fn(async () => 0),
}));

vi.mock("@/lib/canvas-local-persistence", () => ({
  dropCanvasOpsByClientRequestIds: vi.fn(() => []),
  dropCanvasOpsByEdgeIds: vi.fn(() => []),
  dropCanvasOpsByNodeIds: vi.fn(() => []),
  enqueueCanvasOp: vi.fn(() => "op-1"),
  remapCanvasOpNodeId: vi.fn(() => 0),
  resolveCanvasOp: vi.fn(() => undefined),
  resolveCanvasOps: vi.fn(() => undefined),
}));

vi.mock("@/lib/toast", () => ({
  toast: {
    info: vi.fn(),
    warning: vi.fn(),
  },
}));

import { useCanvasSyncEngine } from "@/components/canvas/use-canvas-sync-engine";

const asCanvasId = (id: string): Id<"canvases"> => id as Id<"canvases">;
const asNodeId = (id: string): Id<"nodes"> => id as Id<"nodes">;

const latestHookValueRef: {
  current: ReturnType<typeof useCanvasSyncEngine> | null;
} = { current: null };

(globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true;

function HookHarness({ canvasId }: { canvasId: Id<"canvases"> }) {
  const [, setNodes] = useState<RFNode[]>([]);
  const [edges, setEdges] = useState<RFEdge[]>([]);
  const edgesRef = useRef<RFEdge[]>(edges);
  const deletingNodeIds = useRef(new Set<string>());
  const [, setAssetBrowserTargetNodeId] = useState<string | null>(null);
  const [, setEdgeSyncNonce] = useState(0);

  useEffect(() => {
    edgesRef.current = edges;
  }, [edges]);

  const hookValue = useCanvasSyncEngine({
    canvasId,
    setNodes,
    setEdges,
    edgesRef,
    setAssetBrowserTargetNodeId,
    setEdgeSyncNonce,
    deletingNodeIds,
  });

  useEffect(() => {
    latestHookValueRef.current = hookValue;
  }, [hookValue]);

  return null;
}

describe("useCanvasSyncEngine hook wiring", () => {
  let container: HTMLDivElement | null = null;
  let root: Root | null = null;

  afterEach(async () => {
    latestHookValueRef.current = null;
    mocks.mutationMocks.clear();
    vi.clearAllMocks();
    if (root) {
      await act(async () => {
        root?.unmount();
      });
    }
    container?.remove();
    root = null;
    container = null;
  });

  it("uses the latest canvas id after rerendering the mounted hook", async () => {
    container = document.createElement("div");
    document.body.appendChild(container);
    root = createRoot(container);

    await act(async () => {
      root?.render(<HookHarness canvasId={asCanvasId("canvas-1")} />);
    });

    await act(async () => {
      root?.render(<HookHarness canvasId={asCanvasId("canvas-2")} />);
    });

    await act(async () => {
      await latestHookValueRef.current?.actions.resizeNode({
        nodeId: asNodeId("node-1"),
        width: 480,
        height: 320,
      });
    });

    expect(mocks.enqueueCanvasSyncOp).toHaveBeenLastCalledWith(
      expect.objectContaining({
        canvasId: "canvas-2",
        type: "resizeNode",
        payload: {
          nodeId: "node-1",
          width: 480,
          height: 320,
        },
      }),
    );
  });
});
