import { mockDeep } from 'vitest-mock-extended';
import type { IExecuteFunctions, IDataObject } from 'n8n-workflow';
import { NodeOperationError, jsonParse } from 'n8n-workflow';
import { getAuditLogReasonHeaders, prepareMultiPartForm } from '../utils';

describe('Discord V2 Utils', () => {
	describe('prepareMultiPartForm', () => {
		let mockExecuteFunctions: IExecuteFunctions;

		beforeEach(() => {
			mockExecuteFunctions = mockDeep<IExecuteFunctions>();
		});

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

		it('should create multipart form with single file', async () => {
			const files: IDataObject[] = [{ inputFieldName: 'file1' }];
			const jsonPayload: IDataObject = { content: 'Test message' };
			const itemIndex = 0;

			const binaryData = {
				data: 'base64data',
				mimeType: 'image/png',
				fileName: 'test.png',
				fileExtension: 'png',
			};

			mockExecuteFunctions.helpers.assertBinaryData = vi.fn().mockReturnValue(binaryData);
			mockExecuteFunctions.helpers.getBinaryDataBuffer = vi
				.fn()
				.mockResolvedValue(Buffer.from('test file content'));
			mockExecuteFunctions.getNode = vi.fn().mockReturnValue({ name: 'Discord' });

			const result = await prepareMultiPartForm.call(
				mockExecuteFunctions,
				files,
				jsonPayload,
				itemIndex,
			);

			expect(mockExecuteFunctions.helpers.assertBinaryData).toHaveBeenCalledWith(0, 'file1');
			expect(mockExecuteFunctions.helpers.getBinaryDataBuffer).toHaveBeenCalledWith(0, 'file1');
			expect(result).toBeDefined();
		});

		it('should create multipart form with multiple files', async () => {
			const files: IDataObject[] = [{ inputFieldName: 'file1' }, { inputFieldName: 'file2' }];
			const jsonPayload: IDataObject = { content: 'Test message with multiple files' };
			const itemIndex = 0;

			const binaryData1 = {
				data: 'base64data1',
				mimeType: 'image/png',
				fileName: 'test1.png',
				fileExtension: 'png',
			};

			const binaryData2 = {
				data: 'base64data2',
				mimeType: 'image/jpeg',
				fileName: 'test2.jpg',
				fileExtension: 'jpg',
			};

			mockExecuteFunctions.helpers.assertBinaryData = vi
				.fn()
				.mockReturnValueOnce(binaryData1)
				.mockReturnValueOnce(binaryData2);
			mockExecuteFunctions.helpers.getBinaryDataBuffer = vi
				.fn()
				.mockResolvedValueOnce(Buffer.from('test file content 1'))
				.mockResolvedValueOnce(Buffer.from('test file content 2'));
			mockExecuteFunctions.getNode = vi.fn().mockReturnValue({ name: 'Discord' });

			const result = await prepareMultiPartForm.call(
				mockExecuteFunctions,
				files,
				jsonPayload,
				itemIndex,
			);

			expect(mockExecuteFunctions.helpers.assertBinaryData).toHaveBeenCalledTimes(2);
			expect(mockExecuteFunctions.helpers.getBinaryDataBuffer).toHaveBeenCalledTimes(2);
			expect(result).toBeDefined();
		});

		it('should add file extension from fileExtension property when filename has no extension', async () => {
			const files: IDataObject[] = [{ inputFieldName: 'file1' }];
			const jsonPayload: IDataObject = { content: 'Test message' };
			const itemIndex = 0;

			const binaryData = {
				data: 'base64data',
				mimeType: 'image/png',
				fileName: 'testfile',
				fileExtension: 'png',
			};

			mockExecuteFunctions.helpers.assertBinaryData = vi.fn().mockReturnValue(binaryData);
			mockExecuteFunctions.helpers.getBinaryDataBuffer = vi
				.fn()
				.mockResolvedValue(Buffer.from('test file content'));
			mockExecuteFunctions.getNode = vi.fn().mockReturnValue({ name: 'Discord' });

			const result = await prepareMultiPartForm.call(
				mockExecuteFunctions,
				files,
				jsonPayload,
				itemIndex,
			);

			expect(result).toBeDefined();
		});

		it('should add file extension from mimeType when filename has no extension and fileExtension is missing', async () => {
			const files: IDataObject[] = [{ inputFieldName: 'file1' }];
			const jsonPayload: IDataObject = { content: 'Test message' };
			const itemIndex = 0;

			const binaryData = {
				data: 'base64data',
				mimeType: 'image/jpeg',
				fileName: 'testfile',
			};

			mockExecuteFunctions.helpers.assertBinaryData = vi.fn().mockReturnValue(binaryData);
			mockExecuteFunctions.helpers.getBinaryDataBuffer = vi
				.fn()
				.mockResolvedValue(Buffer.from('test file content'));
			mockExecuteFunctions.getNode = vi.fn().mockReturnValue({ name: 'Discord' });

			const result = await prepareMultiPartForm.call(
				mockExecuteFunctions,
				files,
				jsonPayload,
				itemIndex,
			);

			expect(result).toBeDefined();
		});

		it('should throw error when binary data is not found', async () => {
			const files: IDataObject[] = [{ inputFieldName: 'file1' }];
			const jsonPayload: IDataObject = { content: 'Test message' };
			const itemIndex = 0;

			mockExecuteFunctions.helpers.assertBinaryData = vi.fn().mockReturnValue(null);
			mockExecuteFunctions.getNode = vi.fn().mockReturnValue({ name: 'Discord' });

			await expect(
				prepareMultiPartForm.call(mockExecuteFunctions, files, jsonPayload, itemIndex),
			).rejects.toThrow(NodeOperationError);

			await expect(
				prepareMultiPartForm.call(mockExecuteFunctions, files, jsonPayload, itemIndex),
			).rejects.toThrow('Input item [0] does not contain binary data on property file1');
		});

		it('should handle file with complete filename including extension', async () => {
			const files: IDataObject[] = [{ inputFieldName: 'file1' }];
			const jsonPayload: IDataObject = { content: 'Test message' };
			const itemIndex = 0;

			const binaryData = {
				data: 'base64data',
				mimeType: 'application/pdf',
				fileName: 'document.pdf',
				fileExtension: 'pdf',
			};

			mockExecuteFunctions.helpers.assertBinaryData = vi.fn().mockReturnValue(binaryData);
			mockExecuteFunctions.helpers.getBinaryDataBuffer = vi
				.fn()
				.mockResolvedValue(Buffer.from('pdf content'));
			mockExecuteFunctions.getNode = vi.fn().mockReturnValue({ name: 'Discord' });

			const result = await prepareMultiPartForm.call(
				mockExecuteFunctions,
				files,
				jsonPayload,
				itemIndex,
			);

			expect(result).toBeDefined();
		});

		it('should include attachments in payload_json with correct structure', async () => {
			const files: IDataObject[] = [{ inputFieldName: 'file1' }, { inputFieldName: 'file2' }];
			const jsonPayload: IDataObject = { content: 'Test with attachments' };
			const itemIndex = 0;

			const binaryData1 = {
				data: 'base64data1',
				mimeType: 'image/png',
				fileName: 'image1.png',
			};

			const binaryData2 = {
				data: 'base64data2',
				mimeType: 'text/plain',
				fileName: 'text.txt',
			};

			mockExecuteFunctions.helpers.assertBinaryData = vi
				.fn()
				.mockReturnValueOnce(binaryData1)
				.mockReturnValueOnce(binaryData2);
			mockExecuteFunctions.helpers.getBinaryDataBuffer = vi
				.fn()
				.mockResolvedValueOnce(Buffer.from('image content'))
				.mockResolvedValueOnce(Buffer.from('text content'));
			mockExecuteFunctions.getNode = vi.fn().mockReturnValue({ name: 'Discord' });

			const result = await prepareMultiPartForm.call(
				mockExecuteFunctions,
				files,
				jsonPayload,
				itemIndex,
			);

			expect(result).toBeDefined();

			const payloadJsonField = result
				.getBuffer()
				.toString()
				.match(
					/Content-Disposition: form-data; name="payload_json"[\s\S]*?\r\n\r\n([\s\S]*?)\r\n--/,
				);
			expect(payloadJsonField).not.toBeNull();

			const payloadJson = jsonParse<{
				content: string;
				attachments: Array<{ id: number; filename: string }>;
			}>(payloadJsonField![1]);

			expect(payloadJson.content).toBe('Test with attachments');

			expect(payloadJson.attachments).toBeDefined();
			expect(Array.isArray(payloadJson.attachments)).toBe(true);
			expect(payloadJson.attachments).toHaveLength(2);

			expect(payloadJson.attachments[0]).toEqual({
				id: 0,
				filename: 'image1.png',
			});

			expect(payloadJson.attachments[1]).toEqual({
				id: 1,
				filename: 'text.txt',
			});
		});

		it('should use filename as filename and mimeType as content-type in file parts', async () => {
			const files: IDataObject[] = [{ inputFieldName: 'document' }];
			const jsonPayload: IDataObject = { content: 'Test' };
			const itemIndex = 0;

			const binaryData = {
				data: 'base64data',
				mimeType: 'application/pdf',
				fileName: 'report.pdf',
				fileExtension: 'pdf',
			};

			mockExecuteFunctions.helpers.assertBinaryData = vi.fn().mockReturnValue(binaryData);
			mockExecuteFunctions.helpers.getBinaryDataBuffer = vi
				.fn()
				.mockResolvedValue(Buffer.from('pdf content'));
			mockExecuteFunctions.getNode = vi.fn().mockReturnValue({ name: 'Discord' });

			const result = await prepareMultiPartForm.call(
				mockExecuteFunctions,
				files,
				jsonPayload,
				itemIndex,
			);

			const body = result.getBuffer().toString();

			// File part should have the actual filename, not the mime type
			expect(body).toContain('filename="report.pdf"');
			// File part should have the actual mime type, not the filename
			expect(body).toContain('Content-Type: application/pdf');
			// Sanity check: the values must not be swapped
			expect(body).not.toContain('filename="application/pdf"');
			expect(body).not.toContain('Content-Type: report.pdf');
		});

		it('should handle empty files array', async () => {
			const files: IDataObject[] = [];
			const jsonPayload: IDataObject = { content: 'Test message no files' };
			const itemIndex = 0;

			mockExecuteFunctions.getNode = vi.fn().mockReturnValue({ name: 'Discord' });

			const result = await prepareMultiPartForm.call(
				mockExecuteFunctions,
				files,
				jsonPayload,
				itemIndex,
			);

			expect(result).toBeDefined();
		});

		it('should properly format filename when only mimeType is available', async () => {
			const files: IDataObject[] = [{ inputFieldName: 'file1' }];
			const jsonPayload: IDataObject = { content: 'Test message' };
			const itemIndex = 0;

			const binaryData = {
				data: 'base64data',
				mimeType: 'application/json',
				fileName: 'data',
			};

			mockExecuteFunctions.helpers.assertBinaryData = vi.fn().mockReturnValue(binaryData);
			mockExecuteFunctions.helpers.getBinaryDataBuffer = vi
				.fn()
				.mockResolvedValue(Buffer.from('json content'));
			mockExecuteFunctions.getNode = vi.fn().mockReturnValue({ name: 'Discord' });

			const result = await prepareMultiPartForm.call(
				mockExecuteFunctions,
				files,
				jsonPayload,
				itemIndex,
			);

			expect(result).toBeDefined();
		});

		it('should handle different item indices correctly', async () => {
			const files: IDataObject[] = [{ inputFieldName: 'file1' }];
			const jsonPayload: IDataObject = { content: 'Test message' };
			const itemIndex = 2;

			const binaryData = {
				data: 'base64data',
				mimeType: 'image/png',
				fileName: 'test.png',
			};

			mockExecuteFunctions.helpers.assertBinaryData = vi.fn().mockReturnValue(binaryData);
			mockExecuteFunctions.helpers.getBinaryDataBuffer = vi
				.fn()
				.mockResolvedValue(Buffer.from('test file content'));
			mockExecuteFunctions.getNode = vi.fn().mockReturnValue({ name: 'Discord' });

			const result = await prepareMultiPartForm.call(
				mockExecuteFunctions,
				files,
				jsonPayload,
				itemIndex,
			);

			expect(mockExecuteFunctions.helpers.assertBinaryData).toHaveBeenCalledWith(2, 'file1');
			expect(mockExecuteFunctions.helpers.getBinaryDataBuffer).toHaveBeenCalledWith(2, 'file1');
			expect(result).toBeDefined();
		});
	});

	describe('getAuditLogReasonHeaders', () => {
		let mockExecuteFunctions: IExecuteFunctions;

		// Returns getNodeParameter values keyed by parameter name for a single item.
		const mockParams = (params: Record<string, string>) => {
			mockExecuteFunctions.getNodeParameter = vi
				.fn()
				.mockImplementation((name: string, _i: number, fallback: string) =>
					name in params ? params[name] : fallback,
				);
		};

		beforeEach(() => {
			mockExecuteFunctions = mockDeep<IExecuteFunctions>();
		});

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

		it('maps a preset reason to its label and URL-encodes it', () => {
			mockParams({ reason: 'suspicious_spam' });

			const headers = getAuditLogReasonHeaders.call(mockExecuteFunctions, 0);

			expect(headers).toEqual({ 'X-Audit-Log-Reason': 'Suspicious%20or%20spam%20account' });
		});

		it('uses the custom text when the reason is "other"', () => {
			mockParams({ reason: 'other', reasonCustom: 'Posting phishing links' });

			const headers = getAuditLogReasonHeaders.call(mockExecuteFunctions, 0);

			expect(headers).toEqual({ 'X-Audit-Log-Reason': 'Posting%20phishing%20links' });
		});

		it('returns no header when the reason is "other" but the custom text is empty', () => {
			mockParams({ reason: 'other', reasonCustom: '' });

			expect(getAuditLogReasonHeaders.call(mockExecuteFunctions, 0)).toEqual({});
		});

		it('returns no header when no reason is set', () => {
			mockParams({});

			expect(getAuditLogReasonHeaders.call(mockExecuteFunctions, 0)).toEqual({});
		});
	});
});
