import type { INodeType, IWebhookFunctions } from 'n8n-workflow';
import { NodeOperationError } from 'n8n-workflow';
import type { Mock } from 'vitest';
import { mock } from 'vitest-mock-extended';

import {
	createMicrosoftAgentApplication,
	configureAdapterProcessCallback,
	type MicrosoftAgent365Credentials,
	type ActivityCapture,
} from './microsoft-utils';
import { MicrosoftAgent365Trigger } from './MicrosoftAgent365Trigger.node';
import { authorizeJWT } from '@microsoft/agents-hosting';

// Mock the dependencies
vi.mock('./microsoft-utils', () => ({
	createMicrosoftAgentApplication: vi.fn(),
	configureAdapterProcessCallback: vi.fn(),
	microsoftMcpServers: [
		{ name: 'Calendar', value: 'mcp_CalendarTools' },
		{ name: 'Mail', value: 'mcp_MailTools' },
	],
}));

vi.mock('@microsoft/agents-hosting', () => ({
	authorizeJWT: vi.fn(),
}));

vi.mock('../../agents/Agent/V2/utils', () => ({
	getInputs: vi.fn(),
}));

describe('MicrosoftAgent365Trigger', () => {
	let microsoftAgent365Trigger: INodeType;
	let mockWebhookFunctions: IWebhookFunctions;
	let mockRequest: any;
	let mockResponse: any;
	let mockAdapter: any;

	beforeEach(() => {
		microsoftAgent365Trigger = new MicrosoftAgent365Trigger();

		// Create mock request
		mockRequest = {
			method: 'POST',
			body: {},
			headers: {},
		};

		// Create mock response
		mockResponse = {
			end: vi.fn(),
			status: vi.fn().mockReturnThis(),
			send: vi.fn().mockReturnThis(),
			headersSent: false,
		};

		// Create mock adapter
		mockAdapter = {
			process: vi.fn().mockResolvedValue(undefined),
		};

		// Create mock webhook functions using vitest-mock-extended
		mockWebhookFunctions = mock<IWebhookFunctions>();
		mockWebhookFunctions.getRequestObject = vi.fn().mockReturnValue(mockRequest);
		mockWebhookFunctions.getResponseObject = vi.fn().mockReturnValue(mockResponse);
		mockWebhookFunctions.getCredentials = vi.fn() as any;
		mockWebhookFunctions.getNode = vi.fn().mockReturnValue({
			name: 'Microsoft Agent 365',
			type: 'microsoftAgent365Trigger',
			typeVersion: 1,
		});
		mockWebhookFunctions.helpers = {
			returnJsonArray: vi.fn((data) => {
				if (Array.isArray(data)) {
					return data.map((item) => ({ json: item }));
				}
				return [{ json: data }];
			}),
		} as any;

		// Reset mocks
		vi.clearAllMocks();

		// Default: authorizeJWT authorizes the request (invokes next)
		(authorizeJWT as Mock).mockReturnValue(async (_req: unknown, _res: unknown, next: () => void) =>
			next(),
		);
	});

	describe('Node Description', () => {
		test('should have correct basic properties', () => {
			expect(microsoftAgent365Trigger.description.displayName).toBe('Microsoft Agent 365 Trigger');
			expect(microsoftAgent365Trigger.description.name).toBe('microsoftAgent365Trigger');
			expect(microsoftAgent365Trigger.description.group).toEqual(['trigger']);
		});

		test('should have webhook configuration', () => {
			expect(microsoftAgent365Trigger.description.webhooks).toBeDefined();
			expect(microsoftAgent365Trigger.description.webhooks).toHaveLength(2);

			// Check POST webhook
			const postWebhook = microsoftAgent365Trigger.description.webhooks![0];
			expect(postWebhook.httpMethod).toBe('POST');
			expect(postWebhook.path).toBe('webhook');
			expect(postWebhook.responseMode).toBe('onReceived');

			// Check HEAD webhook
			const headWebhook = microsoftAgent365Trigger.description.webhooks![1];
			expect(headWebhook.httpMethod).toBe('HEAD');
			expect(headWebhook.path).toBe('webhook');
		});

		test('should require microsoftAgent365Api credentials', () => {
			expect(microsoftAgent365Trigger.description.credentials).toBeDefined();
			expect(microsoftAgent365Trigger.description.credentials).toHaveLength(1);
			expect(microsoftAgent365Trigger.description.credentials![0].name).toBe(
				'microsoftAgent365Api',
			);
			expect(microsoftAgent365Trigger.description.credentials![0].required).toBe(true);
		});

		test('should have system prompt property', () => {
			const properties = microsoftAgent365Trigger.description.properties;
			const systemPromptProp = properties.find((p) => p.name === 'systemPrompt');

			expect(systemPromptProp).toBeDefined();
			expect(systemPromptProp?.type).toBe('string');
			expect(systemPromptProp?.displayName).toBe('System Prompt');
		});
	});

	describe('webhook method', () => {
		describe('HEAD request handling', () => {
			test('should handle HEAD request and return immediately', async () => {
				mockRequest.method = 'HEAD';

				const result = await microsoftAgent365Trigger.webhook!.call(mockWebhookFunctions);

				expect(mockResponse.end).toHaveBeenCalled();
				expect(result).toEqual({
					noWebhookResponse: true,
				});
				expect(mockWebhookFunctions.getCredentials).not.toHaveBeenCalled();
			});
		});

		describe('POST request handling', () => {
			let mockCredentials: MicrosoftAgent365Credentials;
			let mockAgent: any;

			beforeEach(() => {
				mockCredentials = {
					clientId: 'test-client-id',
					tenantId: 'test-tenant-id',
					clientSecret: 'test-client-secret',
				};

				mockAgent = {
					adapter: mockAdapter,
				};

				(mockWebhookFunctions.getCredentials as Mock).mockResolvedValue(mockCredentials);
				(createMicrosoftAgentApplication as Mock).mockReturnValue({
					agent: mockAgent,
					authConfig: {},
				});
			});

			test('should process POST request successfully', async () => {
				const mockCallback = vi.fn();
				(configureAdapterProcessCallback as Mock).mockReturnValue(mockCallback);

				const result = await microsoftAgent365Trigger.webhook!.call(mockWebhookFunctions);

				// Verify credentials were retrieved
				expect(mockWebhookFunctions.getCredentials).toHaveBeenCalledWith('microsoftAgent365Api');

				// Verify agent application was created
				expect(createMicrosoftAgentApplication).toHaveBeenCalledWith(mockCredentials);

				// Verify callback was configured
				expect(configureAdapterProcessCallback).toHaveBeenCalledWith(
					mockWebhookFunctions,
					mockAgent,
					mockCredentials,
					expect.objectContaining({
						input: '',
						output: [],
						activity: {},
					}),
				);

				// Verify the inbound request was authenticated before processing
				expect(authorizeJWT).toHaveBeenCalledWith({});

				// Verify adapter process was called
				expect(mockAdapter.process).toHaveBeenCalledWith(mockRequest, mockResponse, mockCallback);

				// Verify result structure
				expect(result).toEqual({
					noWebhookResponse: true,
					workflowData: expect.any(Array),
				});
			});

			test('should capture activity data in workflowData', async () => {
				const mockCallback = vi.fn();
				(configureAdapterProcessCallback as Mock).mockReturnValue(mockCallback);

				const result = await microsoftAgent365Trigger.webhook!.call(mockWebhookFunctions);

				expect(result.workflowData).toBeDefined();
				expect(Array.isArray(result.workflowData)).toBe(true);
				expect(result.workflowData).toHaveLength(1);

				// Verify returnJsonArray was called with activity capture
				expect(mockWebhookFunctions.helpers.returnJsonArray).toHaveBeenCalledWith(
					expect.objectContaining({
						input: '',
						output: [],
						activity: {},
					}),
				);
			});

			test('should transform activity data into flat structure for v1.1 output', async () => {
				// Override getNode to simulate v1.1
				mockWebhookFunctions.getNode = vi.fn().mockReturnValue({
					name: 'Microsoft Agent 365',
					type: 'microsoftAgent365Trigger',
					typeVersion: 1.1,
				});

				// Populate activityCapture when configureAdapterProcessCallback is called
				(configureAdapterProcessCallback as Mock).mockImplementation(
					(_nodeCtx, _agent, _creds, activityCapture) => {
						activityCapture.input = 'Hello agent';
						activityCapture.output = ['Hi there!'];
						activityCapture.activity = {
							conversationId: 'conv-123',
							type: 'message',
							channelId: 'msteams',
						};
						return vi.fn();
					},
				);

				const result = await microsoftAgent365Trigger.webhook!.call(mockWebhookFunctions);

				expect(result.workflowData).toHaveLength(1);

				// conversationId is lifted into conversation.id; other activity fields are spread
				expect(mockWebhookFunctions.helpers.returnJsonArray).toHaveBeenCalledWith(
					expect.objectContaining({
						input: 'Hello agent',
						output: ['Hi there!'],
						type: 'message',
						channelId: 'msteams',
						conversation: { id: 'conv-123' },
					}),
				);

				// conversationId must NOT appear at the top level
				const calledWith = (mockWebhookFunctions.helpers.returnJsonArray as Mock).mock.calls[0][0];
				expect(calledWith).not.toHaveProperty('conversationId');
				expect(calledWith).not.toHaveProperty('activity');
			});

			test('should reject an unauthenticated request and not run the agent', async () => {
				const mockCallback = vi.fn();
				(configureAdapterProcessCallback as Mock).mockReturnValue(mockCallback);
				// authorizeJWT rejects: it writes a 401 itself (sets headersSent) and never calls next()
				(authorizeJWT as Mock).mockReturnValue(async (_req: unknown, res: any) => {
					res.status(401).send({ 'jwt-auth-error': 'authorization header not found' });
					res.headersSent = true;
				});

				const result = await microsoftAgent365Trigger.webhook!.call(mockWebhookFunctions);

				expect(mockAdapter.process).not.toHaveBeenCalled();
				expect(result).toEqual({ noWebhookResponse: true });
				// our backstop must not double-send when the middleware already responded
				expect(mockResponse.status).toHaveBeenCalledTimes(1);
			});

			test('should run the agent only after the request is authorized', async () => {
				const mockCallback = vi.fn();
				(configureAdapterProcessCallback as Mock).mockReturnValue(mockCallback);

				await microsoftAgent365Trigger.webhook!.call(mockWebhookFunctions);

				expect(authorizeJWT).toHaveBeenCalledWith({});
				expect(mockAdapter.process).toHaveBeenCalledWith(mockRequest, mockResponse, mockCallback);
			});

			test('sends a 401 backstop when the middleware rejects without responding', async () => {
				const mockCallback = vi.fn();
				(configureAdapterProcessCallback as Mock).mockReturnValue(mockCallback);
				// middleware neither authorizes (no next) nor writes a response
				(authorizeJWT as Mock).mockReturnValue(async () => {});

				const result = await microsoftAgent365Trigger.webhook!.call(mockWebhookFunctions);

				expect(mockResponse.status).toHaveBeenCalledWith(401);
				expect(mockAdapter.process).not.toHaveBeenCalled();
				expect(result).toEqual({ noWebhookResponse: true });
			});

			test('treats a next(error) from the middleware as unauthorized', async () => {
				const mockCallback = vi.fn();
				(configureAdapterProcessCallback as Mock).mockReturnValue(mockCallback);
				(authorizeJWT as Mock).mockReturnValue(
					async (_req: unknown, _res: unknown, next: (err?: unknown) => void) =>
						next(new Error('invalid token')),
				);

				const result = await microsoftAgent365Trigger.webhook!.call(mockWebhookFunctions);

				expect(mockAdapter.process).not.toHaveBeenCalled();
				expect(result).toEqual({ noWebhookResponse: true });
			});
		});

		describe('Error handling', () => {
			test('should throw NodeOperationError when credentials retrieval fails', async () => {
				const error = new Error('Credentials not found');
				(mockWebhookFunctions.getCredentials as Mock).mockRejectedValue(error);

				await expect(microsoftAgent365Trigger.webhook!.call(mockWebhookFunctions)).rejects.toThrow(
					NodeOperationError,
				);
			});

			test('should handle error with response data containing error object', async () => {
				const errorResponse = {
					response: {
						data: {
							error: 'invalid_client',
							error_description: 'Invalid client credentials',
						},
					},
				};

				(mockWebhookFunctions.getCredentials as Mock).mockRejectedValue(errorResponse);

				await expect(microsoftAgent365Trigger.webhook!.call(mockWebhookFunctions)).rejects.toThrow(
					'Error: invalid_client',
				);
			});

			test('should include error description in NodeOperationError', async () => {
				const errorResponse = {
					response: {
						data: {
							error: 'unauthorized',
							error_description: 'The provided credentials are invalid',
						},
					},
					message: 'Authentication failed',
				};

				(mockWebhookFunctions.getCredentials as Mock).mockRejectedValue(errorResponse);

				try {
					await microsoftAgent365Trigger.webhook!.call(mockWebhookFunctions);
					expect.fail('Should have thrown an error');
				} catch (error) {
					expect(error).toBeInstanceOf(NodeOperationError);
					expect((error as NodeOperationError).description).toBe(
						'The provided credentials are invalid',
					);
				}
			});

			test('should throw NodeOperationError with message when no error object in response', async () => {
				const error = new Error('Network error');
				(mockWebhookFunctions.getCredentials as Mock).mockRejectedValue(error);

				try {
					await microsoftAgent365Trigger.webhook!.call(mockWebhookFunctions);
					expect.fail('Should have thrown an error');
				} catch (err) {
					expect(err).toBeInstanceOf(NodeOperationError);
					expect((err as NodeOperationError).message).toContain('Network error');
				}
			});

			test('should throw NodeOperationError when agent creation fails', async () => {
				const mockCredentials = {
					clientId: 'test-client-id',
					tenantId: 'test-tenant-id',
					clientSecret: 'test-client-secret',
				};

				(mockWebhookFunctions.getCredentials as Mock).mockResolvedValue(mockCredentials);
				(createMicrosoftAgentApplication as Mock).mockImplementation(() => {
					throw new Error('Failed to create agent application');
				});

				await expect(microsoftAgent365Trigger.webhook!.call(mockWebhookFunctions)).rejects.toThrow(
					NodeOperationError,
				);
			});

			test('should throw NodeOperationError when adapter process fails', async () => {
				const mockCredentials = {
					clientId: 'test-client-id',
					tenantId: 'test-tenant-id',
					clientSecret: 'test-client-secret',
				};

				const mockAgent = {
					adapter: {
						process: vi.fn().mockRejectedValue(new Error('Adapter processing failed')),
					},
				};

				(mockWebhookFunctions.getCredentials as Mock).mockResolvedValue(mockCredentials);
				(createMicrosoftAgentApplication as Mock).mockReturnValue({
					agent: mockAgent,
					authConfig: {},
				});

				await expect(microsoftAgent365Trigger.webhook!.call(mockWebhookFunctions)).rejects.toThrow(
					NodeOperationError,
				);
			});
		});

		describe('Integration scenarios', () => {
			test('should handle complete webhook flow with activity capture', async () => {
				const mockCredentials = {
					clientId: 'test-client-id',
					tenantId: 'test-tenant-id',
					clientSecret: 'test-client-secret',
				};

				const mockAgent = {
					adapter: mockAdapter,
				};

				let capturedActivityCapture: ActivityCapture | undefined;
				const mockCallback = vi.fn();

				(mockWebhookFunctions.getCredentials as Mock).mockResolvedValue(mockCredentials);
				(createMicrosoftAgentApplication as Mock).mockReturnValue({
					agent: mockAgent,
					authConfig: {},
				});
				(configureAdapterProcessCallback as Mock).mockImplementation(
					(_ctx, _agent, _creds, activityCapture) => {
						capturedActivityCapture = activityCapture;
						return mockCallback;
					},
				);

				const result = await microsoftAgent365Trigger.webhook!.call(mockWebhookFunctions);

				// Verify activity capture was initialized
				expect(capturedActivityCapture).toBeDefined();
				expect(capturedActivityCapture?.input).toBe('');
				expect(capturedActivityCapture?.output).toEqual([]);
				expect(capturedActivityCapture?.activity).toEqual({});

				// Verify the full flow completed
				expect(createMicrosoftAgentApplication).toHaveBeenCalled();
				expect(configureAdapterProcessCallback).toHaveBeenCalled();
				expect(mockAdapter.process).toHaveBeenCalled();
				expect(result.noWebhookResponse).toBe(true);
				expect(result.workflowData).toBeDefined();
			});
		});
	});
});
