/**
 * useCanvasActions Hook Tests
 *
 * Tests the combined canvas actions hook including:
 * - Adding placeholder nodes (trigger/action)
 * - Multi-action capability checks
 * - Delete node flow (request, confirm, cancel)
 * - Edge management on node operations
 * - App selection for triggers and actions
 * - Closing app selector with placeholder cleanup
 * - Integration name/icon resolution
 */
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { renderHook, act } from '@testing-library/react';
import { Position } from '@xyflow/react';
import { useCanvasActions } from './useCanvasActions';
import { WorkflowReactFlowNode } from '../types/reactflow';

// Mock the UI store selectors
const mockOpenAppSelector = vi.fn();
const mockCloseAppSelector = vi.fn();
const mockRequestDeleteNode = vi.fn();
const mockCancelDeleteNode = vi.fn();
const mockClearNodeToDelete = vi.fn();

// Store the mutable state for tests
let mockNodeToDelete: { id: string; name: string } | null = null;
let mockSelectedPlaceholderId: string | null = null;

vi.mock('../stores', () => ({
  useAppSelector: () => ({
    selectedPlaceholderId: mockSelectedPlaceholderId,
    openAppSelector: mockOpenAppSelector,
    closeAppSelector: mockCloseAppSelector,
  }),
  useDialogState: () => ({
    get nodeToDelete() {
      return mockNodeToDelete;
    },
    requestDeleteNode: mockRequestDeleteNode,
    cancelDeleteNode: mockCancelDeleteNode,
    clearNodeToDelete: mockClearNodeToDelete,
  }),
}));

// Mock the display utilities
vi.mock('../utils/display', () => ({
  getAppNameFromIntegration: vi.fn((integration: string) => {
    const map: Record<string, string> = {
      mailchimp: 'Mailchimp',
      airtable: 'Airtable',
      wordpress: 'WordPress',
    };
    return map[integration] || integration;
  }),
  getAppServiceInfo: vi.fn((_type: string) => ({
    appName: 'WordPress',
    appService: 'wordpress',
  })),
  getActionDisplayName: vi.fn((type: string) => {
    const map: Record<string, string> = {
      'wordpress/send_email': 'Send Email',
      'wordpress/create_post': 'Create Post',
    };
    return map[type] || type;
  }),
  getActionIconName: vi.fn(() => 'Zap'),
  getTriggerDisplayName: vi.fn((type: string) => {
    const map: Record<string, string> = {
      'wordpress/user_registration': 'User Registration',
      'wordpress/post_published': 'Post Published',
    };
    return map[type] || type;
  }),
  getTriggerIconName: vi.fn(() => 'Zap'),
}));

// =============================================================================
// Shared Helpers
// =============================================================================

const createTriggerNode = (overrides = {}): WorkflowReactFlowNode => ({
  id: 'node-trigger',
  type: 'trigger',
  position: { x: 250, y: 80 },
  sourcePosition: Position.Bottom,
  targetPosition: Position.Top,
  data: {
    name: 'User Registration',
    icon: 'Zap',
    status: 'valid',
    config: {},
    actionType: 'wordpress/user_registration',
    ...overrides,
  },
});

const createActionNode = (id: string, overrides = {}): WorkflowReactFlowNode => ({
  id,
  type: 'action',
  position: { x: 250, y: 240 },
  sourcePosition: Position.Bottom,
  targetPosition: Position.Top,
  data: {
    name: 'Send Email',
    icon: 'Zap',
    status: 'valid',
    config: {},
    actionType: 'wordpress/send_email',
    ...overrides,
  },
});

const createPlaceholderNode = (
  idOrType: string,
  type?: 'trigger' | 'action',
): WorkflowReactFlowNode => {
  // Support both (type) and (id, type) signatures
  const nodeType = type || (idOrType as 'trigger' | 'action');
  const nodeId = type ? idOrType : nodeType === 'trigger' ? 'node-trigger' : 'node-placeholder';

  return {
    id: nodeId,
    type: nodeType,
    position: { x: 250, y: 80 },
    sourcePosition: Position.Bottom,
    targetPosition: Position.Top,
    data: {
      name: nodeType === 'trigger' ? 'Choose a trigger' : 'Choose an action',
      icon: 'Zap',
      status: 'placeholder',
      placeholderType: nodeType,
      config: {},
    },
  };
};

const createConfiguredNode = (id: string, type: 'trigger' | 'action'): WorkflowReactFlowNode => ({
  id,
  type,
  position: { x: 250, y: 80 },
  sourcePosition: Position.Bottom,
  targetPosition: Position.Top,
  data: {
    name: 'User Registration',
    icon: 'Zap',
    status: 'valid',
    config: {},
    actionType: 'wordpress/user_registration',
  },
});

// =============================================================================
// Tests
// =============================================================================

describe('useCanvasActions', () => {
  const mockSetNodes = vi.fn();
  const mockSetEdges = vi.fn();
  const mockTakeSnapshot = vi.fn();
  const mockSetSelectedNodeId = vi.fn();

  const defaultOptions = {
    nodes: [] as WorkflowReactFlowNode[],
    edges: [],
    setNodes: mockSetNodes,
    setEdges: mockSetEdges,
    takeSnapshot: mockTakeSnapshot,
    setSelectedNodeId: mockSetSelectedNodeId,
  };

  beforeEach(() => {
    vi.clearAllMocks();
    mockNodeToDelete = null;
    mockSelectedPlaceholderId = null;
    // Reset window.sequensyAPI
    (window as any).sequensyAPI = undefined;
  });

  // ==========================================================================
  // handleAddPlaceholder - Trigger Tests
  // ==========================================================================

  describe('handleAddPlaceholder - trigger', () => {
    it('creates a trigger placeholder node', () => {
      const { result } = renderHook(() => useCanvasActions(defaultOptions));

      act(() => {
        result.current.handleAddPlaceholder('trigger');
      });

      expect(mockTakeSnapshot).toHaveBeenCalledWith([], []);
      expect(mockSetNodes).toHaveBeenCalled();
      expect(mockOpenAppSelector).toHaveBeenCalledWith('trigger', 'node-trigger');
    });

    it('uses node-trigger as ID for trigger placeholders', () => {
      const { result } = renderHook(() => useCanvasActions(defaultOptions));

      act(() => {
        result.current.handleAddPlaceholder('trigger');
      });

      expect(mockOpenAppSelector).toHaveBeenCalledWith('trigger', 'node-trigger');
    });

    it('positions trigger based on existing nodes count', () => {
      const nodes = [createTriggerNode()];
      const { result } = renderHook(() => useCanvasActions({ ...defaultOptions, nodes }));

      act(() => {
        result.current.handleAddPlaceholder('trigger');
      });

      // Verify setNodes was called with a function that adds the node
      const setNodesFn = mockSetNodes.mock.calls[0][0];
      expect(typeof setNodesFn).toBe('function');
    });

    it('opens app selector after creating trigger placeholder', () => {
      const { result } = renderHook(() => useCanvasActions(defaultOptions));

      act(() => {
        result.current.handleAddPlaceholder('trigger');
      });

      expect(mockOpenAppSelector).toHaveBeenCalledWith('trigger', 'node-trigger');
    });
  });

  // ==========================================================================
  // handleAddPlaceholder - Action Tests
  // ==========================================================================

  describe('handleAddPlaceholder - action', () => {
    it('creates an action placeholder node when no action exists', () => {
      const nodes = [createTriggerNode()];
      const { result } = renderHook(() => useCanvasActions({ ...defaultOptions, nodes }));

      act(() => {
        result.current.handleAddPlaceholder('action');
      });

      expect(mockTakeSnapshot).toHaveBeenCalled();
      expect(mockSetNodes).toHaveBeenCalled();
      expect(mockOpenAppSelector).toHaveBeenCalled();
    });

    it('uses timestamp-based ID for action placeholders', () => {
      vi.spyOn(Date, 'now').mockReturnValue(1234567890);

      const nodes = [createTriggerNode()];
      const { result } = renderHook(() => useCanvasActions({ ...defaultOptions, nodes }));

      act(() => {
        result.current.handleAddPlaceholder('action');
      });

      expect(mockOpenAppSelector).toHaveBeenCalledWith('action', 'node-1234567890');

      vi.restoreAllMocks();
    });

    it('creates edge from last node when adding action', () => {
      const triggerNode = createTriggerNode();
      const nodes = [triggerNode];
      const { result } = renderHook(() => useCanvasActions({ ...defaultOptions, nodes }));

      act(() => {
        result.current.handleAddPlaceholder('action');
      });

      expect(mockSetEdges).toHaveBeenCalled();
    });

    it('does not create edge when adding to empty workflow', () => {
      const { result } = renderHook(() => useCanvasActions(defaultOptions));

      act(() => {
        result.current.handleAddPlaceholder('action');
      });

      expect(mockSetEdges).not.toHaveBeenCalled();
    });

    it('applies auto-layout to new nodes', () => {
      const nodes = [createTriggerNode()];
      const { result } = renderHook(() => useCanvasActions({ ...defaultOptions, nodes }));

      act(() => {
        result.current.handleAddPlaceholder('action');
      });

      // setNodes is called with a function that applies autoLayoutNodes
      const setNodesFn = mockSetNodes.mock.calls[0][0];
      expect(typeof setNodesFn).toBe('function');
    });
  });

  // ==========================================================================
  // Multi-action Capability Tests
  // ==========================================================================

  describe('multi-action capability', () => {
    it('allows multiple actions in free plugin (now enabled natively)', () => {
      const nodes = [createTriggerNode(), createActionNode('action-1', { status: 'valid' })];
      const { result } = renderHook(() => useCanvasActions({ ...defaultOptions, nodes }));

      act(() => {
        result.current.handleAddPlaceholder('action');
      });

      // With native multi-action support, adding a second action should work
      expect(mockSetNodes).toHaveBeenCalled();
    });

    it('allows second action when multi_actions capability is true', () => {
      (window as any).sequensyAPI = { capabilities: { multi_actions: true } };

      const nodes = [createTriggerNode(), createActionNode('action-1', { status: 'valid' })];
      const { result } = renderHook(() => useCanvasActions({ ...defaultOptions, nodes }));

      act(() => {
        result.current.handleAddPlaceholder('action');
      });

      expect(mockTakeSnapshot).toHaveBeenCalled();
      expect(mockSetNodes).toHaveBeenCalled();
    });

    it('ignores placeholder actions when checking for existing actions', () => {
      const nodes = [createTriggerNode(), createPlaceholderNode('action')];
      const { result } = renderHook(() => useCanvasActions({ ...defaultOptions, nodes }));

      act(() => {
        result.current.handleAddPlaceholder('action');
      });

      // Should allow adding because existing action is just a placeholder
      expect(mockSetNodes).toHaveBeenCalled();
    });
  });

  // ==========================================================================
  // handleRequestDeleteNode Tests
  // ==========================================================================

  describe('handleRequestDeleteNode', () => {
    it('calls requestDeleteNode with node id and name', () => {
      const nodes = [createTriggerNode()];
      const { result } = renderHook(() => useCanvasActions({ ...defaultOptions, nodes }));

      act(() => {
        result.current.handleRequestDeleteNode('node-trigger');
      });

      expect(mockRequestDeleteNode).toHaveBeenCalledWith('node-trigger', 'User Registration');
    });

    it('does nothing if node is not found', () => {
      const nodes = [createTriggerNode()];
      const { result } = renderHook(() => useCanvasActions({ ...defaultOptions, nodes }));

      act(() => {
        result.current.handleRequestDeleteNode('non-existent');
      });

      expect(mockRequestDeleteNode).not.toHaveBeenCalled();
    });

    it('finds correct node in multiple nodes', () => {
      const nodes = [createTriggerNode(), createActionNode('action-1', { name: 'Send Email' })];
      const { result } = renderHook(() => useCanvasActions({ ...defaultOptions, nodes }));

      act(() => {
        result.current.handleRequestDeleteNode('action-1');
      });

      expect(mockRequestDeleteNode).toHaveBeenCalledWith('action-1', 'Send Email');
    });
  });

  // ==========================================================================
  // handleConfirmDeleteNode Tests
  // ==========================================================================

  describe('handleConfirmDeleteNode', () => {
    it('does nothing if nodeToDelete is null', () => {
      mockNodeToDelete = null;
      const { result } = renderHook(() => useCanvasActions(defaultOptions));

      act(() => {
        result.current.handleConfirmDeleteNode();
      });

      expect(mockTakeSnapshot).not.toHaveBeenCalled();
      expect(mockSetNodes).not.toHaveBeenCalled();
    });

    it('takes snapshot before deleting', () => {
      mockNodeToDelete = { id: 'node-trigger', name: 'User Registration' };
      const nodes = [createTriggerNode()];
      const edges = [{ id: 'e1', source: 'node-trigger', target: 'action-1' }];

      const { result } = renderHook(() => useCanvasActions({ ...defaultOptions, nodes, edges }));

      act(() => {
        result.current.handleConfirmDeleteNode();
      });

      expect(mockTakeSnapshot).toHaveBeenCalledWith(nodes, edges);
    });

    it('clears nodeToDelete state after deletion', () => {
      mockNodeToDelete = { id: 'node-trigger', name: 'User Registration' };
      const nodes = [createTriggerNode()];

      const { result } = renderHook(() => useCanvasActions({ ...defaultOptions, nodes }));

      act(() => {
        result.current.handleConfirmDeleteNode();
      });

      expect(mockClearNodeToDelete).toHaveBeenCalled();
    });

    it('removes deleted node from nodes list', () => {
      mockNodeToDelete = { id: 'node-trigger', name: 'User Registration' };
      const nodes = [createTriggerNode()];

      const { result } = renderHook(() => useCanvasActions({ ...defaultOptions, nodes }));

      act(() => {
        result.current.handleConfirmDeleteNode();
      });

      expect(mockSetNodes).toHaveBeenCalled();
      // Verify the filter function works correctly
      const filterFn = mockSetNodes.mock.calls[0][0];
      const filteredNodes = filterFn(nodes);
      expect(filteredNodes).toHaveLength(0);
    });
  });

  // ==========================================================================
  // handleCancelDeleteNode Tests
  // ==========================================================================

  describe('handleCancelDeleteNode', () => {
    it('calls cancelDeleteNode', () => {
      const { result } = renderHook(() => useCanvasActions(defaultOptions));

      act(() => {
        result.current.handleCancelDeleteNode();
      });

      expect(mockCancelDeleteNode).toHaveBeenCalled();
    });

    it('does not modify nodes or edges', () => {
      const nodes = [createTriggerNode()];
      const edges = [{ id: 'e1', source: 'node-trigger', target: 'action-1' }];

      const { result } = renderHook(() => useCanvasActions({ ...defaultOptions, nodes, edges }));

      act(() => {
        result.current.handleCancelDeleteNode();
      });

      expect(mockSetNodes).not.toHaveBeenCalled();
      expect(mockSetEdges).not.toHaveBeenCalled();
    });
  });

  // ==========================================================================
  // Edge Management Tests
  // ==========================================================================

  describe('edge management', () => {
    it('removes edges connected to deleted node', () => {
      mockNodeToDelete = { id: 'node-trigger', name: 'User Registration' };
      const nodes = [createTriggerNode(), createActionNode('action-1')];
      const edges = [
        { id: 'e1', source: 'node-trigger', target: 'action-1' },
        { id: 'e2', source: 'action-1', target: 'action-2' },
      ];

      const { result } = renderHook(() => useCanvasActions({ ...defaultOptions, nodes, edges }));

      act(() => {
        result.current.handleConfirmDeleteNode();
      });

      expect(mockSetEdges).toHaveBeenCalled();
      // setEdges is called with the reconnected edges array directly
      const filteredEdges = mockSetEdges.mock.calls[0][0];
      expect(filteredEdges).toHaveLength(1);
      expect(filteredEdges[0].id).toBe('e2');
    });
  });

  // ==========================================================================
  // Edge Reconnection Tests
  // ==========================================================================

  describe('edge reconnection on deletion', () => {
    it('reconnects edges when deleting middle node with 1 in/1 out', () => {
      mockNodeToDelete = { id: 'action-2', name: 'Update User' };
      const nodes = [
        createTriggerNode(),
        createActionNode('action-1'),
        createActionNode('action-2'),
        createActionNode('action-3'),
      ];
      const edges = [
        { id: 'e1', source: 'node-trigger', target: 'action-1', type: 'smoothstep' },
        { id: 'e2', source: 'action-1', target: 'action-2', type: 'smoothstep' },
        { id: 'e3', source: 'action-2', target: 'action-3', type: 'smoothstep' },
      ];

      const { result } = renderHook(() => useCanvasActions({ ...defaultOptions, nodes, edges }));

      act(() => {
        result.current.handleConfirmDeleteNode();
      });

      expect(mockSetEdges).toHaveBeenCalled();
      // setEdges is called with the reconnected edges array directly
      const updatedEdges = mockSetEdges.mock.calls[0][0];

      // Should have 2 edges: e1 and reconnection edge (action-1 -> action-3)
      expect(updatedEdges).toHaveLength(2);
      expect(updatedEdges).toContainEqual({
        id: 'e1',
        source: 'node-trigger',
        target: 'action-1',
        type: 'smoothstep',
      });
      expect(updatedEdges).toContainEqual({
        id: 'action-1-action-3',
        source: 'action-1',
        target: 'action-3',
        type: 'smoothstep',
      });
    });

    it('does not reconnect when deleting node with multiple outgoing edges', () => {
      mockNodeToDelete = { id: 'action-1', name: 'Branching Action' };
      const nodes = [
        createTriggerNode(),
        createActionNode('action-1'),
        createActionNode('action-2'),
        createActionNode('action-3'),
      ];
      const edges = [
        { id: 'e1', source: 'node-trigger', target: 'action-1', type: 'smoothstep' },
        { id: 'e2', source: 'action-1', target: 'action-2', type: 'smoothstep' },
        { id: 'e3', source: 'action-1', target: 'action-3', type: 'smoothstep' },
      ];

      const { result } = renderHook(() => useCanvasActions({ ...defaultOptions, nodes, edges }));

      act(() => {
        result.current.handleConfirmDeleteNode();
      });

      expect(mockSetEdges).toHaveBeenCalled();
      // setEdges is called with the reconnected edges array directly
      const updatedEdges = mockSetEdges.mock.calls[0][0];

      // All edges connected to action-1 should be removed (no reconnection because multiple outgoing edges)
      expect(updatedEdges).toHaveLength(0);
    });

    it('reconnects trigger to next action when deleting first action', () => {
      mockNodeToDelete = { id: 'action-1', name: 'First Action' };
      const nodes = [
        createTriggerNode(),
        createActionNode('action-1'),
        createActionNode('action-2'),
      ];
      const edges = [
        { id: 'e1', source: 'node-trigger', target: 'action-1', type: 'smoothstep' },
        { id: 'e2', source: 'action-1', target: 'action-2', type: 'smoothstep' },
      ];

      const { result } = renderHook(() => useCanvasActions({ ...defaultOptions, nodes, edges }));

      act(() => {
        result.current.handleConfirmDeleteNode();
      });

      expect(mockSetEdges).toHaveBeenCalled();
      // setEdges is called with the reconnected edges array directly
      const updatedEdges = mockSetEdges.mock.calls[0][0];

      // Should have reconnection edge from trigger to action-2
      expect(updatedEdges).toHaveLength(1);
      expect(updatedEdges[0]).toEqual({
        id: 'node-trigger-action-2',
        source: 'node-trigger',
        target: 'action-2',
        type: 'smoothstep',
      });
    });

    it('uses smoothstep type for reconnected edges', () => {
      mockNodeToDelete = { id: 'action-1', name: 'Middle Action' };
      const nodes = [
        createTriggerNode(),
        createActionNode('action-1'),
        createActionNode('action-2'),
      ];
      const edges = [
        { id: 'e1', source: 'node-trigger', target: 'action-1', type: 'smoothstep' },
        { id: 'e2', source: 'action-1', target: 'action-2', type: 'smoothstep' },
      ];

      const { result } = renderHook(() => useCanvasActions({ ...defaultOptions, nodes, edges }));

      act(() => {
        result.current.handleConfirmDeleteNode();
      });

      expect(mockSetEdges).toHaveBeenCalled();
      // setEdges is called with the reconnected edges array directly
      const updatedEdges = mockSetEdges.mock.calls[0][0];

      // Verify reconnected edge has smoothstep type
      const reconnectedEdge = updatedEdges.find(
        (e: { id: string }) => e.id === 'node-trigger-action-2',
      );
      expect(reconnectedEdge).toBeDefined();
      expect(reconnectedEdge?.type).toBe('smoothstep');
    });

    it('triggers auto-layout after deleting a node to close gaps', () => {
      mockNodeToDelete = { id: 'action-2', name: 'Middle Action' };
      const nodes = [
        { ...createTriggerNode(), position: { x: 250, y: 80 } },
        { ...createActionNode('action-1'), position: { x: 250, y: 300 } },
        { ...createActionNode('action-2'), position: { x: 250, y: 520 } },
        { ...createActionNode('action-3'), position: { x: 250, y: 740 } },
      ];

      const { result } = renderHook(() => useCanvasActions({ ...defaultOptions, nodes }));

      // Mock requestAnimationFrame to execute callbacks immediately
      const rafCallbacks: FrameRequestCallback[] = [];
      const rafSpy = vi.spyOn(window, 'requestAnimationFrame').mockImplementation((cb) => {
        rafCallbacks.push(cb);
        return 0;
      });

      act(() => {
        result.current.handleConfirmDeleteNode();
      });

      // Execute the RAF callback to apply layout
      act(() => {
        rafCallbacks.forEach((cb) => cb(0));
      });

      // Second setNodes call has the layout changes
      expect(mockSetNodes).toHaveBeenCalledTimes(2);
      const layoutUpdateFn = mockSetNodes.mock.calls[1][0];
      const filteredNodes = nodes.filter((n) => n.id !== 'action-2');
      const updatedNodes = layoutUpdateFn(filteredNodes);

      // Should have 3 nodes after deletion
      expect(updatedNodes).toHaveLength(3);

      // Verify positions are recalculated with uniform spacing
      // START_Y=80, VERTICAL_SPACING=220
      expect(updatedNodes[0].position.y).toBe(80); // Trigger at index 0
      expect(updatedNodes[1].position.y).toBe(300); // Action-1 at index 1
      expect(updatedNodes[2].position.y).toBe(520); // Action-3 at index 2 (gap closed from y=740)

      rafSpy.mockRestore();
    });

    it('maintains uniform spacing after deletion with auto-layout', () => {
      mockNodeToDelete = { id: 'action-1', name: 'First Action' };
      const nodes = [
        createTriggerNode(),
        createActionNode('action-1'),
        createActionNode('action-2'),
        createActionNode('action-3'),
        createActionNode('action-4'),
      ];

      const { result } = renderHook(() => useCanvasActions({ ...defaultOptions, nodes }));

      // Mock requestAnimationFrame to execute callbacks immediately
      const rafCallbacks: FrameRequestCallback[] = [];
      const rafSpy = vi.spyOn(window, 'requestAnimationFrame').mockImplementation((cb) => {
        rafCallbacks.push(cb);
        return 0;
      });

      act(() => {
        result.current.handleConfirmDeleteNode();
      });

      // Execute the RAF callback to apply layout
      act(() => {
        rafCallbacks.forEach((cb) => cb(0));
      });

      const layoutUpdateFn = mockSetNodes.mock.calls[1][0];
      const filteredNodes = nodes.filter((n) => n.id !== 'action-1');
      const updatedNodes = layoutUpdateFn(filteredNodes);

      // Verify all nodes have uniform 220px vertical spacing
      expect(updatedNodes).toHaveLength(4);
      for (let i = 1; i < updatedNodes.length; i++) {
        const spacing = updatedNodes[i].position.y - updatedNodes[i - 1].position.y;
        expect(spacing).toBe(220);
      }

      rafSpy.mockRestore();
    });

    it('combines edge reconnection with auto-layout correctly', () => {
      mockNodeToDelete = { id: 'action-2', name: 'Middle Action' };
      const nodes = [
        { ...createTriggerNode(), position: { x: 250, y: 80 } },
        { ...createActionNode('action-1'), position: { x: 250, y: 300 } },
        { ...createActionNode('action-2'), position: { x: 250, y: 520 } },
        { ...createActionNode('action-3'), position: { x: 250, y: 740 } },
      ];
      const edges = [
        { id: 'e1', source: 'node-trigger', target: 'action-1', type: 'smoothstep' },
        { id: 'e2', source: 'action-1', target: 'action-2', type: 'smoothstep' },
        { id: 'e3', source: 'action-2', target: 'action-3', type: 'smoothstep' },
      ];

      const { result } = renderHook(() => useCanvasActions({ ...defaultOptions, nodes, edges }));

      // Mock requestAnimationFrame to execute callbacks immediately
      const rafCallbacks: FrameRequestCallback[] = [];
      const rafSpy = vi.spyOn(window, 'requestAnimationFrame').mockImplementation((cb) => {
        rafCallbacks.push(cb);
        return 0;
      });

      act(() => {
        result.current.handleConfirmDeleteNode();
      });

      // Execute the RAF callback to apply layout
      act(() => {
        rafCallbacks.forEach((cb) => cb(0));
      });

      // Verify edges are reconnected (setEdges called with array directly)
      const updatedEdges = mockSetEdges.mock.calls[0][0];
      expect(updatedEdges).toHaveLength(2);
      expect(
        updatedEdges.find((e: any) => e.source === 'action-1' && e.target === 'action-3'),
      ).toBeDefined();

      // Verify nodes are auto-laid out (second setNodes call has layout)
      const layoutUpdateFn = mockSetNodes.mock.calls[1][0];
      const filteredNodes = nodes.filter((n) => n.id !== 'action-2');
      const updatedNodes = layoutUpdateFn(filteredNodes);
      expect(updatedNodes).toHaveLength(3);
      expect(updatedNodes[2].position.y).toBe(520); // Action-3 moved from y=740 to y=520

      rafSpy.mockRestore();
    });
  });

  describe('smooth animation effects', () => {
    it('marks nodes as animating before repositioning', () => {
      mockNodeToDelete = { id: 'action-2', name: 'Middle Action' };
      const nodes = [
        createTriggerNode(),
        createActionNode('action-1'),
        createActionNode('action-2'),
        createActionNode('action-3'),
      ];

      const { result } = renderHook(() => useCanvasActions({ ...defaultOptions, nodes }));

      act(() => {
        result.current.handleConfirmDeleteNode();
      });

      // First setNodes call should mark nodes as animating
      expect(mockSetNodes).toHaveBeenCalled();
      const firstUpdate = mockSetNodes.mock.calls[0][0];
      const updatedNodes = firstUpdate(nodes);

      // All remaining nodes should have isAnimating: true
      expect(updatedNodes).toHaveLength(3);
      updatedNodes.forEach((node: any) => {
        expect(node.data.isAnimating).toBe(true);
      });
    });

    it('uses requestAnimationFrame for smooth position updates', () => {
      const rafSpy = vi.spyOn(window, 'requestAnimationFrame');
      mockNodeToDelete = { id: 'action-1', name: 'Action to delete' };
      const nodes = [createTriggerNode(), createActionNode('action-1')];

      const { result } = renderHook(() => useCanvasActions({ ...defaultOptions, nodes }));

      act(() => {
        result.current.handleConfirmDeleteNode();
      });

      expect(rafSpy).toHaveBeenCalled();
      rafSpy.mockRestore();
    });

    it('removes animation flag after applying new positions', () => {
      mockNodeToDelete = { id: 'action-2', name: 'Middle Action' };
      const nodes = [
        createTriggerNode(),
        createActionNode('action-1'),
        createActionNode('action-2'),
      ];

      const { result } = renderHook(() => useCanvasActions({ ...defaultOptions, nodes }));

      // Mock requestAnimationFrame to execute immediately
      const rafCallback: FrameRequestCallback[] = [];
      const rafSpy = vi.spyOn(window, 'requestAnimationFrame').mockImplementation((cb) => {
        rafCallback.push(cb);
        return 0;
      });

      act(() => {
        result.current.handleConfirmDeleteNode();
      });

      // Execute the RAF callback
      act(() => {
        rafCallback.forEach((cb) => cb(0));
      });

      // Second setNodes call should clear animation flag
      expect(mockSetNodes).toHaveBeenCalledTimes(2);
      const secondUpdate = mockSetNodes.mock.calls[1][0];
      const finalNodes = secondUpdate(nodes.filter((n) => n.id !== 'action-2'));

      finalNodes.forEach((node: any) => {
        expect(node.data.isAnimating).toBe(false);
      });

      rafSpy.mockRestore();
    });

    it('does not break edge reconnection with animation', () => {
      mockNodeToDelete = { id: 'action-2', name: 'Middle Action' };
      const nodes = [
        createTriggerNode(),
        createActionNode('action-1'),
        createActionNode('action-2'),
        createActionNode('action-3'),
      ];
      const edges = [
        { id: 'e1', source: 'trigger-1', target: 'action-1', type: 'smoothstep' },
        { id: 'e2', source: 'action-1', target: 'action-2', type: 'smoothstep' },
        { id: 'e3', source: 'action-2', target: 'action-3', type: 'smoothstep' },
      ];

      const { result } = renderHook(() => useCanvasActions({ ...defaultOptions, nodes, edges }));

      act(() => {
        result.current.handleConfirmDeleteNode();
      });

      // Verify edges are still reconnected correctly (setEdges called with array directly)
      const updatedEdges = mockSetEdges.mock.calls[0][0];
      expect(updatedEdges).toHaveLength(2);
      expect(
        updatedEdges.find((e: any) => e.source === 'action-1' && e.target === 'action-3'),
      ).toBeDefined();

      // Verify animation flag was set in first update
      const firstUpdate = mockSetNodes.mock.calls[0][0];
      const animatedNodes = firstUpdate(nodes);
      expect(animatedNodes.every((n: any) => n.data.isAnimating === true)).toBe(true);
    });
  });

  // ==========================================================================
  // handleAppSelected - Trigger Tests
  // ==========================================================================

  describe('handleAppSelected - trigger', () => {
    it('converts placeholder to configured trigger node', () => {
      mockSelectedPlaceholderId = 'node-trigger';
      const nodes = [createPlaceholderNode('node-trigger', 'trigger')];

      const { result } = renderHook(() => useCanvasActions({ ...defaultOptions, nodes }));

      act(() => {
        result.current.handleAppSelected({
          id: 'wordpress/user_registration',
          name: 'User Registration',
          icon: 'UserPlus',
        });
      });

      expect(mockSetNodes).toHaveBeenCalled();
      const updateFn = mockSetNodes.mock.calls[0][0];
      const updatedNodes = updateFn(nodes);

      expect(updatedNodes[0].data.status).toBe('unconfigured');
      expect(updatedNodes[0].data.actionType).toBe('wordpress/user_registration');
      expect(updatedNodes[0].data.placeholderType).toBeUndefined();
    });

    it('sets the node as selected after selection', () => {
      mockSelectedPlaceholderId = 'node-trigger';
      const nodes = [createPlaceholderNode('node-trigger', 'trigger')];

      const { result } = renderHook(() => useCanvasActions({ ...defaultOptions, nodes }));

      act(() => {
        result.current.handleAppSelected({
          id: 'wordpress/user_registration',
          name: 'User Registration',
          icon: 'UserPlus',
        });
      });

      expect(mockSetSelectedNodeId).toHaveBeenCalledWith('node-trigger');
    });

    it('closes app selector after selection', () => {
      mockSelectedPlaceholderId = 'node-trigger';
      const nodes = [createPlaceholderNode('node-trigger', 'trigger')];

      const { result } = renderHook(() => useCanvasActions({ ...defaultOptions, nodes }));

      act(() => {
        result.current.handleAppSelected({
          id: 'wordpress/user_registration',
          name: 'User Registration',
          icon: 'UserPlus',
        });
      });

      expect(mockCloseAppSelector).toHaveBeenCalled();
    });

    it('uses item name when provided', () => {
      mockSelectedPlaceholderId = 'node-trigger';
      const nodes = [createPlaceholderNode('node-trigger', 'trigger')];

      const { result } = renderHook(() => useCanvasActions({ ...defaultOptions, nodes }));

      act(() => {
        result.current.handleAppSelected({
          id: 'wordpress/user_registration',
          name: 'Custom Name',
          icon: 'UserPlus',
        });
      });

      const updateFn = mockSetNodes.mock.calls[0][0];
      const updatedNodes = updateFn(nodes);
      expect(updatedNodes[0].data.name).toBe('Custom Name');
    });
  });

  // ==========================================================================
  // handleAppSelected - Action Tests
  // ==========================================================================

  describe('handleAppSelected - action', () => {
    it('converts placeholder to configured action node', () => {
      mockSelectedPlaceholderId = 'node-action';
      const nodes = [
        createConfiguredNode('node-trigger', 'trigger'),
        createPlaceholderNode('node-action', 'action'),
      ];

      const { result } = renderHook(() => useCanvasActions({ ...defaultOptions, nodes }));

      act(() => {
        result.current.handleAppSelected({
          id: 'wordpress/send_email',
          name: 'Send Email',
          icon: 'Mail',
        });
      });

      expect(mockSetNodes).toHaveBeenCalled();
      const updateFn = mockSetNodes.mock.calls[0][0];
      const updatedNodes = updateFn(nodes);

      const actionNode = updatedNodes.find((n: WorkflowReactFlowNode) => n.id === 'node-action');
      expect(actionNode.data.status).toBe('unconfigured');
      expect(actionNode.data.actionType).toBe('wordpress/send_email');
    });

    it('uses integration for app info when provided', () => {
      mockSelectedPlaceholderId = 'node-action';
      const nodes = [createPlaceholderNode('node-action', 'action')];

      const { result } = renderHook(() => useCanvasActions({ ...defaultOptions, nodes }));

      act(() => {
        result.current.handleAppSelected({
          id: 'mailchimp/add_subscriber',
          name: 'Add Subscriber',
          icon: 'Mail',
          integration: 'mailchimp',
        });
      });

      const updateFn = mockSetNodes.mock.calls[0][0];
      const updatedNodes = updateFn(nodes);

      expect(updatedNodes[0].data.appName).toBe('Mailchimp');
      expect(updatedNodes[0].data.appService).toBe('mailchimp');
    });

    it('falls back to getAppServiceInfo when no integration', () => {
      mockSelectedPlaceholderId = 'node-action';
      const nodes = [createPlaceholderNode('node-action', 'action')];

      const { result } = renderHook(() => useCanvasActions({ ...defaultOptions, nodes }));

      act(() => {
        result.current.handleAppSelected({
          id: 'wordpress/send_email',
          name: 'Send Email',
          icon: 'Mail',
        });
      });

      const updateFn = mockSetNodes.mock.calls[0][0];
      const updatedNodes = updateFn(nodes);

      expect(updatedNodes[0].data.appName).toBe('WordPress');
      expect(updatedNodes[0].data.appService).toBe('wordpress');
    });

    it('does nothing if no placeholder is selected', () => {
      mockSelectedPlaceholderId = null;
      const nodes = [createPlaceholderNode('node-action', 'action')];

      const { result } = renderHook(() => useCanvasActions({ ...defaultOptions, nodes }));

      act(() => {
        result.current.handleAppSelected({
          id: 'wordpress/send_email',
          name: 'Send Email',
          icon: 'Mail',
        });
      });

      expect(mockSetNodes).not.toHaveBeenCalled();
      expect(mockSetSelectedNodeId).not.toHaveBeenCalled();
    });
  });

  // ==========================================================================
  // handleCloseAppSelector - With Placeholder Tests
  // ==========================================================================

  describe('handleCloseAppSelector - with placeholder', () => {
    it('removes placeholder node when closing', () => {
      mockSelectedPlaceholderId = 'node-action';
      const nodes = [
        createConfiguredNode('node-trigger', 'trigger'),
        createPlaceholderNode('node-action', 'action'),
      ];

      const { result } = renderHook(() => useCanvasActions({ ...defaultOptions, nodes }));

      act(() => {
        result.current.handleCloseAppSelector();
      });

      expect(mockSetNodes).toHaveBeenCalled();
      const filterFn = mockSetNodes.mock.calls[0][0];
      const filteredNodes = filterFn(nodes);

      expect(filteredNodes).toHaveLength(1);
      expect(filteredNodes[0].id).toBe('node-trigger');
    });

    it('removes edges connected to placeholder node', () => {
      mockSelectedPlaceholderId = 'node-action';
      const nodes = [
        createConfiguredNode('node-trigger', 'trigger'),
        createPlaceholderNode('node-action', 'action'),
      ];

      const { result } = renderHook(() => useCanvasActions({ ...defaultOptions, nodes }));

      act(() => {
        result.current.handleCloseAppSelector();
      });

      expect(mockSetEdges).toHaveBeenCalled();
      const filterFn = mockSetEdges.mock.calls[0][0];
      const filteredEdges = filterFn([{ id: 'e1', source: 'node-trigger', target: 'node-action' }]);

      expect(filteredEdges).toHaveLength(0);
    });

    it('calls closeAppSelector', () => {
      mockSelectedPlaceholderId = 'node-action';
      const nodes = [createPlaceholderNode('node-action', 'action')];

      const { result } = renderHook(() => useCanvasActions({ ...defaultOptions, nodes }));

      act(() => {
        result.current.handleCloseAppSelector();
      });

      expect(mockCloseAppSelector).toHaveBeenCalled();
    });

    it('does not remove configured nodes', () => {
      mockSelectedPlaceholderId = 'node-trigger';
      const nodes = [createConfiguredNode('node-trigger', 'trigger')];

      const { result } = renderHook(() => useCanvasActions({ ...defaultOptions, nodes }));

      act(() => {
        result.current.handleCloseAppSelector();
      });

      // Should not call setNodes since node is configured
      expect(mockSetNodes).not.toHaveBeenCalled();
    });
  });

  // ==========================================================================
  // handleCloseAppSelector - Without Placeholder Tests
  // ==========================================================================

  describe('handleCloseAppSelector - without placeholder', () => {
    it('just closes app selector when no placeholder selected', () => {
      mockSelectedPlaceholderId = null;
      const nodes = [createConfiguredNode('node-trigger', 'trigger')];

      const { result } = renderHook(() => useCanvasActions({ ...defaultOptions, nodes }));

      act(() => {
        result.current.handleCloseAppSelector();
      });

      expect(mockCloseAppSelector).toHaveBeenCalled();
      expect(mockSetNodes).not.toHaveBeenCalled();
      expect(mockSetEdges).not.toHaveBeenCalled();
    });

    it('does not remove nodes when placeholder not found', () => {
      mockSelectedPlaceholderId = 'non-existent';
      const nodes = [createConfiguredNode('node-trigger', 'trigger')];

      const { result } = renderHook(() => useCanvasActions({ ...defaultOptions, nodes }));

      act(() => {
        result.current.handleCloseAppSelector();
      });

      expect(mockSetNodes).not.toHaveBeenCalled();
    });

    it('handles empty nodes array', () => {
      mockSelectedPlaceholderId = null;

      const { result } = renderHook(() => useCanvasActions(defaultOptions));

      act(() => {
        result.current.handleCloseAppSelector();
      });

      expect(mockCloseAppSelector).toHaveBeenCalled();
    });
  });
});
