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';
import * as transport from '../../transport';

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, Assistant resource', () => {
	beforeEach(() => {
		(transport as any).apiRequest = jest.fn();
	});

	it('create => should throw an error if an assistant with the same name already exists', async () => {
		(transport.apiRequest as jest.Mock).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 () => {
		(transport.apiRequest as jest.Mock).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(transport.apiRequest).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 () => {
		(transport.apiRequest as jest.Mock).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 () => {
		(transport.apiRequest as jest.Mock).mockResolvedValueOnce({});

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

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

	it('list => should call apiRequest with correct parameters', async () => {
		(transport.apiRequest as jest.Mock).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 () => {
		(transport.apiRequest as jest.Mock).mockResolvedValueOnce({
			tools: [{ type: 'existing_tool' }],
		});
		(transport.apiRequest as jest.Mock).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(transport.apiRequest).toHaveBeenCalledTimes(2);
		expect(transport.apiRequest).toHaveBeenCalledWith('GET', '/assistants/assistant-id', {
			headers: { 'OpenAI-Beta': 'assistants=v2' },
		});
		expect(transport.apiRequest).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 () => {
		(transport.apiRequest as jest.Mock).mockResolvedValueOnce({
			tools: [{ type: 'existing_tool' }],
		});
		(transport.apiRequest as jest.Mock).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(transport.apiRequest).toHaveBeenCalledTimes(2);
		expect(transport.apiRequest).toHaveBeenCalledWith('GET', '/assistants/assistant-id', {
			headers: { 'OpenAI-Beta': 'assistants=v2' },
		});
		expect(transport.apiRequest).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 () => {
		(transport.apiRequest as jest.Mock).mockResolvedValueOnce({
			tools: [{ type: 'existing_tool' }],
		});
		(transport.apiRequest as jest.Mock).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(transport.apiRequest).toHaveBeenCalledTimes(2);
		expect(transport.apiRequest).toHaveBeenCalledWith('GET', '/assistants/assistant-id', {
			headers: { 'OpenAI-Beta': 'assistants=v2' },
		});
		expect(transport.apiRequest).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', () => {
	beforeEach(() => {
		(transport as any).apiRequest = jest.fn();
	});

	it('generate => should call apiRequest with correct parameters', async () => {
		(transport.apiRequest as jest.Mock).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(transport.apiRequest).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 () => {
		(transport.apiRequest as jest.Mock).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(transport.apiRequest).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 () => {
		(transport.apiRequest as jest.Mock).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(transport.apiRequest).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', () => {
	beforeEach(() => {
		(transport as any).apiRequest = jest.fn();
	});

	it('deleteFile => should call apiRequest with correct parameters', async () => {
		(transport.apiRequest as jest.Mock).mockResolvedValueOnce({});

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

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

	it('list => should return list of files', async () => {
		(transport.apiRequest as jest.Mock).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 () => {
		(transport.apiRequest as jest.Mock).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(transport.apiRequest).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', () => {
	beforeEach(() => {
		(transport as any).apiRequest = jest.fn();
	});

	it('generate => should call apiRequest with correct parameters, return binary', async () => {
		(transport.apiRequest as jest.Mock).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(transport.apiRequest).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 () => {
		(transport.apiRequest as jest.Mock).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(transport.apiRequest).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 () => {
		(transport.apiRequest as jest.Mock).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(transport.apiRequest).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('OpenAi, Text resource', () => {
	beforeEach(() => {
		(transport as any).apiRequest = jest.fn();
	});

	it('classify => should call apiRequest with correct parameters', async () => {
		(transport.apiRequest as jest.Mock).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(transport.apiRequest).toHaveBeenCalledWith('POST', '/moderations', {
			body: { input: 'input', model: 'text-moderation-stable' },
		});
	});

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

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

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

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