import { createHmac } from 'crypto';

import * as utils from '../GenericFunctions';

jest.mock('n8n-workflow', () => {
	const original = jest.requireActual('n8n-workflow');
	return {
		...original,
		NodeApiError: jest.fn().mockImplementation(function (
			this: { node: unknown; error: unknown },
			node: unknown,
			error: unknown,
		): never {
			const err = new Error('Mock NodeApiError');
			(err as any).node = node;
			(err as any).error = error;
			return Promise.reject(err) as never;
		}),
	};
});

describe('Facebook GenericFunctions', () => {
	let mockExecuteFunctions: any;

	beforeEach(() => {
		jest.clearAllMocks();

		mockExecuteFunctions = {
			getNode: jest.fn().mockReturnValue({
				name: 'Facebook',
				typeVersion: 1,
			}),
			getNodeParameter: jest.fn().mockReturnValue('accessToken'),
			getCredentials: jest.fn().mockImplementation(async (type) => {
				if (type === 'facebookGraphApi') {
					return { accessToken: 'test-access-token' };
				} else if (type === 'facebookGraphAppApi') {
					return { accessToken: 'test-app-access-token' };
				} else if (type === 'facebookGraphApiOAuth2Api') {
					return {};
				} else if (type === 'facebookGraphAppOAuth2Api') {
					return {};
				}
			}),
			helpers: {
				request: jest.fn(),
				requestWithAuthentication: jest.fn(),
			},
		};
	});

	describe('facebookApiRequest', () => {
		it('should use correct credentials for regular nodes', async () => {
			mockExecuteFunctions.helpers.request.mockResolvedValue({ success: true });

			await utils.facebookApiRequest.call(
				mockExecuteFunctions,
				'GET',
				'/me',
				{},
				{ fields: 'id,name' },
			);

			expect(mockExecuteFunctions.getCredentials).toHaveBeenCalledWith('facebookGraphApi');
			expect(mockExecuteFunctions.helpers.request).toHaveBeenCalledWith({
				headers: { accept: 'application/json,text/*;q=0.99' },
				method: 'GET',
				qs: { access_token: 'test-access-token', fields: 'id,name' },
				body: {},
				gzip: true,
				uri: 'https://graph.facebook.com/v23.0/me',
				json: true,
			});
		});

		it('should use app credentials for trigger nodes', async () => {
			mockExecuteFunctions.getNode.mockReturnValue({ name: 'Facebook Trigger' });
			mockExecuteFunctions.helpers.request.mockResolvedValue({ success: true });

			await utils.facebookApiRequest.call(mockExecuteFunctions, 'GET', '/app', {}, {});

			expect(mockExecuteFunctions.getCredentials).toHaveBeenCalledWith('facebookGraphAppApi');
			expect(mockExecuteFunctions.helpers.request).toHaveBeenCalledWith(
				expect.objectContaining({
					qs: { access_token: 'test-app-access-token' },
				}),
			);
		});

		it('should include appsecret_proof and appsecret_time when appSecret is set', async () => {
			const fixedTime = 1700000000;
			jest.spyOn(Date, 'now').mockReturnValue(fixedTime * 1000);

			mockExecuteFunctions.getNode.mockReturnValue({ name: 'Facebook Trigger' });
			mockExecuteFunctions.getCredentials.mockResolvedValue({
				accessToken: 'test-app-access-token',
				appSecret: 'test-app-secret',
			});
			mockExecuteFunctions.helpers.request.mockResolvedValue({ success: true });

			await utils.facebookApiRequest.call(mockExecuteFunctions, 'GET', '/app', {}, {});

			const expectedProof = createHmac('sha256', 'test-app-secret')
				.update(`test-app-access-token|${fixedTime}`)
				.digest('hex');

			expect(mockExecuteFunctions.helpers.request).toHaveBeenCalledWith(
				expect.objectContaining({
					qs: {
						access_token: 'test-app-access-token',
						appsecret_proof: expectedProof,
						appsecret_time: fixedTime,
					},
				}),
			);

			jest.restoreAllMocks();
		});

		it('should not include appsecret_proof when appSecret is empty', async () => {
			mockExecuteFunctions.getNode.mockReturnValue({ name: 'Facebook Trigger' });
			mockExecuteFunctions.getCredentials.mockResolvedValue({
				accessToken: 'test-app-access-token',
				appSecret: '',
			});
			mockExecuteFunctions.helpers.request.mockResolvedValue({ success: true });

			await utils.facebookApiRequest.call(mockExecuteFunctions, 'GET', '/app', {}, {});

			const requestCall = mockExecuteFunctions.helpers.request.mock.calls[0][0];
			expect(requestCall.qs).not.toHaveProperty('appsecret_proof');
			expect(requestCall.qs).not.toHaveProperty('appsecret_time');
		});

		it('should not include appsecret_proof when appSecret is undefined', async () => {
			mockExecuteFunctions.getCredentials.mockResolvedValue({
				accessToken: 'test-access-token',
			});
			mockExecuteFunctions.helpers.request.mockResolvedValue({ success: true });

			await utils.facebookApiRequest.call(mockExecuteFunctions, 'GET', '/me', {}, {});

			const requestCall = mockExecuteFunctions.helpers.request.mock.calls[0][0];
			expect(requestCall.qs).not.toHaveProperty('appsecret_proof');
			expect(requestCall.qs).not.toHaveProperty('appsecret_time');
		});

		it('should allow custom URI', async () => {
			mockExecuteFunctions.helpers.request.mockResolvedValue({ success: true });
			const customUri = 'https://graph.facebook.com/v23.0/me/feed';

			await utils.facebookApiRequest.call(
				mockExecuteFunctions,
				'POST',
				'',
				{ message: 'Hello' },
				{},
				customUri,
			);

			expect(mockExecuteFunctions.helpers.request).toHaveBeenCalledWith(
				expect.objectContaining({ uri: customUri }),
			);
		});

		describe('OAuth2 authentication', () => {
			beforeEach(() => {
				mockExecuteFunctions.getNodeParameter.mockReturnValue('oAuth2');
				mockExecuteFunctions.helpers.requestWithAuthentication.mockResolvedValue({
					success: true,
				});
			});

			it('should use OAuth2 credential for regular nodes', async () => {
				await utils.facebookApiRequest.call(mockExecuteFunctions, 'GET', '/me', {}, {});

				expect(mockExecuteFunctions.getCredentials).toHaveBeenCalledWith(
					'facebookGraphApiOAuth2Api',
				);
				expect(mockExecuteFunctions.helpers.requestWithAuthentication).toHaveBeenCalledWith(
					'facebookGraphApiOAuth2Api',
					expect.objectContaining({
						method: 'GET',
						uri: 'https://graph.facebook.com/v23.0/me',
					}),
				);
				expect(mockExecuteFunctions.helpers.request).not.toHaveBeenCalled();
			});

			it('should use OAuth2 app credential for trigger nodes', async () => {
				mockExecuteFunctions.getNode.mockReturnValue({ name: 'Facebook Trigger' });

				await utils.facebookApiRequest.call(mockExecuteFunctions, 'GET', '/app', {}, {});

				expect(mockExecuteFunctions.getCredentials).toHaveBeenCalledWith(
					'facebookGraphAppOAuth2Api',
				);
				expect(mockExecuteFunctions.helpers.requestWithAuthentication).toHaveBeenCalledWith(
					'facebookGraphAppOAuth2Api',
					expect.objectContaining({ method: 'GET' }),
				);
			});

			it('should not include access_token in qs when using OAuth2', async () => {
				await utils.facebookApiRequest.call(mockExecuteFunctions, 'GET', '/me', {}, {});

				expect(mockExecuteFunctions.helpers.requestWithAuthentication).toHaveBeenCalledWith(
					'facebookGraphApiOAuth2Api',
					expect.objectContaining({
						qs: expect.not.objectContaining({ access_token: expect.anything() }),
					}),
				);
			});

			it('should not compute appsecret_proof when using OAuth2', async () => {
				mockExecuteFunctions.getNode.mockReturnValue({ name: 'Facebook Trigger' });
				mockExecuteFunctions.getCredentials.mockResolvedValue({ appSecret: 'some-secret' });

				await utils.facebookApiRequest.call(mockExecuteFunctions, 'GET', '/app', {}, {});

				expect(mockExecuteFunctions.helpers.requestWithAuthentication).toHaveBeenCalledWith(
					'facebookGraphAppOAuth2Api',
					expect.objectContaining({
						qs: expect.not.objectContaining({ appsecret_proof: expect.anything() }),
					}),
				);
			});
		});
	});

	describe('getFields', () => {
		it('should return all fields for a given object with * option first', () => {
			const fields = utils.getFields('page');

			expect(fields.length).toBeGreaterThan(0);
			expect(fields[0]).toEqual({ name: '*', value: '*' });
			expect(fields.some((field) => field.name === 'Feed')).toBeTruthy();
		});

		it('should return only * for unknown objects', () => {
			const fields = utils.getFields('unknown-object');

			expect(fields).toEqual([{ name: '*', value: '*' }]);
		});

		it('should format field names in capital case', () => {
			const fields = utils.getFields('page');
			const findField = (name: string) => fields.find((field) => field.name === name);

			expect(findField('Feed')).toBeDefined();
			expect(findField('Email')).toBeDefined();
			expect(findField('Company Overview')).toBeDefined();
		});
	});

	describe('getAllFields', () => {
		it('should return all field values without *', () => {
			const allFields = utils.getAllFields('page');

			expect(allFields.length).toBeGreaterThan(0);
			expect(allFields.includes('*')).toBeFalsy();
			expect(allFields.includes('feed')).toBeTruthy();
			expect(allFields.includes('email')).toBeTruthy();
		});

		it('should return empty array for unknown objects', () => {
			expect(utils.getAllFields('unknown-object')).toEqual([]);
		});

		it('should return all raw field values', () => {
			const pageFields = utils.getAllFields('page');
			const instagramFields = utils.getAllFields('instagram');

			expect(pageFields).toContain('feed');
			expect(pageFields).toContain('email');
			expect(pageFields).toContain('website');

			expect(instagramFields).toContain('comments');
			expect(instagramFields).toContain('mentions');
		});
	});
});
