import { mockDeep } from 'jest-mock-extended';
import type {
	IExecuteFunctions,
	INode,
	INodeExecutionData,
	NodeExecutionWithMetadata,
} from 'n8n-workflow';

import * as GenericFunctions from '../GenericFunctions';
import { Telegram } from '../Telegram.node';

describe('Telegram node', () => {
	const executeFunctionsMock = mockDeep<IExecuteFunctions>();
	let apiRequestSpy: jest.SpyInstance;
	const node = new Telegram();

	const legacyBinaryAccessHelper = (index: number, propertyName: string | any) => {
		const items = executeFunctionsMock.getInputData();
		return items[index].binary![propertyName as string];
	};

	beforeEach(() => {
		jest.resetAllMocks();
		apiRequestSpy = jest.spyOn(GenericFunctions, 'apiRequest');
		executeFunctionsMock.getCredentials.mockResolvedValue({
			baseUrl: 'https://api.telegram.org',
			accessToken: 'test-token',
		});
		executeFunctionsMock.getNode.mockReturnValue({
			typeVersion: 1.2,
		} as INode);
		executeFunctionsMock.getInputData.mockReturnValue([{ json: {} }]);
		executeFunctionsMock.helpers.returnJsonArray.mockImplementation(
			(input) => input as INodeExecutionData[],
		);
		executeFunctionsMock.helpers.constructExecutionMetaData.mockImplementation(
			(input) => input as NodeExecutionWithMetadata[],
		);
	});

	describe('file:get', () => {
		beforeEach(() => {
			executeFunctionsMock.getNodeParameter.mockImplementation((p) => {
				switch (p) {
					case 'resource':
						return 'file';
					case 'operation':
						return 'get';
					case 'download':
						return true;
					case 'fileId':
						return 'file-id';
					default:
						return undefined;
				}
			});
		});

		it('should determine the mime type of the file', async () => {
			apiRequestSpy.mockResolvedValueOnce({
				result: {
					file_id: 'file-id',
					file_path: 'documents/file_1.pdf',
				},
			});
			apiRequestSpy.mockResolvedValueOnce({
				body: Buffer.from('test-file'),
			});
			executeFunctionsMock.helpers.prepareBinaryData.mockResolvedValue({
				data: 'test-file',
				mimeType: 'application/pdf',
			});

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

			expect(result).toEqual([
				[
					{
						json: {
							result: {
								file_id: 'file-id',
								file_path: 'documents/file_1.pdf',
							},
						},
						binary: {
							data: {
								data: 'test-file',
								mimeType: 'application/pdf',
							},
						},
						pairedItem: { item: 0 },
					},
				],
			]);
			expect(executeFunctionsMock.helpers.prepareBinaryData).toHaveBeenCalledWith(
				Buffer.from('test-file'),
				'file_1.pdf',
				'application/pdf',
			);
		});

		it('should fallback to application/octet-stream if the mime type cannot be determined', async () => {
			apiRequestSpy.mockResolvedValueOnce({
				result: {
					file_id: 'file-id',
					file_path: 'documents/file_1.foo',
				},
			});
			apiRequestSpy.mockResolvedValueOnce({
				body: Buffer.from('test-file'),
			});
			executeFunctionsMock.helpers.prepareBinaryData.mockResolvedValue({
				data: 'test-file',
				mimeType: 'application/octet-stream',
			});

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

			expect(result).toEqual([
				[
					{
						json: {
							result: {
								file_id: 'file-id',
								file_path: 'documents/file_1.foo',
							},
						},
						binary: {
							data: {
								data: 'test-file',
								mimeType: 'application/octet-stream',
							},
						},
						pairedItem: { item: 0 },
					},
				],
			]);
			expect(executeFunctionsMock.helpers.prepareBinaryData).toHaveBeenCalledWith(
				Buffer.from('test-file'),
				'file_1.foo',
				'application/octet-stream',
			);
		});

		it('should use the provided mime type if it is specified', async () => {
			executeFunctionsMock.getNodeParameter.mockImplementation((p) => {
				switch (p) {
					case 'resource':
						return 'file';
					case 'operation':
						return 'get';
					case 'download':
						return true;
					case 'fileId':
						return 'file-id';
					case 'additionalFields':
						return { mimeType: 'image/jpeg' };
					default:
						return undefined;
				}
			});
			apiRequestSpy.mockResolvedValueOnce({
				result: {
					file_id: 'file-id',
					file_path: 'documents/file_1.pdf',
				},
			});
			apiRequestSpy.mockResolvedValueOnce({
				body: Buffer.from('test-file'),
			});
			executeFunctionsMock.helpers.prepareBinaryData.mockResolvedValue({
				data: 'test-file',
				mimeType: 'image/jpeg',
			});

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

			expect(result).toEqual([
				[
					{
						json: {
							result: {
								file_id: 'file-id',
								file_path: 'documents/file_1.pdf',
							},
						},
						binary: {
							data: {
								data: 'test-file',
								mimeType: 'image/jpeg',
							},
						},
						pairedItem: { item: 0 },
					},
				],
			]);
			expect(executeFunctionsMock.helpers.prepareBinaryData).toHaveBeenCalledWith(
				Buffer.from('test-file'),
				'file_1.pdf',
				'image/jpeg',
			);
		});
	});

	describe('assertBinaryData usage', () => {
		beforeEach(() => {
			executeFunctionsMock.getNodeParameter.mockImplementation((paramName, _) => {
				switch (paramName) {
					case 'resource':
						return 'message';
					case 'operation':
						return 'sendPhoto';
					case 'binaryData':
						return true;
					case 'chatId':
						return 'chat-id';
					case 'binaryPropertyName':
						return 'data';
					case 'additionalFields.fileName':
						return '';
					case 'additionalFields':
						return {};
					default:
						return undefined;
				}
			});
		});

		it('should call assertBinaryData with correct parameters', async () => {
			executeFunctionsMock.getInputData.mockReturnValue([
				{
					json: {},
					binary: {
						data: {
							data: 'binary-data',
							mimeType: 'image/jpeg',
							fileName: 'photo.jpg',
						},
					},
				},
			]);

			executeFunctionsMock.helpers.assertBinaryData.mockReturnValue({
				data: 'binary-data',
				mimeType: 'image/jpeg',
				fileName: 'photo.jpg',
			});

			apiRequestSpy.mockResolvedValue([{ result: { message_id: 123 } }]);

			await node.execute.call(executeFunctionsMock);

			expect(executeFunctionsMock.helpers.assertBinaryData).toHaveBeenCalledWith(0, 'data');
		});

		it('should call assertBinaryData for each item with correct index', async () => {
			executeFunctionsMock.getNodeParameter.mockImplementation((paramName, index) => {
				switch (paramName) {
					case 'resource':
						return 'message';
					case 'operation':
						return 'sendPhoto';
					case 'binaryData':
						return true;
					case 'chatId':
						return `chat-id-${index}`;
					case 'binaryPropertyName':
						return `data${index}`;
					case 'additionalFields.fileName':
						return '';
					case 'additionalFields':
						return {};
					default:
						return undefined;
				}
			});

			executeFunctionsMock.getInputData.mockReturnValue([
				{
					json: {},
					binary: {
						data0: {
							data: 'binary-data-0',
							mimeType: 'image/jpeg',
							fileName: 'photo0.jpg',
						},
					},
				},
				{
					json: {},
					binary: {
						data1: {
							data: 'binary-data-1',
							mimeType: 'image/png',
							fileName: 'photo1.png',
						},
					},
				},
				{
					json: {},
					binary: {
						data2: {
							data: 'binary-data-2',
							mimeType: 'image/gif',
							fileName: 'photo2.gif',
						},
					},
				},
			]);

			executeFunctionsMock.helpers.assertBinaryData.mockImplementation(legacyBinaryAccessHelper);

			apiRequestSpy.mockResolvedValue([{ result: { message_id: 123 } }]);

			await node.execute.call(executeFunctionsMock);

			expect(executeFunctionsMock.helpers.assertBinaryData).toHaveBeenCalledTimes(3);
			expect(executeFunctionsMock.helpers.assertBinaryData).toHaveBeenNthCalledWith(1, 0, 'data0');
			expect(executeFunctionsMock.helpers.assertBinaryData).toHaveBeenNthCalledWith(2, 1, 'data1');
			expect(executeFunctionsMock.helpers.assertBinaryData).toHaveBeenNthCalledWith(3, 2, 'data2');
		});

		it('should throw error when binary data is missing', async () => {
			executeFunctionsMock.getInputData.mockReturnValue([
				{
					json: {},
					// No binary data
				},
			]);

			executeFunctionsMock.helpers.assertBinaryData.mockImplementation(() => {
				throw new Error('No binary data exists on item!');
			});

			await expect(node.execute.call(executeFunctionsMock)).rejects.toThrow(
				'No binary data exists on item!',
			);

			expect(executeFunctionsMock.helpers.assertBinaryData).toHaveBeenCalledWith(0, 'data');
		});

		it('should throw error when specified binary property does not exist', async () => {
			executeFunctionsMock.getNodeParameter.mockImplementation((paramName) => {
				switch (paramName) {
					case 'resource':
						return 'message';
					case 'operation':
						return 'sendPhoto';
					case 'binaryData':
						return true;
					case 'chatId':
						return 'chat-id';
					case 'binaryPropertyName':
						return 'nonExistentProperty';
					case 'additionalFields.fileName':
						return '';
					case 'additionalFields':
						return {};
					default:
						return undefined;
				}
			});

			executeFunctionsMock.getInputData.mockReturnValue([
				{
					json: {},
					binary: {
						data: {
							data: 'binary-data',
							mimeType: 'image/jpeg',
							fileName: 'photo.jpg',
						},
					},
				},
			]);

			executeFunctionsMock.helpers.assertBinaryData.mockImplementation(() => {
				throw new Error("There is no binary data property 'nonExistentProperty' on item!");
			});

			await expect(node.execute.call(executeFunctionsMock)).rejects.toThrow(
				"There is no binary data property 'nonExistentProperty' on item!",
			);

			expect(executeFunctionsMock.helpers.assertBinaryData).toHaveBeenCalledWith(
				0,
				'nonExistentProperty',
			);
		});

		it('should use fileName from assertBinaryData result when additionalFields.fileName is not provided', async () => {
			const mockBinaryData = {
				data: 'binary-data',
				mimeType: 'image/jpeg',
				fileName: 'from-binary-data.jpg',
			};

			executeFunctionsMock.getInputData.mockReturnValue([
				{
					json: {},
					binary: {
						data: mockBinaryData,
					},
				},
			]);

			executeFunctionsMock.helpers.assertBinaryData.mockReturnValue(mockBinaryData);

			apiRequestSpy.mockResolvedValue([{ result: { message_id: 123 } }]);

			await node.execute.call(executeFunctionsMock);

			expect(executeFunctionsMock.helpers.assertBinaryData).toHaveBeenCalledWith(0, 'data');
			expect(apiRequestSpy).toHaveBeenCalledWith(
				'POST',
				'sendPhoto',
				{},
				{},
				expect.objectContaining({
					formData: expect.objectContaining({
						photo: expect.objectContaining({
							options: expect.objectContaining({
								filename: 'from-binary-data.jpg',
								contentType: 'image/jpeg',
							}),
						}),
					}),
				}),
			);
		});
	});

	describe('message:sendPhoto with binary data', () => {
		beforeEach(() => {
			executeFunctionsMock.helpers.assertBinaryData.mockImplementation(legacyBinaryAccessHelper);

			executeFunctionsMock.getNodeParameter.mockImplementation((paramName, index) => {
				switch (paramName) {
					case 'resource':
						return 'message';
					case 'operation':
						return 'sendPhoto';
					case 'binaryData':
						return true;
					case 'chatId':
						return index === 0 ? 'chat-id-0' : index === 1 ? 'chat-id-1' : 'chat-id-2';
					case 'binaryPropertyName':
						return index === 0 ? 'data0' : index === 1 ? 'data1' : 'data2';
					case 'additionalFields.fileName':
						return index === 0 ? 'photo0.jpg' : index === 1 ? 'photo1.png' : 'photo2.gif';
					case 'additionalFields':
						return {};
					default:
						return undefined;
				}
			});
		});

		it('should use correct index for binaryPropertyName parameter', async () => {
			executeFunctionsMock.getInputData.mockReturnValue([
				{
					json: {},
					binary: {
						data0: {
							data: 'binary-data-0',
							mimeType: 'image/jpeg',
							fileName: 'original0.jpg',
						},
					},
				},
				{
					json: {},
					binary: {
						data1: {
							data: 'binary-data-1',
							mimeType: 'image/png',
							fileName: 'original1.png',
						},
					},
				},
			]);

			apiRequestSpy.mockResolvedValue([{ result: { message_id: 123 } }]);

			await node.execute.call(executeFunctionsMock);

			expect(executeFunctionsMock.getNodeParameter).toHaveBeenCalledWith('binaryPropertyName', 0);
			expect(executeFunctionsMock.getNodeParameter).toHaveBeenCalledWith('binaryPropertyName', 1);
			expect(executeFunctionsMock.getNodeParameter).not.toHaveBeenCalledWith(
				'binaryPropertyName',
				0,
				expect.anything(),
			);
			expect(executeFunctionsMock.getNodeParameter).not.toHaveBeenCalledWith(
				'binaryPropertyName',
				1,
				expect.anything(),
			);
		});

		it('should use correct index for additionalFields.fileName parameter', async () => {
			executeFunctionsMock.getInputData.mockReturnValue([
				{
					json: {},
					binary: {
						data0: {
							data: 'binary-data-0',
							mimeType: 'image/jpeg',
						},
					},
				},
				{
					json: {},
					binary: {
						data1: {
							data: 'binary-data-1',
							mimeType: 'image/png',
						},
					},
				},
			]);

			apiRequestSpy.mockResolvedValue([{ result: { message_id: 123 } }]);

			await node.execute.call(executeFunctionsMock);

			expect(executeFunctionsMock.getNodeParameter).toHaveBeenCalledWith(
				'additionalFields.fileName',
				0,
				'',
			);
			expect(executeFunctionsMock.getNodeParameter).toHaveBeenCalledWith(
				'additionalFields.fileName',
				1,
				'',
			);
		});

		it('should use correct binary data for each item based on binaryPropertyName index', async () => {
			executeFunctionsMock.getInputData.mockReturnValue([
				{
					json: {},
					binary: {
						data0: {
							data: 'binary-data-0',
							mimeType: 'image/jpeg',
							fileName: 'original0.jpg',
						},
						wrongData: {
							data: 'wrong-binary-data',
							mimeType: 'image/gif',
						},
					},
				},
				{
					json: {},
					binary: {
						data1: {
							data: 'binary-data-1',
							mimeType: 'image/png',
							fileName: 'original1.png',
						},
						wrongData: {
							data: 'wrong-binary-data',
							mimeType: 'image/gif',
						},
					},
				},
			]);

			apiRequestSpy.mockResolvedValue([{ result: { message_id: 123 } }]);

			await node.execute.call(executeFunctionsMock);

			expect(apiRequestSpy).toHaveBeenCalledTimes(2);

			expect(apiRequestSpy).toHaveBeenNthCalledWith(
				1,
				'POST',
				'sendPhoto',
				{},
				{},
				expect.objectContaining({
					formData: expect.objectContaining({
						chat_id: 'chat-id-0',
						photo: expect.objectContaining({
							value: expect.any(Buffer),
							options: expect.objectContaining({
								filename: 'photo0.jpg',
								contentType: 'image/jpeg',
							}),
						}),
					}),
				}),
			);

			expect(apiRequestSpy).toHaveBeenNthCalledWith(
				2,
				'POST',
				'sendPhoto',
				{},
				{},
				expect.objectContaining({
					formData: expect.objectContaining({
						chat_id: 'chat-id-1',
						photo: expect.objectContaining({
							value: expect.any(Buffer),
							options: expect.objectContaining({
								filename: 'photo1.png',
								contentType: 'image/png',
							}),
						}),
					}),
				}),
			);
		});

		it('should fallback to binary fileName when additionalFields.fileName is empty', async () => {
			executeFunctionsMock.getNodeParameter.mockImplementation((paramName, index) => {
				switch (paramName) {
					case 'resource':
						return 'message';
					case 'operation':
						return 'sendPhoto';
					case 'binaryData':
						return true;
					case 'chatId':
						return index === 0 ? 'chat-id-0' : 'chat-id-1';
					case 'binaryPropertyName':
						return index === 0 ? 'data0' : 'data1';
					case 'additionalFields.fileName':
						return index === 0 ? '' : 'custom-name.jpg';
					case 'additionalFields':
						return {};
					default:
						return undefined;
				}
			});

			executeFunctionsMock.getInputData.mockReturnValue([
				{
					json: {},
					binary: {
						data0: {
							data: 'binary-data-0',
							mimeType: 'image/jpeg',
							fileName: 'fallback-name.jpg',
						},
					},
				},
				{
					json: {},
					binary: {
						data1: {
							data: 'binary-data-1',
							mimeType: 'image/png',
							fileName: 'original-name.png',
						},
					},
				},
			]);

			apiRequestSpy.mockResolvedValue([{ result: { message_id: 123 } }]);

			await node.execute.call(executeFunctionsMock);

			const expectFileName = (index: number, filename: string) => {
				expect(apiRequestSpy).toHaveBeenNthCalledWith(
					index,
					'POST',
					'sendPhoto',
					{},
					{},
					{
						formData: expect.objectContaining({
							photo: expect.objectContaining({
								options: expect.objectContaining({
									filename,
								}),
							}),
						}),
					},
				);
			};

			expectFileName(1, 'fallback-name.jpg');
			expectFileName(2, 'custom-name.jpg');
		});

		it('should process different chat IDs for multiple items correctly', async () => {
			executeFunctionsMock.getInputData.mockReturnValue([
				{
					json: {},
					binary: {
						data0: {
							data: 'binary-data-0',
							mimeType: 'image/jpeg',
							fileName: 'photo0.jpg',
						},
					},
				},
				{
					json: {},
					binary: {
						data1: {
							data: 'binary-data-1',
							mimeType: 'image/png',
							fileName: 'photo1.png',
						},
					},
				},
				{
					json: {},
					binary: {
						data2: {
							data: 'binary-data-2',
							mimeType: 'image/gif',
							fileName: 'photo2.gif',
						},
					},
				},
			]);

			apiRequestSpy.mockResolvedValue([{ result: { message_id: 123 } }]);

			await node.execute.call(executeFunctionsMock);

			expect(executeFunctionsMock.getNodeParameter).toHaveBeenCalledWith('chatId', 0);
			expect(executeFunctionsMock.getNodeParameter).toHaveBeenCalledWith('chatId', 1);
			expect(executeFunctionsMock.getNodeParameter).toHaveBeenCalledWith('chatId', 2);

			expect(apiRequestSpy).toHaveBeenCalledTimes(3);

			const expectChatId = (n: number, chatId: string) => {
				expect(apiRequestSpy).toHaveBeenNthCalledWith(
					n,
					'POST',
					'sendPhoto',
					{},
					{},
					{
						formData: expect.objectContaining({
							chat_id: chatId,
						}),
					},
				);
			};

			expectChatId(1, 'chat-id-0');
			expectChatId(2, 'chat-id-1');
			expectChatId(3, 'chat-id-2');
		});
	});
});
