import { describe, it, expect } from '@jest/globals';

import { buildSemanticGraph } from './semantic-graph';
import type { WorkflowJSON } from '../types/base';

describe('semantic-graph', () => {
	describe('buildSemanticGraph', () => {
		it('creates nodes for all workflow nodes', () => {
			const json: WorkflowJSON = {
				name: 'Test',
				nodes: [
					{
						id: '1',
						name: 'Trigger',
						type: 'n8n-nodes-base.manualTrigger',
						typeVersion: 1,
						position: [0, 0],
					},
					{
						id: '2',
						name: 'Process',
						type: 'n8n-nodes-base.noOp',
						typeVersion: 1,
						position: [100, 0],
					},
				],
				connections: {},
			};

			const graph = buildSemanticGraph(json);

			expect(graph.nodes.size).toBe(2);
			expect(graph.nodes.has('Trigger')).toBe(true);
			expect(graph.nodes.has('Process')).toBe(true);
		});

		it('parses simple linear connection', () => {
			const json: WorkflowJSON = {
				name: 'Test',
				nodes: [
					{
						id: '1',
						name: 'Trigger',
						type: 'n8n-nodes-base.manualTrigger',
						typeVersion: 1,
						position: [0, 0],
					},
					{
						id: '2',
						name: 'Process',
						type: 'n8n-nodes-base.noOp',
						typeVersion: 1,
						position: [100, 0],
					},
				],
				connections: {
					Trigger: {
						main: [[{ node: 'Process', type: 'main', index: 0 }]],
					},
				},
			};

			const graph = buildSemanticGraph(json);

			const trigger = graph.nodes.get('Trigger')!;
			const process = graph.nodes.get('Process')!;

			// Trigger has output0 → Process
			expect(trigger.outputs.get('output0')).toEqual([
				{ target: 'Process', targetInputSlot: 'input0' },
			]);

			// Process has input from Trigger
			expect(process.inputSources.get('input0')).toEqual([
				{ from: 'Trigger', outputSlot: 'output0' },
			]);
		});

		it('uses semantic names for IF node outputs', () => {
			const json: WorkflowJSON = {
				name: 'Test',
				nodes: [
					{
						id: '1',
						name: 'Trigger',
						type: 'n8n-nodes-base.manualTrigger',
						typeVersion: 1,
						position: [0, 0],
					},
					{ id: '2', name: 'IF', type: 'n8n-nodes-base.if', typeVersion: 2, position: [100, 0] },
					{
						id: '3',
						name: 'TrueHandler',
						type: 'n8n-nodes-base.noOp',
						typeVersion: 1,
						position: [200, -50],
					},
					{
						id: '4',
						name: 'FalseHandler',
						type: 'n8n-nodes-base.noOp',
						typeVersion: 1,
						position: [200, 50],
					},
				],
				connections: {
					Trigger: {
						main: [[{ node: 'IF', type: 'main', index: 0 }]],
					},
					IF: {
						main: [
							[{ node: 'TrueHandler', type: 'main', index: 0 }],
							[{ node: 'FalseHandler', type: 'main', index: 0 }],
						],
					},
				},
			};

			const graph = buildSemanticGraph(json);

			const ifNode = graph.nodes.get('IF')!;

			// IF output 0 should be 'trueBranch'
			expect(ifNode.outputs.get('trueBranch')).toEqual([
				{ target: 'TrueHandler', targetInputSlot: 'input0' },
			]);

			// IF output 1 should be 'falseBranch'
			expect(ifNode.outputs.get('falseBranch')).toEqual([
				{ target: 'FalseHandler', targetInputSlot: 'input0' },
			]);
		});

		it('uses semantic names for Merge node inputs', () => {
			const json: WorkflowJSON = {
				name: 'Test',
				nodes: [
					{
						id: '1',
						name: 'Source1',
						type: 'n8n-nodes-base.noOp',
						typeVersion: 1,
						position: [0, -50],
					},
					{
						id: '2',
						name: 'Source2',
						type: 'n8n-nodes-base.noOp',
						typeVersion: 1,
						position: [0, 50],
					},
					{
						id: '3',
						name: 'Merge',
						type: 'n8n-nodes-base.merge',
						typeVersion: 3,
						position: [100, 0],
						parameters: { numberInputs: 2 },
					},
				],
				connections: {
					Source1: {
						main: [[{ node: 'Merge', type: 'main', index: 0 }]],
					},
					Source2: {
						main: [[{ node: 'Merge', type: 'main', index: 1 }]],
					},
				},
			};

			const graph = buildSemanticGraph(json);

			const merge = graph.nodes.get('Merge')!;

			// Merge input 0 should be 'branch0'
			expect(merge.inputSources.get('branch0')).toEqual([
				{ from: 'Source1', outputSlot: 'output0' },
			]);

			// Merge input 1 should be 'branch1'
			expect(merge.inputSources.get('branch1')).toEqual([
				{ from: 'Source2', outputSlot: 'output0' },
			]);
		});

		it('uses semantic names for SplitInBatches outputs', () => {
			const json: WorkflowJSON = {
				name: 'Test',
				nodes: [
					{
						id: '1',
						name: 'Trigger',
						type: 'n8n-nodes-base.manualTrigger',
						typeVersion: 1,
						position: [0, 0],
					},
					{
						id: '2',
						name: 'SplitInBatches',
						type: 'n8n-nodes-base.splitInBatches',
						typeVersion: 3,
						position: [100, 0],
					},
					{
						id: '3',
						name: 'DoneHandler',
						type: 'n8n-nodes-base.noOp',
						typeVersion: 1,
						position: [200, -50],
					},
					{
						id: '4',
						name: 'LoopBody',
						type: 'n8n-nodes-base.noOp',
						typeVersion: 1,
						position: [200, 50],
					},
				],
				connections: {
					Trigger: {
						main: [[{ node: 'SplitInBatches', type: 'main', index: 0 }]],
					},
					SplitInBatches: {
						main: [
							[{ node: 'DoneHandler', type: 'main', index: 0 }],
							[{ node: 'LoopBody', type: 'main', index: 0 }],
						],
					},
				},
			};

			const graph = buildSemanticGraph(json);

			const sib = graph.nodes.get('SplitInBatches')!;

			// SplitInBatches output 0 should be 'done'
			expect(sib.outputs.get('done')).toEqual([
				{ target: 'DoneHandler', targetInputSlot: 'input0' },
			]);

			// SplitInBatches output 1 should be 'loop'
			expect(sib.outputs.get('loop')).toEqual([{ target: 'LoopBody', targetInputSlot: 'input0' }]);
		});

		it('handles fan-out (one source to multiple targets)', () => {
			const json: WorkflowJSON = {
				name: 'Test',
				nodes: [
					{
						id: '1',
						name: 'Trigger',
						type: 'n8n-nodes-base.manualTrigger',
						typeVersion: 1,
						position: [0, 0],
					},
					{
						id: '2',
						name: 'Target1',
						type: 'n8n-nodes-base.noOp',
						typeVersion: 1,
						position: [100, -50],
					},
					{
						id: '3',
						name: 'Target2',
						type: 'n8n-nodes-base.noOp',
						typeVersion: 1,
						position: [100, 50],
					},
				],
				connections: {
					Trigger: {
						main: [
							[
								{ node: 'Target1', type: 'main', index: 0 },
								{ node: 'Target2', type: 'main', index: 0 },
							],
						],
					},
				},
			};

			const graph = buildSemanticGraph(json);

			const trigger = graph.nodes.get('Trigger')!;

			// Trigger output0 should connect to both targets
			expect(trigger.outputs.get('output0')).toHaveLength(2);
			expect(trigger.outputs.get('output0')).toContainEqual({
				target: 'Target1',
				targetInputSlot: 'input0',
			});
			expect(trigger.outputs.get('output0')).toContainEqual({
				target: 'Target2',
				targetInputSlot: 'input0',
			});
		});

		it('identifies trigger nodes as roots', () => {
			const json: WorkflowJSON = {
				name: 'Test',
				nodes: [
					{
						id: '1',
						name: 'Trigger',
						type: 'n8n-nodes-base.manualTrigger',
						typeVersion: 1,
						position: [0, 0],
					},
					{
						id: '2',
						name: 'Process',
						type: 'n8n-nodes-base.noOp',
						typeVersion: 1,
						position: [100, 0],
					},
				],
				connections: {
					Trigger: {
						main: [[{ node: 'Process', type: 'main', index: 0 }]],
					},
				},
			};

			const graph = buildSemanticGraph(json);

			expect(graph.roots).toContain('Trigger');
			expect(graph.roots).not.toContain('Process');
		});

		it('identifies orphan nodes as roots', () => {
			const json: WorkflowJSON = {
				name: 'Test',
				nodes: [
					{
						id: '1',
						name: 'Trigger',
						type: 'n8n-nodes-base.manualTrigger',
						typeVersion: 1,
						position: [0, 0],
					},
					{
						id: '2',
						name: 'Orphan',
						type: 'n8n-nodes-base.noOp',
						typeVersion: 1,
						position: [100, 100],
					},
				],
				connections: {},
			};

			const graph = buildSemanticGraph(json);

			// Both should be roots (trigger + orphan)
			expect(graph.roots).toContain('Trigger');
			expect(graph.roots).toContain('Orphan');
		});

		it('parses AI subnode connections', () => {
			const json: WorkflowJSON = {
				name: 'Test',
				nodes: [
					{
						id: '1',
						name: 'Agent',
						type: '@n8n/n8n-nodes-langchain.agent',
						typeVersion: 1.7,
						position: [0, 0],
					},
					{
						id: '2',
						name: 'Model',
						type: '@n8n/n8n-nodes-langchain.lmChatOpenAi',
						typeVersion: 1,
						position: [-100, 50],
					},
					{
						id: '3',
						name: 'Tool',
						type: '@n8n/n8n-nodes-langchain.toolSerpApi',
						typeVersion: 1,
						position: [-100, 100],
					},
				],
				connections: {
					Model: {
						ai_languageModel: [[{ node: 'Agent', type: 'ai_languageModel', index: 0 }]],
					},
					Tool: {
						ai_tool: [[{ node: 'Agent', type: 'ai_tool', index: 0 }]],
					},
				},
			};

			const graph = buildSemanticGraph(json);

			const agent = graph.nodes.get('Agent')!;

			expect(agent.subnodes).toHaveLength(2);
			expect(agent.subnodes).toContainEqual({
				connectionType: 'ai_languageModel',
				subnodeName: 'Model',
				index: 0,
			});
			expect(agent.subnodes).toContainEqual({
				connectionType: 'ai_tool',
				subnodeName: 'Tool',
				index: 0,
			});
		});

		it('initializes annotations to default values', () => {
			const json: WorkflowJSON = {
				name: 'Test',
				nodes: [
					{
						id: '1',
						name: 'Process',
						type: 'n8n-nodes-base.noOp',
						typeVersion: 1,
						position: [0, 0],
					},
				],
				connections: {},
			};

			const graph = buildSemanticGraph(json);

			const node = graph.nodes.get('Process')!;

			expect(node.annotations.isTrigger).toBe(false);
			expect(node.annotations.isCycleTarget).toBe(false);
			expect(node.annotations.isConvergencePoint).toBe(false);
		});

		it('marks trigger nodes in annotations', () => {
			const json: WorkflowJSON = {
				name: 'Test',
				nodes: [
					{
						id: '1',
						name: 'Trigger',
						type: 'n8n-nodes-base.manualTrigger',
						typeVersion: 1,
						position: [0, 0],
					},
					{
						id: '2',
						name: 'WebhookTrigger',
						type: 'n8n-nodes-base.webhook',
						typeVersion: 1,
						position: [0, 100],
					},
				],
				connections: {},
			};

			const graph = buildSemanticGraph(json);

			expect(graph.nodes.get('Trigger')!.annotations.isTrigger).toBe(true);
			expect(graph.nodes.get('WebhookTrigger')!.annotations.isTrigger).toBe(true);
		});

		it('handles empty workflow', () => {
			const json: WorkflowJSON = {
				name: 'Empty',
				nodes: [],
				connections: {},
			};

			const graph = buildSemanticGraph(json);

			expect(graph.nodes.size).toBe(0);
			expect(graph.roots).toHaveLength(0);
		});

		it('parses error type connections', () => {
			const json: WorkflowJSON = {
				name: 'Test',
				nodes: [
					{
						id: '1',
						name: 'HTTP',
						type: 'n8n-nodes-base.httpRequest',
						typeVersion: 4,
						position: [0, 0],
						onError: 'continueErrorOutput',
					},
					{
						id: '2',
						name: 'ErrorHandler',
						type: 'n8n-nodes-base.noOp',
						typeVersion: 1,
						position: [200, 100],
					},
				],
				connections: {
					HTTP: {
						error: [[{ node: 'ErrorHandler', type: 'main', index: 0 }]],
					},
				},
			};

			const graph = buildSemanticGraph(json);
			const http = graph.nodes.get('HTTP')!;

			expect(http.outputs.get('error')).toEqual([
				{ target: 'ErrorHandler', targetInputSlot: 'input0' },
			]);
		});

		it('normalizes string typeVersion to number', () => {
			const json: WorkflowJSON = {
				name: 'Test',
				nodes: [
					{
						id: '1',
						name: 'Node',
						type: 'n8n-nodes-base.noOp',
						typeVersion: '2' as unknown as number,
						position: [0, 0],
					},
				],
				connections: {},
			};

			const graph = buildSemanticGraph(json);
			const node = graph.nodes.get('Node')!;

			expect(node.json.typeVersion).toBe(2);
			expect(typeof node.json.typeVersion).toBe('number');
		});

		it('handles duplicate node names with unique graph keys', () => {
			const json: WorkflowJSON = {
				name: 'Test',
				nodes: [
					{
						id: '1',
						name: 'HTTP',
						type: 'n8n-nodes-base.httpRequest',
						typeVersion: 4,
						position: [0, 0],
					},
					{
						id: '2',
						name: 'HTTP',
						type: 'n8n-nodes-base.httpRequest',
						typeVersion: 4,
						position: [200, 0],
					},
				],
				connections: {},
			};

			const graph = buildSemanticGraph(json);

			// Both nodes should exist with unique keys
			expect(graph.nodes.size).toBe(2);
			expect(graph.nodes.has('HTTP')).toBe(true);
			expect(graph.nodes.has('HTTP 2')).toBe(true);
		});

		it('normalizes flat tuple connections to object format', () => {
			const json: WorkflowJSON = {
				name: 'Test',
				nodes: [
					{
						id: '1',
						name: 'Trigger',
						type: 'n8n-nodes-base.manualTrigger',
						typeVersion: 1,
						position: [0, 0],
					},
					{
						id: '2',
						name: 'Process',
						type: 'n8n-nodes-base.noOp',
						typeVersion: 1,
						position: [200, 0],
					},
				],
				connections: {
					Trigger: {
						main: [['Process', 'main', 0] as unknown as null],
					},
				},
			};

			const graph = buildSemanticGraph(json);
			const trigger = graph.nodes.get('Trigger')!;

			expect(trigger.outputs.get('output0')).toEqual([
				{ target: 'Process', targetInputSlot: 'input0' },
			]);
		});

		it('does not mutate the input WorkflowJSON', () => {
			const json: WorkflowJSON = {
				name: 'Test',
				nodes: [
					{
						id: '1',
						name: 'Node',
						type: 'n8n-nodes-base.noOp',
						typeVersion: '2' as unknown as number,
						position: [0, 0],
					},
				],
				connections: {
					Node: {
						main: [['Target', 'main', 0] as unknown as null],
					},
				},
			};
			const originalConnections = JSON.stringify(json.connections);
			const originalTypeVersion = json.nodes[0].typeVersion;

			buildSemanticGraph(json);

			expect(json.nodes[0].typeVersion).toBe(originalTypeVersion); // still string
			expect(JSON.stringify(json.connections)).toBe(originalConnections); // still flat tuple
		});
	});
});
