import { GLOBAL_OWNER_ROLE, type AuthenticatedRequest, type User } from '@n8n/db';
import { type Response } from 'express';
import { mock } from 'jest-mock-extended';

import type { AuthService } from '@/auth/auth.service';
import type { EventService } from '@/events/event.service';
import type { InstanceSettingsLoaderConfig } from '@n8n/config';
import type { AuthlessRequest } from '@/requests';
import type { UrlService } from '@/services/url.service';
import { isSamlLicensedAndEnabled } from '@/sso.ee/sso-helpers';

import { extractTestIdFromRelayState, isConnectionTestRequest } from '../saml-helpers';
import { SamlController } from '../saml.controller.ee';
import type { SamlService } from '../saml.service.ee';
import { getServiceProviderConfigTestReturnUrl } from '../service-provider.ee';
import type { SamlUserAttributes } from '../types';

// Mock the saml-helpers module
jest.mock('../saml-helpers', () => ({
	isConnectionTestRequest: jest.fn(),
	extractTestIdFromRelayState: jest.fn(),
}));

jest.mock('@/sso.ee/sso-helpers', () => ({
	isSamlLicensedAndEnabled: jest.fn(),
}));

const authService = mock<AuthService>();
const samlService = mock<SamlService>();
const urlService = mock<UrlService>();
const eventService = mock<EventService>();
const instanceSettingsLoaderConfig = mock<InstanceSettingsLoaderConfig>({ ssoManagedByEnv: false });
const controller = new SamlController(
	authService,
	samlService,
	urlService,
	eventService,
	instanceSettingsLoaderConfig,
);

const user = mock<User>({
	id: '123',
	password: 'password',
	authIdentities: [],
	role: GLOBAL_OWNER_ROLE,
});

const attributes: SamlUserAttributes = {
	email: 'test@example.com',
	firstName: 'Test',
	lastName: 'User',
	userPrincipalName: 'upn:test@example.com',
};
const rawAttributes = {
	email: 'test@example.com',
	givenName: 'Test',
	surname: 'User',
	groups: ['admins', 'engineers'],
};
const rawAttributesJson = JSON.stringify(rawAttributes, null, 2);

describe('Test views', () => {
	const RelayState = getServiceProviderConfigTestReturnUrl();

	beforeEach(() => {
		jest.clearAllMocks();
		// Mock the helper functions for test connection flow
		(isConnectionTestRequest as jest.Mock).mockReturnValue(true);
		(extractTestIdFromRelayState as jest.Mock).mockReturnValue(undefined);
	});

	test('Should render success with template', async () => {
		const req = mock<AuthlessRequest>();
		const res = mock<Response>({
			status: jest.fn().mockReturnThis(),
			json: jest.fn().mockReturnThis(),
		});

		samlService.handleSamlLogin.mockResolvedValueOnce({
			authenticatedUser: user,
			attributes,
			rawAttributes,
			onboardingRequired: false,
		});

		await controller.acsPost(req, res, { RelayState });

		expect(res.render).toBeCalledWith('saml-connection-test-success', {
			...attributes,
			rawAttributesJson,
		});
	});

	test('Should render failure with template', async () => {
		const req = mock<AuthlessRequest>();
		const res = mock<Response>({
			status: jest.fn().mockReturnThis(),
			json: jest.fn().mockReturnThis(),
		});

		samlService.handleSamlLogin.mockResolvedValueOnce({
			authenticatedUser: undefined,
			attributes,
			rawAttributes,
			onboardingRequired: false,
		});

		await controller.acsPost(req, res, { RelayState });

		expect(res.render).toBeCalledWith('saml-connection-test-failed', {
			message: '',
			attributes,
			rawAttributesJson,
		});
	});

	test('Should render error with template', async () => {
		const req = mock<AuthlessRequest>();
		const res = mock<Response>({
			status: jest.fn().mockReturnThis(),
			json: jest.fn().mockReturnThis(),
		});

		samlService.handleSamlLogin.mockRejectedValueOnce(new Error('Test Error'));

		await controller.acsPost(req, res, { RelayState });

		expect(res.render).toBeCalledWith('saml-connection-test-failed', { message: 'Test Error' });
	});

	test('Should pass cached metadata to handleSamlLogin for test connections', async () => {
		const req = mock<AuthlessRequest>();
		const res = mock<Response>({
			status: jest.fn().mockReturnThis(),
			json: jest.fn().mockReturnThis(),
		});
		const testId = 'abc123';
		const metadata = '<EntityDescriptor/>';
		const RelayStateWithTestId = `${RelayState}?t=${testId}`;

		(extractTestIdFromRelayState as jest.Mock).mockReturnValue(testId);
		samlService.consumePendingTestConfig.mockResolvedValueOnce(metadata);
		samlService.handleSamlLogin.mockResolvedValueOnce({
			authenticatedUser: user,
			attributes,
			rawAttributes,
			onboardingRequired: false,
		});

		await controller.acsPost(req, res, { RelayState: RelayStateWithTestId });

		expect(samlService.consumePendingTestConfig).toHaveBeenCalledWith(testId);
		expect(samlService.handleSamlLogin).toHaveBeenCalledWith(req, 'post', metadata);
		expect(res.render).toBeCalledWith('saml-connection-test-success', {
			...attributes,
			rawAttributesJson,
		});
	});

	test('Should still call handleSamlLogin without override when no test token in RelayState', async () => {
		const req = mock<AuthlessRequest>();
		const res = mock<Response>({
			status: jest.fn().mockReturnThis(),
			json: jest.fn().mockReturnThis(),
		});

		samlService.handleSamlLogin.mockResolvedValueOnce({
			authenticatedUser: user,
			attributes,
			rawAttributes,
			onboardingRequired: false,
		});

		await controller.acsPost(req, res, { RelayState });

		expect(samlService.consumePendingTestConfig).not.toHaveBeenCalled();
		expect(samlService.handleSamlLogin).toHaveBeenCalledWith(req, 'post', undefined);
	});
});

describe('configTestPost', () => {
	beforeEach(() => {
		jest.clearAllMocks();
		urlService.getInstanceBaseUrl.mockReturnValue('http://localhost:5678');
	});

	test('caches pasted metadata and embeds testId in RelayState', async () => {
		const req = mock<AuthenticatedRequest>();
		const res = mock<Response>();
		const metadata = '<EntityDescriptor/>';

		samlService.storePendingTestConfig.mockResolvedValueOnce('tok12345');
		samlService.getLoginRequestUrl.mockResolvedValueOnce({
			binding: 'redirect',
			context: { context: 'http://idp.example.com/login' } as any,
		});

		await controller.configTestPost(req, res, { metadata } as any);

		expect(samlService.fetchMetadataFromUrl).not.toHaveBeenCalled();
		expect(samlService.storePendingTestConfig).toHaveBeenCalledWith(metadata);
		expect(samlService.getLoginRequestUrl).toHaveBeenCalledWith(
			'http://localhost:5678/config/test/return?t=tok12345',
			undefined,
			metadata,
		);
	});

	test('fetches metadata from URL and caches it before building login request', async () => {
		const req = mock<AuthenticatedRequest>();
		const res = mock<Response>();
		const metadataUrl = 'https://idp.example.com/metadata';
		const fetchedMetadata = '<EntityDescriptor fetched/>';

		samlService.fetchMetadataFromUrl.mockResolvedValueOnce(fetchedMetadata);
		samlService.storePendingTestConfig.mockResolvedValueOnce('tokXYZ');
		samlService.getLoginRequestUrl.mockResolvedValueOnce({
			binding: 'redirect',
			context: { context: 'http://idp.example.com/login' } as any,
		});

		await controller.configTestPost(req, res, { metadataUrl, ignoreSSL: true } as any);

		expect(samlService.fetchMetadataFromUrl).toHaveBeenCalledWith(metadataUrl, true);
		expect(samlService.storePendingTestConfig).toHaveBeenCalledWith(fetchedMetadata);
		expect(samlService.getLoginRequestUrl).toHaveBeenCalledWith(
			'http://localhost:5678/config/test/return?t=tokXYZ',
			undefined,
			fetchedMetadata,
		);
	});
});

describe('SAML Login Flow', () => {
	beforeEach(() => {
		jest.clearAllMocks();
		// Mock the helper functions for actual login flow (not test connections)
		(isConnectionTestRequest as jest.Mock).mockReturnValue(false);
		(extractTestIdFromRelayState as jest.Mock).mockReturnValue(undefined);
		(isSamlLicensedAndEnabled as jest.Mock).mockReturnValue(true);

		// Mock URL service
		urlService.getInstanceBaseUrl.mockReturnValue('http://localhost:5678');
	});

	test('Should issue cookie with MFA flag set to true on successful SAML login', async () => {
		const req = mock<AuthlessRequest>({ browserId: 'test-browser-id' });
		const res = mock<Response>();

		samlService.handleSamlLogin.mockResolvedValueOnce({
			authenticatedUser: user,
			attributes,
			rawAttributes,
			onboardingRequired: false,
		});

		await controller.acsPost(req, res, { RelayState: '/' });

		// Verify that issueCookie was called with MFA flag set to true
		expect(authService.issueCookie).toHaveBeenCalledWith(res, user, true, 'test-browser-id');
		expect(eventService.emit).toHaveBeenCalledWith('user-logged-in', {
			user,
			authenticationMethod: 'saml',
		});
		expect(res.redirect).toHaveBeenCalledWith('http://localhost:5678/');
	});

	test('Should issue cookie with MFA flag set to true when onboarding is required', async () => {
		const req = mock<AuthlessRequest>({ browserId: 'test-browser-id' });
		const res = mock<Response>();

		samlService.handleSamlLogin.mockResolvedValueOnce({
			authenticatedUser: user,
			attributes,
			rawAttributes,
			onboardingRequired: true,
		});

		await controller.acsPost(req, res, { RelayState: '/' });

		// Verify that issueCookie was called with MFA flag set to true
		expect(authService.issueCookie).toHaveBeenCalledWith(res, user, true, 'test-browser-id');
		expect(res.redirect).toHaveBeenCalledWith('http://localhost:5678/saml/onboarding');
	});

	test('Should respect custom RelayState redirect URL', async () => {
		const req = mock<AuthlessRequest>({ browserId: 'test-browser-id' });
		const res = mock<Response>();
		const customRelayState = '/custom/redirect';

		samlService.handleSamlLogin.mockResolvedValueOnce({
			authenticatedUser: user,
			attributes,
			rawAttributes,
			onboardingRequired: false,
		});

		await controller.acsPost(req, res, { RelayState: customRelayState });

		expect(authService.issueCookie).toHaveBeenCalledWith(res, user, true, 'test-browser-id');
		expect(res.redirect).toHaveBeenCalledWith('http://localhost:5678/custom/redirect');
	});

	describe('Redirect URL Validation', () => {
		test('allows redirect to relative URls starting with slash', async () => {
			const req = mock<AuthlessRequest>({ browserId: 'test-browser-id' });
			const res = mock<Response>();

			samlService.handleSamlLogin.mockResolvedValueOnce({
				authenticatedUser: user,
				attributes,
				rawAttributes,
				onboardingRequired: false,
			});
			await controller.acsPost(req, res, { RelayState: '/workflow/123' });

			expect(res.redirect).toHaveBeenCalledWith('http://localhost:5678/workflow/123');
		});

		test('validates redirect URL that is passed in via URL parameter', async () => {
			const req = mock<AuthlessRequest>({
				query: { redirect: '//evil.com/phishing' },
				headers: {},
			});
			const res = mock<Response>();

			samlService.getLoginRequestUrl.mockResolvedValueOnce({
				binding: 'redirect',
				context: { context: 'http://idp.example.com/login' } as any,
			});

			await controller.initSsoGet(req, res);

			expect(samlService.getLoginRequestUrl).toHaveBeenCalledWith('/');
		});

		test('validates redirect URL that is passed in via referrer header', async () => {
			const req = mock<AuthlessRequest>({
				query: {},
				headers: {
					referer: 'http://localhost:5678/login?redirect=%2F%2Fevil.com%2Fphishing',
				},
			});
			const res = mock<Response>();

			samlService.getLoginRequestUrl.mockResolvedValueOnce({
				binding: 'redirect',
				context: { context: 'http://idp.example.com/login' } as any,
			});

			await controller.initSsoGet(req, res);

			expect(samlService.getLoginRequestUrl).toHaveBeenCalledWith('/');
		});

		const hostWithoutRedirect = 'http://localhost:5678/';
		test.each([
			['https://evil.com/phishing'],
			['//evil.com/phishing'],
			['javascript:alert(1)'],
			['%2F%2Fevil.com/phishing'],
			['workflows/123'],
		])('does not allow redirect to %s', async (blockedUrl: string) => {
			const req = mock<AuthlessRequest>({ browserId: 'test-browser-id' });
			const res = mock<Response>();

			samlService.handleSamlLogin.mockResolvedValueOnce({
				authenticatedUser: user,
				attributes,
				rawAttributes,
				onboardingRequired: false,
			});
			await controller.acsPost(req, res, { RelayState: blockedUrl });

			expect(res.redirect).toHaveBeenCalledWith(hostWithoutRedirect);
		});
	});
});

describe('SAML env-managed write protection', () => {
	const envManagedConfig = mock<InstanceSettingsLoaderConfig>({ ssoManagedByEnv: true });
	const envManagedController = new SamlController(
		authService,
		samlService,
		urlService,
		eventService,
		envManagedConfig,
	);

	test('configPost should reject writes when managed by env', async () => {
		const req = mock<AuthenticatedRequest>();
		const res = mock<Response>();

		await expect(
			envManagedController.configPost(req, res, { loginEnabled: true } as any),
		).rejects.toThrow('cannot be modified through the API');
	});

	test('toggleEnabledPost should reject writes when managed by env', async () => {
		const req = mock<AuthenticatedRequest>();
		const res = mock<Response>();

		await expect(
			envManagedController.toggleEnabledPost(req, res, { loginEnabled: true }),
		).rejects.toThrow('cannot be modified through the API');
	});
});
