import type { Logger } from '@n8n/backend-common';
import { CredentialResolverDataNotFoundError, type ICredentialResolver } from '@n8n/decorators';
import type { Cipher } from 'n8n-core';
import type {
	ICredentialContext,
	ICredentialDataDecryptedObject,
	IExecutionContext,
} from 'n8n-workflow';

import type { CredentialResolveMetadata } from '@/credentials/credential-resolution-provider.interface';
import type { LoadNodesAndCredentials } from '@/load-nodes-and-credentials';

import type { DynamicCredentialResolver } from '../../database/entities/credential-resolver';
import type { DynamicCredentialResolverRepository } from '../../database/repositories/credential-resolver.repository';
import { CredentialResolutionError } from '../../errors/credential-resolution.error';
import type { DynamicCredentialResolverRegistry } from '../credential-resolver-registry.service';
import { DynamicCredentialService } from '../dynamic-credential.service';

describe('DynamicCredentialService', () => {
	let service: DynamicCredentialService;
	let mockResolverRegistry: jest.Mocked<DynamicCredentialResolverRegistry>;
	let mockResolverRepository: jest.Mocked<DynamicCredentialResolverRepository>;
	let mockLoadNodesAndCredentials: jest.Mocked<LoadNodesAndCredentials>;
	let mockCipher: jest.Mocked<Cipher>;
	let mockLogger: jest.Mocked<Logger>;

	const createMockCredentialsMetadata = (overrides: Partial<CredentialResolveMetadata> = {}) =>
		({
			id: 'cred-123',
			name: 'Test Credential',
			isResolvable: true,
			resolvableAllowFallback: false,
			resolverId: 'resolver-456',
			...overrides,
		}) as CredentialResolveMetadata;

	const createMockResolverEntity = (overrides: Partial<DynamicCredentialResolver> = {}) =>
		({
			id: 'resolver-456',
			name: 'test-resolver',
			type: 'stub-resolver-1.0',
			config: 'encrypted-resolver-config', // Simulates encrypted config
			createdAt: new Date(),
			updatedAt: new Date(),
			...overrides,
		}) as DynamicCredentialResolver;

	const createMockResolver = (
		shouldSucceed = true,
		shouldThrowDataNotFound = false,
		customData?: ICredentialDataDecryptedObject,
	): jest.Mocked<ICredentialResolver> => ({
		metadata: {
			name: 'stub-resolver-1.0',
			description: 'Test resolver',
		},
		getSecret: jest.fn().mockImplementation(async () => {
			if (shouldThrowDataNotFound) {
				throw new CredentialResolverDataNotFoundError();
			}
			if (!shouldSucceed) {
				throw new Error('Resolution failed');
			}
			return customData ?? { token: 'dynamic-token', apiKey: 'dynamic-key' };
		}),
		setSecret: jest.fn(),
		validateOptions: jest.fn(),
	});

	const createMockExecutionContext = (credentials?: string): IExecutionContext => ({
		version: 1,
		establishedAt: Date.now(),
		source: 'manual' as const,
		credentials,
	});

	const createMockCredentialContext = (): ICredentialContext => ({
		version: 1,
		identity: 'user-123',
		metadata: {},
	});

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

		mockLogger = {
			debug: jest.fn(),
			info: jest.fn(),
			warn: jest.fn(),
			error: jest.fn(),
		} as unknown as jest.Mocked<Logger>;

		mockResolverRegistry = {
			getResolverByTypename: jest.fn(),
			getAllResolvers: jest.fn(),
			init: jest.fn(),
		} as unknown as jest.Mocked<DynamicCredentialResolverRegistry>;

		mockResolverRepository = {
			findOneBy: jest.fn(),
			save: jest.fn(),
			find: jest.fn(),
			findOne: jest.fn(),
		} as unknown as jest.Mocked<DynamicCredentialResolverRepository>;

		mockCipher = {
			encrypt: jest.fn(),
			decrypt: jest.fn(),
		} as unknown as jest.Mocked<Cipher>;

		mockLoadNodesAndCredentials = {
			getCredential: jest.fn(),
			loadCredentials: jest.fn(),
			loadAllCredentials: jest.fn(),
		} as unknown as jest.Mocked<LoadNodesAndCredentials>;

		mockLoadNodesAndCredentials.getCredential.mockReturnValue({
			sourcePath: 'credentials/Test Credential',
			type: { name: 'testCredentialType', displayName: 'Test Credential Type', properties: [] },
		});

		service = new DynamicCredentialService(
			mockResolverRegistry,
			mockResolverRepository,
			mockLoadNodesAndCredentials,
			mockCipher,
			mockLogger,
		);
	});

	describe('resolveIfNeeded', () => {
		const staticData: ICredentialDataDecryptedObject = {
			token: 'static-token',
			apiKey: 'static-key',
		};

		describe('should return static credentials when', () => {
			it('credential is not resolvable', async () => {
				const credentialsEntity = createMockCredentialsMetadata({
					isResolvable: false,
				});

				const result = await service.resolveIfNeeded(credentialsEntity, staticData, undefined);

				expect(result).toBe(staticData);
				expect(mockResolverRepository.findOneBy).not.toHaveBeenCalled();
			});

			it('credential has no resolver ID', async () => {
				const credentialsEntity = createMockCredentialsMetadata({
					isResolvable: true,
					resolverId: undefined,
				});

				const result = await service.resolveIfNeeded(credentialsEntity, staticData, undefined);

				expect(result).toBe(staticData);
				expect(mockResolverRepository.findOneBy).not.toHaveBeenCalled();
			});

			it('resolver entity is not found and fallback is allowed', async () => {
				const credentialsEntity = createMockCredentialsMetadata({
					resolvableAllowFallback: true,
				});

				mockResolverRepository.findOneBy.mockResolvedValue(null);

				const result = await service.resolveIfNeeded(credentialsEntity, staticData, undefined);

				expect(result).toBe(staticData);
				expect(mockLogger.debug).toHaveBeenCalledWith(
					'Resolver not found, falling back to static credentials',
					expect.objectContaining({
						credentialId: 'cred-123',
						resolverId: 'resolver-456',
					}),
				);
			});

			it('resolver instance is not found in registry and fallback is allowed', async () => {
				const credentialsEntity = createMockCredentialsMetadata({
					resolvableAllowFallback: true,
				});
				const resolverEntity = createMockResolverEntity();

				mockResolverRepository.findOneBy.mockResolvedValue(resolverEntity);
				mockResolverRegistry.getResolverByTypename.mockReturnValue(undefined);

				const result = await service.resolveIfNeeded(credentialsEntity, staticData, undefined);

				expect(result).toBe(staticData);
				expect(mockLogger.debug).toHaveBeenCalled();
			});

			it('execution context is missing and fallback is allowed', async () => {
				const credentialsEntity = createMockCredentialsMetadata({
					resolvableAllowFallback: true,
				});
				const resolverEntity = createMockResolverEntity();
				const mockResolver = createMockResolver();

				mockResolverRepository.findOneBy.mockResolvedValue(resolverEntity);
				mockResolverRegistry.getResolverByTypename.mockReturnValue(mockResolver);

				const result = await service.resolveIfNeeded(credentialsEntity, staticData, undefined);

				expect(result).toBe(staticData);
				expect(mockLogger.debug).toHaveBeenCalledWith(
					'No execution context available, falling back to static credentials',
					expect.objectContaining({
						credentialId: 'cred-123',
					}),
				);
			});

			it('credential context decryption fails and fallback is allowed', async () => {
				const credentialsEntity = createMockCredentialsMetadata({
					resolvableAllowFallback: true,
				});
				const resolverEntity = createMockResolverEntity();
				const mockResolver = createMockResolver();
				const executionContext = createMockExecutionContext('encrypted-credentials');

				mockResolverRepository.findOneBy.mockResolvedValue(resolverEntity);
				mockResolverRegistry.getResolverByTypename.mockReturnValue(mockResolver);
				mockCipher.decrypt.mockImplementation(() => {
					throw new Error('Decryption failed'); // Fails on first call (execution context)
				});

				const result = await service.resolveIfNeeded(
					credentialsEntity,
					staticData,
					executionContext,
				);

				expect(result).toBe(staticData);
				expect(mockLogger.error).toHaveBeenCalledWith(
					'Failed to decrypt credential context from execution context',
					expect.any(Object),
				);
			});

			it('resolver getSecret fails and fallback is allowed', async () => {
				const credentialsEntity = createMockCredentialsMetadata({
					resolvableAllowFallback: true,
				});
				const resolverEntity = createMockResolverEntity();
				const mockResolver = createMockResolver(false); // Will fail
				const executionContext = createMockExecutionContext('encrypted-credentials');
				const credentialContext = createMockCredentialContext();

				mockResolverRepository.findOneBy.mockResolvedValue(resolverEntity);
				mockResolverRegistry.getResolverByTypename.mockReturnValue(mockResolver);
				mockCipher.decrypt
					.mockReturnValueOnce(JSON.stringify(credentialContext)) // First call: decrypt credential context
					.mockReturnValueOnce(JSON.stringify({ prefix: 'test' })); // Second call: decrypt resolver config

				const result = await service.resolveIfNeeded(
					credentialsEntity,
					staticData,
					executionContext,
				);

				expect(result).toBe(staticData);
				expect(mockLogger.debug).toHaveBeenCalledWith(
					'Dynamic credential resolution failed, falling back to static',
					expect.objectContaining({
						credentialId: 'cred-123',
						error: 'Resolution failed',
						isDataNotFound: false,
					}),
				);
			});

			it('resolver throws CredentialResolverDataNotFoundError and fallback is allowed', async () => {
				const credentialsEntity = createMockCredentialsMetadata({
					resolvableAllowFallback: true,
				});
				const resolverEntity = createMockResolverEntity();
				const mockResolver = createMockResolver(false, true); // Throws DataNotFoundError
				const executionContext = createMockExecutionContext('encrypted-credentials');
				const credentialContext = createMockCredentialContext();

				mockResolverRepository.findOneBy.mockResolvedValue(resolverEntity);
				mockResolverRegistry.getResolverByTypename.mockReturnValue(mockResolver);
				mockCipher.decrypt
					.mockReturnValueOnce(JSON.stringify(credentialContext)) // First call: decrypt execution context
					.mockReturnValueOnce(JSON.stringify({ prefix: 'test' })); // Second call: decrypt resolver config

				const result = await service.resolveIfNeeded(
					credentialsEntity,
					staticData,
					executionContext,
				);

				expect(result).toBe(staticData);
				expect(mockLogger.debug).toHaveBeenCalledWith(
					'Dynamic credential resolution failed, falling back to static',
					expect.objectContaining({
						isDataNotFound: true,
					}),
				);
			});

			it('no resolver on credential or workflow settings', async () => {
				const credentialsEntity = createMockCredentialsMetadata({
					resolverId: undefined,
				});
				const workflowSettings = {}; // No credentialResolverId
				const executionContext = createMockExecutionContext('encrypted-credentials');

				const result = await service.resolveIfNeeded(
					credentialsEntity,
					staticData,
					executionContext,
					workflowSettings,
				);

				expect(result).toBe(staticData);
				expect(mockResolverRepository.findOneBy).not.toHaveBeenCalled();
			});
		});

		describe('should throw error when', () => {
			it('resolver entity is not found and fallback is not allowed', async () => {
				const credentialsEntity = createMockCredentialsMetadata({
					resolvableAllowFallback: false,
				});

				mockResolverRepository.findOneBy.mockResolvedValue(null);

				await expect(
					service.resolveIfNeeded(credentialsEntity, staticData, undefined),
				).rejects.toThrow(CredentialResolutionError);

				await expect(
					service.resolveIfNeeded(credentialsEntity, staticData, undefined),
				).rejects.toThrow('Resolver "resolver-456" not found for credential "Test Credential"');
			});

			it('resolver instance is not found in registry and fallback is not allowed', async () => {
				const credentialsEntity = createMockCredentialsMetadata({
					resolvableAllowFallback: false,
				});
				const resolverEntity = createMockResolverEntity();

				mockResolverRepository.findOneBy.mockResolvedValue(resolverEntity);
				mockResolverRegistry.getResolverByTypename.mockReturnValue(undefined);

				await expect(
					service.resolveIfNeeded(credentialsEntity, staticData, undefined),
				).rejects.toThrow(CredentialResolutionError);
			});

			it('execution context is missing and fallback is not allowed', async () => {
				const credentialsEntity = createMockCredentialsMetadata({
					resolvableAllowFallback: false,
				});
				const resolverEntity = createMockResolverEntity();
				const mockResolver = createMockResolver();

				mockResolverRepository.findOneBy.mockResolvedValue(resolverEntity);
				mockResolverRegistry.getResolverByTypename.mockReturnValue(mockResolver);

				await expect(
					service.resolveIfNeeded(credentialsEntity, staticData, undefined),
				).rejects.toThrow(CredentialResolutionError);

				await expect(
					service.resolveIfNeeded(credentialsEntity, staticData, undefined),
				).rejects.toThrow(
					'Cannot resolve dynamic credentials without execution context for "Test Credential"',
				);
			});

			it('resolver getSecret fails and fallback is not allowed', async () => {
				const credentialsEntity = createMockCredentialsMetadata({
					resolvableAllowFallback: false,
				});
				const resolverEntity = createMockResolverEntity();
				const mockResolver = createMockResolver(false); // Will fail
				const executionContext = createMockExecutionContext('encrypted-credentials');
				const credentialContext = createMockCredentialContext();

				mockResolverRepository.findOneBy.mockResolvedValue(resolverEntity);
				mockResolverRegistry.getResolverByTypename.mockReturnValue(mockResolver);
				mockCipher.decrypt
					.mockReturnValueOnce(JSON.stringify(credentialContext)) // First invocation: decrypt execution context
					.mockReturnValueOnce(JSON.stringify({ prefix: 'test' })) // First invocation: decrypt resolver config
					.mockReturnValueOnce(JSON.stringify(credentialContext)) // Second invocation: decrypt execution context
					.mockReturnValueOnce(JSON.stringify({ prefix: 'test' })); // Second invocation: decrypt resolver config

				await expect(
					service.resolveIfNeeded(credentialsEntity, staticData, executionContext),
				).rejects.toThrow(CredentialResolutionError);

				await expect(
					service.resolveIfNeeded(credentialsEntity, staticData, executionContext),
				).rejects.toThrow('Failed to resolve dynamic credentials for "Test Credential"');

				expect(mockLogger.debug).toHaveBeenCalledWith(
					'Dynamic credential resolution failed without fallback',
					expect.any(Object),
				);
			});
		});

		describe('should successfully resolve dynamic credentials when', () => {
			it('all conditions are met and merges static with dynamic data', async () => {
				const credentialsEntity = createMockCredentialsMetadata();
				const resolverEntity = createMockResolverEntity();
				const executionContext = createMockExecutionContext('encrypted-credentials');
				const credentialContext = createMockCredentialContext();

				// Static data includes OAuth client config and old token
				const staticOAuthData: ICredentialDataDecryptedObject = {
					clientId: 'static-client-id',
					clientSecret: 'static-client-secret',
					token: 'static-token', // Will be overridden
					apiKey: 'static-key', // Will be overridden
				};

				// Dynamic data includes new tokens (overrides token) and new fields
				const dynamicData: ICredentialDataDecryptedObject = {
					token: 'dynamic-token',
					apiKey: 'dynamic-key',
					refreshToken: 'dynamic-refresh-token',
				};

				const mockResolver = createMockResolver(true, false, dynamicData);

				mockResolverRepository.findOneBy.mockResolvedValue(resolverEntity);
				mockResolverRegistry.getResolverByTypename.mockReturnValue(mockResolver);
				mockCipher.decrypt
					.mockReturnValueOnce(JSON.stringify(credentialContext)) // First call: decrypt execution context
					.mockReturnValueOnce(JSON.stringify({ prefix: 'test' })); // Second call: decrypt resolver config

				const result = await service.resolveIfNeeded(
					credentialsEntity,
					staticOAuthData,
					executionContext,
				);

				// Verify merge: static fields preserved, dynamic fields added/overridden
				expect(result).toEqual({
					clientId: 'static-client-id', // From static (preserved)
					clientSecret: 'static-client-secret', // From static (preserved)
					token: 'dynamic-token', // From dynamic (overridden)
					apiKey: 'dynamic-key', // From dynamic (overridden)
					refreshToken: 'dynamic-refresh-token', // From dynamic (new field)
				});
				expect(mockResolver.getSecret).toHaveBeenCalledWith('cred-123', credentialContext, {
					resolverId: resolverEntity.id,
					resolverName: resolverEntity.type,
					configuration: {
						prefix: 'test',
					},
				});
				expect(mockLogger.debug).toHaveBeenCalledWith(
					'Successfully resolved dynamic credentials',
					expect.objectContaining({
						credentialId: 'cred-123',
						resolverId: 'resolver-456',
						identity: 'user-123',
					}),
				);
			});

			it('resolver config is parsed correctly', async () => {
				const credentialsEntity = createMockCredentialsMetadata();
				const customConfig = {
					prefix: 'custom',
					timeout: 5000,
					retries: 3,
				};
				const resolverEntity = createMockResolverEntity({
					config: 'encrypted-custom-resolver-config', // Simulates encrypted config
				});
				const mockResolver = createMockResolver();
				const executionContext = createMockExecutionContext('encrypted-credentials');
				const credentialContext = createMockCredentialContext();

				mockResolverRepository.findOneBy.mockResolvedValue(resolverEntity);
				mockResolverRegistry.getResolverByTypename.mockReturnValue(mockResolver);
				mockCipher.decrypt
					.mockReturnValueOnce(JSON.stringify(credentialContext)) // First call: decrypt execution context
					.mockReturnValueOnce(JSON.stringify(customConfig)); // Second call: decrypt resolver config

				await service.resolveIfNeeded(credentialsEntity, staticData, executionContext);

				expect(mockResolver.getSecret).toHaveBeenCalledWith('cred-123', credentialContext, {
					resolverId: resolverEntity.id,
					resolverName: resolverEntity.type,
					configuration: customConfig,
				});
			});

			it('credential context with metadata is properly decrypted', async () => {
				const credentialsEntity = createMockCredentialsMetadata();
				const resolverEntity = createMockResolverEntity();
				const mockResolver = createMockResolver();
				const credentialContext: ICredentialContext = {
					version: 1,
					identity: 'user-456',
					metadata: {
						team: 'engineering',
						environment: 'production',
					},
				};
				const executionContext = createMockExecutionContext('encrypted-credentials');

				mockResolverRepository.findOneBy.mockResolvedValue(resolverEntity);
				mockResolverRegistry.getResolverByTypename.mockReturnValue(mockResolver);
				mockCipher.decrypt
					.mockReturnValueOnce(JSON.stringify(credentialContext)) // First call: decrypt execution context
					.mockReturnValueOnce(JSON.stringify({ prefix: 'test' })); // Second call: decrypt resolver config

				await service.resolveIfNeeded(credentialsEntity, staticData, executionContext);

				expect(mockResolver.getSecret).toHaveBeenCalledWith(
					'cred-123',
					credentialContext,
					expect.any(Object),
				);
				expect(mockLogger.debug).toHaveBeenCalledWith(
					'Successfully resolved dynamic credentials',
					expect.objectContaining({
						identity: 'user-456',
					}),
				);
			});
		});

		describe('should handle edge cases', () => {
			it('execution context without credentials field', async () => {
				const credentialsEntity = createMockCredentialsMetadata({
					resolvableAllowFallback: true,
				});
				const resolverEntity = createMockResolverEntity();
				const mockResolver = createMockResolver();
				const executionContext = createMockExecutionContext(undefined);

				mockResolverRepository.findOneBy.mockResolvedValue(resolverEntity);
				mockResolverRegistry.getResolverByTypename.mockReturnValue(mockResolver);

				const result = await service.resolveIfNeeded(
					credentialsEntity,
					staticData,
					executionContext,
				);

				expect(result).toBe(staticData);
				expect(mockCipher.decrypt).not.toHaveBeenCalled();
			});

			it('invalid JSON in encrypted credentials', async () => {
				const credentialsEntity = createMockCredentialsMetadata({
					resolvableAllowFallback: true,
				});
				const resolverEntity = createMockResolverEntity();
				const mockResolver = createMockResolver();
				const executionContext = createMockExecutionContext('encrypted-credentials');

				mockResolverRepository.findOneBy.mockResolvedValue(resolverEntity);
				mockResolverRegistry.getResolverByTypename.mockReturnValue(mockResolver);
				mockCipher.decrypt.mockReturnValue('not-valid-json');

				const result = await service.resolveIfNeeded(
					credentialsEntity,
					staticData,
					executionContext,
				);

				expect(result).toBe(staticData);
				expect(mockLogger.debug).toHaveBeenCalled();
			});

			it('empty resolver config', async () => {
				const credentialsEntity = createMockCredentialsMetadata();
				const resolverEntity = createMockResolverEntity({
					config: 'encrypted-empty-config', // Simulates encrypted empty config
				});
				const mockResolver = createMockResolver();
				const executionContext = createMockExecutionContext('encrypted-credentials');
				const credentialContext = createMockCredentialContext();

				mockResolverRepository.findOneBy.mockResolvedValue(resolverEntity);
				mockResolverRegistry.getResolverByTypename.mockReturnValue(mockResolver);
				mockCipher.decrypt
					.mockReturnValueOnce(JSON.stringify(credentialContext)) // First call: decrypt execution context
					.mockReturnValueOnce('{}'); // Second call: decrypt resolver config (empty object)

				await service.resolveIfNeeded(credentialsEntity, staticData, executionContext);

				expect(mockResolver.getSecret).toHaveBeenCalledWith('cred-123', credentialContext, {
					resolverId: resolverEntity.id,
					resolverName: resolverEntity.type,
					configuration: {},
				});
			});
		});

		describe('workflow settings fallback', () => {
			it('should use workflow settings resolver when credential has no resolver', async () => {
				const credentialsEntity = createMockCredentialsMetadata({
					resolverId: undefined, // No resolver on credential
				});
				const workflowSettings = {
					credentialResolverId: 'workflow-resolver-789',
				};
				const resolverEntity = createMockResolverEntity({
					id: 'workflow-resolver-789',
				});
				const mockResolver = createMockResolver();
				const executionContext = createMockExecutionContext('encrypted-credentials');
				const credentialContext = createMockCredentialContext();

				mockResolverRepository.findOneBy.mockResolvedValue(resolverEntity);
				mockResolverRegistry.getResolverByTypename.mockReturnValue(mockResolver);
				mockCipher.decrypt
					.mockReturnValueOnce(JSON.stringify(credentialContext)) // First call: decrypt execution context
					.mockReturnValueOnce(JSON.stringify({ prefix: 'test' })); // Second call: decrypt resolver config

				const result = await service.resolveIfNeeded(
					credentialsEntity,
					staticData,
					executionContext,
					workflowSettings,
				);

				expect(mockResolverRepository.findOneBy).toHaveBeenCalledWith({
					id: 'workflow-resolver-789',
				});
				expect(result).toEqual({ token: 'dynamic-token', apiKey: 'dynamic-key' });
				expect(mockLogger.debug).toHaveBeenCalledWith(
					'Successfully resolved dynamic credentials',
					expect.objectContaining({
						resolverId: 'workflow-resolver-789',
						resolverSource: 'workflow',
					}),
				);
			});

			it('should prefer credential resolver over workflow settings resolver', async () => {
				const credentialsEntity = createMockCredentialsMetadata({
					resolverId: 'credential-resolver-456',
				});
				const workflowSettings = {
					credentialResolverId: 'workflow-resolver-789',
				};
				const resolverEntity = createMockResolverEntity({
					id: 'credential-resolver-456',
				});
				const mockResolver = createMockResolver();
				const executionContext = createMockExecutionContext('encrypted-credentials');
				const credentialContext = createMockCredentialContext();

				mockResolverRepository.findOneBy.mockResolvedValue(resolverEntity);
				mockResolverRegistry.getResolverByTypename.mockReturnValue(mockResolver);
				mockCipher.decrypt
					.mockReturnValueOnce(JSON.stringify(credentialContext)) // First call: decrypt execution context
					.mockReturnValueOnce(JSON.stringify({ prefix: 'test' })); // Second call: decrypt resolver config

				const result = await service.resolveIfNeeded(
					credentialsEntity,
					staticData,
					executionContext,
					workflowSettings,
				);

				expect(mockResolverRepository.findOneBy).toHaveBeenCalledWith({
					id: 'credential-resolver-456',
				});
				expect(result).toEqual({ token: 'dynamic-token', apiKey: 'dynamic-key' });
				expect(mockLogger.debug).toHaveBeenCalledWith(
					'Successfully resolved dynamic credentials',
					expect.objectContaining({
						resolverId: 'credential-resolver-456',
						resolverSource: 'credential',
					}),
				);
			});

			it('should fall back to static when workflow resolver not found and fallback allowed', async () => {
				const credentialsEntity = createMockCredentialsMetadata({
					resolverId: undefined,
					resolvableAllowFallback: true,
				});
				const workflowSettings = {
					credentialResolverId: 'workflow-resolver-789',
				};
				const executionContext = createMockExecutionContext('encrypted-credentials');

				mockResolverRepository.findOneBy.mockResolvedValue(null);

				const result = await service.resolveIfNeeded(
					credentialsEntity,
					staticData,
					executionContext,
					workflowSettings,
				);

				expect(result).toBe(staticData);
				expect(mockLogger.debug).toHaveBeenCalledWith(
					'Resolver not found, falling back to static credentials',
					expect.objectContaining({
						resolverId: 'workflow-resolver-789',
						resolverSource: 'workflow',
					}),
				);
			});
		});
	});
});
