import { setActivePinia } from 'pinia';
import type {
	IConnection,
	IConnections,
	INodeTypeDescription,
	IPinData,
	IWebhookDescription,
	INodeConnections,
	WorkflowExecuteMode,
	Workflow,
	IWorkflowGroup,
} from 'n8n-workflow';
import { NodeConnectionTypes, NodeHelpers, UserError, TelemetryHelpers } from 'n8n-workflow';
import type { CanvasConnection, CanvasNode } from '@/features/workflows/canvas/canvas.types';
import { CanvasConnectionMode } from '@/features/workflows/canvas/canvas.types';
import type { AddedNode, INodeUi, IWorkflowDb, WorkflowDataWithTemplateId } from '@/Interface';
import type { IExecutionResponse } from '@/features/execution/executions/executions.types';
import type { ICredentialsResponse } from '@/features/credentials/credentials.types';
import type { IWorkflowTemplate, IWorkflowTemplateNode } from '@n8n/rest-api-client/api/templates';
import {
	AddConnectionCommand,
	AddNodeGroupCommand,
	RemoveNodeCommand,
	RemoveNodeGroupCommand,
	ReplaceNodeParametersCommand,
	UpdateNodeGroupCommand,
} from '@/app/models/history';
import { useWorkflowsStore } from '@/app/stores/workflows.store';
import { useWorkflowExecutionStateStore } from '@/app/stores/workflowExecutionState.store';
import { useUIStore } from '@/app/stores/ui.store';
import { useHistoryStore } from '@/app/stores/history.store';
import { getNDVStoreId, useNDVStore } from '@/features/ndv/shared/ndv.store';
import {
	createTestNode,
	createTestNodeProperties,
	createTestWorkflow,
	createTestWorkflowObject,
	mockNode,
	mockNodeTypeDescription,
} from '@/__tests__/mocks';
import { mock } from 'vitest-mock-extended';
import { useNodeTypesStore } from '@/app/stores/nodeTypes.store';
import { useCredentialsStore } from '@/features/credentials/credentials.store';
import { useExecutionsStore } from '@/features/execution/executions/executions.store';
import { useNodeCreatorStore } from '@/features/shared/nodeCreator/nodeCreator.store';
import { useProjectsStore } from '@/features/collaboration/projects/projects.store';
import { usePostHog } from '@/app/stores/posthog.store';
import { waitFor } from '@testing-library/vue';
import { createTestingPinia } from '@pinia/testing';
import { mockedStore } from '@/__tests__/utils';
import {
	AGENT_NODE_TYPE,
	EXECUTE_WORKFLOW_TRIGGER_NODE_TYPE,
	FORM_TRIGGER_NODE_TYPE,
	MCP_TRIGGER_NODE_TYPE,
	OPEN_AI_CHAT_MODEL_NODE_TYPE,
	SET_NODE_TYPE,
	STICKY_NODE_TYPE,
	UPDATE_WEBHOOK_ID_NODE_TYPES,
	VIEWS,
	WEBHOOK_NODE_TYPE,
} from '@/app/constants';
import { STORES } from '@n8n/stores';
import type { Connection } from '@vue-flow/core';
import { useClipboard } from '@vueuse/core';
import { createCanvasConnectionHandleString } from '@/features/workflows/canvas/canvas.utils';
import { isVNode, nextTick, reactive, ref } from 'vue';
import type { CanvasLayoutEvent } from '@/features/workflows/canvas/composables/useCanvasLayout';
import { useTelemetry } from './useTelemetry';
import { useToast } from '@/app/composables/useToast';
import * as nodeHelpers from '@/app/composables/useNodeHelpers';
import * as workflowsApi from '@/app/api/workflows';
import { useBuilderStore } from '@/features/ai/assistant/builder.store';
import {
	useWorkflowDocumentStore,
	createWorkflowDocumentId,
} from '@/app/stores/workflowDocument.store';

import { useRouter } from 'vue-router';
import { useTemplatesStore } from '@/features/workflows/templates/templates.store';

const mockRoute = reactive({
	query: {},
	params: {},
});

const mockRouterReplace = vi.fn();

vi.mock('vue-router', async (importOriginal) => ({
	...(await importOriginal<typeof import('vue-router')>()),
	useRoute: () => mockRoute,
	useRouter: () => ({
		replace: mockRouterReplace,
	}),
}));

vi.mock('@/app/api/workflows', async (importOriginal) => {
	const actual = await importOriginal<typeof import('@/app/api/workflows')>();
	return {
		...actual,
		getNewWorkflow: vi.fn().mockResolvedValue({ name: 'New Workflow', settings: {} }),
		getNewWorkflowData: vi.fn().mockResolvedValue({ name: 'New Workflow', settings: {} }),
	};
});

vi.mock('@n8n/rest-api-client/api/workflowHistory', () => ({
	getWorkflowHistory: vi.fn().mockResolvedValue([]),
	getWorkflowVersion: vi.fn().mockResolvedValue({ workflow: { nodes: [], connections: {} } }),
}));

import { useCanvasOperations } from '@/app/composables/useCanvasOperations';
import * as workflowHelpersModule from '@/app/composables/useWorkflowHelpers';
import { GRID_SIZE, PUSH_NODES_OFFSET } from '@/app/utils/nodeViewUtils';

vi.mock('n8n-workflow', async (importOriginal) => {
	const actual = await importOriginal<typeof import('n8n-workflow')>();
	return {
		...actual,
		TelemetryHelpers: {
			...actual.TelemetryHelpers,
			generateNodesGraph: vi.fn().mockReturnValue({
				nodeGraph: {
					nodes: [],
				},
			}),
		},
		isHitlToolType: vi.fn((nodeType: string) => {
			return nodeType.toLowerCase().includes('hitltool');
		}),
	};
});

vi.mock('@vueuse/core', async (importOriginal) => {
	const original = await importOriginal<typeof import('@vueuse/core')>();
	const copySpy = vi.fn();
	return {
		...original,
		useClipboard: vi.fn(() => ({ copy: copySpy })),
	};
});

vi.mock('@/app/composables/useTelemetry', () => {
	const track = vi.fn();
	return {
		useTelemetry: () => ({ track }),
	};
});

vi.mock('@/app/composables/useToast', () => {
	const showMessage = vi.fn();
	const showError = vi.fn();
	const showToast = vi.fn();
	return {
		useToast: () => {
			return {
				showMessage,
				showError,
				showToast,
			};
		},
	};
});

const canPinNodeMock = vi.fn();
const setDataMock = vi.fn();
const unsetDataMock = vi.fn();
const hasDataRef = { value: false };
const getInputDataWithPinnedMock = vi.fn();

vi.mock('@/app/composables/usePinnedData', () => {
	return {
		usePinnedData: vi.fn(() => ({
			canPinNode: canPinNodeMock,
			setData: setDataMock,
			unsetData: unsetDataMock,
			hasData: hasDataRef,
		})),
	};
});

vi.mock('@/app/composables/useDataSchema', () => {
	return {
		useDataSchema: vi.fn(() => ({
			getInputDataWithPinned: getInputDataWithPinnedMock,
		})),
	};
});

describe('useCanvasOperations', () => {
	const workflowId = 'test';
	function createInitialState() {
		return {
			[STORES.NODE_TYPES]: {},
			[getNDVStoreId(createWorkflowDocumentId('default'))]: {},
			[STORES.WORKFLOWS]: {
				workflowId,
				workflow: mock<IWorkflowDb>({
					id: workflowId,
					nodes: [],
					connections: {},
					tags: [],
					usedCredentials: [],
				}),
			},
			[STORES.SETTINGS]: {
				settings: {
					enterprise: {},
				},
			},
		};
	}

	type Writable<T> = { -readonly [K in keyof T]: T[K] };
	type WritableDocumentStore = Writable<ReturnType<typeof useWorkflowDocumentStore>>;

	let workflowDocumentStoreInstance: WritableDocumentStore;

	beforeEach(() => {
		vi.restoreAllMocks();
		vi.clearAllMocks();

		const pinia = createTestingPinia({ initialState: createInitialState() });
		setActivePinia(pinia);

		const workflowsStore = useWorkflowsStore();
		workflowDocumentStoreInstance = useWorkflowDocumentStore(
			createWorkflowDocumentId(workflowsStore.workflowId),
		) as WritableDocumentStore;

		mockedStore(usePostHog).isFeatureEnabled.mockReturnValue(true);

		// These actions are stubbed by createTestingPinia, so provide safe defaults.
		// Tests that need custom behavior can override via vi.spyOn.
		vi.mocked(workflowDocumentStoreInstance.getParentNodesByDepth).mockReturnValue([]);
		vi.mocked(workflowDocumentStoreInstance.getConnectedNodes).mockReturnValue([]);
		vi.mocked(workflowDocumentStoreInstance.getWorkflowObjectAccessorSnapshot).mockReturnValue({
			id: workflowDocumentStoreInstance.workflowId,
			connectionsBySourceNode: workflowDocumentStoreInstance.connectionsBySourceNode,
			pinData: workflowDocumentStoreInstance.pinnedDataByNodeName as IPinData,
			expression: workflowDocumentStoreInstance.getExpressionHandler(),
			getNode: workflowDocumentStoreInstance.getNodeByName,
			getParentNodes: workflowDocumentStoreInstance.getParentNodes,
			getNodeConnectionIndexes: workflowDocumentStoreInstance.getNodeConnectionIndexes,
			getParentMainInputNode: workflowDocumentStoreInstance.getParentMainInputNode,
			getChildNodes: workflowDocumentStoreInstance.getChildNodes,
			getParentNodesByDepth: workflowDocumentStoreInstance.getParentNodesByDepth,
		});
	});

	describe('requireNodeTypeDescription', () => {
		it('should return node type description when type and version match', () => {
			const nodeTypesStore = useNodeTypesStore();
			const type = 'testType';
			const version = 1;
			const expectedDescription = mockNodeTypeDescription({ name: type, version });

			nodeTypesStore.nodeTypes = { [type]: { [version]: expectedDescription } };

			const { requireNodeTypeDescription } = useCanvasOperations();
			const result = requireNodeTypeDescription(type, version);

			expect(result).toBe(expectedDescription);
		});

		it('should return node type description when only type is provided and it exists', () => {
			const nodeTypesStore = useNodeTypesStore();
			const type = 'testTypeWithoutVersion';
			const expectedDescription = mockNodeTypeDescription({ name: type });

			nodeTypesStore.nodeTypes = { [type]: { 2: expectedDescription } };

			const { requireNodeTypeDescription } = useCanvasOperations();
			const result = requireNodeTypeDescription(type);

			expect(result).toBe(expectedDescription);
		});

		it("should return placeholder node type description if node type doesn't exist", () => {
			const type = 'nonexistentType';

			const { requireNodeTypeDescription } = useCanvasOperations();
			const result = requireNodeTypeDescription(type);

			expect(result).toEqual({
				name: type,
				displayName: type,
				description: '',
				defaults: {},
				group: [],
				inputs: [],
				outputs: [],
				properties: [],
				version: 1,
			});
		});
	});

	describe('addNode', () => {
		it('should create node with default version when version is undefined', () => {
			const { addNode } = useCanvasOperations();
			const result = addNode(
				{
					name: 'example',
					type: 'type',
					typeVersion: 1,
				},
				mockNodeTypeDescription({ name: 'type' }),
			);

			expect(result.typeVersion).toBe(1);
		});

		it('should create node with default position when position is not provided', () => {
			const { addNode } = useCanvasOperations();
			const result = addNode(
				{
					type: 'type',
					typeVersion: 1,
				},
				mockNodeTypeDescription({ name: 'type' }),
			);

			expect(result.position).toEqual([0, 0]); // Default last click position
		});

		it('should create node with provided position when position is provided', () => {
			const { addNode } = useCanvasOperations();
			const result = addNode(
				{
					type: 'type',
					typeVersion: 1,
					position: [32, 32],
				},
				mockNodeTypeDescription({ name: 'type' }),
			);

			expect(result.position).toEqual([32, 32]);
		});

		it('should not assign credentials when multiple credentials are available', () => {
			const credentialsStore = useCredentialsStore();
			const credentialA = mock<ICredentialsResponse>({ id: '1', name: 'credA', type: 'cred' });
			const credentialB = mock<ICredentialsResponse>({ id: '1', name: 'credB', type: 'cred' });
			const nodeTypeName = 'type';
			const nodeTypeDescription = mockNodeTypeDescription({
				name: nodeTypeName,
				credentials: [{ name: credentialA.name }, { name: credentialB.name }],
			});

			// @ts-expect-error Known pinia issue when spying on store getters
			vi.spyOn(credentialsStore, 'getUsableCredentialByType', 'get').mockReturnValue(() => [
				credentialA,
				credentialB,
			]);

			const { addNode } = useCanvasOperations();
			const result = addNode(
				{
					type: 'type',
					typeVersion: 1,
				},
				nodeTypeDescription,
			);
			expect(result.credentials).toBeUndefined();
		});

		it('should open NDV when specified', async () => {
			const ndvStore = useNDVStore(createWorkflowDocumentId(workflowId));
			const nodeTypeDescription = mockNodeTypeDescription({ name: 'type' });

			const { addNode } = useCanvasOperations();
			addNode(
				{
					type: 'type',
					typeVersion: 1,
					name: 'Test Name',
				},
				nodeTypeDescription,
				{ openNDV: true },
			);

			await waitFor(() =>
				expect(ndvStore.setActiveNodeName).toHaveBeenCalledWith('Test Name', 'added_new_node'),
			);
		});

		it('should not set sticky node type as active node', async () => {
			const ndvStore = useNDVStore(createWorkflowDocumentId(workflowId));
			const nodeTypeDescription = mockNodeTypeDescription({ name: STICKY_NODE_TYPE });

			const { addNode } = useCanvasOperations();
			addNode(
				{
					type: STICKY_NODE_TYPE,
					typeVersion: 1,
					name: 'Test Name',
				},
				nodeTypeDescription,
				{ openNDV: true },
			);

			await waitFor(() => expect(ndvStore.setActiveNodeName).not.toHaveBeenCalled());
		});

		it('should pass actionName to telemetry when adding node with action', async () => {
			const nodeCreatorStore = mockedStore(useNodeCreatorStore);
			const nodeTypeDescription = mockNodeTypeDescription({ name: 'hubspot' });
			const actionName = 'Create Contact';

			nodeCreatorStore.onNodeAddedToCanvas = vi.fn();

			const { addNode } = useCanvasOperations();
			addNode(
				{
					type: 'hubspot',
					typeVersion: 1,
				},
				nodeTypeDescription,
				{
					telemetry: true,
					actionName,
				},
			);

			await waitFor(() => {
				expect(nodeCreatorStore.onNodeAddedToCanvas).toHaveBeenCalledWith(
					expect.objectContaining({
						action: actionName,
						node_type: 'hubspot',
					}),
				);
			});
		});

		it('re-evaluates all credential issues when the added node is a trigger', async () => {
			const nodeTypesStore = mockedStore(useNodeTypesStore);
			nodeTypesStore.isTriggerNode = vi.fn().mockReturnValue(true);

			const updateNodesCredentialsIssuesSpy = vi.fn();
			const nodeHelpersOriginal = nodeHelpers.useNodeHelpers();
			vi.spyOn(nodeHelpers, 'useNodeHelpers').mockImplementation(() => ({
				...nodeHelpersOriginal,
				updateNodesCredentialsIssues: updateNodesCredentialsIssuesSpy,
			}));

			const { addNode } = useCanvasOperations();
			addNode({ type: 'trigger', typeVersion: 1 }, mockNodeTypeDescription({ name: 'trigger' }));

			await waitFor(() => expect(updateNodesCredentialsIssuesSpy).toHaveBeenCalled());
		});

		it('does not re-evaluate all credential issues when the added node is not a trigger', async () => {
			const nodeTypesStore = mockedStore(useNodeTypesStore);
			nodeTypesStore.isTriggerNode = vi.fn().mockReturnValue(false);

			const updateNodesCredentialsIssuesSpy = vi.fn();
			const nodeHelpersOriginal = nodeHelpers.useNodeHelpers();
			vi.spyOn(nodeHelpers, 'useNodeHelpers').mockImplementation(() => ({
				...nodeHelpersOriginal,
				updateNodesCredentialsIssues: updateNodesCredentialsIssuesSpy,
			}));

			const { addNode } = useCanvasOperations();
			addNode({ type: 'set', typeVersion: 1 }, mockNodeTypeDescription({ name: 'set' }));

			await nextTick();
			expect(updateNodesCredentialsIssuesSpy).not.toHaveBeenCalled();
		});
	});

	describe('resolveNodePosition', () => {
		it('should return the node position if it is already set', () => {
			const node = createTestNode({ position: [112, 112] });
			const nodeTypeDescription = mockNodeTypeDescription();

			const { resolveNodePosition } = useCanvasOperations();
			const position = resolveNodePosition(node, nodeTypeDescription);

			expect(position).toEqual([112, 112]);
		});

		it('should place the node at the last cancelled connection position', () => {
			const uiStore = mockedStore(useUIStore);
			const nodeTypesStore = mockedStore(useNodeTypesStore);
			const node = createTestNode({ id: '0' });
			const nodeTypeDescription = mockNodeTypeDescription();

			uiStore.lastInteractedWithNodeId = node.id;
			vi.spyOn(workflowDocumentStoreInstance, 'getNodeById').mockReturnValue(node as INodeUi);
			nodeTypesStore.getNodeType = vi.fn().mockReturnValue(nodeTypeDescription);

			uiStore.lastInteractedWithNodeHandle = 'inputs/main/0';
			uiStore.lastCancelledConnectionPosition = [200, 200];

			const { resolveNodePosition } = useCanvasOperations();
			const position = resolveNodePosition({ ...node, position: undefined }, nodeTypeDescription);

			expect(position).toEqual([208, 160]);
			expect(uiStore.lastCancelledConnectionPosition).toBeUndefined();
		});

		it('should place the node to the right of the last interacted with node', () => {
			const uiStore = mockedStore(useUIStore);
			const nodeTypesStore = mockedStore(useNodeTypesStore);

			const node = createTestNode({ id: '0' });
			const nodeTypeDescription = mockNodeTypeDescription();

			const lastInteracted = createTestNode({
				position: [112, 112],
				type: 'test',
				typeVersion: 1,
			});
			uiStore.lastInteractedWithNodeId = lastInteracted.id;
			vi.spyOn(workflowDocumentStoreInstance, 'getNodeById').mockReturnValue(
				lastInteracted as INodeUi,
			);
			nodeTypesStore.getNodeType = vi.fn().mockReturnValue(nodeTypeDescription);
			vi.spyOn(workflowDocumentStoreInstance, 'getNodeByName').mockReturnValue(
				lastInteracted as INodeUi,
			);

			const { resolveNodePosition } = useCanvasOperations();
			const position = resolveNodePosition({ ...node, position: undefined }, nodeTypeDescription);

			expect(position).toEqual([320, 112]);
		});

		it('should place the node below the last interacted with node if it has non-main outputs', () => {
			const uiStore = mockedStore(useUIStore);
			const nodeTypesStore = mockedStore(useNodeTypesStore);

			const node = createTestNode({ id: '0' });
			const nodeTypeDescription = mockNodeTypeDescription();

			const lastInteracted = createTestNode({
				position: [96, 96],
				type: 'test',
				typeVersion: 1,
			});
			uiStore.lastInteractedWithNodeId = lastInteracted.id;
			vi.spyOn(workflowDocumentStoreInstance, 'getNodeById').mockReturnValue(
				lastInteracted as INodeUi,
			);
			nodeTypesStore.getNodeType = vi.fn().mockReturnValue(nodeTypeDescription);
			vi.spyOn(workflowDocumentStoreInstance, 'getNodeByName').mockReturnValue(
				lastInteracted as INodeUi,
			);

			vi.spyOn(NodeHelpers, 'getNodeOutputs').mockReturnValueOnce([
				{ type: NodeConnectionTypes.AiTool },
			]);
			vi.spyOn(NodeHelpers, 'getConnectionTypes')
				.mockReturnValueOnce([NodeConnectionTypes.AiTool])
				.mockReturnValueOnce([NodeConnectionTypes.AiTool]);

			const { resolveNodePosition } = useCanvasOperations();
			const position = resolveNodePosition({ ...node, position: undefined }, nodeTypeDescription);

			expect(position).toEqual([448, 96]);
		});

		it('should place the node at the last clicked position if no other position is set', () => {
			const node = createTestNode({ id: '0' });
			const nodeTypeDescription = mockNodeTypeDescription();

			workflowDocumentStoreInstance.workflowTriggerNodes = [
				createTestNode({ id: 'trigger', position: [96, 96] }),
			];

			const { resolveNodePosition, lastClickPosition } = useCanvasOperations();
			lastClickPosition.value = [300, 300];

			const position = resolveNodePosition({ ...node, position: undefined }, nodeTypeDescription);

			expect(position).toEqual([304, 304]); // Snapped to grid
		});

		it('should place the trigger node at the root if it is the first trigger node', () => {
			const node = createTestNode({ id: '0' });
			const nodeTypeDescription = mockNodeTypeDescription();

			const { resolveNodePosition } = useCanvasOperations();
			const position = resolveNodePosition({ ...node, position: undefined }, nodeTypeDescription);

			expect(position).toEqual([0, 0]);
		});

		it('should apply custom Y offset for AI Language Model connection type', () => {
			const uiStore = mockedStore(useUIStore);
			const nodeTypesStore = mockedStore(useNodeTypesStore);

			const node = createTestNode({ id: '0' });
			const nodeTypeDescription = mockNodeTypeDescription();

			const lastInteracted = createTestNode({
				position: [100, 100],
				type: 'test',
				typeVersion: 1,
			});
			uiStore.lastInteractedWithNodeId = lastInteracted.id;
			vi.spyOn(workflowDocumentStoreInstance, 'getNodeById').mockReturnValue(
				lastInteracted as INodeUi,
			);
			uiStore.lastInteractedWithNodeHandle = `outputs/${NodeConnectionTypes.AiLanguageModel}/0`;
			nodeTypesStore.getNodeType = vi.fn().mockReturnValue(nodeTypeDescription);
			vi.spyOn(workflowDocumentStoreInstance, 'getNodeByName').mockReturnValue(
				lastInteracted as INodeUi,
			);

			vi.spyOn(NodeHelpers, 'getNodeOutputs').mockReturnValueOnce([
				{ type: NodeConnectionTypes.AiLanguageModel },
			]);
			vi.spyOn(NodeHelpers, 'getConnectionTypes')
				.mockReturnValueOnce([])
				.mockReturnValueOnce([])
				.mockReturnValueOnce([NodeConnectionTypes.AiLanguageModel]);

			const { resolveNodePosition } = useCanvasOperations();
			const position = resolveNodePosition({ ...node, position: undefined }, nodeTypeDescription);

			// Configuration node size is [200, 80], so customOffset = 200 * 2 = 400
			// Expected position: [100 + (200/1) * 1 - 200/2 - 400, 100 + 220] = [-200, 320]
			expect(position[0]).toBeLessThan(100); // Node should be moved left due to custom offset
			expect(position[1]).toEqual(320); // Standard Y position for configuration nodes
		});

		it('should apply custom Y offset for AI Memory connection type', () => {
			const uiStore = mockedStore(useUIStore);
			const nodeTypesStore = mockedStore(useNodeTypesStore);

			const node = createTestNode({ id: '0' });
			const nodeTypeDescription = mockNodeTypeDescription();

			const lastInteracted = createTestNode({
				position: [100, 100],
				type: 'test',
				typeVersion: 1,
			});
			uiStore.lastInteractedWithNodeId = lastInteracted.id;
			vi.spyOn(workflowDocumentStoreInstance, 'getNodeById').mockReturnValue(
				lastInteracted as INodeUi,
			);
			uiStore.lastInteractedWithNodeHandle = `outputs/${NodeConnectionTypes.AiMemory}/0`;
			nodeTypesStore.getNodeType = vi.fn().mockReturnValue(nodeTypeDescription);
			vi.spyOn(workflowDocumentStoreInstance, 'getNodeByName').mockReturnValue(
				lastInteracted as INodeUi,
			);

			vi.spyOn(NodeHelpers, 'getNodeOutputs').mockReturnValueOnce([
				{ type: NodeConnectionTypes.AiMemory },
			]);
			vi.spyOn(NodeHelpers, 'getConnectionTypes')
				.mockReturnValueOnce([])
				.mockReturnValueOnce([])
				.mockReturnValueOnce([NodeConnectionTypes.AiMemory]);

			const { resolveNodePosition } = useCanvasOperations();
			const position = resolveNodePosition({ ...node, position: undefined }, nodeTypeDescription);

			expect(position[0]).toBeLessThan(100); // Node should be moved left due to custom offset
			expect(position[1]).toEqual(320); // Standard Y position for configuration nodes
		});

		it('should not apply custom offset for regular connection types', () => {
			const uiStore = mockedStore(useUIStore);
			const nodeTypesStore = mockedStore(useNodeTypesStore);

			const node = createTestNode({ id: '0' });
			const nodeTypeDescription = mockNodeTypeDescription();

			const lastInteracted = createTestNode({
				position: [100, 100],
				type: 'test',
				typeVersion: 1,
			});
			uiStore.lastInteractedWithNodeId = lastInteracted.id;
			vi.spyOn(workflowDocumentStoreInstance, 'getNodeById').mockReturnValue(
				lastInteracted as INodeUi,
			);
			uiStore.lastInteractedWithNodeHandle = `outputs/${NodeConnectionTypes.AiTool}/0`;
			nodeTypesStore.getNodeType = vi.fn().mockReturnValue(nodeTypeDescription);
			vi.spyOn(workflowDocumentStoreInstance, 'getNodeByName').mockReturnValue(
				lastInteracted as INodeUi,
			);

			vi.spyOn(NodeHelpers, 'getNodeOutputs').mockReturnValueOnce([
				{ type: NodeConnectionTypes.AiTool },
			]);
			vi.spyOn(NodeHelpers, 'getConnectionTypes')
				.mockReturnValueOnce([])
				.mockReturnValueOnce([])
				.mockReturnValueOnce([NodeConnectionTypes.AiTool]);

			const { resolveNodePosition } = useCanvasOperations();
			const position = resolveNodePosition({ ...node, position: undefined }, nodeTypeDescription);

			// No custom offset applied, allowing for some wiggle room when tests are run on different environments
			expect(position[0]).toBeGreaterThanOrEqual(40);
			expect(position[0]).toBeLessThanOrEqual(80);
			expect(position[1]).toBe(320);
		});

		it('should handle missing connection type gracefully', () => {
			const uiStore = mockedStore(useUIStore);
			const nodeTypesStore = mockedStore(useNodeTypesStore);

			const node = createTestNode({ id: '0' });
			const nodeTypeDescription = mockNodeTypeDescription();

			const lastInteracted = createTestNode({
				position: [100, 100],
				type: 'test',
				typeVersion: 1,
			});
			uiStore.lastInteractedWithNodeId = lastInteracted.id;
			vi.spyOn(workflowDocumentStoreInstance, 'getNodeById').mockReturnValue(
				lastInteracted as INodeUi,
			);
			// No lastInteractedWithNodeHandle set
			nodeTypesStore.getNodeType = vi.fn().mockReturnValue(nodeTypeDescription);
			vi.spyOn(workflowDocumentStoreInstance, 'getNodeByName').mockReturnValue(
				lastInteracted as INodeUi,
			);

			vi.spyOn(NodeHelpers, 'getNodeOutputs').mockReturnValueOnce([
				{ type: NodeConnectionTypes.AiTool },
			]);
			vi.spyOn(NodeHelpers, 'getConnectionTypes')
				.mockReturnValueOnce([])
				.mockReturnValueOnce([])
				.mockReturnValueOnce([NodeConnectionTypes.AiTool]);

			const { resolveNodePosition } = useCanvasOperations();
			const position = resolveNodePosition({ ...node, position: undefined }, nodeTypeDescription);

			// No custom offset applied, allowing for some wiggle room when tests are run on different environments
			expect(position[0]).toBeGreaterThanOrEqual(40);
			expect(position[0]).toBeLessThanOrEqual(80);
			expect(position[1]).toBe(320);
		});

		it('should position HITL node vertically between main node and tool node when inserted via connection', () => {
			const uiStore = mockedStore(useUIStore);
			const nodeTypesStore = mockedStore(useNodeTypesStore);

			const mainNode = createTestNode({
				id: 'main',
				name: 'Main Node',
				position: [100, 100],
				type: 'n8n-nodes-base.agent',
			});

			const toolNode = createTestNode({
				id: 'tool',
				name: 'Tool Node',
				position: [100, 400],
				type: 'n8n-nodes-base.searchTool',
			});

			const hitlNode = createTestNode({
				id: 'hitl',
				type: 'n8n-nodes-base.manualChatTriggerHitlTool',
			});

			const nodeTypeDescription = mockNodeTypeDescription();

			// Mock the tool node as lastInteractedWithNode
			uiStore.lastInteractedWithNodeId = toolNode.id;
			uiStore.lastInteractedWithNodeConnection = {
				source: mainNode.id,
				target: mainNode.id,
				sourceHandle: `outputs/${NodeConnectionTypes.AiTool}/0`,
				targetHandle: `inputs/${NodeConnectionTypes.AiTool}/0`,
			};
			uiStore.lastInteractedWithNodeHandle = `outputs/${NodeConnectionTypes.AiTool}/0`;

			nodeTypesStore.getNodeType = vi.fn().mockReturnValue(nodeTypeDescription);
			vi.spyOn(workflowDocumentStoreInstance, 'getNodeByName').mockReturnValue(toolNode as INodeUi);
			vi.spyOn(workflowDocumentStoreInstance, 'getNodeById').mockImplementation((id: string) => {
				if (id === toolNode.id) return toolNode as INodeUi;
				if (id === mainNode.id) return mainNode as INodeUi;
				return undefined;
			});

			const { resolveNodePosition } = useCanvasOperations();
			const position = resolveNodePosition(
				{ ...hitlNode, position: undefined },
				nodeTypeDescription,
			);

			// HITL node should be positioned vertically between main and tool nodes
			// Y position should be halfway between 100 and 400 = 250 + offsets
			expect(position[1]).toBe(256);
			// X position should be tool node X minus half of CONFIGURABLE_NODE_SIZE width
			// CONFIGURABLE_NODE_SIZE = [256, 96], so 100 - 128 = -28 + offsets
			expect(position[0]).toBeLessThan(100);
		});

		it('should move tool node vertically if HITL node would be too close', () => {
			const uiStore = mockedStore(useUIStore);
			const nodeTypesStore = mockedStore(useNodeTypesStore);

			const mainNode = createTestNode({
				id: 'main',
				name: 'Main Node',
				position: [100, 100],
				type: 'n8n-nodes-base.agent',
			});

			// Tool node is too close to main node (less than PUSH_NODES_OFFSET)
			const toolNode = createTestNode({
				id: 'tool',
				name: 'Tool Node',
				position: [100, 150],
				type: 'n8n-nodes-base.searchTool',
			});

			const hitlNode = createTestNode({
				id: 'hitl',
				type: 'n8n-nodes-base.manualChatTriggerHitlTool',
			});

			const nodeTypeDescription = mockNodeTypeDescription();

			uiStore.lastInteractedWithNodeId = toolNode.id;
			uiStore.lastInteractedWithNodeConnection = {
				source: mainNode.id,
				target: mainNode.id,
				sourceHandle: `outputs/${NodeConnectionTypes.AiTool}/0`,
				targetHandle: `inputs/${NodeConnectionTypes.AiTool}/0`,
			};
			uiStore.lastInteractedWithNodeHandle = `outputs/${NodeConnectionTypes.AiTool}/0`;

			nodeTypesStore.getNodeType = vi.fn().mockReturnValue(nodeTypeDescription);
			vi.spyOn(workflowDocumentStoreInstance, 'getNodeByName').mockReturnValue(toolNode as INodeUi);
			vi.spyOn(workflowDocumentStoreInstance, 'getNodeById').mockImplementation((id: string) => {
				if (id === toolNode.id) return toolNode as INodeUi;
				if (id === mainNode.id) return mainNode as INodeUi;
				return undefined;
			});

			const setNodePositionByIdSpy = vi.spyOn(workflowDocumentStoreInstance, 'setNodePositionById');

			const { resolveNodePosition } = useCanvasOperations();
			resolveNodePosition({ ...hitlNode, position: undefined }, nodeTypeDescription);

			// Tool node should be moved vertically because it's too close
			expect(setNodePositionByIdSpy).toHaveBeenCalledWith(
				toolNode.id,
				expect.arrayContaining([100, expect.any(Number)]),
			);

			// The Y position should be different from the original
			const callArgs = setNodePositionByIdSpy.mock.calls[0][1] as [number, number];
			expect(callArgs[1]).not.toBe(150);
		});

		it('should not apply HITL positioning if node is not a HITL tool type', () => {
			const uiStore = mockedStore(useUIStore);
			const nodeTypesStore = mockedStore(useNodeTypesStore);

			const mainNode = createTestNode({
				id: 'main',
				name: 'Main Node',
				position: [100, 100],
				type: 'n8n-nodes-base.agent',
			});

			const toolNode = createTestNode({
				id: 'tool',
				name: 'Tool Node',
				position: [100, 300],
				type: 'n8n-nodes-base.searchTool',
			});

			// Regular node, not a HITL tool
			const regularNode = createTestNode({
				id: 'regular',
				type: 'n8n-nodes-base.set',
			});

			const nodeTypeDescription = mockNodeTypeDescription();

			uiStore.lastInteractedWithNodeId = toolNode.id;
			vi.spyOn(workflowDocumentStoreInstance, 'getNodeById').mockImplementation((id: string) => {
				if (id === toolNode.id) return toolNode as INodeUi;
				if (id === mainNode.id) return mainNode as INodeUi;
				return undefined;
			});
			uiStore.lastInteractedWithNodeConnection = {
				source: mainNode.id,
				target: mainNode.id,
				sourceHandle: `outputs/${NodeConnectionTypes.AiTool}/0`,
				targetHandle: `inputs/${NodeConnectionTypes.AiTool}/0`,
			};
			uiStore.lastInteractedWithNodeHandle = `outputs/${NodeConnectionTypes.AiTool}/0`;

			nodeTypesStore.getNodeType = vi.fn().mockReturnValue(nodeTypeDescription);

			const { resolveNodePosition } = useCanvasOperations();
			const position = resolveNodePosition(
				{ ...regularNode, position: undefined },
				nodeTypeDescription,
			);

			// Regular node should not get HITL positioning
			// It should follow normal positioning rules (to the right of last interacted node)
			expect(position[1]).not.toBe(200); // Not halfway between main and tool
		});

		it('should not apply HITL positioning if lastInteractedWithNode is not a tool type', () => {
			const uiStore = mockedStore(useUIStore);
			const nodeTypesStore = mockedStore(useNodeTypesStore);

			const mainNode = createTestNode({
				id: 'main',
				name: 'Main Node',
				position: [100, 100],
				type: 'n8n-nodes-base.agent',
			});

			// Not a tool node
			const regularNode = createTestNode({
				id: 'regular',
				name: 'Regular Node',
				position: [100, 300],
				type: 'n8n-nodes-base.set',
			});

			const hitlNode = createTestNode({
				id: 'hitl',
				type: 'n8n-nodes-base.manualChatTriggerHitlTool',
			});

			const nodeTypeDescription = mockNodeTypeDescription();

			uiStore.lastInteractedWithNodeId = regularNode.id;
			vi.spyOn(workflowDocumentStoreInstance, 'getNodeById').mockImplementation((id: string) => {
				if (id === regularNode.id) return regularNode as INodeUi;
				if (id === mainNode.id) return mainNode as INodeUi;
				return undefined;
			});
			uiStore.lastInteractedWithNodeConnection = {
				source: mainNode.id,
				target: mainNode.id,
				sourceHandle: `outputs/${NodeConnectionTypes.AiTool}/0`,
				targetHandle: `inputs/${NodeConnectionTypes.AiTool}/0`,
			};
			uiStore.lastInteractedWithNodeHandle = `outputs/${NodeConnectionTypes.AiTool}/0`;

			nodeTypesStore.getNodeType = vi.fn().mockReturnValue(nodeTypeDescription);

			const { resolveNodePosition } = useCanvasOperations();
			const position = resolveNodePosition(
				{ ...hitlNode, position: undefined },
				nodeTypeDescription,
			);

			// Should not apply HITL positioning when last interacted node is not a tool
			expect(position[1]).not.toBe(200);
		});
	});

	describe('updateNodesPosition', () => {
		it('records history for multiple node position updates when tracking is enabled', () => {
			const historyStore = useHistoryStore();
			const events = [
				{ id: 'node1', position: { x: 96, y: 96 } },
				{ id: 'node2', position: { x: 208, y: 208 } },
			];
			const startRecordingUndoSpy = vi.spyOn(historyStore, 'startRecordingUndo');
			const stopRecordingUndoSpy = vi.spyOn(historyStore, 'stopRecordingUndo');

			const { updateNodesPosition } = useCanvasOperations();
			updateNodesPosition(events, { trackHistory: true, trackBulk: true });

			expect(startRecordingUndoSpy).toHaveBeenCalled();
			expect(stopRecordingUndoSpy).toHaveBeenCalled();
		});

		it('updates positions for multiple nodes', () => {
			const events = [
				{ id: 'node1', position: { x: 96, y: 96 } },
				{ id: 'node2', position: { x: 208, y: 208 } },
			];
			const setNodePositionByIdSpy = vi.spyOn(workflowDocumentStoreInstance, 'setNodePositionById');
			const node0 = createTestNode({
				id: events[0].id,
				position: [events[0].position.x, events[0].position.y],
			});
			const node1 = createTestNode({
				id: events[1].id,
				position: [events[1].position.x, events[1].position.y],
			});
			vi.spyOn(workflowDocumentStoreInstance, 'getNodeById')
				.mockReturnValueOnce(node0)
				.mockReturnValueOnce(node1);

			const { updateNodesPosition } = useCanvasOperations();
			updateNodesPosition(events);

			expect(setNodePositionByIdSpy).toHaveBeenCalledTimes(2);
			expect(setNodePositionByIdSpy).toHaveBeenCalledWith('node1', [96, 96]);
			expect(setNodePositionByIdSpy).toHaveBeenCalledWith('node2', [208, 208]);
		});

		it('does not record history when trackHistory is false', () => {
			const historyStore = useHistoryStore();
			const events = [{ id: 'node1', position: { x: 96, y: 96 } }];
			const startRecordingUndoSpy = vi.spyOn(historyStore, 'startRecordingUndo');
			const stopRecordingUndoSpy = vi.spyOn(historyStore, 'stopRecordingUndo');

			const { updateNodesPosition } = useCanvasOperations();
			updateNodesPosition(events, { trackHistory: false, trackBulk: false });

			expect(startRecordingUndoSpy).not.toHaveBeenCalled();
			expect(stopRecordingUndoSpy).not.toHaveBeenCalled();
		});
	});

	describe('tidyUp', () => {
		it('records history for multiple node position updates', () => {
			const historyStore = useHistoryStore();
			const event: CanvasLayoutEvent = {
				source: 'canvas-button',
				target: 'all',
				result: {
					nodes: [
						{ id: 'node1', x: 96, y: 96 },
						{ id: 'node2', x: 208, y: 208 },
					],
					boundingBox: { height: 96, width: 96, x: 0, y: 0 },
				},
			};
			const startRecordingUndoSpy = vi.spyOn(historyStore, 'startRecordingUndo');
			const stopRecordingUndoSpy = vi.spyOn(historyStore, 'stopRecordingUndo');

			const { tidyUp } = useCanvasOperations();
			tidyUp(event);

			expect(startRecordingUndoSpy).toHaveBeenCalled();
			expect(stopRecordingUndoSpy).toHaveBeenCalled();
		});

		it('updates positions for multiple nodes', () => {
			const event: CanvasLayoutEvent = {
				source: 'canvas-button',
				target: 'all',
				result: {
					nodes: [
						{ id: 'node1', x: 96, y: 96 },
						{ id: 'node2', x: 208, y: 208 },
					],
					boundingBox: { height: 96, width: 96, x: 0, y: 0 },
				},
			};
			const setNodePositionByIdSpy = vi.spyOn(workflowDocumentStoreInstance, 'setNodePositionById');
			const tidyNode0 = createTestNode({
				id: event.result.nodes[0].id,
				position: [event.result.nodes[0].x, event.result.nodes[0].y],
			});
			const tidyNode1 = createTestNode({
				id: event.result.nodes[1].id,
				position: [event.result.nodes[1].x, event.result.nodes[1].y],
			});
			vi.spyOn(workflowDocumentStoreInstance, 'getNodeById')
				.mockReturnValueOnce(tidyNode0)
				.mockReturnValueOnce(tidyNode1);

			const { tidyUp } = useCanvasOperations();
			tidyUp(event);

			expect(setNodePositionByIdSpy).toHaveBeenCalledTimes(2);
			expect(setNodePositionByIdSpy).toHaveBeenCalledWith('node1', [96, 96]);
			expect(setNodePositionByIdSpy).toHaveBeenCalledWith('node2', [208, 208]);
		});

		it('should send a "User tidied up workflow" telemetry event', () => {
			const event: CanvasLayoutEvent = {
				source: 'canvas-button',
				target: 'all',
				result: {
					nodes: [
						{ id: 'node1', x: 96, y: 96 },
						{ id: 'node2', x: 208, y: 208 },
					],
					boundingBox: { height: 96, width: 96, x: 0, y: 0 },
				},
			};

			const { tidyUp } = useCanvasOperations();
			tidyUp(event);

			expect(useTelemetry().track).toHaveBeenCalledWith('User tidied up canvas', {
				nodes_count: 2,
				source: 'canvas-button',
				target: 'all',
			});
		});

		it('should send telemetry event when trackEvents is true', () => {
			const event: CanvasLayoutEvent = {
				source: 'canvas-button',
				target: 'all',
				result: {
					nodes: [
						{ id: 'node1', x: 96, y: 96 },
						{ id: 'node2', x: 208, y: 208 },
					],
					boundingBox: { height: 96, width: 96, x: 0, y: 0 },
				},
			};

			const { tidyUp } = useCanvasOperations();
			tidyUp(event, { trackEvents: true });

			expect(useTelemetry().track).toHaveBeenCalledWith('User tidied up canvas', {
				nodes_count: 2,
				source: 'canvas-button',
				target: 'all',
			});
		});

		it('should not send telemetry event when trackEvents is false', () => {
			const event: CanvasLayoutEvent = {
				source: 'canvas-button',
				target: 'all',
				result: {
					nodes: [
						{ id: 'node1', x: 96, y: 96 },
						{ id: 'node2', x: 208, y: 208 },
					],
					boundingBox: { height: 96, width: 96, x: 0, y: 0 },
				},
			};

			const { tidyUp } = useCanvasOperations();
			tidyUp(event, { trackEvents: false });

			expect(useTelemetry().track).not.toHaveBeenCalled();
		});

		it('should send telemetry event when trackEvents is undefined (default behavior)', () => {
			const event: CanvasLayoutEvent = {
				source: 'canvas-button',
				target: 'all',
				result: {
					nodes: [
						{ id: 'node1', x: 96, y: 96 },
						{ id: 'node2', x: 208, y: 208 },
					],
					boundingBox: { height: 96, width: 96, x: 0, y: 0 },
				},
			};

			const { tidyUp } = useCanvasOperations();
			tidyUp(event, {}); // No trackEvents specified, should default to true

			expect(useTelemetry().track).toHaveBeenCalledWith('User tidied up canvas', {
				nodes_count: 2,
				source: 'canvas-button',
				target: 'all',
			});
		});
	});

	describe('updateNodePosition', () => {
		it('should update node position', () => {
			const id = 'node1';
			const position: CanvasNode['position'] = { x: 10, y: 20 };
			const node = createTestNode({
				id,
				type: 'node',
				position: [0, 0],
				name: 'Node 1',
			});
			const setNodePositionByIdSpy = vi.spyOn(workflowDocumentStoreInstance, 'setNodePositionById');

			vi.spyOn(workflowDocumentStoreInstance, 'getNodeById').mockReturnValueOnce(node);

			const { updateNodePosition } = useCanvasOperations();
			updateNodePosition(id, position);

			expect(setNodePositionByIdSpy).toHaveBeenCalledWith(id, [position.x, position.y]);
		});
	});

	describe('setNodeSelected', () => {
		it('should set last selected node when node id is provided and node exists', () => {
			const uiStore = useUIStore();
			const nodeId = 'node1';
			const nodeName = 'Node 1';
			vi.spyOn(workflowDocumentStoreInstance, 'getNodeById').mockReturnValue({
				name: nodeName,
			} as INodeUi);
			uiStore.lastSelectedNode = '';

			const { setNodeSelected } = useCanvasOperations();
			setNodeSelected(nodeId);

			expect(uiStore.lastSelectedNode).toBe(nodeName);
		});

		it('should not change last selected node when node id is provided but node does not exist', () => {
			const uiStore = useUIStore();
			const nodeId = 'node1';
			vi.spyOn(workflowDocumentStoreInstance, 'getNodeById').mockReturnValue(undefined);
			uiStore.lastSelectedNode = 'Existing Node';

			const { setNodeSelected } = useCanvasOperations();
			setNodeSelected(nodeId);

			expect(uiStore.lastSelectedNode).toBe('Existing Node');
		});

		it('should clear last selected node when node id is not provided', () => {
			const uiStore = useUIStore();
			uiStore.lastSelectedNode = 'Existing Node';

			const { setNodeSelected } = useCanvasOperations();
			setNodeSelected();

			expect(uiStore.lastSelectedNode).toBe('');
		});
	});

	describe('addNodes', () => {
		it('should add nodes at specified positions', async () => {
			const nodeTypesStore = useNodeTypesStore();
			const nodeTypeName = 'type';
			const nodes = [
				mockNode({ name: 'Node 1', type: nodeTypeName, position: [32, 32] }),
				mockNode({ name: 'Node 2', type: nodeTypeName, position: [96, 256] }),
			];

			nodeTypesStore.nodeTypes = {
				[nodeTypeName]: { 1: mockNodeTypeDescription({ name: nodeTypeName }) },
			};

			const addNodeSpy = vi.spyOn(workflowDocumentStoreInstance, 'addNode');

			const { addNodes } = useCanvasOperations();
			await addNodes(nodes, {});

			expect(addNodeSpy).toHaveBeenCalledTimes(2);
			expect(addNodeSpy.mock.calls[0][0]).toMatchObject({
				name: nodes[0].name,
				type: nodeTypeName,
				typeVersion: 1,
				position: [32, 32],
				parameters: {},
			});
			expect(addNodeSpy.mock.calls[1][0]).toMatchObject({
				name: nodes[1].name,
				type: nodeTypeName,
				typeVersion: 1,
				position: [96, 256],
				parameters: {},
			});
		});

		it('should add nodes at current position when position is not specified', async () => {
			const nodeTypesStore = mockedStore(useNodeTypesStore);
			const nodeTypeName = 'type';
			const nodes = [
				mockNode({ name: 'Node 1', type: nodeTypeName, position: [128, 128] }),
				mockNode({ name: 'Node 2', type: nodeTypeName, position: [192, 320] }),
			];

			nodeTypesStore.nodeTypes = {
				[nodeTypeName]: { 1: mockNodeTypeDescription({ name: nodeTypeName }) },
			};

			const addNodeSpy = vi.spyOn(workflowDocumentStoreInstance, 'addNode');

			const { addNodes } = useCanvasOperations();
			await addNodes(nodes, { position: [50, 60] });

			expect(addNodeSpy).toHaveBeenCalledTimes(2);
			expect(addNodeSpy.mock.calls[0][0].position).toEqual(
				expect.arrayContaining(nodes[0].position),
			);
			expect(addNodeSpy.mock.calls[1][0].position).toEqual(
				expect.arrayContaining(nodes[1].position),
			);
		});

		it('should adjust the position of nodes with multiple inputs', async () => {
			const nodeTypesStore = useNodeTypesStore();
			const nodeTypeName = 'type';
			const nodes = [
				mockNode({ id: 'a', name: 'Node A', type: nodeTypeName, position: [32, 32] }),
				mockNode({ id: 'b', name: 'Node B', type: nodeTypeName, position: [32, 32] }),
				mockNode({ id: 'c', name: 'Node C', type: nodeTypeName, position: [96, 256] }),
			];

			const setNodePositionByIdSpy = vi.spyOn(workflowDocumentStoreInstance, 'setNodePositionById');
			vi.spyOn(workflowDocumentStoreInstance, 'getNodeByName').mockImplementation((name) => {
				if (name === nodes[1].name) return nodes[1] as INodeUi;
				if (name === nodes[2].name) return nodes[2] as INodeUi;
				return null;
			});
			vi.spyOn(workflowDocumentStoreInstance, 'getNodeById').mockImplementation((id) => {
				if (id === nodes[1].id) return nodes[1] as INodeUi;
				if (id === nodes[2].id) return nodes[2] as INodeUi;
				return undefined;
			});

			nodeTypesStore.nodeTypes = {
				[nodeTypeName]: { 1: mockNodeTypeDescription({ name: nodeTypeName }) },
			};

			vi.mocked(workflowDocumentStoreInstance.getParentNodesByDepth).mockReturnValue(
				nodes.map((node) => ({
					name: node.name,
					depth: 0,
					indicies: [],
				})),
			);

			const { addNodes } = useCanvasOperations();
			await addNodes(nodes, {});

			expect(setNodePositionByIdSpy).toHaveBeenCalledTimes(2);
			expect(setNodePositionByIdSpy).toHaveBeenCalledWith(nodes[1].id, expect.any(Object));
			expect(setNodePositionByIdSpy).toHaveBeenCalledWith(nodes[2].id, expect.any(Object));
		});

		it('should return newly added nodes', async () => {
			const nodeTypesStore = useNodeTypesStore();
			const nodeTypeName = 'type';
			const nodes = [
				mockNode({ name: 'Node 1', type: nodeTypeName, position: [30, 40] }),
				mockNode({ name: 'Node 2', type: nodeTypeName, position: [96, 240] }),
			];

			nodeTypesStore.nodeTypes = {
				[nodeTypeName]: { 1: mockNodeTypeDescription({ name: nodeTypeName }) },
			};

			const { addNodes } = useCanvasOperations();
			const added = await addNodes(nodes, {});
			expect(added.length).toBe(2);
		});

		it('should mark UI state as dirty', async () => {
			const uiStore = mockedStore(useUIStore);
			const nodeTypesStore = useNodeTypesStore();
			const nodeTypeName = 'type';
			const nodes = [mockNode({ name: 'Node 1', type: nodeTypeName, position: [30, 40] })];

			nodeTypesStore.nodeTypes = {
				[nodeTypeName]: { 1: mockNodeTypeDescription({ name: nodeTypeName }) },
			};

			const { addNodes } = useCanvasOperations();
			await addNodes(nodes, { keepPristine: false });

			expect(uiStore.markStateDirty).toHaveBeenCalled();
		});

		it('should not mark UI state as dirty if keepPristine is true', async () => {
			const uiStore = mockedStore(useUIStore);
			const nodeTypesStore = useNodeTypesStore();
			const nodeTypeName = 'type';
			const nodes = [mockNode({ name: 'Node 1', type: nodeTypeName, position: [30, 40] })];

			nodeTypesStore.nodeTypes = {
				[nodeTypeName]: { 1: mockNodeTypeDescription({ name: nodeTypeName }) },
			};

			const { addNodes } = useCanvasOperations();
			await addNodes(nodes, { keepPristine: true });

			expect(uiStore.markStateDirty).not.toHaveBeenCalled();
		});

		it('should pass actionName to telemetry when adding nodes with actions', async () => {
			const nodeCreatorStore = mockedStore(useNodeCreatorStore);
			const nodeTypesStore = useNodeTypesStore();
			const nodeTypeName = 'hubspot';
			const actionName = 'Create Contact';
			const nodes = [
				{
					...mockNode({
						name: 'HubSpot Node',
						type: nodeTypeName,
						position: [100, 100],
					}),
					actionName,
				},
			];

			nodeCreatorStore.onNodeAddedToCanvas = vi.fn();

			nodeTypesStore.nodeTypes = {
				[nodeTypeName]: { 1: mockNodeTypeDescription({ name: nodeTypeName }) },
			};

			const { addNodes } = useCanvasOperations();
			await addNodes(nodes, { telemetry: true });

			expect(nodeCreatorStore.onNodeAddedToCanvas).toHaveBeenCalledWith(
				expect.objectContaining({
					action: actionName,
					node_type: nodeTypeName,
				}),
			);
		});

		it('should respect positionOffset', async () => {
			const nodeTypesStore = useNodeTypesStore();
			const nodeTypeName = 'type';
			const nodes: AddedNode[] = [
				{ name: 'Node 1', type: nodeTypeName },
				{ name: 'Node 2', type: nodeTypeName, positionOffset: [2 * GRID_SIZE, GRID_SIZE] },
			];

			nodeTypesStore.nodeTypes = {
				[nodeTypeName]: { 1: mockNodeTypeDescription({ name: nodeTypeName }) },
			};

			const addNodeSpy = vi.spyOn(workflowDocumentStoreInstance, 'addNode');

			const { addNodes } = useCanvasOperations();
			await addNodes(nodes, { position: [32, 32] });

			expect(addNodeSpy).toHaveBeenCalledTimes(2);
			expect(addNodeSpy.mock.calls[0][0]).toMatchObject({
				name: nodes[0].name,
				type: nodeTypeName,
				typeVersion: 1,
				position: [32, 32],
				parameters: {},
			});
			expect(addNodeSpy.mock.calls[1][0]).toMatchObject({
				name: nodes[1].name,
				type: nodeTypeName,
				typeVersion: 1,
				position: [32 + PUSH_NODES_OFFSET + 2 * GRID_SIZE, 32 + GRID_SIZE],
				parameters: {},
			});
		});
	});

	describe('revertAddNode', () => {
		it('deletes node if it exists', async () => {
			const node = createTestNode();
			vi.spyOn(workflowDocumentStoreInstance, 'getNodeByName').mockReturnValueOnce(node);
			vi.spyOn(workflowDocumentStoreInstance, 'getNodeById').mockReturnValueOnce(node);
			const removeNodeByIdSpy = vi.spyOn(workflowDocumentStoreInstance, 'removeNodeById');

			const { revertAddNode } = useCanvasOperations();
			await revertAddNode(node.name);

			expect(removeNodeByIdSpy).toHaveBeenCalledWith(node.id);
		});
	});

	describe('deleteNode', () => {
		it('should delete node and track history', () => {
			const historyStore = mockedStore(useHistoryStore);
			vi.mocked(workflowDocumentStoreInstance.incomingConnectionsByNodeName).mockReturnValue({});

			const id = 'node1';
			const node: INodeUi = createTestNode({
				id,
				type: 'node',
				position: [10, 20],
				name: 'Node 1',
			});

			vi.spyOn(workflowDocumentStoreInstance, 'getNodeById').mockReturnValue(node);

			const { deleteNode } = useCanvasOperations();
			deleteNode(id, { trackHistory: true });

			expect(workflowDocumentStoreInstance.removeNodeById).toHaveBeenCalledWith(id);
			expect(
				useWorkflowExecutionStateStore(workflowDocumentStoreInstance.documentId)
					.clearActiveNodeExecutionData,
			).toHaveBeenCalledWith(node.name);
			expect(historyStore.pushCommandToUndo).toHaveBeenCalledWith(
				new RemoveNodeCommand(node, expect.any(Number)),
			);
		});

		it('records the group membership change when deleting a grouped node', () => {
			const historyStore = mockedStore(useHistoryStore);
			vi.mocked(workflowDocumentStoreInstance.incomingConnectionsByNodeName).mockReturnValue({});

			const node = createTestNode({ id: 'b', name: 'B' });
			vi.spyOn(workflowDocumentStoreInstance, 'getNodeById').mockReturnValue(node);

			const group = { id: 'g1', name: 'Group 1', nodeIds: ['a', 'b', 'c'] };
			vi.spyOn(workflowDocumentStoreInstance, 'getGroupForNode').mockReturnValue(group);
			vi.spyOn(workflowDocumentStoreInstance, 'getGroupById').mockReturnValue({
				...group,
				nodeIds: ['a', 'c'],
			});

			const { deleteNode } = useCanvasOperations();
			deleteNode('b', { trackHistory: true });

			const groupCommand = historyStore.pushCommandToUndo.mock.calls
				.map(([command]) => command)
				.find((command) => command instanceof UpdateNodeGroupCommand) as
				| UpdateNodeGroupCommand
				| undefined;
			expect(groupCommand).toBeInstanceOf(UpdateNodeGroupCommand);
			expect(groupCommand?.before.nodeIds).toEqual(['a', 'b', 'c']);
			expect(groupCommand?.after.nodeIds).toEqual(['a', 'c']);
		});

		it('records a group removal when deleting the last grouped node', () => {
			const historyStore = mockedStore(useHistoryStore);
			vi.mocked(workflowDocumentStoreInstance.incomingConnectionsByNodeName).mockReturnValue({});

			const node = createTestNode({ id: 'b', name: 'B' });
			vi.spyOn(workflowDocumentStoreInstance, 'getNodeById').mockReturnValue(node);

			const group = { id: 'g1', name: 'Group 1', nodeIds: ['b'] };
			vi.spyOn(workflowDocumentStoreInstance, 'getGroupForNode').mockReturnValue(group);
			vi.spyOn(workflowDocumentStoreInstance, 'getGroupById').mockReturnValue(undefined);

			const { deleteNode } = useCanvasOperations();
			deleteNode('b', { trackHistory: true });

			const groupCommand = historyStore.pushCommandToUndo.mock.calls
				.map(([command]) => command)
				.find((command) => command instanceof RemoveNodeGroupCommand) as
				| RemoveNodeGroupCommand
				| undefined;
			expect(groupCommand).toBeInstanceOf(RemoveNodeGroupCommand);
			expect(groupCommand?.group.nodeIds).toEqual(['b']);
		});

		it('does not record any group command when the deleted node is ungrouped', () => {
			const historyStore = mockedStore(useHistoryStore);
			vi.mocked(workflowDocumentStoreInstance.incomingConnectionsByNodeName).mockReturnValue({});

			const node = createTestNode({ id: 'b', name: 'B' });
			vi.spyOn(workflowDocumentStoreInstance, 'getNodeById').mockReturnValue(node);
			vi.spyOn(workflowDocumentStoreInstance, 'getGroupForNode').mockReturnValue(undefined);

			const { deleteNode } = useCanvasOperations();
			deleteNode('b', { trackHistory: true });

			const groupCommand = historyStore.pushCommandToUndo.mock.calls
				.map(([command]) => command)
				.find(
					(command) =>
						command instanceof UpdateNodeGroupCommand || command instanceof RemoveNodeGroupCommand,
				);
			expect(groupCommand).toBeUndefined();
		});

		it('re-evaluates all credential issues when the deleted node is a trigger', () => {
			const nodeTypesStore = mockedStore(useNodeTypesStore);
			nodeTypesStore.isTriggerNode = vi.fn().mockReturnValue(true);
			vi.mocked(workflowDocumentStoreInstance.incomingConnectionsByNodeName).mockReturnValue({});

			const node = createTestNode({ id: 'trigger-1', type: 'trigger', name: 'Trigger' });
			vi.spyOn(workflowDocumentStoreInstance, 'getNodeById').mockReturnValue(node);

			const updateNodesCredentialsIssuesSpy = vi.fn();
			const nodeHelpersOriginal = nodeHelpers.useNodeHelpers();
			vi.spyOn(nodeHelpers, 'useNodeHelpers').mockImplementation(() => ({
				...nodeHelpersOriginal,
				updateNodesCredentialsIssues: updateNodesCredentialsIssuesSpy,
			}));

			const { deleteNode } = useCanvasOperations();
			deleteNode(node.id);

			expect(updateNodesCredentialsIssuesSpy).toHaveBeenCalled();
		});

		it('does not re-evaluate all credential issues when the deleted node is not a trigger', () => {
			const nodeTypesStore = mockedStore(useNodeTypesStore);
			nodeTypesStore.isTriggerNode = vi.fn().mockReturnValue(false);
			vi.mocked(workflowDocumentStoreInstance.incomingConnectionsByNodeName).mockReturnValue({});

			const node = createTestNode({ id: 'set-1', type: 'set', name: 'Set' });
			vi.spyOn(workflowDocumentStoreInstance, 'getNodeById').mockReturnValue(node);

			const updateNodesCredentialsIssuesSpy = vi.fn();
			const nodeHelpersOriginal = nodeHelpers.useNodeHelpers();
			vi.spyOn(nodeHelpers, 'useNodeHelpers').mockImplementation(() => ({
				...nodeHelpersOriginal,
				updateNodesCredentialsIssues: updateNodesCredentialsIssuesSpy,
			}));

			const { deleteNode } = useCanvasOperations();
			deleteNode(node.id);

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

		it('should delete node without tracking history', () => {
			const historyStore = mockedStore(useHistoryStore);
			vi.mocked(workflowDocumentStoreInstance.incomingConnectionsByNodeName).mockReturnValue({});

			const id = 'node1';
			const node = createTestNode({
				id,
				type: 'node',
				position: [10, 20],
				name: 'Node 1',
				parameters: {},
			});

			vi.spyOn(workflowDocumentStoreInstance, 'getNodeById').mockReturnValue(node);

			const { deleteNode } = useCanvasOperations();
			deleteNode(id, { trackHistory: false });

			expect(workflowDocumentStoreInstance.removeNodeById).toHaveBeenCalledWith(id);
			expect(
				useWorkflowExecutionStateStore(workflowDocumentStoreInstance.documentId)
					.clearActiveNodeExecutionData,
			).toHaveBeenCalledWith(node.name);
			expect(historyStore.pushCommandToUndo).not.toHaveBeenCalled();
		});

		it('should connect adjacent nodes when deleting a node surrounded by other nodes', () => {
			const nodeTypesStore = mockedStore(useNodeTypesStore);

			nodeTypesStore.nodeTypes = {
				[SET_NODE_TYPE]: { 1: mockNodeTypeDescription({ name: SET_NODE_TYPE }) },
			};

			const nodes = [
				createTestNode({
					id: 'input',
					type: SET_NODE_TYPE,
					position: [10, 20],
					name: 'Input Node',
				}),
				createTestNode({
					id: 'middle',
					type: SET_NODE_TYPE,
					position: [10, 20],
					name: 'Middle Node',
				}),
				createTestNode({
					id: 'output',
					type: SET_NODE_TYPE,
					position: [10, 20],
					name: 'Output Node',
				}),
			];

			workflowDocumentStoreInstance.allNodes = nodes;
			workflowDocumentStoreInstance.connectionsBySourceNode = {
				[nodes[0].name]: {
					main: [
						[
							{
								node: nodes[1].name,
								type: NodeConnectionTypes.Main,
								index: 0,
							},
						],
					],
				},
				[nodes[1].name]: {
					main: [
						[
							{
								node: nodes[2].name,
								type: NodeConnectionTypes.Main,
								index: 0,
							},
						],
					],
				},
			};

			vi.mocked(workflowDocumentStoreInstance.incomingConnectionsByNodeName).mockReturnValue({});

			vi.spyOn(workflowDocumentStoreInstance, 'getNodeById').mockReturnValue(nodes[1]);

			const { deleteNode } = useCanvasOperations();
			deleteNode(nodes[1].id);

			expect(workflowDocumentStoreInstance.removeNodeById).toHaveBeenCalledWith(nodes[1].id);
			expect(
				useWorkflowExecutionStateStore(workflowDocumentStoreInstance.documentId)
					.clearActiveNodeExecutionData,
			).toHaveBeenCalledWith(nodes[1].name);
			expect(workflowDocumentStoreInstance.removeNodeById).toHaveBeenCalledWith(nodes[1].id);
		});

		it('should handle nodes with null connections for unconnected indexes', () => {
			const nodeTypesStore = mockedStore(useNodeTypesStore);

			nodeTypesStore.nodeTypes = {
				[SET_NODE_TYPE]: { 1: mockNodeTypeDescription({ name: SET_NODE_TYPE }) },
			};

			const nodes = [
				createTestNode({
					id: 'input',
					type: SET_NODE_TYPE,
					position: [10, 20],
					name: 'Input Node',
				}),
				createTestNode({
					id: 'middle',
					type: SET_NODE_TYPE,
					position: [10, 20],
					name: 'Middle Node',
				}),
				createTestNode({
					id: 'output',
					type: SET_NODE_TYPE,
					position: [10, 20],
					name: 'Output Node',
				}),
			];

			vi.spyOn(workflowDocumentStoreInstance, 'getNodeByName').mockImplementation(
				(name: string) => nodes.find((node) => node.name === name) ?? null,
			);

			workflowDocumentStoreInstance.allNodes = nodes;
			workflowDocumentStoreInstance.connectionsBySourceNode = {
				[nodes[0].name]: {
					main: [
						null,
						[
							{
								node: nodes[1].name,
								type: NodeConnectionTypes.Main,
								index: 0,
							},
						],
					],
				},
				[nodes[1].name]: {
					main: [
						// null here to simulate no connection at index
						null,
						[
							{
								node: nodes[2].name,
								type: NodeConnectionTypes.Main,
								index: 0,
							},
						],
					],
				},
			};

			vi.mocked(workflowDocumentStoreInstance.incomingConnectionsByNodeName).mockReturnValue({});

			vi.spyOn(workflowDocumentStoreInstance, 'getNodeById').mockReturnValue(nodes[1]);

			const { deleteNode } = useCanvasOperations();
			deleteNode(nodes[1].id);

			expect(workflowDocumentStoreInstance.removeNodeById).toHaveBeenCalledWith(nodes[1].id);
			expect(
				useWorkflowExecutionStateStore(workflowDocumentStoreInstance.documentId)
					.clearActiveNodeExecutionData,
			).toHaveBeenCalledWith(nodes[1].name);
			expect(workflowDocumentStoreInstance.removeNodeById).toHaveBeenCalledWith(nodes[1].id);
		});
	});

	describe('revertDeleteNode', () => {
		it('should revert delete node', () => {
			const node = createTestNode({
				id: 'node1',
				type: 'node',
				position: [10, 20],
				name: 'Node 1',
				parameters: {},
			});

			const addNodeSpy = vi.spyOn(workflowDocumentStoreInstance, 'addNode');

			const { revertDeleteNode } = useCanvasOperations();
			revertDeleteNode(node);

			expect(addNodeSpy).toHaveBeenCalledWith(node);
		});
	});

	describe('renameNode', () => {
		it('should rename node and return the actual new name on success', async () => {
			const ndvStore = mockedStore(useNDVStore, createWorkflowDocumentId(workflowId));
			const oldName = 'Old Node';
			const newName = 'New Node';

			const workflowObject = createTestWorkflowObject();
			workflowObject.renameNode = vi.fn();
			vi.mocked(workflowDocumentStoreInstance.cloneWorkflowObject).mockReturnValue(workflowObject);
			vi.spyOn(workflowDocumentStoreInstance, 'getNodeByName').mockReturnValue({
				name: oldName,
			} as INodeUi);
			ndvStore.activeNodeName = oldName;

			const { renameNode } = useCanvasOperations();
			const result = await renameNode(oldName, newName);

			expect(result).toBe(newName);
			expect(workflowObject.renameNode).toHaveBeenCalledWith(oldName, newName);
			expect(workflowDocumentStoreInstance.setConnections).toHaveBeenCalledWith(
				workflowObject.connectionsBySourceNode,
			);
			expect(ndvStore.setActiveNodeName).toHaveBeenCalledWith(newName, expect.any(String));
		});

		it('should return false when new name is same as old name', async () => {
			const ndvStore = mockedStore(useNDVStore, createWorkflowDocumentId(workflowId));
			const oldName = 'Old Node';
			vi.spyOn(workflowDocumentStoreInstance, 'getNodeByName').mockReturnValue({
				name: oldName,
			} as INodeUi);
			ndvStore.activeNodeName = oldName;

			const { renameNode } = useCanvasOperations();
			const result = await renameNode(oldName, oldName);

			expect(result).toBe(false);
			expect(ndvStore.activeNodeName).toBe(oldName);
		});

		it('should show error toast and return false when renameNode throws an error', async () => {
			const ndvStore = mockedStore(useNDVStore, createWorkflowDocumentId(workflowId));
			const toast = useToast();
			const oldName = 'Old Node';
			const newName = 'New Node';
			const errorMessage = 'Node name already exists';
			const errorDescription = 'Please choose a different name';

			const workflowObject = createTestWorkflowObject();
			workflowObject.renameNode = vi.fn().mockImplementation(() => {
				const error = new UserError(errorMessage, { description: errorDescription });
				throw error;
			});
			vi.mocked(workflowDocumentStoreInstance.cloneWorkflowObject).mockReturnValue(workflowObject);
			vi.spyOn(workflowDocumentStoreInstance, 'getNodeByName').mockReturnValue({
				name: oldName,
			} as INodeUi);
			ndvStore.activeNodeName = oldName;

			const { renameNode } = useCanvasOperations();
			const result = await renameNode(oldName, newName);

			expect(result).toBe(false);
			expect(workflowObject.renameNode).toHaveBeenCalledWith(oldName, newName);
			expect(toast.showMessage).toHaveBeenCalledWith({
				type: 'error',
				title: errorMessage,
				message: errorDescription,
			});
		});

		it('should not show error toast when showErrorToast is false', async () => {
			const ndvStore = mockedStore(useNDVStore, createWorkflowDocumentId(workflowId));
			const toast = useToast();
			const oldName = 'Old Node';
			const newName = 'New Node';
			const errorMessage = 'Node name already exists';

			const workflowObject = createTestWorkflowObject();
			workflowObject.renameNode = vi.fn().mockImplementation(() => {
				throw new UserError(errorMessage);
			});
			vi.mocked(workflowDocumentStoreInstance.cloneWorkflowObject).mockReturnValue(workflowObject);
			vi.spyOn(workflowDocumentStoreInstance, 'getNodeByName').mockReturnValue({
				name: oldName,
			} as INodeUi);
			ndvStore.activeNodeName = oldName;

			const { renameNode } = useCanvasOperations();
			const result = await renameNode(oldName, newName, { showErrorToast: false });

			expect(result).toBe(false);
			expect(toast.showMessage).not.toHaveBeenCalled();
		});
	});

	describe('revertRenameNode', () => {
		it('should revert node renaming', async () => {
			const ndvStore = mockedStore(useNDVStore, createWorkflowDocumentId(workflowId));
			const oldName = 'Old Node';
			const currentName = 'New Node';

			const workflowObject = createTestWorkflowObject();
			workflowObject.renameNode = vi.fn();
			vi.mocked(workflowDocumentStoreInstance.cloneWorkflowObject).mockReturnValue(workflowObject);
			vi.spyOn(workflowDocumentStoreInstance, 'getNodeByName').mockReturnValue({
				name: currentName,
			} as INodeUi);
			ndvStore.activeNodeName = currentName;

			const { revertRenameNode } = useCanvasOperations();
			await revertRenameNode(currentName, oldName);

			expect(ndvStore.setActiveNodeName).toHaveBeenCalledWith(oldName, expect.any(String));
		});

		it('should not revert node renaming when old name is same as new name', async () => {
			const ndvStore = mockedStore(useNDVStore, createWorkflowDocumentId(workflowId));
			const oldName = 'Old Node';
			vi.spyOn(workflowDocumentStoreInstance, 'getNodeByName').mockReturnValue({
				name: oldName,
			} as INodeUi);
			ndvStore.activeNodeName = oldName;

			const { revertRenameNode } = useCanvasOperations();
			await revertRenameNode(oldName, oldName);

			expect(ndvStore.activeNodeName).toBe(oldName);
		});
	});

	describe('setNodeActive', () => {
		it('should set active node name when node exists', () => {
			const ndvStore = mockedStore(useNDVStore, createWorkflowDocumentId(workflowId));
			const nodeId = 'node1';
			const nodeName = 'Node 1';
			vi.spyOn(workflowDocumentStoreInstance, 'getNodeById').mockReturnValue({
				name: nodeName,
			} as INodeUi);
			ndvStore.activeNodeName = '';

			const { setNodeActive } = useCanvasOperations();
			setNodeActive(nodeId, 'other');

			expect(ndvStore.setActiveNodeName).toHaveBeenCalledWith(nodeName, expect.any(String));
		});

		it('should not change active node name when node does not exist', () => {
			const ndvStore = mockedStore(useNDVStore, createWorkflowDocumentId(workflowId));
			const nodeId = 'node1';
			vi.spyOn(workflowDocumentStoreInstance, 'getNodeById').mockReturnValue(undefined);
			ndvStore.activeNodeName = 'Existing Node';

			const { setNodeActive } = useCanvasOperations();
			setNodeActive(nodeId, 'other');

			expect(ndvStore.activeNodeName).toBe('Existing Node');
		});

		it('should set node as dirty when node is set active', () => {
			const node = createTestNode();

			vi.spyOn(workflowDocumentStoreInstance, 'getNodeById').mockImplementation(() => node);

			const { setNodeActive } = useCanvasOperations();
			setNodeActive(node.id, 'other');

			expect(workflowDocumentStoreInstance.setNodePristine).toHaveBeenCalledWith(node.name, false);
		});
	});

	describe('setNodeActiveByName', () => {
		it('should set active node name', () => {
			const ndvStore = useNDVStore(createWorkflowDocumentId(workflowId));
			const nodeName = 'Node 1';
			ndvStore.activeNodeName = '';

			const { setNodeActiveByName } = useCanvasOperations();
			setNodeActiveByName(nodeName, 'other');

			expect(ndvStore.setActiveNodeName).toHaveBeenCalledWith(nodeName, expect.any(String));
		});
	});

	describe('toggleNodesDisabled', () => {
		it('disables nodes based on provided ids', async () => {
			const nodes = [
				createTestNode({ id: '1', name: 'A' }),
				createTestNode({ id: '2', name: 'B' }),
			];
			vi.spyOn(workflowDocumentStoreInstance, 'getNodesByIds').mockReturnValue(nodes);
			const updateNodePropertiesSpy = vi.spyOn(
				workflowDocumentStoreInstance,
				'updateNodeProperties',
			);

			const { toggleNodesDisabled } = useCanvasOperations();
			toggleNodesDisabled([nodes[0].id, nodes[1].id], {
				trackHistory: true,
				trackBulk: true,
			});

			expect(updateNodePropertiesSpy).toHaveBeenCalledWith({
				name: nodes[0].name,
				properties: {
					disabled: true,
				},
			});
		});
	});

	describe('revertToggleNodeDisabled', () => {
		it('re-enables a previously disabled node', () => {
			const nodeName = 'testNode';
			const node = createTestNode({ name: nodeName });
			vi.spyOn(workflowDocumentStoreInstance, 'getNodeByName').mockReturnValue(node);
			const updateNodePropertiesSpy = vi.spyOn(
				workflowDocumentStoreInstance,
				'updateNodeProperties',
			);

			const { revertToggleNodeDisabled } = useCanvasOperations();
			revertToggleNodeDisabled(nodeName);

			expect(updateNodePropertiesSpy).toHaveBeenCalledWith({
				name: nodeName,
				properties: {
					disabled: true,
				},
			});
		});
	});

	describe('toggleNodesPinned', () => {
		beforeEach(() => {
			canPinNodeMock.mockReset();
			setDataMock.mockReset();
			unsetDataMock.mockReset();
			getInputDataWithPinnedMock.mockReset();
			hasDataRef.value = false;
		});

		it('should only pin pinnable nodes when mix of pinnable and non-pinnable nodes are selected', () => {
			const historyStore = mockedStore(useHistoryStore);

			const pinnableNode1 = createTestNode({ id: '1', name: 'PinnableNode1' });
			const pinnableNode2 = createTestNode({ id: '2', name: 'PinnableNode2' });
			const nonPinnableNode = createTestNode({ id: '3', name: 'NonPinnableNode' });

			const nodes = [pinnableNode1, nonPinnableNode, pinnableNode2];
			vi.spyOn(workflowDocumentStoreInstance, 'getNodesByIds').mockReturnValue(nodes);

			let checkIndex = 0;
			const nodeOrder: string[] = [];

			// Mock canPinNode based on which node is being checked
			canPinNodeMock.mockImplementation(() => {
				const currentNodeIndex = checkIndex % nodes.length;
				const currentNode = nodes[currentNodeIndex];
				nodeOrder.push(currentNode.id);
				checkIndex++;
				// Make nodes with id 1 and 2 pinnable, 3 non-pinnable
				return currentNode.id !== '3';
			});

			getInputDataWithPinnedMock.mockReturnValue([{ json: { test: 'data' } }]);

			const { toggleNodesPinned } = useCanvasOperations();
			toggleNodesPinned(['1', '2', '3'], 'pin-icon-click');

			expect(historyStore.startRecordingUndo).toHaveBeenCalled();
			expect(historyStore.stopRecordingUndo).toHaveBeenCalled();
			expect(setDataMock).toHaveBeenCalledTimes(2);
			expect(setDataMock).toHaveBeenCalledWith([{ json: { test: 'data' } }], 'pin-icon-click');
			expect(unsetDataMock).not.toHaveBeenCalled();
		});

		it('should correctly unpin pinnable nodes when mix of pinnable and non-pinnable nodes are selected', () => {
			const historyStore = mockedStore(useHistoryStore);

			const pinnableNode1 = createTestNode({ id: '1', name: 'PinnableNode1' });
			const pinnableNode2 = createTestNode({ id: '2', name: 'PinnableNode2' });
			const nonPinnableNode = createTestNode({ id: '3', name: 'NonPinnableNode' });

			const nodes = [pinnableNode1, nonPinnableNode, pinnableNode2];
			vi.spyOn(workflowDocumentStoreInstance, 'getNodesByIds').mockReturnValue(nodes);

			// Indicate all pinnable nodes have pinned data
			hasDataRef.value = true;

			let checkIndex = 0;

			canPinNodeMock.mockImplementation(() => {
				const currentNodeIndex = checkIndex % nodes.length;
				const currentNode = nodes[currentNodeIndex];
				checkIndex++;
				return currentNode.id !== '3';
			});

			const { toggleNodesPinned } = useCanvasOperations();
			toggleNodesPinned(['1', '2', '3'], 'pin-icon-click');

			expect(historyStore.startRecordingUndo).toHaveBeenCalled();
			expect(historyStore.stopRecordingUndo).toHaveBeenCalled();
			expect(unsetDataMock).toHaveBeenCalledTimes(2);
			expect(unsetDataMock).toHaveBeenCalledWith('pin-icon-click');
			expect(setDataMock).not.toHaveBeenCalled();
		});

		it('should handle case where all nodes are non-pinnable', () => {
			const historyStore = mockedStore(useHistoryStore);

			const nonPinnableNode1 = createTestNode({ id: '1', name: 'NonPinnableNode1' });
			const nonPinnableNode2 = createTestNode({ id: '2', name: 'NonPinnableNode2' });

			const nodes = [nonPinnableNode1, nonPinnableNode2];
			vi.spyOn(workflowDocumentStoreInstance, 'getNodesByIds').mockReturnValue(nodes);

			canPinNodeMock.mockReturnValue(false);

			const { toggleNodesPinned } = useCanvasOperations();
			toggleNodesPinned(['1', '2'], 'pin-icon-click');

			expect(historyStore.startRecordingUndo).toHaveBeenCalled();
			expect(historyStore.stopRecordingUndo).toHaveBeenCalled();

			// Verify no pinning or unpinning occurred
			expect(setDataMock).not.toHaveBeenCalled();
			expect(unsetDataMock).not.toHaveBeenCalled();
		});
	});

	describe('addConnections', () => {
		it('should create connections between nodes', async () => {
			const nodeTypesStore = mockedStore(useNodeTypesStore);
			const nodeTypeName = SET_NODE_TYPE;
			const nodeType = mockNodeTypeDescription({
				name: nodeTypeName,
				inputs: [NodeConnectionTypes.Main],
				outputs: [NodeConnectionTypes.Main],
			});
			const nodes = [
				mockNode({ id: 'a', name: 'Node A', type: nodeTypeName, position: [40, 40] }),
				mockNode({ id: 'b', name: 'Node B', type: nodeTypeName, position: [40, 40] }),
				mockNode({ id: 'c', name: 'Node C', type: nodeTypeName, position: [40, 40] }),
			];
			const connections = [
				{
					source: nodes[0].id,
					sourceHandle: createCanvasConnectionHandleString({
						mode: CanvasConnectionMode.Output,
						index: 0,
						type: NodeConnectionTypes.Main,
					}),
					target: nodes[1].id,
					targetHandle: createCanvasConnectionHandleString({
						mode: CanvasConnectionMode.Input,
						index: 0,
						type: NodeConnectionTypes.Main,
					}),
					data: {
						source: { type: NodeConnectionTypes.Main, index: 0 },
						target: { type: NodeConnectionTypes.Main, index: 0 },
					},
				},
				{
					source: nodes[1].id,
					sourceHandle: createCanvasConnectionHandleString({
						mode: CanvasConnectionMode.Output,
						index: 0,
						type: NodeConnectionTypes.Main,
					}),
					target: nodes[2].id,
					targetHandle: createCanvasConnectionHandleString({
						mode: CanvasConnectionMode.Input,
						index: 0,
						type: NodeConnectionTypes.Main,
					}),
					data: {
						source: { type: NodeConnectionTypes.Main, index: 0 },
						target: { type: NodeConnectionTypes.Main, index: 0 },
					},
				},
			];

			workflowDocumentStoreInstance.allNodes = nodes;
			nodeTypesStore.nodeTypes = {
				[nodeTypeName]: { 1: nodeType },
			};

			vi.spyOn(workflowDocumentStoreInstance, 'getNodeById').mockImplementation(
				(id: string) =>
					({
						[nodes[0].id]: nodes[0],
						[nodes[1].id]: nodes[1],
						[nodes[2].id]: nodes[2],
					})[id] as INodeUi | undefined,
			);
			vi.spyOn(workflowDocumentStoreInstance, 'getNodeByName').mockImplementation(
				(name: string) =>
					(({
						[nodes[0].name]: nodes[0],
						[nodes[1].name]: nodes[1],
						[nodes[2].name]: nodes[2],
					})[name] as INodeUi) ?? null,
			);
			nodeTypesStore.getNodeType = vi.fn().mockReturnValue(nodeType);

			const { addConnections } = useCanvasOperations();
			await addConnections(connections);

			expect(workflowDocumentStoreInstance.addConnection).toHaveBeenCalledWith({
				connection: [
					{
						index: 0,
						node: 'Node A',
						type: NodeConnectionTypes.Main,
					},
					{
						index: 0,
						node: 'Node B',
						type: NodeConnectionTypes.Main,
					},
				],
			});
		});

		it('should set UI state as dirty', async () => {
			const uiStore = mockedStore(useUIStore);
			const connections: CanvasConnection[] = [];

			const { addConnections } = useCanvasOperations();
			await addConnections(connections, { keepPristine: false });

			expect(uiStore.markStateDirty).toHaveBeenCalled();
		});

		it('should not set UI state as dirty if keepPristine is true', async () => {
			const uiStore = mockedStore(useUIStore);
			const connections: CanvasConnection[] = [];

			const { addConnections } = useCanvasOperations();
			await addConnections(connections, { keepPristine: true });

			expect(uiStore.markStateDirty).not.toHaveBeenCalled();
		});
	});

	type TestNode = ReturnType<typeof createTestNode>;
	type TestConnectionOptions = {
		sourceType?: IConnection['type'];
		targetType?: IConnection['type'];
		sourceIndex?: number;
		targetIndex?: number;
	};

	function createGroupedNode(id: string, name: string, position?: TestNode['position']): TestNode {
		return createTestNode({ id, name, type: SET_NODE_TYPE, ...(position ? { position } : {}) });
	}

	function nodeEndpoint(
		node: TestNode,
		type: IConnection['type'] = NodeConnectionTypes.Main,
		index = 0,
	): IConnection {
		return { index, node: node.name, type };
	}

	function workflowConnection(
		sourceNode: TestNode,
		targetNode: TestNode,
		options: TestConnectionOptions = {},
	): [IConnection, IConnection] {
		const {
			sourceType = NodeConnectionTypes.Main,
			targetType = sourceType,
			sourceIndex = 0,
			targetIndex = 0,
		} = options;

		return [
			nodeEndpoint(sourceNode, sourceType, sourceIndex),
			nodeEndpoint(targetNode, targetType, targetIndex),
		];
	}

	function canvasConnection(
		sourceNode: TestNode,
		targetNode: TestNode,
		options: TestConnectionOptions = {},
	): Connection {
		const {
			sourceType = NodeConnectionTypes.Main,
			targetType = sourceType,
			sourceIndex = 0,
			targetIndex = 0,
		} = options;

		return {
			source: sourceNode.id,
			sourceHandle: `outputs/${sourceType}/${sourceIndex}`,
			target: targetNode.id,
			targetHandle: `inputs/${targetType}/${targetIndex}`,
		};
	}

	function createConnectionsBySource(
		...connections: Array<[IConnection, IConnection]>
	): IConnections {
		const connectionsBySource: IConnections = {};

		for (const [source, target] of connections) {
			const sourceConnections = (connectionsBySource[source.node] ??= {});
			const typeConnections = (sourceConnections[source.type] ??= []);
			const outputConnections = (typeConnections[source.index] ??= []);
			outputConnections.push(target);
		}

		return connectionsBySource;
	}

	function createGroupedNodeTypeDescription(
		connectionTypes: IConnection['type'][] = [NodeConnectionTypes.Main],
	): INodeTypeDescription {
		return mockNodeTypeDescription({
			name: SET_NODE_TYPE,
			inputs: connectionTypes,
			outputs: connectionTypes,
		});
	}

	function mockWorkflowObjectAccessors(nodes: TestNode[], connections: IConnections) {
		const workflowObject = createTestWorkflowObject({
			nodes,
			connections,
		});

		vi.mocked(workflowDocumentStoreInstance.getParentNodes).mockImplementation((...args) =>
			workflowObject.getParentNodes(...args),
		);
		vi.mocked(workflowDocumentStoreInstance.getChildNodes).mockImplementation((...args) =>
			workflowObject.getChildNodes(...args),
		);
		vi.mocked(workflowDocumentStoreInstance.getConnectionsBetweenNodes).mockImplementation(
			(...args) => workflowObject.getConnectionsBetweenNodes(...args),
		);
	}

	function setupGroupedCanvas({
		nodes,
		lookupNodes = nodes,
		connections = {},
		groups = [],
		nodeTypeDescription = createGroupedNodeTypeDescription(),
		mockWorkflowObject = false,
	}: {
		nodes: TestNode[];
		lookupNodes?: TestNode[];
		connections?: IConnections;
		groups?: IWorkflowGroup[];
		nodeTypeDescription?: INodeTypeDescription;
		mockWorkflowObject?: boolean;
	}) {
		const nodeTypesStore = mockedStore(useNodeTypesStore);
		const nodesById = new Map(lookupNodes.map((node) => [node.id, node]));
		const nodesByName = new Map(lookupNodes.map((node) => [node.name, node]));

		nodeTypesStore.nodeTypes = { [SET_NODE_TYPE]: { 1: nodeTypeDescription } };
		nodeTypesStore.getNodeType = vi.fn().mockReturnValue(nodeTypeDescription);
		workflowDocumentStoreInstance.allNodes = nodes;
		workflowDocumentStoreInstance.connectionsBySourceNode = connections;
		vi.spyOn(workflowDocumentStoreInstance, 'getNodeById').mockImplementation((id) =>
			nodesById.get(id),
		);
		vi.spyOn(workflowDocumentStoreInstance, 'getNodeByName').mockImplementation(
			(name) => nodesByName.get(name) ?? null,
		);
		vi.spyOn(workflowDocumentStoreInstance, 'allGroups', 'get').mockReturnValue(groups);
		vi.spyOn(workflowDocumentStoreInstance, 'getGroupForNode').mockImplementation((nodeId) =>
			groups.find((group) => group.nodeIds.includes(nodeId)),
		);

		if (mockWorkflowObject) {
			mockWorkflowObjectAccessors(nodes, connections);
		}

		return { workflowDocumentStore: workflowDocumentStoreInstance };
	}

	function expectConnectionAdded(
		sourceNode: TestNode,
		targetNode: TestNode,
		options: TestConnectionOptions = {},
	) {
		expect(workflowDocumentStoreInstance.addConnection).toHaveBeenCalledWith({
			connection: workflowConnection(sourceNode, targetNode, options),
		});
	}

	function expectConnectionRemoved(
		sourceNode: TestNode,
		targetNode: TestNode,
		options: TestConnectionOptions = {},
	) {
		expect(workflowDocumentStoreInstance.removeConnection).toHaveBeenCalledWith({
			connection: workflowConnection(sourceNode, targetNode, options),
		});
	}

	describe('createConnection', () => {
		it('should not create a connection if source node does not exist', () => {
			const uiStore = mockedStore(useUIStore);
			const connection: Connection = { source: 'nonexistent', target: 'targetNode' };

			vi.spyOn(workflowDocumentStoreInstance, 'getNodeById').mockReturnValueOnce(undefined);

			const { createConnection } = useCanvasOperations();
			createConnection(connection);

			expect(workflowDocumentStoreInstance.addConnection).not.toHaveBeenCalled();
			expect(uiStore.markStateDirty).not.toHaveBeenCalled();
		});

		it('should not create a connection if target node does not exist', () => {
			const uiStore = mockedStore(useUIStore);
			const connection: Connection = { source: 'sourceNode', target: 'nonexistent' };

			const sourceTestNode = createTestNode();
			vi.spyOn(workflowDocumentStoreInstance, 'getNodeById')
				.mockReturnValueOnce(sourceTestNode)
				.mockReturnValueOnce(undefined);

			const { createConnection } = useCanvasOperations();
			createConnection(connection);

			expect(workflowDocumentStoreInstance.addConnection).not.toHaveBeenCalled();
			expect(uiStore.markStateDirty).not.toHaveBeenCalled();
		});

		it('should create a connection if source and target nodes exist and connection is allowed', () => {
			const uiStore = mockedStore(useUIStore);
			const nodeTypesStore = mockedStore(useNodeTypesStore);

			const nodeTypeDescription = mockNodeTypeDescription({
				name: SET_NODE_TYPE,
				inputs: [NodeConnectionTypes.Main],
				outputs: [NodeConnectionTypes.Main],
			});

			const nodeA = createTestNode({
				id: 'a',
				type: nodeTypeDescription.name,
				name: 'Node A',
			});

			const nodeB = createTestNode({
				id: 'b',
				type: nodeTypeDescription.name,
				name: 'Node B',
			});

			const connection: Connection = {
				source: nodeA.id,
				sourceHandle: `outputs/${NodeConnectionTypes.Main}/0`,
				target: nodeB.id,
				targetHandle: `inputs/${NodeConnectionTypes.Main}/0`,
			};

			nodeTypesStore.nodeTypes = {
				node: { 1: nodeTypeDescription },
			};

			workflowDocumentStoreInstance.allNodes = [nodeA, nodeB];
			vi.spyOn(workflowDocumentStoreInstance, 'getNodeById')
				.mockReturnValueOnce(nodeA)
				.mockReturnValueOnce(nodeB);
			nodeTypesStore.getNodeType = vi.fn().mockReturnValue(nodeTypeDescription);

			vi.spyOn(workflowDocumentStoreInstance, 'getNodeByName').mockImplementation(
				(name: string) =>
					(({
						[nodeA.name]: nodeA,
						[nodeB.name]: nodeB,
					})[name] as INodeUi) ?? null,
			);
			const { createConnection } = useCanvasOperations();

			createConnection(connection);

			expect(workflowDocumentStoreInstance.addConnection).toHaveBeenCalledWith({
				connection: [
					{ index: 0, node: nodeA.name, type: NodeConnectionTypes.Main },
					{ index: 0, node: nodeB.name, type: NodeConnectionTypes.Main },
				],
			});
			expect(uiStore.markStateDirty).toHaveBeenCalled();
		});

		it('should not set UI state as dirty if keepPristine is true', () => {
			const uiStore = mockedStore(useUIStore);
			const nodeTypesStore = mockedStore(useNodeTypesStore);

			const nodeTypeDescription = mockNodeTypeDescription({
				name: SET_NODE_TYPE,
				inputs: [NodeConnectionTypes.Main],
				outputs: [NodeConnectionTypes.Main],
			});

			const nodeA = createTestNode({
				id: 'a',
				type: nodeTypeDescription.name,
				name: 'Node A',
			});

			const nodeB = createTestNode({
				id: 'b',
				type: nodeTypeDescription.name,
				name: 'Node B',
			});

			const connection: Connection = {
				source: nodeA.id,
				sourceHandle: `outputs/${NodeConnectionTypes.Main}/0`,
				target: nodeB.id,
				targetHandle: `inputs/${NodeConnectionTypes.Main}/0`,
			};

			nodeTypesStore.nodeTypes = {
				node: { 1: nodeTypeDescription },
			};

			workflowDocumentStoreInstance.allNodes = [nodeA, nodeB];
			vi.spyOn(workflowDocumentStoreInstance, 'getNodeById')
				.mockReturnValueOnce(nodeA)
				.mockReturnValueOnce(nodeB);
			nodeTypesStore.getNodeType = vi.fn().mockReturnValue(nodeTypeDescription);

			vi.spyOn(workflowDocumentStoreInstance, 'getNodeByName').mockImplementation(
				(name: string) =>
					(({
						[nodeA.name]: nodeA,
						[nodeB.name]: nodeB,
					})[name] as INodeUi) ?? null,
			);
			const { createConnection } = useCanvasOperations();

			createConnection(connection, { keepPristine: true });

			expect(uiStore.markStateDirty).not.toHaveBeenCalled();
		});

		it('adds the off-group endpoint to the group when that keeps the group valid', () => {
			const toast = useToast();
			const nodeA = createGroupedNode('a', 'A');
			const nodeB = createGroupedNode('b', 'B');
			const nodeC = createGroupedNode('c', 'C');
			const nodeD = createGroupedNode('d', 'D');
			const group = { id: 'group', nodeIds: [nodeB.id, nodeC.id], name: 'Group 1' };
			const { workflowDocumentStore } = setupGroupedCanvas({
				nodes: [nodeA, nodeB, nodeC, nodeD],
				connections: createConnectionsBySource(
					workflowConnection(nodeA, nodeB),
					workflowConnection(nodeB, nodeC),
					workflowConnection(nodeC, nodeD),
				),
				groups: [group],
			});
			const addNodesToGroupSpy = vi.spyOn(workflowDocumentStore, 'addNodesToGroup');

			const { createConnection } = useCanvasOperations();
			createConnection(canvasConnection(nodeA, nodeC));

			expect(addNodesToGroupSpy).toHaveBeenCalledWith(group.id, [nodeA.id]);
			expectConnectionAdded(nodeA, nodeC);
			expect(toast.showToast).toHaveBeenCalledWith(
				expect.objectContaining({
					title: "'Group 1' extended",
					message: "Added 'A' to keep 'Group 1' valid with the new connection.",
					type: 'info',
				}),
			);
			expect(toast.showToast).not.toHaveBeenCalledWith(expect.objectContaining({ type: 'error' }));
		});

		it('records the auto-extend and the connection in one bulk when tracking history', () => {
			const historyStore = mockedStore(useHistoryStore);
			const nodeA = createGroupedNode('a', 'A');
			const nodeB = createGroupedNode('b', 'B');
			const nodeC = createGroupedNode('c', 'C');
			const nodeD = createGroupedNode('d', 'D');
			const group = { id: 'group', nodeIds: [nodeB.id, nodeC.id], name: 'Group 1' };
			const { workflowDocumentStore } = setupGroupedCanvas({
				nodes: [nodeA, nodeB, nodeC, nodeD],
				connections: createConnectionsBySource(
					workflowConnection(nodeA, nodeB),
					workflowConnection(nodeB, nodeC),
					workflowConnection(nodeC, nodeD),
				),
				groups: [group],
			});
			vi.spyOn(workflowDocumentStore, 'getGroupById').mockReturnValue({
				...group,
				nodeIds: [nodeB.id, nodeC.id, nodeA.id],
			});

			const { createConnection } = useCanvasOperations();
			createConnection(canvasConnection(nodeA, nodeC), { trackHistory: true });

			expect(historyStore.startRecordingUndo).toHaveBeenCalled();
			expect(historyStore.stopRecordingUndo).toHaveBeenCalled();

			const pushedCommands = historyStore.pushCommandToUndo.mock.calls.map(([command]) => command);
			const groupCommand = pushedCommands.find(
				(command) => command instanceof UpdateNodeGroupCommand,
			) as UpdateNodeGroupCommand | undefined;
			expect(groupCommand).toBeInstanceOf(UpdateNodeGroupCommand);
			expect(groupCommand?.before.nodeIds).toEqual([nodeB.id, nodeC.id]);
			expect(groupCommand?.after.nodeIds).toEqual([nodeB.id, nodeC.id, nodeA.id]);
			expect(pushedCommands.some((command) => command instanceof AddConnectionCommand)).toBe(true);
		});

		it('does not record the auto-extend when history is not tracked', () => {
			const historyStore = mockedStore(useHistoryStore);
			const nodeA = createGroupedNode('a', 'A');
			const nodeB = createGroupedNode('b', 'B');
			const nodeC = createGroupedNode('c', 'C');
			const nodeD = createGroupedNode('d', 'D');
			const group = { id: 'group', nodeIds: [nodeB.id, nodeC.id], name: 'Group 1' };
			const { workflowDocumentStore } = setupGroupedCanvas({
				nodes: [nodeA, nodeB, nodeC, nodeD],
				connections: createConnectionsBySource(
					workflowConnection(nodeA, nodeB),
					workflowConnection(nodeB, nodeC),
					workflowConnection(nodeC, nodeD),
				),
				groups: [group],
			});
			const addNodesToGroupSpy = vi.spyOn(workflowDocumentStore, 'addNodesToGroup');

			const { createConnection } = useCanvasOperations();
			createConnection(canvasConnection(nodeA, nodeC));

			expect(addNodesToGroupSpy).toHaveBeenCalledWith(group.id, [nodeA.id]);
			expect(
				historyStore.pushCommandToUndo.mock.calls.some(
					([command]) => command instanceof UpdateNodeGroupCommand,
				),
			).toBe(false);
		});

		it('should skip node group validation when the grouping feature flag is disabled', () => {
			mockedStore(usePostHog).isFeatureEnabled.mockReturnValue(false);
			const toast = useToast();
			const nodeA = createGroupedNode('a', 'A');
			const nodeB = createGroupedNode('b', 'B');
			const nodeC = createGroupedNode('c', 'C');
			const nodeD = createGroupedNode('d', 'D');
			const group = { id: 'group', nodeIds: [nodeB.id, nodeC.id], name: 'Group 1' };
			const { workflowDocumentStore } = setupGroupedCanvas({
				nodes: [nodeA, nodeB, nodeC, nodeD],
				connections: createConnectionsBySource(
					workflowConnection(nodeA, nodeB),
					workflowConnection(nodeB, nodeC),
					workflowConnection(nodeC, nodeD),
				),
				groups: [group],
			});
			const addNodesToGroupSpy = vi.spyOn(workflowDocumentStore, 'addNodesToGroup');

			const { createConnection } = useCanvasOperations();
			createConnection(canvasConnection(nodeA, nodeC));

			expect(addNodesToGroupSpy).not.toHaveBeenCalled();
			expectConnectionAdded(nodeA, nodeC);
			expect(toast.showToast).not.toHaveBeenCalled();
		});

		it('allows a main connection across the group boundary when the group stays valid', () => {
			const toast = useToast();
			const nodeA = createGroupedNode('a', 'A');
			const nodeB = createGroupedNode('b', 'B');
			const nodeC = createGroupedNode('c', 'C');
			const group = { id: 'group', nodeIds: [nodeA.id, nodeB.id], name: 'Group' };
			const { workflowDocumentStore } = setupGroupedCanvas({
				nodes: [nodeA, nodeB, nodeC],
				connections: createConnectionsBySource(
					workflowConnection(nodeA, nodeB),
					workflowConnection(nodeB, nodeC),
				),
				groups: [group],
			});
			const addNodesToGroupSpy = vi.spyOn(workflowDocumentStore, 'addNodesToGroup');

			const { createConnection } = useCanvasOperations();
			createConnection(canvasConnection(nodeC, nodeA));

			expect(addNodesToGroupSpy).not.toHaveBeenCalled();
			expectConnectionAdded(nodeC, nodeA);
			expect(toast.showToast).not.toHaveBeenCalled();
		});

		it('auto-extends the group when a non-main connection brings the sub-node inside', () => {
			const toast = useToast();
			const nodeA = createGroupedNode('a', 'A');
			const nodeB = createGroupedNode('b', 'B');
			const nodeC = createGroupedNode('c', 'C');
			const group = { id: 'group', nodeIds: [nodeA.id, nodeB.id], name: 'Group 1' };
			const { workflowDocumentStore } = setupGroupedCanvas({
				nodes: [nodeA, nodeB, nodeC],
				connections: createConnectionsBySource(workflowConnection(nodeA, nodeB)),
				groups: [group],
				nodeTypeDescription: createGroupedNodeTypeDescription([
					NodeConnectionTypes.Main,
					NodeConnectionTypes.AiTool,
				]),
			});
			const addNodesToGroupSpy = vi.spyOn(workflowDocumentStore, 'addNodesToGroup');

			const { createConnection } = useCanvasOperations();
			createConnection(canvasConnection(nodeC, nodeB, { sourceType: NodeConnectionTypes.AiTool }));

			expect(addNodesToGroupSpy).toHaveBeenCalledWith(group.id, [nodeC.id]);
			expectConnectionAdded(nodeC, nodeB, { sourceType: NodeConnectionTypes.AiTool });
			expect(toast.showToast).toHaveBeenCalledWith(
				expect.objectContaining({
					title: "'Group 1' extended",
					type: 'info',
				}),
			);
			expect(toast.showToast).not.toHaveBeenCalledWith(expect.objectContaining({ type: 'error' }));
		});

		it('does not auto-extend when a non-main connection stays inside the group', () => {
			const toast = useToast();
			const nodeA = createGroupedNode('a', 'A');
			const nodeB = createGroupedNode('b', 'B');
			const nodeC = createGroupedNode('c', 'C');
			const group = { id: 'group', nodeIds: [nodeA.id, nodeB.id, nodeC.id], name: 'Group' };
			const { workflowDocumentStore } = setupGroupedCanvas({
				nodes: [nodeA, nodeB, nodeC],
				connections: createConnectionsBySource(workflowConnection(nodeA, nodeB)),
				groups: [group],
				nodeTypeDescription: createGroupedNodeTypeDescription([
					NodeConnectionTypes.Main,
					NodeConnectionTypes.AiTool,
				]),
			});
			const addNodesToGroupSpy = vi.spyOn(workflowDocumentStore, 'addNodesToGroup');

			const { createConnection } = useCanvasOperations();
			createConnection(canvasConnection(nodeC, nodeB, { sourceType: NodeConnectionTypes.AiTool }));

			expect(addNodesToGroupSpy).not.toHaveBeenCalled();
			expectConnectionAdded(nodeC, nodeB, { sourceType: NodeConnectionTypes.AiTool });
			expect(toast.showToast).not.toHaveBeenCalled();
		});

		it('does not auto-extend when the off-group endpoint already belongs to another group', () => {
			const toast = useToast();
			const nodeA = createGroupedNode('a', 'A');
			const nodeB = createGroupedNode('b', 'B');
			const nodeC = createGroupedNode('c', 'C');
			const nodeD = createGroupedNode('d', 'D');
			const groupOne = { id: 'one', nodeIds: [nodeA.id], name: 'Group 1' };
			const groupTwo = { id: 'two', nodeIds: [nodeB.id, nodeC.id], name: 'Group 2' };
			const { workflowDocumentStore } = setupGroupedCanvas({
				nodes: [nodeA, nodeB, nodeC, nodeD],
				connections: createConnectionsBySource(
					workflowConnection(nodeA, nodeB),
					workflowConnection(nodeB, nodeC),
					workflowConnection(nodeC, nodeD),
				),
				groups: [groupOne, groupTwo],
			});
			const addNodesToGroupSpy = vi.spyOn(workflowDocumentStore, 'addNodesToGroup');

			const { createConnection } = useCanvasOperations();
			createConnection(canvasConnection(nodeA, nodeC));

			expect(addNodesToGroupSpy).not.toHaveBeenCalled();
			expect(workflowDocumentStoreInstance.addConnection).not.toHaveBeenCalled();
			expect(toast.showToast).toHaveBeenCalledWith(expect.objectContaining({ type: 'error' }));
		});
	});

	describe('revertCreateConnection', () => {
		it('deletes connection if both source and target nodes exist', () => {
			const connection: [IConnection, IConnection] = [
				{ node: 'sourceNode', type: NodeConnectionTypes.Main, index: 0 },
				{ node: 'targetNode', type: NodeConnectionTypes.Main, index: 0 },
			];
			const testNode = createTestNode();

			vi.spyOn(workflowDocumentStoreInstance, 'getNodeByName').mockReturnValue(testNode);
			vi.spyOn(workflowDocumentStoreInstance, 'getNodeById').mockReturnValue(testNode);

			const { revertCreateConnection } = useCanvasOperations();
			revertCreateConnection(connection);

			expect(workflowDocumentStoreInstance.removeConnection).toHaveBeenCalled();
		});

		it('bypasses group validation when undoing an auto-extended connection add', () => {
			const toast = useToast();
			const nodeA = createGroupedNode('a', 'A');
			const nodeB = createGroupedNode('b', 'B');
			const nodeC = createGroupedNode('c', 'C');
			const group = { id: 'group', nodeIds: [nodeA.id, nodeB.id, nodeC.id], name: 'Group 1' };
			const connection = workflowConnection(nodeA, nodeC);

			setupGroupedCanvas({
				nodes: [nodeA, nodeB, nodeC],
				connections: createConnectionsBySource(
					workflowConnection(nodeA, nodeB),
					workflowConnection(nodeA, nodeC),
				),
				groups: [group],
			});

			const { revertCreateConnection } = useCanvasOperations();
			revertCreateConnection(connection);

			expect(workflowDocumentStoreInstance.removeConnection).toHaveBeenCalledWith({
				connection,
			});
			expect(toast.showToast).not.toHaveBeenCalledWith(expect.objectContaining({ type: 'error' }));
		});
	});

	describe('isConnectionAllowed', () => {
		it('should return false if target node type does not have inputs', () => {
			const nodeTypesStore = mockedStore(useNodeTypesStore);
			const sourceNode = mockNode({
				id: '1',
				type: 'sourceType',
				name: 'Source Node',
			});
			const sourceNodeTypeDescription = mockNodeTypeDescription({
				name: sourceNode.type,
				outputs: [],
			});
			const sourceHandle: IConnection = {
				node: sourceNode.name,
				type: NodeConnectionTypes.Main,
				index: 0,
			};
			const targetNode = mockNode({
				id: '2',
				type: 'targetType',
				name: 'Target Node',
			});
			const targetNodeTypeDescription = mockNodeTypeDescription({
				name: targetNode.type,
				inputs: [],
			});
			const targetHandle: IConnection = {
				node: targetNode.name,
				type: NodeConnectionTypes.Main,
				index: 0,
			};

			nodeTypesStore.getNodeType = vi.fn(
				(nodeTypeName: string) =>
					({
						[sourceNode.type]: sourceNodeTypeDescription,
						[targetNode.type]: targetNodeTypeDescription,
					})[nodeTypeName],
			);

			const { isConnectionAllowed } = useCanvasOperations();
			expect(isConnectionAllowed(sourceNode, targetNode, sourceHandle, targetHandle)).toBe(false);
		});

		it('should return false if target node does not exist in the workflow', () => {
			const nodeTypesStore = mockedStore(useNodeTypesStore);

			const sourceNode = mockNode({
				id: '1',
				type: 'sourceType',
				name: 'Source Node',
			});
			const sourceNodeTypeDescription = mockNodeTypeDescription({
				name: sourceNode.type,
				outputs: [],
			});
			const sourceHandle: IConnection = {
				node: sourceNode.name,
				type: NodeConnectionTypes.Main,
				index: 0,
			};
			const targetNode = mockNode({
				id: '2',
				type: 'targetType',
				name: 'Target Node',
			});
			const targetNodeTypeDescription = mockNodeTypeDescription({
				name: targetNode.type,
				inputs: [NodeConnectionTypes.Main],
			});
			const targetHandle: IConnection = {
				node: targetNode.name,
				type: NodeConnectionTypes.Main,
				index: 0,
			};

			nodeTypesStore.getNodeType = vi.fn(
				(nodeTypeName: string) =>
					({
						[sourceNode.type]: sourceNodeTypeDescription,
						[targetNode.type]: targetNodeTypeDescription,
					})[nodeTypeName],
			);

			const { isConnectionAllowed } = useCanvasOperations();
			expect(isConnectionAllowed(sourceNode, targetNode, sourceHandle, targetHandle)).toBe(false);
		});

		it('should return false if source node does not have connection type', () => {
			const nodeTypesStore = mockedStore(useNodeTypesStore);
			const sourceNode = mockNode({
				id: '1',
				type: 'sourceType',
				name: 'Source Node',
			});
			const sourceNodeTypeDescription = mockNodeTypeDescription({
				name: sourceNode.type,
				outputs: [NodeConnectionTypes.Main],
			});
			const sourceHandle: IConnection = {
				node: sourceNode.name,
				type: NodeConnectionTypes.AiTool,
				index: 0,
			};

			const targetNode = mockNode({
				id: '2',
				type: 'targetType',
				name: 'Target Node',
			});
			const targetNodeTypeDescription = mockNodeTypeDescription({
				name: 'targetType',
				inputs: [NodeConnectionTypes.AiTool],
			});
			const targetHandle: IConnection = {
				node: targetNode.name,
				type: NodeConnectionTypes.AiTool,
				index: 0,
			};

			vi.spyOn(workflowDocumentStoreInstance, 'getNodeByName').mockImplementation(
				(name: string) =>
					(({
						[sourceNode.name]: sourceNode,
						[targetNode.name]: targetNode,
					})[name] as INodeUi) ?? null,
			);
			const { isConnectionAllowed } = useCanvasOperations();

			nodeTypesStore.getNodeType = vi.fn(
				(nodeTypeName: string) =>
					({
						[sourceNode.type]: sourceNodeTypeDescription,
						[targetNode.type]: targetNodeTypeDescription,
					})[nodeTypeName],
			);

			expect(isConnectionAllowed(sourceNode, targetNode, sourceHandle, targetHandle)).toBe(false);
		});

		it('should return false if target node does not have connection type', () => {
			const nodeTypesStore = mockedStore(useNodeTypesStore);
			const sourceNode = mockNode({
				id: '1',
				type: 'sourceType',
				name: 'Source Node',
			});
			const sourceNodeTypeDescription = mockNodeTypeDescription({
				name: sourceNode.type,
				outputs: [NodeConnectionTypes.Main],
			});
			const sourceHandle: IConnection = {
				node: sourceNode.name,
				type: NodeConnectionTypes.Main,
				index: 0,
			};

			const targetNode = mockNode({
				id: '2',
				type: 'targetType',
				name: 'Target Node',
			});
			const targetNodeTypeDescription = mockNodeTypeDescription({
				name: 'targetType',
				inputs: [NodeConnectionTypes.AiTool],
			});
			const targetHandle: IConnection = {
				node: targetNode.name,
				type: NodeConnectionTypes.AiTool,
				index: 0,
			};

			vi.spyOn(workflowDocumentStoreInstance, 'getNodeByName').mockImplementation(
				(name: string) =>
					(({
						[sourceNode.name]: sourceNode,
						[targetNode.name]: targetNode,
					})[name] as INodeUi) ?? null,
			);
			const { isConnectionAllowed } = useCanvasOperations();

			nodeTypesStore.getNodeType = vi.fn(
				(nodeTypeName: string) =>
					({
						[sourceNode.type]: sourceNodeTypeDescription,
						[targetNode.type]: targetNodeTypeDescription,
					})[nodeTypeName],
			);

			expect(isConnectionAllowed(sourceNode, targetNode, sourceHandle, targetHandle)).toBe(false);
		});

		it('should return false if source node type is not allowed by target node input filter', () => {
			const nodeTypesStore = mockedStore(useNodeTypesStore);
			const sourceNode = mockNode({
				id: '1',
				type: 'sourceType',
				name: 'Source Node',
				typeVersion: 1,
			});
			const sourceNodeTypeDescription = mockNodeTypeDescription({
				name: sourceNode.type,
				outputs: [NodeConnectionTypes.Main],
			});
			const sourceHandle: IConnection = {
				node: sourceNode.name,
				type: NodeConnectionTypes.Main,
				index: 0,
			};

			const targetNode = mockNode({
				id: '2',
				type: 'targetType',
				name: 'Target Node',
				typeVersion: 1,
			});
			const targetNodeTypeDescription = mockNodeTypeDescription({
				name: 'targetType',
				inputs: [
					{
						type: NodeConnectionTypes.Main,
						filter: {
							nodes: ['allowedType'],
						},
					},
				],
			});
			const targetHandle: IConnection = {
				node: targetNode.name,
				type: NodeConnectionTypes.Main,
				index: 0,
			};

			vi.spyOn(workflowDocumentStoreInstance, 'getNodeByName').mockImplementation(
				(name: string) =>
					(({
						[sourceNode.name]: sourceNode,
						[targetNode.name]: targetNode,
					})[name] as INodeUi) ?? null,
			);
			const { isConnectionAllowed } = useCanvasOperations();

			nodeTypesStore.getNodeType = vi.fn(
				(nodeTypeName: string) =>
					({
						[sourceNode.type]: sourceNodeTypeDescription,
						[targetNode.type]: targetNodeTypeDescription,
					})[nodeTypeName],
			);

			expect(isConnectionAllowed(sourceNode, targetNode, sourceHandle, targetHandle)).toBe(false);
		});

		it('should return false if target node type is not allowed by source node output filter', () => {
			const nodeTypesStore = mockedStore(useNodeTypesStore);
			const sourceNode = mockNode({
				id: '1',
				type: 'sourceType',
				name: 'Source Node',
				typeVersion: 1,
			});
			const sourceNodeTypeDescription = mockNodeTypeDescription({
				name: sourceNode.type,
				outputs: [
					{
						type: NodeConnectionTypes.Main,
						filter: {
							nodes: ['allowedType'],
						},
					},
				],
			});
			const sourceHandle: IConnection = {
				node: sourceNode.name,
				type: NodeConnectionTypes.Main,
				index: 0,
			};

			const targetNode = mockNode({
				id: '2',
				type: 'targetType',
				name: 'Target Node',
				typeVersion: 1,
			});
			const targetNodeTypeDescription = mockNodeTypeDescription({
				name: 'targetType',
				inputs: [
					{
						type: NodeConnectionTypes.Main,
					},
				],
			});
			const targetHandle: IConnection = {
				node: targetNode.name,
				type: NodeConnectionTypes.Main,
				index: 0,
			};

			vi.spyOn(workflowDocumentStoreInstance, 'getNodeByName').mockImplementation(
				(name: string) =>
					(({
						[sourceNode.name]: sourceNode,
						[targetNode.name]: targetNode,
					})[name] as INodeUi) ?? null,
			);
			const { isConnectionAllowed } = useCanvasOperations();

			nodeTypesStore.getNodeType = vi.fn(
				(nodeTypeName: string) =>
					({
						[sourceNode.type]: sourceNodeTypeDescription,
						[targetNode.type]: targetNodeTypeDescription,
					})[nodeTypeName],
			);

			expect(isConnectionAllowed(sourceNode, targetNode, sourceHandle, targetHandle)).toBe(false);
		});

		it('should return false if source node type does not have connection type index', () => {
			const nodeTypesStore = mockedStore(useNodeTypesStore);
			const sourceNode = mockNode({
				id: '1',
				type: 'sourceType',
				name: 'Source Node',
				typeVersion: 1,
			});
			const sourceNodeTypeDescription = mockNodeTypeDescription({
				name: sourceNode.type,
				outputs: [NodeConnectionTypes.Main],
			});
			const sourceHandle: IConnection = {
				node: sourceNode.name,
				type: NodeConnectionTypes.Main,
				index: 1,
			};

			const targetNode = mockNode({
				id: '2',
				type: 'targetType',
				name: 'Target Node',
				typeVersion: 1,
			});
			const targetNodeTypeDescription = mockNodeTypeDescription({
				name: targetNode.type,
				inputs: [
					{
						type: NodeConnectionTypes.Main,
						filter: {
							nodes: [sourceNode.type],
						},
					},
				],
			});
			const targetHandle: IConnection = {
				node: targetNode.name,
				type: NodeConnectionTypes.Main,
				index: 0,
			};

			vi.spyOn(workflowDocumentStoreInstance, 'getNodeByName').mockImplementation(
				(name: string) =>
					(({
						[sourceNode.name]: sourceNode,
						[targetNode.name]: targetNode,
					})[name] as INodeUi) ?? null,
			);
			const { isConnectionAllowed } = useCanvasOperations();

			nodeTypesStore.getNodeType = vi.fn(
				(nodeTypeName: string) =>
					({
						[sourceNode.type]: sourceNodeTypeDescription,
						[targetNode.type]: targetNodeTypeDescription,
					})[nodeTypeName],
			);

			expect(isConnectionAllowed(sourceNode, targetNode, sourceHandle, targetHandle)).toBe(false);
		});

		it('should return false if target node type does not have connection type index', () => {
			const nodeTypesStore = mockedStore(useNodeTypesStore);
			const sourceNode = mockNode({
				id: '1',
				type: 'sourceType',
				name: 'Source Node',
				typeVersion: 1,
			});
			const sourceNodeTypeDescription = mockNodeTypeDescription({
				name: sourceNode.type,
				outputs: [NodeConnectionTypes.Main],
			});
			const sourceHandle: IConnection = {
				node: sourceNode.name,
				type: NodeConnectionTypes.Main,
				index: 0,
			};

			const targetNode = mockNode({
				id: '2',
				type: 'targetType',
				name: 'Target Node',
				typeVersion: 1,
			});
			const targetNodeTypeDescription = mockNodeTypeDescription({
				name: targetNode.type,
				inputs: [
					{
						type: NodeConnectionTypes.Main,
						filter: {
							nodes: [sourceNode.type],
						},
					},
				],
			});
			const targetHandle: IConnection = {
				node: targetNode.name,
				type: NodeConnectionTypes.Main,
				index: 1,
			};

			vi.spyOn(workflowDocumentStoreInstance, 'getNodeByName').mockImplementation(
				(name: string) =>
					(({
						[sourceNode.name]: sourceNode,
						[targetNode.name]: targetNode,
					})[name] as INodeUi) ?? null,
			);
			const { isConnectionAllowed } = useCanvasOperations();

			nodeTypesStore.getNodeType = vi.fn(
				(nodeTypeName: string) =>
					({
						[sourceNode.type]: sourceNodeTypeDescription,
						[targetNode.type]: targetNodeTypeDescription,
					})[nodeTypeName],
			);

			expect(isConnectionAllowed(sourceNode, targetNode, sourceHandle, targetHandle)).toBe(false);
		});

		it('should return true if all conditions including filter are met', () => {
			const nodeTypesStore = mockedStore(useNodeTypesStore);

			const sourceNode = mockNode({
				id: '1',
				type: 'sourceType',
				name: 'Source Node',
				typeVersion: 1,
			});
			const sourceNodeTypeDescription = mockNodeTypeDescription({
				name: sourceNode.type,
				outputs: [NodeConnectionTypes.Main],
			});
			const sourceHandle: IConnection = {
				node: sourceNode.name,
				type: NodeConnectionTypes.Main,
				index: 0,
			};

			const targetNode = mockNode({
				id: '2',
				type: 'targetType',
				name: 'Target Node',
				typeVersion: 1,
			});
			const targetNodeTypeDescription = mockNodeTypeDescription({
				name: targetNode.type,
				inputs: [
					{
						type: NodeConnectionTypes.Main,
						filter: {
							nodes: [sourceNode.type],
						},
					},
				],
			});
			const targetHandle: IConnection = {
				node: targetNode.name,
				type: NodeConnectionTypes.Main,
				index: 0,
			};

			vi.spyOn(workflowDocumentStoreInstance, 'getNodeByName').mockImplementation(
				(name: string) =>
					(({
						[sourceNode.name]: sourceNode,
						[targetNode.name]: targetNode,
					})[name] as INodeUi) ?? null,
			);
			const { isConnectionAllowed } = useCanvasOperations();

			nodeTypesStore.getNodeType = vi.fn(
				(nodeTypeName: string) =>
					({
						[sourceNode.type]: sourceNodeTypeDescription,
						[targetNode.type]: targetNodeTypeDescription,
					})[nodeTypeName],
			);

			expect(isConnectionAllowed(sourceNode, targetNode, sourceHandle, targetHandle)).toBe(true);
		});

		it('should return true if all conditions are met and no filter is set', () => {
			const nodeTypesStore = mockedStore(useNodeTypesStore);

			const sourceNode = mockNode({
				id: '1',
				type: 'sourceType',
				name: 'Source Node',
				typeVersion: 1,
			});
			const sourceNodeTypeDescription = mockNodeTypeDescription({
				name: sourceNode.type,
				outputs: [NodeConnectionTypes.Main],
			});
			const sourceHandle: IConnection = {
				node: sourceNode.name,
				type: NodeConnectionTypes.Main,
				index: 0,
			};

			const targetNode = mockNode({
				id: '2',
				type: 'targetType',
				name: 'Target Node',
				typeVersion: 1,
			});
			const targetNodeTypeDescription = mockNodeTypeDescription({
				name: targetNode.type,
				inputs: [
					{
						type: NodeConnectionTypes.Main,
					},
				],
			});
			const targetHandle: IConnection = {
				node: targetNode.name,
				type: NodeConnectionTypes.Main,
				index: 0,
			};

			vi.spyOn(workflowDocumentStoreInstance, 'getNodeByName').mockImplementation(
				(name: string) =>
					(({
						[sourceNode.name]: sourceNode,
						[targetNode.name]: targetNode,
					})[name] as INodeUi) ?? null,
			);
			const { isConnectionAllowed } = useCanvasOperations();

			nodeTypesStore.getNodeType = vi.fn(
				(nodeTypeName: string) =>
					({
						[sourceNode.type]: sourceNodeTypeDescription,
						[targetNode.type]: targetNodeTypeDescription,
					})[nodeTypeName],
			);

			expect(isConnectionAllowed(sourceNode, targetNode, sourceHandle, targetHandle)).toBe(true);
		});

		it.each(['target' as const, 'source' as const])(
			'should return true if source node is not installed verified node',
			(type) => {
				const nodeTypesStore = mockedStore(useNodeTypesStore);

				const sourceNode = mockNode({
					id: '1',
					type: 'sourceType',
					name: 'Source Node',
					typeVersion: 1,
				});
				const sourceNodeTypeDescription = mockNodeTypeDescription({
					name: sourceNode.type,
					outputs: [NodeConnectionTypes.Main],
				});
				const sourceHandle: IConnection = {
					node: sourceNode.name,
					type: NodeConnectionTypes.Main,
					index: 0,
				};

				const targetNode = mockNode({
					id: '2',
					type: 'targetType',
					name: 'Target Node',
					typeVersion: 1,
				});
				const targetNodeTypeDescription = mockNodeTypeDescription({
					name: targetNode.type,
					inputs: [
						{
							type: NodeConnectionTypes.Main,
						},
					],
				});
				const targetHandle: IConnection = {
					node: targetNode.name,
					type: NodeConnectionTypes.Main,
					index: 0,
				};

				vi.spyOn(workflowDocumentStoreInstance, 'getNodeByName').mockImplementation(
					(name: string) =>
						(({
							[sourceNode.name]: sourceNode,
							[targetNode.name]: targetNode,
						})[name] as INodeUi) ?? null,
				);
				const { isConnectionAllowed } = useCanvasOperations();

				if (type === 'source') {
					nodeTypesStore.getNodeType = vi.fn(
						(nodeTypeName: string) =>
							({
								[targetNode.type]: targetNodeTypeDescription,
							})[nodeTypeName],
					);
					nodeTypesStore.communityNodeType = vi
						.fn()
						.mockReturnValue({ nodeDescription: sourceNodeTypeDescription });
				} else {
					nodeTypesStore.getNodeType = vi.fn(
						(nodeTypeName: string) =>
							({
								[sourceNode.type]: sourceNodeTypeDescription,
							})[nodeTypeName],
					);
					nodeTypesStore.communityNodeType = vi
						.fn()
						.mockReturnValue({ nodeDescription: targetNodeTypeDescription });
				}

				expect(isConnectionAllowed(sourceNode, targetNode, sourceHandle, targetHandle)).toBe(true);
			},
		);

		it.each(['target' as const, 'source' as const])(
			'should return true if %s node is not installed unverified node',
			(type) => {
				const nodeTypesStore = mockedStore(useNodeTypesStore);

				const sourceNode = mockNode({
					id: '1',
					type: 'n8n-nodes-community.sourceType',
					name: 'Source Node',
					typeVersion: 1,
				});
				const sourceHandle: IConnection = {
					node: sourceNode.name,
					type: NodeConnectionTypes.Main,
					index: 0,
				};

				const targetNode = mockNode({
					id: '2',
					type: 'n8n-nodes-community.targetType',
					name: 'Target Node',
					typeVersion: 1,
				});
				const targetNodeTypeDescription = mockNodeTypeDescription({
					name: targetNode.type,
					inputs: [
						{
							type: NodeConnectionTypes.Main,
						},
					],
				});
				const sourceNodeTypeDescription = mockNodeTypeDescription({
					name: sourceNode.type,
					outputs: [NodeConnectionTypes.Main],
				});
				const targetHandle: IConnection = {
					node: targetNode.name,
					type: NodeConnectionTypes.Main,
					index: 0,
				};

				vi.spyOn(workflowDocumentStoreInstance, 'getNodeByName').mockImplementation(
					(name: string) =>
						(({
							[sourceNode.name]: sourceNode,
							[targetNode.name]: targetNode,
						})[name] as INodeUi) ?? null,
				);
				const { isConnectionAllowed } = useCanvasOperations();

				if (type === 'source') {
					nodeTypesStore.getNodeType = vi.fn(
						(nodeTypeName: string) =>
							({
								[targetNode.type]: targetNodeTypeDescription,
							})[nodeTypeName],
					);
				} else {
					nodeTypesStore.getNodeType = vi.fn(
						(nodeTypeName: string) =>
							({
								[sourceNode.type]: sourceNodeTypeDescription,
							})[nodeTypeName],
					);
				}

				nodeTypesStore.getIsNodeInstalled =
					type === 'source'
						? vi.fn().mockReturnValueOnce(false).mockReturnValueOnce(true)
						: vi.fn().mockReturnValueOnce(true).mockReturnValueOnce(false);

				expect(isConnectionAllowed(sourceNode, targetNode, sourceHandle, targetHandle)).toBe(true);
			},
		);

		it('should return true if node connecting to itself', () => {
			const nodeTypesStore = mockedStore(useNodeTypesStore);

			const sourceNode = mockNode({
				id: '1',
				type: 'sourceType',
				name: 'Source Node',
				typeVersion: 1,
			});
			const sourceNodeTypeDescription = mockNodeTypeDescription({
				name: sourceNode.type,
				outputs: [NodeConnectionTypes.Main],
			});
			const sourceHandle: IConnection = {
				node: sourceNode.name,
				type: NodeConnectionTypes.Main,
				index: 0,
			};
			const targetHandle: IConnection = {
				node: sourceNode.name,
				type: NodeConnectionTypes.Main,
				index: 0,
			};

			vi.spyOn(workflowDocumentStoreInstance, 'getNodeByName').mockImplementation(
				(name: string) =>
					(({
						[sourceNode.name]: sourceNode,
					})[name] as INodeUi) ?? null,
			);
			const { isConnectionAllowed } = useCanvasOperations();

			nodeTypesStore.getNodeType = vi.fn(
				(nodeTypeName: string) =>
					({
						[sourceNode.type]: sourceNodeTypeDescription,
					})[nodeTypeName],
			);

			expect(isConnectionAllowed(sourceNode, sourceNode, sourceHandle, targetHandle)).toBe(true);
		});
	});

	describe('deleteConnection', () => {
		it('should not delete a connection if source node does not exist', () => {
			const connection: Connection = { source: 'nonexistent', target: 'targetNode' };

			const targetTestNode = createTestNode();
			vi.spyOn(workflowDocumentStoreInstance, 'getNodeById')
				.mockReturnValueOnce(undefined)
				.mockReturnValueOnce(targetTestNode);

			const { deleteConnection } = useCanvasOperations();
			deleteConnection(connection);

			expect(workflowDocumentStoreInstance.removeConnection).not.toHaveBeenCalled();
		});

		it('should not delete a connection if target node does not exist', () => {
			const connection: Connection = { source: 'sourceNode', target: 'nonexistent' };

			const sourceTestNode = createTestNode();
			vi.spyOn(workflowDocumentStoreInstance, 'getNodeById')
				.mockReturnValueOnce(sourceTestNode)
				.mockReturnValueOnce(undefined);

			const { deleteConnection } = useCanvasOperations();
			deleteConnection(connection);

			expect(workflowDocumentStoreInstance.removeConnection).not.toHaveBeenCalled();
		});

		it('should delete a connection if source and target nodes exist', () => {
			const nodeA = createTestNode({
				id: 'a',
				type: 'node',
				name: 'Node A',
			});

			const nodeB = createTestNode({
				id: 'b',
				type: 'node',
				name: 'Node B',
			});

			const connection: Connection = {
				source: nodeA.id,
				sourceHandle: `outputs/${NodeConnectionTypes.Main}/0`,
				target: nodeB.id,
				targetHandle: `inputs/${NodeConnectionTypes.Main}/0`,
			};

			vi.spyOn(workflowDocumentStoreInstance, 'getNodeById')
				.mockReturnValueOnce(nodeA)
				.mockReturnValueOnce(nodeB);

			const { deleteConnection } = useCanvasOperations();
			deleteConnection(connection);

			expect(workflowDocumentStoreInstance.removeConnection).toHaveBeenCalledWith({
				connection: [
					{ index: 0, node: nodeA.name, type: NodeConnectionTypes.Main },
					{ index: 0, node: nodeB.name, type: NodeConnectionTypes.Main },
				],
			});
		});

		it('should not delete a connection that would make an affected node group invalid', () => {
			const toast = useToast();
			const nodeA = createGroupedNode('a', 'A');
			const nodeB = createGroupedNode('b', 'B');
			const group = { id: 'group', nodeIds: [nodeA.id, nodeB.id], name: 'Group' };
			const { workflowDocumentStore } = setupGroupedCanvas({
				nodes: [nodeA, nodeB],
				connections: createConnectionsBySource(workflowConnection(nodeA, nodeB)),
				groups: [group],
			});
			const deleteGroupSpy = vi.spyOn(workflowDocumentStore, 'deleteGroup');

			const { deleteConnection } = useCanvasOperations();
			deleteConnection(canvasConnection(nodeA, nodeB));

			expect(workflowDocumentStoreInstance.removeConnection).not.toHaveBeenCalled();
			expect(toast.showToast).toHaveBeenCalledWith(
				expect.objectContaining({
					title: 'Connection not removed',
					type: 'error',
				}),
			);

			const toastConfig = vi.mocked(toast.showToast).mock.calls.at(-1)?.[0];
			expect(toastConfig).toBeDefined();
			expect(isVNode(toastConfig?.message)).toBe(true);
			if (!toastConfig || !isVNode(toastConfig.message)) {
				throw new Error('Expected toast message to be a vnode');
			}

			const messageChildren = toastConfig.message.children;
			if (!Array.isArray(messageChildren)) {
				throw new Error('Expected toast message children to be an array');
			}

			expect(messageChildren[0]).toBe(
				"'Group' must stay connected from first node to last node. You can make this change after ungrouping it.",
			);
			const ungroupAction = messageChildren[2];
			expect(isVNode(ungroupAction)).toBe(true);
			if (!isVNode(ungroupAction)) {
				throw new Error('Expected ungroup action to be a vnode');
			}

			expect(ungroupAction.children).toBe('Ungroup nodes');
			expect(ungroupAction.props).toEqual(
				expect.objectContaining({ href: '#', class: 'primary-color' }),
			);

			const event = {
				preventDefault: vi.fn(),
				stopPropagation: vi.fn(),
			} as unknown as MouseEvent;
			ungroupAction.props?.onClick(event);

			expect(event.preventDefault).toHaveBeenCalled();
			expect(event.stopPropagation).toHaveBeenCalled();
			expect(deleteGroupSpy).toHaveBeenCalledWith(group.id);
		});

		it('should update node input issues for both nodes after deleting connection', async () => {
			const nodeA = createTestNode({
				id: 'a',
				type: 'node',
				name: 'Node A',
			});

			const nodeB = createTestNode({
				id: 'b',
				type: 'node',
				name: 'Node B',
			});

			const connection: Connection = {
				source: nodeA.id,
				sourceHandle: `outputs/${NodeConnectionTypes.Main}/0`,
				target: nodeB.id,
				targetHandle: `inputs/${NodeConnectionTypes.Main}/0`,
			};

			vi.spyOn(workflowDocumentStoreInstance, 'getNodeById')
				.mockReturnValueOnce(nodeA)
				.mockReturnValueOnce(nodeB);

			const updateNodeInputIssuesSpy = vi.fn();
			const nodeHelpersOriginal = nodeHelpers.useNodeHelpers();
			vi.spyOn(nodeHelpers, 'useNodeHelpers').mockImplementation(() => ({
				...nodeHelpersOriginal,
				updateNodeInputIssues: updateNodeInputIssuesSpy,
			}));

			const { deleteConnection } = useCanvasOperations();
			deleteConnection(connection);

			await nextTick();

			expect(updateNodeInputIssuesSpy).toHaveBeenCalledWith(nodeA);
			expect(updateNodeInputIssuesSpy).toHaveBeenCalledWith(nodeB);
		});
	});

	describe('revertDeleteConnection', () => {
		it('should revert delete connection', () => {
			const nodeTypesStore = mockedStore(useNodeTypesStore);
			const sourceNode = createTestNode({
				id: 'source',
				name: 'sourceNode',
				type: SET_NODE_TYPE,
			});
			const targetNode = createTestNode({
				id: 'target',
				name: 'targetNode',
				type: SET_NODE_TYPE,
			});
			const connection: [IConnection, IConnection] = [
				{ node: sourceNode.name, type: NodeConnectionTypes.Main, index: 1 },
				{ node: targetNode.name, type: NodeConnectionTypes.Main, index: 2 },
			];

			nodeTypesStore.getNodeType = vi.fn().mockReturnValue(
				mockNodeTypeDescription({
					name: SET_NODE_TYPE,
					inputs: [NodeConnectionTypes.Main, NodeConnectionTypes.Main, NodeConnectionTypes.Main],
					outputs: [NodeConnectionTypes.Main, NodeConnectionTypes.Main, NodeConnectionTypes.Main],
				}),
			);
			workflowDocumentStoreInstance.allNodes = [sourceNode, targetNode];
			vi.spyOn(workflowDocumentStoreInstance, 'getNodeByName').mockImplementation(
				(name) =>
					({
						[sourceNode.name]: sourceNode,
						[targetNode.name]: targetNode,
					})[name] ?? null,
			);
			vi.spyOn(workflowDocumentStoreInstance, 'getNodeById').mockImplementation(
				(id) =>
					({
						[sourceNode.id]: sourceNode,
						[targetNode.id]: targetNode,
					})[id],
			);

			const { revertDeleteConnection } = useCanvasOperations();
			revertDeleteConnection(connection);

			expect(workflowDocumentStoreInstance.addConnection).toHaveBeenCalledWith({ connection });
		});

		it('re-adds the connection without re-running group validation', () => {
			// Undo restores an already-valid state, so the connection re-adds without validation.
			const toast = useToast();
			const nodeA = createGroupedNode('a', 'A');
			const nodeB = createGroupedNode('b', 'B');
			const nodeC = createGroupedNode('c', 'C');
			const nodeD = createGroupedNode('d', 'D');
			const group = { id: 'group', nodeIds: [nodeB.id, nodeC.id], name: 'Group 1' };
			const connection = workflowConnection(nodeA, nodeC);
			const { workflowDocumentStore } = setupGroupedCanvas({
				nodes: [nodeA, nodeB, nodeC, nodeD],
				connections: createConnectionsBySource(
					workflowConnection(nodeA, nodeB),
					workflowConnection(nodeB, nodeC),
					workflowConnection(nodeC, nodeD),
				),
				groups: [group],
			});
			const addNodesToGroupSpy = vi.spyOn(workflowDocumentStore, 'addNodesToGroup');

			const { revertDeleteConnection } = useCanvasOperations();
			revertDeleteConnection(connection);

			expect(addNodesToGroupSpy).not.toHaveBeenCalled();
			expect(workflowDocumentStoreInstance.addConnection).toHaveBeenCalledWith({ connection });
			expect(toast.showToast).not.toHaveBeenCalledWith(expect.objectContaining({ type: 'error' }));
		});
	});

	describe('revalidateNodeInputConnections', () => {
		it('should not delete connections when target node does not exist', () => {
			const nonexistentId = 'nonexistent';
			vi.spyOn(workflowDocumentStoreInstance, 'getNodeById').mockReturnValue(undefined);

			const { revalidateNodeInputConnections } = useCanvasOperations();
			revalidateNodeInputConnections(nonexistentId);

			expect(workflowDocumentStoreInstance.removeConnection).not.toHaveBeenCalled();
		});

		it('should not delete connections when node type description is not found', () => {
			const nodeTypesStore = mockedStore(useNodeTypesStore);
			const nodeId = 'test-node';
			const node = createTestNode({ id: nodeId, type: 'unknown-type' });

			vi.spyOn(workflowDocumentStoreInstance, 'getNodeById').mockReturnValue(node);
			nodeTypesStore.getNodeType = () => null;

			const { revalidateNodeInputConnections } = useCanvasOperations();
			revalidateNodeInputConnections(nodeId);

			expect(workflowDocumentStoreInstance.removeConnection).not.toHaveBeenCalled();
		});

		it('should remove invalid connections that do not match input type', async () => {
			const nodeTypesStore = mockedStore(useNodeTypesStore);

			vi.mocked(workflowDocumentStoreInstance.removeConnection).mockClear();

			const targetNodeId = 'target';
			const targetNode = createTestNode({
				id: targetNodeId,
				name: 'Target Node',
				type: SET_NODE_TYPE,
			});
			const targetNodeType = mockNodeTypeDescription({
				name: SET_NODE_TYPE,
				inputs: [NodeConnectionTypes.Main],
			});

			const sourceNodeId = 'source';
			const sourceNode = createTestNode({
				id: sourceNodeId,
				name: 'Source Node',
				type: AGENT_NODE_TYPE,
			});
			const sourceNodeType = mockNodeTypeDescription({
				name: AGENT_NODE_TYPE,
				outputs: [NodeConnectionTypes.AiTool],
			});

			workflowDocumentStoreInstance.connectionsBySourceNode = {
				[sourceNode.name]: {
					[NodeConnectionTypes.AiTool]: [
						[{ node: targetNode.name, type: NodeConnectionTypes.Main, index: 0 }],
					],
				},
			};
			workflowDocumentStoreInstance.allNodes = [sourceNode, targetNode];

			vi.spyOn(workflowDocumentStoreInstance, 'getNodeById').mockImplementation((id) => {
				if (id === sourceNodeId) return sourceNode;
				if (id === targetNodeId) return targetNode;
				return undefined;
			});

			nodeTypesStore.getNodeType = vi
				.fn()
				.mockReturnValueOnce(targetNodeType)
				.mockReturnValueOnce(sourceNodeType);

			const { revalidateNodeInputConnections } = useCanvasOperations();
			revalidateNodeInputConnections(targetNodeId);

			await nextTick();

			expect(workflowDocumentStoreInstance.removeConnection).toHaveBeenCalledWith({
				connection: [
					{ node: sourceNode.name, type: NodeConnectionTypes.AiTool, index: 0 },
					{ node: targetNode.name, type: NodeConnectionTypes.Main, index: 0 },
				],
			});
		});

		it('should keep valid connections that match input type', () => {
			const nodeTypesStore = mockedStore(useNodeTypesStore);

			vi.mocked(workflowDocumentStoreInstance.removeConnection).mockClear();

			const targetNodeId = 'target';
			const targetNode = createTestNode({
				id: targetNodeId,
				name: 'Target Node',
				type: SET_NODE_TYPE,
			});
			const targetNodeType = mockNodeTypeDescription({
				name: SET_NODE_TYPE,
				inputs: [NodeConnectionTypes.Main],
			});

			const sourceNodeId = 'source';
			const sourceNode = createTestNode({
				id: sourceNodeId,
				name: 'Source Node',
				type: AGENT_NODE_TYPE,
			});
			const sourceNodeType = mockNodeTypeDescription({
				name: AGENT_NODE_TYPE,
				outputs: [NodeConnectionTypes.Main],
			});

			workflowDocumentStoreInstance.allNodes = [sourceNode, targetNode];
			workflowDocumentStoreInstance.connectionsBySourceNode = {
				[sourceNode.name]: {
					[NodeConnectionTypes.Main]: [
						[{ node: targetNode.name, type: NodeConnectionTypes.Main, index: 0 }],
					],
				},
			};

			vi.spyOn(workflowDocumentStoreInstance, 'getNodeById').mockImplementation((id) => {
				if (id === sourceNodeId) return sourceNode;
				if (id === targetNodeId) return targetNode;
				return undefined;
			});

			nodeTypesStore.getNodeType = vi
				.fn()
				.mockReturnValueOnce(targetNodeType)
				.mockReturnValueOnce(sourceNodeType);

			const { revalidateNodeInputConnections } = useCanvasOperations();
			revalidateNodeInputConnections(targetNodeId);

			expect(workflowDocumentStoreInstance.removeConnection).not.toHaveBeenCalled();
		});

		it('should remove connections if the input port index is no longer valid for the type', async () => {
			const nodeTypesStore = mockedStore(useNodeTypesStore);

			vi.mocked(workflowDocumentStoreInstance.removeConnection).mockClear();

			const targetNodeId = 'target';
			const targetNode = createTestNode({
				id: targetNodeId,
				name: 'Target Node',
				type: AGENT_NODE_TYPE,
			});
			const targetNodeType = mockNodeTypeDescription({
				name: AGENT_NODE_TYPE,
				inputs: [NodeConnectionTypes.Main, NodeConnectionTypes.AiLanguageModel],
			});

			const sourceNodeId = 'source';
			const sourceNode = createTestNode({
				id: sourceNodeId,
				name: 'Source Node',
				type: OPEN_AI_CHAT_MODEL_NODE_TYPE,
			});
			const sourceNodeType = mockNodeTypeDescription({
				name: OPEN_AI_CHAT_MODEL_NODE_TYPE,
				outputs: [NodeConnectionTypes.AiLanguageModel],
			});

			workflowDocumentStoreInstance.connectionsBySourceNode = {
				[sourceNode.name]: {
					[NodeConnectionTypes.AiLanguageModel]: [
						[{ node: targetNode.name, type: NodeConnectionTypes.AiLanguageModel, index: 1 }],
					],
				},
			};
			workflowDocumentStoreInstance.allNodes = [sourceNode, targetNode];

			vi.spyOn(workflowDocumentStoreInstance, 'getNodeById').mockImplementation((id) => {
				if (id === sourceNodeId) return sourceNode;
				if (id === targetNodeId) return targetNode;
				return undefined;
			});

			nodeTypesStore.getNodeType = vi
				.fn()
				.mockReturnValueOnce(targetNodeType)
				.mockReturnValueOnce(sourceNodeType);

			const { revalidateNodeInputConnections } = useCanvasOperations();
			revalidateNodeInputConnections(targetNodeId);

			await nextTick();

			expect(workflowDocumentStoreInstance.removeConnection).toHaveBeenCalledWith({
				connection: [
					{ node: sourceNode.name, type: NodeConnectionTypes.AiLanguageModel, index: 0 },
					{ node: targetNode.name, type: NodeConnectionTypes.AiLanguageModel, index: 1 },
				],
			});
		});

		it('should keep connections if the input port index is still valid for the type', async () => {
			const nodeTypesStore = mockedStore(useNodeTypesStore);

			vi.mocked(workflowDocumentStoreInstance.removeConnection).mockClear();

			const targetNodeId = 'target';
			const targetNode = createTestNode({
				id: targetNodeId,
				name: 'Target Node',
				type: AGENT_NODE_TYPE,
			});
			const targetNodeType = mockNodeTypeDescription({
				name: AGENT_NODE_TYPE,
				inputs: [
					NodeConnectionTypes.Main,
					NodeConnectionTypes.AiLanguageModel,
					NodeConnectionTypes.AiLanguageModel,
				],
			});

			const sourceNodeId = 'source';
			const sourceNode = createTestNode({
				id: sourceNodeId,
				name: 'Source Node',
				type: OPEN_AI_CHAT_MODEL_NODE_TYPE,
			});
			const sourceNodeType = mockNodeTypeDescription({
				name: OPEN_AI_CHAT_MODEL_NODE_TYPE,
				outputs: [NodeConnectionTypes.AiLanguageModel],
			});

			workflowDocumentStoreInstance.allNodes = [sourceNode, targetNode];
			workflowDocumentStoreInstance.connectionsBySourceNode = {
				[sourceNode.name]: {
					[NodeConnectionTypes.AiLanguageModel]: [
						[{ node: targetNode.name, type: NodeConnectionTypes.AiLanguageModel, index: 1 }],
					],
				},
			};

			vi.spyOn(workflowDocumentStoreInstance, 'getNodeById').mockImplementation((id) => {
				if (id === sourceNodeId) return sourceNode;
				if (id === targetNodeId) return targetNode;
				return undefined;
			});

			vi.spyOn(workflowDocumentStoreInstance, 'getNodeByName').mockImplementation(
				(name: string) => {
					if (name === sourceNode.name) return sourceNode;
					if (name === targetNode.name) return targetNode;
					return null;
				},
			);

			nodeTypesStore.getNodeType = vi.fn().mockImplementation((type) => {
				if (type === AGENT_NODE_TYPE) return targetNodeType;
				if (type === OPEN_AI_CHAT_MODEL_NODE_TYPE) return sourceNodeType;
				return undefined;
			});

			const { revalidateNodeInputConnections } = useCanvasOperations();
			revalidateNodeInputConnections(targetNodeId);

			await nextTick();

			expect(workflowDocumentStoreInstance.removeConnection).not.toHaveBeenCalled();
		});
	});

	describe('revalidateNodeOutputConnections', () => {
		it('should not delete connections when source node does not exist', () => {
			const nonexistentId = 'nonexistent';
			vi.spyOn(workflowDocumentStoreInstance, 'getNodeById').mockReturnValue(undefined);

			const { revalidateNodeOutputConnections } = useCanvasOperations();
			revalidateNodeOutputConnections(nonexistentId);

			expect(workflowDocumentStoreInstance.removeConnection).not.toHaveBeenCalled();
		});

		it('should not delete connections when node type description is not found', () => {
			const nodeTypesStore = mockedStore(useNodeTypesStore);
			const nodeId = 'test-node';
			const node = createTestNode({ id: nodeId, type: 'unknown-type' });

			vi.spyOn(workflowDocumentStoreInstance, 'getNodeById').mockReturnValue(node);
			nodeTypesStore.getNodeType = () => null;

			const { revalidateNodeOutputConnections } = useCanvasOperations();
			revalidateNodeOutputConnections(nodeId);

			expect(workflowDocumentStoreInstance.removeConnection).not.toHaveBeenCalled();
		});

		it('should remove invalid connections that do not match output type', async () => {
			const nodeTypesStore = mockedStore(useNodeTypesStore);

			vi.mocked(workflowDocumentStoreInstance.removeConnection).mockClear();

			const targetNodeId = 'target';
			const targetNode = createTestNode({
				id: targetNodeId,
				name: 'Target Node',
				type: SET_NODE_TYPE,
			});
			const targetNodeType = mockNodeTypeDescription({
				name: SET_NODE_TYPE,
				inputs: [NodeConnectionTypes.Main],
			});

			const sourceNodeId = 'source';
			const sourceNode = createTestNode({
				id: sourceNodeId,
				name: 'Source Node',
				type: AGENT_NODE_TYPE,
			});
			const sourceNodeType = mockNodeTypeDescription({
				name: AGENT_NODE_TYPE,
				outputs: [NodeConnectionTypes.AiTool],
			});

			workflowDocumentStoreInstance.connectionsBySourceNode = {
				[sourceNode.name]: {
					[NodeConnectionTypes.AiTool]: [
						[{ node: targetNode.name, type: NodeConnectionTypes.Main, index: 0 }],
					],
				},
			};
			workflowDocumentStoreInstance.allNodes = [sourceNode, targetNode];

			vi.spyOn(workflowDocumentStoreInstance, 'getNodeById').mockImplementation((id) => {
				if (id === sourceNodeId) return sourceNode;
				if (id === targetNodeId) return targetNode;
				return undefined;
			});

			nodeTypesStore.getNodeType = vi
				.fn()
				.mockReturnValueOnce(targetNodeType)
				.mockReturnValueOnce(sourceNodeType);

			const { revalidateNodeOutputConnections } = useCanvasOperations();
			revalidateNodeOutputConnections(sourceNodeId);

			await nextTick();

			expect(workflowDocumentStoreInstance.removeConnection).toHaveBeenCalledWith({
				connection: [
					{ node: sourceNode.name, type: NodeConnectionTypes.AiTool, index: 0 },
					{ node: targetNode.name, type: NodeConnectionTypes.Main, index: 0 },
				],
			});
		});

		it('should keep valid connections that match output type', () => {
			const nodeTypesStore = mockedStore(useNodeTypesStore);

			vi.mocked(workflowDocumentStoreInstance.removeConnection).mockClear();

			const targetNodeId = 'target';
			const targetNode = createTestNode({
				id: targetNodeId,
				name: 'Target Node',
				type: SET_NODE_TYPE,
			});
			const targetNodeType = mockNodeTypeDescription({
				name: SET_NODE_TYPE,
				inputs: [NodeConnectionTypes.Main],
			});

			const sourceNodeId = 'source';
			const sourceNode = createTestNode({
				id: sourceNodeId,
				name: 'Source Node',
				type: AGENT_NODE_TYPE,
			});
			const sourceNodeType = mockNodeTypeDescription({
				name: AGENT_NODE_TYPE,
				outputs: [NodeConnectionTypes.Main],
			});

			workflowDocumentStoreInstance.allNodes = [sourceNode, targetNode];
			workflowDocumentStoreInstance.connectionsBySourceNode = {
				[sourceNode.name]: {
					[NodeConnectionTypes.AiTool]: [
						[{ node: targetNode.name, type: NodeConnectionTypes.Main, index: 0 }],
					],
				},
			};

			vi.spyOn(workflowDocumentStoreInstance, 'getNodeById').mockImplementation((id) => {
				if (id === sourceNodeId) return sourceNode;
				if (id === targetNodeId) return targetNode;
				return undefined;
			});

			nodeTypesStore.getNodeType = vi
				.fn()
				.mockReturnValueOnce(targetNodeType)
				.mockReturnValueOnce(sourceNodeType);

			const { revalidateNodeOutputConnections } = useCanvasOperations();
			revalidateNodeOutputConnections(sourceNodeId);

			expect(workflowDocumentStoreInstance.removeConnection).not.toHaveBeenCalled();
		});
	});

	describe('deleteConnectionsByNodeId', () => {
		it('should delete all connections for a given node ID', () => {
			const { deleteConnectionsByNodeId } = useCanvasOperations();

			const node1 = createTestNode({ id: 'node1', name: 'Node 1' });
			const node2 = createTestNode({ id: 'node2', name: 'Node 1' });

			workflowDocumentStoreInstance.connectionsBySourceNode = {
				[node1.name]: {
					[NodeConnectionTypes.Main]: [
						[{ node: node2.name, type: NodeConnectionTypes.Main, index: 0 }],
					],
				},
				node2: {
					[NodeConnectionTypes.Main]: [
						[{ node: node1.name, type: NodeConnectionTypes.Main, index: 0 }],
					],
				},
			};

			vi.spyOn(workflowDocumentStoreInstance, 'getNodeById').mockReturnValue(node1);
			vi.spyOn(workflowDocumentStoreInstance, 'getNodeByName')
				.mockReturnValueOnce(node1)
				.mockReturnValueOnce(node2);

			deleteConnectionsByNodeId(node1.id);

			expect(workflowDocumentStoreInstance.removeConnection).toHaveBeenCalledWith({
				connection: [
					{ node: node1.name, type: NodeConnectionTypes.Main, index: 0 },
					{ node: node2.name, type: NodeConnectionTypes.Main, index: 0 },
				],
			});

			expect(workflowDocumentStoreInstance.removeConnection).toHaveBeenCalledWith({
				connection: [
					{ node: node2.name, type: NodeConnectionTypes.Main, index: 0 },
					{ node: node1.name, type: NodeConnectionTypes.Main, index: 0 },
				],
			});

			expect(workflowDocumentStoreInstance.removeAllNodeConnection).toHaveBeenCalledWith(node1);
		});

		it('should not delete connections if node ID does not exist', () => {
			const { deleteConnectionsByNodeId } = useCanvasOperations();

			const nodeId = 'nonexistent';
			vi.spyOn(workflowDocumentStoreInstance, 'getNodeById').mockReturnValue(undefined);

			deleteConnectionsByNodeId(nodeId);

			expect(workflowDocumentStoreInstance.removeConnection).not.toHaveBeenCalled();
		});

		it('should delete all connections of a node with multiple connections', () => {
			const { deleteConnectionsByNodeId } = useCanvasOperations();

			const sourceNode = createTestNode({ id: 'source', name: 'Source Node' });
			const targetNode = createTestNode({ id: 'target', name: 'Target Node' });

			const connections: IConnections = {
				[sourceNode.name]: {
					[NodeConnectionTypes.Main]: [
						[
							{ node: targetNode.name, type: NodeConnectionTypes.Main, index: 0 },
							{ node: targetNode.name, type: NodeConnectionTypes.Main, index: 1 },
						],
					],
				},
				[targetNode.name]: {
					[NodeConnectionTypes.Main]: [],
				},
			};
			workflowDocumentStoreInstance.connectionsBySourceNode = connections;

			vi.spyOn(workflowDocumentStoreInstance, 'getNodeById').mockImplementation((id) => {
				if (id === sourceNode.id) return sourceNode;
				if (id === targetNode.id) return targetNode;
				return undefined;
			});
			vi.spyOn(workflowDocumentStoreInstance, 'getNodeByName').mockImplementation((name) => {
				if (name === sourceNode.name) return sourceNode;
				if (name === targetNode.name) return targetNode;
				return null;
			});

			vi.mocked(workflowDocumentStoreInstance.removeConnection).mockImplementation(
				(data: { connection: IConnection[] }) => {
					const sourceData = data.connection[0];
					const destinationData = data.connection[1];

					const conns = connections[sourceData.node][sourceData.type][sourceData.index];

					for (const index in conns) {
						if (
							conns[+index].node === destinationData.node &&
							conns[+index].type === destinationData.type &&
							conns[+index].index === destinationData.index
						) {
							conns.splice(parseInt(index, 10), 1);
						}
					}
				},
			);

			deleteConnectionsByNodeId(targetNode.id);

			expect(workflowDocumentStoreInstance.removeConnection).toHaveBeenCalledTimes(2);

			expect(workflowDocumentStoreInstance.removeConnection).toHaveBeenCalledWith({
				connection: [
					{ node: sourceNode.name, type: NodeConnectionTypes.Main, index: 0 },
					{ node: targetNode.name, type: NodeConnectionTypes.Main, index: 0 },
				],
			});

			expect(workflowDocumentStoreInstance.removeConnection).toHaveBeenCalledWith({
				connection: [
					{ node: sourceNode.name, type: NodeConnectionTypes.Main, index: 0 },
					{ node: targetNode.name, type: NodeConnectionTypes.Main, index: 1 },
				],
			});

			expect(connections[sourceNode.name][NodeConnectionTypes.Main][0]).toEqual([]);
		});
	});

	describe('copyNodes', () => {
		it('should copy nodes', async () => {
			const nodeTypesStore = useNodeTypesStore();
			const nodeTypeDescription = mockNodeTypeDescription({ name: SET_NODE_TYPE });

			nodeTypesStore.nodeTypes = {
				[SET_NODE_TYPE]: { 1: nodeTypeDescription },
			};

			const nodes = buildImportNodes();
			workflowDocumentStoreInstance.allNodes = nodes;
			vi.spyOn(workflowDocumentStoreInstance, 'getNodesByIds').mockReturnValue(nodes);
			vi.mocked(workflowDocumentStoreInstance.outgoingConnectionsByNodeName).mockReturnValue({});

			const { copyNodes } = useCanvasOperations();
			await copyNodes(['1', '2']);

			expect(useClipboard().copy).toHaveBeenCalledTimes(1);
			expect(vi.mocked(useClipboard().copy).mock.calls).toMatchSnapshot();
		});

		it('should include nodeGroups when all group members are copied', async () => {
			const nodeTypesStore = useNodeTypesStore();
			const nodeTypeDescription = mockNodeTypeDescription({ name: SET_NODE_TYPE });

			nodeTypesStore.nodeTypes = {
				[SET_NODE_TYPE]: { 1: nodeTypeDescription },
			};

			const nodes = buildImportNodes();
			workflowDocumentStoreInstance.allNodes = nodes;
			vi.spyOn(workflowDocumentStoreInstance, 'allGroups', 'get').mockReturnValue([
				{ id: 'group-1', name: 'My Group', nodeIds: ['1', '2'] },
			]);
			vi.mocked(workflowDocumentStoreInstance.outgoingConnectionsByNodeName).mockReturnValue({});

			const { getNodesToSave } = useCanvasOperations();
			const copiedData = getNodesToSave(nodes);

			expect(copiedData.nodeGroups).toEqual([
				{ id: 'group-1', name: 'My Group', nodeIds: ['1', '2'] },
			]);
		});

		it('should omit nodeGroups when only part of a group is copied', async () => {
			const nodeTypesStore = useNodeTypesStore();
			const nodeTypeDescription = mockNodeTypeDescription({ name: SET_NODE_TYPE });

			nodeTypesStore.nodeTypes = {
				[SET_NODE_TYPE]: { 1: nodeTypeDescription },
			};

			const nodes = buildImportNodes();
			workflowDocumentStoreInstance.allNodes = nodes;
			vi.spyOn(workflowDocumentStoreInstance, 'allGroups', 'get').mockReturnValue([
				{ id: 'group-1', name: 'My Group', nodeIds: ['1', '2'] },
			]);
			vi.mocked(workflowDocumentStoreInstance.outgoingConnectionsByNodeName).mockReturnValue({});

			const { getNodesToSave } = useCanvasOperations();
			const copiedData = getNodesToSave([nodes[0]]);

			expect(copiedData.nodeGroups).toBeUndefined();
		});
	});

	describe('cutNodes', () => {
		it('should copy and delete nodes', async () => {
			const nodeTypesStore = useNodeTypesStore();
			const nodeTypeDescription = mockNodeTypeDescription({ name: SET_NODE_TYPE });

			nodeTypesStore.nodeTypes = {
				[SET_NODE_TYPE]: { 1: nodeTypeDescription },
			};

			const nodes = buildImportNodes();
			workflowDocumentStoreInstance.allNodes = nodes;
			vi.spyOn(workflowDocumentStoreInstance, 'getNodesByIds').mockReturnValue(nodes);
			vi.mocked(workflowDocumentStoreInstance.outgoingConnectionsByNodeName).mockReturnValue({});

			const { cutNodes } = useCanvasOperations();
			await cutNodes(['1', '2']);
			expect(useClipboard().copy).toHaveBeenCalledTimes(1);
			expect(vi.mocked(useClipboard().copy).mock.calls).toMatchSnapshot();
		});
	});

	describe('resolveNodeWebhook', () => {
		const nodeTypeDescription = mock<INodeTypeDescription>({
			webhooks: [mock<IWebhookDescription>()],
		});

		it("should set webhookId if it doesn't already exist", () => {
			const node = mock<INodeUi>({ webhookId: undefined });

			const { resolveNodeWebhook } = useCanvasOperations();
			resolveNodeWebhook(node, nodeTypeDescription);

			expect(node.webhookId).toBeDefined();
		});

		it('should not set webhookId if it already exists', () => {
			const node = mock<INodeUi>({ webhookId: 'random-id' });

			const { resolveNodeWebhook } = useCanvasOperations();
			resolveNodeWebhook(node, nodeTypeDescription);

			expect(node.webhookId).toBe('random-id');
		});

		it("should not set webhookId if node description doesn't define any webhooks", () => {
			const node = mock<INodeUi>({ webhookId: undefined });

			const { resolveNodeWebhook } = useCanvasOperations();
			resolveNodeWebhook(node, mock<INodeTypeDescription>({ webhooks: [] }));

			expect(node.webhookId).toBeUndefined();
		});

		test.each([WEBHOOK_NODE_TYPE, FORM_TRIGGER_NODE_TYPE])(
			'should update the webhook path, if the node type is %s, and the path parameter is empty',
			(nodeType) => {
				const node = mock<INodeUi>({
					webhookId: 'random-id',
					type: nodeType,
					parameters: { path: '' },
				});

				const { resolveNodeWebhook } = useCanvasOperations();
				resolveNodeWebhook(node, nodeTypeDescription);

				expect(node.webhookId).toBe('random-id');
				expect(node.parameters.path).toBe('random-id');
			},
		);
	});

	describe('initializeWorkspace', () => {
		it('should initialize the workspace', async () => {
			const workflow = createTestWorkflow({
				id: workflowId,
				nodes: [createTestNode()],
				connections: {},
			});

			const setNodesSpy = vi.spyOn(workflowDocumentStoreInstance, 'setNodes');
			const { initializeWorkspace } = useCanvasOperations();
			await initializeWorkspace(workflow);

			expect(setNodesSpy).toHaveBeenCalled();
			expect(workflowDocumentStoreInstance.setConnections).toHaveBeenCalled();
		});

		it('should set connections even when workflowId is initially empty', async () => {
			const workflowsStore = useWorkflowsStore();
			// Simulate the state after resetWorkspace() — workflowId is cleared
			workflowsStore.workflowId = '';

			const testConnections = {
				'Node 1': { main: [[{ node: 'Node 2', type: 'main' as const, index: 0 }]] },
			};
			const newWorkflowId = 'new-workflow-id';
			const workflow = createTestWorkflow({
				id: newWorkflowId,
				nodes: [createTestNode({ name: 'Node 1' }), createTestNode({ name: 'Node 2' })],
				connections: testConnections,
			});

			const { initializeWorkspace } = useCanvasOperations();
			await initializeWorkspace(workflow);

			// initState creates a new document store for the workflow ID.
			// Without the fix, the computed workflowDocumentStore would be undefined
			// (empty workflowId at start) and setConnections would be silently skipped.
			const workflowDocumentStore = useWorkflowDocumentStore(
				createWorkflowDocumentId(newWorkflowId),
			);
			expect(workflowDocumentStore.setConnections).toHaveBeenCalledWith(testConnections);
		});

		it('should default position to [0, 0] for nodes with missing position', async () => {
			const { position: _, ...nodeWithoutPosition } = createTestNode({ name: 'Start' });

			const workflow = createTestWorkflow({
				id: workflowId,
				nodes: [nodeWithoutPosition as INodeUi],
				connections: {},
			});

			const setNodesSpy = vi.spyOn(workflowDocumentStoreInstance, 'setNodes');
			const { initializeWorkspace } = useCanvasOperations();
			await initializeWorkspace(workflow);

			expect(setNodesSpy).toHaveBeenCalledWith([
				expect.objectContaining({ name: 'Start', position: [0, 0] }),
			]);
		});

		it('should remove connections for nodes filtered out during initialization', async () => {
			const validNode = createTestNode({ name: 'Start' });
			const invalidNode = createTestNode({ name: 'Missing', type: '' });
			const targetNode = createTestNode({ name: 'End' });
			const workflow = createTestWorkflow({
				id: workflowId,
				nodes: [validNode, invalidNode, targetNode],
				connections: {
					Start: {
						main: [
							[
								{ node: 'End', type: 'main', index: 0 },
								{ node: 'Missing', type: 'main', index: 0 },
							],
						],
					},
					Missing: {
						main: [[{ node: 'End', type: 'main', index: 0 }]],
					},
				},
			});

			const { initializeWorkspace } = useCanvasOperations();
			await initializeWorkspace(workflow);

			expect(workflowDocumentStoreInstance.setConnections).toHaveBeenCalledWith({
				Start: {
					main: [[{ node: 'End', type: 'main', index: 0 }]],
				},
			});
		});

		it('should initialize node data from node type description', async () => {
			const nodeTypesStore = mockedStore(useNodeTypesStore);
			const type = SET_NODE_TYPE;
			const version = 1;
			const expectedDescription = mockNodeTypeDescription({
				name: type,
				version,
				properties: [
					createTestNodeProperties({
						displayName: 'Value',
						name: 'value',
						type: 'boolean',
						default: true,
					}),
				],
			});

			nodeTypesStore.nodeTypes = { [type]: { [version]: expectedDescription } };

			const workflow = createTestWorkflow({
				id: workflowId,
				nodes: [createTestNode()],
				connections: {},
			});

			const setNodesSpy = vi.spyOn(workflowDocumentStoreInstance, 'setNodes');
			const { initializeWorkspace } = useCanvasOperations();
			await initializeWorkspace(workflow);

			expect(setNodesSpy).toHaveBeenCalledWith([
				expect.objectContaining({ parameters: { value: true } }),
			]);
		});
	});

	describe('initializeUnknownNodes', () => {
		it('should initialize nodes', () => {
			const updateNodeByIdSpy = vi.spyOn(workflowDocumentStoreInstance, 'updateNodeById');
			const nodes = [
				createTestNode({ type: 'n8n-nodes-community.testNode1', name: 'testNode1' }),
				createTestNode({ type: 'n8n-nodes-community.testNode2', name: 'testNode2' }),
			];
			const workflow = createTestWorkflow({
				nodes,
				connections: {},
			});
			workflowDocumentStoreInstance.allNodes = nodes;
			vi.spyOn(workflowDocumentStoreInstance, 'getNodeByName').mockImplementation(
				(name: string) => nodes.find((n) => n.name === name) ?? null,
			);
			const { initializeUnknownNodes } = useCanvasOperations();
			initializeUnknownNodes(workflow.nodes);

			expect(updateNodeByIdSpy).toHaveBeenCalledTimes(2);
			expect(updateNodeByIdSpy).toHaveBeenNthCalledWith(1, nodes[0].id, workflow.nodes[0]);
			expect(updateNodeByIdSpy).toHaveBeenNthCalledWith(2, nodes[1].id, workflow.nodes[1]);
		});

		it('should remove preview token from node type when initializing', () => {
			const updateNodeByIdSpy = vi.spyOn(workflowDocumentStoreInstance, 'updateNodeById');
			const nodeWithPreview = createTestNode({
				type: 'n8n-nodes-community.testNode-preview',
				name: 'testNode',
			});
			const workflow = createTestWorkflow({
				nodes: [nodeWithPreview],
				connections: {},
			});
			workflowDocumentStoreInstance.allNodes = [nodeWithPreview];
			vi.spyOn(workflowDocumentStoreInstance, 'getNodeByName').mockReturnValue(nodeWithPreview);
			const { initializeUnknownNodes } = useCanvasOperations();
			initializeUnknownNodes(workflow.nodes);

			expect(updateNodeByIdSpy).toHaveBeenCalledTimes(1);
			const updatedNode = updateNodeByIdSpy.mock.calls[0][1] as Record<string, unknown>;
			expect(updatedNode.type).toBe('n8n-nodes-community.testNode');
			expect(updatedNode.type).not.toContain('-preview');
		});
	});

	describe('resolveNodeData', () => {
		it('should resolve node parameters and webhooks for installed nodes', () => {
			const nodeTypesStore = mockedStore(useNodeTypesStore);
			nodeTypesStore.getIsNodeInstalled = vi.fn().mockReturnValue(true);

			const nodeTypeDescription = mockNodeTypeDescription({
				name: 'n8n-nodes-base.httpRequest',
				webhooks: [
					{
						name: 'default',
						httpMethod: 'GET',
						path: 'test',
						responseMode: 'onReceived',
					},
				],
			});

			const { addNode } = useCanvasOperations();
			const node = addNode(
				{
					type: 'n8n-nodes-base.httpRequest',
					typeVersion: 1,
					position: [100, 100],
				},
				nodeTypeDescription,
			);

			expect(nodeTypesStore.getIsNodeInstalled).toHaveBeenCalledWith('n8n-nodes-base.httpRequest');
			expect(node).toBeDefined();
		});

		it('should skip resolving parameters and webhooks for non-installed nodes', () => {
			const nodeTypesStore = mockedStore(useNodeTypesStore);
			nodeTypesStore.getIsNodeInstalled = vi.fn().mockReturnValue(false);

			const nodeTypeDescription = mockNodeTypeDescription({
				name: 'n8n-nodes-community.notInstalled',
			});

			const { addNode } = useCanvasOperations();
			const node = addNode(
				{
					type: 'n8n-nodes-community.notInstalled',
					typeVersion: 1,
					position: [100, 100],
				},
				nodeTypeDescription,
			);

			expect(nodeTypesStore.getIsNodeInstalled).toHaveBeenCalledWith(
				'n8n-nodes-community.notInstalled',
			);
			expect(node).toBeDefined();
		});
	});

	describe('resetWorkspace', () => {
		it('should reset the workspace', () => {
			const nodeCreatorStore = mockedStore(useNodeCreatorStore);
			const workflowsStore = mockedStore(useWorkflowsStore);
			const uiStore = mockedStore(useUIStore);
			const executionsStore = mockedStore(useExecutionsStore);
			const builderStore = mockedStore(useBuilderStore);

			const credentialsUpdatedRef = ref(true);
			const credentialsSpy = vi.spyOn(credentialsUpdatedRef, 'value', 'set');
			const nodeHelpersOriginal = nodeHelpers.useNodeHelpers();
			vi.spyOn(nodeHelpers, 'useNodeHelpers').mockImplementation(() => {
				return {
					...nodeHelpersOriginal,
					credentialsUpdated: credentialsUpdatedRef,
				};
			});

			nodeCreatorStore.setNodeCreatorState = vi.fn();
			workflowsStore.removeTestWebhook = vi.fn();
			workflowsStore.resetWorkflow = vi.fn();
			workflowsStore.resetState = vi.fn();
			const resetWorkflowSpy = workflowsStore.resetWorkflow as ReturnType<typeof vi.fn>;
			uiStore.resetLastInteractedWith = vi.fn();
			executionsStore.activeExecution = null;

			workflowsStore.workflowId = 'workflow-id';
			const executionStateStore = useWorkflowExecutionStateStore(
				createWorkflowDocumentId('workflow-id'),
			);
			// Spy on the getter — readonly wrapping prevents direct assignment, and
			// createTestingPinia stubs setExecutionWaitingForWebhook so the action is a no-op.
			vi.spyOn(executionStateStore, 'executionWaitingForWebhook', 'get').mockReturnValue(true);

			const { resetWorkspace } = useCanvasOperations();

			resetWorkspace();

			expect(nodeCreatorStore.setNodeCreatorState).toHaveBeenCalledWith({
				workflowId: 'workflow-id',
				createNodeActive: false,
			});
			expect(workflowsStore.removeTestWebhook).toHaveBeenCalledWith('workflow-id');
			expect(executionStateStore.resetExecutionState).toHaveBeenCalled();
			expect(builderStore.resetManualExecutionStats).toHaveBeenCalled();
			expect(resetWorkflowSpy).toHaveBeenCalled();
			// The execution-state reset must run BEFORE resetWorkflow() — it targets the
			// per-workflow execution-state store keyed on workflowId, and resetWorkflow()
			// empties that id.
			const resetExecutionStateSpy = executionStateStore.resetExecutionState as ReturnType<
				typeof vi.fn
			>;
			expect(resetExecutionStateSpy.mock.invocationCallOrder[0]).toBeLessThan(
				resetWorkflowSpy.mock.invocationCallOrder[0],
			);
			expect(uiStore.resetLastInteractedWith).toHaveBeenCalled();
			expect(uiStore.markStateClean).toHaveBeenCalled();
			expect(executionsStore.activeExecution).toBeNull();
			expect(credentialsSpy).toHaveBeenCalledWith(false);
			expect(credentialsUpdatedRef.value).toBe(false);
		});

		it('tears down the explicitly passed outgoing workflow, not the current id', () => {
			const nodeCreatorStore = mockedStore(useNodeCreatorStore);
			const workflowsStore = mockedStore(useWorkflowsStore);

			nodeCreatorStore.setNodeCreatorState = vi.fn();
			workflowsStore.removeTestWebhook = vi.fn();
			workflowsStore.resetWorkflow = vi.fn();
			workflowsStore.clearCurrentWorkflowExecutions = vi.fn();
			workflowsStore.setLastSuccessfulExecution = vi.fn();
			const resetWorkflowSpy = workflowsStore.resetWorkflow as ReturnType<typeof vi.fn>;

			// The current id is already the INCOMING workflow (dispose-first flow); the
			// OUTGOING id is passed explicitly and must be the one torn down.
			workflowsStore.workflowId = 'incoming-id';
			const outgoingExecutionStateStore = useWorkflowExecutionStateStore(
				createWorkflowDocumentId('outgoing-id'),
			);
			vi.spyOn(outgoingExecutionStateStore, 'executionWaitingForWebhook', 'get').mockReturnValue(
				true,
			);

			const { resetWorkspace } = useCanvasOperations();

			resetWorkspace('outgoing-id');

			expect(nodeCreatorStore.setNodeCreatorState).toHaveBeenCalledWith({
				workflowId: 'outgoing-id',
				createNodeActive: false,
			});
			expect(workflowsStore.removeTestWebhook).toHaveBeenCalledWith('outgoing-id');
			expect(outgoingExecutionStateStore.resetExecutionState).toHaveBeenCalled();
			expect(resetWorkflowSpy).toHaveBeenCalledWith('outgoing-id');
		});

		it('should not call removeTestWebhook if executionWaitingForWebhook is false', () => {
			const nodeCreatorStore = mockedStore(useNodeCreatorStore);
			const workflowsStore = mockedStore(useWorkflowsStore);

			nodeCreatorStore.setNodeCreatorState = vi.fn();
			workflowsStore.removeTestWebhook = vi.fn();
			workflowsStore.workflowId = 'workflow-id';
			// Default state-store value is false; no override needed.

			const { resetWorkspace } = useCanvasOperations();

			resetWorkspace();

			expect(workflowsStore.removeTestWebhook).not.toHaveBeenCalled();
		});
	});

	describe('filterConnectionsByNodes', () => {
		it('should return filtered connections when all nodes are included', () => {
			const connections: INodeConnections = {
				[NodeConnectionTypes.Main]: [
					[
						{ node: 'node1', type: NodeConnectionTypes.Main, index: 0 },
						{ node: 'node2', type: NodeConnectionTypes.Main, index: 0 },
					],
					[{ node: 'node3', type: NodeConnectionTypes.Main, index: 0 }],
				],
			};
			const includeNodeNames = new Set<string>(['node1', 'node2', 'node3']);

			const { filterConnectionsByNodes } = useCanvasOperations();
			const result = filterConnectionsByNodes(connections, includeNodeNames);

			expect(result).toEqual(connections);
		});

		it('should return empty connections when no nodes are included', () => {
			const connections: INodeConnections = {
				[NodeConnectionTypes.Main]: [
					[
						{ node: 'node1', type: NodeConnectionTypes.Main, index: 0 },
						{ node: 'node2', type: NodeConnectionTypes.Main, index: 0 },
					],
					[{ node: 'node3', type: NodeConnectionTypes.Main, index: 0 }],
				],
			};
			const includeNodeNames = new Set<string>();

			const { filterConnectionsByNodes } = useCanvasOperations();
			const result = filterConnectionsByNodes(connections, includeNodeNames);

			expect(result).toEqual({
				[NodeConnectionTypes.Main]: [[], []],
			});
		});

		it('should return partially filtered connections when some nodes are included', () => {
			const connections: INodeConnections = {
				[NodeConnectionTypes.Main]: [
					[
						{ node: 'node1', type: NodeConnectionTypes.Main, index: 0 },
						{ node: 'node2', type: NodeConnectionTypes.Main, index: 0 },
					],
					[{ node: 'node3', type: NodeConnectionTypes.Main, index: 0 }],
				],
			};
			const includeNodeNames = new Set<string>(['node1']);

			const { filterConnectionsByNodes } = useCanvasOperations();
			const result = filterConnectionsByNodes(connections, includeNodeNames);

			expect(result).toEqual({
				[NodeConnectionTypes.Main]: [
					[{ node: 'node1', type: NodeConnectionTypes.Main, index: 0 }],
					[],
				],
			});
		});

		it('should handle empty connections input', () => {
			const connections: INodeConnections = {};
			const includeNodeNames = new Set<string>(['node1']);

			const { filterConnectionsByNodes } = useCanvasOperations();
			const result = filterConnectionsByNodes(connections, includeNodeNames);

			expect(result).toEqual({});
		});

		it('should handle connections with no valid nodes', () => {
			const connections: INodeConnections = {
				[NodeConnectionTypes.Main]: [
					[
						{ node: 'node4', type: NodeConnectionTypes.Main, index: 0 },
						{ node: 'node5', type: NodeConnectionTypes.Main, index: 0 },
					],
					[{ node: 'node6', type: NodeConnectionTypes.Main, index: 0 }],
				],
			};
			const includeNodeNames = new Set<string>(['node1', 'node2', 'node3']);

			const { filterConnectionsByNodes } = useCanvasOperations();
			const result = filterConnectionsByNodes(connections, includeNodeNames);

			expect(result).toEqual({
				[NodeConnectionTypes.Main]: [[], []],
			});
		});
	});

	describe('openExecution', () => {
		it('should initialize workspace and set execution data when execution is found', async () => {
			const workflowsStore = mockedStore(useWorkflowsStore);
			const uiStore = mockedStore(useUIStore);
			// Production stages the execution on the store keyed by the document
			// `initializeWorkspace` opens — i.e. the execution's `workflowData.id`.
			const setWorkflowExecutionData = vi.spyOn(
				useWorkflowExecutionStateStore(createWorkflowDocumentId(workflowId)),
				'setWorkflowExecutionData',
			);

			const { openExecution } = useCanvasOperations();

			const executionId = '123';
			const executionData: IExecutionResponse = {
				id: executionId,
				finished: true,
				status: 'success',
				startedAt: new Date(),
				createdAt: new Date(),
				workflowData: createTestWorkflow({ id: workflowId }),
				mode: 'manual' as WorkflowExecuteMode,
			};

			workflowsStore.getExecution.mockResolvedValue(executionData);

			const result = await openExecution(executionId);

			expect(setWorkflowExecutionData).toHaveBeenCalledWith(executionData);
			expect(uiStore.markStateClean).toHaveBeenCalled();
			expect(result).toEqual(executionData);
		});

		it('should throw error when execution data is undefined', async () => {
			const workflowsStore = mockedStore(useWorkflowsStore);
			const executionId = '123';
			const { openExecution } = useCanvasOperations();

			workflowsStore.getExecution.mockResolvedValue(undefined);

			await expect(openExecution(executionId)).rejects.toThrow(
				`Execution with id "${executionId}" could not be found!`,
			);
		});

		it('should clear workflow pin data if execution mode is not manual', async () => {
			const workflowsStore = mockedStore(useWorkflowsStore);
			const setPinDataSpy = vi.spyOn(workflowDocumentStoreInstance, 'setPinData');
			const { openExecution } = useCanvasOperations();

			const executionId = '123';
			const executionData: IExecutionResponse = {
				id: executionId,
				finished: true,
				status: 'success',
				startedAt: new Date(),
				createdAt: new Date(),
				workflowData: createTestWorkflow({ id: workflowId }),
				mode: 'trigger' as WorkflowExecuteMode,
			};

			workflowsStore.getExecution.mockResolvedValue(executionData);

			await openExecution(executionId);

			expect(setPinDataSpy).toHaveBeenCalledWith({});
		});
		it('should show an error notification for failed executions', async () => {
			const workflowsStore = mockedStore(useWorkflowsStore);
			const { openExecution } = useCanvasOperations();
			const toast = useToast();

			const executionId = '123';
			const executionData: IExecutionResponse = {
				id: executionId,
				finished: true,
				status: 'error',
				startedAt: new Date(),
				createdAt: new Date(),
				workflowData: createTestWorkflow({ id: workflowId }),
				mode: 'manual',
				data: {
					resultData: {
						error: { message: 'Crashed', node: { name: 'Step1' } },
						lastNodeExecuted: 'Last Node',
					},
				} as IExecutionResponse['data'],
			};

			workflowsStore.getExecution.mockResolvedValue(executionData);

			await openExecution(executionId);

			expect(toast.showMessage).toHaveBeenCalledWith({
				duration: 0,
				message: 'Crashed',
				title: 'Problem in node ‘Last Node‘',
				type: 'error',
			});
		});
		it('should set active node when nodeId is provided and node exists', async () => {
			const workflowsStore = mockedStore(useWorkflowsStore);
			const ndvStore = mockedStore(useNDVStore, createWorkflowDocumentId(workflowId));
			const { openExecution } = useCanvasOperations();

			const executionId = '123';
			const nodeId = 'node-123';
			const mockNode = { id: nodeId, name: 'Test Node', type: 'test' } as INodeUi;
			const executionData: IExecutionResponse = {
				id: executionId,
				finished: true,
				status: 'success',
				startedAt: new Date(),
				createdAt: new Date(),
				workflowData: createTestWorkflow({ id: workflowId }),
				mode: 'manual' as WorkflowExecuteMode,
			};

			workflowsStore.getExecution.mockResolvedValue(executionData);
			vi.spyOn(workflowDocumentStoreInstance, 'getNodeById').mockReturnValue(mockNode);

			await openExecution(executionId, nodeId);

			expect(workflowDocumentStoreInstance.getNodeById).toHaveBeenCalledWith(nodeId);
			expect(ndvStore.setActiveNodeName).toHaveBeenCalledWith(mockNode.name, expect.any(String));
		});

		it('should show error when nodeId is provided but node does not exist', async () => {
			const workflowsStore = mockedStore(useWorkflowsStore);
			const { openExecution } = useCanvasOperations();
			const toast = useToast();

			const executionId = '123';
			const nodeId = 'non-existent-node';
			const executionData: IExecutionResponse = {
				id: executionId,
				finished: true,
				status: 'success',
				startedAt: new Date(),
				createdAt: new Date(),
				workflowData: createTestWorkflow({ id: workflowId }),
				mode: 'manual' as WorkflowExecuteMode,
			};

			workflowsStore.getExecution.mockResolvedValue(executionData);
			vi.spyOn(workflowDocumentStoreInstance, 'getNodeById').mockReturnValue(undefined);

			await openExecution(executionId, nodeId);

			expect(workflowDocumentStoreInstance.getNodeById).toHaveBeenCalledWith(nodeId);
			expect(toast.showError).toHaveBeenCalledWith(
				new Error(`Node with id "${nodeId}" could not be found!`),
				'Problem opening node in execution',
			);
		});
	});

	describe('connectAdjacentNodes', () => {
		it('should connect nodes that were connected through the removed node', () => {
			const nodeTypesStore = mockedStore(useNodeTypesStore);
			const historyStore = mockedStore(useHistoryStore);

			// Create three nodes in a sequence: A -> B -> C
			const nodeA = createTestNode({ id: 'A', name: 'Node A', position: [0, 0] });
			const nodeB = createTestNode({ id: 'B', name: 'Node B', position: [96, 0] });
			const nodeC = createTestNode({ id: 'C', name: 'Node C', position: [208, 0] });

			const nodeTypeDescription = mockNodeTypeDescription({
				name: nodeA.type,
				inputs: [NodeConnectionTypes.Main],
				outputs: [NodeConnectionTypes.Main],
			});

			nodeTypesStore.getNodeType = vi.fn(() => nodeTypeDescription);

			// Set up the workflow connections A -> B -> C
			workflowDocumentStoreInstance.allNodes = [nodeA, nodeB, nodeC];
			workflowDocumentStoreInstance.connectionsBySourceNode = {
				[nodeA.name]: {
					main: [[{ node: nodeB.name, type: NodeConnectionTypes.Main, index: 0 }]],
				},
				[nodeB.name]: {
					main: [[{ node: nodeC.name, type: NodeConnectionTypes.Main, index: 0 }]],
				},
			};

			// Mock store methods
			vi.spyOn(workflowDocumentStoreInstance, 'getNodeById').mockImplementation(
				(id: string) =>
					({
						[nodeA.id]: nodeA,
						[nodeB.id]: nodeB,
						[nodeC.id]: nodeC,
					})[id],
			);
			vi.spyOn(workflowDocumentStoreInstance, 'getNodeByName').mockImplementation(
				(name: string) =>
					({
						[nodeA.name]: nodeA,
						[nodeB.name]: nodeB,
						[nodeC.name]: nodeC,
					})[name],
			);
			vi.mocked(workflowDocumentStoreInstance.outgoingConnectionsByNodeName).mockReturnValue({
				main: [[{ node: nodeC.name, type: NodeConnectionTypes.Main, index: 0 }]],
			});
			vi.mocked(workflowDocumentStoreInstance.incomingConnectionsByNodeName).mockReturnValue({
				main: [[{ node: nodeA.name, type: NodeConnectionTypes.Main, index: 0 }]],
			});

			const { connectAdjacentNodes } = useCanvasOperations();
			connectAdjacentNodes(nodeB.id, { trackHistory: true });

			// Check that A was connected directly to C
			expect(workflowDocumentStoreInstance.addConnection).toHaveBeenCalledWith({
				connection: [
					{ node: nodeA.name, type: NodeConnectionTypes.Main, index: 0 },
					{ node: nodeC.name, type: NodeConnectionTypes.Main, index: 0 },
				],
			});

			// Verify the connection was tracked in history
			expect(historyStore.pushCommandToUndo).toHaveBeenCalled();
		});

		it('should connect nodes that were connected through the removed node at different indices', () => {
			const nodeTypesStore = mockedStore(useNodeTypesStore);
			const historyStore = mockedStore(useHistoryStore);

			// Create three nodes in a sequence: A -> B -> C
			const nodeA = createTestNode({ id: 'A', name: 'Node A', position: [0, 0] });
			const nodeB = createTestNode({ id: 'B', name: 'Node B', position: [96, 0] });
			const nodeC = createTestNode({ id: 'C', name: 'Node C', position: [208, 0] });

			const nodeTypeDescription = mockNodeTypeDescription({
				name: nodeA.type,
				inputs: [NodeConnectionTypes.Main, NodeConnectionTypes.Main],
				outputs: [NodeConnectionTypes.Main, NodeConnectionTypes.Main],
			});

			nodeTypesStore.getNodeType = vi.fn(() => nodeTypeDescription);

			// Set up the workflow connections A -> B -> C
			workflowDocumentStoreInstance.allNodes = [nodeA, nodeB, nodeC];
			workflowDocumentStoreInstance.connectionsBySourceNode = {
				[nodeA.name]: {
					main: [[{ node: nodeB.name, type: NodeConnectionTypes.Main, index: 1 }]],
				},
				[nodeB.name]: {
					main: [[{ node: nodeC.name, type: NodeConnectionTypes.Main, index: 0 }]],
				},
			};

			// Mock store methods
			vi.spyOn(workflowDocumentStoreInstance, 'getNodeById').mockImplementation(
				(id: string) =>
					({
						[nodeA.id]: nodeA,
						[nodeB.id]: nodeB,
						[nodeC.id]: nodeC,
					})[id],
			);
			vi.spyOn(workflowDocumentStoreInstance, 'getNodeByName').mockImplementation(
				(name: string) =>
					({
						[nodeA.name]: nodeA,
						[nodeB.name]: nodeB,
						[nodeC.name]: nodeC,
					})[name],
			);
			vi.mocked(workflowDocumentStoreInstance.outgoingConnectionsByNodeName).mockReturnValue({
				main: [[{ node: nodeC.name, type: NodeConnectionTypes.Main, index: 1 }]],
			});
			vi.mocked(workflowDocumentStoreInstance.incomingConnectionsByNodeName).mockReturnValue({
				main: [[{ node: nodeA.name, type: NodeConnectionTypes.Main, index: 0 }]],
			});

			const { connectAdjacentNodes } = useCanvasOperations();
			connectAdjacentNodes(nodeB.id, { trackHistory: true });

			// Check that A was connected directly to C
			expect(workflowDocumentStoreInstance.addConnection).toHaveBeenCalledWith({
				connection: [
					{ node: nodeA.name, type: NodeConnectionTypes.Main, index: 0 },
					{ node: nodeC.name, type: NodeConnectionTypes.Main, index: 1 },
				],
			});

			// Verify the connection was tracked in history
			expect(historyStore.pushCommandToUndo).toHaveBeenCalled();
		});

		it('should not create connections if middle node has no incoming connections', () => {
			// Create nodes: B -> C (no incoming to B)
			const nodeB = createTestNode({ id: 'B', name: 'Node B', position: [96, 0] });
			const nodeC = createTestNode({ id: 'C', name: 'Node C', position: [208, 0] });

			workflowDocumentStoreInstance.allNodes = [nodeB, nodeC];
			workflowDocumentStoreInstance.connectionsBySourceNode = {
				[nodeB.name]: {
					main: [[{ node: nodeC.name, type: NodeConnectionTypes.Main, index: 0 }]],
				},
			};

			vi.spyOn(workflowDocumentStoreInstance, 'getNodeById').mockReturnValue(nodeB);
			vi.mocked(workflowDocumentStoreInstance.outgoingConnectionsByNodeName).mockReturnValue({
				main: [[{ node: nodeC.name, type: NodeConnectionTypes.Main, index: 0 }]],
			});
			vi.mocked(workflowDocumentStoreInstance.incomingConnectionsByNodeName).mockReturnValue({});

			const { connectAdjacentNodes } = useCanvasOperations();
			connectAdjacentNodes(nodeB.id);

			expect(workflowDocumentStoreInstance.addConnection).not.toHaveBeenCalled();
		});

		it('should not create connections if middle node has no outgoing connections', () => {
			// Create nodes: A -> B (no outgoing from B)
			const nodeA = createTestNode({ id: 'A', name: 'Node A', position: [0, 0] });
			const nodeB = createTestNode({ id: 'B', name: 'Node B', position: [96, 0] });

			workflowDocumentStoreInstance.allNodes = [nodeA, nodeB];
			workflowDocumentStoreInstance.connectionsBySourceNode = {
				[nodeA.name]: {
					main: [[{ node: nodeB.name, type: NodeConnectionTypes.Main, index: 0 }]],
				},
			};

			vi.spyOn(workflowDocumentStoreInstance, 'getNodeById').mockReturnValue(nodeB);
			vi.mocked(workflowDocumentStoreInstance.outgoingConnectionsByNodeName).mockReturnValue({});
			vi.mocked(workflowDocumentStoreInstance.incomingConnectionsByNodeName).mockReturnValue({
				main: [[{ node: nodeA.name, type: NodeConnectionTypes.Main, index: 0 }]],
			});

			const { connectAdjacentNodes } = useCanvasOperations();
			connectAdjacentNodes(nodeB.id);

			expect(workflowDocumentStoreInstance.addConnection).not.toHaveBeenCalled();
		});
	});

	describe('importWorkflowData', () => {
		const workflowData = {
			nodes: [], //buildImportNodes(),
			connections: {},
		};

		it('should show an error and import nothing when data is not a valid workflow', async () => {
			const toast = useToast();

			const canvasOperations = useCanvasOperations();
			const result = await canvasOperations.importWorkflowData({ name: 'Not a workflow' }, 'file');

			expect(result).toEqual({});
			expect(toast.showError).toHaveBeenCalledWith(
				new Error(
					"The imported data does not contain valid workflow data ('nodes' and 'connections' are missing)",
				),
				'Problem importing workflow',
			);
		});

		it('should not show the invalid workflow error when data is a valid workflow', async () => {
			const toast = useToast();

			const canvasOperations = useCanvasOperations();
			await canvasOperations.importWorkflowData(workflowData, 'file');

			expect(toast.showError).not.toHaveBeenCalledWith(
				new Error(
					"The imported data does not contain valid workflow data ('nodes' and 'connections' are missing)",
				),
				'Problem importing workflow',
			);
		});

		it('should track telemetry when trackEvents is true (default)', async () => {
			const telemetry = useTelemetry();

			const canvasOperations = useCanvasOperations();
			await canvasOperations.importWorkflowData(workflowData, 'paste');

			expect(telemetry.track).toHaveBeenCalledWith('User pasted nodes', {
				workflow_id: expect.any(String),
				node_graph_string: expect.any(String),
			});
		});

		it('should track telemetry when trackEvents is explicitly true', async () => {
			const telemetry = useTelemetry();

			const canvasOperations = useCanvasOperations();
			await canvasOperations.importWorkflowData(workflowData, 'duplicate', {
				trackEvents: true,
			});

			expect(telemetry.track).toHaveBeenCalledWith('User duplicated nodes', {
				workflow_id: expect.any(String),
				node_graph_string: expect.any(String),
			});
		});

		it('should not track telemetry when trackEvents is false', async () => {
			const telemetry = useTelemetry();

			const canvasOperations = useCanvasOperations();
			await canvasOperations.importWorkflowData(workflowData, 'paste', {
				trackEvents: false,
			});

			expect(telemetry.track).not.toHaveBeenCalledWith('User pasted nodes', expect.any(Object));
		});

		it('should track different telemetry events for different sources with trackEvents true', async () => {
			const telemetry = useTelemetry();
			const canvasOperations = useCanvasOperations();

			// Test 'paste' source
			await canvasOperations.importWorkflowData(workflowData, 'paste', { trackEvents: true });
			expect(telemetry.track).toHaveBeenCalledWith('User pasted nodes', {
				workflow_id: expect.any(String),
				node_graph_string: expect.any(String),
			});
		});

		it('should track duplicate telemetry when source is duplicate with trackEvents true', async () => {
			const telemetry = useTelemetry();
			const canvasOperations = useCanvasOperations();

			// Test 'duplicate' source
			await canvasOperations.importWorkflowData(workflowData, 'duplicate', { trackEvents: true });
			expect(telemetry.track).toHaveBeenCalledWith('User duplicated nodes', {
				workflow_id: expect.any(String),
				node_graph_string: expect.any(String),
			});
		});

		it('should track import telemetry when source is file with trackEvents true', async () => {
			const telemetry = useTelemetry();
			const canvasOperations = useCanvasOperations();

			// Test other source
			await canvasOperations.importWorkflowData(workflowData, 'file', { trackEvents: true });
			expect(telemetry.track).toHaveBeenCalledWith('User imported workflow', {
				source: 'file',
				workflow_id: expect.any(String),
				node_graph_string: expect.any(String),
			});
		});

		it('should not track any telemetry events when trackEvents is false regardless of source', async () => {
			const telemetry = useTelemetry();
			const canvasOperations = useCanvasOperations();

			// Test 'paste' source with trackEvents: false
			await canvasOperations.importWorkflowData(workflowData, 'paste', {
				trackEvents: false,
			});
			expect(telemetry.track).not.toHaveBeenCalledWith('User pasted nodes', expect.any(Object));

			// Test 'duplicate' source with trackEvents: false
			await canvasOperations.importWorkflowData(workflowData, 'duplicate', {
				trackEvents: false,
			});
			expect(telemetry.track).not.toHaveBeenCalledWith('User duplicated nodes', expect.any(Object));

			// Test other source with trackEvents: false
			await canvasOperations.importWorkflowData(workflowData, 'file', {
				trackEvents: false,
			});
			expect(telemetry.track).not.toHaveBeenCalledWith(
				'User imported workflow',
				expect.any(Object),
			);

			// Ensure no telemetry was called at all
			expect(telemetry.track).not.toHaveBeenCalled();
		});

		it('should set workflow name when importing with name', async () => {
			// This mock is needed for addImportedNodesToWorkflow to work
			vi.mocked(workflowDocumentStoreInstance.createWorkflowObject).mockReturnValue({
				nodes: {},
				connections: {},
				connectionsBySourceNode: {},
				renameNode: vi.fn(),
			} as unknown as Workflow);

			const setNameSpy = vi.spyOn(workflowDocumentStoreInstance, 'setName');

			const canvasOperations = useCanvasOperations();
			const workflowDataWithName = {
				name: 'Test Workflow Name',
				nodes: [],
				connections: {},
			};

			await canvasOperations.importWorkflowData(workflowDataWithName, 'file');

			expect(setNameSpy).toHaveBeenCalledWith('Test Workflow Name');
		});

		it('should not crash when importing nodes that exceed maxNodes limit', async () => {
			const toast = useToast();
			const nodeTypesStore = useNodeTypesStore();

			// Create a node type with maxNodes: 1
			const executeWorkflowTriggerNodeType = mockNodeTypeDescription({
				name: EXECUTE_WORKFLOW_TRIGGER_NODE_TYPE,
				maxNodes: 1,
			});

			nodeTypesStore.nodeTypes = {
				[EXECUTE_WORKFLOW_TRIGGER_NODE_TYPE]: { 1: executeWorkflowTriggerNodeType },
			};

			// Create workflow data with two nodes of the same type (that has maxNodes: 1)
			const nodesToImport = [
				{
					id: 'import-1',
					name: 'Execute Workflow Trigger 1',
					type: EXECUTE_WORKFLOW_TRIGGER_NODE_TYPE,
					typeVersion: 1,
					position: [200, 200] as [number, number],
					parameters: {},
				},
				{
					id: 'import-2',
					name: 'Execute Workflow Trigger 2',
					type: EXECUTE_WORKFLOW_TRIGGER_NODE_TYPE,
					typeVersion: 1,
					position: [300, 300] as [number, number],
					parameters: {},
				},
			];

			const workflowDataToImport = {
				nodes: nodesToImport,
				connections: {},
				pinData: {
					'Execute Workflow Trigger 1': [{ json: { test: 'data1' } }],
					'Execute Workflow Trigger 2': [{ json: { test: 'data2' } }],
				},
			};

			// Mock createWorkflowObject to return a workflow with no nodes (since none can be created due to maxNodes limit)
			const workflowObject = createTestWorkflowObject({ nodes: [], connections: {} });
			vi.mocked(workflowDocumentStoreInstance.createWorkflowObject).mockReturnValue(workflowObject);

			const canvasOperations = useCanvasOperations();

			// This should not throw even when nodes can't be added due to maxNodes limit
			await expect(
				canvasOperations.importWorkflowData(workflowDataToImport, 'paste'),
			).resolves.not.toThrow();
			expect(toast.showError).not.toHaveBeenCalled();
			expect(toast.showMessage).toHaveBeenCalledWith({
				type: 'error',
				title: 'Could not insert node',
				message: `Only one '${EXECUTE_WORKFLOW_TRIGGER_NODE_TYPE}' node is allowed in a workflow`,
			});
		});

		it('should remove connections for nodes filtered out during import', async () => {
			vi.mocked(workflowDocumentStoreInstance.createWorkflowObject).mockImplementation(
				(nodes, connections) =>
					createTestWorkflowObject({
						nodes: nodes as INodeUi[],
						connections,
					}),
			);

			const workflowDataToImport = {
				nodes: [
					createTestNode({ id: 'start-id', name: 'Start' }),
					createTestNode({ id: 'missing-id', name: 'Missing', type: '' }),
					createTestNode({ id: 'end-id', name: 'End' }),
				],
				connections: {
					Start: {
						main: [
							[
								{ node: 'End', type: 'main' as const, index: 0 },
								{ node: 'Missing', type: 'main' as const, index: 0 },
							],
						],
					},
					Missing: {
						main: [[{ node: 'End', type: 'main' as const, index: 0 }]],
					},
				},
			};

			const canvasOperations = useCanvasOperations();
			const workflow = await canvasOperations.importWorkflowData(workflowDataToImport, 'paste');

			expect(workflow.connections).toEqual({
				Start: {
					main: [[{ node: 'End', type: 'main', index: 0 }]],
				},
			});
		});

		it.each(UPDATE_WEBHOOK_ID_NODE_TYPES)(
			'should regenerate webhook ids for node type "%s" on pasting into canvas and sync default paths',
			async (type) => {
				// This mock is needed for addImportedNodesToWorkflow to work
				vi.mocked(workflowDocumentStoreInstance.createWorkflowObject).mockReturnValue({
					nodes: {},
					connections: {},
					connectionsBySourceNode: {},
					renameNode: vi.fn(),
				} as unknown as Workflow);

				const nodesToImport = [
					{
						id: 'import-1',
						name: 'Execute Workflow Trigger 1',
						type,
						typeVersion: 1,
						position: [200, 200] as [number, number],
						webhookId: 'first-webhook',
						parameters: {
							path: 'first-webhook',
						},
					},
					{
						id: 'import-2',
						name: 'Execute Workflow Trigger 2',
						type,
						typeVersion: 1,
						position: [300, 300] as [number, number],
						webhookId: 'second-webhook',
						parameters: {
							options: {
								path: 'second-webhook',
							},
						},
					},
				];

				const workflowDataToImport = {
					nodes: nodesToImport,
					connections: {},
					pinData: {
						'Execute Workflow Trigger 1': [{ json: { test: 'data1' } }],
						'Execute Workflow Trigger 2': [{ json: { test: 'data2' } }],
					},
				};

				const canvasOperations = useCanvasOperations();

				const workflow = await canvasOperations.importWorkflowData(workflowDataToImport, 'paste');

				expect(workflow.nodes).toHaveLength(2);
				expect(workflow.nodes![0].name).toBe('Execute Workflow Trigger 1');
				expect(workflow.nodes![0].webhookId).not.toBe('first-webhook');
				expect(workflow.nodes![0].parameters.path).toBe(workflow.nodes![0].webhookId);
				expect(workflow.nodes![1].name).toBe('Execute Workflow Trigger 2');
				expect(workflow.nodes![1].webhookId).not.toBe('second-webhook');
				expect((workflow.nodes![1].parameters.options as { path: string }).path).toBe(
					workflow.nodes![1].webhookId,
				);
			},
		);

		it.each(UPDATE_WEBHOOK_ID_NODE_TYPES)(
			'should preserve user-defined paths for node type "%s" on importing into canvas',
			async (type) => {
				// This mock is needed for addImportedNodesToWorkflow to work
				vi.mocked(workflowDocumentStoreInstance.createWorkflowObject).mockReturnValue({
					nodes: {},
					connections: {},
					connectionsBySourceNode: {},
					renameNode: vi.fn(),
				} as unknown as Workflow);

				const nodesToImport = [
					{
						id: 'import-1',
						name: 'Execute Workflow Trigger 1',
						type,
						typeVersion: 1,
						position: [200, 200] as [number, number],
						webhookId: 'first-webhook',
						parameters: {
							path: 'my-custom-path',
						},
					},
					{
						id: 'import-2',
						name: 'Execute Workflow Trigger 2',
						type,
						typeVersion: 1,
						position: [300, 300] as [number, number],
						webhookId: 'second-webhook',
						parameters: {
							options: {
								path: 'my-custom-form',
							},
						},
					},
				];

				const workflowDataToImport = {
					nodes: nodesToImport,
					connections: {},
				};

				const canvasOperations = useCanvasOperations();

				const workflow = await canvasOperations.importWorkflowData(workflowDataToImport, 'file');

				expect(workflow.nodes).toHaveLength(2);
				expect(workflow.nodes![0].webhookId).not.toBe('first-webhook');
				expect(workflow.nodes![0].parameters.path).toBe('my-custom-path');
				expect(workflow.nodes![1].webhookId).not.toBe('second-webhook');
				expect((workflow.nodes![1].parameters.options as { path: string }).path).toBe(
					'my-custom-form',
				);
			},
		);

		it.each(UPDATE_WEBHOOK_ID_NODE_TYPES)(
			'should preserve user-defined paths for node type "%s" on pasting into canvas',
			async (type) => {
				vi.mocked(workflowDocumentStoreInstance.createWorkflowObject).mockReturnValue({
					nodes: {},
					connections: {},
					connectionsBySourceNode: {},
					renameNode: vi.fn(),
				} as unknown as Workflow);

				const nodesToImport = [
					{
						id: 'import-1',
						name: 'Execute Workflow Trigger 1',
						type,
						typeVersion: 1,
						position: [200, 200] as [number, number],
						webhookId: 'first-webhook',
						parameters: {
							options: {
								path: 'my-custom-form',
							},
						},
					},
				];

				const workflowDataToImport = {
					nodes: nodesToImport,
					connections: {},
				};

				const canvasOperations = useCanvasOperations();

				const workflow = await canvasOperations.importWorkflowData(workflowDataToImport, 'paste');

				expect(workflow.nodes).toHaveLength(1);
				expect(workflow.nodes![0].webhookId).not.toBe('first-webhook');
				expect((workflow.nodes![0].parameters.options as { path: string }).path).toBe(
					'my-custom-form',
				);
			},
		);

		it.each(UPDATE_WEBHOOK_ID_NODE_TYPES)(
			'should regenerate webhook id for node type "%s" when no path is defined',
			async (type) => {
				vi.mocked(workflowDocumentStoreInstance.createWorkflowObject).mockReturnValue({
					nodes: {},
					connections: {},
					connectionsBySourceNode: {},
					renameNode: vi.fn(),
				} as unknown as Workflow);

				const nodesToImport = [
					{
						id: 'import-1',
						name: 'Execute Workflow Trigger 1',
						type,
						typeVersion: 1,
						position: [200, 200] as [number, number],
						webhookId: 'first-webhook',
						parameters: {},
					},
				];

				const workflowDataToImport = {
					nodes: nodesToImport,
					connections: {},
				};

				const canvasOperations = useCanvasOperations();

				const workflow = await canvasOperations.importWorkflowData(workflowDataToImport, 'paste');

				expect(workflow.nodes).toHaveLength(1);
				expect(workflow.nodes![0].webhookId).not.toBe('first-webhook');
				expect(workflow.nodes![0].parameters.path).toBeUndefined();
				expect(workflow.nodes![0].parameters.options).toBeUndefined();
			},
		);

		it.each(UPDATE_WEBHOOK_ID_NODE_TYPES)(
			'should preserve user-defined parameters.path even when it equals the empty string for node type "%s"',
			async (type) => {
				vi.mocked(workflowDocumentStoreInstance.createWorkflowObject).mockReturnValue({
					nodes: {},
					connections: {},
					connectionsBySourceNode: {},
					renameNode: vi.fn(),
				} as unknown as Workflow);

				const nodesToImport = [
					{
						id: 'import-1',
						name: 'Execute Workflow Trigger 1',
						type,
						typeVersion: 1,
						position: [200, 200] as [number, number],
						webhookId: 'first-webhook',
						parameters: {
							options: {
								path: '',
							},
						},
					},
				];

				const workflowDataToImport = {
					nodes: nodesToImport,
					connections: {},
				};

				const canvasOperations = useCanvasOperations();

				const workflow = await canvasOperations.importWorkflowData(workflowDataToImport, 'file');

				expect(workflow.nodes).toHaveLength(1);
				expect(workflow.nodes![0].webhookId).not.toBe('first-webhook');
				// Empty options.path is not equal to previous webhookId, so it should not be replaced
				expect((workflow.nodes![0].parameters.options as { path: string }).path).toBe('');
			},
		);

		it.each([WEBHOOK_NODE_TYPE, MCP_TRIGGER_NODE_TYPE])(
			'should not regenerate webhook ids for node type "%s" on pasting into canvas',
			async (type) => {
				// This mock is needed for addImportedNodesToWorkflow to work
				vi.mocked(workflowDocumentStoreInstance.createWorkflowObject).mockReturnValue({
					nodes: {},
					connections: {},
					connectionsBySourceNode: {},
					renameNode: vi.fn(),
				} as unknown as Workflow);

				const nodesToImport = [
					{
						id: 'import-1',
						name: 'Execute Workflow Trigger 1',
						type,
						typeVersion: 1,
						position: [200, 200] as [number, number],
						webhookId: 'first-webhook',
						parameters: {
							path: 'some-path',
						},
					},
					{
						id: 'import-2',
						name: 'Execute Workflow Trigger 2',
						type,
						typeVersion: 1,
						position: [300, 300] as [number, number],
						webhookId: 'second-webhook',
						parameters: {
							options: {
								path: 'some-path',
							},
						},
					},
				];

				const workflowDataToImport = {
					nodes: nodesToImport,
					connections: {},
					pinData: {
						'Execute Workflow Trigger 1': [{ json: { test: 'data1' } }],
						'Execute Workflow Trigger 2': [{ json: { test: 'data2' } }],
					},
				};

				const canvasOperations = useCanvasOperations();

				// This should not throw even when nodes can't be added due to maxNodes limit
				const workflow = await canvasOperations.importWorkflowData(workflowDataToImport, 'paste');

				expect(workflow.nodes).toHaveLength(2);
				expect(workflow.nodes![0].name).toBe('Execute Workflow Trigger 1');
				expect(workflow.nodes![0].webhookId).toBe('first-webhook');
				expect(workflow.nodes![0].parameters.path).toBe('some-path');
				expect(workflow.nodes![1].name).toBe('Execute Workflow Trigger 2');
				expect(workflow.nodes![1].webhookId).toBe('second-webhook');
				expect((workflow.nodes![1].parameters.options as { path: string }).path).toBe('some-path');
			},
		);

		it('should remap nodeGroups nodeIds when regenerating IDs', async () => {
			const oldId1 = 'old-node-id-1';
			const oldId2 = 'old-node-id-2';

			const workflowDataWithGroups = {
				nodes: [
					{
						id: oldId1,
						name: 'Node 1',
						type: 'n8n-nodes-base.noOp',
						typeVersion: 1,
						position: [0, 0] as [number, number],
						parameters: {},
					},
					{
						id: oldId2,
						name: 'Node 2',
						type: 'n8n-nodes-base.noOp',
						typeVersion: 1,
						position: [200, 0] as [number, number],
						parameters: {},
					},
				],
				connections: {},
				nodeGroups: [{ id: 'group-1', name: 'My Group', nodeIds: [oldId1, oldId2] }],
			};

			vi.mocked(workflowDocumentStoreInstance.createWorkflowObject).mockImplementation(
				(nodes, connections) =>
					createTestWorkflowObject({
						nodes,
						connections,
					}),
			);
			vi.mocked(workflowDocumentStoreInstance.getNextDefaultName).mockImplementation(
				(name) => name,
			);
			const createGroupSpy = vi
				.mocked(workflowDocumentStoreInstance.createGroup)
				.mockImplementation((nodeIds, name) => ({
					id: 'imported-group-id',
					nodeIds,
					name,
				}));

			const canvasOperations = useCanvasOperations();
			const result = await canvasOperations.importWorkflowData(workflowDataWithGroups, 'paste', {
				regenerateIds: true,
			});

			// Node IDs should have been reassigned
			const newId1 = result.nodes![0].id;
			const newId2 = result.nodes![1].id;
			expect(newId1).not.toBe(oldId1);
			expect(newId2).not.toBe(oldId2);

			// nodeGroups should reference the new IDs
			expect(result.nodeGroups).toEqual([
				{ id: 'group-1', name: 'My Group', nodeIds: [newId1, newId2] },
			]);

			expect(createGroupSpy).toHaveBeenCalledWith([newId1, newId2], 'My Group', {
				markDirty: true,
				startCollapsed: true,
			});
			expect(workflowDocumentStoreInstance.getNextDefaultName).not.toHaveBeenCalled();
		});

		it('should deduplicate imported group names only when they already exist', async () => {
			const oldId1 = 'old-node-id-1';
			const oldId2 = 'old-node-id-2';

			const workflowDataWithGroups = {
				nodes: [
					{
						id: oldId1,
						name: 'Node 1',
						type: 'n8n-nodes-base.noOp',
						typeVersion: 1,
						position: [0, 0] as [number, number],
						parameters: {},
					},
					{
						id: oldId2,
						name: 'Node 2',
						type: 'n8n-nodes-base.noOp',
						typeVersion: 1,
						position: [200, 0] as [number, number],
						parameters: {},
					},
				],
				connections: {},
				nodeGroups: [{ id: 'group-1', name: 'My Group', nodeIds: [oldId1, oldId2] }],
			};

			vi.spyOn(workflowDocumentStoreInstance, 'allGroups', 'get').mockReturnValue([
				{ id: 'existing-group', name: 'My Group', nodeIds: ['other-1'] },
			]);
			vi.mocked(workflowDocumentStoreInstance.createWorkflowObject).mockImplementation(
				(nodes, connections) =>
					createTestWorkflowObject({
						nodes,
						connections,
					}),
			);
			vi.mocked(workflowDocumentStoreInstance.getNextDefaultName).mockReturnValue('My Group 2');
			const createGroupSpy = vi
				.mocked(workflowDocumentStoreInstance.createGroup)
				.mockImplementation((nodeIds, name) => ({
					id: 'imported-group-id',
					nodeIds,
					name,
				}));

			const canvasOperations = useCanvasOperations();
			await canvasOperations.importWorkflowData(workflowDataWithGroups, 'paste', {
				regenerateIds: true,
			});

			expect(workflowDocumentStoreInstance.getNextDefaultName).toHaveBeenCalledWith('My Group');
			expect(createGroupSpy).toHaveBeenCalledWith(expect.any(Array), 'My Group 2', {
				markDirty: true,
				startCollapsed: true,
			});
		});

		it('should keep imported node groups pristine when requested', async () => {
			const oldId1 = 'old-node-id-1';
			const oldId2 = 'old-node-id-2';

			const workflowDataWithGroups = {
				nodes: [
					{
						id: oldId1,
						name: 'Node 1',
						type: 'n8n-nodes-base.noOp',
						typeVersion: 1,
						position: [0, 0] as [number, number],
						parameters: {},
					},
					{
						id: oldId2,
						name: 'Node 2',
						type: 'n8n-nodes-base.noOp',
						typeVersion: 1,
						position: [200, 0] as [number, number],
						parameters: {},
					},
				],
				connections: {},
				nodeGroups: [{ id: 'group-1', name: 'My Group', nodeIds: [oldId1, oldId2] }],
			};

			vi.mocked(workflowDocumentStoreInstance.createWorkflowObject).mockImplementation(
				(nodes, connections) =>
					createTestWorkflowObject({
						nodes,
						connections,
					}),
			);
			const createGroupSpy = vi
				.mocked(workflowDocumentStoreInstance.createGroup)
				.mockImplementation((nodeIds, name) => ({
					id: 'imported-group-id',
					nodeIds,
					name,
				}));

			const canvasOperations = useCanvasOperations();
			const result = await canvasOperations.importWorkflowData(workflowDataWithGroups, 'paste', {
				regenerateIds: true,
				setStateDirty: false,
			});

			expect(createGroupSpy).toHaveBeenCalledWith(result.nodeGroups?.[0].nodeIds, 'My Group', {
				markDirty: false,
				startCollapsed: true,
			});
		});

		it('records imported node groups as undoable history within the import bulk', async () => {
			const historyStore = mockedStore(useHistoryStore);
			const workflowDataWithGroups = {
				nodes: [
					{
						id: 'old-node-id-1',
						name: 'Node 1',
						type: 'n8n-nodes-base.noOp',
						typeVersion: 1,
						position: [0, 0] as [number, number],
						parameters: {},
					},
					{
						id: 'old-node-id-2',
						name: 'Node 2',
						type: 'n8n-nodes-base.noOp',
						typeVersion: 1,
						position: [200, 0] as [number, number],
						parameters: {},
					},
				],
				connections: {},
				nodeGroups: [
					{ id: 'group-1', name: 'My Group', nodeIds: ['old-node-id-1', 'old-node-id-2'] },
				],
			};

			vi.mocked(workflowDocumentStoreInstance.createWorkflowObject).mockImplementation(
				(nodes, connections) => createTestWorkflowObject({ nodes, connections }),
			);
			vi.mocked(workflowDocumentStoreInstance.getNextDefaultName).mockImplementation(
				(name) => name,
			);
			vi.mocked(workflowDocumentStoreInstance.createGroup).mockImplementation((nodeIds, name) => ({
				id: 'imported-group-id',
				nodeIds,
				name,
			}));

			const canvasOperations = useCanvasOperations();
			await canvasOperations.importWorkflowData(workflowDataWithGroups, 'paste', {
				regenerateIds: true,
			});

			expect(historyStore.startRecordingUndo).toHaveBeenCalled();
			expect(historyStore.stopRecordingUndo).toHaveBeenCalled();
			const groupCommand = historyStore.pushCommandToUndo.mock.calls
				.map(([command]) => command)
				.find((command) => command instanceof AddNodeGroupCommand) as
				| AddNodeGroupCommand
				| undefined;
			expect(groupCommand).toBeInstanceOf(AddNodeGroupCommand);
			expect(groupCommand?.group.id).toBe('imported-group-id');
			expect(groupCommand?.group.name).toBe('My Group');
		});
	});

	describe('duplicateNodes', () => {
		it('should duplicate nodes', async () => {
			const nodeTypesStore = useNodeTypesStore();
			const nodeTypeDescription = mockNodeTypeDescription({ name: SET_NODE_TYPE });

			nodeTypesStore.nodeTypes = {
				[SET_NODE_TYPE]: { 1: nodeTypeDescription },
			};

			const nodes = buildImportNodes();
			workflowDocumentStoreInstance.allNodes = nodes;
			vi.spyOn(workflowDocumentStoreInstance, 'getNodesByIds').mockReturnValue(nodes);
			vi.mocked(workflowDocumentStoreInstance.outgoingConnectionsByNodeName).mockReturnValue({});

			const workflowObject = createTestWorkflowObject({
				nodes: workflowDocumentStoreInstance.allNodes,
				connections: workflowDocumentStoreInstance.connectionsBySourceNode,
			});
			vi.mocked(workflowDocumentStoreInstance.createWorkflowObject).mockReturnValue(workflowObject);

			const canvasOperations = useCanvasOperations();
			const duplicatedNodeIds = await canvasOperations.duplicateNodes(['1', '2']);

			expect(duplicatedNodeIds.length).toBe(2);
			expect(duplicatedNodeIds).not.toContain('1');
			expect(duplicatedNodeIds).not.toContain('2');
		});

		it('should show max node type error when duplicating nodes that exceed maxNodes limit', async () => {
			const toast = useToast();
			const nodeTypesStore = useNodeTypesStore();
			const nodeTypeDescription = mockNodeTypeDescription({
				name: EXECUTE_WORKFLOW_TRIGGER_NODE_TYPE,
				maxNodes: 1,
			});
			const node = createTestNode({
				id: '1',
				name: 'Execute Workflow Trigger',
				type: EXECUTE_WORKFLOW_TRIGGER_NODE_TYPE,
			});

			nodeTypesStore.nodeTypes = {
				[EXECUTE_WORKFLOW_TRIGGER_NODE_TYPE]: { 1: nodeTypeDescription },
			};
			workflowDocumentStoreInstance.allNodes = [node];
			vi.spyOn(workflowDocumentStoreInstance, 'getNodesByIds').mockReturnValue([node]);
			vi.mocked(workflowDocumentStoreInstance.outgoingConnectionsByNodeName).mockReturnValue({});

			const workflowObject = createTestWorkflowObject({ nodes: [], connections: {} });
			vi.mocked(workflowDocumentStoreInstance.createWorkflowObject).mockReturnValue(workflowObject);

			const canvasOperations = useCanvasOperations();
			await canvasOperations.duplicateNodes(['1']);

			expect(toast.showMessage).toHaveBeenCalledWith({
				type: 'error',
				title: 'Could not insert node',
				message: `Only one '${EXECUTE_WORKFLOW_TRIGGER_NODE_TYPE}' node is allowed in a workflow`,
			});
		});

		it('should not crash when TelemetryHelpers.generateNodesGraph throws error', async () => {
			const telemetry = useTelemetry();
			const nodeTypesStore = useNodeTypesStore();
			const nodeTypeDescription = mockNodeTypeDescription({ name: SET_NODE_TYPE });

			nodeTypesStore.nodeTypes = {
				[SET_NODE_TYPE]: { 1: nodeTypeDescription },
			};

			const nodes = buildImportNodes();
			workflowDocumentStoreInstance.allNodes = nodes;
			vi.spyOn(workflowDocumentStoreInstance, 'getNodesByIds').mockReturnValue(nodes);
			vi.mocked(workflowDocumentStoreInstance.outgoingConnectionsByNodeName).mockReturnValue({});

			const workflowObject = createTestWorkflowObject({
				nodes: workflowDocumentStoreInstance.allNodes,
				connections: workflowDocumentStoreInstance.connectionsBySourceNode,
			});
			vi.mocked(workflowDocumentStoreInstance.createWorkflowObject).mockReturnValue(workflowObject);

			// Mock TelemetryHelpers.generateNodesGraph to throw an error for this test
			vi.mocked(TelemetryHelpers.generateNodesGraph).mockImplementationOnce(() => {
				throw new Error('Telemetry generation failed');
			});

			// This should not throw even when telemetry fails
			const canvasOperations = useCanvasOperations();
			await expect(canvasOperations.duplicateNodes(['1', '2'])).resolves.not.toThrow();

			// Telemetry should not be tracked when generation fails
			expect(telemetry.track).not.toHaveBeenCalledWith(
				'User imported workflow',
				expect.any(Object),
			);
		});
	});

	describe('importTemplate', () => {
		it('should import template to canvas', async () => {
			const projectsStore = mockedStore(useProjectsStore);
			projectsStore.currentProjectId = 'test-project-id';

			const workflowsStore = mockedStore(useWorkflowsStore);
			workflowsStore.convertTemplateNodeToNodeUi.mockImplementation((node) => ({
				...node,
				credentials: {},
			}));

			// Create nodes: A -> B (no outgoing from B)
			const nodeA: IWorkflowTemplateNode = createTestNode({
				id: 'X',
				name: 'Node X',
				position: [80, 80],
			});
			const nodeB: IWorkflowTemplateNode = createTestNode({
				id: 'Y',
				name: 'Node Y',
				position: [192, 80],
			});

			const workflow: IWorkflowTemplate['workflow'] = {
				nodes: [nodeA, nodeB],
				connections: {
					[nodeA.name]: {
						main: [[{ node: nodeB.name, type: NodeConnectionTypes.Main, index: 0 }]],
					},
				},
			};

			const getNewWorkflowData = vi.mocked(workflowsApi.getNewWorkflowData);
			const setConnectionsSpy = vi.spyOn(workflowDocumentStoreInstance, 'setConnections');

			const { importTemplate } = useCanvasOperations();

			const templateName = 'template name';
			await importTemplate({
				name: templateName,
				workflow,
			});

			expect(setConnectionsSpy).toHaveBeenCalledWith(workflow.connections);
			expect(workflowDocumentStoreInstance.addNode).toHaveBeenNthCalledWith(1, {
				...nodeA,
				credentials: {},
				disabled: false,
			});
			expect(workflowDocumentStoreInstance.setNodePristine).toHaveBeenCalledWith(nodeA.name, true);
			expect(workflowDocumentStoreInstance.addNode).toHaveBeenNthCalledWith(2, {
				...nodeB,
				credentials: {},
				disabled: false,
			});
			expect(workflowDocumentStoreInstance.setNodePristine).toHaveBeenCalledWith(nodeB.name, true);
			expect(getNewWorkflowData).toHaveBeenCalledWith(
				expect.anything(),
				templateName,
				projectsStore.currentProjectId,
			);
		});
	});
	describe('replaceNodeParameters', () => {
		it('should replace node parameters and track history', () => {
			const historyStore = mockedStore(useHistoryStore);

			const nodeId = 'node1';
			const currentParameters = { param1: 'value1' };
			const newParameters = { param1: 'value2' };

			const node = createTestNode({
				id: nodeId,
				type: 'node',
				name: 'Node 1',
				parameters: currentParameters,
			});

			vi.spyOn(workflowDocumentStoreInstance, 'getNodeById').mockReturnValue(node);
			const setNodeParametersSpy = vi.spyOn(workflowDocumentStoreInstance, 'setNodeParameters');

			const { replaceNodeParameters } = useCanvasOperations();
			replaceNodeParameters(nodeId, currentParameters, newParameters, { trackHistory: true });

			expect(setNodeParametersSpy).toHaveBeenCalledWith({
				name: node.name,
				value: newParameters,
			});
			expect(historyStore.pushCommandToUndo).toHaveBeenCalledWith(
				new ReplaceNodeParametersCommand(
					nodeId,
					currentParameters,
					newParameters,
					expect.any(Number),
				),
			);
		});

		it('should replace node parameters without tracking history', () => {
			const historyStore = mockedStore(useHistoryStore);

			const nodeId = 'node1';
			const currentParameters = { param1: 'value1' };
			const newParameters = { param1: 'value2' };

			const node = createTestNode({
				id: nodeId,
				type: 'node',
				name: 'Node 1',
				parameters: currentParameters,
			});

			vi.spyOn(workflowDocumentStoreInstance, 'getNodeById').mockReturnValue(node);
			const setNodeParametersSpy = vi.spyOn(workflowDocumentStoreInstance, 'setNodeParameters');

			const { replaceNodeParameters } = useCanvasOperations();
			replaceNodeParameters(nodeId, currentParameters, newParameters, { trackHistory: false });

			expect(setNodeParametersSpy).toHaveBeenCalledWith({
				name: node.name,
				value: newParameters,
			});
			expect(historyStore.pushCommandToUndo).not.toHaveBeenCalled();
		});

		it('should not replace parameters if node does not exist', () => {
			const nodeId = 'nonexistent';
			const currentParameters = { param1: 'value1' };
			const newParameters = { param1: 'value2' };

			vi.spyOn(workflowDocumentStoreInstance, 'getNodeById').mockReturnValue(undefined);
			const setNodeParametersSpy = vi.spyOn(workflowDocumentStoreInstance, 'setNodeParameters');

			const { replaceNodeParameters } = useCanvasOperations();
			replaceNodeParameters(nodeId, currentParameters, newParameters);

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

		it('should handle bulk tracking when replacing parameters for multiple nodes', () => {
			const historyStore = mockedStore(useHistoryStore);

			const nodeId1 = 'node1';
			const nodeId2 = 'node2';
			const currentParameters1 = { param1: 'value1' };
			const newParameters1 = { param1: 'value2' };
			const currentParameters2 = { param2: 'value3' };
			const newParameters2 = { param2: 'value4' };

			const node1 = createTestNode({
				id: nodeId1,
				type: 'node',
				name: 'Node 1',
				parameters: currentParameters1,
			});
			const node2 = createTestNode({
				id: nodeId2,
				type: 'node',
				name: 'Node 2',
				parameters: currentParameters2,
			});

			const setNodeParametersSpy = vi.spyOn(workflowDocumentStoreInstance, 'setNodeParameters');
			vi.spyOn(workflowDocumentStoreInstance, 'getNodeById')
				.mockReturnValueOnce(node1)
				.mockReturnValueOnce(node2);

			const { replaceNodeParameters } = useCanvasOperations();
			replaceNodeParameters(nodeId1, currentParameters1, newParameters1, {
				trackHistory: true,
				trackBulk: false,
			});
			replaceNodeParameters(nodeId2, currentParameters2, newParameters2, {
				trackHistory: true,
				trackBulk: false,
			});

			expect(historyStore.startRecordingUndo).not.toHaveBeenCalled();
			expect(historyStore.stopRecordingUndo).not.toHaveBeenCalled();
			expect(setNodeParametersSpy).toHaveBeenCalledTimes(2);
		});

		it('should revert replaced node parameters', async () => {
			const nodeId = 'node1';
			const currentParameters = { param1: 'value1' };
			const newParameters = { param1: 'value2' };

			const node = createTestNode({
				id: nodeId,
				type: 'node',
				name: 'Node 1',
				parameters: newParameters,
			});

			const setNodeParametersSpy = vi.spyOn(workflowDocumentStoreInstance, 'setNodeParameters');
			vi.spyOn(workflowDocumentStoreInstance, 'getNodeById').mockReturnValue(node);

			const { revertReplaceNodeParameters } = useCanvasOperations();
			await revertReplaceNodeParameters(nodeId, currentParameters, newParameters);

			expect(setNodeParametersSpy).toHaveBeenCalledWith({
				name: node.name,
				value: currentParameters,
			});
		});
	});
	describe('replaceNodeConnections', () => {
		const sourceNode = createTestNode({ id: 'source', name: 'Source Node' });
		const targetNode = createTestNode({ id: 'target', name: 'Target Node' });
		const replacementNode = createTestNode({ id: 'replacement', name: 'Replacement Node' });
		const nextNode = createTestNode({ id: 'next', name: 'Next Node' });

		let historyStore: ReturnType<typeof mockedStore<typeof useHistoryStore>>;
		let nodeTypesStore: ReturnType<typeof mockedStore<typeof useNodeTypesStore>>;

		beforeEach(() => {
			historyStore = mockedStore(useHistoryStore);
			nodeTypesStore = mockedStore(useNodeTypesStore);

			const nodeTypeDescription = mockNodeTypeDescription({
				inputs: [NodeConnectionTypes.Main, NodeConnectionTypes.Main],
				outputs: [NodeConnectionTypes.Main, NodeConnectionTypes.Main],
			});
			nodeTypesStore.getNodeType = vi.fn().mockReturnValue(nodeTypeDescription);
		});
		afterEach(() => {
			// Mock cleanup handled automatically by test isolation
		});

		describe('common cases', () => {
			beforeEach(() => {
				workflowDocumentStoreInstance.allNodes = [
					sourceNode,
					targetNode,
					replacementNode,
					nextNode,
				];
				workflowDocumentStoreInstance.connectionsBySourceNode = {
					[sourceNode.name]: {
						[NodeConnectionTypes.Main]: [
							[
								{ node: targetNode.name, type: NodeConnectionTypes.Main, index: 0 },
								{ node: targetNode.name, type: NodeConnectionTypes.Main, index: 1 },
							],
						],
					},
					[targetNode.name]: {
						[NodeConnectionTypes.Main]: [
							[{ node: nextNode.name, type: NodeConnectionTypes.Main, index: 0 }],
						],
					},
				};

				vi.spyOn(workflowDocumentStoreInstance, 'getNodeById').mockImplementation((id) => {
					if (id === sourceNode.id) return sourceNode;
					if (id === targetNode.id) return targetNode;
					if (id === replacementNode.id) return replacementNode;
					if (id === nextNode.id) return nextNode;
					return undefined;
				});
				vi.spyOn(workflowDocumentStoreInstance, 'getNodeByName').mockImplementation((name) => {
					if (name === sourceNode.name) return sourceNode;
					if (name === targetNode.name) return targetNode;
					if (name === replacementNode.name) return replacementNode;
					if (name === nextNode.name) return nextNode;
					return null;
				});
			});
			it('should replace connections for a node and track history', () => {
				const workflowObject = createTestWorkflowObject({
					nodes: workflowDocumentStoreInstance.allNodes,
					connections: workflowDocumentStoreInstance.connectionsBySourceNode,
				});
				vi.mocked(workflowDocumentStoreInstance.getParentNodes).mockImplementation((...args) =>
					workflowObject.getParentNodes(...args),
				);
				vi.mocked(workflowDocumentStoreInstance.getChildNodes).mockImplementation((...args) =>
					workflowObject.getChildNodes(...args),
				);
				vi.mocked(workflowDocumentStoreInstance.getConnectionsBetweenNodes).mockImplementation(
					(...args) => workflowObject.getConnectionsBetweenNodes(...args),
				);

				const { replaceNodeConnections } = useCanvasOperations();
				replaceNodeConnections(targetNode.id, replacementNode.id, { trackHistory: true });

				expect(workflowDocumentStoreInstance.removeConnection).toHaveBeenCalledTimes(3);
				expect(workflowDocumentStoreInstance.removeConnection).toHaveBeenCalledWith({
					connection: [
						{
							index: 0,
							node: 'Source Node',
							type: NodeConnectionTypes.Main,
						},
						{
							index: 0,
							node: 'Target Node',
							type: NodeConnectionTypes.Main,
						},
					],
				});
				expect(workflowDocumentStoreInstance.removeConnection).toHaveBeenCalledWith({
					connection: [
						{
							index: 0,
							node: 'Source Node',
							type: NodeConnectionTypes.Main,
						},
						{
							index: 1,
							node: 'Target Node',
							type: NodeConnectionTypes.Main,
						},
					],
				});
				expect(workflowDocumentStoreInstance.removeConnection).toHaveBeenCalledWith({
					connection: [
						{
							index: 0,
							node: 'Target Node',
							type: NodeConnectionTypes.Main,
						},
						{
							index: 0,
							node: 'Next Node',
							type: NodeConnectionTypes.Main,
						},
					],
				});
				expect(workflowDocumentStoreInstance.addConnection).toHaveBeenCalledTimes(3);
				expect(workflowDocumentStoreInstance.addConnection).toHaveBeenCalledWith({
					connection: [
						{
							index: 0,
							node: 'Source Node',
							type: 'main',
						},
						{
							index: 0,
							node: 'Replacement Node',
							type: 'main',
						},
					],
				});
				expect(workflowDocumentStoreInstance.addConnection).toHaveBeenCalledWith({
					connection: [
						{
							index: 0,
							node: 'Source Node',
							type: NodeConnectionTypes.Main,
						},
						{
							index: 1,
							node: 'Replacement Node',
							type: NodeConnectionTypes.Main,
						},
					],
				});
				expect(workflowDocumentStoreInstance.addConnection).toHaveBeenCalledWith({
					connection: [
						{
							index: 0,
							node: 'Replacement Node',
							type: NodeConnectionTypes.Main,
						},
						{
							index: 0,
							node: 'Next Node',
							type: NodeConnectionTypes.Main,
						},
					],
				});

				expect(historyStore.startRecordingUndo).toHaveBeenCalled();
				expect(historyStore.stopRecordingUndo).toHaveBeenCalled();
			});

			it('should replace connections without tracking history', () => {
				const workflowObject = createTestWorkflowObject({
					nodes: workflowDocumentStoreInstance.allNodes,
					connections: workflowDocumentStoreInstance.connectionsBySourceNode,
				});
				vi.mocked(workflowDocumentStoreInstance.getParentNodes).mockImplementation((...args) =>
					workflowObject.getParentNodes(...args),
				);
				vi.mocked(workflowDocumentStoreInstance.getChildNodes).mockImplementation((...args) =>
					workflowObject.getChildNodes(...args),
				);
				vi.mocked(workflowDocumentStoreInstance.getConnectionsBetweenNodes).mockImplementation(
					(...args) => workflowObject.getConnectionsBetweenNodes(...args),
				);

				const { replaceNodeConnections } = useCanvasOperations();
				replaceNodeConnections(targetNode.id, replacementNode.id, { trackHistory: false });

				expect(workflowDocumentStoreInstance.removeConnection).toHaveBeenCalled();
				expect(workflowDocumentStoreInstance.addConnection).toHaveBeenCalled();
				expect(historyStore.startRecordingUndo).not.toHaveBeenCalled();
				expect(historyStore.stopRecordingUndo).not.toHaveBeenCalled();
			});

			it('should not replace connections if previous node does not exist', () => {
				const { replaceNodeConnections } = useCanvasOperations();
				replaceNodeConnections('nonexistent', replacementNode.id);

				expect(workflowDocumentStoreInstance.removeConnection).not.toHaveBeenCalled();
				expect(workflowDocumentStoreInstance.addConnection).not.toHaveBeenCalled();
			});

			it('should not replace connections if new node does not exist', () => {
				const { replaceNodeConnections } = useCanvasOperations();
				replaceNodeConnections(targetNode.id, 'nonexistent');

				expect(workflowDocumentStoreInstance.removeConnection).not.toHaveBeenCalled();
				expect(workflowDocumentStoreInstance.addConnection).not.toHaveBeenCalled();
			});

			it('should respect replaceInputs being false', () => {
				const { replaceNodeConnections } = useCanvasOperations();
				// nextNode only has an input connection
				replaceNodeConnections(nextNode.id, replacementNode.id, {
					trackHistory: true,
					replaceInputs: false,
				});

				expect(workflowDocumentStoreInstance.removeConnection).not.toHaveBeenCalled();
				expect(workflowDocumentStoreInstance.addConnection).not.toHaveBeenCalled();
			});

			it('should respect replaceOutputs being false', () => {
				const { replaceNodeConnections } = useCanvasOperations();
				// sourceNode only has an output connection
				replaceNodeConnections(sourceNode.id, replacementNode.id, {
					trackHistory: true,
					replaceOutputs: false,
				});

				expect(workflowDocumentStoreInstance.removeConnection).not.toHaveBeenCalled();
				expect(workflowDocumentStoreInstance.addConnection).not.toHaveBeenCalled();
			});
		});
		it('should handle bulk tracking when replacing connections for multiple nodes', () => {
			const previousNode1 = createTestNode({
				id: 'node1',
				name: 'Previous Node 1',
			});
			const newNode1 = createTestNode({
				id: 'node2',
				name: 'New Node 1',
			});
			const previousNode2 = createTestNode({
				id: 'node3',
				name: 'Previous Node 2',
			});
			const newNode2 = createTestNode({
				id: 'node4',
				name: 'New Node 2',
			});
			const targetNode = createTestNode({
				id: 'node5',
				name: 'Target Node',
			});

			workflowDocumentStoreInstance.allNodes = [
				previousNode1,
				previousNode2,
				newNode1,
				newNode2,
				targetNode,
			];
			workflowDocumentStoreInstance.connectionsBySourceNode = {
				[previousNode1.name]: {
					[NodeConnectionTypes.Main]: [
						[{ node: targetNode.name, type: NodeConnectionTypes.Main, index: 0 }],
					],
				},
				[previousNode2.name]: {
					[NodeConnectionTypes.Main]: [
						[{ node: targetNode.name, type: NodeConnectionTypes.Main, index: 0 }],
					],
				},
			};

			vi.spyOn(workflowDocumentStoreInstance, 'getNodeById').mockImplementation((id) => {
				if (id === previousNode1.id) return previousNode1;
				if (id === newNode1.id) return newNode1;
				if (id === previousNode2.id) return previousNode1;
				if (id === newNode2.id) return newNode2;
				if (id === targetNode.id) return targetNode;
				return undefined;
			});
			vi.spyOn(workflowDocumentStoreInstance, 'getNodeByName').mockImplementation((name) => {
				if (name === previousNode1.name) return previousNode1;
				if (name === newNode1.name) return newNode1;
				if (name === previousNode2.name) return previousNode1;
				if (name === newNode2.name) return newNode2;
				if (name === targetNode.name) return targetNode;
				return null;
			});

			const workflowObject = createTestWorkflowObject({
				nodes: workflowDocumentStoreInstance.allNodes,
				connections: workflowDocumentStoreInstance.connectionsBySourceNode,
			});
			vi.mocked(workflowDocumentStoreInstance.getParentNodes).mockImplementation((...args) =>
				workflowObject.getParentNodes(...args),
			);
			vi.mocked(workflowDocumentStoreInstance.getChildNodes).mockImplementation((...args) =>
				workflowObject.getChildNodes(...args),
			);
			vi.mocked(workflowDocumentStoreInstance.getConnectionsBetweenNodes).mockImplementation(
				(...args) => workflowObject.getConnectionsBetweenNodes(...args),
			);

			const { replaceNodeConnections } = useCanvasOperations();
			replaceNodeConnections(previousNode1.id, newNode1.id, {
				trackHistory: true,
				trackBulk: false,
			});
			replaceNodeConnections(previousNode2.id, newNode2.id, {
				trackHistory: true,
				trackBulk: false,
			});

			expect(historyStore.startRecordingUndo).not.toHaveBeenCalled();
			expect(historyStore.stopRecordingUndo).not.toHaveBeenCalled();
			expect(workflowDocumentStoreInstance.removeConnection).toHaveBeenCalledTimes(2);
			expect(workflowDocumentStoreInstance.addConnection).toHaveBeenCalledTimes(2);
		});

		describe('node group validation', () => {
			const firstNode = createTestNode({
				id: 'first',
				name: 'First Node',
				type: SET_NODE_TYPE,
			});
			const selectedStartNode = createTestNode({
				id: 'selected-start',
				name: 'Selected Start Node',
				type: SET_NODE_TYPE,
			});
			const selectedEndNode = createTestNode({
				id: 'selected-end',
				name: 'Selected End Node',
				type: SET_NODE_TYPE,
			});
			const lastNode = createTestNode({
				id: 'last',
				name: 'Last Node',
				type: SET_NODE_TYPE,
			});
			const replacementNode = createTestNode({
				id: 'replacement',
				name: 'Replacement Node',
				type: SET_NODE_TYPE,
			});
			const group = {
				id: 'group',
				nodeIds: [firstNode.id, selectedStartNode.id, selectedEndNode.id, lastNode.id],
				name: 'Group 1',
			};

			beforeEach(() => {
				const nodes = [firstNode, selectedStartNode, selectedEndNode, lastNode, replacementNode];

				setupGroupedCanvas({
					nodes,
					connections: createConnectionsBySource(
						workflowConnection(firstNode, selectedStartNode),
						workflowConnection(selectedStartNode, selectedEndNode),
						workflowConnection(selectedEndNode, lastNode),
					),
					groups: [group],
					mockWorkflowObject: true,
				});
			});

			it('should not add duplicate replacement connections when a grouped replacement is blocked', () => {
				const toast = useToast();
				const { replaceNodeConnections } = useCanvasOperations();

				replaceNodeConnections(selectedEndNode.id, replacementNode.id, {
					replaceInputs: false,
				});

				expect(workflowDocumentStoreInstance.removeConnection).not.toHaveBeenCalled();
				expect(workflowDocumentStoreInstance.addConnection).not.toHaveBeenCalled();
				expect(toast.showToast).toHaveBeenCalledWith(
					expect.objectContaining({ title: 'Connection not added', type: 'error' }),
				);
			});

			it('should replace grouped connections without per-edge validation for graph rewrites', () => {
				const toast = useToast();
				const { replaceNodeConnections } = useCanvasOperations();

				replaceNodeConnections(selectedEndNode.id, replacementNode.id, {
					replaceInputs: false,
					validateNodeGroups: false,
				});

				expect(workflowDocumentStoreInstance.removeConnection).toHaveBeenCalledTimes(1);
				expectConnectionRemoved(selectedEndNode, lastNode);
				expect(workflowDocumentStoreInstance.addConnection).toHaveBeenCalledTimes(1);
				expectConnectionAdded(replacementNode, lastNode);
				expect(toast.showToast).not.toHaveBeenCalledWith(
					expect.objectContaining({ type: 'error' }),
				);
			});
		});
	});
	describe('replaceNode', () => {
		const sourceNode = createTestNode({ id: 'source', name: 'Source Node' });
		const targetNode = createTestNode({ id: 'target', name: 'Target Node' });
		const replacementNode = createTestNode({
			id: 'replacement',
			name: 'Replacement Node',
		});
		const nextNode = createTestNode({ id: 'next', name: 'Next Node' });
		const connectionSourceMain0 = {
			node: sourceNode.name,
			type: NodeConnectionTypes.Main,
			index: 0,
		};
		const connectionSourceMain1 = {
			node: sourceNode.name,
			type: NodeConnectionTypes.Main,
			index: 1,
		};
		const connectionTargetMain0 = {
			node: targetNode.name,
			type: NodeConnectionTypes.Main,
			index: 0,
		};
		const connectionTargetMain1 = {
			node: targetNode.name,
			type: NodeConnectionTypes.Main,
			index: 1,
		};
		const connectionReplacementMain0 = {
			node: replacementNode.name,
			type: NodeConnectionTypes.Main,
			index: 0,
		};
		const connectionReplacementMain1 = {
			node: replacementNode.name,
			type: NodeConnectionTypes.Main,
			index: 1,
		};

		const connectionNextMain0 = {
			node: nextNode.name,
			type: NodeConnectionTypes.Main,
			index: 0,
		};

		let historyStore: ReturnType<typeof mockedStore<typeof useHistoryStore>>;
		let nodeTypesStore: ReturnType<typeof mockedStore<typeof useNodeTypesStore>>;

		beforeEach(() => {
			historyStore = mockedStore(useHistoryStore);
			nodeTypesStore = mockedStore(useNodeTypesStore);

			const nodeTypeDescription = mockNodeTypeDescription({
				inputs: [NodeConnectionTypes.Main, NodeConnectionTypes.Main],
				outputs: [NodeConnectionTypes.Main, NodeConnectionTypes.Main],
			});
			nodeTypesStore.getNodeType = vi.fn().mockReturnValue(nodeTypeDescription);
		});
		afterEach(() => {
			// Mock cleanup handled automatically by test isolation
		});

		describe('common cases', () => {
			beforeEach(() => {
				workflowDocumentStoreInstance.allNodes = [
					sourceNode,
					targetNode,
					replacementNode,
					nextNode,
				];
				workflowDocumentStoreInstance.connectionsBySourceNode = {
					[sourceNode.name]: {
						[NodeConnectionTypes.Main]: [[connectionTargetMain0], [connectionTargetMain1]],
					},
					[targetNode.name]: {
						[NodeConnectionTypes.Main]: [[connectionNextMain0]],
					},
				};

				vi.spyOn(workflowDocumentStoreInstance, 'getNodeById').mockImplementation((id) => {
					if (id === sourceNode.id) return sourceNode;
					if (id === targetNode.id) return targetNode;
					if (id === replacementNode.id) return replacementNode;
					if (id === nextNode.id) return nextNode;
					return undefined;
				});
				vi.spyOn(workflowDocumentStoreInstance, 'getNodeByName').mockImplementation((name) => {
					if (name === sourceNode.name) return sourceNode;
					if (name === targetNode.name) return targetNode;
					if (name === replacementNode.name) return replacementNode;
					if (name === nextNode.name) return nextNode;
					return null;
				});
				vi.mocked(workflowDocumentStoreInstance.incomingConnectionsByNodeName).mockImplementation(
					(name) => {
						if (name === targetNode.name)
							return {
								[NodeConnectionTypes.Main]: [[connectionSourceMain0], [connectionSourceMain1]],
							};
						if (name === nextNode.name)
							return {
								[NodeConnectionTypes.Main]: [[connectionTargetMain0]],
							};

						return {} as INodeConnections;
					},
				);
				vi.mocked(workflowDocumentStoreInstance.outgoingConnectionsByNodeName).mockImplementation(
					(name) => {
						if (name === sourceNode.name)
							return {
								[NodeConnectionTypes.Main]: [[connectionTargetMain0], [connectionTargetMain1]],
							};
						if (name === targetNode.name)
							return {
								[NodeConnectionTypes.Main]: [[connectionNextMain0]],
							};

						return {} as INodeConnections;
					},
				);
			});
			it('should replace an existing node and track history', () => {
				const workflowObject = createTestWorkflowObject({
					nodes: workflowDocumentStoreInstance.allNodes,
					connections: workflowDocumentStoreInstance.connectionsBySourceNode,
				});
				vi.mocked(workflowDocumentStoreInstance.getParentNodes).mockImplementation((...args) =>
					workflowObject.getParentNodes(...args),
				);
				vi.mocked(workflowDocumentStoreInstance.getChildNodes).mockImplementation((...args) =>
					workflowObject.getChildNodes(...args),
				);
				vi.mocked(workflowDocumentStoreInstance.getConnectionsBetweenNodes).mockImplementation(
					(...args) => workflowObject.getConnectionsBetweenNodes(...args),
				);

				const { replaceNode } = useCanvasOperations();
				replaceNode(targetNode.id, replacementNode.id, { trackHistory: true });

				expect(workflowDocumentStoreInstance.removeConnection).toHaveBeenCalledTimes(6);
				// These are called twice with the same input, once for input connections, once for output connections
				expect(workflowDocumentStoreInstance.removeConnection).toHaveBeenNthCalledWith(1, {
					connection: [connectionSourceMain0, connectionTargetMain0],
				});
				expect(workflowDocumentStoreInstance.removeConnection).toHaveBeenNthCalledWith(2, {
					connection: [connectionSourceMain1, connectionTargetMain1],
				});
				expect(workflowDocumentStoreInstance.removeConnection).toHaveBeenNthCalledWith(3, {
					connection: [connectionTargetMain0, connectionNextMain0],
				});
				expect(workflowDocumentStoreInstance.removeConnection).toHaveBeenNthCalledWith(4, {
					connection: [connectionSourceMain0, connectionTargetMain0],
				});
				expect(workflowDocumentStoreInstance.removeConnection).toHaveBeenNthCalledWith(5, {
					connection: [connectionSourceMain1, connectionTargetMain1],
				});
				expect(workflowDocumentStoreInstance.removeConnection).toHaveBeenNthCalledWith(6, {
					connection: [connectionTargetMain0, connectionNextMain0],
				});
				expect(workflowDocumentStoreInstance.addConnection).toHaveBeenCalledTimes(4);
				expect(workflowDocumentStoreInstance.addConnection).toHaveBeenCalledWith({
					connection: [connectionSourceMain0, connectionReplacementMain0],
				});
				expect(workflowDocumentStoreInstance.addConnection).toHaveBeenCalledWith({
					connection: [connectionSourceMain1, connectionReplacementMain1],
				});
				expect(workflowDocumentStoreInstance.addConnection).toHaveBeenCalledWith({
					connection: [connectionReplacementMain0, connectionNextMain0],
				});
				expect(workflowDocumentStoreInstance.removeNodeById).toHaveBeenCalledWith(targetNode.id);

				expect(historyStore.startRecordingUndo).toHaveBeenCalled();
				expect(historyStore.stopRecordingUndo).toHaveBeenCalled();
			});
		});
		describe('node group validation', () => {
			it('should replace a grouped node against the final graph and transfer group membership', () => {
				const toast = useToast();
				const group = {
					id: 'group',
					nodeIds: [sourceNode.id, targetNode.id, nextNode.id],
					name: 'Group 1',
				};
				const connections: IConnections = {
					[sourceNode.name]: {
						[NodeConnectionTypes.Main]: [[connectionTargetMain0]],
					},
					[targetNode.name]: {
						[NodeConnectionTypes.Main]: [[connectionNextMain0]],
					},
				};
				const nodes = [sourceNode, targetNode, replacementNode, nextNode];

				const { workflowDocumentStore } = setupGroupedCanvas({
					nodes,
					connections,
					groups: [group],
					nodeTypeDescription: createGroupedNodeTypeDescription([
						NodeConnectionTypes.Main,
						NodeConnectionTypes.Main,
					]),
					mockWorkflowObject: true,
				});
				const replaceNodeInGroupSpy = vi
					.spyOn(workflowDocumentStore, 'replaceNodeInGroup')
					.mockImplementation((id, previousNodeId, newNodeId) => {
						if (id !== group.id) return;
						group.nodeIds = group.nodeIds.map((nodeId) =>
							nodeId === previousNodeId ? newNodeId : nodeId,
						);
					});
				vi.spyOn(workflowDocumentStore, 'getGroupById').mockImplementation((id) =>
					id === group.id ? group : undefined,
				);

				vi.mocked(workflowDocumentStoreInstance.incomingConnectionsByNodeName).mockImplementation(
					(name) => {
						const sourceConnections = connections[sourceNode.name]?.[NodeConnectionTypes.Main];
						const hasSourceToTarget = sourceConnections?.[0]?.some(
							(connection) => connection.node === targetNode.name,
						);
						if (name === targetNode.name && hasSourceToTarget) {
							return { [NodeConnectionTypes.Main]: [[connectionSourceMain0]] };
						}

						return {} as INodeConnections;
					},
				);
				vi.mocked(workflowDocumentStoreInstance.outgoingConnectionsByNodeName).mockImplementation(
					(name) => {
						const targetConnections = connections[targetNode.name]?.[NodeConnectionTypes.Main];
						const hasTargetToNext = targetConnections?.[0]?.some(
							(connection) => connection.node === nextNode.name,
						);
						if (name === targetNode.name && hasTargetToNext) {
							return { [NodeConnectionTypes.Main]: [[connectionNextMain0]] };
						}

						return {} as INodeConnections;
					},
				);
				vi.mocked(workflowDocumentStoreInstance.removeConnection).mockImplementation(
					({ connection }: { connection: IConnection[] }) => {
						const sourceData = connection[0];
						const destinationData = connection[1];
						if (!sourceData || !destinationData) return;
						const sourceConnections =
							connections[sourceData.node]?.[sourceData.type]?.[sourceData.index];
						if (!sourceConnections) return;

						connections[sourceData.node][sourceData.type][sourceData.index] =
							sourceConnections.filter(
								(connectionData) =>
									connectionData.node !== destinationData.node ||
									connectionData.type !== destinationData.type ||
									connectionData.index !== destinationData.index,
							);
					},
				);
				vi.mocked(workflowDocumentStoreInstance.addConnection).mockImplementation(
					({ connection }: { connection: IConnection[] }) => {
						const sourceData = connection[0];
						const destinationData = connection[1];
						if (!sourceData || !destinationData) return;
						connections[sourceData.node] = connections[sourceData.node] ?? {};
						connections[sourceData.node][sourceData.type] =
							connections[sourceData.node][sourceData.type] ?? [];
						connections[sourceData.node][sourceData.type][sourceData.index] =
							connections[sourceData.node][sourceData.type][sourceData.index] ?? [];
						connections[sourceData.node][sourceData.type][sourceData.index]?.push(destinationData);
					},
				);

				const { replaceNode } = useCanvasOperations();
				replaceNode(targetNode.id, replacementNode.id, { trackHistory: true });

				expect(replaceNodeInGroupSpy).toHaveBeenCalledWith(
					group.id,
					targetNode.id,
					replacementNode.id,
				);
				expect(group.nodeIds).toEqual([sourceNode.id, replacementNode.id, nextNode.id]);

				const groupCommand = historyStore.pushCommandToUndo.mock.calls
					.map(([command]) => command)
					.find((command) => command instanceof UpdateNodeGroupCommand) as
					| UpdateNodeGroupCommand
					| undefined;
				expect(groupCommand).toBeInstanceOf(UpdateNodeGroupCommand);
				expect(groupCommand?.before.nodeIds).toEqual([sourceNode.id, targetNode.id, nextNode.id]);
				expect(groupCommand?.after.nodeIds).toEqual([
					sourceNode.id,
					replacementNode.id,
					nextNode.id,
				]);

				expect(workflowDocumentStoreInstance.removeConnection).toHaveBeenCalledTimes(2);
				expectConnectionRemoved(sourceNode, targetNode);
				expectConnectionRemoved(targetNode, nextNode);
				expect(workflowDocumentStoreInstance.addConnection).toHaveBeenCalledTimes(2);
				expectConnectionAdded(sourceNode, replacementNode);
				expectConnectionAdded(replacementNode, nextNode);
				expect(workflowDocumentStoreInstance.removeNodeById).toHaveBeenCalledWith(targetNode.id);
				expect(toast.showToast).not.toHaveBeenCalledWith(
					expect.objectContaining({ type: 'error' }),
				);
			});
		});
		it('should not track history if flag is false', () => {
			const { replaceNode } = useCanvasOperations();
			replaceNode(targetNode.id, replacementNode.id, { trackHistory: false });

			expect(historyStore.startRecordingUndo).not.toHaveBeenCalled();
			expect(historyStore.stopRecordingUndo).not.toHaveBeenCalled();
		});
	});
	describe('openWorkflowTemplate', () => {
		let templatesStore: ReturnType<typeof mockedStore<typeof useTemplatesStore>>;
		let projectsStore: ReturnType<typeof mockedStore<typeof useProjectsStore>>;

		beforeEach(() => {
			templatesStore = mockedStore(useTemplatesStore);
			projectsStore = mockedStore(useProjectsStore);

			projectsStore.currentProjectId = 'test-project-id';
		});

		it('should open workflow template', async () => {
			const router = useRouter();
			const telemetry = useTelemetry();

			templatesStore.getFixedWorkflowTemplate = vi.fn().mockReturnValue({
				id: 'workflow-id',
				name: 'Template Name',
				workflow: { nodes: [], connections: {} },
			});

			const getNewWorkflowData = vi.mocked(workflowsApi.getNewWorkflowData);

			const { openWorkflowTemplate } = useCanvasOperations();
			await openWorkflowTemplate('template-id');

			expect(templatesStore.getFixedWorkflowTemplate).toHaveBeenCalledWith('template-id');
			expect(getNewWorkflowData).toHaveBeenCalledWith(
				expect.anything(),
				'Template Name',
				'test-project-id',
			);

			expect(telemetry.track).toHaveBeenCalledWith('User inserted workflow template', {
				source: 'workflow',
				template_id: 'template-id',
				wf_template_repo_session_id: '',
			});
			expect(router.replace).toHaveBeenCalledWith({
				name: VIEWS.NEW_WORKFLOW,
				query: { templateId: 'template-id' },
			});

			expect(
				useWorkflowDocumentStore(createWorkflowDocumentId(useWorkflowsStore().workflowId))
					.addToMeta,
			).toHaveBeenCalledWith({
				templateId: 'template-id',
			});
		});

		it('should not mark UI state as dirty on template import', async () => {
			const uiStore = mockedStore(useUIStore);

			templatesStore.getFixedWorkflowTemplate = vi.fn().mockReturnValue({
				id: 'workflow-id',
				name: 'Template Name',
				workflow: { nodes: [], connections: {} },
			});

			const { openWorkflowTemplate } = useCanvasOperations();
			await openWorkflowTemplate('template-id');

			expect(uiStore.markStateDirty).not.toHaveBeenCalled();
		});
	});

	describe('openWorkflowTempalateFromJSON', () => {
		let router: ReturnType<typeof useRouter>;
		let projectsStore: ReturnType<typeof mockedStore<typeof useProjectsStore>>;

		beforeEach(() => {
			router = useRouter();
			projectsStore = mockedStore(useProjectsStore);

			projectsStore.currentProjectId = 'test-project-id';
		});

		it('should open workflow template from JSON', async () => {
			const template: WorkflowDataWithTemplateId = {
				id: 'workflow-id',
				name: 'Template Name',
				nodes: [],
				connections: {},
				meta: { templateId: 'template-id' },
			};

			const getNewWorkflowData = vi.mocked(workflowsApi.getNewWorkflowData);

			const { openWorkflowTemplateFromJSON } = useCanvasOperations();
			await openWorkflowTemplateFromJSON(template);

			expect(getNewWorkflowData).toHaveBeenCalledWith(
				expect.anything(),
				'Template Name',
				'test-project-id',
			);

			expect(router.replace).toHaveBeenCalledWith({
				name: VIEWS.NEW_WORKFLOW,
				query: {
					templateId: 'template-id',
					projectId: 'test-project-id',
					parentFolderId: undefined,
				},
			});

			expect(
				useWorkflowDocumentStore(createWorkflowDocumentId(useWorkflowsStore().workflowId))
					.addToMeta,
			).toHaveBeenCalledWith({
				templateId: 'template-id',
			});
		});

		it('should not mark UI state as dirty on template import', async () => {
			const uiStore = mockedStore(useUIStore);

			const template: WorkflowDataWithTemplateId = {
				id: 'workflow-id',
				name: 'Template Name',
				nodes: [],
				connections: {},
				meta: { templateId: 'template-id' },
			};

			const { openWorkflowTemplateFromJSON } = useCanvasOperations();
			await openWorkflowTemplateFromJSON(template);

			expect(uiStore.markStateDirty).not.toHaveBeenCalled();
		});
	});

	describe('getNodesToShift', () => {
		it('should not shift downstream nodes unless they are to the right of the insertion point', () => {
			// Create nodes: Start -> Loop -> End -> Start (cycle)
			const nodeA = createTestNode({ id: 'A', name: 'Start', position: [0, 0] });
			const nodeB = createTestNode({ id: 'B', name: 'Loop', position: [104, 0] });
			const nodeC = createTestNode({ id: 'C', name: 'End', position: [208, 0] });

			const pinia = createTestingPinia({
				initialState: { [STORES.WORKFLOWS]: { workflowId } },
			});
			setActivePinia(pinia);

			workflowDocumentStoreInstance = useWorkflowDocumentStore(
				createWorkflowDocumentId(useWorkflowsStore().workflowId),
			) as WritableDocumentStore;
			workflowDocumentStoreInstance.allNodes = [nodeA, nodeB, nodeC];
			workflowDocumentStoreInstance.connectionsBySourceNode = {
				[nodeA.name]: {
					main: [[{ node: nodeB.name, type: NodeConnectionTypes.Main, index: 0 }]],
				},
				[nodeB.name]: {
					main: [[{ node: nodeC.name, type: NodeConnectionTypes.Main, index: 0 }]],
				},
				[nodeC.name]: {
					main: [[{ node: nodeA.name, type: NodeConnectionTypes.Main, index: 0 }]],
				},
			};
			vi.mocked(workflowDocumentStoreInstance.getConnectedNodes).mockReturnValue([]);
			vi.mocked(workflowDocumentStoreInstance.getNodeByName).mockImplementation(
				(name) => workflowDocumentStoreInstance.allNodes.find((n) => n.name === name) ?? null,
			);

			const { getNodesToShift } = useCanvasOperations();

			const { nodesToMove } = getNodesToShift([50, 0], 'Start');

			expect(nodesToMove).toHaveLength(2);
			expect(nodesToMove.find((n) => n.name === 'Start')).toBeUndefined();
		});

		it('should classify sticky notes as stretch-only when they contain the source node and insertion point is inside the sticky note', () => {
			/**
			 * Visual representation of the test scenario:
			 *
			 * Before insertion:
			 * ┌─────────────────────────────┐
			 * │  Sticky Note (50, 50)       │
			 * │                             │
			 * │    [Source]────────────────> [Target]
			 * │   (100, 100)               (400, 100)
			 * │                             │
			 * └─────────────────────────────┘
			 *
			 * Insertion point: (250, 100) - between Source and Target
			 *
			 * Expected behavior:
			 * - Sticky should ONLY stretch (not move) because it contains the source node
			 * - Source node acts as an anchor point
			 * - Target node will shift right
			 * - Sticky will expand to accommodate the new node while staying anchored
			 */
			const sourceNode = createTestNode({ id: 'source', name: 'Source', position: [100, 100] });
			const targetNode = createTestNode({ id: 'target', name: 'Target', position: [400, 100] });
			const stickyNote = createTestNode({
				id: 'sticky',
				name: 'Sticky',
				type: STICKY_NODE_TYPE,
				position: [50, 50],
				parameters: { width: 300, height: 200 },
			});

			const pinia = createTestingPinia({
				initialState: { [STORES.WORKFLOWS]: { workflowId } },
			});
			setActivePinia(pinia);

			workflowDocumentStoreInstance = useWorkflowDocumentStore(
				createWorkflowDocumentId(useWorkflowsStore().workflowId),
			) as WritableDocumentStore;
			workflowDocumentStoreInstance.allNodes = [sourceNode, targetNode, stickyNote];
			workflowDocumentStoreInstance.connectionsBySourceNode = {
				[sourceNode.name]: {
					main: [[{ node: targetNode.name, type: NodeConnectionTypes.Main, index: 0 }]],
				},
			};
			vi.mocked(workflowDocumentStoreInstance.getConnectedNodes).mockReturnValue([]);
			vi.mocked(workflowDocumentStoreInstance.getNodeByName).mockImplementation(
				(name) => workflowDocumentStoreInstance.allNodes.find((n) => n.name === name) ?? null,
			);

			const { getNodesToShift } = useCanvasOperations();
			const insertPosition: [number, number] = [250, 100];

			const { nodesToMove, stickiesToStretch, stickiesToMoveAndStretch } = getNodesToShift(
				insertPosition,
				'Source',
			);

			// Sticky containing source node should only stretch (anchored)
			expect(stickiesToStretch).toHaveLength(1);
			expect(stickiesToStretch[0].id).toBe('sticky');
			expect(stickiesToMoveAndStretch).toHaveLength(0);
			expect(nodesToMove.find((n) => n.id === 'sticky')).toBeUndefined();
		});

		it('should classify sticky notes to only move when they are far from the insertion point', () => {
			/**
			 * Visual representation:
			 *
			 * Before insertion:
			 *                                                    ┌─────────────┐
			 *                                                    │  Sticky     │
			 * [Source]───────────────────────────> [Target]      │  (600, 50)  │
			 * (100, 100)                          (500, 100)     │             │
			 *                                                    └─────────────┘
			 *
			 * Insertion point: (250, 100)
			 *
			 * Expected behavior:
			 * - Sticky is entirely to the right and far from insertion point
			 * - Sticky should ONLY move (no stretching needed)
			 * - Target node will also shift right
			 */
			const sourceNode = createTestNode({ id: 'source', name: 'Source', position: [100, 100] });
			const targetNode = createTestNode({ id: 'target', name: 'Target', position: [500, 100] });
			const stickyNote = createTestNode({
				id: 'sticky',
				name: 'Sticky',
				type: STICKY_NODE_TYPE,
				position: [600, 50],
				parameters: { width: 200, height: 200 },
			});

			const pinia = createTestingPinia({
				initialState: { [STORES.WORKFLOWS]: { workflowId } },
			});
			setActivePinia(pinia);

			workflowDocumentStoreInstance = useWorkflowDocumentStore(
				createWorkflowDocumentId(useWorkflowsStore().workflowId),
			) as WritableDocumentStore;
			workflowDocumentStoreInstance.allNodes = [sourceNode, targetNode, stickyNote];
			workflowDocumentStoreInstance.connectionsBySourceNode = {
				[sourceNode.name]: {
					main: [[{ node: targetNode.name, type: NodeConnectionTypes.Main, index: 0 }]],
				},
			};
			vi.mocked(workflowDocumentStoreInstance.getConnectedNodes).mockReturnValue([]);
			vi.mocked(workflowDocumentStoreInstance.getNodeByName).mockImplementation(
				(name) => workflowDocumentStoreInstance.allNodes.find((n) => n.name === name) ?? null,
			);

			const { getNodesToShift } = useCanvasOperations();
			const insertPosition: [number, number] = [250, 100];

			const { nodesToMove, stickiesToStretch, stickiesToMoveAndStretch } = getNodesToShift(
				insertPosition,
				'Source',
			);

			// Sticky far from insertion should only move
			expect(stickiesToMoveAndStretch).toHaveLength(0);
			expect(stickiesToStretch).toHaveLength(0);
			expect(nodesToMove).toHaveLength(2); // target + sticky
			expect(nodesToMove).toContainEqual(expect.objectContaining({ id: 'target' }));
			expect(nodesToMove).toContainEqual(expect.objectContaining({ id: 'sticky' }));
		});

		it('should not move sticky notes that do not overlap vertically with the insertion area', () => {
			/**
			 * Visual representation:
			 *
			 * [Source]──────────────────────> [Target]
			 * (100, 100)                     (400, 100)
			 *            ↑
			 *       Insertion: (250, 100)
			 *
			 *
			 *
			 *       ┌─────────────┐
			 *       │  Sticky     │
			 *       │  (300, 500) │  ← Far below, no vertical overlap
			 *       │             │
			 *       └─────────────┘
			 *
			 * Expected behavior:
			 * - Sticky is far below the insertion area (no vertical overlap)
			 * - Sticky should NOT be affected (no move, no stretch)
			 */
			const sourceNode = createTestNode({ id: 'source', name: 'Source', position: [100, 100] });
			const targetNode = createTestNode({ id: 'target', name: 'Target', position: [400, 100] });
			const stickyNote = createTestNode({
				id: 'sticky',
				name: 'Sticky',
				type: STICKY_NODE_TYPE,
				position: [300, 500],
				parameters: { width: 200, height: 200 },
			});

			const pinia = createTestingPinia({
				initialState: { [STORES.WORKFLOWS]: { workflowId } },
			});
			setActivePinia(pinia);

			workflowDocumentStoreInstance = useWorkflowDocumentStore(
				createWorkflowDocumentId(useWorkflowsStore().workflowId),
			) as WritableDocumentStore;
			workflowDocumentStoreInstance.allNodes = [sourceNode, targetNode, stickyNote];
			workflowDocumentStoreInstance.connectionsBySourceNode = {
				[sourceNode.name]: {
					main: [[{ node: targetNode.name, type: NodeConnectionTypes.Main, index: 0 }]],
				},
			};
			vi.mocked(workflowDocumentStoreInstance.getConnectedNodes).mockReturnValue([]);
			vi.mocked(workflowDocumentStoreInstance.getNodeByName).mockImplementation(
				(name) => workflowDocumentStoreInstance.allNodes.find((n) => n.name === name) ?? null,
			);

			const { getNodesToShift } = useCanvasOperations();
			const insertPosition: [number, number] = [250, 100];

			const { nodesToMove, stickiesToStretch, stickiesToMoveAndStretch } = getNodesToShift(
				insertPosition,
				'Source',
			);

			// Sticky not overlapping vertically should not be affected
			expect(stickiesToStretch).toHaveLength(0);
			expect(stickiesToMoveAndStretch).toHaveLength(0);
			expect(nodesToMove).toHaveLength(1); // only target moves
			expect(nodesToMove).toContainEqual(expect.objectContaining({ id: 'target' }));
			expect(nodesToMove.find((n) => n.id === 'sticky')).toBeUndefined();
		});

		it('should track associated nodes for sticky notes', () => {
			/**
			 * Visual representation:
			 *
			 *                                      ┌─────────┐
			 *                                      │ Sticky  │
			 * [Source]──────────────────-─────────>│[Target] │
			 * (100, 100)                           │(400,100)│
			 *                                      └─────────┘
			 *                 ↑
			 *            Insertion: (250, 100)
			 *
			 * Expected behavior:
			 * - Sticky contains the target node (center proximity check)
			 * - Target node will move, so sticky will move too
			 * - Sticky should track Target as an associated node for stretching calculations
			 */
			const sourceNode = createTestNode({ id: 'source', name: 'Source', position: [100, 100] });
			const targetNode = createTestNode({ id: 'target', name: 'Target', position: [400, 100] });
			const stickyNote = createTestNode({
				id: 'sticky',
				name: 'Sticky',
				type: STICKY_NODE_TYPE,
				position: [370, 70],
				parameters: { width: 100, height: 100 },
			});

			const pinia = createTestingPinia({
				initialState: { [STORES.WORKFLOWS]: { workflowId } },
			});
			setActivePinia(pinia);

			workflowDocumentStoreInstance = useWorkflowDocumentStore(
				createWorkflowDocumentId(useWorkflowsStore().workflowId),
			) as WritableDocumentStore;
			workflowDocumentStoreInstance.allNodes = [sourceNode, targetNode, stickyNote];
			workflowDocumentStoreInstance.connectionsBySourceNode = {
				[sourceNode.name]: {
					main: [[{ node: targetNode.name, type: NodeConnectionTypes.Main, index: 0 }]],
				},
			};
			vi.mocked(workflowDocumentStoreInstance.getConnectedNodes).mockReturnValue([]);
			vi.mocked(workflowDocumentStoreInstance.getNodeByName).mockImplementation(
				(name) => workflowDocumentStoreInstance.allNodes.find((n) => n.name === name) ?? null,
			);

			const { getNodesToShift } = useCanvasOperations();
			const insertPosition: [number, number] = [250, 100];

			const { nodesToMove, stickyAssociatedNodes } = getNodesToShift(insertPosition, 'Source');

			// Should track the target node as associated with the sticky
			expect(nodesToMove).toHaveLength(2); // target + sticky
			expect(nodesToMove).toContainEqual(expect.objectContaining({ id: 'target' }));
			expect(nodesToMove).toContainEqual(expect.objectContaining({ id: 'sticky' }));

			const associatedNodes = stickyAssociatedNodes.get('sticky');
			expect(associatedNodes).toBeDefined();
			expect(associatedNodes).toHaveLength(1);
			expect(associatedNodes?.[0].id).toBe('target');
		});

		it('should handle multiple sticky notes with different behaviors', () => {
			/**
			 * Visual representation:
			 *
			 * ┌──────────────────────┐            ┌───────┐              ┌────────────┐
			 * │  Sticky-Anchor       │            │Sticky │              │Sticky-Move │
			 * │                      │            │[Tgt]  │              │            │
			 * │    [Source]──────────────────────>│(400)  │              │  (600, 50) │
			 * │   (100, 100)         │            └───────┘              │            │
			 * │                      │                                   └────────────┘
			 * └──────────────────────┘
			 *              ↑
			 *         Insertion: (250, 100)
			 *
			 * Expected behavior:
			 * - Sticky-Anchor: Contains source node → ONLY stretch (anchored)
			 * - Sticky-WithTarget: Contains target node that will move → move + track association
			 * - Sticky-Move: Far to the right → ONLY move
			 */
			const sourceNode = createTestNode({ id: 'source', name: 'Source', position: [100, 100] });
			const targetNode = createTestNode({ id: 'target', name: 'Target', position: [400, 100] });
			const stickyAnchor = createTestNode({
				id: 'sticky-anchor',
				name: 'StickyAnchor',
				type: STICKY_NODE_TYPE,
				position: [50, 50],
				parameters: { width: 300, height: 200 },
			});
			const stickyWithTarget = createTestNode({
				id: 'sticky-with-target',
				name: 'StickyWithTarget',
				type: STICKY_NODE_TYPE,
				position: [370, 70],
				parameters: { width: 100, height: 100 },
			});
			const stickyMove = createTestNode({
				id: 'sticky-move',
				name: 'StickyMove',
				type: STICKY_NODE_TYPE,
				position: [600, 50],
				parameters: { width: 200, height: 200 },
			});

			const pinia = createTestingPinia({
				initialState: { [STORES.WORKFLOWS]: { workflowId } },
			});
			setActivePinia(pinia);

			workflowDocumentStoreInstance = useWorkflowDocumentStore(
				createWorkflowDocumentId(useWorkflowsStore().workflowId),
			) as WritableDocumentStore;
			workflowDocumentStoreInstance.allNodes = [
				sourceNode,
				targetNode,
				stickyAnchor,
				stickyWithTarget,
				stickyMove,
			];
			workflowDocumentStoreInstance.connectionsBySourceNode = {
				[sourceNode.name]: {
					main: [[{ node: targetNode.name, type: NodeConnectionTypes.Main, index: 0 }]],
				},
			};
			vi.mocked(workflowDocumentStoreInstance.getConnectedNodes).mockReturnValue([]);
			vi.mocked(workflowDocumentStoreInstance.getNodeByName).mockImplementation(
				(name) => workflowDocumentStoreInstance.allNodes.find((n) => n.name === name) ?? null,
			);

			const { getNodesToShift } = useCanvasOperations();
			const insertPosition: [number, number] = [250, 100];

			const { nodesToMove, stickiesToStretch, stickyAssociatedNodes } = getNodesToShift(
				insertPosition,
				'Source',
			);

			// Sticky containing source should only stretch
			expect(stickiesToStretch).toHaveLength(1);
			expect(stickiesToStretch).toContainEqual(expect.objectContaining({ id: 'sticky-anchor' }));

			// Sticky containing target should move (and track associated nodes)
			expect(nodesToMove).toHaveLength(3); // target + sticky-with-target + sticky-move
			expect(nodesToMove).toContainEqual(expect.objectContaining({ id: 'target' }));
			expect(nodesToMove).toContainEqual(expect.objectContaining({ id: 'sticky-with-target' }));
			expect(stickyAssociatedNodes.get('sticky-with-target')).toHaveLength(1);

			// Sticky far to the right should only move
			expect(nodesToMove).toContainEqual(expect.objectContaining({ id: 'sticky-move' }));
		});

		it('should handle sticky notes with nodes at the center boundary', () => {
			/**
			 * Visual representation:
			 *
			 *            ┌────────────────────┐
			 *            │  Sticky (200, 50)  │
			 * [Source]───│───────────────────────────> [Target]
			 * (100, 100) │        ↑           │       (400, 100)
			 *            │   Insertion point  │
			 *            │    (250, 100)      │
			 *            └────────────────────┘
			 *
			 * Expected behavior:
			 * - Sticky overlaps the insertion point (inside its bounds)
			 * - Sticky does NOT contain source node
			 * - Sticky should ONLY stretch to accommodate the new node
			 */
			const sourceNode = createTestNode({ id: 'source', name: 'Source', position: [100, 100] });
			const targetNode = createTestNode({ id: 'target', name: 'Target', position: [400, 100] });
			const stickyNote = createTestNode({
				id: 'sticky',
				name: 'Sticky',
				type: STICKY_NODE_TYPE,
				position: [200, 50],
				parameters: { width: 200, height: 200 },
			});

			const pinia = createTestingPinia({
				initialState: { [STORES.WORKFLOWS]: { workflowId } },
			});
			setActivePinia(pinia);

			workflowDocumentStoreInstance = useWorkflowDocumentStore(
				createWorkflowDocumentId(useWorkflowsStore().workflowId),
			) as WritableDocumentStore;
			workflowDocumentStoreInstance.allNodes = [sourceNode, targetNode, stickyNote];
			workflowDocumentStoreInstance.connectionsBySourceNode = {
				[sourceNode.name]: {
					main: [[{ node: targetNode.name, type: NodeConnectionTypes.Main, index: 0 }]],
				},
			};
			vi.mocked(workflowDocumentStoreInstance.getConnectedNodes).mockReturnValue([]);
			vi.mocked(workflowDocumentStoreInstance.getNodeByName).mockImplementation(
				(name) => workflowDocumentStoreInstance.allNodes.find((n) => n.name === name) ?? null,
			);

			const { getNodesToShift } = useCanvasOperations();
			const insertPosition: [number, number] = [250, 100];

			const { stickiesToStretch } = getNodesToShift(insertPosition, 'Source');

			// Sticky overlapping insertion point should stretch
			expect(stickiesToStretch).toHaveLength(1);
			expect(stickiesToStretch[0].id).toBe('sticky');
		});

		it('should track multiple associated nodes for sticky stretching', () => {
			/**
			 * Visual representation:
			 *
			 *                                     ┌──────────┐
			 *                                     │  Sticky  │
			 *                           ┌────────>│[Target1] │
			 *                           │         │ (400,80) │
			 * [Source]──────────────────┤         │          │
			 * (100, 100)                │         │[Target2] │
			 *                           └────────>│(400,120) │
			 *                                     └──────────┘
			 *                 ↑
			 *            Insertion: (250, 100)
			 *
			 * Expected behavior:
			 * - Sticky contains BOTH target nodes
			 * - Both target nodes will move
			 * - Sticky should track BOTH targets as associated nodes
			 * - This ensures proper stretching to encompass all associated nodes
			 */
			const sourceNode = createTestNode({
				id: 'source',
				name: 'Source',
				position: [100, 100],
			});

			const targetNode1 = createTestNode({
				id: 'target1',
				name: 'Target1',
				position: [400, 80],
			});

			const targetNode2 = createTestNode({
				id: 'target2',
				name: 'Target2',
				position: [400, 120],
			});

			const stickyNote = createTestNode({
				id: 'sticky',
				name: 'Sticky',
				type: STICKY_NODE_TYPE,
				position: [370, 50],
				parameters: { width: 100, height: 150 },
			});

			const pinia = createTestingPinia({
				initialState: { [STORES.WORKFLOWS]: { workflowId } },
			});
			setActivePinia(pinia);

			workflowDocumentStoreInstance = useWorkflowDocumentStore(
				createWorkflowDocumentId(useWorkflowsStore().workflowId),
			) as WritableDocumentStore;
			workflowDocumentStoreInstance.allNodes = [sourceNode, targetNode1, targetNode2, stickyNote];
			workflowDocumentStoreInstance.connectionsBySourceNode = {
				[sourceNode.name]: {
					main: [
						[
							{ node: targetNode1.name, type: NodeConnectionTypes.Main, index: 0 },
							{ node: targetNode2.name, type: NodeConnectionTypes.Main, index: 0 },
						],
					],
				},
			};
			vi.mocked(workflowDocumentStoreInstance.getConnectedNodes).mockReturnValue([]);
			vi.mocked(workflowDocumentStoreInstance.getNodeByName).mockImplementation(
				(name) => workflowDocumentStoreInstance.allNodes.find((n) => n.name === name) ?? null,
			);

			const { getNodesToShift } = useCanvasOperations();
			const insertPosition: [number, number] = [250, 100];

			const { nodesToMove, stickyAssociatedNodes } = getNodesToShift(insertPosition, 'Source');

			// Should track both target nodes as associated with the sticky
			expect(nodesToMove).toHaveLength(3); // target1 + target2 + sticky
			expect(nodesToMove).toContainEqual(expect.objectContaining({ id: 'target1' }));
			expect(nodesToMove).toContainEqual(expect.objectContaining({ id: 'target2' }));
			expect(nodesToMove).toContainEqual(expect.objectContaining({ id: 'sticky' }));

			const associatedNodes = stickyAssociatedNodes.get('sticky');
			expect(associatedNodes).toBeDefined();
			expect(associatedNodes).toHaveLength(2);
			expect(associatedNodes?.map((n) => n.id).sort()).toEqual(['target1', 'target2']);
		});
	});

	describe('createConnectionToLastInteractedWithNode - HITL node handling', () => {
		it('should create HITL node connection pattern when adding HITL tool node with existing connection', async () => {
			const uiStore = mockedStore(useUIStore);
			const nodeTypesStore = mockedStore(useNodeTypesStore);

			// Setup: Agent -> Tool connection
			const agentNode = createTestNode({
				id: 'agent',
				name: 'Agent',
				type: AGENT_NODE_TYPE,
				position: [100, 100],
			});

			const toolNode = createTestNode({
				id: 'tool',
				name: 'Tool',
				type: 'n8n-nodes-base.calculator',
				position: [300, 300],
			});

			const hitlNode = createTestNode({
				id: 'hitl',
				name: 'HITL',
				type: 'n8n-nodes-base.manualChatTriggerHitlTool',
			});

			const agentNodeTypeDescription = mockNodeTypeDescription({
				name: AGENT_NODE_TYPE,
				inputs: [NodeConnectionTypes.Main],
				outputs: [NodeConnectionTypes.AiTool],
			});
			const toolNodeTypeDescription = mockNodeTypeDescription({
				name: 'n8n-nodes-base.calculator',
				inputs: [NodeConnectionTypes.AiTool],
				outputs: [NodeConnectionTypes.Main],
			});
			const hitlNodeTypeDescription = mockNodeTypeDescription({
				name: 'n8n-nodes-base.manualChatTriggerHitlTool',
				inputs: [NodeConnectionTypes.AiTool],
				outputs: [NodeConnectionTypes.AiTool],
			});

			nodeTypesStore.getNodeType = vi.fn((type: string) => {
				if (type === AGENT_NODE_TYPE) return agentNodeTypeDescription;
				if (type === 'n8n-nodes-base.calculator') return toolNodeTypeDescription;
				if (type === 'n8n-nodes-base.manualChatTriggerHitlTool') return hitlNodeTypeDescription;
				return null;
			});

			workflowDocumentStoreInstance.allNodes = [agentNode, toolNode];
			workflowDocumentStoreInstance.connectionsBySourceNode = {
				[agentNode.name]: {
					[NodeConnectionTypes.AiTool]: [
						[{ node: toolNode.name, type: NodeConnectionTypes.AiTool, index: 0 }],
					],
				},
			};

			vi.spyOn(workflowDocumentStoreInstance, 'getNodeById').mockImplementation((id: string) => {
				if (id === 'agent') return agentNode;
				if (id === 'tool') return toolNode;
				if (id === 'hitl') return hitlNode;
				return undefined;
			});
			vi.spyOn(workflowDocumentStoreInstance, 'getNodeByName').mockImplementation(
				(name: string) => {
					if (name === 'Agent') return agentNode;
					if (name === 'Tool') return toolNode;
					if (name === 'HITL') return hitlNode;
					return null;
				},
			);

			// Mock the last interacted node as the tool node with an existing connection
			uiStore.lastInteractedWithNodeId = toolNode.id;
			uiStore.lastInteractedWithNodeConnection = {
				source: agentNode.id,
				target: toolNode.id,
				sourceHandle: `outputs/${NodeConnectionTypes.AiTool}/0`,
				targetHandle: `inputs/${NodeConnectionTypes.AiTool}/0`,
			};
			uiStore.lastInteractedWithNodeHandle = `outputs/${NodeConnectionTypes.AiTool}/0`;

			const { addNode } = useCanvasOperations();
			const workflowDocumentStore = useWorkflowDocumentStore(createWorkflowDocumentId(workflowId));
			const addConnectionSpy = vi.spyOn(workflowDocumentStore, 'addConnection');
			const removeConnectionSpy = vi.spyOn(workflowDocumentStore, 'removeConnection');

			// Act: Add HITL node
			await nextTick();
			addNode(hitlNode, hitlNodeTypeDescription, { isAutoAdd: false });
			await nextTick();

			// Assert: Should create 2 connections:
			// 1. Connection from HITL to Tool (original target)
			// 2. Connection from Agent to HITL (new)
			// And delete the original connection from Agent to Tool
			expect(addConnectionSpy).toHaveBeenCalledTimes(2);
			expect(removeConnectionSpy).toHaveBeenCalledTimes(1);
		});
	});

	describe('createConnectionToLastInteractedWithNode - grouped edge insertion', () => {
		it('should replace the grouped edge and extend the group when inserting a node between grouped nodes', () => {
			const uiStore = mockedStore(useUIStore);
			const toast = useToast();
			const nodeTypeDescription = createGroupedNodeTypeDescription();
			const sourceNode = createGroupedNode('source', 'Source', [0, 0]);
			const targetNode = createGroupedNode('target', 'Target', [500, 0]);
			const insertedNode = createGroupedNode('inserted', 'Inserted', [250, 0]);
			const group = {
				id: 'group',
				nodeIds: [sourceNode.id, targetNode.id],
				name: 'Group 1',
			};

			const { workflowDocumentStore } = setupGroupedCanvas({
				nodes: [sourceNode, targetNode],
				lookupNodes: [sourceNode, targetNode, insertedNode],
				connections: createConnectionsBySource(workflowConnection(sourceNode, targetNode)),
				groups: [group],
				nodeTypeDescription,
			});
			const addNodesToGroupSpy = vi.spyOn(workflowDocumentStore, 'addNodesToGroup');

			uiStore.lastInteractedWithNodeId = sourceNode.id;
			uiStore.lastInteractedWithNodeConnection = canvasConnection(sourceNode, targetNode);
			uiStore.lastInteractedWithNodeHandle = `outputs/${NodeConnectionTypes.Main}/0`;

			const { addNode } = useCanvasOperations();
			addNode(insertedNode, nodeTypeDescription, { isAutoAdd: false });

			expect(addNodesToGroupSpy).toHaveBeenCalledWith(group.id, [insertedNode.id]);
			expect(workflowDocumentStoreInstance.removeConnection).toHaveBeenCalledTimes(1);
			expectConnectionRemoved(sourceNode, targetNode);
			expect(workflowDocumentStoreInstance.addConnection).toHaveBeenCalledTimes(2);
			expectConnectionAdded(sourceNode, insertedNode);
			expectConnectionAdded(insertedNode, targetNode);
			expect(toast.showToast).toHaveBeenCalledWith(
				expect.objectContaining({
					title: "'Group 1' extended",
					type: 'info',
				}),
			);
			expect(toast.showToast).not.toHaveBeenCalledWith(
				expect.objectContaining({ title: 'Connection not removed' }),
			);
		});
	});

	describe('tryToOpenSubworkflowInNewTab', () => {
		let windowOpenSpy: ReturnType<typeof vi.spyOn>;

		beforeEach(() => {
			windowOpenSpy = vi.spyOn(window, 'open').mockImplementation(() => null);
		});

		it('should open sub-workflow with static ID', async () => {
			const node = createTestNode({
				type: 'n8n-nodes-base.executeWorkflow',
				parameters: {
					workflowId: {
						__rl: true,
						mode: 'id',
						value: 'workflow-123',
					},
				},
			});

			vi.spyOn(workflowDocumentStoreInstance, 'getNodeById').mockReturnValue(node);
			const { tryToOpenSubworkflowInNewTab } = useCanvasOperations();

			const result = await tryToOpenSubworkflowInNewTab(node.id);

			expect(result).toBe(true);
			expect(windowOpenSpy).toHaveBeenCalledWith(
				expect.stringContaining('workflow/workflow-123'),
				'_blank',
			);
		});

		it('should resolve expression before opening sub-workflow', async () => {
			const mockResolveExpression = vi.fn().mockResolvedValue('resolved-workflow-id');
			vi.spyOn(workflowHelpersModule, 'useWorkflowHelpers').mockReturnValue({
				resolveExpression: mockResolveExpression,
			} as unknown as ReturnType<typeof workflowHelpersModule.useWorkflowHelpers>);

			const node = createTestNode({
				type: 'n8n-nodes-base.executeWorkflow',
				parameters: {
					workflowId: {
						__rl: true,
						mode: 'id',
						value: '={{ $workflow.id }}',
					},
				},
			});

			vi.spyOn(workflowDocumentStoreInstance, 'getNodeById').mockReturnValue(node);
			const { tryToOpenSubworkflowInNewTab } = useCanvasOperations();

			const result = await tryToOpenSubworkflowInNewTab(node.id);

			expect(result).toBe(true);
			expect(mockResolveExpression).toHaveBeenCalledWith(
				'={{ $workflow.id }}',
				{},
				{ contextNodeName: node.name },
			);
			expect(windowOpenSpy).toHaveBeenCalledWith(
				expect.stringContaining('workflow/resolved-workflow-id'),
				'_blank',
			);
		});

		it('should return false when expression resolves to non-string', async () => {
			const mockResolveExpression = vi.fn().mockResolvedValue(null);
			vi.spyOn(workflowHelpersModule, 'useWorkflowHelpers').mockReturnValue({
				resolveExpression: mockResolveExpression,
			} as unknown as ReturnType<typeof workflowHelpersModule.useWorkflowHelpers>);

			const node = createTestNode({
				type: 'n8n-nodes-base.executeWorkflow',
				parameters: {
					workflowId: {
						__rl: true,
						mode: 'id',
						value: '={{ $workflow.id }}',
					},
				},
			});

			vi.spyOn(workflowDocumentStoreInstance, 'getNodeById').mockReturnValue(node);
			const { tryToOpenSubworkflowInNewTab } = useCanvasOperations();

			const result = await tryToOpenSubworkflowInNewTab(node.id);

			expect(result).toBe(false);
			expect(windowOpenSpy).not.toHaveBeenCalled();
		});

		it('should return false when expression resolution throws', async () => {
			const mockResolveExpression = vi.fn().mockRejectedValue(new Error('Invalid expression'));
			vi.spyOn(workflowHelpersModule, 'useWorkflowHelpers').mockReturnValue({
				resolveExpression: mockResolveExpression,
			} as unknown as ReturnType<typeof workflowHelpersModule.useWorkflowHelpers>);

			const node = createTestNode({
				type: 'n8n-nodes-base.executeWorkflow',
				parameters: {
					workflowId: {
						__rl: true,
						mode: 'id',
						value: '={{ $invalid.expression }}',
					},
				},
			});

			vi.spyOn(workflowDocumentStoreInstance, 'getNodeById').mockReturnValue(node);
			const { tryToOpenSubworkflowInNewTab } = useCanvasOperations();

			const result = await tryToOpenSubworkflowInNewTab(node.id);

			expect(result).toBe(false);
			expect(windowOpenSpy).not.toHaveBeenCalled();
		});

		it('should return false when node does not exist', async () => {
			const { tryToOpenSubworkflowInNewTab } = useCanvasOperations();

			const result = await tryToOpenSubworkflowInNewTab('non-existent-id');

			expect(result).toBe(false);
			expect(windowOpenSpy).not.toHaveBeenCalled();
		});
	});
});

function buildImportNodes() {
	return [
		mockNode({ id: '1', name: 'Node 1', type: SET_NODE_TYPE }),
		mockNode({ id: '2', name: 'Node 2', type: SET_NODE_TYPE }),
	].map((node) => {
		// Setting position in mockNode will wrap it in a Proxy
		// This causes deepCopy to remove position -> set position after instead
		node.position = [40, 40];
		node.credentials = undefined;
		return node;
	});
}
