import FormData from 'form-data';
import get from 'lodash/get';
import type { IDataObject, IExecuteFunctions } from 'n8n-workflow';

import * as assistant from '../../v1/actions/assistant';
import * as audio from '../../v1/actions/audio';
import * as file from '../../v1/actions/file';
import * as image from '../../v1/actions/image';
import * as text from '../../v1/actions/text';

const { apiRequestMock } = vi.hoisted(() => ({
	apiRequestMock: vi.fn(),
}));
vi.mock('../../transport', () => ({
	apiRequest: apiRequestMock,
}));

const createExecuteFunctionsMock = (parameters: IDataObject) => {
	const nodeParameters = parameters;
	return {
		getExecutionCancelSignal() {
			return new AbortController().signal;
		},
		getNodeParameter(parameter: string) {
			return get(nodeParameters, parameter);
		},
		getNode() {
			return {};
		},
		getInputConnectionData() {
			return undefined;
		},
		helpers: {
			prepareBinaryData() {
				return {};
			},
			assertBinaryData() {
				return {
					filename: 'filenale.flac',
					contentType: 'audio/flac',
				};
			},
			getBinaryDataBuffer() {
				return 'data buffer data';
			},
		},
	} as unknown as IExecuteFunctions;
};

describe('OpenAi', () => {
	beforeEach(() => {
		vi.resetAllMocks();
	});

	describe('OpenAi, Assistant resource', () => {
		it('create => should throw an error if an assistant with the same name already exists', async () => {
			apiRequestMock.mockResolvedValueOnce({
				data: [{ name: 'name' }],
				has_more: false,
			});

			try {
				await assistant.create.execute.call(
					createExecuteFunctionsMock({
						name: 'name',
						options: {
							failIfExists: true,
						},
					}),
					0,
				);
				expect(true).toBe(false);
			} catch (error) {
				expect(error.message).toBe("An assistant with the same name 'name' already exists");
			}
		});

		it('create => should call apiRequest with correct parameters', async () => {
			apiRequestMock.mockResolvedValueOnce({});

			await assistant.create.execute.call(
				createExecuteFunctionsMock({
					modelId: 'gpt-model',
					name: 'name',
					description: 'description',
					instructions: 'some instructions',
					codeInterpreter: true,
					knowledgeRetrieval: true,
					file_ids: [],
					options: {},
				}),
				0,
			);

			expect(apiRequestMock).toHaveBeenCalledWith('POST', '/assistants', {
				body: {
					description: 'description',
					instructions: 'some instructions',
					model: 'gpt-model',
					name: 'name',
					tool_resources: {
						code_interpreter: {
							file_ids: [],
						},
						file_search: {
							vector_stores: [
								{
									file_ids: [],
								},
							],
						},
					},
					tools: [{ type: 'code_interpreter' }, { type: 'file_search' }],
				},
				headers: { 'OpenAI-Beta': 'assistants=v2' },
			});
		});

		it('create => should throw error if more then 20 files selected', async () => {
			apiRequestMock.mockResolvedValueOnce({});

			try {
				await assistant.create.execute.call(
					createExecuteFunctionsMock({
						file_ids: Array.from({ length: 25 }),
						options: {},
					}),
					0,
				);
				expect(true).toBe(false);
			} catch (error) {
				expect(error.message).toBe(
					'The maximum number of files that can be attached to the assistant is 20',
				);
			}
		});

		it('delete => should call apiRequest with correct parameters', async () => {
			apiRequestMock.mockResolvedValueOnce({});

			await assistant.deleteAssistant.execute.call(
				createExecuteFunctionsMock({
					assistantId: 'assistant-id',
				}),
				0,
			);

			expect(apiRequestMock).toHaveBeenCalledWith('DELETE', '/assistants/assistant-id', {
				headers: { 'OpenAI-Beta': 'assistants=v2' },
			});
		});

		it('list => should call apiRequest with correct parameters', async () => {
			apiRequestMock.mockResolvedValueOnce({
				data: [
					{ name: 'name1', id: 'id-1', model: 'gpt-model', other: 'other' },
					{ name: 'name2', id: 'id-2', model: 'gpt-model', other: 'other' },
					{ name: 'name3', id: 'id-3', model: 'gpt-model', other: 'other' },
				],
				has_more: false,
			});

			const response = await assistant.list.execute.call(
				createExecuteFunctionsMock({
					simplify: true,
				}),
				0,
			);

			expect(response).toEqual([
				{
					json: { name: 'name1', id: 'id-1', model: 'gpt-model' },
					pairedItem: { item: 0 },
				},
				{
					json: { name: 'name2', id: 'id-2', model: 'gpt-model' },
					pairedItem: { item: 0 },
				},
				{
					json: { name: 'name3', id: 'id-3', model: 'gpt-model' },
					pairedItem: { item: 0 },
				},
			]);
		});

		it('update => should call apiRequest with correct parameters', async () => {
			apiRequestMock.mockResolvedValueOnce({
				tools: [{ type: 'existing_tool' }],
			});
			apiRequestMock.mockResolvedValueOnce({});

			await assistant.update.execute.call(
				createExecuteFunctionsMock({
					assistantId: 'assistant-id',
					options: {
						modelId: 'gpt-model',
						name: 'name',
						instructions: 'some instructions',
						codeInterpreter: true,
						knowledgeRetrieval: true,
						file_ids: [],
						removeCustomTools: false,
					},
				}),
				0,
			);

			expect(apiRequestMock).toHaveBeenCalledTimes(2);
			expect(apiRequestMock).toHaveBeenCalledWith('GET', '/assistants/assistant-id', {
				headers: { 'OpenAI-Beta': 'assistants=v2' },
			});
			expect(apiRequestMock).toHaveBeenCalledWith('POST', '/assistants/assistant-id', {
				body: {
					instructions: 'some instructions',
					model: 'gpt-model',
					name: 'name',
					tool_resources: {
						code_interpreter: {
							file_ids: [],
						},
					},
					tools: [{ type: 'existing_tool' }, { type: 'code_interpreter' }, { type: 'file_search' }],
				},
				headers: { 'OpenAI-Beta': 'assistants=v2' },
			});
		});

		it('update => should call apiRequest with file_ids as an array for search', async () => {
			apiRequestMock.mockResolvedValueOnce({
				tools: [{ type: 'existing_tool' }],
			});
			apiRequestMock.mockResolvedValueOnce({});

			await assistant.update.execute.call(
				createExecuteFunctionsMock({
					assistantId: 'assistant-id',
					options: {
						modelId: 'gpt-model',
						name: 'name',
						instructions: 'some instructions',
						codeInterpreter: true,
						knowledgeRetrieval: true,
						file_ids: ['1234'],
						removeCustomTools: false,
					},
				}),
				0,
			);

			expect(apiRequestMock).toHaveBeenCalledTimes(2);
			expect(apiRequestMock).toHaveBeenCalledWith('GET', '/assistants/assistant-id', {
				headers: { 'OpenAI-Beta': 'assistants=v2' },
			});
			expect(apiRequestMock).toHaveBeenCalledWith('POST', '/assistants/assistant-id', {
				body: {
					instructions: 'some instructions',
					model: 'gpt-model',
					name: 'name',
					tool_resources: {
						code_interpreter: {
							file_ids: ['1234'],
						},
					},
					tools: [{ type: 'existing_tool' }, { type: 'code_interpreter' }, { type: 'file_search' }],
				},
				headers: { 'OpenAI-Beta': 'assistants=v2' },
			});
		});

		it('update => should call apiRequest with file_ids as strings for search', async () => {
			apiRequestMock.mockResolvedValueOnce({
				tools: [{ type: 'existing_tool' }],
			});
			apiRequestMock.mockResolvedValueOnce({});

			await assistant.update.execute.call(
				createExecuteFunctionsMock({
					assistantId: 'assistant-id',
					options: {
						modelId: 'gpt-model',
						name: 'name',
						instructions: 'some instructions',
						codeInterpreter: true,
						knowledgeRetrieval: true,
						file_ids: '1234, 5678, 90',
						removeCustomTools: false,
					},
				}),
				0,
			);

			expect(apiRequestMock).toHaveBeenCalledTimes(2);
			expect(apiRequestMock).toHaveBeenCalledWith('GET', '/assistants/assistant-id', {
				headers: { 'OpenAI-Beta': 'assistants=v2' },
			});
			expect(apiRequestMock).toHaveBeenCalledWith('POST', '/assistants/assistant-id', {
				body: {
					instructions: 'some instructions',
					model: 'gpt-model',
					name: 'name',
					tool_resources: {
						code_interpreter: {
							file_ids: ['1234', '5678', '90'],
						},
					},
					tools: [{ type: 'existing_tool' }, { type: 'code_interpreter' }, { type: 'file_search' }],
				},
				headers: { 'OpenAI-Beta': 'assistants=v2' },
			});
		});
	});

	describe('OpenAi, Audio resource', () => {
		it('generate => should call apiRequest with correct parameters', async () => {
			apiRequestMock.mockResolvedValueOnce({});

			const returnData = await audio.generate.execute.call(
				createExecuteFunctionsMock({
					model: 'tts-model',
					input: 'input',
					voice: 'fable',
					options: {
						response_format: 'flac',
						speed: 1.25,
						binaryPropertyOutput: 'myData',
					},
				}),
				0,
			);

			expect(returnData.length).toEqual(1);
			expect(returnData[0].binary?.myData).toBeDefined();
			expect(returnData[0].pairedItem).toBeDefined();

			expect(apiRequestMock).toHaveBeenCalledWith('POST', '/audio/speech', {
				body: {
					input: 'input',
					model: 'tts-model',
					response_format: 'flac',
					speed: 1.25,
					voice: 'fable',
				},
				option: { encoding: 'arraybuffer', json: false, returnFullResponse: true, useStream: true },
			});
		});

		it('transcribe => should call apiRequest with correct parameters', async () => {
			apiRequestMock.mockResolvedValueOnce({ text: 'transcribtion' });

			const returnData = await audio.transcribe.execute.call(
				createExecuteFunctionsMock({
					binaryPropertyName: 'myData',
					options: {
						language: 'en',
						temperature: 1.1,
					},
				}),
				0,
			);

			expect(returnData.length).toEqual(1);
			expect(returnData[0].pairedItem).toBeDefined();
			expect(returnData[0].json).toEqual({ text: 'transcribtion' });

			expect(apiRequestMock).toHaveBeenCalledWith(
				'POST',
				'/audio/transcriptions',
				expect.objectContaining({
					headers: expect.objectContaining({
						'content-type': expect.stringMatching(/^multipart\/form-data; boundary=/),
					}),
					option: expect.objectContaining({
						formData: expect.any(FormData),
					}),
				}),
			);
		});

		it('translate => should call apiRequest with correct parameters', async () => {
			apiRequestMock.mockResolvedValueOnce({ text: 'translations' });

			const returnData = await audio.translate.execute.call(
				createExecuteFunctionsMock({
					binaryPropertyName: 'myData',
					options: {},
				}),
				0,
			);

			expect(returnData.length).toEqual(1);
			expect(returnData[0].pairedItem).toBeDefined();
			expect(returnData[0].json).toEqual({ text: 'translations' });

			expect(apiRequestMock).toHaveBeenCalledWith(
				'POST',
				'/audio/translations',
				expect.objectContaining({
					headers: expect.objectContaining({
						'content-type': expect.stringMatching(/^multipart\/form-data; boundary=/),
					}),
					option: expect.objectContaining({
						formData: expect.any(FormData),
					}),
				}),
			);
		});
	});

	describe('OpenAi, File resource', () => {
		it('deleteFile => should call apiRequest with correct parameters', async () => {
			apiRequestMock.mockResolvedValueOnce({});

			await file.deleteFile.execute.call(
				createExecuteFunctionsMock({
					fileId: 'file-id',
				}),
				0,
			);

			expect(apiRequestMock).toHaveBeenCalledWith('DELETE', '/files/file-id');
		});

		it('list => should return list of files', async () => {
			apiRequestMock.mockResolvedValueOnce({
				data: [{ file: 'file1' }, { file: 'file2' }, { file: 'file3' }],
			});

			const returnData = await file.list.execute.call(
				createExecuteFunctionsMock({ options: {} }),
				2,
			);

			expect(returnData.length).toEqual(3);
			expect(returnData).toEqual([
				{
					json: { file: 'file1' },
					pairedItem: { item: 2 },
				},
				{
					json: { file: 'file2' },
					pairedItem: { item: 2 },
				},
				{
					json: { file: 'file3' },
					pairedItem: { item: 2 },
				},
			]);
		});

		it('upload => should call apiRequest with correct parameters', async () => {
			apiRequestMock.mockResolvedValueOnce({ success: true });

			const returnData = await file.upload.execute.call(
				createExecuteFunctionsMock({
					binaryPropertyName: 'myData',
					options: {},
				}),
				0,
			);

			expect(returnData.length).toEqual(1);
			expect(returnData[0].pairedItem).toBeDefined();
			expect(returnData[0].json).toEqual({ success: true });

			expect(apiRequestMock).toHaveBeenCalledWith(
				'POST',
				'/files',
				expect.objectContaining({
					headers: expect.objectContaining({
						'content-type': expect.stringMatching(/^multipart\/form-data; boundary=/),
					}),
					option: expect.objectContaining({
						formData: expect.any(FormData),
					}),
				}),
			);
		});
	});

	describe('OpenAi, Image resource', () => {
		it('generate => should call apiRequest with correct parameters, return binary', async () => {
			apiRequestMock.mockResolvedValueOnce({ data: [{ b64_json: 'image1' }] });

			const returnData = await image.generate.execute.call(
				createExecuteFunctionsMock({
					model: 'dall-e-3',
					prompt: 'cat with a hat',
					options: {
						size: '1024x1024',
						style: 'vivid',
						quality: 'hd',
						binaryPropertyOutput: 'myData',
					},
				}),
				0,
			);

			expect(returnData.length).toEqual(1);
			expect(returnData[0].binary?.myData).toBeDefined();
			expect(returnData[0].pairedItem).toBeDefined();

			expect(apiRequestMock).toHaveBeenCalledWith('POST', '/images/generations', {
				body: {
					model: 'dall-e-3',
					prompt: 'cat with a hat',
					quality: 'hd',
					response_format: 'b64_json',
					size: '1024x1024',
					style: 'vivid',
				},
			});
		});

		it('generate => should call apiRequest with correct parameters, return urls', async () => {
			apiRequestMock.mockResolvedValueOnce({ data: [{ url: 'image-url' }] });

			const returnData = await image.generate.execute.call(
				createExecuteFunctionsMock({
					model: 'dall-e-3',
					prompt: 'cat with a hat',
					options: {
						size: '1024x1024',
						style: 'vivid',
						quality: 'hd',
						binaryPropertyOutput: 'myData',
						returnImageUrls: true,
					},
				}),
				0,
			);

			expect(returnData.length).toEqual(1);
			expect(returnData[0].pairedItem).toBeDefined();
			expect(returnData).toEqual([{ json: { url: 'image-url' }, pairedItem: { item: 0 } }]);

			expect(apiRequestMock).toHaveBeenCalledWith('POST', '/images/generations', {
				body: {
					model: 'dall-e-3',
					prompt: 'cat with a hat',
					quality: 'hd',
					response_format: 'url',
					size: '1024x1024',
					style: 'vivid',
				},
			});
		});

		it('analyze => should call apiRequest with correct parameters', async () => {
			apiRequestMock.mockResolvedValueOnce({ success: true });

			const returnData = await image.analyze.execute.call(
				createExecuteFunctionsMock({
					text: 'image text',
					inputType: 'url',
					imageUrls: 'image-url1, image-url2',
					options: {
						detail: 'low',
					},
				}),
				0,
			);

			expect(returnData.length).toEqual(1);
			expect(returnData[0].pairedItem).toBeDefined();
			expect(returnData[0].json).toEqual({ success: true });

			expect(apiRequestMock).toHaveBeenCalledWith('POST', '/chat/completions', {
				body: {
					max_tokens: 300,
					messages: [
						{
							content: [
								{ text: 'image text', type: 'text' },
								{ image_url: { detail: 'low', url: 'image-url1' }, type: 'image_url' },
								{ image_url: { detail: 'low', url: 'image-url2' }, type: 'image_url' },
							],
							role: 'user',
						},
					],
					model: 'gpt-4-vision-preview',
				},
			});
		});
	});

	describe('Empty Prompt Validation', () => {
		it('image generate => should throw error for empty prompt', async () => {
			await expect(
				image.generate.execute.call(
					createExecuteFunctionsMock({
						model: 'dall-e-3',
						prompt: '',
						options: {},
					}),
					0,
				),
			).rejects.toThrow('A non-empty prompt is required.');
		});

		it('image generate => should throw error for whitespace-only prompt', async () => {
			await expect(
				image.generate.execute.call(
					createExecuteFunctionsMock({
						model: 'dall-e-3',
						prompt: '   ',
						options: {},
					}),
					0,
				),
			).rejects.toThrow('A non-empty prompt is required.');
		});

		it('image analyze => should throw error for empty text', async () => {
			await expect(
				image.analyze.execute.call(
					createExecuteFunctionsMock({
						text: '',
						inputType: 'url',
						imageUrls: 'https://example.com/image.jpg',
						options: {},
					}),
					0,
				),
			).rejects.toThrow('A non-empty prompt is required.');
		});

		it('text message => should throw error for empty messages', async () => {
			await expect(
				text.message.execute.call(
					createExecuteFunctionsMock({
						modelId: 'gpt-model',
						messages: {
							values: [{ role: 'user', content: '' }],
						},
						options: {},
					}),
					0,
				),
			).rejects.toThrow('A non-empty prompt is required.');
		});

		it('text message => should throw error for whitespace-only messages', async () => {
			await expect(
				text.message.execute.call(
					createExecuteFunctionsMock({
						modelId: 'gpt-model',
						messages: {
							values: [{ role: 'user', content: '   ' }],
						},
						options: {},
					}),
					0,
				),
			).rejects.toThrow('A non-empty prompt is required.');
		});
	});

	describe('OpenAi, Text resource', () => {
		it('classify => should call apiRequest with correct parameters', async () => {
			apiRequestMock.mockResolvedValueOnce({ results: [{ flagged: true }] });

			const returnData = await text.classify.execute.call(
				createExecuteFunctionsMock({
					input: 'input',
					options: { useStableModel: true },
				}),
				0,
			);

			expect(returnData.length).toEqual(1);
			expect(returnData[0].pairedItem).toBeDefined();
			expect(returnData[0].json).toEqual({ flagged: true });

			expect(apiRequestMock).toHaveBeenCalledWith('POST', '/moderations', {
				body: { input: 'input', model: 'text-moderation-stable' },
			});
		});

		it('message => should call apiRequest with correct parameters, no tool call', async () => {
			apiRequestMock.mockResolvedValueOnce({
				choices: [{ message: { tool_calls: undefined } }],
			});

			await text.message.execute.call(
				createExecuteFunctionsMock({
					modelId: 'gpt-model',
					messages: {
						values: [{ role: 'user', content: 'message' }],
					},

					options: {},
				}),
				0,
			);

			expect(apiRequestMock).toHaveBeenCalledWith('POST', '/chat/completions', {
				body: {
					messages: [{ content: 'message', role: 'user' }],
					model: 'gpt-model',
					response_format: undefined,
					tools: undefined,
				},
			});
		});
	});
});
