import { mockDeep } from 'jest-mock-extended';
import type {
	ICredentialsDecrypted,
	ICredentialTestFunctions,
	IHookFunctions,
	ILoadOptionsFunctions,
	INode,
	IWebhookFunctions,
} from 'n8n-workflow';

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

describe('BitbucketTrigger', () => {
	let bitbucketTrigger: BitbucketTrigger;
	let bitbucketApiRequestSpy: jest.SpyInstance;
	let bitbucketApiRequestAllItemsSpy: jest.SpyInstance;

	const mockNode: INode = {
		id: 'test-node-id',
		name: 'Bitbucket Trigger Test',
		type: 'n8n-nodes-base.bitbucketTrigger',
		typeVersion: 1.1,
		position: [0, 0],
		parameters: {},
	};

	beforeEach(() => {
		jest.resetAllMocks();
		bitbucketApiRequestSpy = jest.spyOn(GenericFunctions, 'bitbucketApiRequest');
		bitbucketApiRequestAllItemsSpy = jest.spyOn(GenericFunctions, 'bitbucketApiRequestAllItems');
		bitbucketTrigger = new BitbucketTrigger();
	});

	describe('credential test', () => {
		const mockCredentialTestFunctions = mockDeep<ICredentialTestFunctions>();

		beforeEach(() => {
			mockCredentialTestFunctions.helpers.request.mockClear();
		});

		it('should return success for valid credentials', async () => {
			const mockCredentials: ICredentialsDecrypted = {
				id: 'test-cred-id',
				name: 'Test Bitbucket Credentials',
				type: 'bitbucketApi',
				data: {
					username: 'testuser',
					appPassword: 'testpassword',
				},
			};

			const mockResponse = {
				username: 'testuser',
				display_name: 'Test User',
			};

			mockCredentialTestFunctions.helpers.request.mockResolvedValue(mockResponse);

			const result = await bitbucketTrigger.methods.credentialTest.bitbucketApiTest.call(
				mockCredentialTestFunctions,
				mockCredentials,
			);

			expect(mockCredentialTestFunctions.helpers.request).toHaveBeenCalledWith({
				method: 'GET',
				auth: {
					user: 'testuser',
					password: 'testpassword',
				},
				uri: 'https://api.bitbucket.org/2.0/user',
				json: true,
				timeout: 5000,
			});

			expect(result).toEqual({
				status: 'OK',
				message: 'Authentication successful!',
			});
		});

		it('should return error for invalid credentials', async () => {
			const mockCredentials: ICredentialsDecrypted = {
				id: 'test-cred-id',
				name: 'Test Bitbucket Credentials',
				type: 'bitbucketApi',
				data: {
					username: 'testuser',
					appPassword: 'wrongpassword',
				},
			};

			const mockResponse = {
				error: 'Invalid credentials',
			};

			mockCredentialTestFunctions.helpers.request.mockResolvedValue(mockResponse);

			const result = await bitbucketTrigger.methods.credentialTest.bitbucketApiTest.call(
				mockCredentialTestFunctions,
				mockCredentials,
			);

			expect(result).toEqual({
				status: 'Error',
				message: 'Token is not valid: Invalid credentials',
			});
		});

		it('should return error when request fails', async () => {
			const mockCredentials: ICredentialsDecrypted = {
				id: 'test-cred-id',
				name: 'Test Bitbucket Credentials',
				type: 'bitbucketApi',
				data: {
					username: 'testuser',
					appPassword: 'testpassword',
				},
			};

			const mockError = new Error('Network error');
			mockCredentialTestFunctions.helpers.request.mockRejectedValue(mockError);

			const result = await bitbucketTrigger.methods.credentialTest.bitbucketApiTest.call(
				mockCredentialTestFunctions,
				mockCredentials,
			);

			expect(result).toEqual({
				status: 'Error',
				message: 'Settings are not valid: Error: Network error',
			});
		});
	});

	describe('load options methods', () => {
		const mockLoadOptionsFunctions = mockDeep<ILoadOptionsFunctions>();

		beforeEach(() => {
			mockLoadOptionsFunctions.getNode.mockReturnValue(mockNode);
		});

		describe('getWorkspaceEvents', () => {
			it('should return workspace events', async () => {
				const mockEvents = [
					{
						event: 'repo:push',
						description: 'Repository push',
					},
					{
						event: 'repo:fork',
						description: 'Repository fork',
					},
				];

				bitbucketApiRequestAllItemsSpy.mockResolvedValue(mockEvents);

				const result =
					await bitbucketTrigger.methods.loadOptions.getWorkspaceEvents.call(
						mockLoadOptionsFunctions,
					);

				expect(bitbucketApiRequestAllItemsSpy).toHaveBeenCalledWith(
					'values',
					'GET',
					'/hook_events/workspace',
				);

				expect(result).toEqual([
					{
						name: 'repo:push',
						value: 'repo:push',
						description: 'Repository push',
					},
					{
						name: 'repo:fork',
						value: 'repo:fork',
						description: 'Repository fork',
					},
				]);
			});

			it('should handle empty events list', async () => {
				bitbucketApiRequestAllItemsSpy.mockResolvedValue([]);

				const result =
					await bitbucketTrigger.methods.loadOptions.getWorkspaceEvents.call(
						mockLoadOptionsFunctions,
					);

				expect(result).toEqual([]);
			});
		});

		describe('getRepositoriesEvents', () => {
			it('should return repository events', async () => {
				const mockEvents = [
					{
						event: 'repo:push',
						description: 'Repository push',
					},
					{
						event: 'pullrequest:created',
						description: 'Pull request created',
					},
				];

				bitbucketApiRequestAllItemsSpy.mockResolvedValue(mockEvents);

				const result =
					await bitbucketTrigger.methods.loadOptions.getRepositoriesEvents.call(
						mockLoadOptionsFunctions,
					);

				expect(bitbucketApiRequestAllItemsSpy).toHaveBeenCalledWith(
					'values',
					'GET',
					'/hook_events/repository',
				);

				expect(result).toEqual([
					{
						name: 'repo:push',
						value: 'repo:push',
						description: 'Repository push',
					},
					{
						name: 'pullrequest:created',
						value: 'pullrequest:created',
						description: 'Pull request created',
					},
				]);
			});
		});

		describe('getRepositories', () => {
			it('should return repositories for workspace', async () => {
				const mockRepositories = [
					{
						slug: 'repo1',
						description: 'First repository',
					},
					{
						slug: 'repo2',
						description: 'Second repository',
					},
				];

				mockLoadOptionsFunctions.getCurrentNodeParameter.mockReturnValue('test-workspace');
				bitbucketApiRequestAllItemsSpy.mockResolvedValue(mockRepositories);

				const result =
					await bitbucketTrigger.methods.loadOptions.getRepositories.call(mockLoadOptionsFunctions);

				expect(mockLoadOptionsFunctions.getCurrentNodeParameter).toHaveBeenCalledWith('workspace');
				expect(bitbucketApiRequestAllItemsSpy).toHaveBeenCalledWith(
					'values',
					'GET',
					'/repositories/test-workspace',
				);

				expect(result).toEqual([
					{
						// eslint-disable-next-line n8n-nodes-base/node-param-display-name-miscased
						name: 'repo1',
						value: 'repo1',
						description: 'First repository',
					},
					{
						// eslint-disable-next-line n8n-nodes-base/node-param-display-name-miscased
						name: 'repo2',
						value: 'repo2',
						description: 'Second repository',
					},
				]);
			});
		});

		describe('getWorkspaces', () => {
			it('should return workspaces', async () => {
				const mockWorkspaces = [
					{
						workspace: {
							name: 'Workspace 1',
							slug: 'workspace1',
						},
					},
					{
						workspace: {
							slug: 'workspace2',
						},
					},
				];

				bitbucketApiRequestAllItemsSpy.mockResolvedValue(mockWorkspaces);

				const result =
					await bitbucketTrigger.methods.loadOptions.getWorkspaces.call(mockLoadOptionsFunctions);

				expect(bitbucketApiRequestAllItemsSpy).toHaveBeenCalledWith(
					'values',
					'GET',
					'/user/workspaces',
				);

				expect(result).toEqual([
					{
						name: 'Workspace 1',
						value: 'workspace1',
					},
					{
						// eslint-disable-next-line n8n-nodes-base/node-param-display-name-miscased
						name: 'workspace2',
						value: 'workspace2',
					},
				]);
			});
		});
	});

	describe('webhook methods', () => {
		const mockHookFunctions = mockDeep<IHookFunctions>();

		beforeEach(() => {
			mockHookFunctions.getNode.mockReturnValue(mockNode);
			mockHookFunctions.getNodeWebhookUrl.mockReturnValue('https://test.n8n.io/webhook/test');
			mockHookFunctions.getWorkflowStaticData.mockReturnValue({});
		});

		describe('checkExists', () => {
			it('should return true if webhook exists for workspace', async () => {
				const mockHooks = {
					values: [
						{
							uuid: '{12345678-1234-1234-1234-123456789012}',
							url: 'https://test.n8n.io/webhook/test',
							active: true,
						},
						{
							uuid: '{87654321-4321-4321-4321-210987654321}',
							url: 'https://other.webhook.url',
							active: true,
						},
					],
				};

				mockHookFunctions.getNodeParameter.mockImplementation((paramName: string) => {
					if (paramName === 'resource') return 'workspace';
					if (paramName === 'workspace') return 'test-workspace';
					return undefined;
				});

				bitbucketApiRequestSpy.mockResolvedValue(mockHooks);

				const result =
					await bitbucketTrigger.webhookMethods.default.checkExists.call(mockHookFunctions);

				expect(bitbucketApiRequestSpy).toHaveBeenCalledWith(
					'GET',
					'/workspaces/test-workspace/hooks',
				);
				expect(result).toBe(true);

				// Check that webhook ID is stored
				const staticData = mockHookFunctions.getWorkflowStaticData('node');
				expect(staticData.webhookId).toBe('12345678-1234-1234-1234-123456789012');
			});

			it('should return true if webhook exists for repository', async () => {
				const mockHooks = {
					values: [
						{
							uuid: '{12345678-1234-1234-1234-123456789012}',
							url: 'https://test.n8n.io/webhook/test',
							active: true,
						},
					],
				};

				mockHookFunctions.getNodeParameter.mockImplementation((paramName: string) => {
					if (paramName === 'resource') return 'repository';
					if (paramName === 'workspace') return 'test-workspace';
					if (paramName === 'repository') return 'test-repo';
					return undefined;
				});

				bitbucketApiRequestSpy.mockResolvedValue(mockHooks);

				const result =
					await bitbucketTrigger.webhookMethods.default.checkExists.call(mockHookFunctions);

				expect(bitbucketApiRequestSpy).toHaveBeenCalledWith(
					'GET',
					'/repositories/test-workspace/test-repo/hooks',
				);
				expect(result).toBe(true);
			});

			it('should return false if webhook does not exist', async () => {
				const mockHooks = {
					values: [
						{
							uuid: '{87654321-4321-4321-4321-210987654321}',
							url: 'https://other.webhook.url',
							active: true,
						},
					],
				};

				mockHookFunctions.getNodeParameter.mockImplementation((paramName: string) => {
					if (paramName === 'resource') return 'workspace';
					if (paramName === 'workspace') return 'test-workspace';
					return undefined;
				});

				bitbucketApiRequestSpy.mockResolvedValue(mockHooks);

				const result =
					await bitbucketTrigger.webhookMethods.default.checkExists.call(mockHookFunctions);

				expect(result).toBe(false);
			});

			it('should return false if webhook exists but is inactive', async () => {
				const mockHooks = {
					values: [
						{
							uuid: '{12345678-1234-1234-1234-123456789012}',
							url: 'https://test.n8n.io/webhook/test',
							active: false,
						},
					],
				};

				mockHookFunctions.getNodeParameter.mockImplementation((paramName: string) => {
					if (paramName === 'resource') return 'workspace';
					if (paramName === 'workspace') return 'test-workspace';
					return undefined;
				});

				bitbucketApiRequestSpy.mockResolvedValue(mockHooks);

				const result =
					await bitbucketTrigger.webhookMethods.default.checkExists.call(mockHookFunctions);

				expect(result).toBe(false);
			});
		});

		describe('create', () => {
			it('should create webhook for workspace', async () => {
				const mockResponse = {
					uuid: '{12345678-1234-1234-1234-123456789012}',
					url: 'https://test.n8n.io/webhook/test',
					active: true,
				};

				mockHookFunctions.getNodeParameter.mockImplementation((paramName: string) => {
					if (paramName === 'resource') return 'workspace';
					if (paramName === 'workspace') return 'test-workspace';
					if (paramName === 'events') return ['repo:push', 'repo:fork'];
					return undefined;
				});

				bitbucketApiRequestSpy.mockResolvedValue(mockResponse);

				const result = await bitbucketTrigger.webhookMethods.default.create.call(mockHookFunctions);

				expect(bitbucketApiRequestSpy).toHaveBeenCalledWith(
					'POST',
					'/workspaces/test-workspace/hooks',
					{
						description: 'n8n webhook',
						url: 'https://test.n8n.io/webhook/test',
						active: true,
						events: ['repo:push', 'repo:fork'],
					},
				);

				expect(result).toBe(true);

				// Check that webhook ID is stored
				const staticData = mockHookFunctions.getWorkflowStaticData('node');
				expect(staticData.webhookId).toBe('12345678-1234-1234-1234-123456789012');
			});

			it('should create webhook for repository', async () => {
				const mockResponse = {
					uuid: '{12345678-1234-1234-1234-123456789012}',
					url: 'https://test.n8n.io/webhook/test',
					active: true,
				};

				mockHookFunctions.getNodeParameter.mockImplementation((paramName: string) => {
					if (paramName === 'resource') return 'repository';
					if (paramName === 'workspace') return 'test-workspace';
					if (paramName === 'repository') return 'test-repo';
					if (paramName === 'events') return ['pullrequest:created'];
					return undefined;
				});

				bitbucketApiRequestSpy.mockResolvedValue(mockResponse);

				const result = await bitbucketTrigger.webhookMethods.default.create.call(mockHookFunctions);

				expect(bitbucketApiRequestSpy).toHaveBeenCalledWith(
					'POST',
					'/repositories/test-workspace/test-repo/hooks',
					{
						description: 'n8n webhook',
						url: 'https://test.n8n.io/webhook/test',
						active: true,
						events: ['pullrequest:created'],
					},
				);

				expect(result).toBe(true);
			});
		});

		describe('delete', () => {
			it('should delete webhook for workspace', async () => {
				const staticData = { webhookId: '12345678-1234-1234-1234-123456789012' };
				mockHookFunctions.getWorkflowStaticData.mockReturnValue(staticData);

				mockHookFunctions.getNodeParameter.mockImplementation((paramName: string) => {
					if (paramName === 'resource') return 'workspace';
					if (paramName === 'workspace') return 'test-workspace';
					return undefined;
				});

				bitbucketApiRequestSpy.mockResolvedValue({});

				const result = await bitbucketTrigger.webhookMethods.default.delete.call(mockHookFunctions);

				expect(bitbucketApiRequestSpy).toHaveBeenCalledWith(
					'DELETE',
					'/workspaces/test-workspace/hooks/12345678-1234-1234-1234-123456789012',
				);

				expect(result).toBe(true);
				expect(staticData.webhookId).toBeUndefined();
			});

			it('should delete webhook for repository', async () => {
				const staticData = { webhookId: '12345678-1234-1234-1234-123456789012' };
				mockHookFunctions.getWorkflowStaticData.mockReturnValue(staticData);

				mockHookFunctions.getNodeParameter.mockImplementation((paramName: string) => {
					if (paramName === 'resource') return 'repository';
					if (paramName === 'workspace') return 'test-workspace';
					if (paramName === 'repository') return 'test-repo';
					return undefined;
				});

				bitbucketApiRequestSpy.mockResolvedValue({});

				const result = await bitbucketTrigger.webhookMethods.default.delete.call(mockHookFunctions);

				expect(bitbucketApiRequestSpy).toHaveBeenCalledWith(
					'DELETE',
					'/repositories/test-workspace/test-repo/hooks/12345678-1234-1234-1234-123456789012',
				);

				expect(result).toBe(true);
				expect(staticData.webhookId).toBeUndefined();
			});

			it('should return false if delete fails', async () => {
				const staticData = { webhookId: '12345678-1234-1234-1234-123456789012' };
				mockHookFunctions.getWorkflowStaticData.mockReturnValue(staticData);

				mockHookFunctions.getNodeParameter.mockImplementation((paramName: string) => {
					if (paramName === 'resource') return 'workspace';
					if (paramName === 'workspace') return 'test-workspace';
					return undefined;
				});

				bitbucketApiRequestSpy.mockRejectedValue(new Error('Delete failed'));

				const result = await bitbucketTrigger.webhookMethods.default.delete.call(mockHookFunctions);

				expect(result).toBe(false);
				// Webhook ID should still be present since delete failed
				expect(staticData.webhookId).toBe('12345678-1234-1234-1234-123456789012');
			});
		});
	});

	describe('webhook', () => {
		const mockWebhookFunctions = mockDeep<IWebhookFunctions>();

		beforeEach(() => {
			mockWebhookFunctions.getNode.mockReturnValue(mockNode);
		});

		it('should process webhook with matching UUID', async () => {
			const webhookId = '12345678-1234-1234-1234-123456789012';
			const staticData = { webhookId };
			const mockRequestBody = {
				repository: {
					name: 'test-repo',
				},
				push: {
					changes: [
						{
							new: {
								name: 'main',
							},
						},
					],
				},
			};

			mockWebhookFunctions.getWorkflowStaticData.mockReturnValue(staticData);
			mockWebhookFunctions.getRequestObject.mockReturnValue({
				body: mockRequestBody,
			} as any);
			mockWebhookFunctions.getHeaderData.mockReturnValue({
				'x-hook-uuid': webhookId,
			});
			mockWebhookFunctions.helpers.returnJsonArray.mockReturnValue([{ json: mockRequestBody }]);

			const result = await bitbucketTrigger.webhook.call(mockWebhookFunctions);

			expect(result).toEqual({
				workflowData: [[{ json: mockRequestBody }]],
			});
		});

		it('should return empty object for non-matching UUID', async () => {
			const webhookId = '12345678-1234-1234-1234-123456789012';
			const staticData = { webhookId };

			mockWebhookFunctions.getWorkflowStaticData.mockReturnValue(staticData);
			mockWebhookFunctions.getHeaderData.mockReturnValue({
				'x-hook-uuid': 'different-uuid',
			});

			const result = await bitbucketTrigger.webhook.call(mockWebhookFunctions);

			expect(result).toEqual({});
		});

		it('should handle array request body', async () => {
			const webhookId = '12345678-1234-1234-1234-123456789012';
			const staticData = { webhookId };
			const mockRequestBody = [
				{
					repository: { name: 'repo1' },
				},
				{
					repository: { name: 'repo2' },
				},
			];

			mockWebhookFunctions.getWorkflowStaticData.mockReturnValue(staticData);
			mockWebhookFunctions.getRequestObject.mockReturnValue({
				body: mockRequestBody,
			} as any);
			mockWebhookFunctions.getHeaderData.mockReturnValue({
				'x-hook-uuid': webhookId,
			});
			mockWebhookFunctions.helpers.returnJsonArray.mockReturnValue([
				{ json: mockRequestBody[0] },
				{ json: mockRequestBody[1] },
			]);

			const result = await bitbucketTrigger.webhook.call(mockWebhookFunctions);

			expect(mockWebhookFunctions.helpers.returnJsonArray).toHaveBeenCalledWith(mockRequestBody);
			expect(result).toEqual({
				workflowData: [[{ json: mockRequestBody[0] }, { json: mockRequestBody[1] }]],
			});
		});
	});
});
