import { nextBatch } from './next-batch';
import { splitInBatches } from './split-in-batches';
import { parseWorkflowCode } from '../../codegen/parse-workflow-code';
import type { NodeInstance } from '../../types/base';
import { workflow } from '../../workflow-builder';
import { node, trigger } from '../node-builders/node-builder';

// Helper type for SplitInBatches node
type SibNode = NodeInstance<'n8n-nodes-base.splitInBatches', string, unknown>;

describe('Split In Batches', () => {
	describe('splitInBatches()', () => {
		it('should create a split in batches builder', () => {
			const sib = splitInBatches({
				version: 3,
				config: { parameters: { batchSize: 10 } },
			});
			expect(sib).toBeDefined();
		});

		it('should support explicit version', () => {
			const sib = splitInBatches({
				version: 2,
				config: { parameters: { batchSize: 5 } },
			});
			expect(sib.sibNode.version).toBe('2');
		});

		it('should use specified version', () => {
			const sib = splitInBatches({
				version: 3,
				config: { parameters: { batchSize: 10 } },
			});
			expect(sib.sibNode.version).toBe('3');
		});
	});

	describe('workflow integration', () => {
		it('should integrate splitInBatches with workflow builder', () => {
			const t = trigger({ type: 'n8n-nodes-base.webhookTrigger', version: 1, config: {} });
			const generateItems = node({
				type: 'n8n-nodes-base.set',
				version: 3,
				config: { name: 'Generate Items' },
			});
			const sibNode = node({
				type: 'n8n-nodes-base.splitInBatches',
				version: 3,
				config: { name: 'Loop', parameters: { batchSize: 10 } },
			}) as SibNode;
			const processNode = node({
				type: 'n8n-nodes-base.httpRequest',
				version: 4.2,
				config: { name: 'Process Batch' },
			});
			const finalizeNode = node({
				type: 'n8n-nodes-base.set',
				version: 3,
				config: { name: 'Finalize' },
			});

			const wf = workflow('test-id', 'Test')
				.add(t)
				.to(generateItems)
				.to(splitInBatches(sibNode).onDone(finalizeNode).onEachBatch(processNode.to(sibNode)));

			const json = wf.toJSON();

			// Should have: trigger, generateItems, splitInBatches, processNode, finalizeNode
			expect(json.nodes.length).toBeGreaterThanOrEqual(4);

			// Find split in batches node
			const foundSibNode = json.nodes.find((n) => n.type === 'n8n-nodes-base.splitInBatches');
			expect(foundSibNode).toBeDefined();
			expect(foundSibNode!.name).toBeDefined();
			expect(foundSibNode!.parameters?.batchSize).toBe(10);

			// Check split in batches has two outputs
			const sibConnections = json.connections[foundSibNode!.name!];
			expect(sibConnections).toBeDefined();
			expect(sibConnections.main).toHaveLength(2);

			// Output 0 should connect to finalize
			expect(sibConnections.main[0]).toHaveLength(1);
			expect(sibConnections.main[0]![0].node).toBe('Finalize');

			// Output 1 should connect to process
			expect(sibConnections.main[1]).toHaveLength(1);
			expect(sibConnections.main[1]![0].node).toBe('Process Batch');

			// Process should loop back to split in batches
			const processConnections = json.connections['Process Batch'];
			expect(processConnections).toBeDefined();
			expect(processConnections.main[0]![0].node).toBe(foundSibNode!.name);
		});

		it('should support fan-out with plain array in .onDone() for parallel branches', () => {
			// This pattern is generated by codegen for workflows like 6150 where
			// the "done" output of SplitInBatches fans out to multiple parallel nodes
			const t = trigger({ type: 'n8n-nodes-base.manualTrigger', version: 1, config: {} });
			const sibNode = node({
				type: 'n8n-nodes-base.splitInBatches',
				version: 3,
				config: { name: 'Process Items', parameters: { batchSize: 1 } },
			}) as SibNode;

			const fetchSchufaNode = node({
				type: 'n8n-nodes-base.googleDrive',
				version: 1,
				config: { name: 'Fetch Schufa' },
			});

			const fetchSalaryNode = node({
				type: 'n8n-nodes-base.googleDrive',
				version: 1,
				config: { name: 'Fetch Salary' },
			});

			const generateLetterNode = node({
				type: 'n8n-nodes-base.code',
				version: 2,
				config: { name: 'Generate Cover Letter' },
			});

			// Fan-out pattern with plain array: .onDone([branch1, branch2])
			// This mimics: SplitInBatches -> [Fetch Schufa, Fetch Salary] -> Generate Cover Letter
			const wf = workflow('test-id', 'Test')
				.add(t)
				.to(
					splitInBatches(sibNode).onDone([
						fetchSchufaNode.to(generateLetterNode),
						fetchSalaryNode.to(generateLetterNode),
					]),
				);

			const json = wf.toJSON();

			// Should have: trigger, splitInBatches, fetchSchufa, fetchSalary, generateLetter
			const nodeNames = json.nodes.map((n) => n.name).sort();
			expect(nodeNames).toEqual(
				[
					'Manual Trigger',
					'Process Items',
					'Fetch Schufa',
					'Fetch Salary',
					'Generate Cover Letter',
				].sort(),
			);

			// SplitInBatches done output should connect to both fetch nodes
			const sibConnections = json.connections['Process Items'];
			expect(sibConnections).toBeDefined();
			expect(sibConnections.main[0]).toHaveLength(2);
			const doneTargets = sibConnections.main[0]!.map((c: { node: string }) => c.node).sort();
			expect(doneTargets).toEqual(['Fetch Salary', 'Fetch Schufa']);

			// Both fetch nodes should connect to generate letter
			const schufaConns = json.connections['Fetch Schufa'];
			expect(schufaConns).toBeDefined();
			expect(schufaConns.main[0]![0].node).toBe('Generate Cover Letter');

			const salaryConns = json.connections['Fetch Salary'];
			expect(salaryConns).toBeDefined();
			expect(salaryConns.main[0]![0].node).toBe('Generate Cover Letter');
		});

		it('should support fan-out with plain array in .onEachBatch() for parallel branches', () => {
			const t = trigger({ type: 'n8n-nodes-base.manualTrigger', version: 1, config: {} });
			const sibNode = node({
				type: 'n8n-nodes-base.splitInBatches',
				version: 3,
				config: { name: 'Loop', parameters: { batchSize: 1 } },
			}) as SibNode;

			const branch1Node = node({
				type: 'n8n-nodes-base.code',
				version: 2,
				config: { name: 'Branch 1' },
			});

			const branch2Node = node({
				type: 'n8n-nodes-base.code',
				version: 2,
				config: { name: 'Branch 2' },
			});

			const wf = workflow('test-id', 'Test')
				.add(t)
				.to(splitInBatches(sibNode).onEachBatch([branch1Node, branch2Node]));

			const json = wf.toJSON();

			// Should have: trigger, splitInBatches, branch1, branch2
			expect(json.nodes).toHaveLength(4);

			// SplitInBatches each output (index 1) should connect to both branch nodes
			const sibConnections = json.connections['Loop'];
			expect(sibConnections).toBeDefined();
			expect(sibConnections.main[1]).toHaveLength(2);
			const eachTargets = sibConnections.main[1]!.map((c: { node: string }) => c.node).sort();
			expect(eachTargets).toEqual(['Branch 1', 'Branch 2']);
		});

		it('should handle splitInBatches nested in node chain (node.to(sib))', () => {
			const t = trigger({ type: 'n8n-nodes-base.webhookTrigger', version: 1, config: {} });
			const generateItems = node({
				type: 'n8n-nodes-base.set',
				version: 3,
				config: { name: 'Generate Items' },
			});
			const sibNode = node({
				type: 'n8n-nodes-base.splitInBatches',
				version: 3,
				config: { name: 'Loop', parameters: { batchSize: 5 } },
			}) as SibNode;
			const waitNode = node({
				type: 'n8n-nodes-base.wait',
				version: 1.1,
				config: { name: 'Wait' },
			});
			const codeNode = node({
				type: 'n8n-nodes-base.code',
				version: 2,
				config: { name: 'Code' },
			});
			const finalizeNode = node({
				type: 'n8n-nodes-base.set',
				version: 3,
				config: { name: 'Finalize' },
			});

			// Bug case: node.to(splitInBatches(...)) - nodes inside chains are lost
			// Now using new syntax with .onDone() and .onEachBatch()
			const chain = t
				.to(generateItems)
				.to(
					splitInBatches(sibNode)
						.onDone(finalizeNode)
						.onEachBatch(waitNode.to(codeNode).to(sibNode)),
				);

			const wf = workflow('test-id', 'Test').add(chain);
			const json = wf.toJSON();

			// Should have: trigger, generateItems, splitInBatches, waitNode, codeNode, finalizeNode
			const nodeNames = json.nodes.map((n) => n.name).sort();
			expect(nodeNames).toEqual(
				['Webhook Trigger', 'Generate Items', 'Loop', 'Wait', 'Code', 'Finalize'].sort(),
			);

			// Generate Items should connect to Loop (splitInBatches)
			const generateConnections = json.connections['Generate Items'];
			expect(generateConnections).toBeDefined();
			expect(generateConnections.main[0]![0].node).toBe('Loop');

			// Loop output 0 (done) should connect to Finalize
			const loopConnections = json.connections['Loop'];
			expect(loopConnections).toBeDefined();
			expect(loopConnections.main[0]![0].node).toBe('Finalize');

			// Loop output 1 (each) should connect to Wait
			expect(loopConnections.main[1]![0].node).toBe('Wait');

			// Wait should connect to Code
			const waitConnections = json.connections['Wait'];
			expect(waitConnections).toBeDefined();
			expect(waitConnections.main[0]![0].node).toBe('Code');

			// Code should loop back to Loop
			const codeConnections = json.connections['Code'];
			expect(codeConnections).toBeDefined();
			expect(codeConnections.main[0]![0].node).toBe('Loop');
		});
	});

	describe('fluent API syntax with .onEachBatch() and .onDone()', () => {
		it('should support splitInBatches(node).onEachBatch().onDone() syntax', () => {
			const t = trigger({ type: 'n8n-nodes-base.manualTrigger', version: 1, config: {} });
			const sibNode = node({
				type: 'n8n-nodes-base.splitInBatches',
				version: 3,
				config: { name: 'Loop', parameters: { batchSize: 10 } },
			}) as SibNode;
			const finalizeNode = node({
				type: 'n8n-nodes-base.set',
				version: 3,
				config: { name: 'Finalize' },
			});
			const processNode = node({
				type: 'n8n-nodes-base.httpRequest',
				version: 4.2,
				config: { name: 'Process Batch' },
			});

			// Fluent API syntax
			const wf = workflow('test-id', 'Test')
				.add(t)
				.to(splitInBatches(sibNode).onEachBatch(processNode.to(sibNode)).onDone(finalizeNode));

			const json = wf.toJSON();

			// Should have: trigger, splitInBatches, finalizeNode, processNode
			expect(json.nodes).toHaveLength(4);

			// SIB output 0 (done) should connect to Finalize
			const sibConnections = json.connections['Loop'];
			expect(sibConnections).toBeDefined();
			expect(sibConnections.main[0]).toHaveLength(1);
			expect(sibConnections.main[0]![0].node).toBe('Finalize');

			// SIB output 1 (each) should connect to Process Batch
			expect(sibConnections.main[1]).toHaveLength(1);
			expect(sibConnections.main[1]![0].node).toBe('Process Batch');

			// Process Batch should loop back to Loop
			const processConnections = json.connections['Process Batch'];
			expect(processConnections).toBeDefined();
			expect(processConnections.main[0]![0].node).toBe('Loop');
		});

		it('should support methods in any order: .onDone().onEachBatch()', () => {
			const t = trigger({ type: 'n8n-nodes-base.manualTrigger', version: 1, config: {} });
			const sibNode = node({
				type: 'n8n-nodes-base.splitInBatches',
				version: 3,
				config: { name: 'Loop', parameters: { batchSize: 10 } },
			}) as SibNode;
			const finalizeNode = node({
				type: 'n8n-nodes-base.set',
				version: 3,
				config: { name: 'Finalize' },
			});
			const processNode = node({
				type: 'n8n-nodes-base.httpRequest',
				version: 4.2,
				config: { name: 'Process Batch' },
			});

			// Order: onDone first, then onEachBatch
			const wf = workflow('test-id', 'Test')
				.add(t)
				.to(splitInBatches(sibNode).onDone(finalizeNode).onEachBatch(processNode.to(sibNode)));

			const json = wf.toJSON();

			// Should have: trigger, splitInBatches, finalizeNode, processNode
			expect(json.nodes).toHaveLength(4);

			// SIB output 0 (done) should connect to Finalize
			const sibConnections = json.connections['Loop'];
			expect(sibConnections.main[0]![0].node).toBe('Finalize');

			// SIB output 1 (each) should connect to Process Batch
			expect(sibConnections.main[1]![0].node).toBe('Process Batch');
		});

		it('should support omitting .onDone() for empty done branch', () => {
			const t = trigger({ type: 'n8n-nodes-base.manualTrigger', version: 1, config: {} });
			const sibNode = node({
				type: 'n8n-nodes-base.splitInBatches',
				version: 3,
				config: { name: 'Loop', parameters: { batchSize: 10 } },
			}) as SibNode;
			const processNode = node({
				type: 'n8n-nodes-base.httpRequest',
				version: 4.2,
				config: { name: 'Process Batch' },
			});

			// Only onEachBatch, no onDone
			const wf = workflow('test-id', 'Test')
				.add(t)
				.to(splitInBatches(sibNode).onEachBatch(processNode.to(sibNode)));

			const json = wf.toJSON();

			// Should have: trigger, splitInBatches, processNode
			expect(json.nodes).toHaveLength(3);

			const sibConnections = json.connections['Loop'];
			expect(sibConnections).toBeDefined();

			// done output (0) should be empty
			expect(sibConnections.main[0] ?? []).toHaveLength(0);

			// each output (1) should connect to Process Batch
			expect(sibConnections.main[1]).toHaveLength(1);
			expect(sibConnections.main[1]![0].node).toBe('Process Batch');
		});

		it('should support omitting .onEachBatch() for empty each branch', () => {
			const t = trigger({ type: 'n8n-nodes-base.manualTrigger', version: 1, config: {} });
			const sibNode = node({
				type: 'n8n-nodes-base.splitInBatches',
				version: 3,
				config: { name: 'Loop', parameters: { batchSize: 10 } },
			}) as SibNode;
			const finalizeNode = node({
				type: 'n8n-nodes-base.set',
				version: 3,
				config: { name: 'Finalize' },
			});

			// Only onDone, no onEachBatch
			const wf = workflow('test-id', 'Test')
				.add(t)
				.to(splitInBatches(sibNode).onDone(finalizeNode));

			const json = wf.toJSON();

			// Should have: trigger, splitInBatches, finalizeNode
			expect(json.nodes).toHaveLength(3);

			const sibConnections = json.connections['Loop'];
			expect(sibConnections).toBeDefined();

			// done output (0) should connect to Finalize
			expect(sibConnections.main[0]).toHaveLength(1);
			expect(sibConnections.main[0]![0].node).toBe('Finalize');

			// each output (1) should be empty
			expect(sibConnections.main[1] ?? []).toHaveLength(0);
		});

		it('should support plain array for multiple targets in onEachBatch', () => {
			const t = trigger({ type: 'n8n-nodes-base.manualTrigger', version: 1, config: {} });
			const sibNode = node({
				type: 'n8n-nodes-base.splitInBatches',
				version: 3,
				config: { name: 'Loop', parameters: { batchSize: 10 } },
			}) as SibNode;
			const branch1 = node({
				type: 'n8n-nodes-base.set',
				version: 3,
				config: { name: 'Branch 1' },
			});
			const branch2 = node({
				type: 'n8n-nodes-base.set',
				version: 3,
				config: { name: 'Branch 2' },
			});

			// Fan-out from each branch with plain array
			const wf = workflow('test-id', 'Test')
				.add(t)
				.to(splitInBatches(sibNode).onEachBatch([branch1, branch2]));

			const json = wf.toJSON();

			// Should have: trigger, splitInBatches, branch1, branch2
			expect(json.nodes).toHaveLength(4);

			// SIB each output (1) should connect to both branches
			const sibConnections = json.connections['Loop'];
			expect(sibConnections.main[1]).toHaveLength(2);
			const eachTargets = sibConnections.main[1]!.map((c: { node: string }) => c.node).sort();
			expect(eachTargets).toEqual(['Branch 1', 'Branch 2']);
		});
	});

	describe('named object syntax', () => {
		it('should support splitInBatches(node, { done, each }) syntax', () => {
			const t = trigger({ type: 'n8n-nodes-base.manualTrigger', version: 1, config: {} });
			const sibNode = node({
				type: 'n8n-nodes-base.splitInBatches',
				version: 3,
				config: { name: 'Loop', parameters: { batchSize: 10 } },
			}) as SibNode;
			const finalizeNode = node({
				type: 'n8n-nodes-base.set',
				version: 3,
				config: { name: 'Finalize' },
			});
			const processNode = node({
				type: 'n8n-nodes-base.httpRequest',
				version: 4.2,
				config: { name: 'Process Batch' },
			});

			// Named object syntax
			const wf = workflow('test-id', 'Test')
				.add(t)
				.to(
					splitInBatches(sibNode, {
						done: finalizeNode,
						each: processNode.to(sibNode), // loop back to SIB
					}),
				);

			const json = wf.toJSON();

			// Should have: trigger, splitInBatches, finalizeNode, processNode
			expect(json.nodes).toHaveLength(4);

			// SIB output 0 (done) should connect to Finalize
			const sibConnections = json.connections['Loop'];
			expect(sibConnections).toBeDefined();
			expect(sibConnections.main[0]).toHaveLength(1);
			expect(sibConnections.main[0]![0].node).toBe('Finalize');

			// SIB output 1 (each) should connect to Process Batch
			expect(sibConnections.main[1]).toHaveLength(1);
			expect(sibConnections.main[1]![0].node).toBe('Process Batch');

			// Process Batch should loop back to Loop
			const processConnections = json.connections['Process Batch'];
			expect(processConnections).toBeDefined();
			expect(processConnections.main[0]![0].node).toBe('Loop');
		});

		it('should support null for empty branches', () => {
			const t = trigger({ type: 'n8n-nodes-base.manualTrigger', version: 1, config: {} });
			const sibNode = node({
				type: 'n8n-nodes-base.splitInBatches',
				version: 3,
				config: { name: 'Loop', parameters: { batchSize: 10 } },
			}) as SibNode;

			// Self-loop on each only
			const wf = workflow('test-id', 'Test')
				.add(t)
				.to(
					splitInBatches(sibNode, {
						done: null,
						each: sibNode, // self-loop
					}),
				);

			const json = wf.toJSON();

			// Should have: trigger, splitInBatches
			expect(json.nodes).toHaveLength(2);

			// SIB should have connections
			const sibConnections = json.connections['Loop'];
			expect(sibConnections).toBeDefined();

			// done output (0) should be empty or not connect to anything
			expect(sibConnections.main[0] ?? []).toHaveLength(0);

			// each output (1) should connect back to itself
			expect(sibConnections.main[1]).toHaveLength(1);
			expect(sibConnections.main[1]![0].node).toBe('Loop');
		});

		it('should support plain array for multiple targets from one branch', () => {
			const t = trigger({ type: 'n8n-nodes-base.manualTrigger', version: 1, config: {} });
			const sibNode = node({
				type: 'n8n-nodes-base.splitInBatches',
				version: 3,
				config: { name: 'Loop', parameters: { batchSize: 10 } },
			}) as SibNode;
			const branch1 = node({
				type: 'n8n-nodes-base.set',
				version: 3,
				config: { name: 'Branch 1' },
			});
			const branch2 = node({
				type: 'n8n-nodes-base.set',
				version: 3,
				config: { name: 'Branch 2' },
			});

			// Fan-out from done branch with plain array
			const wf = workflow('test-id', 'Test')
				.add(t)
				.to(
					splitInBatches(sibNode, {
						done: [branch1, branch2],
						each: sibNode,
					}),
				);

			const json = wf.toJSON();

			// Should have: trigger, splitInBatches, branch1, branch2
			expect(json.nodes).toHaveLength(4);

			// SIB done output (0) should connect to both branches
			const sibConnections = json.connections['Loop'];
			expect(sibConnections).toBeDefined();
			expect(sibConnections.main[0]).toHaveLength(2);
			const doneTargets = sibConnections.main[0]!.map((c: { node: string }) => c.node).sort();
			expect(doneTargets).toEqual(['Branch 1', 'Branch 2']);
		});
	});

	describe('multiple independent SIB loops', () => {
		it('should handle multiple independent SIB loops with same-named nodes', () => {
			const morningTrigger = trigger({
				type: 'n8n-nodes-base.scheduleTrigger',
				version: 1.3,
				config: { name: 'Morning Schedule' },
			});
			const afternoonTrigger = trigger({
				type: 'n8n-nodes-base.scheduleTrigger',
				version: 1.3,
				config: { name: 'Afternoon Schedule' },
			});

			const createMorningBatch = node({
				type: 'n8n-nodes-base.set',
				version: 3.4,
				config: { name: 'Create Batch' },
			});
			const createAfternoonBatch = node({
				type: 'n8n-nodes-base.set',
				version: 3.4,
				config: { name: 'Create Batch' },
			});

			const morningLoop = node({
				type: 'n8n-nodes-base.splitInBatches',
				version: 3,
				config: { name: 'Morning Loop' },
			}) as SibNode;
			const afternoonLoop = node({
				type: 'n8n-nodes-base.splitInBatches',
				version: 3,
				config: { name: 'Afternoon Loop' },
			}) as SibNode;

			const processMorning = node({
				type: 'n8n-nodes-base.httpRequest',
				version: 4.2,
				config: { name: 'Process' },
			});
			const processAfternoon = node({
				type: 'n8n-nodes-base.httpRequest',
				version: 4.2,
				config: { name: 'Process' },
			});

			const wf = workflow('test-id', 'Test')
				.add(
					morningTrigger
						.to(createMorningBatch)
						.to(splitInBatches(morningLoop).onEachBatch(processMorning.to(morningLoop))),
				)
				.add(
					afternoonTrigger
						.to(createAfternoonBatch)
						.to(splitInBatches(afternoonLoop).onEachBatch(processAfternoon.to(afternoonLoop))),
				);

			const json = wf.toJSON();

			// Should have nodes from both chains with renamed duplicates
			const nodeNames = json.nodes.map((n) => n.name).sort();
			expect(nodeNames).toContain('Morning Schedule');
			expect(nodeNames).toContain('Afternoon Schedule');
			expect(nodeNames).toContain('Morning Loop');
			expect(nodeNames).toContain('Afternoon Loop');
			expect(nodeNames).toContain('Process');
			expect(nodeNames).toContain('Process 1');
			expect(nodeNames).toContain('Create Batch');
			expect(nodeNames).toContain('Create Batch 1');

			// Morning Loop output 1 should connect to "Process" (the first one)
			expect(json.connections['Morning Loop'].main[1]![0].node).toBe('Process');

			// Afternoon Loop output 1 should connect to "Process 1" (the renamed one)
			expect(json.connections['Afternoon Loop'].main[1]![0].node).toBe('Process 1');

			// Verify loop-back connections are correct
			expect(json.connections['Process'].main[0]![0].node).toBe('Morning Loop');
			expect(json.connections['Process 1'].main[0]![0].node).toBe('Afternoon Loop');
		});
	});

	describe('circular reference handling', () => {
		it('should handle splitInBatches with long chain looping back without stack overflow', () => {
			// This test reproduces a bug where parsing code with splitInBatches
			// that has a long chain in onEachBatch looping back to the SIB node
			// causes "Maximum call stack size exceeded" error
			const code = `
const sibNode = node({
	type: 'n8n-nodes-base.splitInBatches',
	version: 3,
	config: { name: 'Process Each Video', parameters: { batchSize: 1 } },
});

const processNode = node({
	type: 'n8n-nodes-base.httpRequest',
	version: 4.2,
	config: { name: 'Process Batch' },
});

const uploadNode = node({
	type: 'n8n-nodes-base.httpRequest',
	version: 4.2,
	config: { name: 'Upload' },
});

const finalizeNode = node({
	type: 'n8n-nodes-base.set',
	version: 3,
	config: { name: 'Finalize' },
});

export default workflow('test-id', 'Test')
	.add(trigger({ type: 'n8n-nodes-base.manualTrigger', version: 1, config: {} }))
	.to(splitInBatches(sibNode)
		.onDone(finalizeNode)
		.onEachBatch(processNode.to(uploadNode).to(sibNode))
	);
`;

			// This should NOT throw "Maximum call stack size exceeded"
			const json = parseWorkflowCode(code);

			// Verify the workflow was parsed correctly
			expect(json.nodes).toHaveLength(5); // trigger, sib, process, upload, finalize

			// Verify connections
			const sibConnections = json.connections['Process Each Video'];
			expect(sibConnections).toBeDefined();

			// Output 0 (done) connects to Finalize
			expect(sibConnections.main[0]).toHaveLength(1);
			expect(sibConnections.main[0]![0].node).toBe('Finalize');

			// Output 1 (each) connects to Process Batch
			expect(sibConnections.main[1]).toHaveLength(1);
			expect(sibConnections.main[1]![0].node).toBe('Process Batch');

			// Upload should loop back to SIB
			const uploadConnections = json.connections['Upload'];
			expect(uploadConnections).toBeDefined();
			expect(uploadConnections.main[0]![0].node).toBe('Process Each Video');
		});

		it('should handle real-world YouTube Shorts workflow with AI agent and subnodes', () => {
			// This is the exact code from user's bug report that causes stack overflow
			const code = `// Define subnodes first
const openAiModel = languageModel({
  type: '@n8n/n8n-nodes-langchain.lmChatOpenAi',
  version: 1.3,
  config: {
    name: 'OpenAI GPT-4',
    parameters: {
      model: { mode: 'list', value: 'gpt-4o' },
      options: {
        temperature: 0.8
      }
    },
    credentials: {
      openAiApi: newCredential('OpenAI')
    },
    position: [1440, 600]
  }
});

// Define main nodes
const scheduleTrigger = trigger({
  type: 'n8n-nodes-base.scheduleTrigger',
  version: 1.3,
  config: {
    name: 'Run 3 Times Daily',
    parameters: {
      rule: {
        interval: [
          {
            field: 'hours',
            hoursInterval: 6,
            triggerAtMinute: 0
          }
        ]
      }
    },
    position: [240, 300]
  }
});

const createBatchItems = node({
  type: 'n8n-nodes-base.set',
  version: 3.4,
  config: {
    name: 'Create 3 Video Items',
    parameters: {
      mode: 'manual',
      fields: {
        values: [
          {
            name: 'videoCount',
            type: 'arrayValue',
            arrayValue: '=[1, 2, 3]'
          }
        ]
      },
      include: 'none'
    },
    position: [540, 300]
  }
});

const generateStory = node({
  type: '@n8n/n8n-nodes-langchain.agent',
  version: 3.1,
  config: {
    name: 'Generate Short Story Script',
    parameters: {
      promptType: 'define',
      text: 'Generate a unique, engaging short story script for a YouTube Short video (60 seconds or less). The story should be captivating, have a clear beginning, middle, and end, and be suitable for video format. Include visual descriptions and narration text. Make each story completely different and creative.',
      options: {
        systemMessage: 'You are a creative storyteller specializing in short-form video content. Generate compelling, original stories perfect for YouTube Shorts.',
        maxIterations: 5
      }
    },
    subnodes: {
      model: openAiModel
    },
    position: [1140, 400]
  }
});

const generateVideo = node({
  type: 'n8n-nodes-base.httpRequest',
  version: 4.3,
  config: {
    name: 'Generate Video with Sora',
    parameters: {
      method: 'POST',
      url: placeholder('Sora API endpoint URL'),
      authentication: 'predefinedCredentialType',
      nodeCredentialType: 'httpHeaderAuth',
      sendHeaders: true,
      specifyHeaders: 'keypair',
      headerParameters: {
        parameters: [
          {
            name: 'Content-Type',
            value: 'application/json'
          }
        ]
      },
      sendBody: true,
      contentType: 'json',
      specifyBody: 'json',
      jsonBody: '={{ { "prompt": $json.output, "duration": 60, "aspect_ratio": "9:16" } }}',
      options: {
        response: {
          response: {
            responseFormat: 'json'
          }
        },
        timeout: 300000
      }
    },
    credentials: {
      httpHeaderAuth: newCredential('Sora API Key')
    },
    position: [1440, 400]
  }
});

const uploadToYouTube = node({
  type: 'n8n-nodes-base.youTube',
  version: 1,
  config: {
    name: 'Upload to YouTube',
    parameters: {
      resource: 'video',
      operation: 'upload',
      title: '={{ "Short Story " + $now.toFormat("yyyy-MM-dd HH:mm") }}',
      binaryProperty: 'data',
      options: {
        description: 'An engaging short story created with AI',
        privacyStatus: 'public',
        tags: 'shorts,short story,ai generated,storytelling',
        selfDeclaredMadeForKids: false,
        notifySubscribers: true
      }
    },
    credentials: {
      youTubeOAuth2Api: newCredential('YouTube OAuth2')
    },
    position: [1740, 400]
  }
});

const allVideosComplete = node({
  type: 'n8n-nodes-base.noOp',
  version: 1,
  config: {
    name: 'All Videos Uploaded',
    parameters: {},
    position: [1140, 200]
  }
});

const splitVideos = splitInBatches({
  version: 3,
  config: {
    name: 'Process Each Video',
    parameters: {
      batchSize: 1,
      options: {}
    },
    position: [840, 300]
  }
});

// Compose workflow with loop
export default workflow('eval-1769451317134-scv1mk1th', 'YouTube Shorts Auto-Publisher')
  .add(scheduleTrigger)
  .to(createBatchItems)
  .to(splitVideos
    .onDone(allVideosComplete)
    .onEachBatch(generateStory.to(generateVideo).to(uploadToYouTube).to(splitVideos))
  );`;

			// This should NOT throw "Maximum call stack size exceeded"
			const json = parseWorkflowCode(code);

			// Verify the workflow was parsed correctly
			const nodeNames = json.nodes.map((n) => n.name).sort();
			expect(nodeNames).toContain('Run 3 Times Daily');
			expect(nodeNames).toContain('Create 3 Video Items');
			expect(nodeNames).toContain('Process Each Video');
			expect(nodeNames).toContain('Generate Short Story Script');
			expect(nodeNames).toContain('Generate Video with Sora');
			expect(nodeNames).toContain('Upload to YouTube');
			expect(nodeNames).toContain('All Videos Uploaded');

			// Verify the loop structure
			const sibConnections = json.connections['Process Each Video'];
			expect(sibConnections).toBeDefined();

			// Output 0 (done) connects to All Videos Uploaded
			expect(sibConnections.main[0]).toHaveLength(1);
			expect(sibConnections.main[0]![0].node).toBe('All Videos Uploaded');

			// Output 1 (each) connects to Generate Short Story Script
			expect(sibConnections.main[1]).toHaveLength(1);
			expect(sibConnections.main[1]![0].node).toBe('Generate Short Story Script');

			// Upload to YouTube should loop back to Process Each Video
			const uploadConnections = json.connections['Upload to YouTube'];
			expect(uploadConnections).toBeDefined();
			expect(uploadConnections.main[0]![0].node).toBe('Process Each Video');
		});

		it('should handle nextBatch() looping back to SIB builder without stack overflow', () => {
			// Test that nextBatch(sibBuilder) pattern works correctly
			// nextBatch() returns the underlying sibNode, not the builder,
			// so it should not trigger the WeakSet guard and should work correctly
			const t = trigger({ type: 'n8n-nodes-base.manualTrigger', version: 1, config: {} });
			const sibNode = node({
				type: 'n8n-nodes-base.splitInBatches',
				version: 3,
				config: { name: 'Split Videos', parameters: { batchSize: 1 } },
			}) as SibNode;
			const sibBuilder = splitInBatches(sibNode);
			const processNode = node({
				type: 'n8n-nodes-base.httpRequest',
				version: 4.2,
				config: { name: 'Process' },
			});
			const doneNode = node({
				type: 'n8n-nodes-base.noOp',
				version: 1,
				config: { name: 'Done' },
			});

			// Use nextBatch(sibBuilder) - should NOT cause infinite recursion
			const wf = workflow('test-id', 'Test')
				.add(t)
				.to(sibBuilder.onEachBatch(processNode.to(nextBatch(sibBuilder))).onDone(doneNode));

			const json = wf.toJSON();

			// Verify nodes are present
			const nodeNames = json.nodes.map((n) => n.name).sort();
			expect(nodeNames).toEqual(['Done', 'Manual Trigger', 'Process', 'Split Videos']);

			// Verify connections
			const sibConnections = json.connections['Split Videos'];
			expect(sibConnections).toBeDefined();

			// Output 0 (done) connects to Done
			expect(sibConnections.main[0]).toHaveLength(1);
			expect(sibConnections.main[0]![0].node).toBe('Done');

			// Output 1 (each) connects to Process
			expect(sibConnections.main[1]).toHaveLength(1);
			expect(sibConnections.main[1]![0].node).toBe('Process');

			// Process should loop back to Split Videos
			const processConnections = json.connections['Process'];
			expect(processConnections).toBeDefined();
			expect(processConnections.main[0]![0].node).toBe('Split Videos');
		});
	});
});
