import type { INodeTypes } from 'n8n-workflow';

import { validateWorkflow } from '../validation';
import { workflow } from '../workflow-builder';
import { node, trigger, merge } from '../workflow-builder/node-builders/node-builder';

describe('input index validation', () => {
	const mockNodeTypesProvider: INodeTypes = {
		getByNameAndVersion: (type: string) => {
			if (type === 'n8n-nodes-base.aggregate') {
				return { description: { inputs: ['main'] } };
			}
			if (type === 'n8n-nodes-base.set') {
				return { description: { inputs: ['main'] } };
			}
			if (type === 'n8n-nodes-base.merge') {
				// Merge uses expression-based inputs
				return { description: { inputs: '={{$parameter.numberInputs}}' } };
			}
			if (type === 'n8n-nodes-base.manualTrigger') {
				return { description: { inputs: [] } };
			}
			// Default single input
			return { description: { inputs: ['main'] } };
		},
		getByName: (type: string) => mockNodeTypesProvider.getByNameAndVersion(type),
		getKnownTypes: () => ({}),
	} as INodeTypes;

	it('warns when connecting to invalid input index on single-input node', () => {
		const myTrigger = trigger({ type: 'n8n-nodes-base.manualTrigger', version: 1, config: {} });
		const aggregate = node({ type: 'n8n-nodes-base.aggregate', version: 1, config: {} });

		// Invalid! Aggregate has only input 0
		const wf = workflow('test-id', 'Test').add(myTrigger.to(aggregate.input(1)));

		const result = validateWorkflow(wf, { nodeTypesProvider: mockNodeTypesProvider });

		expect(result.warnings).toContainEqual(
			expect.objectContaining({ code: 'INVALID_INPUT_INDEX' }),
		);
	});

	it('does not warn for valid input index', () => {
		const myTrigger = trigger({ type: 'n8n-nodes-base.manualTrigger', version: 1, config: {} });
		const aggregate = node({ type: 'n8n-nodes-base.aggregate', version: 1, config: {} });

		// Valid - input 0 exists
		const wf = workflow('test-id', 'Test').add(myTrigger.to(aggregate.input(0)));

		const result = validateWorkflow(wf, { nodeTypesProvider: mockNodeTypesProvider });

		expect(result.warnings.filter((w) => w.code === 'INVALID_INPUT_INDEX')).toHaveLength(0);
	});

	it('skips validation when no provider given (backward compatible)', () => {
		const myTrigger = trigger({ type: 'n8n-nodes-base.manualTrigger', version: 1, config: {} });
		const aggregate = node({ type: 'n8n-nodes-base.aggregate', version: 1, config: {} });

		// Would be invalid, but no provider
		const wf = workflow('test-id', 'Test').add(myTrigger.to(aggregate.input(5)));

		const result = validateWorkflow(wf); // No provider

		expect(result.warnings.filter((w) => w.code === 'INVALID_INPUT_INDEX')).toHaveLength(0);
	});

	it('warns when a Merge input index exceeds numberInputs even though inputs are dynamic', () => {
		const myTrigger = trigger({ type: 'n8n-nodes-base.manualTrigger', version: 1, config: {} });
		const mergeNode = merge({ version: 3 });

		// checkNodeInputIndices can't resolve dynamic inputs, but the Merge-specific
		// check uses the numberInputs parameter (default 2) and catches the overflow.
		const wf = workflow('test-id', 'Test').add(myTrigger.to(mergeNode.input(5)));

		const result = validateWorkflow(wf, { nodeTypesProvider: mockNodeTypesProvider });

		const mergeWarnings = result.warnings.filter((w) => w.code === 'INVALID_INPUT_INDEX');
		expect(mergeWarnings).toHaveLength(1);
		expect(mergeWarnings[0].message).toContain("'numberInputs' is 2");
	});

	it('reports warning with node name and input index in message', () => {
		const myTrigger = trigger({ type: 'n8n-nodes-base.manualTrigger', version: 1, config: {} });
		const aggregate = node({
			type: 'n8n-nodes-base.aggregate',
			version: 1,
			config: { name: 'My Aggregate' },
		});

		const wf = workflow('test-id', 'Test').add(myTrigger.to(aggregate.input(2)));

		const result = validateWorkflow(wf, { nodeTypesProvider: mockNodeTypesProvider });

		const warning = result.warnings.find((w) => w.code === 'INVALID_INPUT_INDEX');
		expect(warning).toBeDefined();
		expect(warning?.nodeName).toBe('My Aggregate');
		expect(warning?.message).toContain('input index 2');
		expect(warning?.message).toContain('only has 1 input');
	});

	it('warns for multiple invalid inputs to same node', () => {
		const trigger1 = trigger({
			type: 'n8n-nodes-base.manualTrigger',
			version: 1,
			config: { name: 'Trigger 1' },
		});
		const trigger2 = trigger({
			type: 'n8n-nodes-base.manualTrigger',
			version: 1,
			config: { name: 'Trigger 2' },
		});
		const aggregate = node({
			type: 'n8n-nodes-base.aggregate',
			version: 1,
			config: { name: 'Aggregate' },
		});

		const wf = workflow('test-id', 'Test')
			.add(trigger1.to(aggregate.input(1)))
			.add(trigger2.to(aggregate.input(2)));

		const result = validateWorkflow(wf, { nodeTypesProvider: mockNodeTypesProvider });

		const invalidInputWarnings = result.warnings.filter((w) => w.code === 'INVALID_INPUT_INDEX');
		expect(invalidInputWarnings.length).toBe(2);
	});
});

describe('merge node input-count validation', () => {
	it('warns when connections exceed the default numberInputs of 2', () => {
		const result = validateWorkflow({
			id: 'test-id',
			name: 'Test',
			nodes: [
				{
					id: '1',
					name: 'Trigger',
					type: 'n8n-nodes-base.manualTrigger',
					typeVersion: 1,
					position: [0, 0],
				},
				{ id: 'a', name: 'A', type: 'n8n-nodes-base.set', typeVersion: 3, position: [100, 0] },
				{ id: 'b', name: 'B', type: 'n8n-nodes-base.set', typeVersion: 3, position: [200, 0] },
				{ id: 'c', name: 'C', type: 'n8n-nodes-base.set', typeVersion: 3, position: [300, 0] },
				// Merge has no parameters → numberInputs defaults to 2, but three branches connect.
				{
					id: 'm',
					name: 'Merge',
					type: 'n8n-nodes-base.merge',
					typeVersion: 3.2,
					position: [500, 0],
					parameters: {},
				},
			],
			connections: {
				Trigger: { main: [[{ node: 'A', type: 'main', index: 0 }]] },
				A: { main: [[{ node: 'Merge', type: 'main', index: 0 }]] },
				B: { main: [[{ node: 'Merge', type: 'main', index: 1 }]] },
				C: { main: [[{ node: 'Merge', type: 'main', index: 2 }]] },
			},
		});

		const warning = result.warnings.find(
			(w) => w.code === 'INVALID_INPUT_INDEX' && w.nodeName === 'Merge',
		);
		expect(warning).toBeDefined();
		expect(warning?.message).toContain("'numberInputs' is 2");
		expect(warning?.message).toContain('input index 2');
		expect(warning?.parameterPath).toBe('numberInputs');
	});

	it('does not warn when numberInputs matches connection count', () => {
		const result = validateWorkflow({
			id: 'test-id',
			name: 'Test',
			nodes: [
				{ id: 'a', name: 'A', type: 'n8n-nodes-base.set', typeVersion: 3, position: [100, 0] },
				{ id: 'b', name: 'B', type: 'n8n-nodes-base.set', typeVersion: 3, position: [200, 0] },
				{ id: 'c', name: 'C', type: 'n8n-nodes-base.set', typeVersion: 3, position: [300, 0] },
				{
					id: 'm',
					name: 'Merge',
					type: 'n8n-nodes-base.merge',
					typeVersion: 3.2,
					position: [500, 0],
					parameters: { numberInputs: 3 },
				},
			],
			connections: {
				A: { main: [[{ node: 'Merge', type: 'main', index: 0 }]] },
				B: { main: [[{ node: 'Merge', type: 'main', index: 1 }]] },
				C: { main: [[{ node: 'Merge', type: 'main', index: 2 }]] },
			},
		});

		const warning = result.warnings.find(
			(w) => w.code === 'INVALID_INPUT_INDEX' && w.nodeName === 'Merge',
		);
		expect(warning).toBeUndefined();
	});

	it('does not warn on the default 2-branch merge case', () => {
		const result = validateWorkflow({
			id: 'test-id',
			name: 'Test',
			nodes: [
				{ id: 'a', name: 'A', type: 'n8n-nodes-base.set', typeVersion: 3, position: [100, 0] },
				{ id: 'b', name: 'B', type: 'n8n-nodes-base.set', typeVersion: 3, position: [200, 0] },
				{
					id: 'm',
					name: 'Merge',
					type: 'n8n-nodes-base.merge',
					typeVersion: 3.2,
					position: [500, 0],
					parameters: {},
				},
			],
			connections: {
				A: { main: [[{ node: 'Merge', type: 'main', index: 0 }]] },
				B: { main: [[{ node: 'Merge', type: 'main', index: 1 }]] },
			},
		});

		const warning = result.warnings.find(
			(w) => w.code === 'INVALID_INPUT_INDEX' && w.nodeName === 'Merge',
		);
		expect(warning).toBeUndefined();
	});

	it('suggests the correct numberInputs value in the message', () => {
		const result = validateWorkflow({
			id: 'test-id',
			name: 'Test',
			nodes: [
				{ id: 'a', name: 'A', type: 'n8n-nodes-base.set', typeVersion: 3, position: [100, 0] },
				{ id: 'b', name: 'B', type: 'n8n-nodes-base.set', typeVersion: 3, position: [200, 0] },
				{ id: 'c', name: 'C', type: 'n8n-nodes-base.set', typeVersion: 3, position: [300, 0] },
				{ id: 'd', name: 'D', type: 'n8n-nodes-base.set', typeVersion: 3, position: [400, 0] },
				{
					id: 'm',
					name: 'Merge',
					type: 'n8n-nodes-base.merge',
					typeVersion: 3.2,
					position: [500, 0],
					parameters: { numberInputs: 2 },
				},
			],
			connections: {
				A: { main: [[{ node: 'Merge', type: 'main', index: 0 }]] },
				B: { main: [[{ node: 'Merge', type: 'main', index: 1 }]] },
				C: { main: [[{ node: 'Merge', type: 'main', index: 2 }]] },
				D: { main: [[{ node: 'Merge', type: 'main', index: 3 }]] },
			},
		});

		const warning = result.warnings.find(
			(w) => w.code === 'INVALID_INPUT_INDEX' && w.nodeName === 'Merge',
		);
		expect(warning).toBeDefined();
		expect(warning?.message).toContain("'numberInputs' to 4");
	});
});
