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

import * as helpers from '@utils/helpers';

import * as audio from './actions/audio';
import * as file from './actions/file';
import * as image from './actions/image';
import * as text from './actions/text';
import * as video from './actions/video';
import * as utils from './helpers/utils';
import * as transport from './transport';

describe('GoogleGemini Node', () => {
	const executeFunctionsMock = mockDeep<IExecuteFunctions>();
	const apiRequestMock = vi.spyOn(transport, 'apiRequest');
	const getConnectedToolsMock = vi.spyOn(helpers, 'getConnectedTools');
	const downloadFileMock = vi.spyOn(utils, 'downloadFile');
	const uploadFileMock = vi.spyOn(utils, 'uploadFile');
	const transferFileMock = vi.spyOn(utils, 'transferFile');

	beforeEach(() => {
		vi.clearAllMocks();
		executeFunctionsMock.getNode.mockReturnValue({ typeVersion: 1 } as INode);
	});

	describe('Text -> Message', () => {
		it('should call the api with the correct parameters', async () => {
			executeFunctionsMock.getNodeParameter.mockImplementation((parameter: string) => {
				switch (parameter) {
					case 'modelId':
						return 'models/gemini-2.5-flash';
					case 'messages.values':
						return [{ role: 'user', content: 'Hello, world!' }];
					case 'simplify':
						return true;
					case 'jsonOutput':
						return true;
					case 'options':
						return {
							systemMessage: 'You are a helpful assistant.',
							codeExecution: true,
							frequencyPenalty: 0,
							maxOutputTokens: 100,
							candidateCount: 1,
							presencePenalty: 0,
							temperature: 0.5,
							topP: 0.5,
							topK: 10,
						};
					default:
						return undefined;
				}
			});
			executeFunctionsMock.getNodeInputs.mockReturnValue([{ type: 'main' }, { type: 'ai_tool' }]);
			getConnectedToolsMock.mockResolvedValue([]);
			apiRequestMock.mockResolvedValue({
				candidates: [
					{
						content: {
							parts: [{ text: 'Hello, world!' }],
							role: 'model',
						},
					},
				],
			});

			const result = await text.message.execute.call(executeFunctionsMock, 0);

			expect(result).toEqual([
				{
					json: {
						content: {
							parts: [{ text: 'Hello, world!' }],
							role: 'model',
						},
					},
					pairedItem: { item: 0 },
				},
			]);
			expect(apiRequestMock).toHaveBeenCalledWith(
				'POST',
				'/v1beta/models/gemini-2.5-flash:generateContent',
				expect.objectContaining({
					body: {
						contents: [
							{
								parts: [{ text: 'Hello, world!' }],
								role: 'user',
							},
						],
						tools: [
							{
								codeExecution: {},
							},
						],
						generationConfig: {
							candidateCount: 1,
							frequencyPenalty: 0,
							maxOutputTokens: 100,
							presencePenalty: 0,
							temperature: 0.5,
							topP: 0.5,
							topK: 10,
							responseMimeType: 'application/json',
						},
						systemInstruction: {
							parts: [{ text: 'You are a helpful assistant.' }],
						},
					},
				}),
			);
		});

		it('should include thinking options when the thinking budget is specified', async () => {
			executeFunctionsMock.getNodeParameter.mockImplementation((parameter: string) => {
				switch (parameter) {
					case 'modelId':
						return 'models/gemini-2.5-flash';
					case 'messages.values':
						return [{ role: 'user', content: 'Hello, world!' }];
					case 'simplify':
						return true;
					case 'jsonOutput':
						return false;
					case 'options':
						return {
							thinkingBudget: 1024,
							maxOutputTokens: 100,
							temperature: 0.5,
						};
					default:
						return undefined;
				}
			});
			executeFunctionsMock.getNodeInputs.mockReturnValue([{ type: 'main' }]);
			apiRequestMock.mockResolvedValue({
				candidates: [
					{
						content: {
							parts: [{ text: 'Hello with thinking!' }],
							role: 'model',
						},
					},
				],
			});

			const result = await text.message.execute.call(executeFunctionsMock, 0);

			expect(result).toEqual([
				{
					json: {
						content: {
							parts: [{ text: 'Hello with thinking!' }],
							role: 'model',
						},
					},
					pairedItem: { item: 0 },
				},
			]);
			expect(apiRequestMock).toHaveBeenCalledWith(
				'POST',
				'/v1beta/models/gemini-2.5-flash:generateContent',
				{
					body: {
						contents: [
							{
								parts: [{ text: 'Hello, world!' }],
								role: 'user',
							},
						],
						tools: [],
						generationConfig: {
							maxOutputTokens: 100,
							temperature: 0.5,
							thinkingConfig: {
								thinkingBudget: 1024,
							},
						},
					},
				},
			);
		});

		describe('Version 1.0', () => {
			it('should use codeExecution from options for version 1.0', async () => {
				executeFunctionsMock.getNodeParameter.mockImplementation((parameter: string) => {
					switch (parameter) {
						case 'modelId':
							return 'models/gemini-2.5-flash';
						case 'messages.values':
							return [{ role: 'user', content: 'Hello, world!' }];
						case 'simplify':
							return true;
						case 'jsonOutput':
							return false;
						case 'builtInTools':
							return {};
						case 'options':
							return {
								codeExecution: true,
							};
						default:
							return undefined;
					}
				});
				executeFunctionsMock.getNodeInputs.mockReturnValue([{ type: 'main' }]);
				apiRequestMock.mockResolvedValue({
					candidates: [
						{
							content: {
								parts: [{ text: 'Hello, world!' }],
								role: 'model',
							},
						},
					],
				});

				await text.message.execute.call(executeFunctionsMock, 0);

				expect(apiRequestMock).toHaveBeenCalledWith(
					'POST',
					'/v1beta/models/gemini-2.5-flash:generateContent',
					expect.objectContaining({
						body: expect.objectContaining({
							tools: expect.arrayContaining([
								{
									codeExecution: {},
								},
							]),
						}),
					}),
				);
			});
		});

		describe('Version 1.1', () => {
			beforeEach(() => {
				executeFunctionsMock.getNode.mockReturnValue({ typeVersion: 1.1 } as INode);
			});

			it('should include Google Search tool when enabled', async () => {
				executeFunctionsMock.getNodeParameter.mockImplementation((parameter: string) => {
					switch (parameter) {
						case 'modelId':
							return 'models/gemini-2.5-flash';
						case 'messages.values':
							return [{ role: 'user', content: 'What is the weather today?' }];
						case 'simplify':
							return true;
						case 'jsonOutput':
							return false;
						case 'builtInTools':
							return {
								googleSearch: true,
							};
						case 'options':
							return {};
						default:
							return undefined;
					}
				});
				executeFunctionsMock.getNodeInputs.mockReturnValue([{ type: 'main' }]);
				apiRequestMock.mockResolvedValue({
					candidates: [
						{
							content: {
								parts: [{ text: 'The weather is sunny.' }],
								role: 'model',
							},
						},
					],
				});

				await text.message.execute.call(executeFunctionsMock, 0);

				expect(apiRequestMock).toHaveBeenCalledWith(
					'POST',
					'/v1beta/models/gemini-2.5-flash:generateContent',
					expect.objectContaining({
						body: expect.objectContaining({
							tools: expect.arrayContaining([
								{
									googleSearch: {},
								},
							]),
						}),
					}),
				);
			});

			it('should include Google Maps tool when enabled', async () => {
				executeFunctionsMock.getNodeParameter.mockImplementation((parameter: string) => {
					switch (parameter) {
						case 'modelId':
							return 'models/gemini-2.5-flash';
						case 'messages.values':
							return [{ role: 'user', content: 'Find restaurants near me' }];
						case 'simplify':
							return true;
						case 'jsonOutput':
							return false;
						case 'builtInTools':
							return {
								googleMaps: {},
							};
						case 'options':
							return {};
						default:
							return undefined;
					}
				});
				executeFunctionsMock.getNodeInputs.mockReturnValue([{ type: 'main' }]);
				apiRequestMock.mockResolvedValue({
					candidates: [
						{
							content: {
								parts: [{ text: 'Here are some restaurants.' }],
								role: 'model',
							},
						},
					],
				});

				await text.message.execute.call(executeFunctionsMock, 0);

				expect(apiRequestMock).toHaveBeenCalledWith(
					'POST',
					'/v1beta/models/gemini-2.5-flash:generateContent',
					expect.objectContaining({
						body: expect.objectContaining({
							tools: expect.arrayContaining([
								{
									googleMaps: {},
								},
							]),
						}),
					}),
				);
			});

			it('should include toolConfig with latLng when Google Maps has coordinates', async () => {
				executeFunctionsMock.getNodeParameter.mockImplementation((parameter: string) => {
					switch (parameter) {
						case 'modelId':
							return 'models/gemini-2.5-flash';
						case 'messages.values':
							return [{ role: 'user', content: 'Find restaurants near me' }];
						case 'simplify':
							return true;
						case 'jsonOutput':
							return false;
						case 'builtInTools':
							return {
								googleMaps: {
									latitude: 34.050481,
									longitude: -118.248526,
								},
							};
						case 'options':
							return {};
						default:
							return undefined;
					}
				});
				executeFunctionsMock.getNodeInputs.mockReturnValue([{ type: 'main' }]);
				apiRequestMock.mockResolvedValue({
					candidates: [
						{
							content: {
								parts: [{ text: 'Here are some restaurants.' }],
								role: 'model',
							},
						},
					],
				});

				await text.message.execute.call(executeFunctionsMock, 0);

				expect(apiRequestMock).toHaveBeenCalledWith(
					'POST',
					'/v1beta/models/gemini-2.5-flash:generateContent',
					expect.objectContaining({
						body: expect.objectContaining({
							tools: expect.arrayContaining([
								{
									googleMaps: {},
								},
							]),
							toolConfig: {
								retrievalConfig: {
									latLng: {
										latitude: 34.050481,
										longitude: -118.248526,
									},
								},
							},
						}),
					}),
				);
			});

			it('should include URL Context tool when enabled', async () => {
				executeFunctionsMock.getNodeParameter.mockImplementation((parameter: string) => {
					switch (parameter) {
						case 'modelId':
							return 'models/gemini-2.5-flash';
						case 'messages.values':
							return [{ role: 'user', content: 'Summarize this URL' }];
						case 'simplify':
							return true;
						case 'jsonOutput':
							return false;
						case 'builtInTools':
							return {
								urlContext: true,
							};
						case 'options':
							return {};
						default:
							return undefined;
					}
				});
				executeFunctionsMock.getNodeInputs.mockReturnValue([{ type: 'main' }]);
				apiRequestMock.mockResolvedValue({
					candidates: [
						{
							content: {
								parts: [{ text: 'URL summary.' }],
								role: 'model',
							},
						},
					],
				});

				await text.message.execute.call(executeFunctionsMock, 0);

				expect(apiRequestMock).toHaveBeenCalledWith(
					'POST',
					'/v1beta/models/gemini-2.5-flash:generateContent',
					expect.objectContaining({
						body: expect.objectContaining({
							tools: expect.arrayContaining([
								{
									urlContext: {},
								},
							]),
						}),
					}),
				);
			});

			it('should include File Search tool with fileSearchStoreNames when enabled', async () => {
				executeFunctionsMock.getNodeParameter.mockImplementation((parameter: string) => {
					switch (parameter) {
						case 'modelId':
							return 'models/gemini-2.5-flash';
						case 'messages.values':
							return [{ role: 'user', content: 'Search my files' }];
						case 'simplify':
							return true;
						case 'jsonOutput':
							return false;
						case 'builtInTools':
							return {
								fileSearch: {
									fileSearchStoreNames: '["store1", "store2"]',
								},
							};
						case 'options':
							return {};
						default:
							return undefined;
					}
				});
				executeFunctionsMock.getNodeInputs.mockReturnValue([{ type: 'main' }]);
				apiRequestMock.mockResolvedValue({
					candidates: [
						{
							content: {
								parts: [{ text: 'File search results.' }],
								role: 'model',
							},
						},
					],
				});

				await text.message.execute.call(executeFunctionsMock, 0);

				expect(apiRequestMock).toHaveBeenCalledWith(
					'POST',
					'/v1beta/models/gemini-2.5-flash:generateContent',
					expect.objectContaining({
						body: expect.objectContaining({
							tools: expect.arrayContaining([
								{
									fileSearch: {
										fileSearchStoreNames: ['store1', 'store2'],
									},
								},
							]),
						}),
					}),
				);
			});

			it('should include File Search tool with metadataFilter when provided', async () => {
				executeFunctionsMock.getNodeParameter.mockImplementation((parameter: string) => {
					switch (parameter) {
						case 'modelId':
							return 'models/gemini-2.5-flash';
						case 'messages.values':
							return [{ role: 'user', content: 'Tell me about the book' }];
						case 'simplify':
							return true;
						case 'jsonOutput':
							return false;
						case 'builtInTools':
							return {
								fileSearch: {
									fileSearchStoreNames: '["store1"]',
									metadataFilter: 'author="Robert Graves"',
								},
							};
						case 'options':
							return {};
						default:
							return undefined;
					}
				});
				executeFunctionsMock.getNodeInputs.mockReturnValue([{ type: 'main' }]);
				apiRequestMock.mockResolvedValue({
					candidates: [
						{
							content: {
								parts: [{ text: 'Book information.' }],
								role: 'model',
							},
						},
					],
				});

				await text.message.execute.call(executeFunctionsMock, 0);

				expect(apiRequestMock).toHaveBeenCalledWith(
					'POST',
					'/v1beta/models/gemini-2.5-flash:generateContent',
					expect.objectContaining({
						body: expect.objectContaining({
							tools: expect.arrayContaining([
								{
									fileSearch: {
										fileSearchStoreNames: ['store1'],
										metadataFilter: 'author="Robert Graves"',
									},
								},
							]),
						}),
					}),
				);
			});

			it('should include Code Execution tool when enabled', async () => {
				executeFunctionsMock.getNodeParameter.mockImplementation((parameter: string) => {
					switch (parameter) {
						case 'modelId':
							return 'models/gemini-2.5-flash';
						case 'messages.values':
							return [{ role: 'user', content: 'Calculate 2+2' }];
						case 'simplify':
							return true;
						case 'jsonOutput':
							return false;
						case 'builtInTools':
							return {
								codeExecution: true,
							};
						case 'options':
							return {};
						default:
							return undefined;
					}
				});
				executeFunctionsMock.getNodeInputs.mockReturnValue([{ type: 'main' }]);
				apiRequestMock.mockResolvedValue({
					candidates: [
						{
							content: {
								parts: [{ text: 'The result is 4.' }],
								role: 'model',
							},
						},
					],
				});

				await text.message.execute.call(executeFunctionsMock, 0);

				expect(apiRequestMock).toHaveBeenCalledWith(
					'POST',
					'/v1beta/models/gemini-2.5-flash:generateContent',
					expect.objectContaining({
						body: expect.objectContaining({
							tools: expect.arrayContaining([
								{
									codeExecution: {},
								},
							]),
						}),
					}),
				);
			});

			it('should include multiple built-in tools when enabled', async () => {
				executeFunctionsMock.getNodeParameter.mockImplementation((parameter: string) => {
					switch (parameter) {
						case 'modelId':
							return 'models/gemini-2.5-flash';
						case 'messages.values':
							return [{ role: 'user', content: 'Complex query' }];
						case 'simplify':
							return true;
						case 'jsonOutput':
							return false;
						case 'builtInTools':
							return {
								googleSearch: true,
								urlContext: true,
								codeExecution: true,
							};
						case 'options':
							return {};
						default:
							return undefined;
					}
				});
				executeFunctionsMock.getNodeInputs.mockReturnValue([{ type: 'main' }]);
				apiRequestMock.mockResolvedValue({
					candidates: [
						{
							content: {
								parts: [{ text: 'Response with multiple tools.' }],
								role: 'model',
							},
						},
					],
				});

				await text.message.execute.call(executeFunctionsMock, 0);

				expect(apiRequestMock).toHaveBeenCalledWith(
					'POST',
					'/v1beta/models/gemini-2.5-flash:generateContent',
					expect.objectContaining({
						body: expect.objectContaining({
							tools: expect.arrayContaining([
								{
									googleSearch: {},
								},
								{
									urlContext: {},
								},
								{
									codeExecution: {},
								},
							]),
						}),
					}),
				);
			});

			it('should not include toolConfig when Google Maps coordinates are empty', async () => {
				executeFunctionsMock.getNodeParameter.mockImplementation((parameter: string) => {
					switch (parameter) {
						case 'modelId':
							return 'models/gemini-2.5-flash';
						case 'messages.values':
							return [{ role: 'user', content: 'Find restaurants' }];
						case 'simplify':
							return true;
						case 'jsonOutput':
							return false;
						case 'builtInTools':
							return {
								googleMaps: {
									latitude: '',
									longitude: '',
								},
							};
						case 'options':
							return {};
						default:
							return undefined;
					}
				});
				executeFunctionsMock.getNodeInputs.mockReturnValue([{ type: 'main' }]);
				apiRequestMock.mockResolvedValue({
					candidates: [
						{
							content: {
								parts: [{ text: 'Restaurants found.' }],
								role: 'model',
							},
						},
					],
				});

				await text.message.execute.call(executeFunctionsMock, 0);

				expect(apiRequestMock).toHaveBeenCalledWith(
					'POST',
					'/v1beta/models/gemini-2.5-flash:generateContent',
					expect.objectContaining({
						body: expect.not.objectContaining({
							toolConfig: expect.anything(),
						}),
					}),
				);
				expect(apiRequestMock).toHaveBeenCalledWith(
					'POST',
					'/v1beta/models/gemini-2.5-flash:generateContent',
					expect.objectContaining({
						body: expect.objectContaining({
							tools: expect.arrayContaining([
								{
									googleMaps: {},
								},
							]),
						}),
					}),
				);
			});

			it('should handle undefined entries in parts array', async () => {
				executeFunctionsMock.getNodeParameter.mockImplementation((parameter: string) => {
					switch (parameter) {
						case 'modelId':
							return 'models/gemini-2.5-flash';
						case 'messages.values':
							return [{ role: 'user', content: 'foo' }];
						case 'simplify':
							return true;
						case 'jsonOutput':
							return false;
						case 'builtInTools':
							return {};
						case 'options':
							return {};
						case 'options.maxToolsIterations':
							return 15;
						default:
							return undefined;
					}
				});
				executeFunctionsMock.getNodeInputs.mockReturnValue([{ type: 'main' }]);
				apiRequestMock.mockResolvedValue({
					candidates: [
						{
							content: {
								parts: [undefined, { text: 'bar' }],
								role: 'model',
							},
						},
					],
				});

				const result = await text.message.execute.call(executeFunctionsMock, 0);

				expect(result).toEqual([
					{
						json: {
							content: {
								parts: [undefined, { text: 'bar' }],
								role: 'model',
							},
						},
						pairedItem: { item: 0 },
					},
				]);
			});

			describe('includeMergedResponse', () => {
				it('should include mergedResponse per candidate when enabled and simplify is true', async () => {
					executeFunctionsMock.getNodeParameter.mockImplementation((parameter: string) => {
						switch (parameter) {
							case 'modelId':
								return 'models/gemini-2.5-flash';
							case 'messages.values':
								return [{ role: 'user', content: 'Hello' }];
							case 'simplify':
								return true;
							case 'jsonOutput':
								return false;
							case 'builtInTools':
								return {};
							case 'options':
								return {
									includeMergedResponse: true,
								};
							case 'options.maxToolsIterations':
								return 15;
							default:
								return undefined;
						}
					});
					executeFunctionsMock.getNodeInputs.mockReturnValue([{ type: 'main' }]);
					apiRequestMock.mockResolvedValue({
						candidates: [
							{
								content: {
									parts: [{ text: 'Hello' }, { text: ' World' }],
									role: 'model',
								},
							},
						],
					});

					const result = await text.message.execute.call(executeFunctionsMock, 0);

					expect(result).toEqual([
						{
							json: {
								content: {
									parts: [{ text: 'Hello' }, { text: ' World' }],
									role: 'model',
								},
								mergedResponse: 'Hello World',
							},
							pairedItem: { item: 0 },
						},
					]);
				});

				it('should include mergedResponse per candidate when enabled and simplify is false', async () => {
					executeFunctionsMock.getNodeParameter.mockImplementation((parameter: string) => {
						switch (parameter) {
							case 'modelId':
								return 'models/gemini-2.5-flash';
							case 'messages.values':
								return [{ role: 'user', content: 'Hello' }];
							case 'simplify':
								return false;
							case 'jsonOutput':
								return false;
							case 'builtInTools':
								return {};
							case 'options':
								return {
									includeMergedResponse: true,
								};
							case 'options.maxToolsIterations':
								return 15;
							default:
								return undefined;
						}
					});
					executeFunctionsMock.getNodeInputs.mockReturnValue([{ type: 'main' }]);
					apiRequestMock.mockResolvedValue({
						candidates: [
							{
								content: {
									parts: [{ text: 'Response' }, { text: ' text' }],
									role: 'model',
								},
							},
						],
					});

					const result = await text.message.execute.call(executeFunctionsMock, 0);

					expect(result).toEqual([
						{
							json: {
								candidates: [
									{
										content: {
											parts: [{ text: 'Response' }, { text: ' text' }],
											role: 'model',
										},
										mergedResponse: 'Response text',
									},
								],
							},
							pairedItem: { item: 0 },
						},
					]);
				});

				it('should not include mergedResponse when disabled', async () => {
					executeFunctionsMock.getNodeParameter.mockImplementation((parameter: string) => {
						switch (parameter) {
							case 'modelId':
								return 'models/gemini-2.5-flash';
							case 'messages.values':
								return [{ role: 'user', content: 'Hello' }];
							case 'simplify':
								return true;
							case 'jsonOutput':
								return false;
							case 'builtInTools':
								return {};
							case 'options':
								return {
									includeMergedResponse: false,
								};
							case 'options.maxToolsIterations':
								return 15;
							default:
								return undefined;
						}
					});
					executeFunctionsMock.getNodeInputs.mockReturnValue([{ type: 'main' }]);
					apiRequestMock.mockResolvedValue({
						candidates: [
							{
								content: {
									parts: [{ text: 'Hello' }, { text: ' World' }],
									role: 'model',
								},
							},
						],
					});

					const result = await text.message.execute.call(executeFunctionsMock, 0);

					expect(result).toEqual([
						{
							json: {
								content: {
									parts: [{ text: 'Hello' }, { text: ' World' }],
									role: 'model',
								},
							},
							pairedItem: { item: 0 },
						},
					]);
					expect(result[0].json).not.toHaveProperty('mergedResponse');
				});

				it('should handle multiple candidates with different merged responses', async () => {
					executeFunctionsMock.getNodeParameter.mockImplementation((parameter: string) => {
						switch (parameter) {
							case 'modelId':
								return 'models/gemini-2.5-flash';
							case 'messages.values':
								return [{ role: 'user', content: 'Hello' }];
							case 'simplify':
								return true;
							case 'jsonOutput':
								return false;
							case 'builtInTools':
								return {};
							case 'options':
								return {
									includeMergedResponse: true,
									candidateCount: 2,
								};
							case 'options.maxToolsIterations':
								return 15;
							default:
								return undefined;
						}
					});
					executeFunctionsMock.getNodeInputs.mockReturnValue([{ type: 'main' }]);
					apiRequestMock.mockResolvedValue({
						candidates: [
							{
								content: {
									parts: [{ text: 'First' }, { text: ' candidate' }],
									role: 'model',
								},
							},
							{
								content: {
									parts: [{ text: 'Second' }, { text: ' candidate' }],
									role: 'model',
								},
							},
						],
					});

					const result = await text.message.execute.call(executeFunctionsMock, 0);

					expect(result).toHaveLength(2);
					expect(result[0].json).toEqual({
						content: {
							parts: [{ text: 'First' }, { text: ' candidate' }],
							role: 'model',
						},
						mergedResponse: 'First candidate',
					});
					expect(result[1].json).toEqual({
						content: {
							parts: [{ text: 'Second' }, { text: ' candidate' }],
							role: 'model',
						},
						mergedResponse: 'Second candidate',
					});
				});

				it('should only merge text parts and ignore non-text parts', async () => {
					executeFunctionsMock.getNodeParameter.mockImplementation((parameter: string) => {
						switch (parameter) {
							case 'modelId':
								return 'models/gemini-2.5-flash';
							case 'messages.values':
								return [{ role: 'user', content: 'Hello' }];
							case 'simplify':
								return true;
							case 'jsonOutput':
								return false;
							case 'builtInTools':
								return {};
							case 'options':
								return {
									includeMergedResponse: true,
								};
							case 'options.maxToolsIterations':
								return 15;
							default:
								return undefined;
						}
					});
					executeFunctionsMock.getNodeInputs.mockReturnValue([{ type: 'main' }]);
					apiRequestMock.mockResolvedValue({
						candidates: [
							{
								content: {
									parts: [{ text: 'Text' }, { executableCode: {} }, { text: ' more text' }],
									role: 'model',
								},
							},
						],
					});

					const result = await text.message.execute.call(executeFunctionsMock, 0);

					expect(result[0].json).toEqual({
						content: {
							parts: [{ text: 'Text' }, { executableCode: {} }, { text: ' more text' }],
							role: 'model',
						},
						mergedResponse: 'Text more text',
					});
				});
			});
		});
	});

	describe('Empty Prompt Validation', () => {
		it('should throw error when text messages are all empty', async () => {
			executeFunctionsMock.getNodeParameter.mockImplementation((parameter: string) => {
				switch (parameter) {
					case 'modelId':
						return 'models/gemini-2.5-flash';
					case 'messages.values':
						return [{ role: 'user', content: '' }];
					case 'simplify':
						return true;
					case 'jsonOutput':
						return false;
					case 'options':
						return {};
					default:
						return undefined;
				}
			});
			executeFunctionsMock.getNodeInputs.mockReturnValue([{ type: 'main' }]);

			await expect(text.message.execute.call(executeFunctionsMock, 0)).rejects.toThrow(
				'A non-empty prompt is required.',
			);
		});

		it('should throw error when text messages are whitespace-only', async () => {
			executeFunctionsMock.getNodeParameter.mockImplementation((parameter: string) => {
				switch (parameter) {
					case 'modelId':
						return 'models/gemini-2.5-flash';
					case 'messages.values':
						return [{ role: 'user', content: '   \t\n  ' }];
					case 'simplify':
						return true;
					case 'jsonOutput':
						return false;
					case 'options':
						return {};
					default:
						return undefined;
				}
			});
			executeFunctionsMock.getNodeInputs.mockReturnValue([{ type: 'main' }]);

			await expect(text.message.execute.call(executeFunctionsMock, 0)).rejects.toThrow(
				'A non-empty prompt is required.',
			);
		});

		it('should throw error when image generate prompt is empty', async () => {
			executeFunctionsMock.getNodeParameter.mockImplementation((parameter: string) => {
				switch (parameter) {
					case 'modelId':
						return 'models/gemini-2.0-flash-preview-image-generation';
					case 'prompt':
						return '';
					default:
						return undefined;
				}
			});

			await expect(image.generate.execute.call(executeFunctionsMock, 0)).rejects.toThrow(
				'A non-empty prompt is required.',
			);
		});

		it('should throw error when video generate prompt is empty', async () => {
			executeFunctionsMock.getNodeParameter.mockImplementation((parameter: string) => {
				switch (parameter) {
					case 'modelId':
						return 'models/veo-3.0-generate-002';
					case 'prompt':
						return '   ';
					default:
						return undefined;
				}
			});

			await expect(video.generate.execute.call(executeFunctionsMock, 0)).rejects.toThrow(
				'A non-empty prompt is required.',
			);
		});

		it('should throw error when audio analyze text is empty', async () => {
			executeFunctionsMock.getNodeParameter.mockImplementation((parameter: string) => {
				switch (parameter) {
					case 'modelId':
						return 'models/gemini-2.5-flash';
					case 'inputType':
						return 'url';
					case 'audioUrls':
						return 'https://example.com/audio.mp3';
					case 'text':
						return '';
					case 'simplify':
						return true;
					case 'options':
						return {};
					default:
						return undefined;
				}
			});

			await expect(audio.analyze.execute.call(executeFunctionsMock, 0)).rejects.toThrow(
				'A non-empty prompt is required.',
			);
		});
	});

	describe('Audio -> Analyze', () => {
		it('should analyze audio from URL', async () => {
			executeFunctionsMock.getNodeParameter.mockImplementation((parameter: string) => {
				switch (parameter) {
					case 'modelId':
						return 'models/gemini-2.5-flash';
					case 'inputType':
						return 'url';
					case 'audioUrls':
						return 'https://example.com/audio.mp3';
					case 'text':
						return "What's in this audio?";
					case 'simplify':
						return true;
					case 'options':
						return {
							maxOutputTokens: 300,
						};
					default:
						return undefined;
				}
			});
			downloadFileMock.mockResolvedValue({
				fileContent: Buffer.from('test'),
				mimeType: 'audio/mpeg',
			});
			uploadFileMock.mockResolvedValue({
				fileUri: 'https://generativelanguage.googleapis.com/v1/files/abc123',
				mimeType: 'audio/mpeg',
			});
			apiRequestMock.mockResolvedValue({
				candidates: [
					{
						content: {
							parts: [{ text: 'This audio contains a person speaking about AI.' }],
							role: 'model',
						},
					},
				],
			});

			const result = await audio.analyze.execute.call(executeFunctionsMock, 0);

			expect(result).toEqual([
				{
					json: {
						content: {
							parts: [{ text: 'This audio contains a person speaking about AI.' }],
							role: 'model',
						},
					},
					pairedItem: { item: 0 },
				},
			]);
			expect(downloadFileMock).toHaveBeenCalledWith('https://example.com/audio.mp3', 'audio/mpeg');
			expect(uploadFileMock).toHaveBeenCalledWith(Buffer.from('test'), 'audio/mpeg');
			expect(apiRequestMock).toHaveBeenCalledWith(
				'POST',
				'/v1beta/models/gemini-2.5-flash:generateContent',
				{
					body: {
						contents: [
							{
								parts: [
									{
										fileData: {
											fileUri: 'https://generativelanguage.googleapis.com/v1/files/abc123',
											mimeType: 'audio/mpeg',
										},
									},
									{
										text: "What's in this audio?",
									},
								],
								role: 'user',
							},
						],
						generationConfig: {
							maxOutputTokens: 300,
						},
					},
				},
			);
		});

		it('should analyze audio from binary data', async () => {
			executeFunctionsMock.getNodeParameter.mockImplementation((parameter: string) => {
				switch (parameter) {
					case 'modelId':
						return 'models/gemini-2.5-flash';
					case 'inputType':
						return 'binary';
					case 'binaryPropertyName':
						return 'data';
					case 'text':
						return "What's in this audio?";
					case 'simplify':
						return true;
					case 'options':
						return {
							maxOutputTokens: 300,
						};
					default:
						return undefined;
				}
			});
			const mockBinaryData: IBinaryData = {
				mimeType: 'audio/mpeg',
				fileName: 'test.mp3',
				fileSize: '1024',
				fileExtension: 'mp3',
				data: 'test',
			};
			executeFunctionsMock.helpers.assertBinaryData.mockReturnValue(mockBinaryData);
			executeFunctionsMock.helpers.getBinaryDataBuffer.mockResolvedValue(Buffer.from('test'));
			uploadFileMock.mockResolvedValue({
				fileUri: 'https://generativelanguage.googleapis.com/v1/files/abc123',
				mimeType: 'audio/mpeg',
			});
			apiRequestMock.mockResolvedValue({
				candidates: [
					{
						content: {
							parts: [{ text: 'This audio contains a person speaking about AI.' }],
							role: 'model',
						},
					},
				],
			});

			const result = await audio.analyze.execute.call(executeFunctionsMock, 0);

			expect(result).toEqual([
				{
					json: {
						content: {
							parts: [{ text: 'This audio contains a person speaking about AI.' }],
							role: 'model',
						},
					},
					pairedItem: { item: 0 },
				},
			]);
			expect(uploadFileMock).toHaveBeenCalledWith(Buffer.from('test'), 'audio/mpeg');
			expect(apiRequestMock).toHaveBeenCalledWith(
				'POST',
				'/v1beta/models/gemini-2.5-flash:generateContent',
				{
					body: {
						contents: [
							{
								parts: [
									{
										fileData: {
											fileUri: 'https://generativelanguage.googleapis.com/v1/files/abc123',
											mimeType: 'audio/mpeg',
										},
									},
									{
										text: "What's in this audio?",
									},
								],
								role: 'user',
							},
						],
						generationConfig: {
							maxOutputTokens: 300,
						},
					},
				},
			);
		});

		it('should analyze audio from Google API URL', async () => {
			executeFunctionsMock.getNodeParameter.mockImplementation((parameter: string) => {
				switch (parameter) {
					case 'modelId':
						return 'models/gemini-2.5-flash';
					case 'inputType':
						return 'url';
					case 'audioUrls':
						return 'https://generativelanguage.googleapis.com/v1/files/abc123';
					case 'text':
						return "What's in this audio?";
					case 'simplify':
						return true;
					case 'options':
						return {
							maxOutputTokens: 300,
						};
					default:
						return undefined;
				}
			});

			apiRequestMock.mockImplementation(async (method: string) => {
				if (method === 'GET') {
					return { mimeType: 'audio/mpeg' };
				}
				return {
					candidates: [
						{
							content: {
								parts: [{ text: 'This audio contains a person speaking about AI.' }],
								role: 'model',
							},
						},
					],
				};
			});

			const result = await audio.analyze.execute.call(executeFunctionsMock, 0);

			expect(result).toEqual([
				{
					json: {
						content: {
							parts: [{ text: 'This audio contains a person speaking about AI.' }],
							role: 'model',
						},
					},
					pairedItem: { item: 0 },
				},
			]);

			expect(downloadFileMock).not.toHaveBeenCalled();
			expect(uploadFileMock).not.toHaveBeenCalled();
			expect(apiRequestMock).toHaveBeenCalledWith('GET', '', {
				option: { url: 'https://generativelanguage.googleapis.com/v1/files/abc123' },
			});
			expect(apiRequestMock).toHaveBeenCalledWith(
				'POST',
				'/v1beta/models/gemini-2.5-flash:generateContent',
				{
					body: {
						contents: [
							{
								parts: [
									{
										fileData: {
											fileUri: 'https://generativelanguage.googleapis.com/v1/files/abc123',
											mimeType: 'audio/mpeg',
										},
									},
									{
										text: "What's in this audio?",
									},
								],
								role: 'user',
							},
						],
						generationConfig: {
							maxOutputTokens: 300,
						},
					},
				},
			);
		});
	});

	describe('Audio -> Transcribe', () => {
		it('should transcribe audio from URL', async () => {
			executeFunctionsMock.getNodeParameter.mockImplementation((parameter: string) => {
				switch (parameter) {
					case 'modelId':
						return 'models/gemini-2.5-flash';
					case 'inputType':
						return 'url';
					case 'audioUrls':
						return 'https://example.com/audio.mp3';
					case 'simplify':
						return true;
					case 'options':
						return {
							startTime: '00:15',
							endTime: '02:15',
						};
					default:
						return undefined;
				}
			});
			downloadFileMock.mockResolvedValue({
				fileContent: Buffer.from('test'),
				mimeType: 'audio/mpeg',
			});
			uploadFileMock.mockResolvedValue({
				fileUri: 'https://generativelanguage.googleapis.com/v1/files/abc123',
				mimeType: 'audio/mpeg',
			});
			apiRequestMock.mockResolvedValue({
				candidates: [
					{
						content: {
							parts: [{ text: 'This is the transcribed text from 00:15 to 02:15.' }],
							role: 'model',
						},
					},
				],
			});

			const result = await audio.transcribe.execute.call(executeFunctionsMock, 0);

			expect(result).toEqual([
				{
					json: {
						content: {
							parts: [{ text: 'This is the transcribed text from 00:15 to 02:15.' }],
							role: 'model',
						},
					},
					pairedItem: { item: 0 },
				},
			]);
			expect(downloadFileMock).toHaveBeenCalledWith('https://example.com/audio.mp3', 'audio/mpeg');
			expect(uploadFileMock).toHaveBeenCalledWith(Buffer.from('test'), 'audio/mpeg');
			expect(apiRequestMock).toHaveBeenCalledWith(
				'POST',
				'/v1beta/models/gemini-2.5-flash:generateContent',
				{
					body: {
						contents: [
							{
								parts: [
									{
										fileData: {
											fileUri: 'https://generativelanguage.googleapis.com/v1/files/abc123',
											mimeType: 'audio/mpeg',
										},
									},
									{
										text: 'Generate a transcript of the speech from 00:15 to 02:15',
									},
								],
								role: 'user',
							},
						],
					},
				},
			);
		});

		it('should transcribe audio from binary data', async () => {
			executeFunctionsMock.getNodeParameter.mockImplementation((parameter: string) => {
				switch (parameter) {
					case 'modelId':
						return 'models/gemini-2.5-flash';
					case 'inputType':
						return 'binary';
					case 'binaryPropertyName':
						return 'data';
					case 'simplify':
						return true;
					case 'options':
						return {};
					default:
						return undefined;
				}
			});
			const mockBinaryData: IBinaryData = {
				mimeType: 'audio/mpeg',
				fileName: 'test.mp3',
				fileSize: '1024',
				fileExtension: 'mp3',
				data: 'test',
			};
			executeFunctionsMock.helpers.assertBinaryData.mockReturnValue(mockBinaryData);
			executeFunctionsMock.helpers.getBinaryDataBuffer.mockResolvedValue(Buffer.from('test'));
			uploadFileMock.mockResolvedValue({
				fileUri: 'https://generativelanguage.googleapis.com/v1/files/abc123',
				mimeType: 'audio/mpeg',
			});
			apiRequestMock.mockResolvedValue({
				candidates: [
					{
						content: {
							parts: [{ text: 'This is the transcribed text.' }],
							role: 'model',
						},
					},
				],
			});

			const result = await audio.transcribe.execute.call(executeFunctionsMock, 0);
			expect(result).toEqual([
				{
					json: {
						content: {
							parts: [{ text: 'This is the transcribed text.' }],
							role: 'model',
						},
					},
					pairedItem: { item: 0 },
				},
			]);
			expect(uploadFileMock).toHaveBeenCalledWith(Buffer.from('test'), 'audio/mpeg');
			expect(apiRequestMock).toHaveBeenCalledWith(
				'POST',
				'/v1beta/models/gemini-2.5-flash:generateContent',
				{
					body: {
						contents: [
							{
								parts: [
									{
										fileData: {
											fileUri: 'https://generativelanguage.googleapis.com/v1/files/abc123',
											mimeType: 'audio/mpeg',
										},
									},
									{
										text: 'Generate a transcript of the speech',
									},
								],
								role: 'user',
							},
						],
					},
				},
			);
		});

		it('should transcribe audio from Google API URL', async () => {
			executeFunctionsMock.getNodeParameter.mockImplementation((parameter: string) => {
				switch (parameter) {
					case 'modelId':
						return 'models/gemini-2.5-flash';
					case 'inputType':
						return 'url';
					case 'audioUrls':
						return 'https://generativelanguage.googleapis.com/v1/files/abc123';
					case 'text':
						return "What's in this audio?";
					case 'simplify':
						return true;
					case 'options':
						return {
							startTime: '00:15',
							endTime: '02:15',
						};
					default:
						return undefined;
				}
			});

			apiRequestMock.mockImplementation(async (method: string) => {
				if (method === 'GET') {
					return { mimeType: 'audio/mpeg' };
				}
				return {
					candidates: [
						{
							content: {
								parts: [{ text: 'This is the transcribed text from 00:15 to 02:15.' }],
								role: 'model',
							},
						},
					],
				};
			});

			const result = await audio.transcribe.execute.call(executeFunctionsMock, 0);

			expect(result).toEqual([
				{
					json: {
						content: {
							parts: [{ text: 'This is the transcribed text from 00:15 to 02:15.' }],
							role: 'model',
						},
					},
					pairedItem: { item: 0 },
				},
			]);

			expect(downloadFileMock).not.toHaveBeenCalled();
			expect(uploadFileMock).not.toHaveBeenCalled();
			expect(apiRequestMock).toHaveBeenCalledWith('GET', '', {
				option: { url: 'https://generativelanguage.googleapis.com/v1/files/abc123' },
			});
			expect(apiRequestMock).toHaveBeenCalledWith(
				'POST',
				'/v1beta/models/gemini-2.5-flash:generateContent',
				{
					body: {
						contents: [
							{
								parts: [
									{
										fileData: {
											fileUri: 'https://generativelanguage.googleapis.com/v1/files/abc123',
											mimeType: 'audio/mpeg',
										},
									},
									{
										text: 'Generate a transcript of the speech from 00:15 to 02:15',
									},
								],
								role: 'user',
							},
						],
					},
				},
			);
		});
	});

	describe('File -> Upload', () => {
		it('should upload file from URL', async () => {
			executeFunctionsMock.getNodeParameter.mockImplementation((parameter: string) => {
				switch (parameter) {
					case 'inputType':
						return 'url';
					case 'fileUrl':
						return 'https://example.com/file.pdf';
					default:
						return undefined;
				}
			});
			transferFileMock.mockResolvedValue({
				fileUri: 'https://generativelanguage.googleapis.com/v1/files/abc123',
				mimeType: 'application/pdf',
			});
			const result = await file.upload.execute.call(executeFunctionsMock, 0);

			expect(result).toEqual([
				{
					json: {
						fileUri: 'https://generativelanguage.googleapis.com/v1/files/abc123',
						mimeType: 'application/pdf',
					},
					pairedItem: { item: 0 },
				},
			]);
			expect(transferFileMock).toHaveBeenCalledWith(
				0,
				'https://example.com/file.pdf',
				'application/octet-stream',
			);
		});

		it('should upload file from binary data', async () => {
			executeFunctionsMock.getNodeParameter.mockImplementation((parameter: string) => {
				switch (parameter) {
					case 'inputType':
						return 'binary';
					case 'binaryPropertyName':
						return 'data';
					default:
						return undefined;
				}
			});
			transferFileMock.mockResolvedValue({
				fileUri: 'https://generativelanguage.googleapis.com/v1/files/abc123',
				mimeType: 'application/pdf',
			});

			const result = await file.upload.execute.call(executeFunctionsMock, 0);

			expect(result).toEqual([
				{
					json: {
						fileUri: 'https://generativelanguage.googleapis.com/v1/files/abc123',
						mimeType: 'application/pdf',
					},
					pairedItem: { item: 0 },
				},
			]);
			expect(transferFileMock).toHaveBeenCalledWith(0, undefined, 'application/octet-stream');
		});
	});

	describe('Image -> Generate', () => {
		it('should generate image using Gemini model', async () => {
			executeFunctionsMock.getNodeParameter.mockImplementation((parameter: string) => {
				switch (parameter) {
					case 'modelId':
						return 'models/gemini-2.0-flash-preview-image-generation';
					case 'prompt':
						return 'A cute cat eating a dinosaur';
					case 'options.binaryPropertyOutput':
						return 'data';
					default:
						return undefined;
				}
			});
			apiRequestMock.mockResolvedValue({
				candidates: [
					{
						content: {
							parts: [
								{
									inlineData: {
										data: 'abcdefgh',
										mimeType: 'image/png',
									},
								},
							],
						},
					},
				],
			});
			executeFunctionsMock.helpers.prepareBinaryData.mockResolvedValue({
				mimeType: 'image/png',
				fileName: 'image.png',
				fileSize: '100',
				data: 'abcdefgh',
			});

			const result = await image.generate.execute.call(executeFunctionsMock, 0);

			expect(result).toEqual([
				{
					binary: {
						data: {
							mimeType: 'image/png',
							fileName: 'image.png',
							fileSize: '100',
							data: 'abcdefgh',
						},
					},
					json: {
						mimeType: 'image/png',
						fileName: 'image.png',
						fileSize: '100',
					},
					pairedItem: { item: 0 },
				},
			]);
			expect(apiRequestMock).toHaveBeenCalledWith(
				'POST',
				'/v1beta/models/gemini-2.0-flash-preview-image-generation:generateContent',
				{
					body: {
						contents: [
							{
								role: 'user',
								parts: [{ text: 'A cute cat eating a dinosaur' }],
							},
						],
						generationConfig: {
							responseModalities: ['IMAGE', 'TEXT'],
						},
					},
				},
			);
		});

		it('should derive Gemini image filename from MIME type', async () => {
			executeFunctionsMock.getNodeParameter.mockImplementation((parameter: string) => {
				switch (parameter) {
					case 'modelId':
						return 'models/gemini-2.0-flash-preview-image-generation';
					case 'prompt':
						return 'A cute cat eating a dinosaur';
					case 'options.binaryPropertyOutput':
						return 'data';
					default:
						return undefined;
				}
			});
			apiRequestMock.mockResolvedValue({
				candidates: [
					{
						content: {
							parts: [
								{
									inlineData: {
										data: 'abcdefgh',
										mimeType: 'image/jpeg',
									},
								},
							],
						},
					},
				],
			});
			executeFunctionsMock.helpers.prepareBinaryData.mockResolvedValue({
				mimeType: 'image/jpeg',
				fileName: 'image.jpg',
				fileSize: '100',
				data: 'abcdefgh',
			});

			await image.generate.execute.call(executeFunctionsMock, 0);

			expect(executeFunctionsMock.helpers.prepareBinaryData).toHaveBeenCalledWith(
				Buffer.from('abcdefgh', 'base64'),
				'image.jpg',
				'image/jpeg',
			);
		});

		it('should generate multiple images using Imagen model', async () => {
			executeFunctionsMock.getNodeParameter.mockImplementation((parameter: string) => {
				switch (parameter) {
					case 'modelId':
						return 'models/imagen-3.0-generate-002';
					case 'prompt':
						return 'A cute cat eating a dinosaur';
					case 'options.sampleCount':
						return 2;
					case 'options.binaryPropertyOutput':
						return 'data';
					default:
						return undefined;
				}
			});
			apiRequestMock.mockResolvedValue({
				predictions: [
					{
						bytesBase64Encoded: 'abcdefgh',
						mimeType: 'image/png',
					},
					{
						bytesBase64Encoded: 'abcdefgh',
						mimeType: 'image/png',
					},
				],
			});
			executeFunctionsMock.helpers.prepareBinaryData.mockResolvedValue({
				mimeType: 'image/png',
				fileName: 'image.png',
				fileSize: '100',
				data: 'abcdefgh',
			});

			const result = await image.generate.execute.call(executeFunctionsMock, 0);

			expect(result).toEqual([
				{
					binary: {
						data: {
							mimeType: 'image/png',
							fileName: 'image.png',
							fileSize: '100',
							data: 'abcdefgh',
						},
					},
					json: {
						mimeType: 'image/png',
						fileName: 'image.png',
						fileSize: '100',
					},
					pairedItem: { item: 0 },
				},
				{
					binary: {
						data: {
							mimeType: 'image/png',
							fileName: 'image.png',
							fileSize: '100',
							data: 'abcdefgh',
						},
					},
					json: {
						mimeType: 'image/png',
						fileName: 'image.png',
						fileSize: '100',
					},
					pairedItem: { item: 0 },
				},
			]);
			expect(apiRequestMock).toHaveBeenCalledWith(
				'POST',
				'/v1beta/models/imagen-3.0-generate-002:predict',
				{
					body: {
						instances: [
							{
								prompt: 'A cute cat eating a dinosaur',
							},
						],
						parameters: {
							sampleCount: 2,
						},
					},
				},
			);
		});

		it('should throw error for unsupported model', async () => {
			executeFunctionsMock.getNodeParameter.mockImplementation((parameter: string) => {
				switch (parameter) {
					case 'modelId':
						return 'models/unsupported-model';
					case 'prompt':
						return 'A cute cat eating a dinosaur';
					default:
						return undefined;
				}
			});
			executeFunctionsMock.getNode.mockReturnValue({
				id: '1',
				name: 'Google Gemini',
			} as INode);

			const execution = image.generate.execute.call(executeFunctionsMock, 0);

			await expect(execution).rejects.toThrow(NodeOperationError);
			await expect(execution).rejects.toThrow(
				'Model models/unsupported-model is not supported for image generation',
			);
		});
	});

	describe('Video -> Generate', () => {
		beforeEach(() => {
			vi.useFakeTimers({ shouldAdvanceTime: true });
		});

		afterEach(() => {
			vi.useRealTimers();
		});

		it('should generate video using Veo model', async () => {
			executeFunctionsMock.getNodeParameter.mockImplementation((parameter: string) => {
				switch (parameter) {
					case 'modelId':
						return 'models/veo-3.0-generate-002';
					case 'prompt':
						return 'Panning wide shot of a calico kitten sleeping in the sunshine';
					case 'options':
						return {
							aspectRatio: '16:9',
							personGeneration: 'dont_allow',
							sampleCount: 1,
							durationSeconds: 8,
						};
					case 'options.binaryPropertyOutput':
						return 'data';
					case 'returnAs':
						return 'video';
					default:
						return undefined;
				}
			});
			executeFunctionsMock.getCredentials.mockResolvedValue({ apiKey: 'test-api-key' });
			let pollCount = 0;
			apiRequestMock.mockImplementation(async (_method: string, path: string) => {
				if (path.includes(':predictLongRunning')) {
					return {
						name: 'operations/123',
						done: false,
					};
				}
				pollCount++;
				return {
					name: 'operations/123',
					done: pollCount > 1,
					response:
						pollCount > 1
							? {
									generateVideoResponse: {
										generatedSamples: [
											{
												video: {
													uri: 'https://example.com/video.mp4',
												},
											},
										],
									},
								}
							: undefined,
				};
			});
			downloadFileMock.mockResolvedValue({
				fileContent: Buffer.from('abcdefgh'),
				mimeType: 'video/mp4',
			});
			executeFunctionsMock.helpers.prepareBinaryData.mockResolvedValue({
				mimeType: 'video/mp4',
				fileName: 'video.mp4',
				fileSize: '1000',
				data: 'abcdefgh',
			});

			const promise = video.generate.execute.call(executeFunctionsMock, 0);
			await vi.advanceTimersByTimeAsync(5000);
			await vi.advanceTimersByTimeAsync(5000);
			const result = await promise;

			expect(result).toEqual([
				{
					binary: {
						data: {
							mimeType: 'video/mp4',
							fileName: 'video.mp4',
							fileSize: '1000',
							data: 'abcdefgh',
						},
					},
					json: {
						mimeType: 'video/mp4',
						fileName: 'video.mp4',
						fileSize: '1000',
					},
					pairedItem: { item: 0 },
				},
			]);
			expect(apiRequestMock).toHaveBeenCalledWith(
				'POST',
				'/v1beta/models/veo-3.0-generate-002:predictLongRunning',
				{
					body: {
						instances: [
							{
								prompt: 'Panning wide shot of a calico kitten sleeping in the sunshine',
							},
						],
						parameters: {
							aspectRatio: '16:9',
							personGeneration: 'dont_allow',
							sampleCount: 1,
							durationSeconds: 8,
						},
					},
				},
			);
			expect(apiRequestMock).toHaveBeenCalledWith('GET', '/v1beta/operations/123');
			expect(pollCount).toBe(2);
			expect(downloadFileMock).toHaveBeenCalledWith('https://example.com/video.mp4', 'video/mp4', {
				key: 'test-api-key',
			});
		});

		it('should derive video filename from MIME type', async () => {
			executeFunctionsMock.getNodeParameter.mockImplementation((parameter: string) => {
				switch (parameter) {
					case 'modelId':
						return 'models/veo-3.0-generate-002';
					case 'prompt':
						return 'Panning wide shot of a calico kitten sleeping in the sunshine';
					case 'options':
						return {
							aspectRatio: '16:9',
							personGeneration: 'dont_allow',
							sampleCount: 1,
						};
					case 'options.binaryPropertyOutput':
						return 'data';
					case 'returnAs':
						return 'video';
					default:
						return undefined;
				}
			});
			executeFunctionsMock.getCredentials.mockResolvedValue({ apiKey: 'test-api-key' });
			apiRequestMock
				.mockResolvedValueOnce({
					name: 'operations/123',
					done: false,
				})
				.mockResolvedValueOnce({
					name: 'operations/123',
					done: true,
					response: {
						generateVideoResponse: {
							generatedSamples: [
								{
									video: {
										uri: 'https://example.com/video.mp4',
									},
								},
							],
						},
					},
				});
			downloadFileMock.mockResolvedValue({
				fileContent: Buffer.from('abcdefgh'),
				mimeType: 'video/webm',
			});
			executeFunctionsMock.helpers.prepareBinaryData.mockResolvedValue({
				mimeType: 'video/webm',
				fileName: 'video.webm',
				fileSize: '1000',
				data: 'abcdefgh',
			});

			const promise = video.generate.execute.call(executeFunctionsMock, 0);
			await vi.advanceTimersByTimeAsync(5000);
			const result = await promise;

			expect(result[0]?.binary?.data?.fileName).toBe('video.webm');
			expect(executeFunctionsMock.helpers.prepareBinaryData).toHaveBeenCalledWith(
				Buffer.from('abcdefgh'),
				'video.webm',
				'video/webm',
			);
		});

		it('should not pass durationSeconds if not provided', async () => {
			executeFunctionsMock.getNodeParameter.mockImplementation((parameter: string) => {
				switch (parameter) {
					case 'modelId':
						return 'models/veo-3.0-generate-002';
					case 'prompt':
						return 'Panning wide shot of a calico kitten sleeping in the sunshine';
					case 'options':
						return {
							aspectRatio: '16:9',
							personGeneration: 'dont_allow',
							sampleCount: 1,
						};
					case 'returnAs':
						return 'url';
					default:
						return undefined;
				}
			});
			executeFunctionsMock.getCredentials.mockResolvedValue({ apiKey: 'test-api-key' });
			apiRequestMock.mockResolvedValue({
				name: 'operations/123',
				done: true,
				response: {
					generateVideoResponse: {
						generatedSamples: [
							{
								video: {
									uri: 'https://example.com/video.mp4',
								},
							},
						],
					},
				},
			});

			const result = await video.generate.execute.call(executeFunctionsMock, 0);

			expect(result).toEqual([
				{
					json: {
						url: 'https://example.com/video.mp4',
					},
					pairedItem: { item: 0 },
				},
			]);
			expect(apiRequestMock).toHaveBeenCalledWith(
				'POST',
				'/v1beta/models/veo-3.0-generate-002:predictLongRunning',
				{
					body: {
						instances: [
							{
								prompt: 'Panning wide shot of a calico kitten sleeping in the sunshine',
							},
						],
						parameters: {
							aspectRatio: '16:9',
							personGeneration: 'dont_allow',
							sampleCount: 1,
						},
					},
				},
			);
		});

		it('should handle errors from video generation', async () => {
			executeFunctionsMock.getNodeParameter.mockImplementation((parameter: string) => {
				switch (parameter) {
					case 'modelId':
						return 'models/veo-3.0-generate-002';
					case 'prompt':
						return 'Panning wide shot of a calico kitten sleeping in the sunshine';
					case 'options':
						return {};
					default:
						return undefined;
				}
			});
			executeFunctionsMock.getCredentials.mockResolvedValue({ apiKey: 'test-api-key' });
			apiRequestMock.mockImplementationOnce(async () => {
				return {
					name: 'operations/123',
					done: true,
					error: {
						message: 'Failed to generate video',
					},
				};
			});
			executeFunctionsMock.getNode.mockReturnValue({ name: 'Google Gemini' } as INode);

			const promise = video.generate.execute.call(executeFunctionsMock, 0);
			await expect(promise).rejects.toThrow(NodeOperationError);
			await expect(promise).rejects.toThrow('Failed to generate video');
		});

		it('should throw error for non-Veo model', async () => {
			executeFunctionsMock.getNodeParameter.mockImplementation((parameter: string) => {
				switch (parameter) {
					case 'modelId':
						return 'models/gemini-2.0-flash';
					case 'prompt':
						return 'Panning wide shot of a calico kitten sleeping in the sunshine';
					default:
						return undefined;
				}
			});

			executeFunctionsMock.getNode.mockReturnValue({ name: 'Google Gemini' } as INode);

			const promise = video.generate.execute.call(executeFunctionsMock, 0);
			await expect(promise).rejects.toThrow(NodeOperationError);
			await expect(promise).rejects.toThrow(
				'Model models/gemini-2.0-flash is not supported for video generation. Please use a Veo model',
			);
		});

		describe('Video -> Download', () => {
			it('should derive download filename from MIME type', async () => {
				executeFunctionsMock.getNodeParameter.mockImplementation((parameter: string) => {
					switch (parameter) {
						case 'url':
							return 'https://example.com/video.mp4';
						case 'options.binaryPropertyOutput':
							return 'data';
						default:
							return undefined;
					}
				});
				executeFunctionsMock.getCredentials.mockResolvedValue({ apiKey: 'test-api-key' });
				downloadFileMock.mockResolvedValue({
					fileContent: Buffer.from('abcdefgh'),
					mimeType: 'video/webm',
				});
				executeFunctionsMock.helpers.prepareBinaryData.mockResolvedValue({
					mimeType: 'video/webm',
					fileName: 'video.webm',
					fileSize: '1000',
					data: 'abcdefgh',
				});

				const result = await video.download.execute.call(executeFunctionsMock, 0);

				expect(result).toEqual([
					{
						binary: {
							data: {
								mimeType: 'video/webm',
								fileName: 'video.webm',
								fileSize: '1000',
								data: 'abcdefgh',
							},
						},
						json: {
							mimeType: 'video/webm',
							fileName: 'video.webm',
							fileSize: '1000',
						},
						pairedItem: { item: 0 },
					},
				]);
				expect(downloadFileMock).toHaveBeenCalledWith(
					'https://example.com/video.mp4',
					'video/mp4',
					{
						key: 'test-api-key',
					},
				);
				expect(executeFunctionsMock.helpers.prepareBinaryData).toHaveBeenCalledWith(
					Buffer.from('abcdefgh'),
					'video.webm',
					'video/webm',
				);
			});
		});
	});
});
