import type { IHttpRequestOptions } from 'n8n-workflow';
import { NodeOperationError } from 'n8n-workflow';

import {
	preprocessTags,
	deleteGroupMembers,
	validatePath,
	validateUserPath,
	removeUserFromGroups,
	findUsersForGroup,
	simplifyGetGroupsResponse,
	simplifyGetAllGroupsResponse,
	simplifyGetAllUsersResponse,
	validateName,
	validatePermissionsBoundary,
	encodeBodyAsFormUrlEncoded,
} from '../../helpers/utils';
import { awsApiRequest } from '../../transport';

jest.mock('../../transport', () => ({
	awsApiRequest: jest.fn(),
}));

describe('AWS IAM - Helper Functions', () => {
	let mockNode: any;

	beforeEach(() => {
		mockNode = {
			getNodeParameter: jest.fn(),
			getNode: jest.fn(),
			helpers: {
				returnJsonArray: jest.fn((input: unknown[]) => input.map((i) => ({ json: i }))),
			},
		};
	});

	describe('encodeBodyAsFormUrlEncoded', () => {
		it('should encode the body as application/x-www-form-urlencoded', async () => {
			const requestOptions: IHttpRequestOptions = {
				body: {
					client_id: 'myClient',
					client_secret: 'mySecret',
					grant_type: 'client_credentials',
				},
				url: '',
				headers: {},
			};

			const result = await encodeBodyAsFormUrlEncoded.call(mockNode, requestOptions);

			expect(result.body).toBe(
				'client_id=myClient&client_secret=mySecret&grant_type=client_credentials',
			);
		});

		it('should return unchanged options if no body is present', async () => {
			const requestOptions: IHttpRequestOptions = { url: '', headers: {} };
			const result = await encodeBodyAsFormUrlEncoded.call(mockNode, requestOptions);

			expect(result).toEqual({ url: '', headers: {} });
		});
	});

	describe('findUsersForGroup', () => {
		it('should return users for a valid groupName', async () => {
			mockNode.getNodeParameter.mockReturnValue('groupName');
			const mockResponse = {
				GetGroupResponse: { GetGroupResult: { Users: [{ UserName: 'user1' }] } },
			};
			(awsApiRequest as jest.Mock).mockResolvedValue(mockResponse);

			const result = await findUsersForGroup.call(mockNode, 'groupName');
			expect(result).toEqual([{ UserName: 'user1' }]);
		});
	});

	describe('preprocessTags', () => {
		it('should preprocess tags correctly into request body', async () => {
			mockNode.getNodeParameter.mockReturnValue({
				tags: [
					{ key: 'Department', value: 'Engineering' },
					{ key: 'Role', value: 'Developer' },
				],
			});

			const requestOptions = { body: '', headers: {}, url: '' };
			const result = await preprocessTags.call(mockNode, requestOptions);

			expect(result.body).toBe(
				'Tags.member.1.Key=Department&Tags.member.1.Value=Engineering&Tags.member.2.Key=Role&Tags.member.2.Value=Developer',
			);
		});

		it('should throw error if a tag is missing a key', async () => {
			mockNode.getNodeParameter.mockReturnValue({
				tags: [{ key: '', value: 'Engineering' }],
			});

			const requestOptions = { body: '', headers: {}, url: '' };

			await expect(preprocessTags.call(mockNode, requestOptions)).rejects.toThrow(
				NodeOperationError,
			);

			await expect(preprocessTags.call(mockNode, requestOptions)).rejects.toThrow(
				"Tag at position 1 is missing 'Key'. Both 'Key' and 'Value' are required.",
			);
		});

		it('should throw error if a tag is missing a value', async () => {
			mockNode.getNodeParameter.mockReturnValue({
				tags: [{ key: 'Department', value: '' }],
			});

			const requestOptions = { body: '', headers: {}, url: '' };

			await expect(preprocessTags.call(mockNode, requestOptions)).rejects.toThrow(
				NodeOperationError,
			);

			await expect(preprocessTags.call(mockNode, requestOptions)).rejects.toThrow(
				"Tag at position 1 is missing 'Value'. Both 'Key' and 'Value' are required.",
			);
		});
	});

	describe('deleteGroupMembers', () => {
		it('should attempt to remove users from a group', async () => {
			mockNode.getNodeParameter.mockImplementation((param: string) => {
				if (param === 'group') {
					return 'groupName';
				}
				return null;
			});

			const mockUsers = [{ UserName: 'user1' }];
			(awsApiRequest as jest.Mock).mockResolvedValue(mockUsers);

			const requestOptions = { headers: {}, url: '' };
			const result = await deleteGroupMembers.call(mockNode, requestOptions);

			expect(result).toEqual(requestOptions);

			expect(awsApiRequest).toHaveBeenCalledWith(
				expect.objectContaining({
					method: 'POST',
					body: expect.stringContaining('GroupName=groupName'),
				}),
			);
		});
	});

	describe('validatePath', () => {
		it('should throw an error for invalid path length', async () => {
			mockNode.getNodeParameter.mockReturnValue('');
			await expect(validatePath.call(mockNode, { headers: {}, url: '' })).rejects.toThrowError(
				NodeOperationError,
			);
		});

		it('should throw an error for invalid path format', async () => {
			mockNode.getNodeParameter.mockReturnValue('/invalidPath');
			await expect(validatePath.call(mockNode, { url: '' })).rejects.toThrowError(
				NodeOperationError,
			);
		});

		it('should pass for a valid path', async () => {
			mockNode.getNodeParameter.mockReturnValue('/valid/path/');
			const result = await validatePath.call(mockNode, { headers: {}, url: '' });
			expect(result).toEqual({ headers: {}, url: '' });
		});
	});

	describe('validateUserPath', () => {
		it('should throw an error for invalid path prefix', async () => {
			mockNode.getNodeParameter.mockReturnValue('/invalidPrefix');

			const mockResponse = {
				ListUsersResponse: {
					ListUsersResult: {
						Users: [{ UserName: 'user1', Path: '/validPrefix/user1' }],
					},
				},
			};

			(awsApiRequest as jest.Mock).mockResolvedValue(mockResponse);

			await expect(validateUserPath.call(mockNode, { headers: {}, url: '' })).rejects.toThrowError(
				NodeOperationError,
			);
		});

		it('should modify the request body with a valid path', async () => {
			mockNode.getNodeParameter.mockReturnValue('/validPrefix/');
			const requestOptions = { body: {}, headers: {}, url: '' };

			const mockResponse = {
				ListUsersResponse: {
					ListUsersResult: {
						Users: [{ UserName: 'user1', Path: '/validPrefix/user1' }],
					},
				},
			};

			(awsApiRequest as jest.Mock).mockResolvedValue(mockResponse);

			const result = await validateUserPath.call(mockNode, requestOptions);
			expect(result.body).toHaveProperty('PathPrefix', '/validPrefix/');
		});
	});

	describe('validateName', () => {
		const requestOptions: IHttpRequestOptions = { body: {}, url: '' };

		it('should throw an error if userName contains spaces', async () => {
			mockNode.getNodeParameter.mockImplementation((param: string) => {
				if (param === 'resource') return 'user';
				if (param === 'userName') return 'John Doe';
				return '';
			});

			const execution = validateName.call(mockNode, requestOptions);
			await expect(execution).rejects.toThrow(NodeOperationError);
			await expect(execution).rejects.toThrow('User name should not contain spaces.');
		});

		it('should throw an error if userName contains invalid characters', async () => {
			mockNode.getNodeParameter.mockImplementation((param: string) => {
				if (param === 'resource') return 'user';
				if (param === 'userName') return 'John@Doe';
				return '';
			});

			const execution = validateName.call(mockNode, requestOptions);
			await expect(execution).rejects.toThrow(NodeOperationError);
			await expect(execution).rejects.toThrow(
				'User name can have up to 64 characters. Valid characters: letters, numbers, hyphens (-), and underscores (_).',
			);
		});

		it('should pass validation for valid userName', async () => {
			mockNode.getNodeParameter.mockImplementation((param: string) => {
				if (param === 'resource') return 'user';
				if (param === 'userName') return 'John_Doe123';
				return '';
			});

			await expect(validateName.call(mockNode, requestOptions)).resolves.toEqual(requestOptions);
		});

		it('should throw an error if groupName contains spaces', async () => {
			mockNode.getNodeParameter.mockImplementation((param: string) => {
				if (param === 'resource') return 'group';
				if (param === 'groupName') return 'Group Name';
				return '';
			});

			const execution = validateName.call(mockNode, requestOptions);
			await expect(execution).rejects.toThrow(NodeOperationError);
			await expect(execution).rejects.toThrow('Group name should not contain spaces.');
		});

		it('should throw an error if groupName contains invalid characters', async () => {
			mockNode.getNodeParameter.mockImplementation((param: string) => {
				if (param === 'resource') return 'group';
				if (param === 'groupName') return 'Group@Name';
				return '';
			});

			const execution = validateName.call(mockNode, requestOptions);
			await expect(execution).rejects.toThrow(NodeOperationError);
			await expect(execution).rejects.toThrow(
				'Group name can have up to 128 characters. Valid characters: letters, numbers, hyphens (-), and underscores (_).',
			);
		});

		it('should pass validation for valid groupName', async () => {
			mockNode.getNodeParameter.mockImplementation((param: string) => {
				if (param === 'resource') return 'group';
				if (param === 'groupName') return 'Group_Name-123';
				return '';
			});

			await expect(validateName.call(mockNode, requestOptions)).resolves.toEqual(requestOptions);
		});
	});

	describe('validatePermissionsBoundary', () => {
		const requestOptions: IHttpRequestOptions = { body: {}, url: '' };

		it('should return the request options unchanged if no permissions boundary is set', async () => {
			mockNode.getNodeParameter.mockReturnValue(undefined);

			const result = await validatePermissionsBoundary.call(mockNode, requestOptions);

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

		it('should add a valid permissions boundary to the request body', async () => {
			const validArn = 'arn:aws:iam::123456789012:policy/ExamplePolicy';
			mockNode.getNodeParameter.mockReturnValue(validArn);

			const result = await validatePermissionsBoundary.call(mockNode, requestOptions);

			expect(result.body).toEqual({ PermissionsBoundary: validArn });
		});

		it('should throw an error for invalid permissions boundary format', async () => {
			const invalidArn = 'invalid:arn:format';
			mockNode.getNodeParameter.mockReturnValue(invalidArn);

			await expect(
				validatePermissionsBoundary.call(mockNode, { body: {}, url: '', headers: {} }),
			).rejects.toThrow(NodeOperationError);
		});
	});

	describe('simplifyGetGroupsResponse', () => {
		it('should return group data', async () => {
			mockNode.getNodeParameter.mockReturnValue(false);
			const mockResponse = {
				body: { GetGroupResponse: { GetGroupResult: { Group: { GroupName: 'TestGroup' } } } },
				headers: {},
				url: '',
				statusCode: 200,
			};

			const result = await simplifyGetGroupsResponse.call(mockNode, [], mockResponse);

			expect(result).toEqual([{ json: { GroupName: 'TestGroup' } }]);
		});

		it('should include users if "includeUsers" is true', async () => {
			mockNode.getNodeParameter.mockReturnValue(true);
			const mockResponse = {
				body: {
					GetGroupResponse: {
						GetGroupResult: { Group: { GroupName: 'TestGroup' }, Users: [{ UserName: 'user1' }] },
					},
				},
				headers: {},
				url: '',
				statusCode: 200,
			};

			const result = await simplifyGetGroupsResponse.call(mockNode, [], mockResponse);

			expect(result).toEqual([
				{ json: { GroupName: 'TestGroup', Users: [{ UserName: 'user1' }] } },
			]);
		});
	});

	describe('simplifyGetAllGroupsResponse', () => {
		it('should return groups without users if "includeUsers" is false', async () => {
			mockNode.getNodeParameter.mockReturnValue(false);
			const mockResponse = {
				body: {
					ListGroupsResponse: { ListGroupsResult: { Groups: [{ GroupName: 'TestGroup' }] } },
				},
				headers: {},
				url: '',
				statusCode: 200,
			};

			const result = await simplifyGetAllGroupsResponse.call(mockNode, [], mockResponse);

			expect(result).toEqual([{ json: { GroupName: 'TestGroup' } }]);
		});

		it('should return groups with users if "includeUsers" is true', async () => {
			mockNode.getNodeParameter.mockReturnValue(true);
			const mockResponse = {
				body: {
					ListGroupsResponse: { ListGroupsResult: { Groups: [{ GroupName: 'TestGroup' }] } },
				},
				headers: {},
				url: '',
				statusCode: 200,
			};

			const mockUsers = [{ UserName: 'user1' }];
			(awsApiRequest as jest.Mock).mockResolvedValueOnce({
				GetGroupResponse: { GetGroupResult: { Users: mockUsers } },
			});

			const result = await simplifyGetAllGroupsResponse.call(mockNode, [], mockResponse);

			expect(result).toEqual([{ json: { GroupName: 'TestGroup', Users: mockUsers } }]);
		});
	});

	describe('simplifyGetAllUsersResponse', () => {
		it('should return all users', async () => {
			const mockResponse = {
				body: { ListUsersResponse: { ListUsersResult: { Users: [{ UserName: 'user1' }] } } },
				headers: {},
				url: '',
				statusCode: 200,
			};
			const result = await simplifyGetAllUsersResponse.call(mockNode, [], mockResponse);
			expect(result).toEqual([{ json: { UserName: 'user1' } }]);
		});
	});

	describe('removeUserFromGroups', () => {
		it('should remove a user from all groups', async () => {
			mockNode.getNodeParameter.mockReturnValue('user1');
			const mockUserGroups = { results: [{ value: 'group1' }, { value: 'group2' }] };
			(awsApiRequest as jest.Mock).mockResolvedValue(mockUserGroups);

			const requestOptions = { headers: {}, url: '' };
			const result = await removeUserFromGroups.call(mockNode, requestOptions);

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