import { mockInstance } from '@n8n/backend-test-utils';
import type { AuthIdentity } from '@n8n/db';
import { generateNanoId, User, AuthIdentityRepository, UserRepository } from '@n8n/db';
import { Container } from '@n8n/di';

import { UrlService } from '@/services/url.service';

import * as helpers from '../saml-helpers';
import type { SamlUserAttributes } from '../types';

const userRepository = mockInstance(UserRepository);
mockInstance(AuthIdentityRepository);

describe('sso/saml/samlHelpers', () => {
	describe('updateUserFromSamlAttributes', () => {
		// We need to use `save` so that that the subscriber in
		// packages/@n8n/db/src/entities/Project.ts receives the full user.
		// With `update` it would only receive the updated fields, e.g. the `id`
		// would be missing.
		test('does not user `Repository.update`, but `Repository.save` instead', async () => {
			//
			// ARRANGE
			//
			const user = Object.assign(new User(), {
				id: generateNanoId(),
				authIdentities: [] as AuthIdentity[],
			} as User);
			const samlUserAttributes: SamlUserAttributes = {
				firstName: 'Nathan',
				lastName: 'Nathaniel',
				email: 'n@8.n',
				userPrincipalName: 'Huh?',
				n8nInstanceRole: 'n8n_instance_role',
			};

			userRepository.findOne.mockImplementationOnce(async (_) => user);
			userRepository.save.mockImplementationOnce(async (_) => user);

			//
			// ACT
			//
			await helpers.updateUserFromSamlAttributes(user, samlUserAttributes);

			//
			// ASSERT
			//
			expect(userRepository.save).toHaveBeenCalledWith(
				{
					...user,
					firstName: samlUserAttributes.firstName,
					lastName: samlUserAttributes.lastName,
				},
				{ transaction: false },
			);
			expect(userRepository.update).not.toHaveBeenCalled();
		});
	});

	describe('getMappedSamlAttributesFromFlowResult', () => {
		test('returns the mapped attributes from the flow result', () => {
			const flowResult = {
				extract: {
					attributes: {
						email: 'test@test.com',
						firstName: 'test',
						lastName: 'test',
						userPrincipalName: 'test',
					},
				},
			} as any;
			const attributeMapping = {
				email: 'email',
				firstName: 'firstName',
				lastName: 'lastName',
				userPrincipalName: 'userPrincipalName',
			};
			const jitClaimNames = {
				instanceRole: 'instanceRole',
				projectRoles: 'projectRoles',
			};

			const result = helpers.getMappedSamlAttributesFromFlowResult(
				flowResult,
				attributeMapping,
				jitClaimNames,
			);

			expect(result).toEqual({
				attributes: {
					email: 'test@test.com',
					firstName: 'test',
					lastName: 'test',
					userPrincipalName: 'test',
				},
				missingAttributes: [],
				rawAttributes: {
					email: 'test@test.com',
					firstName: 'test',
					lastName: 'test',
					userPrincipalName: 'test',
				},
			});
		});

		test('returns the missing attributes from the flow result', () => {
			const flowResult = {
				extract: {
					attributes: {
						email: 'test@test.com',
					},
				},
			} as any;
			const attributeMapping = {
				email: 'email',
				firstName: 'firstName',
				lastName: 'lastName',
				userPrincipalName: 'userPrincipalName',
			};
			const jitClaimNames = {
				instanceRole: 'instanceRole',
				projectRoles: 'projectRoles',
			};

			const result = helpers.getMappedSamlAttributesFromFlowResult(
				flowResult,
				attributeMapping,
				jitClaimNames,
			);

			expect(result).toEqual({
				attributes: {
					email: 'test@test.com',
				},
				missingAttributes: ['userPrincipalName', 'firstName', 'lastName'],
				rawAttributes: { email: 'test@test.com' },
			});
		});
		test('returns the attributes from the flow result with instance role', () => {
			const flowResult = {
				extract: {
					attributes: {
						email: 'test@test.com',
						firstName: 'test',
						lastName: 'test',
						userPrincipalName: 'test',
						instanceRole: 'instanceRole',
					},
				},
			} as any;
			const attributeMapping = {
				email: 'email',
				instanceRole: 'instanceRole',
				firstName: 'firstName',
				lastName: 'lastName',
				userPrincipalName: 'userPrincipalName',
			};
			const jitClaimNames = {
				instanceRole: 'instanceRole',
				projectRoles: 'projectRoles',
			};
			const result = helpers.getMappedSamlAttributesFromFlowResult(
				flowResult,
				attributeMapping,
				jitClaimNames,
			);
			expect(result).toEqual({
				attributes: {
					email: 'test@test.com',
					n8nInstanceRole: 'instanceRole',
					firstName: 'test',
					lastName: 'test',
					userPrincipalName: 'test',
				},
				missingAttributes: [],
				rawAttributes: {
					email: 'test@test.com',
					firstName: 'test',
					lastName: 'test',
					userPrincipalName: 'test',
					instanceRole: 'instanceRole',
				},
			});
		});
	});

	test('returns the attributes from the flow result with project roles', () => {
		const flowResult = {
			extract: {
				attributes: {
					email: 'test@test.com',
					firstName: 'test',
					lastName: 'test',
					userPrincipalName: 'test',
					projectRoles: ['projectRole1', 'projectRole2'],
				},
			},
		} as any;
		const attributeMapping = {
			email: 'email',
			instanceRole: 'instanceRole',
			firstName: 'firstName',
			lastName: 'lastName',
			userPrincipalName: 'userPrincipalName',
		};
		const jitClaimNames = {
			instanceRole: 'instanceRole',
			projectRoles: 'projectRoles',
		};
		const result = helpers.getMappedSamlAttributesFromFlowResult(
			flowResult,
			attributeMapping,
			jitClaimNames,
		);
		expect(result).toEqual({
			attributes: {
				email: 'test@test.com',
				firstName: 'test',
				lastName: 'test',
				userPrincipalName: 'test',
				n8nProjectRoles: ['projectRole1', 'projectRole2'],
			},
			missingAttributes: [],
			rawAttributes: {
				email: 'test@test.com',
				firstName: 'test',
				lastName: 'test',
				userPrincipalName: 'test',
				projectRoles: ['projectRole1', 'projectRole2'],
			},
		});
	});

	test('maps single projectRoles string to array', () => {
		const flowResult = {
			extract: {
				attributes: {
					email: 'test@test.com',
					firstName: 'test',
					lastName: 'test',
					userPrincipalName: 'test',
					projectRoles: 'projectRole1',
				},
			},
		} as any;
		const attributeMapping = {
			email: 'email',
			instanceRole: 'instanceRole',
			firstName: 'firstName',
			lastName: 'lastName',
			userPrincipalName: 'userPrincipalName',
		};
		const jitClaimNames = {
			instanceRole: 'instanceRole',
			projectRoles: 'projectRoles',
		};
		const result = helpers.getMappedSamlAttributesFromFlowResult(
			flowResult,
			attributeMapping,
			jitClaimNames,
		);
		expect(result).toEqual({
			attributes: {
				email: 'test@test.com',
				firstName: 'test',
				lastName: 'test',
				userPrincipalName: 'test',
				n8nProjectRoles: ['projectRole1'],
			},
			missingAttributes: [],
			rawAttributes: {
				email: 'test@test.com',
				firstName: 'test',
				lastName: 'test',
				userPrincipalName: 'test',
				projectRoles: 'projectRole1',
			},
		});
	});

	test('includes all raw attributes including custom ones not in the mapping', () => {
		const flowResult = {
			extract: {
				attributes: {
					email: 'test@test.com',
					firstName: 'test',
					lastName: 'test',
					userPrincipalName: 'test',
					customDepartment: 'engineering',
					groups: ['devops', 'n8n-admins'],
				},
			},
		} as any;
		const attributeMapping = {
			email: 'email',
			firstName: 'firstName',
			lastName: 'lastName',
			userPrincipalName: 'userPrincipalName',
		};
		const jitClaimNames = { instanceRole: null, projectRoles: null };

		const result = helpers.getMappedSamlAttributesFromFlowResult(
			flowResult,
			attributeMapping,
			jitClaimNames,
		);

		expect(result.rawAttributes).toEqual({
			email: 'test@test.com',
			firstName: 'test',
			lastName: 'test',
			userPrincipalName: 'test',
			customDepartment: 'engineering',
			groups: ['devops', 'n8n-admins'],
		});
	});

	test('returns empty rawAttributes when flowResult has no attributes', () => {
		const flowResult = { extract: {} } as any;
		const attributeMapping = {
			email: 'email',
			firstName: 'firstName',
			lastName: 'lastName',
			userPrincipalName: 'userPrincipalName',
		};
		const jitClaimNames = { instanceRole: null, projectRoles: null };

		const result = helpers.getMappedSamlAttributesFromFlowResult(
			flowResult,
			attributeMapping,
			jitClaimNames,
		);

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

	describe('isConnectionTestRequest', () => {
		const baseUrl = 'http://localhost:5678';
		const testReturnUrl = `${baseUrl}/config/test/return`;

		beforeAll(() => {
			const urlService = mockInstance(UrlService);
			urlService.getInstanceBaseUrl.mockReturnValue(baseUrl);
			Container.set(UrlService, urlService);
		});

		test('returns true when RelayState matches the test return URL exactly', () => {
			expect(helpers.isConnectionTestRequest({ RelayState: testReturnUrl })).toBe(true);
		});

		test('returns true when RelayState starts with the test return URL (with testId)', () => {
			expect(helpers.isConnectionTestRequest({ RelayState: `${testReturnUrl}?t=abc123` })).toBe(
				true,
			);
		});

		test('returns false when RelayState is unrelated', () => {
			expect(helpers.isConnectionTestRequest({ RelayState: '/workflow/123' })).toBe(false);
		});

		test('returns false when RelayState is missing', () => {
			expect(helpers.isConnectionTestRequest({})).toBe(false);
		});
	});

	describe('extractTestIdFromRelayState', () => {
		test('returns the value of the t query parameter', () => {
			expect(
				helpers.extractTestIdFromRelayState('http://localhost:5678/config/test/return?t=tok123'),
			).toBe('tok123');
		});

		test('returns undefined when t query parameter is absent', () => {
			expect(
				helpers.extractTestIdFromRelayState('http://localhost:5678/config/test/return'),
			).toBeUndefined();
		});

		test('returns undefined when RelayState is empty or malformed', () => {
			expect(helpers.extractTestIdFromRelayState(undefined)).toBeUndefined();
			expect(helpers.extractTestIdFromRelayState('')).toBeUndefined();
			expect(helpers.extractTestIdFromRelayState('not-a-url')).toBeUndefined();
		});
	});
});
