/* eslint-disable @typescript-eslint/unbound-method */
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
import { Logger } from '@n8n/backend-common';
import { mockInstance } from '@n8n/backend-test-utils';
import type { WorkflowsConfig } from '@n8n/config';
import { WorkflowDependencyRepository, WorkflowEntity, WorkflowRepository } from '@n8n/db';
import { mock } from 'jest-mock-extended';
import type { Span } from 'n8n-core';
import { ErrorReporter, Tracing } from 'n8n-core';
import type { INode, IWorkflowBase } from 'n8n-workflow';

import { EventService } from '@/events/event.service';

import { WorkflowIndexService } from '../workflow-index.service';

describe('WorkflowIndexService', () => {
	let service: WorkflowIndexService;
	const mockWorkflowDependencyRepository = mockInstance(WorkflowDependencyRepository);
	const mockWorkflowRepository = mockInstance(WorkflowRepository);
	const mockLogger = mockInstance(Logger);
	const mockErrorReporter = mockInstance(ErrorReporter);
	const mockEventService = mockInstance(EventService);
	const mockTracing = mockInstance(Tracing);

	const createWorkflowsConfig = (overrides: Partial<WorkflowsConfig> = {}) =>
		mock<WorkflowsConfig>({ indexingBatchSize: 10, ...overrides });

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

		mockTracing.startSpan.mockImplementation(async (_opts, spanCb) => await spanCb(mock<Span>()));

		service = new WorkflowIndexService(
			mockWorkflowDependencyRepository,
			mockWorkflowRepository,
			mockEventService,
			mockLogger,
			mockErrorReporter,
			mockTracing,
			createWorkflowsConfig(),
		);
	});

	const createNode = (overrides: Partial<INode> & { id: string; type: string }): INode => {
		const { parameters, ...rest } = overrides;
		return {
			name: overrides.id,
			typeVersion: 1,
			position: [0, 0],
			parameters: parameters ?? {},
			...rest,
		};
	};

	const createWorkflow = (nodes: INode[]): IWorkflowBase => ({
		id: 'workflow-123',
		name: 'Test Workflow',
		active: true,
		activeVersionId: 'some-version-id',
		isArchived: false,
		createdAt: new Date(),
		updatedAt: new Date(),
		versionCounter: 1,
		nodes,
		connections: {},
	});

	const createWorkflowEntity = (nodes: INode[]): WorkflowEntity => {
		const workflow = createWorkflow(nodes);
		return Object.assign(new WorkflowEntity(), workflow);
	};

	describe('updateIndexFor()', () => {
		it('should extract all dependency types correctly', async () => {
			mockWorkflowDependencyRepository.updateDependenciesForWorkflow.mockResolvedValue(true);

			const workflow = createWorkflow([
				createNode({
					id: 'node-1',
					type: 'n8n-nodes-base.webhook',
					parameters: { path: 'webhook-1' },
				}),
				createNode({
					id: 'node-2',
					type: 'n8n-nodes-base.httpRequest',
					credentials: {
						httpAuth: { id: 'cred-1', name: 'Auth 1' },
						apiKey: { id: 'cred-2', name: 'Auth 2' },
					},
				}),
				createNode({
					id: 'node-3',
					type: 'n8n-nodes-base.executeWorkflow',
					parameters: { workflowId: 'sub-workflow-1' },
				}),
				createNode({
					id: 'node-4',
					type: 'n8n-nodes-base.executeWorkflow',
					parameters: { workflowId: { value: 'sub-workflow-2' } },
				}),
			]);

			await service.updateIndexForDraft(workflow);

			expect(mockWorkflowDependencyRepository.updateDependenciesForWorkflow).toHaveBeenCalledWith(
				'workflow-123',
				expect.objectContaining({
					dependencies: expect.arrayContaining([
						// nodeType dependencies
						expect.objectContaining({
							dependencyType: 'nodeType',
							dependencyKey: 'n8n-nodes-base.webhook',
							dependencyInfo: { nodeId: 'node-1', nodeVersion: 1 },
						}),
						expect.objectContaining({
							dependencyType: 'nodeType',
							dependencyKey: 'n8n-nodes-base.httpRequest',
							dependencyInfo: { nodeId: 'node-2', nodeVersion: 1 },
						}),
						expect.objectContaining({
							dependencyType: 'nodeType',
							dependencyKey: 'n8n-nodes-base.executeWorkflow',
							dependencyInfo: { nodeId: 'node-3', nodeVersion: 1 },
						}),
						expect.objectContaining({
							dependencyType: 'nodeType',
							dependencyKey: 'n8n-nodes-base.executeWorkflow',
							dependencyInfo: { nodeId: 'node-4', nodeVersion: 1 },
						}),
						// webhookPath dependencies
						expect.objectContaining({
							dependencyType: 'webhookPath',
							dependencyKey: 'webhook-1',
							dependencyInfo: { nodeId: 'node-1', nodeVersion: 1 },
						}),
						// credentialId dependencies
						expect.objectContaining({
							dependencyType: 'credentialId',
							dependencyKey: 'cred-1',
							dependencyInfo: { nodeId: 'node-2', nodeVersion: 1 },
						}),
						expect.objectContaining({
							dependencyType: 'credentialId',
							dependencyKey: 'cred-2',
							dependencyInfo: { nodeId: 'node-2', nodeVersion: 1 },
						}),
						// workflowCall dependencies (both string and object format)
						expect.objectContaining({
							dependencyType: 'workflowCall',
							dependencyKey: 'sub-workflow-1',
							dependencyInfo: { nodeId: 'node-3', nodeVersion: 1 },
						}),
						expect.objectContaining({
							dependencyType: 'workflowCall',
							dependencyKey: 'sub-workflow-2',
							dependencyInfo: { nodeId: 'node-4', nodeVersion: 1 },
						}),
					]),
				}),
			);
		});

		it('should handle repository errors gracefully', async () => {
			const error = new Error('Database error');
			mockWorkflowDependencyRepository.updateDependenciesForWorkflow.mockRejectedValue(error);

			const workflow = createWorkflow([
				createNode({
					id: 'node-1',
					type: 'n8n-nodes-base.manualTrigger',
				}),
			]);

			await service.updateIndexForDraft(workflow);

			expect(mockLogger.error).toHaveBeenCalledWith(
				'Failed to update workflow draft dependency index for workflow workflow-123: Database error',
			);
			expect(mockErrorReporter.error).toHaveBeenCalledWith(error);
		});

		it('should not create workflowCall dependencies for parameter, localFile, and url sources', async () => {
			mockWorkflowDependencyRepository.updateDependenciesForWorkflow.mockResolvedValue(true);

			const workflow = createWorkflow([
				createNode({
					id: 'node-1',
					type: 'n8n-nodes-base.executeWorkflow',
					parameters: { source: 'parameter' },
				}),
				createNode({
					id: 'node-2',
					type: 'n8n-nodes-base.executeWorkflow',
					parameters: { source: 'localFile' },
				}),
				createNode({
					id: 'node-3',
					type: 'n8n-nodes-base.executeWorkflow',
					parameters: { source: 'url' },
				}),
			]);

			await service.updateIndexForDraft(workflow);

			expect(mockWorkflowDependencyRepository.updateDependenciesForWorkflow).toHaveBeenCalledWith(
				'workflow-123',
				expect.objectContaining({
					dependencies: expect.not.arrayContaining([
						expect.objectContaining({
							dependencyType: 'workflowCall',
						}),
					]),
				}),
			);
		});

		it('should not create workflowCall dependency when executeWorkflow node has no workflowId parameter', async () => {
			mockWorkflowDependencyRepository.updateDependenciesForWorkflow.mockResolvedValue(true);

			const workflow = createWorkflow([
				createNode({
					id: 'node-1',
					type: 'n8n-nodes-base.executeWorkflow',
					parameters: {}, // No workflowId — node was just added to canvas
				}),
			]);

			await service.updateIndexForDraft(workflow);

			expect(mockWorkflowDependencyRepository.updateDependenciesForWorkflow).toHaveBeenCalledWith(
				'workflow-123',
				expect.objectContaining({
					dependencies: expect.not.arrayContaining([
						expect.objectContaining({
							dependencyType: 'workflowCall',
						}),
					]),
				}),
			);
		});

		it('should handle multiple credentials on a single node', async () => {
			mockWorkflowDependencyRepository.updateDependenciesForWorkflow.mockResolvedValue(true);

			const workflow = createWorkflow([
				createNode({
					id: 'node-1',
					type: 'n8n-nodes-base.httpRequest',
					credentials: {
						httpAuth: { id: 'cred-1', name: 'Basic Auth' },
						apiKey: { id: 'cred-2', name: 'API Key' },
						oAuth2: { id: 'cred-3', name: 'OAuth2' },
					},
				}),
			]);

			await service.updateIndexForDraft(workflow);

			expect(mockWorkflowDependencyRepository.updateDependenciesForWorkflow).toHaveBeenCalledWith(
				'workflow-123',
				expect.objectContaining({
					dependencies: expect.arrayContaining([
						expect.objectContaining({
							dependencyType: 'credentialId',
							dependencyKey: 'cred-1',
							dependencyInfo: { nodeId: 'node-1', nodeVersion: 1 },
						}),
						expect.objectContaining({
							dependencyType: 'credentialId',
							dependencyKey: 'cred-2',
							dependencyInfo: { nodeId: 'node-1', nodeVersion: 1 },
						}),
						expect.objectContaining({
							dependencyType: 'credentialId',
							dependencyKey: 'cred-3',
							dependencyInfo: { nodeId: 'node-1', nodeVersion: 1 },
						}),
					]),
				}),
			);
		});

		it('should skip credentials with null or empty id', async () => {
			mockWorkflowDependencyRepository.updateDependenciesForWorkflow.mockResolvedValue(true);

			const workflow = createWorkflow([
				createNode({
					id: 'node-1',
					type: 'n8n-nodes-base.httpRequest',
					credentials: {
						httpAuth: { id: 'cred-1', name: 'Valid Auth' },
						apiKey: { id: null, name: 'Invalid API Key' },
						oAuth2: { id: '', name: 'Empty OAuth2' },
					},
				}),
			]);

			await service.updateIndexForDraft(workflow);

			const call = mockWorkflowDependencyRepository.updateDependenciesForWorkflow.mock.calls[0];
			const dependencies = call[1].dependencies;

			// Should have nodeType + only 1 credential (cred-1)
			expect(dependencies).toHaveLength(2);
			expect(dependencies).toEqual(
				expect.arrayContaining([
					expect.objectContaining({
						dependencyType: 'nodeType',
						dependencyKey: 'n8n-nodes-base.httpRequest',
						dependencyInfo: { nodeId: 'node-1', nodeVersion: 1 },
					}),
					expect.objectContaining({
						dependencyType: 'credentialId',
						dependencyKey: 'cred-1',
						dependencyInfo: { nodeId: 'node-1', nodeVersion: 1 },
					}),
				]),
			);
		});

		it('should skip nodes with missing type', async () => {
			mockWorkflowDependencyRepository.updateDependenciesForWorkflow.mockResolvedValue(true);

			const workflow = createWorkflow([
				createNode({
					id: 'node-1',
					type: 'n8n-nodes-base.httpRequest',
				}),
				{
					id: 'node-2',
					name: 'node-2',
					typeVersion: 1,
					position: [0, 0] as [number, number],
					parameters: {},
				} as INode,
			]);

			await service.updateIndexForDraft(workflow);

			const call = mockWorkflowDependencyRepository.updateDependenciesForWorkflow.mock.calls[0];
			const dependencies = call[1].dependencies;

			expect(dependencies).toHaveLength(1);
			expect(dependencies[0]).toEqual(
				expect.objectContaining({
					dependencyType: 'nodeType',
					dependencyKey: 'n8n-nodes-base.httpRequest',
					dependencyInfo: { nodeId: 'node-1', nodeVersion: 1 },
				}),
			);
		});

		it('should skip webhook nodes with missing path', async () => {
			mockWorkflowDependencyRepository.updateDependenciesForWorkflow.mockResolvedValue(true);

			const workflow = createWorkflow([
				createNode({
					id: 'node-1',
					type: 'n8n-nodes-base.webhook',
					parameters: { path: 'valid-path' },
				}),
				createNode({
					id: 'node-2',
					type: 'n8n-nodes-base.webhook',
					parameters: {},
				}),
			]);

			await service.updateIndexForDraft(workflow);

			const call = mockWorkflowDependencyRepository.updateDependenciesForWorkflow.mock.calls[0];
			const dependencies = call[1].dependencies;

			expect(dependencies).toHaveLength(3);
			expect(dependencies).toEqual(
				expect.arrayContaining([
					expect.objectContaining({
						dependencyType: 'nodeType',
						dependencyKey: 'n8n-nodes-base.webhook',
						dependencyInfo: { nodeId: 'node-1', nodeVersion: 1 },
					}),
					expect.objectContaining({
						dependencyType: 'nodeType',
						dependencyKey: 'n8n-nodes-base.webhook',
						dependencyInfo: { nodeId: 'node-2', nodeVersion: 1 },
					}),
					expect.objectContaining({
						dependencyType: 'webhookPath',
						dependencyKey: 'valid-path',
						dependencyInfo: { nodeId: 'node-1', nodeVersion: 1 },
					}),
				]),
			);
		});

		it('should extract dataTableId from all data-table-related node types', async () => {
			mockWorkflowDependencyRepository.updateDependenciesForWorkflow.mockResolvedValue(true);

			const workflow = createWorkflow([
				createNode({
					id: 'node-1',
					type: 'n8n-nodes-base.dataTable',
					parameters: { dataTableId: { mode: 'name', value: 'My Table 1' } },
				}),
				createNode({
					id: 'node-2',
					type: 'n8n-nodes-base.dataTableTool',
					parameters: { dataTableId: { mode: 'id', value: 'table-2' } },
				}),
				createNode({
					id: 'node-3',
					type: 'n8n-nodes-base.evaluationTrigger',
					parameters: { source: 'dataTable', dataTableId: { mode: 'list', value: 'table-3' } },
				}),
				createNode({
					id: 'node-4',
					type: 'n8n-nodes-base.evaluation',
					parameters: { source: 'dataTable', dataTableId: { mode: 'list', value: 'table-4' } },
				}),
			]);

			await service.updateIndexForDraft(workflow);

			const call = mockWorkflowDependencyRepository.updateDependenciesForWorkflow.mock.calls[0];
			const dataTableDeps = call[1].dependencies.filter(
				(d: { dependencyType: string }) => d.dependencyType === 'dataTableId',
			);

			expect(dataTableDeps).toHaveLength(4);
			expect(dataTableDeps).toEqual(
				expect.arrayContaining([
					expect.objectContaining({
						dependencyType: 'dataTableId',
						dependencyKey: 'My Table 1',
						dependencyInfo: expect.objectContaining({ nodeId: 'node-1', mode: 'name' }),
					}),
					expect.objectContaining({
						dependencyType: 'dataTableId',
						dependencyKey: 'table-2',
						dependencyInfo: expect.objectContaining({ nodeId: 'node-2', mode: 'id' }),
					}),
					expect.objectContaining({
						dependencyType: 'dataTableId',
						dependencyKey: 'table-3',
						dependencyInfo: expect.objectContaining({ nodeId: 'node-3', mode: 'list' }),
					}),
					expect.objectContaining({
						dependencyType: 'dataTableId',
						dependencyKey: 'table-4',
						dependencyInfo: expect.objectContaining({ nodeId: 'node-4', mode: 'list' }),
					}),
				]),
			);
		});

		it('should insert placeholder for workflow with no nodes', async () => {
			mockWorkflowDependencyRepository.updateDependenciesForWorkflow.mockResolvedValue(true);

			const workflow = createWorkflow([]);

			await service.updateIndexForDraft(workflow);

			expect(mockWorkflowDependencyRepository.updateDependenciesForWorkflow).toHaveBeenCalledWith(
				'workflow-123',
				expect.objectContaining({
					dependencies: expect.arrayContaining([
						expect.objectContaining({
							dependencyType: 'workflowIndexed',
							dependencyKey: '__INDEXED__',
							dependencyInfo: null,
						}),
					]),
				}),
			);

			// Verify only the placeholder exists (exactly 1 dependency)
			const call = mockWorkflowDependencyRepository.updateDependenciesForWorkflow.mock.calls[0];
			expect(call[1].dependencies).toHaveLength(1);
		});

		it('should insert placeholder for workflow with nodes but no extractable dependencies', async () => {
			mockWorkflowDependencyRepository.updateDependenciesForWorkflow.mockResolvedValue(true);

			// Node with empty type - no nodeType dependency will be created
			const workflow = createWorkflow([
				{
					id: 'node-1',
					name: 'EmptyNode',
					type: '', // Empty type - no dependency extracted
					typeVersion: 1,
					position: [0, 0] as [number, number],
					parameters: {},
				} as INode,
			]);

			await service.updateIndexForDraft(workflow);

			expect(mockWorkflowDependencyRepository.updateDependenciesForWorkflow).toHaveBeenCalledWith(
				'workflow-123',
				expect.objectContaining({
					dependencies: expect.arrayContaining([
						expect.objectContaining({
							dependencyType: 'workflowIndexed',
							dependencyKey: '__INDEXED__',
							dependencyInfo: null,
						}),
					]),
				}),
			);

			// Verify only the placeholder exists (exactly 1 dependency)
			const call = mockWorkflowDependencyRepository.updateDependenciesForWorkflow.mock.calls[0];
			expect(call[1].dependencies).toHaveLength(1);
		});

		it('should index published nodes (not draft nodes) when calling updateIndexForPublished', async () => {
			mockWorkflowDependencyRepository.updateDependenciesForWorkflow.mockResolvedValue(true);

			const draftNodes = [
				createNode({
					id: 'node-1',
					type: 'n8n-nodes-base.manualTrigger',
				}),
			];
			const publishedNodes = [
				createNode({
					id: 'node-2',
					type: 'n8n-nodes-base.httpRequest',
				}),
			];
			const workflow = createWorkflow(draftNodes);

			const publishedVersionId = 'published-version-123';
			await service.updateIndexForPublished(workflow, publishedVersionId, publishedNodes);

			expect(mockWorkflowDependencyRepository.updateDependenciesForWorkflow).toHaveBeenCalledWith(
				'workflow-123',
				expect.objectContaining({
					publishedVersionId: 'published-version-123',
					dependencies: expect.arrayContaining([
						expect.objectContaining({
							dependencyType: 'nodeType',
							dependencyKey: 'n8n-nodes-base.httpRequest',
						}),
					]),
				}),
			);
			// Should NOT contain the draft node type
			expect(mockWorkflowDependencyRepository.updateDependenciesForWorkflow).toHaveBeenCalledWith(
				'workflow-123',
				expect.objectContaining({
					dependencies: expect.not.arrayContaining([
						expect.objectContaining({
							dependencyKey: 'n8n-nodes-base.manualTrigger',
						}),
					]),
				}),
			);
		});

		it('should extract errorWorkflow dependency from workflow settings', async () => {
			mockWorkflowDependencyRepository.updateDependenciesForWorkflow.mockResolvedValue(true);

			const workflow = createWorkflow([
				createNode({ id: 'node-1', type: 'n8n-nodes-base.manualTrigger' }),
			]);
			workflow.settings = { errorWorkflow: 'error-wf-1' };

			await service.updateIndexForDraft(workflow);

			expect(mockWorkflowDependencyRepository.updateDependenciesForWorkflow).toHaveBeenCalledWith(
				'workflow-123',
				expect.objectContaining({
					dependencies: expect.arrayContaining([
						expect.objectContaining({
							dependencyType: 'errorWorkflow',
							dependencyKey: 'error-wf-1',
							dependencyInfo: null,
						}),
					]),
				}),
			);
		});

		it('should pass null publishedVersionId when calling updateIndexForDraft', async () => {
			mockWorkflowDependencyRepository.updateDependenciesForWorkflow.mockResolvedValue(true);

			const workflow = createWorkflow([
				createNode({
					id: 'node-1',
					type: 'n8n-nodes-base.manualTrigger',
				}),
			]);

			await service.updateIndexForDraft(workflow);

			expect(mockWorkflowDependencyRepository.updateDependenciesForWorkflow).toHaveBeenCalledWith(
				'workflow-123',
				expect.objectContaining({
					publishedVersionId: null,
				}),
			);
		});
	});

	describe('init()', () => {
		it('should register event listeners for workflow events', () => {
			service.init();

			expect(mockEventService.on).toHaveBeenCalledTimes(5);
			expect(mockEventService.on).toHaveBeenCalledWith('server-started', expect.any(Function));
			expect(mockEventService.on).toHaveBeenCalledWith('workflow-created', expect.any(Function));
			expect(mockEventService.on).toHaveBeenCalledWith('workflow-saved', expect.any(Function));
			expect(mockEventService.on).toHaveBeenCalledWith('workflow-deleted', expect.any(Function));
			expect(mockEventService.on).toHaveBeenCalledWith('workflow-activated', expect.any(Function));
		});
	});

	describe('buildIndex()', () => {
		it('should skip published index when workflow has activeVersionId but no activeVersion nodes', async () => {
			mockWorkflowRepository.findWorkflowsNeedingIndexing.mockResolvedValue([]);

			const workflow = createWorkflowEntity([
				createNode({ id: 'node-1', type: 'n8n-nodes-base.manualTrigger' }),
			]);
			workflow.activeVersionId = 'some-version-id';
			workflow.activeVersion = null;

			mockWorkflowRepository.findWorkflowsNeedingPublishedVersionIndexing
				.mockResolvedValueOnce([workflow])
				.mockResolvedValueOnce([]);

			mockWorkflowDependencyRepository.updateDependenciesForWorkflow.mockResolvedValue(true);

			await service.buildIndex();

			expect(mockWorkflowDependencyRepository.updateDependenciesForWorkflow).not.toHaveBeenCalled();
		});

		it('should retrieve unindexed workflows and update their dependencies', async () => {
			const workflow1 = createWorkflowEntity([
				createNode({ id: 'node-1', type: 'n8n-nodes-base.manualTrigger' }),
			]);
			const workflow2 = createWorkflowEntity([
				createNode({ id: 'node-2', type: 'n8n-nodes-base.webhook', parameters: { path: 'test' } }),
			]);
			workflow2.id = 'workflow-456';

			// Mock findWorkflowsNeedingIndexing to return workflows on first call, empty on second
			mockWorkflowRepository.findWorkflowsNeedingIndexing
				.mockResolvedValueOnce([workflow1, workflow2])
				.mockResolvedValueOnce([]);

			// Mock findWorkflowsNeedingPublishedVersionIndexing to return empty (no published versions needing indexing)
			mockWorkflowRepository.findWorkflowsNeedingPublishedVersionIndexing.mockResolvedValue([]);

			mockWorkflowDependencyRepository.updateDependenciesForWorkflow.mockResolvedValue(true);

			await service.buildIndex();

			// Verify findWorkflowsNeedingIndexing was called with correct pagination
			expect(mockWorkflowRepository.findWorkflowsNeedingIndexing).toHaveBeenCalledWith(10); // default batch size

			// Verify updateDependenciesForWorkflow was called for each workflow
			expect(mockWorkflowDependencyRepository.updateDependenciesForWorkflow).toHaveBeenCalledTimes(
				2,
			);
			expect(mockWorkflowDependencyRepository.updateDependenciesForWorkflow).toHaveBeenCalledWith(
				'workflow-123',
				expect.any(Object),
			);
			expect(mockWorkflowDependencyRepository.updateDependenciesForWorkflow).toHaveBeenCalledWith(
				'workflow-456',
				expect.any(Object),
			);
		});

		it('should process workflows in batches', async () => {
			// Create a service with a small batch size to test batching behavior
			const batchSize = 2;
			const serviceWithSmallBatch = new WorkflowIndexService(
				mockWorkflowDependencyRepository,
				mockWorkflowRepository,
				mockEventService,
				mockLogger,
				mockErrorReporter,
				mockTracing,
				createWorkflowsConfig({ indexingBatchSize: batchSize }),
			);

			// Create 5 workflows to test multiple batches
			const workflows = Array.from({ length: 5 }, (_, i) => {
				const workflow = createWorkflowEntity([
					createNode({ id: `node-${i}`, type: 'n8n-nodes-base.manualTrigger' }),
				]);
				workflow.id = `workflow-${i}`;
				return workflow;
			});

			// Mock findWorkflowsNeedingIndexing to return workflows in batches
			mockWorkflowRepository.findWorkflowsNeedingIndexing
				.mockResolvedValueOnce([workflows[0], workflows[1]]) // First batch: 2 workflows
				.mockResolvedValueOnce([workflows[2], workflows[3]]) // Second batch: 2 workflows
				.mockResolvedValueOnce([workflows[4]]); // Third batch: 1 workflow (partial, should stop)

			// Mock findWorkflowsNeedingPublishedVersionIndexing to return empty (no published versions needing indexing)
			mockWorkflowRepository.findWorkflowsNeedingPublishedVersionIndexing.mockResolvedValue([]);

			mockWorkflowDependencyRepository.updateDependenciesForWorkflow.mockResolvedValue(true);

			await serviceWithSmallBatch.buildIndex();

			// Verify findWorkflowsNeedingIndexing was called 3 times with correct pagination
			expect(mockWorkflowRepository.findWorkflowsNeedingIndexing).toHaveBeenCalledTimes(3);
			expect(mockWorkflowRepository.findWorkflowsNeedingIndexing).toHaveBeenNthCalledWith(
				1,
				batchSize,
			);
			expect(mockWorkflowRepository.findWorkflowsNeedingIndexing).toHaveBeenNthCalledWith(
				2,
				batchSize,
			);
			expect(mockWorkflowRepository.findWorkflowsNeedingIndexing).toHaveBeenNthCalledWith(
				3,
				batchSize,
			);

			// Verify all 5 workflows were processed
			expect(mockWorkflowDependencyRepository.updateDependenciesForWorkflow).toHaveBeenCalledTimes(
				5,
			);
			for (let i = 0; i < 5; i++) {
				expect(mockWorkflowDependencyRepository.updateDependenciesForWorkflow).toHaveBeenCalledWith(
					`workflow-${i}`,
					expect.any(Object),
				);
			}
		});
	});
});
