import { mockDeep } from 'jest-mock-extended';
import type { IExecuteFunctions, ExecuteAgentData } from 'n8n-workflow';
import { NodeOperationError } from 'n8n-workflow';

import { MessageAnAgent } from '../MessageAnAgent.node';

describe('MessageAnAgent Node', () => {
	let node: MessageAnAgent;
	let executeFunctions: jest.Mocked<IExecuteFunctions>;

	const mockSession = {
		agentId: 'agent-1',
		projectId: 'project-1',
		sessionId: 'exec-123-0',
	};

	const mockAgentResult: ExecuteAgentData = {
		response: 'Hello from agent',
		structuredOutput: null,
		usage: {
			promptTokens: 10,
			completionTokens: 20,
			totalTokens: 30,
		},
		toolCalls: [],
		finishReason: 'stop',
		session: mockSession,
	};

	beforeEach(() => {
		node = new MessageAnAgent();
		executeFunctions = mockDeep<IExecuteFunctions>();
		jest.clearAllMocks();

		executeFunctions.getNode.mockReturnValue({
			id: 'test-node-id',
			name: 'Message an Agent',
			type: 'n8n-nodes-base.messageAnAgent',
			typeVersion: 1,
			position: [0, 0],
			parameters: {},
		});

		executeFunctions.getExecutionId.mockReturnValue('exec-123');
	});

	it('should send a message and return the agent response', async () => {
		executeFunctions.getInputData.mockReturnValue([{ json: {} }]);
		executeFunctions.getNodeParameter.mockImplementation(
			(param: string, _itemIndex?: number, fallback?: unknown) => {
				if (param === 'agentId') return { mode: 'id', value: 'agent-1' };
				if (param === 'message') return 'Hello agent';
				if (param === 'advanced') return fallback ?? {};
				return undefined;
			},
		);
		executeFunctions.executeAgent.mockResolvedValue(mockAgentResult);

		const result = await node.execute.call(executeFunctions);

		expect(executeFunctions.executeAgent).toHaveBeenCalledWith(
			{ agentId: 'agent-1', sessionId: undefined },
			'Hello agent',
			'exec-123',
			0,
		);
		expect(result).toEqual([
			[
				{
					json: {
						response: 'Hello from agent',
						structuredOutput: null,
						usage: { promptTokens: 10, completionTokens: 20, totalTokens: 30 },
						toolCalls: [],
						finishReason: 'stop',
						session: mockSession,
					},
					pairedItem: { item: 0 },
				},
			],
		]);
	});

	it('should forward a user-supplied sessionId from the Advanced collection', async () => {
		executeFunctions.getInputData.mockReturnValue([{ json: {} }]);
		executeFunctions.getNodeParameter.mockImplementation((param: string) => {
			if (param === 'agentId') return { mode: 'id', value: 'agent-1' };
			if (param === 'message') return 'Hello agent';
			if (param === 'advanced') return { sessionId: '  thread-42  ' };
			return undefined as unknown as string;
		});
		executeFunctions.executeAgent.mockResolvedValue(mockAgentResult);

		await node.execute.call(executeFunctions);

		expect(executeFunctions.executeAgent).toHaveBeenCalledWith(
			{ agentId: 'agent-1', sessionId: 'thread-42' },
			'Hello agent',
			'exec-123',
			0,
		);
	});

	it('should treat a whitespace-only sessionId as no override', async () => {
		executeFunctions.getInputData.mockReturnValue([{ json: {} }]);
		executeFunctions.getNodeParameter.mockImplementation((param: string) => {
			if (param === 'agentId') return { mode: 'id', value: 'agent-1' };
			if (param === 'message') return 'Hello agent';
			if (param === 'advanced') return { sessionId: '   ' };
			return undefined as unknown as string;
		});
		executeFunctions.executeAgent.mockResolvedValue(mockAgentResult);

		await node.execute.call(executeFunctions);

		expect(executeFunctions.executeAgent).toHaveBeenCalledWith(
			{ agentId: 'agent-1', sessionId: undefined },
			'Hello agent',
			'exec-123',
			0,
		);
	});

	it('should throw NodeOperationError when message is empty', async () => {
		executeFunctions.getInputData.mockReturnValue([{ json: {} }]);
		executeFunctions.getNodeParameter.mockImplementation(
			(param: string, _itemIndex?: number, fallback?: unknown) => {
				if (param === 'agentId') return { mode: 'id', value: 'agent-1' };
				if (param === 'message') return '   ';
				if (param === 'advanced') return fallback ?? {};
				return undefined;
			},
		);
		executeFunctions.continueOnFail.mockReturnValue(false);

		await expect(node.execute.call(executeFunctions)).rejects.toThrow(NodeOperationError);
		await expect(node.execute.call(executeFunctions)).rejects.toThrow('Message cannot be empty');
	});

	it('should process multiple items with different itemIndex values', async () => {
		executeFunctions.getInputData.mockReturnValue([{ json: {} }, { json: {} }]);
		executeFunctions.getNodeParameter.mockImplementation(
			(param: string, itemIndex?: number, fallback?: unknown) => {
				if (param === 'agentId') return { mode: 'id', value: `agent-${(itemIndex ?? 0) + 1}` };
				if (param === 'message') return `Message ${(itemIndex ?? 0) + 1}`;
				if (param === 'advanced') return fallback ?? {};
				return undefined;
			},
		);

		const resultForItem0: ExecuteAgentData = {
			...mockAgentResult,
			response: 'Response 1',
		};
		const resultForItem1: ExecuteAgentData = {
			...mockAgentResult,
			response: 'Response 2',
		};

		executeFunctions.executeAgent
			.mockResolvedValueOnce(resultForItem0)
			.mockResolvedValueOnce(resultForItem1);

		const result = await node.execute.call(executeFunctions);

		expect(executeFunctions.executeAgent).toHaveBeenCalledTimes(2);
		expect(executeFunctions.executeAgent).toHaveBeenCalledWith(
			{ agentId: 'agent-1', sessionId: undefined },
			'Message 1',
			'exec-123',
			0,
		);
		expect(executeFunctions.executeAgent).toHaveBeenCalledWith(
			{ agentId: 'agent-2', sessionId: undefined },
			'Message 2',
			'exec-123',
			1,
		);
		expect(result[0]).toHaveLength(2);
		expect(result[0][0].json.response).toBe('Response 1');
		expect(result[0][0].pairedItem).toEqual({ item: 0 });
		expect(result[0][1].json.response).toBe('Response 2');
		expect(result[0][1].pairedItem).toEqual({ item: 1 });
	});

	it('should return error item instead of throwing when continueOnFail is true', async () => {
		executeFunctions.getInputData.mockReturnValue([{ json: {} }]);
		executeFunctions.getNodeParameter.mockImplementation(
			(param: string, _itemIndex?: number, fallback?: unknown) => {
				if (param === 'agentId') return { mode: 'id', value: 'agent-1' };
				if (param === 'message') return 'Hello';
				if (param === 'advanced') return fallback ?? {};
				return undefined;
			},
		);
		executeFunctions.continueOnFail.mockReturnValue(true);
		executeFunctions.executeAgent.mockRejectedValue(new Error('Agent unavailable'));

		const result = await node.execute.call(executeFunctions);

		expect(result).toEqual([
			[
				{
					json: { error: 'Agent unavailable' },
					pairedItem: { item: 0 },
				},
			],
		]);
	});

	it('should pass through structuredOutput from agent result', async () => {
		const structuredResult: ExecuteAgentData = {
			...mockAgentResult,
			structuredOutput: { key: 'value', nested: { data: 123 } },
			toolCalls: [{ toolName: 'search', input: { query: 'test' }, result: { found: true } }],
		};

		executeFunctions.getInputData.mockReturnValue([{ json: {} }]);
		executeFunctions.getNodeParameter.mockImplementation(
			(param: string, _itemIndex?: number, fallback?: unknown) => {
				if (param === 'agentId') return { mode: 'list', value: 'agent-1' };
				if (param === 'message') return 'Structured query';
				if (param === 'advanced') return fallback ?? {};
				return undefined;
			},
		);
		executeFunctions.executeAgent.mockResolvedValue(structuredResult);

		const result = await node.execute.call(executeFunctions);

		expect(result[0][0].json.structuredOutput).toEqual({
			key: 'value',
			nested: { data: 123 },
		});
		expect(result[0][0].json.toolCalls).toEqual([
			{ toolName: 'search', input: { query: 'test' }, result: { found: true } },
		]);
	});
});
