/**
 * Test file for New Simplified SDK API
 *
 * This file tests the new simplified API design that:
 * 1. Is easier for LLMs to understand and generate
 * 2. Reduces implementation complexity
 * 3. Provides better developer ergonomics
 *
 * TDD: All tests are written FIRST before implementing the features.
 * Each test should FAIL initially until the corresponding feature is implemented.
 */

import type { NodeInstance } from './types/base';
import { workflow } from './workflow-builder';
import { nextBatch } from './workflow-builder/control-flow-builders/next-batch';
import { splitInBatches } from './workflow-builder/control-flow-builders/split-in-batches';
import { node, trigger } from './workflow-builder/node-builders/node-builder';
import { languageModel, tool } from './workflow-builder/node-builders/subnode-builders';

/**
 * Helper to create a simple node with a name
 */
function createNode(name: string, type = 'n8n-nodes-base.set') {
	return node({
		type,
		version: 3.4,
		config: { name },
	});
}

/**
 * Helper to create a trigger node
 */
function createTrigger(name: string) {
	return trigger({
		type: 'n8n-nodes-base.manualTrigger',
		version: 1,
		config: { name },
	});
}

/**
 * Helper to create an IF node
 */
function createIfNode(name: string) {
	return node({
		type: 'n8n-nodes-base.if',
		version: 2.2,
		config: {
			name,
			parameters: {
				conditions: {
					conditions: [
						{
							leftValue: '={{ $json.value }}',
							operator: { type: 'boolean', operation: 'true' },
						},
					],
				},
			},
		},
	}) as NodeInstance<'n8n-nodes-base.if', string, unknown>;
}

/**
 * Helper to create a Switch node
 */
function createSwitchNode(name: string) {
	return node({
		type: 'n8n-nodes-base.switch',
		version: 3.2,
		config: {
			name,
			parameters: { mode: 'rules' },
		},
	}) as NodeInstance<'n8n-nodes-base.switch', string, unknown>;
}

/**
 * Helper to create a Merge node
 */
function createMergeNode(name: string) {
	return node({
		type: 'n8n-nodes-base.merge',
		version: 3,
		config: {
			name,
			parameters: { mode: 'combine' },
		},
	});
}

/**
 * Helper to create a SplitInBatches node
 */
function createSplitInBatchesNode(name: string) {
	return node({
		type: 'n8n-nodes-base.splitInBatches',
		version: 3,
		config: {
			name,
			parameters: { batchSize: 10 },
		},
	}) as NodeInstance<'n8n-nodes-base.splitInBatches', string, unknown>;
}

describe('New SDK API', () => {
	describe('Edge Case 1: Terminal vs Chainable Targets', () => {
		it('allows chaining after .output(n)', () => {
			const nodeA = createNode('A');
			const nodeB = createNode('B');

			// .output(n).to() should return a chainable target
			// This tests that .output(n) is NOT terminal - you can continue chaining
			const chain = nodeA.to(nodeB, 1); // outputIndex = 1

			// Verify chain is defined and chainable
			expect(chain).toBeDefined();
			expect(chain.head).toBe(nodeA);
			expect(chain.tail).toBe(nodeB);
		});

		it('connects to specific output index via .to(target, outputIndex)', () => {
			const nodeA = createNode('A');
			const nodeB = createNode('B');
			const t = createTrigger('Start');

			const wf = workflow('test', 'Test').add(t.to(nodeA).to(nodeB, 1)); // Connect A's output 1 to B

			const json = wf.toJSON();

			// Verify A connects to B via output 1
			const connA = json.connections['A'];
			expect(connA).toBeDefined();
			// Output 1 should have the connection
			expect(connA.main[1]).toBeDefined();
			expect(connA.main[1]![0].node).toBe('B');
		});

		it('supports .input(n) for connecting to specific input indices (future API)', () => {
			// NOTE: This test documents the desired NEW API behavior
			// The current API doesn't have .input(n) method - it needs to be added

			const nodeA = createNode('A');
			const mergeNode = createMergeNode('Merge');
			const t = createTrigger('Start');

			// Use .input(n) to specify target input index
			const wf = workflow('test', 'Test').add(t.to(nodeA.to(mergeNode.input(1)))); // Connect to input 1 of merge

			const json = wf.toJSON();

			// Verify connection exists with correct target input index
			const connA = json.connections['A'];
			expect(connA).toBeDefined();
			expect(connA.main[0]![0].node).toBe('Merge');
			expect(connA.main[0]![0].index).toBe(1); // Input index 1
		});

		it('returns terminal InputTarget that cannot be chained', () => {
			const mergeNode = createMergeNode('Merge');

			// .input(n) returns InputTarget which should NOT have .to()
			const inputTarget = mergeNode.input(0);

			expect(inputTarget).toHaveProperty('_isInputTarget', true);
			expect(inputTarget).toHaveProperty('node', mergeNode);
			expect(inputTarget).toHaveProperty('inputIndex', 0);
			// InputTarget should not be chainable (no .then method)
			expect(typeof (inputTarget as unknown as { then?: unknown }).then).toBe('undefined');
		});
	});

	describe('Edge Case 2: Fan-out Within Branches', () => {
		it('supports plain array inside onTrue()', () => {
			const nodeA = createNode('A');
			const nodeB = createNode('B');
			const ifNode = createIfNode('IF');
			const t = createTrigger('Start');

			const wf = workflow('test', 'Test')
				.add(t)
				.to(
					ifNode.onTrue!([nodeA, nodeB]), // IF output 0 (true) -> both A and B
				);

			const json = wf.toJSON();

			// Verify IF output 0 connects to both A and B
			const ifConns = json.connections['IF'];
			expect(ifConns).toBeDefined();
			expect(ifConns.main[0]).toBeDefined();
			expect(ifConns.main[0]).toHaveLength(2);

			const targetNodes = ifConns.main[0]!.map((c) => c.node).sort();
			expect(targetNodes).toEqual(['A', 'B']);
		});

		it('supports plain array inside onCase()', () => {
			const nodeA = createNode('A');
			const nodeB = createNode('B');
			const nodeC = createNode('C');
			const switchNode = createSwitchNode('Router');
			const t = createTrigger('Start');

			const wf = workflow('test', 'Test')
				.add(t)
				.to(switchNode.onCase!(0, [nodeA, nodeB]).onCase(1, nodeC));

			const json = wf.toJSON();

			// Verify switch output 0 connects to both A and B
			const switchConns = json.connections['Router'];
			expect(switchConns).toBeDefined();
			expect(switchConns.main[0]).toHaveLength(2);
			expect(switchConns.main[0]!.map((c) => c.node).sort()).toEqual(['A', 'B']);

			// Verify switch output 1 connects to C
			expect(switchConns.main[1]).toHaveLength(1);
			expect(switchConns.main[1]![0].node).toBe('C');
		});
	});

	describe('Edge Case 3: Builders Are Terminal', () => {
		it('allows nesting builders inside .to() chains', () => {
			// Fluent builders (ifNode.onTrue().onFalse(), switchNode.onCase()) can be passed to .to()
			// and internally handle their branch nodes

			const nodeA = createNode('A');
			const nodeB = createNode('B');
			const ifNode = createIfNode('IF');
			const t = createTrigger('Start');

			// trigger -> IF -> (true: A, false: B)
			const wf = workflow('test', 'Test').add(t).to(ifNode.onTrue!(nodeA).onFalse(nodeB));

			const json = wf.toJSON();

			// All nodes should be present
			expect(json.nodes).toHaveLength(4); // Start, IF, A, B

			// Trigger connects to IF
			expect(json.connections['Start'].main[0]![0].node).toBe('IF');

			// IF connects to branches
			expect(json.connections['IF'].main[0]![0].node).toBe('A');
			expect(json.connections['IF'].main[1]![0].node).toBe('B');
		});
	});

	describe('Edge Case 4: Branch Convergence', () => {
		it('deduplicates same node in multiple branches', () => {
			const convergence = createNode('Convergence');
			const nodeA = createNode('A');
			const nodeB = createNode('B');
			const ifNode = createIfNode('IF');
			const t = createTrigger('Start');

			// Both branches converge to the same node instance
			const wf = workflow('test', 'Test')
				.add(t)
				.to(
					ifNode.onTrue!(nodeA.to(convergence)).onFalse(nodeB.to(convergence)), // Same instance
				);

			const json = wf.toJSON();

			// Should have 5 nodes: Start, IF, A, B, Convergence (not 6)
			expect(json.nodes).toHaveLength(5);

			// Convergence should only appear once
			const convergenceNodes = json.nodes.filter((n) => n.name === 'Convergence');
			expect(convergenceNodes).toHaveLength(1);

			// Both branches should connect to the same Convergence node
			expect(json.connections['A'].main[0]![0].node).toBe('Convergence');
			expect(json.connections['B'].main[0]![0].node).toBe('Convergence');
		});
	});

	describe('Edge Case 5: Cycle/Loop Connections', () => {
		it('allows node to connect back to earlier node', () => {
			const checkStatus = node({
				type: 'n8n-nodes-base.httpRequest',
				version: 4.2,
				config: { name: 'Check' },
			});
			const wait = node({
				type: 'n8n-nodes-base.wait',
				version: 1,
				config: { name: 'Wait' },
			});
			const getResult = createNode('Result');
			const jobComplete = createIfNode('Done?');
			const t = createTrigger('Start');

			// Start -> Check -> Done? -> (true: Result, false: Wait -> Check)
			// Use workflow.to() with fluent builder
			const wf = workflow('test', 'Test')
				.add(t.to(checkStatus))
				.to(
					jobComplete.onTrue!(getResult).onFalse(wait.to(checkStatus)), // Loop back
				);

			const json = wf.toJSON();

			// Verify Wait connects back to Check
			expect(json.connections['Wait']).toBeDefined();
			expect(json.connections['Wait'].main[0]![0].node).toBe('Check');
		});
	});

	describe('Edge Case 6: Sparse Switch Cases', () => {
		it('only emits configured cases, skip indices', () => {
			const nodeA = createNode('A');
			const nodeB = createNode('B');
			const switchNode = createSwitchNode('Router');
			const t = createTrigger('Start');

			// Only case0 and case3, skip 1 and 2
			const wf = workflow('test', 'Test').add(t).to(
				switchNode.onCase!(0, nodeA).onCase(3, nodeB), // Skip 1, 2
			);

			const json = wf.toJSON();

			const switchConns = json.connections['Router'].main;

			// Output 0 connects to A
			expect(switchConns[0]).toBeDefined();
			expect(switchConns[0]![0].node).toBe('A');

			// Outputs 1, 2 should be undefined or empty
			expect(switchConns[1] ?? []).toHaveLength(0);
			expect(switchConns[2] ?? []).toHaveLength(0);

			// Output 3 connects to B
			expect(switchConns[3]).toBeDefined();
			expect(switchConns[3]![0].node).toBe('B');
		});
	});

	describe('Edge Case 7: Single-Branch IF', () => {
		it('works with only onTrue()', () => {
			const nodeA = createNode('A');
			const ifNode = createIfNode('IF');
			const t = createTrigger('Start');

			const wf = workflow('test', 'Test').add(t).to(ifNode.onTrue!(nodeA));

			const json = wf.toJSON();

			// IF should only have output 0 connected
			expect(json.connections['IF'].main[0]![0].node).toBe('A');
			// Output 1 should have no connections
			expect(json.connections['IF'].main[1] ?? []).toHaveLength(0);
		});

		it('works with only onFalse()', () => {
			const nodeB = createNode('B');
			const ifNode = createIfNode('IF');
			const t = createTrigger('Start');

			const wf = workflow('test', 'Test').add(t).to(ifNode.onFalse!(nodeB));

			const json = wf.toJSON();

			// IF output 0 should have no connections
			expect(json.connections['IF'].main[0] ?? []).toHaveLength(0);
			// Output 1 should be connected
			expect(json.connections['IF'].main[1]![0].node).toBe('B');
		});
	});

	describe('Edge Case 8: Multiple Triggers to Shared Nodes', () => {
		it('preserves all trigger connections to shared node', () => {
			const shared = createNode('Shared');
			const manual = createTrigger('Manual');
			const schedule = trigger({
				type: 'n8n-nodes-base.scheduleTrigger',
				version: 1.1,
				config: { name: 'Schedule' },
			});

			const wf = workflow('test', 'Test').add(manual.to(shared)).add(schedule.to(shared));

			const json = wf.toJSON();

			// Both triggers should connect to Shared
			expect(json.connections['Manual'].main[0]![0].node).toBe('Shared');
			expect(json.connections['Schedule'].main[0]![0].node).toBe('Shared');

			// Shared should only exist once
			const sharedNodes = json.nodes.filter((n) => n.name === 'Shared');
			expect(sharedNodes).toHaveLength(1);
		});
	});

	describe('Edge Case 9: Multi-Output Nodes', () => {
		it('supports .to(target, outputIndex) on any node type', () => {
			// Text classifier has multiple outputs based on classification
			const classifier = node({
				type: '@n8n/n8n-nodes-langchain.textClassifier',
				version: 1.2,
				config: { name: 'Classifier' },
			});
			const categoryA = createNode('Category A');
			const categoryB = createNode('Category B');
			const categoryC = createNode('Category C');
			const t = createTrigger('Start');

			const wf = workflow('test', 'Test')
				.add(t.to(classifier))
				.add(classifier.to(categoryA, 0)) // Output 0 -> Category A
				.add(classifier.to(categoryB, 1)) // Output 1 -> Category B
				.add(classifier.to(categoryC, 2)); // Output 2 -> Category C

			const json = wf.toJSON();

			const classifierConns = json.connections['Classifier'].main;
			expect(classifierConns[0]![0].node).toBe('Category A');
			expect(classifierConns[1]![0].node).toBe('Category B');
			expect(classifierConns[2]![0].node).toBe('Category C');
		});
	});

	describe('Edge Case 10: SplitInBatches to Different Merge Inputs', () => {
		it('connects done/each to different merge inputs', () => {
			const mergeNode = createMergeNode('Merge');
			const sibNode = createSplitInBatchesNode('Batch');
			const processNode = createNode('Process');
			const summaryNode = createNode('Summary');
			const finalNode = createNode('Final');
			const t = createTrigger('Start');

			// Using named object syntax with explicit connections
			const wf = workflow('test', 'Test')
				.add(t)
				.to(
					splitInBatches(sibNode, {
						done: summaryNode, // Done output (0) -> Summary
						each: processNode, // Each output (1) -> Process
					}),
				)
				// Connect summary to merge input 0
				.add(summaryNode)
				.add(mergeNode)
				.connect(summaryNode, 0, mergeNode, 0)
				// Connect process to merge input 1
				.connect(processNode, 0, mergeNode, 1)
				// Merge to final
				.add(finalNode)
				.connect(mergeNode, 0, finalNode, 0);

			const json = wf.toJSON();

			// Verify SIB connections
			const sibConns = json.connections['Batch'].main;
			expect(sibConns[0]![0].node).toBe('Summary'); // done -> Summary
			expect(sibConns[1]![0].node).toBe('Process'); // each -> Process

			// Verify merge input connections
			const summaryConn = json.connections['Summary'].main[0]![0];
			expect(summaryConn.node).toBe('Merge');
			expect(summaryConn.index).toBe(0);

			const processConn = json.connections['Process'].main[0]![0];
			expect(processConn.node).toBe('Merge');
			expect(processConn.index).toBe(1);
		});
	});

	describe('Edge Case 11: Fan-out Then Merge', () => {
		it('supports fan-out targets connecting to merge inputs', () => {
			const mergeNode = createMergeNode('Merge');
			const nodeA = createNode('A');
			const nodeB = createNode('B');
			const finalNode = createNode('Final');
			const t = createTrigger('Start');

			// Fan out from trigger, then merge
			// Use array syntax for fan-out since that's well-typed
			const wf = workflow('test', 'Test')
				.add(t.to([nodeA, nodeB]))
				.add(mergeNode)
				// Connect A to merge input 0
				.connect(nodeA, 0, mergeNode, 0)
				// Connect B to merge input 1
				.connect(nodeB, 0, mergeNode, 1)
				.add(finalNode)
				.connect(mergeNode, 0, finalNode, 0);

			const json = wf.toJSON();

			// Trigger should connect to both A and B
			expect(json.connections['Start'].main[0]).toHaveLength(2);

			// A and B should connect to different merge inputs
			expect(json.connections['A'].main[0]![0].node).toBe('Merge');
			expect(json.connections['A'].main[0]![0].index).toBe(0);

			expect(json.connections['B'].main[0]![0].node).toBe('Merge');
			expect(json.connections['B'].main[0]![0].index).toBe(1);
		});
	});

	describe('Edge Case 12: Error Output Connections', () => {
		it('supports .onError() for error output', () => {
			const http = node({
				type: 'n8n-nodes-base.httpRequest',
				version: 4.2,
				config: {
					name: 'HTTP',
					onError: 'continueErrorOutput',
				},
			});
			const success = createNode('Success');
			const errorHandler = createNode('Error');
			const t = createTrigger('Start');

			const wf = workflow('test', 'Test')
				.add(t.to(http))
				.add(http.to(success)) // Output 0 -> Success
				.add(http.onError(errorHandler)); // Output 1 (error) -> Error

			const json = wf.toJSON();

			// HTTP output 0 -> Success
			expect(json.connections['HTTP'].main[0]![0].node).toBe('Success');
			// HTTP error output -> Error (modern format: error pin at main[1])
			expect(json.connections['HTTP'].main[1]![0].node).toBe('Error');
		});
	});

	describe('Edge Case 13: Subnode Connections', () => {
		it('handles subnodes via object config', () => {
			const openAiModel = languageModel({
				type: '@n8n/n8n-nodes-langchain.lmChatOpenAi',
				version: 1.2,
				config: {
					name: 'OpenAI Model',
					parameters: { model: 'gpt-4' },
				},
			});

			const httpTool = tool({
				type: '@n8n/n8n-nodes-langchain.toolHttpRequest',
				version: 1.1,
				config: {
					name: 'HTTP Tool',
					parameters: { url: 'https://api.example.com' },
				},
			});

			const agent = node({
				type: '@n8n/n8n-nodes-langchain.agent',
				version: 1.7,
				config: {
					name: 'AI Agent',
					parameters: { promptType: 'define', text: 'You are helpful' },
					subnodes: {
						model: openAiModel,
						tools: [httpTool],
					},
				},
			});

			const t = createTrigger('Start');
			const wf = workflow('test', 'Test').add(t.to(agent));

			const json = wf.toJSON();

			// Agent node should be present
			expect(json.nodes.find((n) => n.name === 'AI Agent')).toBeDefined();

			// Subnode connections should use ai_* connection types
			const modelConnections = json.connections['OpenAI Model'];
			expect(modelConnections?.ai_languageModel?.[0]?.[0]?.node).toBe('AI Agent');

			const toolConnections = json.connections['HTTP Tool'];
			expect(toolConnections?.ai_tool?.[0]?.[0]?.node).toBe('AI Agent');
		});
	});

	describe('Edge Case 14: Duplicate Node Names', () => {
		it('auto-renames duplicate names at build time', () => {
			const nodeA = node({
				type: 'n8n-nodes-base.set',
				version: 3.4,
				config: { name: 'Process' },
			});
			const nodeB = node({
				type: 'n8n-nodes-base.set',
				version: 3.4,
				config: { name: 'Process' }, // Same name
			});
			const t = createTrigger('Start');

			const wf = workflow('test', 'Test').add(t.to(nodeA).to(nodeB));

			const json = wf.toJSON();

			// Extract names
			const names = json.nodes.map((n) => n.name);

			// All names should be unique
			expect(new Set(names).size).toBe(names.length);

			// Should have renamed one to "Process 1" or similar
			expect(names).toContain('Process');
			// Second one should have been renamed
			const processVariants = names.filter((n) => n?.startsWith('Process'));
			expect(processVariants.length).toBe(2);
			expect(processVariants.some((n) => n !== 'Process')).toBe(true);
		});
	});

	describe('New API Design: workflow.add() chainable', () => {
		it('workflow.add() returns workflow for fluent chaining', () => {
			const nodeA = createNode('A');
			const nodeB = createNode('B');
			const t = createTrigger('Start');

			// Multiple .add() calls should chain
			const wf = workflow('test', 'Test').add(t.to(nodeA)).add(nodeB); // Returns workflow, not void

			expect(wf.toJSON).toBeDefined();

			const json = wf.toJSON();
			expect(json.nodes).toHaveLength(3);
		});
	});

	describe('nextBatch() Helper', () => {
		it('returns the sibNode from a SplitInBatches builder', () => {
			const sibNode = createSplitInBatchesNode('Batch');
			const processNode = createNode('Process');

			// Using splitInBatches returns a builder with sibNode property
			const builder = splitInBatches(sibNode, {
				done: createNode('Done'),
				each: processNode,
			});

			// nextBatch() should extract the sibNode from the builder
			const result = nextBatch(builder);

			// Result should be the original sibNode
			expect(result).toBe(sibNode);
		});

		it('returns the node directly when passed a NodeInstance', () => {
			const sibNode = createSplitInBatchesNode('Batch');

			// When passing NodeInstance directly, nextBatch() should return it
			const result = nextBatch(sibNode);

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

		it('enables loop-back connections in split in batches workflows', () => {
			const t = createTrigger('Start');
			const sibNode = createSplitInBatchesNode('Process Batches');
			const processItem = createNode('Process Item');
			const summarize = createNode('Summarize');

			// Using nextBatch(sibNode) directly for loop-back
			// (passing the node instead of builder avoids circular reference)
			const sibBuilder = splitInBatches(sibNode, {
				done: summarize,
				each: processItem.to(nextBatch(sibNode)), // Use nextBatch for explicit loop-back
			});

			const wf = workflow('test', 'Batch Processing').add(t).to(sibBuilder);

			const json = wf.toJSON();

			// Verify batch loop structure
			expect(json.connections['Process Batches'].main[0]![0].node).toBe('Summarize');
			expect(json.connections['Process Batches'].main[1]![0].node).toBe('Process Item');
			// Process Item loops back to Split In Batches via nextBatch()
			expect(json.connections['Process Item'].main[0]![0].node).toBe('Process Batches');
		});
	});

	describe('API Integration: Complete Workflow Examples', () => {
		it('builds a basic workflow with branching', () => {
			const t = createTrigger('Start');
			const ifNode = createIfNode('Check Value');
			const trueHandler = createNode('Handle True');
			const falseHandler = createNode('Handle False');
			const finalNode = createNode('Finalize');

			const wf = workflow('test', 'Branching Workflow')
				.add(t)
				.to(
					ifNode.onTrue!(trueHandler.to(finalNode)).onFalse(falseHandler.to(finalNode)), // Converge
				);

			const json = wf.toJSON();

			// Verify structure
			expect(json.name).toBe('Branching Workflow');
			expect(json.nodes).toHaveLength(5);

			// Verify connections
			expect(json.connections['Start'].main[0]![0].node).toBe('Check Value');
			expect(json.connections['Check Value'].main[0]![0].node).toBe('Handle True');
			expect(json.connections['Check Value'].main[1]![0].node).toBe('Handle False');
			expect(json.connections['Handle True'].main[0]![0].node).toBe('Finalize');
			expect(json.connections['Handle False'].main[0]![0].node).toBe('Finalize');
		});

		it('builds a batch processing workflow with loop', () => {
			const t = createTrigger('Start');
			const fetchData = node({
				type: 'n8n-nodes-base.httpRequest',
				version: 4.2,
				config: { name: 'Fetch Data' },
			});
			const sibNode = createSplitInBatchesNode('Process Batches');
			const processItem = createNode('Process Item');
			const summarize = createNode('Summarize');

			const wf = workflow('test', 'Batch Processing')
				.add(t.to(fetchData))
				.to(
					splitInBatches(sibNode, {
						done: summarize,
						each: processItem.to(sibNode), // Loop back
					}),
				);

			const json = wf.toJSON();

			// Verify batch loop structure
			expect(json.connections['Process Batches'].main[0]![0].node).toBe('Summarize');
			expect(json.connections['Process Batches'].main[1]![0].node).toBe('Process Item');
			// Process Item loops back to Split In Batches
			expect(json.connections['Process Item'].main[0]![0].node).toBe('Process Batches');
		});

		it('builds an error handling workflow', () => {
			const t = createTrigger('Start');
			const riskyOperation = node({
				type: 'n8n-nodes-base.httpRequest',
				version: 4.2,
				config: {
					name: 'Risky API Call',
					onError: 'continueErrorOutput',
				},
			});
			const successPath = createNode('Process Success');
			const errorPath = createNode('Handle Error');
			const notify = createNode('Notify');

			const wf = workflow('test', 'Error Handling')
				.add(t.to(riskyOperation))
				.add(riskyOperation.to(successPath.to(notify)))
				.add(riskyOperation.onError(errorPath.to(notify)));

			const json = wf.toJSON();

			// Both success and error paths should converge at Notify
			expect(json.connections['Risky API Call'].main[0]![0].node).toBe('Process Success');
			expect(json.connections['Risky API Call'].main[1]![0].node).toBe('Handle Error');
			expect(json.connections['Process Success'].main[0]![0].node).toBe('Notify');
			expect(json.connections['Handle Error'].main[0]![0].node).toBe('Notify');
		});
	});

	describe('Nested control-flow targets', () => {
		it('allows ifElse.onTrue(splitInBatchesBuilder)', () => {
			const t = createTrigger('Start');
			const ifNode = createIfNode('HasItems?');
			const sibNode = createSplitInBatchesNode('Loop');
			const processNode = createNode('Process');
			const doneNode = createNode('Done');

			const loop = splitInBatches(sibNode).onEachBatch(processNode).onDone(doneNode);

			const wf = workflow('test', 'Test').add(t).to(ifNode.onTrue!(loop).onFalse(null));

			const json = wf.toJSON();

			// IF true output wires to the SIB node (head of the nested builder)
			expect(json.connections['HasItems?'].main[0]![0].node).toBe('Loop');
			// SIB branches are wired by the SIB handler
			expect(json.connections['Loop'].main[0]![0].node).toBe('Done');
			expect(json.connections['Loop'].main[1]![0].node).toBe('Process');
		});

		it('allows ifElse.onFalse(splitInBatchesBuilder)', () => {
			const t = createTrigger('Start');
			const ifNode = createIfNode('Flag?');
			const sibNode = createSplitInBatchesNode('Loop');
			const processNode = createNode('Process');
			const successNode = createNode('Success');

			const loop = splitInBatches(sibNode).onEachBatch(processNode);

			const wf = workflow('test', 'Test').add(t).to(ifNode.onTrue!(successNode).onFalse(loop));

			const json = wf.toJSON();

			expect(json.connections['Flag?'].main[0]![0].node).toBe('Success');
			expect(json.connections['Flag?'].main[1]![0].node).toBe('Loop');
			expect(json.connections['Loop'].main[1]![0].node).toBe('Process');
		});

		it('allows ifElse.onError(splitInBatchesBuilder)', () => {
			const t = createTrigger('Start');
			const ifNode = node({
				type: 'n8n-nodes-base.if',
				version: 2.2,
				config: {
					name: 'Gate',
					onError: 'continueErrorOutput',
					parameters: {
						conditions: {
							conditions: [
								{
									leftValue: '={{ $json.value }}',
									operator: { type: 'boolean', operation: 'true' },
								},
							],
						},
					},
				},
			}) as NodeInstance<'n8n-nodes-base.if', string, unknown>;
			const sibNode = createSplitInBatchesNode('ErrorLoop');
			const recover = createNode('Recover');
			const ok = createNode('OK');

			const loop = splitInBatches(sibNode).onEachBatch(recover);

			const wf = workflow('test', 'Test').add(t).to(ifNode.onTrue!(ok).onFalse(null).onError(loop));

			const json = wf.toJSON();

			// IF with onError='continueErrorOutput' emits three main outputs: true/false/error.
			// The error branch lands on output 2 and points at the SIB head.
			expect(json.connections['Gate'].main[2]![0].node).toBe('ErrorLoop');
		});

		it('allows switch.onCase(n, splitInBatchesBuilder)', () => {
			const t = createTrigger('Start');
			const switchNode = createSwitchNode('Route');
			const sibNode = createSplitInBatchesNode('BatchA');
			const processA = createNode('ProcessA');
			const simple = createNode('Simple');

			const loop = splitInBatches(sibNode).onEachBatch(processA);

			const wf = workflow('test', 'Test').add(t).to(switchNode.onCase!(0, loop).onCase(1, simple));

			const json = wf.toJSON();

			expect(json.connections['Route'].main[0]![0].node).toBe('BatchA');
			expect(json.connections['Route'].main[1]![0].node).toBe('Simple');
		});

		it('allows splitInBatches.onEachBatch(ifElseBuilder)', () => {
			const t = createTrigger('Start');
			const sibNode = createSplitInBatchesNode('Loop');
			const ifNode = createIfNode('PerItem');
			const hit = createNode('Hit');
			const miss = createNode('Miss');
			const finalize = createNode('Finalize');

			const loop = splitInBatches(sibNode)
				.onEachBatch(ifNode.onTrue!(hit).onFalse(miss))
				.onDone(finalize);

			const wf = workflow('test', 'Test').add(t).to(loop);

			const json = wf.toJSON();

			// SIB each output (index 1) -> IF head
			expect(json.connections['Loop'].main[1]![0].node).toBe('PerItem');
			// IF branches materialised
			expect(json.connections['PerItem'].main[0]![0].node).toBe('Hit');
			expect(json.connections['PerItem'].main[1]![0].node).toBe('Miss');
			// Done output wired to finalize
			expect(json.connections['Loop'].main[0]![0].node).toBe('Finalize');
		});
	});
});
