import type { Request, Response } from 'express';
import { type MockProxy, mock } from 'vitest-mock-extended';
import type {
	IExecuteFunctions,
	INodeProperties,
	IWebhookFunctions,
	IWorkflowSettings,
} from 'n8n-workflow';
import { NodeOperationError, WAIT_INDEFINITELY } from 'n8n-workflow';

import { configureWaitTillDate } from '../configureWaitTillDate.util';
import {
	getSendAndWaitProperties,
	getSendAndWaitConfig,
	createEmail,
	sendAndWaitWebhook,
} from '../utils';

describe('Send and Wait utils tests', () => {
	let mockExecuteFunctions: MockProxy<IExecuteFunctions>;
	let mockWebhookFunctions: MockProxy<IWebhookFunctions>;

	beforeEach(() => {
		mockExecuteFunctions = mock<IExecuteFunctions>();
		mockWebhookFunctions = mock<IWebhookFunctions>();
		mockWebhookFunctions.getWorkflowSettings.mockReturnValue(mock<IWorkflowSettings>({}));
	});

	describe('getSendAndWaitProperties', () => {
		it('should return properties with correct display options', () => {
			const targetProperties: INodeProperties[] = [
				{
					displayName: 'Test Property',
					name: 'testProperty',
					type: 'string',
					default: '',
				},
			];
			const extraOptions: INodeProperties[] = [
				{
					displayName: 'Extra Property',
					name: 'extraProperty',
					type: 'string',
					default: '',
				},
			];

			const result = getSendAndWaitProperties(targetProperties, undefined, undefined, {
				extraOptions,
			});

			expect(result).toEqual(
				expect.arrayContaining([
					expect.objectContaining({
						name: 'options',
						options: expect.arrayContaining([
							expect.objectContaining({
								name: 'extraProperty',
							}),
						]),
					}),
				]),
			);
		});

		it('should include extra options when provided', () => {
			const targetProperties: INodeProperties[] = [
				{
					displayName: 'Test Property',
					name: 'testProperty',
					type: 'string',
					default: '',
				},
			];
			const extraOptions: INodeProperties[] = [
				{
					displayName: 'Extra Property',
					name: 'extraProperty',
					type: 'string',
					default: '',
				},
			];
			const result = getSendAndWaitProperties(targetProperties, undefined, undefined, {
				extraOptions,
			});
			expect(result).toEqual(
				expect.arrayContaining([
					expect.objectContaining({
						displayOptions: {
							show: {
								resource: ['message'],
								operation: ['sendAndWait'],
							},
						},
					}),
				]),
			);
		});
	});

	describe('getSendAndWaitConfig', () => {
		it('should return correct config for single approval', () => {
			mockExecuteFunctions.getNodeParameter.mockImplementation((parameterName: string) => {
				const params: { [key: string]: any } = {
					message: 'Test message',
					subject: 'Test subject',
					'approvalOptions.values': {
						approvalType: 'single',
						approveLabel: 'Approve',
						buttonApprovalStyle: 'primary',
					},
				};
				return params[parameterName];
			});

			mockExecuteFunctions.getSignedResumeUrl.mockReturnValue(
				'http://localhost/waiting-webhook/nodeID?approved=true&signature=abc',
			);
			const config = getSendAndWaitConfig(mockExecuteFunctions);

			expect(config).toEqual({
				appendAttribution: undefined,
				title: 'Test subject',
				message: 'Test message',
				options: [
					{
						label: 'Approve',
						style: 'primary',
						url: 'http://localhost/waiting-webhook/nodeID?approved=true&signature=abc',
						approved: true,
					},
				],
			});
		});

		it('should return correct config for double approval', () => {
			mockExecuteFunctions.getNodeParameter.mockImplementation((parameterName: string) => {
				const params: { [key: string]: any } = {
					message: 'Test message',
					subject: 'Test subject',
					'approvalOptions.values': {
						approvalType: 'double',
						approveLabel: 'Approve',
						buttonApprovalStyle: 'primary',
						disapproveLabel: 'Reject',
						buttonDisapprovalStyle: 'secondary',
					},
				};
				return params[parameterName];
			});

			mockExecuteFunctions.getSignedResumeUrl.mockReturnValueOnce(
				'http://localhost/waiting-webhook/nodeID?approved=true&signature=abc',
			);
			mockExecuteFunctions.getSignedResumeUrl.mockReturnValueOnce(
				'http://localhost/waiting-webhook/nodeID?approved=false&signature=abc',
			);

			const config = getSendAndWaitConfig(mockExecuteFunctions);

			expect(config.options).toHaveLength(2);
			expect(config.options).toEqual(
				expect.arrayContaining([
					{
						label: 'Reject',
						style: 'secondary',
						url: 'http://localhost/waiting-webhook/nodeID?approved=false&signature=abc',
						approved: false,
					},
					{
						label: 'Approve',
						style: 'primary',
						url: 'http://localhost/waiting-webhook/nodeID?approved=true&signature=abc',
						approved: true,
					},
				]),
			);
		});

		describe('customForm validation', () => {
			const mockCustomFormParameters = (params: { [key: string]: any }) =>
				mockExecuteFunctions.getNodeParameter.mockImplementation(
					(parameterName: string) =>
						({
							message: 'Pick a customer',
							responseType: 'customForm',
							...params,
						})[parameterName],
				);

			it('should throw when the form JSON resolves to invalid form fields', () => {
				mockExecuteFunctions.getNode.mockReturnValue({ name: 'Send Email' } as any);
				mockCustomFormParameters({
					defineForm: 'json',
					jsonOutput: '={{ JSON.stringify($json.formFields) }}',
				});
				// A customer row without a name resolves to `{ option: null }`
				mockExecuteFunctions.evaluateExpression.mockReturnValue([
					{
						fieldLabel: 'Customer',
						fieldType: 'dropdown',
						fieldOptions: { values: [{ option: null }] },
					},
				] as any);

				expect(() => getSendAndWaitConfig(mockExecuteFunctions)).toThrow(
					'Field dropdown in field 0 has an invalid option 0',
				);
			});

			it('should return the config when the form JSON resolves to valid form fields', () => {
				mockCustomFormParameters({
					defineForm: 'json',
					jsonOutput: '={{ JSON.stringify($json.formFields) }}',
					'options.messageButtonLabel': 'Respond',
				});
				mockExecuteFunctions.evaluateExpression.mockReturnValue([
					{
						fieldLabel: 'Customer',
						fieldType: 'dropdown',
						fieldOptions: { values: [{ option: 'Acme Corp' }] },
					},
				] as any);
				mockExecuteFunctions.getSignedResumeUrl.mockReturnValue(
					'http://localhost/waiting-webhook/nodeID?approved=true&signature=abc',
				);

				const config = getSendAndWaitConfig(mockExecuteFunctions);

				expect(config.options).toEqual([
					{
						label: 'Respond',
						style: 'primary',
						url: 'http://localhost/waiting-webhook/nodeID?approved=true&signature=abc',
						approved: true,
					},
				]);
			});

			const readParameterNames = () =>
				mockExecuteFunctions.getNodeParameter.mock.calls.map(([name]) => name);

			it('should not read the form JSON when the form is defined with fields', () => {
				mockCustomFormParameters({ defineForm: 'fields' });

				getSendAndWaitConfig(mockExecuteFunctions);

				expect(readParameterNames()).not.toContain('jsonOutput');
			});

			it('should not read the form JSON for approval response types', () => {
				mockCustomFormParameters({
					responseType: 'approval',
					'approvalOptions.values': { approvalType: 'single' },
					defineForm: 'json',
					jsonOutput: '={{ JSON.stringify($json.formFields) }}',
				});

				getSendAndWaitConfig(mockExecuteFunctions);

				expect(readParameterNames()).not.toContain('jsonOutput');
			});
		});
	});

	describe('createEmail', () => {
		beforeEach(() => {
			mockExecuteFunctions.getNodeParameter.mockImplementation((parameterName: string) => {
				const params: { [key: string]: any } = {
					sendTo: 'test@example.com',
					message: 'Test message',
					subject: 'Test subject',
					'approvalOptions.values': {
						approvalType: 'single',
						approveLabel: 'Approve',
						buttonApprovalStyle: 'primary',
					},
				};
				return params[parameterName];
			});

			mockExecuteFunctions.getSignedResumeUrl.mockReturnValue('http://localhost/testNodeId');
		});

		it('should create a valid email object', () => {
			const email = createEmail(mockExecuteFunctions);

			expect(email).toEqual({
				to: 'test@example.com',
				subject: 'Test subject',
				body: '',
				htmlBody: expect.stringContaining('Test message'),
			});
		});

		it('should throw NodeOperationError for invalid email address', () => {
			mockExecuteFunctions.getNodeParameter.mockImplementation((parameterName: string) => {
				const params: { [key: string]: any } = {
					sendTo: 'invalid@@email.com',
					message: 'Test message',
					subject: 'Test subject',
					'approvalOptions.values': {
						approvalType: 'single',
					},
				};
				return params[parameterName];
			});

			expect(() => createEmail(mockExecuteFunctions)).toThrow(NodeOperationError);
		});
	});

	describe('sendAndWaitWebhook', () => {
		it('should handle approved webhook', async () => {
			mockWebhookFunctions.getRequestObject.mockReturnValue({
				query: { approved: 'true' },
			} as any);

			const result = await sendAndWaitWebhook.call(mockWebhookFunctions);

			expect(result).toEqual({
				webhookResponse: expect.any(String),
				workflowData: [[{ json: { data: { approved: true, respondedAt: expect.any(String) } } }]],
			});
		});

		it('should handle disapproved webhook', async () => {
			mockWebhookFunctions.getRequestObject.mockReturnValue({
				query: { approved: 'false' },
			} as any);

			const result = await sendAndWaitWebhook.call(mockWebhookFunctions);

			expect(result).toEqual({
				webhookResponse: expect.any(String),
				workflowData: [[{ json: { data: { approved: false, respondedAt: expect.any(String) } } }]],
			});
		});

		it('should handle freeText GET webhook', async () => {
			const mockRender = vi.fn();
			const mockSetHeader = vi.fn();

			mockWebhookFunctions.getRequestObject.mockReturnValue({
				method: 'GET',
			} as any);

			mockWebhookFunctions.getResponseObject.mockReturnValue({
				render: mockRender,
				setHeader: mockSetHeader,
			} as any);

			mockWebhookFunctions.getNodeParameter.mockImplementation((parameterName: string) => {
				const params: { [key: string]: any } = {
					responseType: 'freeText',
					message: 'Test message',
					options: {},
				};
				return params[parameterName];
			});

			const result = await sendAndWaitWebhook.call(mockWebhookFunctions);

			expect(result).toEqual({
				noWebhookResponse: true,
			});

			expect(mockSetHeader).toHaveBeenCalledWith(
				'Content-Security-Policy',
				'sandbox allow-downloads allow-forms allow-modals allow-orientation-lock allow-pointer-lock allow-popups allow-popups-to-escape-sandbox allow-presentation allow-scripts allow-top-navigation-by-user-activation allow-top-navigation-to-custom-protocols',
			);

			expect(mockRender).toHaveBeenCalledWith('form-trigger', {
				testRun: false,
				formTitle: '',
				formDescription: 'Test message',
				formDescriptionMetadata: 'Test message',
				formSubmittedHeader: 'Got it, thanks',
				formSubmittedText: 'This page can be closed now',
				n8nWebsiteLink: 'https://n8n.io/?utm_source=n8n-internal&utm_medium=form-trigger',
				formFields: [
					{
						id: 'field-0',
						errorId: 'error-field-0',
						label: 'Response',
						inputRequired: 'form-required',
						defaultValue: '',
						isTextarea: true,
					},
				],
				appendAttribution: true,
				buttonLabel: 'Submit',
			});
		});

		it('should handle freeText POST webhook', async () => {
			mockWebhookFunctions.getRequestObject.mockReturnValue({
				method: 'POST',
			} as any);

			mockWebhookFunctions.getBodyData.mockReturnValue({
				data: {
					'field-0': 'test value',
				},
			} as any);

			mockWebhookFunctions.getNodeParameter.mockImplementation((parameterName: string) => {
				const params: { [key: string]: any } = {
					responseType: 'freeText',
				};
				return params[parameterName];
			});

			const result = await sendAndWaitWebhook.call(mockWebhookFunctions);

			expect(result.workflowData).toEqual([
				[{ json: { data: { text: 'test value', respondedAt: expect.any(String) } } }],
			]);
		});

		it('should handle customForm GET webhook', async () => {
			const mockRender = vi.fn();
			const mockSetHeader = vi.fn();

			mockWebhookFunctions.getRequestObject.mockReturnValue({
				method: 'GET',
			} as any);

			mockWebhookFunctions.getResponseObject.mockReturnValue({
				render: mockRender,
				setHeader: mockSetHeader,
			} as any);

			mockWebhookFunctions.getNodeParameter.mockImplementation((parameterName: string) => {
				const params: { [key: string]: any } = {
					responseType: 'customForm',
					message: 'Test message',
					defineForm: 'fields',
					'formFields.values': [{ label: 'Field 1', fieldType: 'text', requiredField: true }],
					options: {
						responseFormTitle: 'Test title',
						responseFormDescription: 'Test description',
						responseFormButtonLabel: 'Test button',
						responseFormCustomCss: 'body { background-color: red; }',
					},
				};
				return params[parameterName];
			});

			const result = await sendAndWaitWebhook.call(mockWebhookFunctions);

			expect(result).toEqual({
				noWebhookResponse: true,
			});

			expect(mockSetHeader).toHaveBeenCalledWith(
				'Content-Security-Policy',
				'sandbox allow-downloads allow-forms allow-modals allow-orientation-lock allow-pointer-lock allow-popups allow-popups-to-escape-sandbox allow-presentation allow-scripts allow-top-navigation-by-user-activation allow-top-navigation-to-custom-protocols',
			);

			expect(mockRender).toHaveBeenCalledWith('form-trigger', {
				testRun: false,
				formTitle: 'Test title',
				formDescription: 'Test description',
				formDescriptionMetadata: 'Test description',
				formSubmittedHeader: 'Got it, thanks',
				formSubmittedText: 'This page can be closed now',
				n8nWebsiteLink: 'https://n8n.io/?utm_source=n8n-internal&utm_medium=form-trigger',
				formFields: [
					{
						id: 'field-0',
						errorId: 'error-field-0',
						inputRequired: 'form-required',
						defaultValue: '',
						isInput: true,
						type: 'text',
					},
				],
				appendAttribution: true,
				buttonLabel: 'Test button',
				dangerousCustomCss: 'body { background-color: red; }',
			});
		});

		it('should resolve expressions in HTML fields for customForm GET webhook', async () => {
			const mockRender = vi.fn();
			const mockSetHeader = vi.fn();

			mockWebhookFunctions.getRequestObject.mockReturnValue({
				method: 'GET',
			} as any);

			mockWebhookFunctions.getResponseObject.mockReturnValue({
				render: mockRender,
				setHeader: mockSetHeader,
			} as any);

			// Mock evaluateExpression to resolve the expression
			mockWebhookFunctions.evaluateExpression.mockImplementation((expression) => {
				if (expression === '{{ $json.videoUrl }}') {
					return 'https://example.com/video.mp4';
				}
				return expression;
			});

			mockWebhookFunctions.getNodeParameter.mockImplementation((parameterName: string) => {
				const params: { [key: string]: any } = {
					responseType: 'customForm',
					message: 'Test message',
					defineForm: 'fields',
					'formFields.values': [
						{
							fieldLabel: 'Custom HTML',
							fieldType: 'html',
							// Use <source> tag inside <video> since sanitizeHtml allows src on source, not video
							html: '<video controls><source src="{{ $json.videoUrl }}" type="video/mp4" /></video>',
						},
					],
					options: {},
				};
				return params[parameterName];
			});

			await sendAndWaitWebhook.call(mockWebhookFunctions);

			expect(mockRender).toHaveBeenCalledWith(
				'form-trigger',
				expect.objectContaining({
					formFields: expect.arrayContaining([
						expect.objectContaining({
							html: '<video controls><source src="https://example.com/video.mp4" type="video/mp4"></source></video>',
						}),
					]),
				}),
			);
		});

		// Form fields are re-resolved from upstream data when the form is rendered, so an execution
		// that is already waiting with bad data still breaks the read-only GET. Pins the current
		// behaviour; unmasking the reason at render time is a separate follow-up.
		it('should throw when a data-driven customForm resolves to invalid form fields', async () => {
			mockWebhookFunctions.getRequestObject.mockReturnValue({ method: 'GET' } as any);
			mockWebhookFunctions.getResponseObject.mockReturnValue({
				render: vi.fn(),
				setHeader: vi.fn(),
			} as any);
			mockWebhookFunctions.getNode.mockReturnValue({ name: 'Dropdown' } as any);

			// A customer row without a name resolves to `{ option: null }`
			mockWebhookFunctions.evaluateExpression.mockReturnValue([
				{
					fieldLabel: 'Customer',
					fieldType: 'dropdown',
					fieldOptions: { values: [{ option: 'Acme Corp' }, { option: null }] },
				},
			] as any);

			mockWebhookFunctions.getNodeParameter.mockImplementation((parameterName: string) => {
				const params: { [key: string]: any } = {
					responseType: 'customForm',
					message: 'Pick a customer',
					defineForm: 'json',
					jsonOutput: '={{ JSON.stringify($json.formFields) }}',
					options: {},
				};
				return params[parameterName];
			});

			const error = await sendAndWaitWebhook
				.call(mockWebhookFunctions)
				.catch((e: NodeOperationError) => e);

			expect(error).toBeInstanceOf(NodeOperationError);
			expect((error as NodeOperationError).message).toBe(
				'Field dropdown in field 0 has an invalid option 1',
			);
			// Without `type: 'manual-form-test'` the reason is masked as
			// "Workflow Webhook Error: Workflow could not be started!" by webhook-helpers
			expect((error as NodeOperationError).type).toBeUndefined();
		});

		it('should handle customForm POST webhook', async () => {
			mockWebhookFunctions.getRequestObject.mockReturnValue({
				method: 'POST',
				contentType: 'multipart/form-data',
			} as any);
			mockWebhookFunctions.getNode.mockReturnValue({} as any);

			mockWebhookFunctions.getNodeParameter.mockImplementation((parameterName: string) => {
				const params: { [key: string]: any } = {
					responseType: 'customForm',
					defineForm: 'fields',
					'formFields.values': [
						{
							fieldLabel: 'test 1',
							fieldType: 'text',
						},
					],
				};
				return params[parameterName];
			});

			mockWebhookFunctions.getBodyData.mockReturnValue({
				data: {
					'field-0': 'test value',
				},
			} as any);

			const result = await sendAndWaitWebhook.call(mockWebhookFunctions);

			expect(result.workflowData).toEqual([
				[{ json: { data: { 'test 1': 'test value', respondedAt: expect.any(String) } } }],
			]);
		});

		it('overrides a form field named respondedAt with the server timestamp', async () => {
			mockWebhookFunctions.getRequestObject.mockReturnValue({
				method: 'POST',
				contentType: 'multipart/form-data',
			} as any);
			mockWebhookFunctions.getNode.mockReturnValue({} as any);

			mockWebhookFunctions.getNodeParameter.mockImplementation((parameterName: string) => {
				const params: { [key: string]: any } = {
					responseType: 'customForm',
					defineForm: 'fields',
					'formFields.values': [
						{
							fieldLabel: 'respondedAt',
							fieldType: 'text',
						},
					],
				};
				return params[parameterName];
			});

			mockWebhookFunctions.getBodyData.mockReturnValue({
				data: {
					'field-0': 'user-supplied-value',
				},
			} as any);

			const result = await sendAndWaitWebhook.call(mockWebhookFunctions);

			const json = (result.workflowData as any)[0][0].json;
			// The server-set timestamp must win over a same-named form field.
			expect(json.data.respondedAt).not.toBe('user-supplied-value');
			expect(new Date(json.data.respondedAt as string).toISOString()).toBe(json.data.respondedAt);
		});

		it('should return noWebhookResponse if method GET and user-agent is bot', async () => {
			mockWebhookFunctions.getRequestObject.mockReturnValue({
				method: 'GET',
				headers: {
					'user-agent': 'Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)',
				},
				query: { approved: 'false' },
			} as any);

			const send = vi.fn();

			mockWebhookFunctions.getResponseObject.mockReturnValue({
				send,
			} as any);

			mockWebhookFunctions.getNodeParameter.mockImplementation((parameterName: string) => {
				const params: { [key: string]: any } = {
					responseType: 'approval',
				};
				return params[parameterName];
			});

			const result = await sendAndWaitWebhook.call(mockWebhookFunctions);

			expect(send).toHaveBeenCalledWith('');
			expect(result).toEqual({ noWebhookResponse: true });
		});

		it('should return noWebhookResponse if user-agent is empty (Microsoft Preview Service)', async () => {
			const send = vi.fn();
			mockWebhookFunctions.getRequestObject.mockReturnValue({
				headers: {},
				query: { approved: 'true' },
			} as unknown as Request);
			mockWebhookFunctions.getResponseObject.mockReturnValue({
				send,
			} as unknown as Response);
			mockWebhookFunctions.getNodeParameter.mockImplementation(
				(parameterName: string, fallbackValue?: any) => {
					const params: Record<string, unknown> = { responseType: 'approval' };
					return params[parameterName] ?? fallbackValue;
				},
			);

			const result = await sendAndWaitWebhook.call(mockWebhookFunctions);

			expect(result).toEqual({ noWebhookResponse: true });
			expect(send).toHaveBeenCalledWith('');
		});

		it.each([
			'SkypeSpaces/1.0',
			'Microsoft Teams/1.0',
			'SkypeUriPreview Preview/1.0',
			'Preview Service/1.0',
		])(
			'should return noWebhookResponse if user-agent contains %s (Microsoft Preview Service)',
			async (userAgent) => {
				const send = vi.fn();
				mockWebhookFunctions.getRequestObject.mockReturnValue({
					headers: { 'user-agent': userAgent },
					query: { approved: 'true' },
				} as unknown as Request);
				mockWebhookFunctions.getResponseObject.mockReturnValue({
					send,
				} as unknown as Response);
				mockWebhookFunctions.getNodeParameter.mockImplementation(
					(parameterName: string, fallbackValue?: any) => {
						const params: Record<string, unknown> = { responseType: 'approval' };
						return params[parameterName] ?? fallbackValue;
					},
				);

				const result = await sendAndWaitWebhook.call(mockWebhookFunctions);

				expect(result).toEqual({ noWebhookResponse: true });
				expect(send).toHaveBeenCalledWith('');
			},
		);

		it.each([
			['freeText' as const, ''],
			['freeText' as const, 'SkypeUriPreview Preview/1.0'],
			['customForm' as const, ''],
			['customForm' as const, 'SkypeUriPreview Preview/1.0'],
		])(
			'should not block Microsoft Preview Service when responseType is %s (user-agent: %s)',
			async (responseType, userAgent) => {
				const mockRender = vi.fn();
				const mockSetHeader = vi.fn();
				mockWebhookFunctions.getRequestObject.mockReturnValue({
					method: 'GET',
					headers: { 'user-agent': userAgent },
					query: {},
				} as unknown as Request);
				mockWebhookFunctions.getResponseObject.mockReturnValue({
					render: mockRender,
					setHeader: mockSetHeader,
				} as unknown as Response);
				const formFieldParams: Record<string, unknown> =
					responseType === 'customForm'
						? {
								defineForm: 'fields',
								'formFields.values': [{ label: 'Field 1', fieldType: 'text', requiredField: true }],
							}
						: {};
				mockWebhookFunctions.getNodeParameter.mockImplementation(
					(parameterName: string, fallbackValue?: any) => {
						const params: Record<string, unknown> = {
							responseType,
							message: 'Test message',
							options: {},
							...formFieldParams,
						};
						return params[parameterName] ?? fallbackValue;
					},
				);

				const result = await sendAndWaitWebhook.call(mockWebhookFunctions);

				expect(result).toEqual({ noWebhookResponse: true });
				expect(mockRender).toHaveBeenCalled();
			},
		);
	});
});

describe('configureWaitTillDate', () => {
	let mockExecuteFunctions: MockProxy<IExecuteFunctions>;

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

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

	it('should return WAIT_INDEFINITELY if limitWaitTime is empty', () => {
		mockExecuteFunctions.getNodeParameter.mockReturnValueOnce({});
		const result = configureWaitTillDate(mockExecuteFunctions);
		expect(result).toBe(WAIT_INDEFINITELY);
	});

	it('should calculate future date correctly for afterTimeInterval with minutes', () => {
		const resumeAmount = 5;
		const resumeUnit = 'minutes';
		mockExecuteFunctions.getNodeParameter.mockReturnValueOnce({
			limitType: 'afterTimeInterval',
			resumeAmount,
			resumeUnit,
		});

		const result = configureWaitTillDate(mockExecuteFunctions);
		const expectedDate = new Date(new Date().getTime() + 5 * 60 * 1000);
		expect(result.getTime()).toBeCloseTo(expectedDate.getTime(), -2); // Allowing 100ms difference
	});

	it('should calculate future date correctly for afterTimeInterval with hours', () => {
		const resumeAmount = 2;
		const resumeUnit = 'hours';
		mockExecuteFunctions.getNodeParameter.mockReturnValueOnce({
			limitType: 'afterTimeInterval',
			resumeAmount,
			resumeUnit,
		});

		const result = configureWaitTillDate(mockExecuteFunctions);
		const expectedDate = new Date(new Date().getTime() + 2 * 60 * 60 * 1000);
		expect(result.getTime()).toBeCloseTo(expectedDate.getTime(), -2);
	});

	it('should calculate future date correctly for afterTimeInterval with days', () => {
		const resumeAmount = 1;
		const resumeUnit = 'days';
		mockExecuteFunctions.getNodeParameter.mockReturnValueOnce({
			limitType: 'afterTimeInterval',
			resumeAmount,
			resumeUnit,
		});

		const result = configureWaitTillDate(mockExecuteFunctions);
		const expectedDate = new Date(new Date().getTime() + 1 * 24 * 60 * 60 * 1000);
		expect(result.getTime()).toBeCloseTo(expectedDate.getTime(), -2);
	});

	it('should return the specified maxDateAndTime for maxDateAndTime limitType', () => {
		const maxDateAndTime = '2023-12-31T23:59:59Z';
		mockExecuteFunctions.getNodeParameter.mockReturnValueOnce({
			limitType: 'maxDateAndTime',
			maxDateAndTime,
		});

		const result = configureWaitTillDate(mockExecuteFunctions);
		expect(result).toEqual(new Date(maxDateAndTime));
	});

	it('should throw NodeOperationError for invalid maxDateAndTime format', () => {
		const invalidMaxDateAndTime = 'invalid-date';
		mockExecuteFunctions.getNodeParameter.mockReturnValue({
			limitType: 'maxDateAndTime',
			maxDateAndTime: invalidMaxDateAndTime,
		});

		expect(() => configureWaitTillDate(mockExecuteFunctions)).toThrow(NodeOperationError);
		expect(() => configureWaitTillDate(mockExecuteFunctions)).toThrow(
			'Could not configure Limit Wait Time',
		);
	});

	it('should throw NodeOperationError for invalid resumeAmount or resumeUnit', () => {
		mockExecuteFunctions.getNodeParameter.mockReturnValue({
			limitType: 'afterTimeInterval',
			resumeAmount: 'invalid',
			resumeUnit: 'minutes',
		});

		expect(() => configureWaitTillDate(mockExecuteFunctions)).toThrow(NodeOperationError);
		expect(() => configureWaitTillDate(mockExecuteFunctions)).toThrow(
			'Could not configure Limit Wait Time',
		);
	});

	it('should return WAIT_INDEFINITELY when limitWaitTime is false', () => {
		mockExecuteFunctions.getNodeParameter.mockReturnValueOnce(false);
		const result = configureWaitTillDate(mockExecuteFunctions, 'root');
		expect(result).toBe(WAIT_INDEFINITELY);
	});

	it('should calculate minutes correctly in root location', () => {
		mockExecuteFunctions.getNodeParameter
			.mockReturnValueOnce(true) // limitWaitTime
			.mockReturnValueOnce('afterTimeInterval') // limitType
			.mockReturnValueOnce(15) // resumeAmount
			.mockReturnValueOnce('minutes'); // resumeUnit

		const result = configureWaitTillDate(mockExecuteFunctions, 'root');
		const expectedDate = new Date(new Date().getTime() + 15 * 60 * 1000);
		expect(result.getTime()).toBeCloseTo(expectedDate.getTime(), -2);
	});

	it('should calculate hours correctly in root location', () => {
		mockExecuteFunctions.getNodeParameter
			.mockReturnValueOnce(true)
			.mockReturnValueOnce('afterTimeInterval')
			.mockReturnValueOnce(3)
			.mockReturnValueOnce('hours');

		const result = configureWaitTillDate(mockExecuteFunctions, 'root');
		const expectedDate = new Date(new Date().getTime() + 3 * 60 * 60 * 1000);
		expect(result.getTime()).toBeCloseTo(expectedDate.getTime(), -2);
	});

	it('should calculate days correctly in root location', () => {
		mockExecuteFunctions.getNodeParameter
			.mockReturnValueOnce(true)
			.mockReturnValueOnce('afterTimeInterval')
			.mockReturnValueOnce(5)
			.mockReturnValueOnce('days');

		const result = configureWaitTillDate(mockExecuteFunctions, 'root');
		const expectedDate = new Date(new Date().getTime() + 5 * 24 * 60 * 60 * 1000);
		expect(result.getTime()).toBeCloseTo(expectedDate.getTime(), -2);
	});

	it('should handle maxDateAndTime in root location', () => {
		const maxDateAndTime = '2024-12-31T23:59:59Z';
		mockExecuteFunctions.getNodeParameter
			.mockReturnValueOnce(true)
			.mockReturnValueOnce('maxDateAndTime')
			.mockReturnValueOnce(maxDateAndTime);

		const result = configureWaitTillDate(mockExecuteFunctions, 'root');
		expect(result).toEqual(new Date(maxDateAndTime));
	});

	it('should throw error for invalid date in root location', () => {
		mockExecuteFunctions.getNodeParameter
			.mockReturnValueOnce(true)
			.mockReturnValueOnce('maxDateAndTime')
			.mockReturnValueOnce('not-a-valid-date');

		expect(() => configureWaitTillDate(mockExecuteFunctions, 'root')).toThrow(NodeOperationError);
	});

	it('should throw error for invalid resumeAmount in root location', () => {
		mockExecuteFunctions.getNodeParameter
			.mockReturnValueOnce(true)
			.mockReturnValueOnce('afterTimeInterval')
			.mockReturnValueOnce('not-a-number')
			.mockReturnValueOnce('minutes');

		expect(() => configureWaitTillDate(mockExecuteFunctions, 'root')).toThrow(NodeOperationError);
	});
});

describe('getSendAndWaitProperties additionalProperties', () => {
	const hitl: INodeProperties = {
		displayName: 'Capture Who Responded',
		name: 'captureResponder',
		type: 'boolean',
		default: false,
	};

	it('renders additionalProperties before the Options collection', () => {
		const props = getSendAndWaitProperties([], undefined, [hitl]);
		// The first "options" collection is the approval one, emitted right after "Approval Options".
		const firstOptionsIndex = props.findIndex((p) => p.name === 'options');
		const hitlIndex = props.findIndex((p) => p.name === 'captureResponder');
		expect(hitlIndex).toBeGreaterThan(-1);
		expect(hitlIndex).toBeLessThan(firstOptionsIndex);
	});
});
