import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
import { render, screen, fireEvent, waitFor } from "@testing-library/react";
import { WorkflowCanvas, isPointInQuickAddHotZone } from "@/components/WorkflowCanvas";
import { ReactFlowProvider } from "@xyflow/react";

// Mock the workflow store
const mockOnNodesChange = vi.fn();
const mockOnEdgesChange = vi.fn();
const mockOnConnect = vi.fn();
const mockAddNode = vi.fn().mockReturnValue("new-node-id");
const mockUpdateNodeData = vi.fn();
const mockLoadWorkflow = vi.fn();
const mockGetNodeById = vi.fn();
const mockAddToGlobalHistory = vi.fn();
const mockSetNodeGroupId = vi.fn();
const mockExecuteWorkflow = vi.fn();
const mockCopySelectedNodes = vi.fn();
const mockPasteNodes = vi.fn();
const mockClearClipboard = vi.fn();
const mockSetShowQuickstart = vi.fn();
const mockSetHoveredNodeId = vi.fn();
const mockRegenerateNode = vi.fn();
const mockExecuteSelectedNodes = vi.fn();
const mockClearWorkflow = vi.fn();
const mockSelectSingleNode = vi.fn();
const mockUpdateCanvasNavigationSettings = vi.fn();
const mockUseWorkflowStore = vi.fn();

vi.mock("@/store/workflowStore", () => ({
  useWorkflowStore: (selector?: (state: unknown) => unknown) => {
    if (selector) {
      return mockUseWorkflowStore(selector);
    }
    return mockUseWorkflowStore((s: unknown) => s);
  },
  useProviderApiKeys: () => ({
    replicateApiKey: null,
    falApiKey: null,
    kieApiKey: null,
    wavespeedApiKey: null,
  }),
}));

// Mock useReactFlow
const mockScreenToFlowPosition = vi.fn((pos) => pos);
const mockGetViewport = vi.fn(() => ({ x: 0, y: 0, zoom: 1 }));
const mockZoomIn = vi.fn();
const mockZoomOut = vi.fn();
const mockSetViewport = vi.fn();
const mockSetCenter = vi.fn();
const mockSetNodes = vi.fn();
const mockFitView = vi.fn();

vi.mock("@xyflow/react", async () => {
  const actual = await vi.importActual("@xyflow/react");
  return {
    ...actual,
    useReactFlow: () => ({
      screenToFlowPosition: mockScreenToFlowPosition,
      getViewport: mockGetViewport,
      zoomIn: mockZoomIn,
      zoomOut: mockZoomOut,
      setViewport: mockSetViewport,
      setCenter: mockSetCenter,
      setNodes: mockSetNodes,
      fitView: mockFitView,
    }),
  };
});

// Mock the child components that aren't being tested
vi.mock("@/components/ConnectionDropMenu", () => ({
  ConnectionDropMenu: ({ handleType, connectionType, mode, sourceNodeType, onSelect }: any) => (
    <div
      data-testid="connection-drop-menu"
      data-handle-type={handleType}
      data-connection-type={connectionType}
      data-mode={mode}
      data-source-node-type={sourceNodeType}
    >
      <button onClick={() => onSelect({ type: "nanoBanana", isAction: false })}>
        Create Generate Image
      </button>
      <button onClick={() => onSelect({ type: "multiAngleImage", isAction: true })}>
        Open Multi Angle
      </button>
    </div>
  ),
}));

vi.mock("@/components/MultiSelectToolbar", () => ({
  MultiSelectToolbar: () => <div data-testid="multi-select-toolbar" />,
}));

vi.mock("@/components/EdgeToolbar", () => ({
  EdgeToolbar: () => <div data-testid="edge-toolbar" />,
}));

vi.mock("@/components/GlobalImageHistory", () => ({
  GlobalImageHistory: () => null,
}));

vi.mock("@/components/composer/GenerationComposer", () => ({
  GenerationComposer: () => null,
}));

vi.mock("@/components/GroupsOverlay", () => ({
  GroupBackgroundsPortal: () => null,
  GroupControlsOverlay: () => null,
}));

vi.mock("@/components/quickstart", () => ({
  WelcomeModal: () => <div data-testid="welcome-modal" />,
}));

vi.mock("@/utils/gridSplitter", () => ({
  detectAndSplitGrid: vi.fn(),
}));

vi.mock("@/utils/logger", () => ({
  logger: {
    log: vi.fn(),
    error: vi.fn(),
  },
}));

// Create mock nodes for testing
const createMockNode = (id: string, type: string, overrides = {}) => ({
  id,
  type,
  position: { x: 100, y: 100 },
  data: {},
  selected: false,
  ...overrides,
});

// Default provider settings
const defaultProviderSettings = {
  providers: {
    gemini: { id: "gemini", name: "Gemini", enabled: true, apiKey: null, apiKeyEnvVar: "GEMINI_API_KEY" },
    openai: { id: "openai", name: "OpenAI", enabled: false, apiKey: null },
    replicate: { id: "replicate", name: "Replicate", enabled: false, apiKey: null },
    fal: { id: "fal", name: "fal.ai", enabled: true, apiKey: null },
    kie: { id: "kie", name: "Kie.ai", enabled: false, apiKey: null },
    wavespeed: { id: "wavespeed", name: "WaveSpeed", enabled: false, apiKey: null },
  },
};

// Default store state factory
const createDefaultState = (overrides = {}) => ({
  nodes: [],
  edges: [],
  groups: {},
  onNodesChange: mockOnNodesChange,
  onEdgesChange: mockOnEdgesChange,
  onConnect: mockOnConnect,
  addNode: mockAddNode,
  updateNodeData: mockUpdateNodeData,
  loadWorkflow: mockLoadWorkflow,
  getNodeById: mockGetNodeById,
  addToGlobalHistory: mockAddToGlobalHistory,
  setNodeGroupId: mockSetNodeGroupId,
  executeWorkflow: mockExecuteWorkflow,
  executeSelectedNodes: mockExecuteSelectedNodes,
  isRunning: false,
  isModalOpen: false,
  showQuickstart: false,
  setShowQuickstart: mockSetShowQuickstart,
  copySelectedNodes: mockCopySelectedNodes,
  pasteNodes: mockPasteNodes,
  clearClipboard: mockClearClipboard,
  clipboard: null,
  providerSettings: defaultProviderSettings,
  edgeStyle: "angular" as const,
  currentNodeIds: [],
  navigationTarget: null,
  setNavigationTarget: vi.fn(),
  getNodesWithComments: vi.fn(() => []),
  getConnectedInputs: vi.fn(() => ({ images: [], videos: [], audio: [], text: null })),
  markCommentViewed: vi.fn(),
  canvasNavigationSettings: { panMode: "space", zoomMode: "altScroll", selectionMode: "click", assistantEnabled: false },
  updateCanvasNavigationSettings: mockUpdateCanvasNavigationSettings,
  dimmedNodeIds: new Set<string>(),
  skippedNodeIds: new Set<string>(),
  hoveredNodeId: null,
  setHoveredNodeId: mockSetHoveredNodeId,
  captureSnapshot: vi.fn(),
  applyEditOperations: vi.fn(() => ({ applied: 0, skipped: [] })),
  setWorkflowMetadata: vi.fn(),
  regenerateNode: mockRegenerateNode,
  clearWorkflow: mockClearWorkflow,
  selectSingleNode: mockSelectSingleNode,
  ...overrides,
});

// Wrapper component for React Flow context
function TestWrapper({ children }: { children: React.ReactNode }) {
  return <ReactFlowProvider>{children}</ReactFlowProvider>;
}

describe("WorkflowCanvas", () => {
  const originalProviderMode = process.env.NEXT_PUBLIC_PROVIDER_MODE;

  beforeEach(() => {
    vi.clearAllMocks();
    // Default mock implementation
    mockUseWorkflowStore.mockImplementation((selector) => {
      return selector(createDefaultState());
    });
  });

  afterEach(() => {
    vi.unstubAllGlobals();
    if (originalProviderMode === undefined) {
      delete process.env.NEXT_PUBLIC_PROVIDER_MODE;
    } else {
      process.env.NEXT_PUBLIC_PROVIDER_MODE = originalProviderMode;
    }
  });

  describe("Basic Rendering", () => {
    it("should render ReactFlow component", () => {
      render(
        <TestWrapper>
          <WorkflowCanvas />
        </TestWrapper>
      );

      // ReactFlow container should be present
      expect(document.querySelector(".react-flow")).toBeInTheDocument();
    });

    it("should render Background component", () => {
      render(
        <TestWrapper>
          <WorkflowCanvas />
        </TestWrapper>
      );

      // Background pattern should be rendered
      expect(document.querySelector(".react-flow__background")).toBeInTheDocument();
    });

    it("should render Controls component", () => {
      render(
        <TestWrapper>
          <WorkflowCanvas />
        </TestWrapper>
      );

      // Controls panel should be rendered
      expect(document.querySelector(".react-flow__controls")).toBeInTheDocument();
    });

    it("should render MiniMap component", () => {
      render(
        <TestWrapper>
          <WorkflowCanvas />
        </TestWrapper>
      );

      // MiniMap should be rendered
      expect(document.querySelector(".react-flow__minimap")).toBeInTheDocument();
    });

    it("should render EdgeToolbar component", () => {
      render(
        <TestWrapper>
          <WorkflowCanvas />
        </TestWrapper>
      );

      expect(screen.getByTestId("edge-toolbar")).toBeInTheDocument();
    });

    it("should keep the canvas assistant closed and hidden by default", () => {
      render(
        <TestWrapper>
          <WorkflowCanvas />
        </TestWrapper>
      );

      expect(screen.queryByRole("heading", { name: /Canvas Assistant/i })).not.toBeInTheDocument();
      expect(screen.queryByRole("button", { name: "Assistant" })).not.toBeInTheDocument();
    });

    it("should show the canvas assistant on the right when the canvas setting is enabled", () => {
      mockUseWorkflowStore.mockImplementation((selector) => {
        return selector(createDefaultState({
          canvasNavigationSettings: {
            panMode: "space",
            zoomMode: "altScroll",
            selectionMode: "click",
            assistantEnabled: true,
          },
        }));
      });

      render(
        <TestWrapper>
          <WorkflowCanvas />
        </TestWrapper>
      );

      expect(screen.getByRole("heading", { name: /Canvas Assistant/i })).toBeInTheDocument();
      expect(screen.getByRole("heading", { name: /Canvas Assistant/i }).closest(".fixed")).toHaveClass("right-0");
    });

    it("should disable the canvas assistant setting when the right assistant panel is closed", () => {
      const enabledSettings = {
        panMode: "space" as const,
        zoomMode: "altScroll" as const,
        selectionMode: "click" as const,
        assistantEnabled: true,
      };
      mockUseWorkflowStore.mockImplementation((selector) => {
        return selector(createDefaultState({
          canvasNavigationSettings: enabledSettings,
        }));
      });

      render(
        <TestWrapper>
          <WorkflowCanvas />
        </TestWrapper>
      );

      fireEvent.click(screen.getByRole("button", { name: "Close chat" }));

      expect(mockUpdateCanvasNavigationSettings).toHaveBeenCalledWith({
        ...enabledSettings,
        assistantEnabled: false,
      });
    });

    it("should center and zoom a node when the node shell is double-clicked", async () => {
      mockGetViewport.mockReturnValue({ x: 0, y: 0, zoom: 0.6 });
      mockUseWorkflowStore.mockImplementation((selector) => {
        return selector(createDefaultState({
          nodes: [
            createMockNode("prompt-1", "prompt", {
              position: { x: 200, y: 120 },
              style: { width: 320, height: 220 },
            }),
          ],
        }));
      });

      const { container } = render(
        <TestWrapper>
          <WorkflowCanvas />
        </TestWrapper>
      );

      const node = await waitFor(() => {
        const element = container.querySelector('.react-flow__node[data-id="prompt-1"]');
        expect(element).toBeInTheDocument();
        return element;
      });

      fireEvent.doubleClick(node!);

      expect(mockSelectSingleNode).toHaveBeenCalledWith("prompt-1");
      expect(mockSetCenter).toHaveBeenCalledWith(360, 230, {
        duration: 300,
        zoom: 1,
      });
    });

    it("should render MultiSelectToolbar component", () => {
      render(
        <TestWrapper>
          <WorkflowCanvas />
        </TestWrapper>
      );

      expect(screen.getByTestId("multi-select-toolbar")).toBeInTheDocument();
    });

    it("hides the fallback model button in Popi provider mode", async () => {
      process.env.NEXT_PUBLIC_PROVIDER_MODE = "popi";
      vi.stubGlobal("fetch", vi.fn(() => Promise.resolve({
        ok: true,
        json: () => Promise.resolve({ success: true, models: [] }),
      })));
      mockUseWorkflowStore.mockImplementation((selector) => {
        return selector(createDefaultState({
          nodes: [
            createMockNode("video-1", "generateVideo", {
              selected: true,
              data: {
                selectedModel: {
                  provider: "newapiwg",
                  modelId: "video-model",
                  displayName: "Video Model",
                },
              },
            }),
          ],
        }));
      });

      render(
        <TestWrapper>
          <WorkflowCanvas />
        </TestWrapper>
      );

      await waitFor(() => {
        expect(screen.getAllByText("Video Model").length).toBeGreaterThan(0);
      });
      expect(screen.queryByTitle("Set fallback model (runs if primary fails)")).not.toBeInTheDocument();
    });

    it("treats the plus affordance region as a quick-add hot zone", () => {
      const node = createMockNode("image-1", "imageInput", {
        measured: { width: 300, height: 220 },
      });

      expect(isPointInQuickAddHotZone(node as any, { x: 418, y: 210 })).toBe(true);
      expect(isPointInQuickAddHotZone(node as any, { x: 510, y: 210 })).toBe(false);
    });

    it("renders the visible quick-add affordance without taking pointer events", async () => {
      mockUseWorkflowStore.mockImplementation((selector) => {
        return selector(createDefaultState({
          nodes: [
            createMockNode("image-1", "imageInput", {
              selected: true,
              measured: { width: 300, height: 220 },
            }),
          ],
        }));
      });

      render(
        <TestWrapper>
          <WorkflowCanvas />
        </TestWrapper>
      );

      const affordance = await screen.findByTestId("node-quick-add-affordance");
      expect(affordance).toHaveClass("pointer-events-none");
      expect(affordance).toHaveAttribute("aria-hidden", "true");
    });

    it("should open the connection menu from a node quick-add button and create a connected node", async () => {
      mockUseWorkflowStore.mockImplementation((selector) => {
        return selector(createDefaultState({
          nodes: [
            createMockNode("image-1", "imageInput", {
              selected: true,
              measured: { width: 300, height: 220 },
            }),
          ],
        }));
      });

      render(
        <TestWrapper>
          <WorkflowCanvas />
        </TestWrapper>
      );

      const quickAddButton = screen.getByRole("button", { name: "Add connected node" });
      fireEvent.pointerDown(quickAddButton, {
        clientX: 418,
        clientY: 210,
      });
      fireEvent.click(quickAddButton, {
        clientX: 418,
        clientY: 210,
      });

      const menu = await screen.findByTestId("connection-drop-menu");
      expect(menu).toHaveAttribute("data-handle-type", "image");
      expect(menu).toHaveAttribute("data-connection-type", "source");
      expect(menu).toHaveAttribute("data-mode", "quickAdd");
      expect(menu).toHaveAttribute("data-source-node-type", "imageInput");

      fireEvent.click(screen.getByText("Create Generate Image"));

      expect(mockAddNode).toHaveBeenCalledWith("nanoBanana", { x: 560, y: 100 });
      expect(mockOnConnect).toHaveBeenCalledWith({
        source: "image-1",
        sourceHandle: "image",
        target: "new-node-id",
        targetHandle: "image",
      });
      expect(mockSelectSingleNode).toHaveBeenCalledWith("new-node-id");
    });

    it("should open the image multi-angle editor from the connection menu and generate a connected image node", async () => {
      const sourceNode = createMockNode("image-1", "imageInput", {
        selected: true,
        measured: { width: 300, height: 220 },
        data: {
          image: "data:image/png;base64,input",
          filename: "input.png",
          dimensions: { width: 640, height: 480 },
        },
      });

      mockGetNodeById.mockImplementation((id) => (id === "image-1" ? sourceNode : null));
      mockUseWorkflowStore.mockImplementation((selector) => {
        return selector(createDefaultState({
          nodes: [sourceNode],
        }));
      });

      render(
        <TestWrapper>
          <WorkflowCanvas />
        </TestWrapper>
      );

      const quickAddButton = screen.getByRole("button", { name: "Add connected node" });
      fireEvent.pointerDown(quickAddButton, {
        clientX: 418,
        clientY: 210,
      });
      fireEvent.click(quickAddButton, {
        clientX: 418,
        clientY: 210,
      });

      fireEvent.click(await screen.findByText("Open Multi Angle"));

      expect(screen.getByRole("dialog", { name: "Image Multi-Angle Editor" })).toBeInTheDocument();
      fireEvent.click(screen.getByRole("button", { name: "Fisheye" }));
      fireEvent.click(screen.getByRole("button", { name: "Generate multi-angle" }));

      await waitFor(() => {
        expect(mockRegenerateNode).toHaveBeenCalledWith("new-node-id");
      });
      expect(mockAddNode).toHaveBeenCalledWith(
        "nanoBanana",
        { x: 560, y: 100 },
        expect.objectContaining({
          inputImages: ["data:image/png;base64,input"],
          inputPrompt: expect.stringContaining("超近距离鱼眼镜头"),
          aspectRatio: "16:9",
          resolution: "2K",
        })
      );
      expect(mockOnConnect).toHaveBeenCalledWith({
        source: "image-1",
        sourceHandle: "image",
        target: "new-node-id",
        targetHandle: "image",
      });
      expect(mockSelectSingleNode).toHaveBeenCalledWith("new-node-id");
    });
  });

  describe("Welcome Modal", () => {
    it("should show welcome modal when canvas is empty and showQuickstart is true", () => {
      mockUseWorkflowStore.mockImplementation((selector) => {
        return selector(createDefaultState({
          nodes: [],
          showQuickstart: true,
        }));
      });

      render(
        <TestWrapper>
          <WorkflowCanvas />
        </TestWrapper>
      );

      expect(screen.getByTestId("welcome-modal")).toBeInTheDocument();
    });

    it("should not show welcome modal when canvas has nodes", () => {
      // When a workflow with nodes is loaded, the store sets showQuickstart to false.
      // The component renders the modal based solely on showQuickstart state.
      mockUseWorkflowStore.mockImplementation((selector) => {
        return selector(createDefaultState({
          nodes: [createMockNode("node-1", "prompt")],
          showQuickstart: false,
        }));
      });

      render(
        <TestWrapper>
          <WorkflowCanvas />
        </TestWrapper>
      );

      expect(screen.queryByTestId("welcome-modal")).not.toBeInTheDocument();
    });

    it("should not show welcome modal when showQuickstart is false", () => {
      mockUseWorkflowStore.mockImplementation((selector) => {
        return selector(createDefaultState({
          nodes: [],
          showQuickstart: false,
        }));
      });

      render(
        <TestWrapper>
          <WorkflowCanvas />
        </TestWrapper>
      );

      expect(screen.queryByTestId("welcome-modal")).not.toBeInTheDocument();
    });
  });

  describe("Node Types Registration", () => {
    it("should register all required node types for the canvas", () => {
      // We verify the canvas renders with the node types object defined
      // The actual node type registration happens at module level
      // This test confirms the canvas component can be rendered
      render(
        <TestWrapper>
          <WorkflowCanvas />
        </TestWrapper>
      );

      // Canvas should render and have the ReactFlow component
      expect(document.querySelector(".react-flow")).toBeInTheDocument();
    });
  });

  describe("Edge Types Registration", () => {
    it("should register editable and reference edge types for the canvas", () => {
      // Edge types are registered at module level
      // This test confirms the canvas renders with edge type config
      render(
        <TestWrapper>
          <WorkflowCanvas />
        </TestWrapper>
      );

      // Canvas should render with edge types configured
      expect(document.querySelector(".react-flow")).toBeInTheDocument();
    });
  });

  describe("Drag and Drop", () => {
    it("should show drop overlay when dragging node type over canvas", () => {
      render(
        <TestWrapper>
          <WorkflowCanvas />
        </TestWrapper>
      );

      const canvas = document.querySelector(".bg-canvas-bg") as HTMLElement;

      // Simulate drag over with node type data
      const mockDataTransfer = {
        types: ["application/node-type"],
        items: [],
        dropEffect: "",
        effectAllowed: "",
        getData: vi.fn(),
      };

      fireEvent.dragOver(canvas, {
        dataTransfer: mockDataTransfer,
        preventDefault: vi.fn(),
      });

      // Should show "Drop to create node" indicator
      expect(screen.getByText("Drop to create node")).toBeInTheDocument();
    });

    it("should show drop overlay when dragging image over canvas", () => {
      render(
        <TestWrapper>
          <WorkflowCanvas />
        </TestWrapper>
      );

      const canvas = document.querySelector(".bg-canvas-bg") as HTMLElement;

      // Simulate drag over with image file
      const mockDataTransfer = {
        types: [],
        items: [{ kind: "file", type: "image/png" }],
        dropEffect: "",
        effectAllowed: "",
        getData: vi.fn(),
      };

      fireEvent.dragOver(canvas, {
        dataTransfer: mockDataTransfer,
        preventDefault: vi.fn(),
      });

      // Should show "Drop image to create node" indicator
      expect(screen.getByText("Drop image to create node")).toBeInTheDocument();
    });

    it("should show drop overlay when dragging workflow JSON over canvas", () => {
      render(
        <TestWrapper>
          <WorkflowCanvas />
        </TestWrapper>
      );

      const canvas = document.querySelector(".bg-canvas-bg") as HTMLElement;

      // Simulate drag over with JSON file
      const mockDataTransfer = {
        types: [],
        items: [{ kind: "file", type: "application/json" }],
        dropEffect: "",
        effectAllowed: "",
        getData: vi.fn(),
      };

      fireEvent.dragOver(canvas, {
        dataTransfer: mockDataTransfer,
        preventDefault: vi.fn(),
      });

      // Should show "Drop to load workflow" indicator
      expect(screen.getByText("Drop to load workflow")).toBeInTheDocument();
    });

    it("should hide drop overlay on drag leave", () => {
      render(
        <TestWrapper>
          <WorkflowCanvas />
        </TestWrapper>
      );

      const canvas = document.querySelector(".bg-canvas-bg") as HTMLElement;

      // First drag over
      const mockDataTransfer = {
        types: ["application/node-type"],
        items: [],
        dropEffect: "",
        effectAllowed: "",
        getData: vi.fn(),
      };

      fireEvent.dragOver(canvas, {
        dataTransfer: mockDataTransfer,
        preventDefault: vi.fn(),
      });

      expect(screen.getByText("Drop to create node")).toBeInTheDocument();

      // Then drag leave
      fireEvent.dragLeave(canvas, { preventDefault: vi.fn() });

      expect(screen.queryByText("Drop to create node")).not.toBeInTheDocument();
    });

    it("should call addNode when node type is dropped on canvas", () => {
      render(
        <TestWrapper>
          <WorkflowCanvas />
        </TestWrapper>
      );

      const canvas = document.querySelector(".bg-canvas-bg") as HTMLElement;

      const mockDataTransfer = {
        types: ["application/node-type"],
        items: [],
        files: [],
        dropEffect: "",
        effectAllowed: "",
        getData: vi.fn().mockReturnValue("prompt"),
      };

      fireEvent.drop(canvas, {
        dataTransfer: mockDataTransfer,
        preventDefault: vi.fn(),
        clientX: 500,
        clientY: 300,
      });

      expect(mockAddNode).toHaveBeenCalledWith("prompt", expect.any(Object));
    });
  });

  describe("Keyboard Shortcuts", () => {
    it("should call executeWorkflow on Ctrl+Enter", () => {
      render(
        <TestWrapper>
          <WorkflowCanvas />
        </TestWrapper>
      );

      fireEvent.keyDown(window, { key: "Enter", ctrlKey: true });

      expect(mockExecuteWorkflow).toHaveBeenCalled();
    });

    it("should call executeWorkflow on Cmd+Enter", () => {
      render(
        <TestWrapper>
          <WorkflowCanvas />
        </TestWrapper>
      );

      fireEvent.keyDown(window, { key: "Enter", metaKey: true });

      expect(mockExecuteWorkflow).toHaveBeenCalled();
    });

    it("should call copySelectedNodes on Ctrl+C", () => {
      render(
        <TestWrapper>
          <WorkflowCanvas />
        </TestWrapper>
      );

      fireEvent.keyDown(window, { key: "c", ctrlKey: true });

      expect(mockCopySelectedNodes).toHaveBeenCalled();
    });

    it("should not copy when typing in input field", () => {
      render(
        <TestWrapper>
          <WorkflowCanvas />
          <input data-testid="test-input" />
        </TestWrapper>
      );

      const input = screen.getByTestId("test-input");
      fireEvent.keyDown(input, { key: "c", ctrlKey: true });

      // Should not call copy when target is input
      expect(mockCopySelectedNodes).not.toHaveBeenCalled();
    });

    it("should add prompt node on Shift+P", () => {
      render(
        <TestWrapper>
          <WorkflowCanvas />
        </TestWrapper>
      );

      fireEvent.keyDown(window, { key: "p", shiftKey: true });

      expect(mockAddNode).toHaveBeenCalledWith("prompt", expect.any(Object));
    });

    it("should add imageInput node on Shift+I", () => {
      render(
        <TestWrapper>
          <WorkflowCanvas />
        </TestWrapper>
      );

      fireEvent.keyDown(window, { key: "i", shiftKey: true });

      expect(mockAddNode).toHaveBeenCalledWith("imageInput", expect.any(Object));
    });

    it("should add nanoBanana node on Shift+G", () => {
      render(
        <TestWrapper>
          <WorkflowCanvas />
        </TestWrapper>
      );

      fireEvent.keyDown(window, { key: "g", shiftKey: true });

      expect(mockAddNode).toHaveBeenCalledWith("nanoBanana", expect.any(Object));
    });

    it("should add llmGenerate node on Shift+L", () => {
      render(
        <TestWrapper>
          <WorkflowCanvas />
        </TestWrapper>
      );

      fireEvent.keyDown(window, { key: "l", shiftKey: true });

      expect(mockAddNode).toHaveBeenCalledWith("llmGenerate", expect.any(Object));
    });

    it("should add annotation node on Shift+A", () => {
      render(
        <TestWrapper>
          <WorkflowCanvas />
        </TestWrapper>
      );

      fireEvent.keyDown(window, { key: "a", shiftKey: true });

      expect(mockAddNode).toHaveBeenCalledWith("annotation", expect.any(Object));
    });
  });

  describe("Connection Validation", () => {
    it("should render canvas with isValidConnection callback configured", () => {
      // The isValidConnection function is passed to ReactFlow to validate connections
      // This test confirms the canvas is properly configured
      render(
        <TestWrapper>
          <WorkflowCanvas />
        </TestWrapper>
      );

      // Canvas should have ReactFlow with validation configured
      expect(document.querySelector(".react-flow")).toBeInTheDocument();
    });
  });

  describe("Group Handling", () => {
    it("should render canvas with group support enabled", () => {
      mockUseWorkflowStore.mockImplementation((selector) => {
        return selector(createDefaultState({
          groups: {
            "group-1": {
              id: "group-1",
              position: { x: 100, y: 100 },
              size: { width: 400, height: 400 },
              locked: false,
            },
          },
        }));
      });

      render(
        <TestWrapper>
          <WorkflowCanvas />
        </TestWrapper>
      );

      // The canvas should render with groups functionality enabled
      expect(document.querySelector(".react-flow")).toBeInTheDocument();
    });
  });

  describe("Canvas Configuration", () => {
    it("should configure delete key codes for node deletion", () => {
      render(
        <TestWrapper>
          <WorkflowCanvas />
        </TestWrapper>
      );

      // ReactFlow is configured with deleteKeyCode={["Backspace", "Delete"]}
      expect(document.querySelector(".react-flow")).toBeInTheDocument();
    });

    it("should configure multi-selection with Shift key", () => {
      render(
        <TestWrapper>
          <WorkflowCanvas />
        </TestWrapper>
      );

      // ReactFlow is configured with multiSelectionKeyCode="Shift"
      expect(document.querySelector(".react-flow")).toBeInTheDocument();
    });

    it("should configure zoom constraints", () => {
      render(
        <TestWrapper>
          <WorkflowCanvas />
        </TestWrapper>
      );

      // ReactFlow is configured with minZoom={0.1} maxZoom={4}
      expect(document.querySelector(".react-flow")).toBeInTheDocument();
    });

    it("should use editable as default edge type", () => {
      render(
        <TestWrapper>
          <WorkflowCanvas />
        </TestWrapper>
      );

      // ReactFlow is configured with defaultEdgeOptions={{ type: "editable" }}
      expect(document.querySelector(".react-flow")).toBeInTheDocument();
    });
  });

  describe("Popiai embed viewport", () => {
    it("fits the canvas when the embed bridge requests it", () => {
      vi.useFakeTimers();
      try {
        render(
          <TestWrapper>
            <WorkflowCanvas />
          </TestWrapper>
        );

        window.dispatchEvent(new CustomEvent("popiai:fit-view"));

        expect(mockFitView).not.toHaveBeenCalled();

        vi.advanceTimersByTime(80);

        expect(mockFitView).toHaveBeenCalledWith({
          maxZoom: 1,
          padding: 0.2,
          duration: 500,
        });
      } finally {
        vi.useRealTimers();
      }
    });
  });

  describe("Clipboard Paste", () => {
    // Helper to mock clipboard with image data
    const mockClipboardWithImage = () => {
      const mockBlob = new Blob(["fake-image-data"], { type: "image/png" });
      const mockClipboardItem = {
        types: ["image/png"],
        getType: vi.fn().mockResolvedValue(mockBlob),
      };
      const mockRead = vi.fn().mockResolvedValue([mockClipboardItem]);
      Object.defineProperty(navigator, "clipboard", {
        value: { read: mockRead },
        configurable: true,
        writable: true,
      });
      return { mockRead, mockClipboardItem };
    };

    // Helper to mock FileReader
    const mockFileReader = (dataUrl: string) => {
      class MockFileReader {
        onload: ((event: ProgressEvent<FileReader>) => void) | null = null;
        result: string = dataUrl;
        readAsDataURL() {
          setTimeout(() => {
            this.onload?.({ target: { result: this.result } } as ProgressEvent<FileReader>);
          }, 0);
        }
      }
      global.FileReader = MockFileReader as unknown as typeof FileReader;
    };

    // Helper to mock Image
    const mockImage = (width: number, height: number) => {
      class MockImage {
        onload: (() => void) | null = null;
        width: number = width;
        height: number = height;
        private _src: string = "";
        get src() { return this._src; }
        set src(value: string) {
          this._src = value;
          setTimeout(() => {
            this.onload?.();
          }, 0);
        }
      }
      global.Image = MockImage as unknown as typeof Image;
    };

    it("should update selected imageInput node when pasting image from clipboard", async () => {
      mockClipboardWithImage();
      mockFileReader("data:image/png;base64,test123");
      mockImage(1024, 768);

      const selectedImageNode = createMockNode("image-1", "imageInput", {
        selected: true,
        data: { image: null, filename: null, dimensions: null },
      });

      mockUseWorkflowStore.mockImplementation((selector) => {
        return selector(createDefaultState({
          nodes: [selectedImageNode],
          clipboard: null,
        }));
      });

      render(
        <TestWrapper>
          <WorkflowCanvas />
        </TestWrapper>
      );

      // Trigger Ctrl+V
      fireEvent.keyDown(window, { key: "v", ctrlKey: true });

      await waitFor(() => {
        expect(mockUpdateNodeData).toHaveBeenCalledWith(
          "image-1",
          expect.objectContaining({
            image: "data:image/png;base64,test123",
            filename: expect.stringContaining("pasted-"),
            dimensions: { width: 1024, height: 768 },
          })
        );
      });

      // Should NOT create a new node
      expect(mockAddNode).not.toHaveBeenCalled();
    });

    it("should create new imageInput node when pasting image with no selection", async () => {
      mockClipboardWithImage();
      mockFileReader("data:image/png;base64,newimage");
      mockImage(800, 600);

      // No nodes selected
      mockUseWorkflowStore.mockImplementation((selector) => {
        return selector(createDefaultState({
          nodes: [],
          clipboard: null,
        }));
      });

      render(
        <TestWrapper>
          <WorkflowCanvas />
        </TestWrapper>
      );

      // Trigger Ctrl+V
      fireEvent.keyDown(window, { key: "v", ctrlKey: true });

      await waitFor(() => {
        expect(mockAddNode).toHaveBeenCalledWith("imageInput", expect.any(Object));
      });

      await waitFor(() => {
        expect(mockUpdateNodeData).toHaveBeenCalledWith(
          "new-node-id",
          expect.objectContaining({
            image: "data:image/png;base64,newimage",
            dimensions: { width: 800, height: 600 },
          })
        );
      });
    });

    it("should create new imageInput node when pasting image with non-imageInput node selected", async () => {
      mockClipboardWithImage();
      mockFileReader("data:image/png;base64,anotherimage");
      mockImage(640, 480);

      // Prompt node selected (not imageInput)
      const selectedPromptNode = createMockNode("prompt-1", "prompt", {
        selected: true,
        data: { prompt: "test" },
      });

      mockUseWorkflowStore.mockImplementation((selector) => {
        return selector(createDefaultState({
          nodes: [selectedPromptNode],
          clipboard: null,
        }));
      });

      render(
        <TestWrapper>
          <WorkflowCanvas />
        </TestWrapper>
      );

      // Trigger Ctrl+V
      fireEvent.keyDown(window, { key: "v", ctrlKey: true });

      await waitFor(() => {
        expect(mockAddNode).toHaveBeenCalledWith("imageInput", expect.any(Object));
      });

      await waitFor(() => {
        expect(mockUpdateNodeData).toHaveBeenCalledWith(
          "new-node-id",
          expect.objectContaining({
            image: "data:image/png;base64,anotherimage",
            dimensions: { width: 640, height: 480 },
          })
        );
      });
    });

    it("should prioritize internal clipboard over system clipboard when nodes are copied", async () => {
      mockClipboardWithImage();
      mockFileReader("data:image/png;base64,ignored");
      mockImage(100, 100);

      // Internal clipboard has nodes
      mockUseWorkflowStore.mockImplementation((selector) => {
        return selector(createDefaultState({
          nodes: [],
          clipboard: {
            nodes: [createMockNode("copied-1", "prompt")],
            edges: [],
          },
        }));
      });

      render(
        <TestWrapper>
          <WorkflowCanvas />
        </TestWrapper>
      );

      // Trigger Ctrl+V
      fireEvent.keyDown(window, { key: "v", ctrlKey: true });

      // Should paste internal clipboard nodes, not system clipboard image
      expect(mockPasteNodes).toHaveBeenCalled();
      expect(mockClearClipboard).toHaveBeenCalled();

      // Should NOT update or add nodes from system clipboard
      expect(mockUpdateNodeData).not.toHaveBeenCalled();
      expect(mockAddNode).not.toHaveBeenCalled();
    });
  });
});
