import type { Mocked } from 'vitest';
import type { CredentialsRepository, WorkflowRepository } from '@n8n/db';
import { CredentialsEntity, WorkflowEntity } from '@n8n/db';
import type { ICredentialResolver } from '@n8n/decorators';
import type { Cipher } from 'n8n-core';
import type { INode, NodeParameterValueType } from 'n8n-workflow';

import type { DynamicCredentialsProxy } from '@/credentials/dynamic-credentials-proxy';
import type { WorkflowFinderService } from '@/workflows/workflow-finder.service';

import { DynamicCredentialResolver } from '../../database/entities/credential-resolver';
import type { DynamicCredentialResolverRepository } from '../../database/repositories/credential-resolver.repository';
import type { DynamicCredentialResolverRegistry } from '../credential-resolver-registry.service';
import { CredentialResolverWorkflowService } from '../credential-resolver-workflow.service';

const createMockWorkflow = (overrides: Partial<WorkflowEntity> = {}): WorkflowEntity => {
	const workflow = new WorkflowEntity();
	workflow.id = 'workflow-1';
	workflow.name = 'Test Workflow';
	workflow.description = null;
	workflow.active = false;
	workflow.isArchived = false;
	workflow.nodes = [];
	workflow.connections = {};
	workflow.settings = {};
	workflow.createdAt = new Date('2024-01-01');
	workflow.updatedAt = new Date('2024-01-01');
	Object.assign(workflow, overrides);
	return workflow;
};

const createMockCredential = (overrides: Partial<CredentialsEntity> = {}): CredentialsEntity => {
	const credential = new CredentialsEntity();
	credential.id = 'cred-1';
	credential.name = 'Test Credential';
	credential.type = 'oauth2Api';
	credential.data = '';
	credential.shared = [];
	credential.isManaged = false;
	credential.isGlobal = false;
	credential.isResolvable = true;
	credential.resolvableAllowFallback = false;
	credential.resolverId = null;
	credential.createdAt = new Date('2024-01-01');
	credential.updatedAt = new Date('2024-01-01');
	Object.assign(credential, overrides);
	return credential;
};

const createMockResolver = (
	overrides: Partial<DynamicCredentialResolver> = {},
): DynamicCredentialResolver => {
	const resolver = new DynamicCredentialResolver();
	resolver.id = 'resolver-1';
	resolver.name = 'Test Resolver';
	resolver.type = 'test.resolver';
	resolver.config = 'encrypted-config';
	resolver.createdAt = new Date('2024-01-01');
	resolver.updatedAt = new Date('2024-01-01');
	Object.assign(resolver, overrides);
	return resolver;
};

const createMockNode = (overrides: Partial<INode> = {}): INode => {
	return {
		id: 'node-1',
		name: 'Node1',
		type: 'n8n-nodes-base.httpRequest',
		typeVersion: 1,
		position: [0, 0],
		parameters: {},
		credentials: {},
		...overrides,
	};
};

describe('CredentialResolverWorkflowService', () => {
	let service: CredentialResolverWorkflowService;
	let mockWorkflowRepository: Mocked<WorkflowRepository>;
	let mockCredentialRepository: Mocked<CredentialsRepository>;
	let mockResolverRegistry: Mocked<DynamicCredentialResolverRegistry>;
	let mockResolverRepository: Mocked<DynamicCredentialResolverRepository>;
	let mockCipher: Mocked<Cipher>;
	let mockResolverImplementation: Mocked<ICredentialResolver>;
	let mockDynamicCredentialsProxy: Mocked<DynamicCredentialsProxy>;
	let mockWorkflowFinderService: Mocked<WorkflowFinderService>;

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

		mockWorkflowRepository = {
			get: vi.fn(),
		} as unknown as Mocked<WorkflowRepository>;

		mockCredentialRepository = {
			find: vi.fn(),
		} as unknown as Mocked<CredentialsRepository>;

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

		mockResolverRepository = {
			findOneBy: vi.fn(),
		} as unknown as Mocked<DynamicCredentialResolverRepository>;

		mockCipher = {
			decryptV2: vi.fn(),
		} as unknown as Mocked<Cipher>;

		mockResolverImplementation = {
			metadata: {
				name: 'test.resolver',
				description: 'A test resolver',
			},
			getSecret: vi.fn(),
			setSecret: vi.fn(),
			validateOptions: vi.fn(),
		};

		mockDynamicCredentialsProxy = {
			getSystemResolverId: vi.fn().mockReturnValue(null),
			// Default to the real semantics with no system resolver seeded:
			// pass through the workflow override if any, otherwise null.
			getEffectiveResolverId: vi.fn((settings) => settings?.credentialResolverId ?? null),
		} as unknown as Mocked<DynamicCredentialsProxy>;

		mockWorkflowFinderService = {
			findWorkflowForUser: vi.fn(),
		} as unknown as Mocked<WorkflowFinderService>;

		service = new CredentialResolverWorkflowService(
			mockWorkflowRepository,
			mockCredentialRepository,
			mockResolverRegistry,
			mockResolverRepository,
			mockCipher,
			mockDynamicCredentialsProxy,
			mockWorkflowFinderService,
		);
	});

	describe('getWorkflowStatus', () => {
		it('should throw when workflow not found', async () => {
			mockWorkflowRepository.get.mockResolvedValue(null);

			await expect(
				service.getWorkflowStatus('workflow-1', {
					identity: 'token-123',
					version: 1 as const,
					metadata: {},
				}),
			).rejects.toThrow('Workflow not found');
		});

		it('should return empty array when workflow has no nodes', async () => {
			mockWorkflowRepository.get.mockResolvedValue(createMockWorkflow({ nodes: [] }));

			const result = await service.getWorkflowStatus('workflow-1', {
				identity: 'token-123',
				version: 1 as const,
				metadata: {},
			});

			expect(result).toEqual([]);
			expect(mockCredentialRepository.find).not.toHaveBeenCalled();
		});

		it('should return empty array when workflow has no credentials', async () => {
			mockWorkflowRepository.get.mockResolvedValue(
				createMockWorkflow({
					nodes: [createMockNode({ credentials: {} })],
				}),
			);

			const result = await service.getWorkflowStatus('workflow-1', {
				identity: 'token-123',
				version: 1 as const,
				metadata: {},
			});

			expect(result).toEqual([]);
			expect(mockCredentialRepository.find).not.toHaveBeenCalled();
		});

		it('should return configured status when getSecret succeeds', async () => {
			const mockWorkflow = createMockWorkflow({
				nodes: [
					createMockNode({
						credentials: {
							oauth2Api: { id: 'cred-1', name: 'OAuth2 API' },
						},
					}),
				],
				settings: { credentialResolverId: 'resolver-1' },
			});

			const mockCredential = createMockCredential({
				id: 'cred-1',
				type: 'oauth2Api',
				name: 'OAuth2 API',
				isResolvable: true,
				resolverId: null,
			});

			const mockResolver = createMockResolver({
				id: 'resolver-1',
				type: 'test.resolver',
				config: 'encrypted-config',
			});

			mockWorkflowRepository.get.mockResolvedValue(mockWorkflow);
			mockCredentialRepository.find.mockResolvedValue([mockCredential]);
			mockResolverRepository.findOneBy.mockResolvedValue(mockResolver);
			mockResolverRegistry.getResolverByTypename.mockReturnValue(mockResolverImplementation);
			mockCipher.decryptV2.mockResolvedValue(JSON.stringify({ prefix: 'test' }));
			mockResolverImplementation.getSecret.mockResolvedValue('secret-value' as any);

			const credentialContext = {
				identity: 'token-123',
				version: 1 as const,
				metadata: {},
			};

			const result = await service.getWorkflowStatus('workflow-1', credentialContext);

			expect(result).toHaveLength(1);
			expect(result[0]).toEqual({
				credentialId: 'cred-1',
				resolverId: 'resolver-1',
				credentialName: 'OAuth2 API',
				status: 'configured',
				credentialType: 'oauth2Api',
			});
			expect(mockResolverImplementation.getSecret).toHaveBeenCalledWith(
				'cred-1',
				credentialContext,
				{
					configuration: { prefix: 'test' },
					resolverName: 'test.resolver',
					resolverId: 'resolver-1',
				},
			);
		});

		it('should return missing status when getSecret fails', async () => {
			const mockWorkflow = createMockWorkflow({
				nodes: [
					createMockNode({
						credentials: {
							oauth2Api: { id: 'cred-1', name: 'OAuth2 API' },
						},
					}),
				],
				settings: { credentialResolverId: 'resolver-1' },
			});

			const mockCredential = createMockCredential({
				id: 'cred-1',
				type: 'oauth2Api',
				name: 'OAuth2 API',
				isResolvable: true,
				resolverId: null,
			});

			const mockResolver = createMockResolver({
				id: 'resolver-1',
				type: 'test.resolver',
				config: 'encrypted-config',
			});

			mockWorkflowRepository.get.mockResolvedValue(mockWorkflow);
			mockCredentialRepository.find.mockResolvedValue([mockCredential]);
			mockResolverRepository.findOneBy.mockResolvedValue(mockResolver);
			mockResolverRegistry.getResolverByTypename.mockReturnValue(mockResolverImplementation);
			mockCipher.decryptV2.mockResolvedValue(JSON.stringify({ prefix: 'test' }));
			mockResolverImplementation.getSecret.mockRejectedValue(new Error('Secret not found'));

			const result = await service.getWorkflowStatus('workflow-1', {
				identity: 'token-123',
				version: 1 as const,
				metadata: {},
			});

			expect(result).toHaveLength(1);
			expect(result[0]).toEqual({
				credentialId: 'cred-1',
				resolverId: 'resolver-1',
				credentialName: 'OAuth2 API',
				status: 'missing',
				credentialType: 'oauth2Api',
			});
		});

		it('should use credential-level resolver when present', async () => {
			const mockWorkflow = createMockWorkflow({
				nodes: [
					createMockNode({
						credentials: {
							oauth2Api: { id: 'cred-1', name: 'OAuth2 API' },
						},
					}),
				],
				settings: { credentialResolverId: 'resolver-1' },
			});

			const mockCredential = createMockCredential({
				id: 'cred-1',
				type: 'oauth2Api',
				isResolvable: true,
				resolverId: 'resolver-2',
			});

			const mockResolver2 = createMockResolver({
				id: 'resolver-2',
				type: 'test.resolver',
				config: 'encrypted-config-2',
			});

			mockWorkflowRepository.get.mockResolvedValue(mockWorkflow);
			mockCredentialRepository.find.mockResolvedValue([mockCredential]);
			// Only the credential's own resolver is fetched; the unused workflow resolver is not.
			mockResolverRepository.findOneBy.mockResolvedValue(mockResolver2);
			mockResolverRegistry.getResolverByTypename.mockReturnValue(mockResolverImplementation);
			mockCipher.decryptV2.mockResolvedValue(JSON.stringify({ prefix: 'test-2' }));
			mockResolverImplementation.getSecret.mockResolvedValue('secret-value' as any);

			const credentialContext = {
				identity: 'token-123',
				version: 1 as const,
				metadata: {},
			};

			const result = await service.getWorkflowStatus('workflow-1', credentialContext);

			expect(result).toHaveLength(1);
			expect(result[0].resolverId).toBe('resolver-2');
			expect(mockResolverImplementation.getSecret).toHaveBeenCalledWith(
				'cred-1',
				credentialContext,
				{
					configuration: { prefix: 'test-2' },
					resolverName: 'test.resolver',
					resolverId: 'resolver-2',
				},
			);
		});

		it('should return resolver_missing status for credentials without resolver ID', async () => {
			const mockWorkflow = createMockWorkflow({
				nodes: [
					createMockNode({
						credentials: {
							oauth2Api: { id: 'cred-1', name: 'OAuth2 API' },
						},
					}),
				],
				settings: {},
			});

			const mockCredential = createMockCredential({
				id: 'cred-1',
				type: 'oauth2Api',
				name: 'OAuth2 API',
				isResolvable: true,
				resolverId: null,
			});

			mockWorkflowRepository.get.mockResolvedValue(mockWorkflow);
			mockCredentialRepository.find.mockResolvedValue([mockCredential]);

			const result = await service.getWorkflowStatus('workflow-1', {
				identity: 'token-123',
				version: 1 as const,
				metadata: {},
			});

			expect(result).toHaveLength(1);
			expect(result[0]).toEqual({
				credentialId: 'cred-1',
				credentialName: 'OAuth2 API',
				status: 'resolver_missing',
				credentialType: 'oauth2Api',
			});
			expect(mockResolverImplementation.getSecret).not.toHaveBeenCalled();
		});

		it('should handle multiple credentials in parallel', async () => {
			const mockWorkflow = createMockWorkflow({
				nodes: [
					createMockNode({
						credentials: {
							oauth2Api: { id: 'cred-1', name: 'OAuth2 API' },
							httpBasicAuth: { id: 'cred-2', name: 'Basic Auth' },
						},
					}),
				],
				settings: { credentialResolverId: 'resolver-1' },
			});

			const mockCredentials = [
				createMockCredential({
					id: 'cred-1',
					type: 'oauth2Api',
					isResolvable: true,
					resolverId: null,
				}),
				createMockCredential({
					id: 'cred-2',
					type: 'httpBasicAuth',
					isResolvable: true,
					resolverId: null,
				}),
			];

			const mockResolver = createMockResolver({
				id: 'resolver-1',
				type: 'test.resolver',
				config: 'encrypted-config',
			});

			mockWorkflowRepository.get.mockResolvedValue(mockWorkflow);
			mockCredentialRepository.find.mockResolvedValue(mockCredentials);
			mockResolverRepository.findOneBy.mockResolvedValue(mockResolver);
			mockResolverRegistry.getResolverByTypename.mockReturnValue(mockResolverImplementation);
			mockCipher.decryptV2.mockResolvedValue(JSON.stringify({ prefix: 'test' }));
			mockResolverImplementation.getSecret
				.mockResolvedValueOnce('secret-1' as any)
				.mockResolvedValueOnce('secret-2' as any);

			const result = await service.getWorkflowStatus('workflow-1', {
				identity: 'token-123',
				version: 1 as const,
				metadata: {},
			});

			expect(result).toHaveLength(2);
			expect(mockResolverImplementation.getSecret).toHaveBeenCalledTimes(2);
			expect(result[0].status).toBe('configured');
			expect(result[1].status).toBe('configured');
		});

		it('should handle mixed success and failure statuses', async () => {
			const mockWorkflow = createMockWorkflow({
				nodes: [
					createMockNode({
						credentials: {
							oauth2Api: { id: 'cred-1', name: 'OAuth2 API' },
							httpBasicAuth: { id: 'cred-2', name: 'Basic Auth' },
						},
					}),
				],
				settings: { credentialResolverId: 'resolver-1' },
			});

			const mockCredentials = [
				createMockCredential({
					id: 'cred-1',
					type: 'oauth2Api',
					isResolvable: true,
					resolverId: null,
				}),
				createMockCredential({
					id: 'cred-2',
					type: 'httpBasicAuth',
					isResolvable: true,
					resolverId: null,
				}),
			];

			const mockResolver = createMockResolver({
				id: 'resolver-1',
				type: 'test.resolver',
				config: 'encrypted-config',
			});

			mockWorkflowRepository.get.mockResolvedValue(mockWorkflow);
			mockCredentialRepository.find.mockResolvedValue(mockCredentials);
			mockResolverRepository.findOneBy.mockResolvedValue(mockResolver);
			mockResolverRegistry.getResolverByTypename.mockReturnValue(mockResolverImplementation);
			mockCipher.decryptV2.mockResolvedValue(JSON.stringify({ prefix: 'test' }));
			mockResolverImplementation.getSecret
				.mockResolvedValueOnce('secret-1' as any)
				.mockRejectedValueOnce(new Error('Secret not found'));

			const result = await service.getWorkflowStatus('workflow-1', {
				identity: 'token-123',
				version: 1 as const,
				metadata: {},
			});

			expect(result).toHaveLength(2);
			expect(result[0].status).toBe('configured');
			expect(result[1].status).toBe('missing');
		});

		describe('sub-workflows', () => {
			const credentialContext = {
				identity: 'token-123',
				version: 1 as const,
				metadata: {},
			};

			const createExecuteWorkflowNode = (subWorkflowId: unknown, overrides: Partial<INode> = {}) =>
				createMockNode({
					id: 'exec-node',
					name: 'Execute Workflow',
					type: 'n8n-nodes-base.executeWorkflow',
					parameters: { source: 'database', workflowId: subWorkflowId as NodeParameterValueType },
					...overrides,
				});

			const nodeWithCredential = (credentialId: string, overrides: Partial<INode> = {}) =>
				createMockNode({
					credentials: { oauth2Api: { id: credentialId, name: credentialId } },
					...overrides,
				});

			// Resolve credentials by the ids requested via the In() operator so dedup is exercised.
			const mockFindReturning = (credentials: CredentialsEntity[]) => {
				mockCredentialRepository.find.mockImplementation(async (options) => {
					const requestedIds = (options?.where as { id?: { value?: string[] } })?.id?.value ?? [];
					return credentials.filter((c) => requestedIds.includes(c.id) && c.isResolvable);
				});
			};

			const mockWorkflowsById = (workflows: Record<string, WorkflowEntity | null>) => {
				mockWorkflowRepository.get.mockImplementation(async ({ id }: { id: string }) =>
					workflows[id] === undefined ? null : workflows[id],
				);
			};

			beforeEach(() => {
				mockResolverRepository.findOneBy.mockResolvedValue(
					createMockResolver({ id: 'resolver-1', type: 'test.resolver' }),
				);
				mockResolverRegistry.getResolverByTypename.mockReturnValue(mockResolverImplementation);
				mockCipher.decryptV2.mockResolvedValue(JSON.stringify({ prefix: 'test' }));
				mockResolverImplementation.getSecret.mockResolvedValue('secret' as any);
			});

			it('collects resolvable credentials from referenced sub-workflows', async () => {
				mockWorkflowsById({
					'workflow-1': createMockWorkflow({
						id: 'workflow-1',
						nodes: [nodeWithCredential('cred-root'), createExecuteWorkflowNode({ value: 'sub-1' })],
						settings: { credentialResolverId: 'resolver-1' },
					}),
					'sub-1': createMockWorkflow({
						id: 'sub-1',
						nodes: [nodeWithCredential('cred-sub')],
						settings: { credentialResolverId: 'resolver-1' },
					}),
				});
				mockFindReturning([
					createMockCredential({ id: 'cred-root' }),
					createMockCredential({ id: 'cred-sub' }),
				]);

				const result = await service.getWorkflowStatus('workflow-1', credentialContext);

				expect(result.map((s) => s.credentialId).sort()).toEqual(['cred-root', 'cred-sub']);
			});

			it('follows AI Workflow Tool (toolWorkflow) references', async () => {
				mockWorkflowsById({
					'workflow-1': createMockWorkflow({
						id: 'workflow-1',
						nodes: [
							createExecuteWorkflowNode(
								{ value: 'sub-1' },
								{ type: '@n8n/n8n-nodes-langchain.toolWorkflow' },
							),
						],
						settings: { credentialResolverId: 'resolver-1' },
					}),
					'sub-1': createMockWorkflow({
						id: 'sub-1',
						nodes: [nodeWithCredential('cred-sub')],
						settings: { credentialResolverId: 'resolver-1' },
					}),
				});
				mockFindReturning([createMockCredential({ id: 'cred-sub' })]);

				const result = await service.getWorkflowStatus('workflow-1', credentialContext);

				expect(result.map((s) => s.credentialId)).toEqual(['cred-sub']);
			});

			it('follows Workflow Retriever (retrieverWorkflow) references', async () => {
				mockWorkflowsById({
					'workflow-1': createMockWorkflow({
						id: 'workflow-1',
						nodes: [
							createExecuteWorkflowNode(
								{ value: 'sub-1' },
								{ type: '@n8n/n8n-nodes-langchain.retrieverWorkflow' },
							),
						],
						settings: { credentialResolverId: 'resolver-1' },
					}),
					'sub-1': createMockWorkflow({
						id: 'sub-1',
						nodes: [nodeWithCredential('cred-sub')],
						settings: { credentialResolverId: 'resolver-1' },
					}),
				});
				mockFindReturning([createMockCredential({ id: 'cred-sub' })]);

				const result = await service.getWorkflowStatus('workflow-1', credentialContext);

				expect(result.map((s) => s.credentialId)).toEqual(['cred-sub']);
			});

			it('handles cycles and self-references without re-processing workflows', async () => {
				mockWorkflowsById({
					'workflow-1': createMockWorkflow({
						id: 'workflow-1',
						nodes: [
							nodeWithCredential('cred-root'),
							createExecuteWorkflowNode({ value: 'workflow-1' }, { id: 'self-ref' }),
							createExecuteWorkflowNode({ value: 'sub-1' }, { id: 'to-sub' }),
						],
						settings: { credentialResolverId: 'resolver-1' },
					}),
					'sub-1': createMockWorkflow({
						id: 'sub-1',
						nodes: [
							nodeWithCredential('cred-sub'),
							createExecuteWorkflowNode({ value: 'workflow-1' }, { id: 'back-ref' }),
						],
						settings: { credentialResolverId: 'resolver-1' },
					}),
				});
				mockFindReturning([
					createMockCredential({ id: 'cred-root' }),
					createMockCredential({ id: 'cred-sub' }),
				]);

				const result = await service.getWorkflowStatus('workflow-1', credentialContext);

				expect(result.map((s) => s.credentialId).sort()).toEqual(['cred-root', 'cred-sub']);
				// Each workflow is loaded exactly once despite the cycle.
				expect(mockWorkflowRepository.get).toHaveBeenCalledTimes(2);
			});

			it('deduplicates a credential shared between parent and sub-workflow', async () => {
				mockWorkflowsById({
					'workflow-1': createMockWorkflow({
						id: 'workflow-1',
						nodes: [
							nodeWithCredential('cred-shared'),
							createExecuteWorkflowNode({ value: 'sub-1' }),
						],
						settings: { credentialResolverId: 'resolver-1' },
					}),
					'sub-1': createMockWorkflow({
						id: 'sub-1',
						nodes: [nodeWithCredential('cred-shared')],
						settings: { credentialResolverId: 'resolver-1' },
					}),
				});
				mockFindReturning([createMockCredential({ id: 'cred-shared' })]);

				const result = await service.getWorkflowStatus('workflow-1', credentialContext);

				expect(result).toHaveLength(1);
				expect(result[0].credentialId).toBe('cred-shared');
			});

			it('deduplicates a credential used by multiple nodes in the same workflow', async () => {
				mockWorkflowsById({
					'workflow-1': createMockWorkflow({
						id: 'workflow-1',
						nodes: [
							nodeWithCredential('cred-shared', { id: 'node-a', name: 'A' }),
							nodeWithCredential('cred-shared', { id: 'node-b', name: 'B' }),
						],
						settings: { credentialResolverId: 'resolver-1' },
					}),
				});
				mockFindReturning([createMockCredential({ id: 'cred-shared' })]);

				const result = await service.getWorkflowStatus('workflow-1', credentialContext);

				expect(result).toHaveLength(1);
				expect(result[0].credentialId).toBe('cred-shared');
			});

			it('throws when the sub-workflow tree exceeds the traversal limit', async () => {
				const subCount = 150;
				const workflows: Record<string, WorkflowEntity | null> = {
					'workflow-1': createMockWorkflow({
						id: 'workflow-1',
						nodes: Array.from({ length: subCount }, (_, i) =>
							createExecuteWorkflowNode({ value: `sub-${i}` }, { id: `exec-${i}` }),
						),
						settings: {},
					}),
				};
				for (let i = 0; i < subCount; i++) {
					workflows[`sub-${i}`] = createMockWorkflow({ id: `sub-${i}`, nodes: [], settings: {} });
				}
				mockWorkflowsById(workflows);
				mockFindReturning([]);

				await expect(service.getWorkflowStatus('workflow-1', credentialContext)).rejects.toThrow(
					/too many sub-workflows/i,
				);
			});

			it('skips credentials on disabled nodes', async () => {
				mockWorkflowsById({
					'workflow-1': createMockWorkflow({
						id: 'workflow-1',
						nodes: [
							nodeWithCredential('cred-active'),
							nodeWithCredential('cred-disabled', { id: 'disabled-node', disabled: true }),
						],
						settings: { credentialResolverId: 'resolver-1' },
					}),
				});
				mockFindReturning([
					createMockCredential({ id: 'cred-active' }),
					createMockCredential({ id: 'cred-disabled' }),
				]);

				const result = await service.getWorkflowStatus('workflow-1', credentialContext);

				expect(result.map((s) => s.credentialId)).toEqual(['cred-active']);
			});

			it('returns no credentials when every account belongs to a disabled node', async () => {
				mockWorkflowsById({
					'workflow-1': createMockWorkflow({
						id: 'workflow-1',
						nodes: [nodeWithCredential('cred-disabled', { id: 'disabled-node', disabled: true })],
						settings: { credentialResolverId: 'resolver-1' },
					}),
				});
				mockFindReturning([createMockCredential({ id: 'cred-disabled' })]);

				const result = await service.getWorkflowStatus('workflow-1', credentialContext);

				expect(result).toEqual([]);
				expect(mockCredentialRepository.find).not.toHaveBeenCalled();
			});

			it('still includes a credential that also appears on an enabled node', async () => {
				mockWorkflowsById({
					'workflow-1': createMockWorkflow({
						id: 'workflow-1',
						nodes: [
							nodeWithCredential('cred-shared', { id: 'enabled-node' }),
							nodeWithCredential('cred-shared', { id: 'disabled-node', disabled: true }),
						],
						settings: { credentialResolverId: 'resolver-1' },
					}),
				});
				mockFindReturning([createMockCredential({ id: 'cred-shared' })]);

				const result = await service.getWorkflowStatus('workflow-1', credentialContext);

				expect(result).toHaveLength(1);
				expect(result[0].credentialId).toBe('cred-shared');
			});

			it('skips disabled sub-workflow nodes', async () => {
				mockWorkflowsById({
					'workflow-1': createMockWorkflow({
						id: 'workflow-1',
						nodes: [
							nodeWithCredential('cred-root'),
							createExecuteWorkflowNode({ value: 'sub-1' }, { disabled: true }),
						],
						settings: { credentialResolverId: 'resolver-1' },
					}),
					'sub-1': createMockWorkflow({
						id: 'sub-1',
						nodes: [nodeWithCredential('cred-sub')],
						settings: { credentialResolverId: 'resolver-1' },
					}),
				});
				mockFindReturning([
					createMockCredential({ id: 'cred-root' }),
					createMockCredential({ id: 'cred-sub' }),
				]);

				const result = await service.getWorkflowStatus('workflow-1', credentialContext);

				expect(result.map((s) => s.credentialId)).toEqual(['cred-root']);
				expect(mockWorkflowRepository.get).not.toHaveBeenCalledWith({ id: 'sub-1' });
			});

			it('skips non-database sub-workflow sources', async () => {
				mockWorkflowsById({
					'workflow-1': createMockWorkflow({
						id: 'workflow-1',
						nodes: [
							nodeWithCredential('cred-root'),
							createExecuteWorkflowNode(undefined, { parameters: { source: 'parameter' } }),
						],
						settings: { credentialResolverId: 'resolver-1' },
					}),
				});
				mockFindReturning([createMockCredential({ id: 'cred-root' })]);

				const result = await service.getWorkflowStatus('workflow-1', credentialContext);

				expect(result.map((s) => s.credentialId)).toEqual(['cred-root']);
				expect(mockWorkflowRepository.get).toHaveBeenCalledTimes(1);
			});

			it('ignores expression-based sub-workflow ids that cannot be resolved statically', async () => {
				mockWorkflowsById({
					'workflow-1': createMockWorkflow({
						id: 'workflow-1',
						nodes: [
							nodeWithCredential('cred-root'),
							createExecuteWorkflowNode({ value: '={{ $json.workflowId }}' }),
						],
						settings: { credentialResolverId: 'resolver-1' },
					}),
				});
				mockFindReturning([createMockCredential({ id: 'cred-root' })]);

				const result = await service.getWorkflowStatus('workflow-1', credentialContext);

				expect(result.map((s) => s.credentialId)).toEqual(['cred-root']);
				expect(mockWorkflowRepository.get).toHaveBeenCalledTimes(1);
			});

			it('tolerates a deleted sub-workflow without failing the check', async () => {
				mockWorkflowsById({
					'workflow-1': createMockWorkflow({
						id: 'workflow-1',
						nodes: [
							nodeWithCredential('cred-root'),
							createExecuteWorkflowNode({ value: 'missing' }),
						],
						settings: { credentialResolverId: 'resolver-1' },
					}),
					missing: null,
				});
				mockFindReturning([createMockCredential({ id: 'cred-root' })]);

				const result = await service.getWorkflowStatus('workflow-1', credentialContext);

				expect(result.map((s) => s.credentialId)).toEqual(['cred-root']);
			});

			it('uses each sub-workflow own effective resolver as the fallback', async () => {
				mockWorkflowsById({
					'workflow-1': createMockWorkflow({
						id: 'workflow-1',
						nodes: [createExecuteWorkflowNode({ value: 'sub-1' })],
						settings: { credentialResolverId: 'resolver-1' },
					}),
					'sub-1': createMockWorkflow({
						id: 'sub-1',
						nodes: [nodeWithCredential('cred-sub')],
						settings: { credentialResolverId: 'resolver-2' },
					}),
				});
				mockFindReturning([createMockCredential({ id: 'cred-sub', resolverId: null })]);
				mockResolverRepository.findOneBy.mockResolvedValue(
					createMockResolver({ id: 'resolver-2', type: 'test.resolver' }),
				);

				const result = await service.getWorkflowStatus('workflow-1', credentialContext);

				expect(result).toHaveLength(1);
				expect(result[0].resolverId).toBe('resolver-2');
			});

			it('checks a shared resolver-less credential under each workflow effective resolver', async () => {
				// Same credential (no own resolverId) used in a parent and a sub-workflow that
				// override the resolver differently: it must be checked under both, since at
				// execution time each workflow resolves it with its own effective resolver.
				mockWorkflowsById({
					'workflow-1': createMockWorkflow({
						id: 'workflow-1',
						nodes: [
							nodeWithCredential('cred-shared'),
							createExecuteWorkflowNode({ value: 'sub-1' }),
						],
						settings: { credentialResolverId: 'resolver-1' },
					}),
					'sub-1': createMockWorkflow({
						id: 'sub-1',
						nodes: [nodeWithCredential('cred-shared')],
						settings: { credentialResolverId: 'resolver-2' },
					}),
				});
				mockFindReturning([createMockCredential({ id: 'cred-shared', resolverId: null })]);
				mockResolverRepository.findOneBy.mockImplementation(async ({ id }: { id: string }) =>
					createMockResolver({ id, type: 'test.resolver' }),
				);
				// Configured under resolver-1, missing under resolver-2.
				mockResolverImplementation.getSecret.mockImplementation(async (_id, _ctx, opts) => {
					if (opts.resolverId === 'resolver-2') {
						throw new Error('Secret not found');
					}
					return 'secret' as any;
				});

				const result = await service.getWorkflowStatus('workflow-1', credentialContext);

				expect(result).toHaveLength(2);
				expect(result).toEqual(
					expect.arrayContaining([
						expect.objectContaining({
							credentialId: 'cred-shared',
							resolverId: 'resolver-1',
							status: 'configured',
						}),
						expect.objectContaining({
							credentialId: 'cred-shared',
							resolverId: 'resolver-2',
							status: 'missing',
						}),
					]),
				);
			});

			it('enforces user access on the root but resolves sub-workflows by id', async () => {
				const user = { id: 'user-1' } as unknown as Parameters<typeof service.getWorkflowStatus>[2];
				mockWorkflowFinderService.findWorkflowForUser.mockResolvedValue(
					createMockWorkflow({
						id: 'workflow-1',
						nodes: [nodeWithCredential('cred-root'), createExecuteWorkflowNode({ value: 'sub-1' })],
						settings: { credentialResolverId: 'resolver-1' },
					}),
				);
				mockWorkflowsById({
					'sub-1': createMockWorkflow({
						id: 'sub-1',
						nodes: [nodeWithCredential('cred-sub')],
						settings: { credentialResolverId: 'resolver-1' },
					}),
				});
				mockFindReturning([
					createMockCredential({ id: 'cred-root' }),
					createMockCredential({ id: 'cred-sub' }),
				]);

				const result = await service.getWorkflowStatus('workflow-1', credentialContext, user);

				expect(result.map((s) => s.credentialId).sort()).toEqual(['cred-root', 'cred-sub']);
				expect(mockWorkflowFinderService.findWorkflowForUser).toHaveBeenCalledWith(
					'workflow-1',
					user,
					['workflow:read'],
				);
				// Sub-workflow loaded by id, not through the user-scoped finder.
				expect(mockWorkflowRepository.get).toHaveBeenCalledWith({ id: 'sub-1' });
				expect(mockWorkflowFinderService.findWorkflowForUser).toHaveBeenCalledTimes(1);
			});
		});
	});
});
