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

import * as image from './image';
import { router } from './router';
import * as text from './text';

vi.mock('./image');
vi.mock('./text');

describe('Ollama Router', () => {
	const executeFunctionsMock = mockDeep<IExecuteFunctions>();
	const mockImageExecute = vi.fn();
	const mockTextExecute = vi.fn();

	beforeEach(() => {
		vi.resetAllMocks();
		(image as any).analyze = { execute: mockImageExecute };
		(text as any).message = { execute: mockTextExecute };

		executeFunctionsMock.getInputData.mockReturnValue([
			{ json: { input: 'test1' } },
			{ json: { input: 'test2' } },
		]);
	});

	describe('router', () => {
		it('should route to text.message operation', async () => {
			executeFunctionsMock.getNodeParameter.mockImplementation((parameter: string) => {
				if (parameter === 'resource') return 'text';
				if (parameter === 'operation') return 'message';
				return undefined;
			});

			mockTextExecute.mockResolvedValueOnce([
				{ json: { result: 'response1' }, pairedItem: { item: 0 } },
			]);
			mockTextExecute.mockResolvedValueOnce([
				{ json: { result: 'response2' }, pairedItem: { item: 1 } },
			]);

			const result = await router.call(executeFunctionsMock);

			expect(result).toEqual([
				[
					{ json: { result: 'response1' }, pairedItem: { item: 0 } },
					{ json: { result: 'response2' }, pairedItem: { item: 1 } },
				],
			]);
			expect(mockTextExecute).toHaveBeenCalledTimes(2);
			expect(mockTextExecute).toHaveBeenNthCalledWith(1, 0);
			expect(mockTextExecute).toHaveBeenNthCalledWith(2, 1);
		});

		it('should route to image.analyze operation', async () => {
			executeFunctionsMock.getNodeParameter.mockImplementation((parameter: string) => {
				if (parameter === 'resource') return 'image';
				if (parameter === 'operation') return 'analyze';
				return undefined;
			});

			mockImageExecute.mockResolvedValueOnce([
				{ json: { result: 'image analysis 1' }, pairedItem: { item: 0 } },
			]);
			mockImageExecute.mockResolvedValueOnce([
				{ json: { result: 'image analysis 2' }, pairedItem: { item: 1 } },
			]);

			const result = await router.call(executeFunctionsMock);

			expect(result).toEqual([
				[
					{ json: { result: 'image analysis 1' }, pairedItem: { item: 0 } },
					{ json: { result: 'image analysis 2' }, pairedItem: { item: 1 } },
				],
			]);
			expect(mockImageExecute).toHaveBeenCalledTimes(2);
			expect(mockImageExecute).toHaveBeenNthCalledWith(1, 0);
			expect(mockImageExecute).toHaveBeenNthCalledWith(2, 1);
		});

		it('should throw error for unsupported resource', async () => {
			executeFunctionsMock.getNodeParameter.mockImplementation((parameter: string) => {
				if (parameter === 'resource') return 'unsupported';
				if (parameter === 'operation') return 'test';
				return undefined;
			});

			const mockNode = { name: 'Ollama', type: 'n8n-nodes-langchain.ollama' } as INode;
			executeFunctionsMock.getNode.mockReturnValue(mockNode);

			await expect(router.call(executeFunctionsMock)).rejects.toThrow(NodeOperationError);
			await expect(router.call(executeFunctionsMock)).rejects.toThrow(
				'The resource "unsupported" is not supported!',
			);
		});

		it('should handle execution errors with continueOnFail enabled', async () => {
			executeFunctionsMock.getNodeParameter.mockImplementation((parameter: string) => {
				if (parameter === 'resource') return 'text';
				if (parameter === 'operation') return 'message';
				return undefined;
			});

			executeFunctionsMock.continueOnFail.mockReturnValue(true);
			mockTextExecute.mockResolvedValueOnce([
				{ json: { result: 'success' }, pairedItem: { item: 0 } },
			]);
			mockTextExecute.mockRejectedValueOnce(new Error('API Error'));

			const result = await router.call(executeFunctionsMock);

			expect(result).toEqual([
				[
					{ json: { result: 'success' }, pairedItem: { item: 0 } },
					{ json: { error: 'API Error' }, pairedItem: { item: 1 } },
				],
			]);
			expect(mockTextExecute).toHaveBeenCalledTimes(2);
		});

		it('should throw NodeOperationError when continueOnFail is disabled', async () => {
			executeFunctionsMock.getNodeParameter.mockImplementation((parameter: string) => {
				if (parameter === 'resource') return 'text';
				if (parameter === 'operation') return 'message';
				return undefined;
			});

			executeFunctionsMock.continueOnFail.mockReturnValue(false);
			const mockNode = { name: 'Ollama', type: 'n8n-nodes-langchain.ollama' } as INode;
			executeFunctionsMock.getNode.mockReturnValue(mockNode);

			const originalError = new Error('API Connection Failed');
			mockTextExecute.mockRejectedValueOnce(originalError);

			await expect(router.call(executeFunctionsMock)).rejects.toThrow(NodeOperationError);
		});

		it('should process multiple items and accumulate results', async () => {
			executeFunctionsMock.getInputData.mockReturnValue([
				{ json: { input: 'test1' } },
				{ json: { input: 'test2' } },
				{ json: { input: 'test3' } },
			]);

			executeFunctionsMock.getNodeParameter.mockImplementation((parameter: string) => {
				if (parameter === 'resource') return 'text';
				if (parameter === 'operation') return 'message';
				return undefined;
			});

			mockTextExecute.mockResolvedValueOnce([
				{ json: { result: 'response1' }, pairedItem: { item: 0 } },
			]);
			mockTextExecute.mockResolvedValueOnce([
				{ json: { result: 'response2a' }, pairedItem: { item: 1 } },
				{ json: { result: 'response2b' }, pairedItem: { item: 1 } },
			]);
			mockTextExecute.mockResolvedValueOnce([
				{ json: { result: 'response3' }, pairedItem: { item: 2 } },
			]);

			const result = await router.call(executeFunctionsMock);

			expect(result).toEqual([
				[
					{ json: { result: 'response1' }, pairedItem: { item: 0 } },
					{ json: { result: 'response2a' }, pairedItem: { item: 1 } },
					{ json: { result: 'response2b' }, pairedItem: { item: 1 } },
					{ json: { result: 'response3' }, pairedItem: { item: 2 } },
				],
			]);
			expect(mockTextExecute).toHaveBeenCalledTimes(3);
			expect(mockTextExecute).toHaveBeenNthCalledWith(1, 0);
			expect(mockTextExecute).toHaveBeenNthCalledWith(2, 1);
			expect(mockTextExecute).toHaveBeenNthCalledWith(3, 2);
		});

		it('should handle empty input data', async () => {
			executeFunctionsMock.getInputData.mockReturnValue([]);
			executeFunctionsMock.getNodeParameter.mockImplementation((parameter: string) => {
				if (parameter === 'resource') return 'text';
				if (parameter === 'operation') return 'message';
				return undefined;
			});

			const result = await router.call(executeFunctionsMock);

			expect(result).toEqual([[]]);
			expect(mockTextExecute).not.toHaveBeenCalled();
		});

		it('should handle mixed success and failure with continueOnFail', async () => {
			executeFunctionsMock.getInputData.mockReturnValue([
				{ json: { input: 'test1' } },
				{ json: { input: 'test2' } },
				{ json: { input: 'test3' } },
			]);

			executeFunctionsMock.getNodeParameter.mockImplementation((parameter: string) => {
				if (parameter === 'resource') return 'text';
				if (parameter === 'operation') return 'message';
				return undefined;
			});

			executeFunctionsMock.continueOnFail.mockReturnValue(true);
			mockTextExecute.mockResolvedValueOnce([
				{ json: { result: 'success1' }, pairedItem: { item: 0 } },
			]);
			mockTextExecute.mockRejectedValueOnce(new Error('Error in item 2'));
			mockTextExecute.mockResolvedValueOnce([
				{ json: { result: 'success3' }, pairedItem: { item: 2 } },
			]);

			const result = await router.call(executeFunctionsMock);

			expect(result).toEqual([
				[
					{ json: { result: 'success1' }, pairedItem: { item: 0 } },
					{ json: { error: 'Error in item 2' }, pairedItem: { item: 1 } },
					{ json: { result: 'success3' }, pairedItem: { item: 2 } },
				],
			]);
		});
	});
});
