import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import {
  DocumentationBatchProcessor,
  BatchProcessorOptions,
  BatchProcessorResult,
} from '@/community/documentation-batch-processor';
import type { NodeRepository } from '@/database/node-repository';
import type { CommunityNodeFetcher } from '@/community/community-node-fetcher';
import type { DocumentationGenerator, DocumentationResult } from '@/community/documentation-generator';

// Mock logger to suppress output during tests
vi.mock('@/utils/logger', () => ({
  logger: {
    info: vi.fn(),
    warn: vi.fn(),
    error: vi.fn(),
    debug: vi.fn(),
  },
}));

/**
 * Factory for creating mock community nodes
 */
function createMockCommunityNode(overrides: Partial<{
  nodeType: string;
  displayName: string;
  description: string;
  npmPackageName: string;
  npmReadme: string | null;
  aiDocumentationSummary: object | null;
  npmDownloads: number;
}> = {}) {
  return {
    nodeType: overrides.nodeType || 'n8n-nodes-test.testNode',
    displayName: overrides.displayName || 'Test Node',
    description: overrides.description || 'A test community node',
    npmPackageName: overrides.npmPackageName || 'n8n-nodes-test',
    npmReadme: overrides.npmReadme === undefined ? null : overrides.npmReadme,
    aiDocumentationSummary: overrides.aiDocumentationSummary || null,
    npmDownloads: overrides.npmDownloads || 1000,
  };
}

/**
 * Factory for creating mock documentation summaries
 */
function createMockDocumentationSummary(nodeType: string) {
  return {
    purpose: `Node ${nodeType} does something useful`,
    capabilities: ['capability1', 'capability2'],
    authentication: 'API key required',
    commonUseCases: ['use case 1'],
    limitations: [],
    relatedNodes: [],
  };
}

/**
 * Create mock NodeRepository
 */
function createMockRepository(): NodeRepository {
  return {
    getCommunityNodes: vi.fn().mockReturnValue([]),
    getCommunityNodesWithoutReadme: vi.fn().mockReturnValue([]),
    getCommunityNodesWithoutAISummary: vi.fn().mockReturnValue([]),
    updateNodeReadme: vi.fn(),
    updateNodeAISummary: vi.fn(),
    getDocumentationStats: vi.fn().mockReturnValue({
      total: 10,
      withReadme: 5,
      withAISummary: 3,
      needingReadme: 5,
      needingAISummary: 2,
    }),
  } as unknown as NodeRepository;
}

/**
 * Create mock CommunityNodeFetcher
 */
function createMockFetcher(): CommunityNodeFetcher {
  return {
    fetchReadmesBatch: vi.fn().mockResolvedValue(new Map()),
  } as unknown as CommunityNodeFetcher;
}

/**
 * Create mock DocumentationGenerator
 */
function createMockGenerator(): DocumentationGenerator {
  return {
    testConnection: vi.fn().mockResolvedValue({ success: true, message: 'Connected' }),
    generateBatch: vi.fn().mockResolvedValue([]),
    generateSummary: vi.fn(),
  } as unknown as DocumentationGenerator;
}

describe('DocumentationBatchProcessor', () => {
  let processor: DocumentationBatchProcessor;
  let mockRepository: ReturnType<typeof createMockRepository>;
  let mockFetcher: ReturnType<typeof createMockFetcher>;
  let mockGenerator: ReturnType<typeof createMockGenerator>;

  beforeEach(() => {
    vi.clearAllMocks();
    mockRepository = createMockRepository();
    mockFetcher = createMockFetcher();
    mockGenerator = createMockGenerator();
    processor = new DocumentationBatchProcessor(mockRepository, mockFetcher, mockGenerator);
  });

  afterEach(() => {
    vi.restoreAllMocks();
  });

  describe('constructor', () => {
    it('should create instance with all dependencies', () => {
      expect(processor).toBeDefined();
    });

    it('should use provided repository', () => {
      const customRepo = createMockRepository();
      const proc = new DocumentationBatchProcessor(customRepo);
      expect(proc).toBeDefined();
    });
  });

  describe('processAll - default options', () => {
    it('should process both READMEs and summaries with default options', async () => {
      const nodes = [
        createMockCommunityNode({ nodeType: 'node1', npmPackageName: 'pkg1' }),
        createMockCommunityNode({ nodeType: 'node2', npmPackageName: 'pkg2' }),
      ];

      vi.mocked(mockRepository.getCommunityNodes).mockReturnValue(nodes);
      vi.mocked(mockFetcher.fetchReadmesBatch).mockResolvedValue(
        new Map([
          ['pkg1', '# README for pkg1'],
          ['pkg2', '# README for pkg2'],
        ])
      );

      const nodesWithReadme = [
        createMockCommunityNode({ nodeType: 'node1', npmPackageName: 'pkg1', npmReadme: '# README' }),
      ];
      vi.mocked(mockRepository.getCommunityNodes).mockReturnValue(nodesWithReadme);
      vi.mocked(mockGenerator.generateBatch).mockResolvedValue([
        {
          nodeType: 'node1',
          summary: createMockDocumentationSummary('node1'),
        },
      ]);

      const result = await processor.processAll();

      expect(result).toBeDefined();
      expect(result.errors).toEqual([]);
      expect(result.durationSeconds).toBeGreaterThanOrEqual(0);
    });

    it('should return result with duration even when no nodes to process', async () => {
      vi.mocked(mockRepository.getCommunityNodes).mockReturnValue([]);

      const result = await processor.processAll();

      expect(result.readmesFetched).toBe(0);
      expect(result.readmesFailed).toBe(0);
      expect(result.summariesGenerated).toBe(0);
      expect(result.summariesFailed).toBe(0);
      expect(result.durationSeconds).toBeGreaterThanOrEqual(0);
    });

    it('should accumulate skipped counts from both phases', async () => {
      const result = await processor.processAll({
        skipExistingReadme: true,
        skipExistingSummary: true,
      });

      expect(result).toBeDefined();
      expect(typeof result.skipped).toBe('number');
    });
  });

  describe('processAll - readmeOnly option', () => {
    it('should skip AI generation when readmeOnly is true', async () => {
      const nodes = [
        createMockCommunityNode({ nodeType: 'node1', npmPackageName: 'pkg1' }),
      ];

      vi.mocked(mockRepository.getCommunityNodes).mockReturnValue(nodes);
      vi.mocked(mockFetcher.fetchReadmesBatch).mockResolvedValue(
        new Map([['pkg1', '# README content']])
      );

      const result = await processor.processAll({ readmeOnly: true });

      expect(mockGenerator.testConnection).not.toHaveBeenCalled();
      expect(mockGenerator.generateBatch).not.toHaveBeenCalled();
      expect(result.summariesGenerated).toBe(0);
      expect(result.summariesFailed).toBe(0);
    });

    it('should still fetch READMEs when readmeOnly is true', async () => {
      const nodes = [
        createMockCommunityNode({ nodeType: 'node1', npmPackageName: 'pkg1' }),
      ];

      vi.mocked(mockRepository.getCommunityNodes).mockReturnValue(nodes);
      vi.mocked(mockFetcher.fetchReadmesBatch).mockResolvedValue(
        new Map([['pkg1', '# README content']])
      );

      await processor.processAll({ readmeOnly: true });

      expect(mockFetcher.fetchReadmesBatch).toHaveBeenCalledTimes(1);
      expect(mockRepository.updateNodeReadme).toHaveBeenCalledWith('node1', '# README content');
    });
  });

  describe('processAll - summaryOnly option', () => {
    it('should skip README fetching when summaryOnly is true', async () => {
      const nodesWithReadme = [
        createMockCommunityNode({ nodeType: 'node1', npmReadme: '# Existing README' }),
      ];

      vi.mocked(mockRepository.getCommunityNodes).mockReturnValue(nodesWithReadme);
      vi.mocked(mockGenerator.generateBatch).mockResolvedValue([
        {
          nodeType: 'node1',
          summary: createMockDocumentationSummary('node1'),
        },
      ]);

      const result = await processor.processAll({ summaryOnly: true });

      expect(mockFetcher.fetchReadmesBatch).not.toHaveBeenCalled();
      expect(result.readmesFetched).toBe(0);
      expect(result.readmesFailed).toBe(0);
    });

    it('should still generate summaries when summaryOnly is true', async () => {
      const nodesWithReadme = [
        createMockCommunityNode({ nodeType: 'node1', npmReadme: '# README' }),
      ];

      vi.mocked(mockRepository.getCommunityNodes).mockReturnValue(nodesWithReadme);
      vi.mocked(mockGenerator.generateBatch).mockResolvedValue([
        {
          nodeType: 'node1',
          summary: createMockDocumentationSummary('node1'),
        },
      ]);

      await processor.processAll({ summaryOnly: true });

      expect(mockGenerator.testConnection).toHaveBeenCalled();
      expect(mockGenerator.generateBatch).toHaveBeenCalled();
    });
  });

  describe('processAll - skipExistingReadme option', () => {
    it('should use getCommunityNodesWithoutReadme when skipExistingReadme is true', async () => {
      const nodesWithoutReadme = [
        createMockCommunityNode({ nodeType: 'node1', npmPackageName: 'pkg1', npmReadme: null }),
      ];

      vi.mocked(mockRepository.getCommunityNodesWithoutReadme).mockReturnValue(nodesWithoutReadme);
      vi.mocked(mockFetcher.fetchReadmesBatch).mockResolvedValue(
        new Map([['pkg1', '# New README']])
      );

      await processor.processAll({ skipExistingReadme: true, readmeOnly: true });

      expect(mockRepository.getCommunityNodesWithoutReadme).toHaveBeenCalled();
      expect(mockRepository.getCommunityNodes).not.toHaveBeenCalled();
    });

    it('should use getCommunityNodes when skipExistingReadme is false', async () => {
      const allNodes = [
        createMockCommunityNode({ nodeType: 'node1', npmPackageName: 'pkg1', npmReadme: '# Old' }),
        createMockCommunityNode({ nodeType: 'node2', npmPackageName: 'pkg2', npmReadme: null }),
      ];

      vi.mocked(mockRepository.getCommunityNodes).mockReturnValue(allNodes);
      vi.mocked(mockFetcher.fetchReadmesBatch).mockResolvedValue(new Map());

      await processor.processAll({ skipExistingReadme: false, readmeOnly: true });

      expect(mockRepository.getCommunityNodes).toHaveBeenCalledWith({ orderBy: 'downloads' });
      expect(mockRepository.getCommunityNodesWithoutReadme).not.toHaveBeenCalled();
    });
  });

  describe('processAll - skipExistingSummary option', () => {
    it('should use getCommunityNodesWithoutAISummary when skipExistingSummary is true', async () => {
      const nodesWithoutSummary = [
        createMockCommunityNode({
          nodeType: 'node1',
          npmReadme: '# README',
          aiDocumentationSummary: null,
        }),
      ];

      vi.mocked(mockRepository.getCommunityNodesWithoutAISummary).mockReturnValue(nodesWithoutSummary);
      vi.mocked(mockGenerator.generateBatch).mockResolvedValue([
        { nodeType: 'node1', summary: createMockDocumentationSummary('node1') },
      ]);

      await processor.processAll({ skipExistingSummary: true, summaryOnly: true });

      expect(mockRepository.getCommunityNodesWithoutAISummary).toHaveBeenCalled();
    });

    it('should filter nodes by existing README when skipExistingSummary is false', async () => {
      const allNodes = [
        createMockCommunityNode({ nodeType: 'node1', npmReadme: '# README1' }),
        createMockCommunityNode({ nodeType: 'node2', npmReadme: '' }), // Empty README
        createMockCommunityNode({ nodeType: 'node3', npmReadme: null }), // No README
      ];

      vi.mocked(mockRepository.getCommunityNodes).mockReturnValue(allNodes);
      vi.mocked(mockGenerator.generateBatch).mockResolvedValue([
        { nodeType: 'node1', summary: createMockDocumentationSummary('node1') },
      ]);

      await processor.processAll({ skipExistingSummary: false, summaryOnly: true });

      // Should filter to only nodes with non-empty README
      expect(mockGenerator.generateBatch).toHaveBeenCalled();
      const callArgs = vi.mocked(mockGenerator.generateBatch).mock.calls[0];
      expect(callArgs[0]).toHaveLength(1);
      expect(callArgs[0][0].nodeType).toBe('node1');
    });
  });

  describe('processAll - limit option', () => {
    it('should limit number of nodes processed for READMEs', async () => {
      const manyNodes = Array.from({ length: 10 }, (_, i) =>
        createMockCommunityNode({
          nodeType: `node${i}`,
          npmPackageName: `pkg${i}`,
        })
      );

      vi.mocked(mockRepository.getCommunityNodes).mockReturnValue(manyNodes);
      vi.mocked(mockFetcher.fetchReadmesBatch).mockResolvedValue(new Map());

      await processor.processAll({ limit: 3, readmeOnly: true });

      expect(mockFetcher.fetchReadmesBatch).toHaveBeenCalled();
      const packageNames = vi.mocked(mockFetcher.fetchReadmesBatch).mock.calls[0][0];
      expect(packageNames).toHaveLength(3);
    });

    it('should limit number of nodes processed for summaries', async () => {
      const manyNodes = Array.from({ length: 10 }, (_, i) =>
        createMockCommunityNode({
          nodeType: `node${i}`,
          npmReadme: `# README ${i}`,
        })
      );

      vi.mocked(mockRepository.getCommunityNodes).mockReturnValue(manyNodes);
      vi.mocked(mockGenerator.generateBatch).mockResolvedValue([]);

      await processor.processAll({ limit: 5, summaryOnly: true });

      expect(mockGenerator.generateBatch).toHaveBeenCalled();
      const inputs = vi.mocked(mockGenerator.generateBatch).mock.calls[0][0];
      expect(inputs).toHaveLength(5);
    });
  });

  describe('fetchReadmes - progress tracking', () => {
    it('should call progress callback during README fetching', async () => {
      const nodes = [
        createMockCommunityNode({ nodeType: 'node1', npmPackageName: 'pkg1' }),
        createMockCommunityNode({ nodeType: 'node2', npmPackageName: 'pkg2' }),
      ];

      vi.mocked(mockRepository.getCommunityNodes).mockReturnValue(nodes);
      vi.mocked(mockFetcher.fetchReadmesBatch).mockImplementation(
        async (packageNames, progressCallback) => {
          if (progressCallback) {
            progressCallback('Fetching READMEs', 1, 2);
            progressCallback('Fetching READMEs', 2, 2);
          }
          return new Map([
            ['pkg1', '# README 1'],
            ['pkg2', '# README 2'],
          ]);
        }
      );

      const progressCallback = vi.fn();
      await processor.processAll({ readmeOnly: true, progressCallback });

      expect(mockFetcher.fetchReadmesBatch).toHaveBeenCalledWith(
        expect.any(Array),
        progressCallback,
        expect.any(Number)
      );
    });

    it('should pass concurrency option to fetchReadmesBatch', async () => {
      const nodes = [
        createMockCommunityNode({ nodeType: 'node1', npmPackageName: 'pkg1' }),
      ];

      vi.mocked(mockRepository.getCommunityNodes).mockReturnValue(nodes);
      vi.mocked(mockFetcher.fetchReadmesBatch).mockResolvedValue(new Map());

      await processor.processAll({ readmeOnly: true, readmeConcurrency: 10 });

      expect(mockFetcher.fetchReadmesBatch).toHaveBeenCalledWith(
        ['pkg1'],
        undefined,
        10
      );
    });

    it('should use default concurrency of 5 for README fetching', async () => {
      const nodes = [
        createMockCommunityNode({ nodeType: 'node1', npmPackageName: 'pkg1' }),
      ];

      vi.mocked(mockRepository.getCommunityNodes).mockReturnValue(nodes);
      vi.mocked(mockFetcher.fetchReadmesBatch).mockResolvedValue(new Map());

      await processor.processAll({ readmeOnly: true });

      expect(mockFetcher.fetchReadmesBatch).toHaveBeenCalledWith(
        ['pkg1'],
        undefined,
        5
      );
    });
  });

  describe('generateSummaries - LLM connection test failure', () => {
    it('should fail all summaries when LLM connection fails', async () => {
      const nodes = [
        createMockCommunityNode({ nodeType: 'node1', npmReadme: '# README 1' }),
        createMockCommunityNode({ nodeType: 'node2', npmReadme: '# README 2' }),
        createMockCommunityNode({ nodeType: 'node3', npmReadme: '# README 3' }),
      ];

      vi.mocked(mockRepository.getCommunityNodes).mockReturnValue(nodes);
      vi.mocked(mockGenerator.testConnection).mockResolvedValue({
        success: false,
        message: 'Connection refused: ECONNREFUSED',
      });

      const result = await processor.processAll({ summaryOnly: true });

      expect(result.summariesGenerated).toBe(0);
      expect(result.summariesFailed).toBe(3);
      expect(result.errors).toHaveLength(1);
      expect(result.errors[0]).toContain('LLM connection failed');
      expect(result.errors[0]).toContain('Connection refused');
    });

    it('should not call generateBatch when connection test fails', async () => {
      const nodes = [
        createMockCommunityNode({ nodeType: 'node1', npmReadme: '# README' }),
      ];

      vi.mocked(mockRepository.getCommunityNodes).mockReturnValue(nodes);
      vi.mocked(mockGenerator.testConnection).mockResolvedValue({
        success: false,
        message: 'Model not found',
      });

      await processor.processAll({ summaryOnly: true });

      expect(mockGenerator.generateBatch).not.toHaveBeenCalled();
    });

    it('should proceed with generation when connection test succeeds', async () => {
      const nodes = [
        createMockCommunityNode({ nodeType: 'node1', npmReadme: '# README' }),
      ];

      vi.mocked(mockRepository.getCommunityNodes).mockReturnValue(nodes);
      vi.mocked(mockGenerator.testConnection).mockResolvedValue({
        success: true,
        message: 'Connected to qwen3-4b',
      });
      vi.mocked(mockGenerator.generateBatch).mockResolvedValue([
        { nodeType: 'node1', summary: createMockDocumentationSummary('node1') },
      ]);

      const result = await processor.processAll({ summaryOnly: true });

      expect(mockGenerator.generateBatch).toHaveBeenCalled();
      expect(result.summariesGenerated).toBe(1);
    });
  });

  describe('getStats', () => {
    it('should return documentation statistics from repository', () => {
      const expectedStats = {
        total: 25,
        withReadme: 20,
        withAISummary: 15,
        needingReadme: 5,
        needingAISummary: 5,
      };

      vi.mocked(mockRepository.getDocumentationStats).mockReturnValue(expectedStats);

      const stats = processor.getStats();

      expect(stats).toEqual(expectedStats);
      expect(mockRepository.getDocumentationStats).toHaveBeenCalled();
    });

    it('should handle empty statistics', () => {
      const emptyStats = {
        total: 0,
        withReadme: 0,
        withAISummary: 0,
        needingReadme: 0,
        needingAISummary: 0,
      };

      vi.mocked(mockRepository.getDocumentationStats).mockReturnValue(emptyStats);

      const stats = processor.getStats();

      expect(stats.total).toBe(0);
      expect(stats.withReadme).toBe(0);
    });
  });

  describe('error handling', () => {
    it('should collect errors when README update fails', async () => {
      const nodes = [
        createMockCommunityNode({ nodeType: 'node1', npmPackageName: 'pkg1' }),
      ];

      vi.mocked(mockRepository.getCommunityNodes).mockReturnValue(nodes);
      vi.mocked(mockFetcher.fetchReadmesBatch).mockResolvedValue(
        new Map([['pkg1', '# README']])
      );
      vi.mocked(mockRepository.updateNodeReadme).mockImplementation(() => {
        throw new Error('Database write error');
      });

      const result = await processor.processAll({ readmeOnly: true });

      expect(result.readmesFetched).toBe(0);
      expect(result.readmesFailed).toBe(1);
      expect(result.errors).toHaveLength(1);
      expect(result.errors[0]).toContain('Failed to save README');
      expect(result.errors[0]).toContain('Database write error');
    });

    it('should collect errors when summary generation fails', async () => {
      const nodes = [
        createMockCommunityNode({ nodeType: 'node1', npmReadme: '# README' }),
      ];

      vi.mocked(mockRepository.getCommunityNodes).mockReturnValue(nodes);
      vi.mocked(mockGenerator.generateBatch).mockResolvedValue([
        {
          nodeType: 'node1',
          summary: createMockDocumentationSummary('node1'),
          error: 'LLM timeout',
        },
      ]);

      const result = await processor.processAll({ summaryOnly: true });

      expect(result.summariesGenerated).toBe(0);
      expect(result.summariesFailed).toBe(1);
      expect(result.errors).toContain('node1: LLM timeout');
    });

    it('should collect errors when summary storage fails', async () => {
      const nodes = [
        createMockCommunityNode({ nodeType: 'node1', npmReadme: '# README' }),
      ];

      vi.mocked(mockRepository.getCommunityNodes).mockReturnValue(nodes);
      vi.mocked(mockGenerator.generateBatch).mockResolvedValue([
        { nodeType: 'node1', summary: createMockDocumentationSummary('node1') },
      ]);
      vi.mocked(mockRepository.updateNodeAISummary).mockImplementation(() => {
        throw new Error('Database constraint violation');
      });

      const result = await processor.processAll({ summaryOnly: true });

      expect(result.summariesGenerated).toBe(0);
      expect(result.summariesFailed).toBe(1);
      expect(result.errors).toHaveLength(1);
      expect(result.errors[0]).toContain('Failed to save summary');
    });

    it('should handle batch processing exception gracefully', async () => {
      vi.mocked(mockRepository.getCommunityNodes).mockImplementation(() => {
        throw new Error('Database connection lost');
      });

      const result = await processor.processAll();

      expect(result.errors).toHaveLength(1);
      expect(result.errors[0]).toContain('Batch processing failed');
      expect(result.errors[0]).toContain('Database connection lost');
      expect(result.durationSeconds).toBeGreaterThanOrEqual(0);
    });

    it('should accumulate errors from both README and summary phases', async () => {
      const nodes = [
        createMockCommunityNode({ nodeType: 'node1', npmPackageName: 'pkg1' }),
      ];

      vi.mocked(mockRepository.getCommunityNodes).mockReturnValue(nodes);
      vi.mocked(mockFetcher.fetchReadmesBatch).mockResolvedValue(new Map());

      // First call for README phase returns nodes, subsequent calls for summary phase
      vi.mocked(mockRepository.getCommunityNodes)
        .mockReturnValueOnce(nodes)  // README phase
        .mockReturnValue([]);         // Summary phase (no nodes with README)

      const result = await processor.processAll();

      // Should complete without errors since no READMEs fetched means no summary phase
      expect(result.errors).toEqual([]);
    });
  });

  describe('README fetching edge cases', () => {
    it('should skip nodes without npmPackageName', async () => {
      const nodes = [
        createMockCommunityNode({ nodeType: 'node1', npmPackageName: 'pkg1' }),
        { ...createMockCommunityNode({ nodeType: 'node2' }), npmPackageName: undefined },
        { ...createMockCommunityNode({ nodeType: 'node3' }), npmPackageName: null },
      ];

      vi.mocked(mockRepository.getCommunityNodes).mockReturnValue(nodes as any);
      vi.mocked(mockFetcher.fetchReadmesBatch).mockResolvedValue(
        new Map([['pkg1', '# README']])
      );

      await processor.processAll({ readmeOnly: true });

      // Should only request README for pkg1
      expect(mockFetcher.fetchReadmesBatch).toHaveBeenCalledWith(
        ['pkg1'],
        undefined,
        5
      );
    });

    it('should handle failed README fetches (null in map)', async () => {
      const nodes = [
        createMockCommunityNode({ nodeType: 'node1', npmPackageName: 'pkg1' }),
        createMockCommunityNode({ nodeType: 'node2', npmPackageName: 'pkg2' }),
      ];

      vi.mocked(mockRepository.getCommunityNodes).mockReturnValue(nodes);
      vi.mocked(mockFetcher.fetchReadmesBatch).mockResolvedValue(
        new Map([
          ['pkg1', '# README'],
          ['pkg2', null], // Failed to fetch
        ])
      );

      const result = await processor.processAll({ readmeOnly: true });

      expect(result.readmesFetched).toBe(1);
      expect(result.readmesFailed).toBe(1);
      expect(mockRepository.updateNodeReadme).toHaveBeenCalledTimes(1);
    });

    it('should handle empty package name list', async () => {
      vi.mocked(mockRepository.getCommunityNodes).mockReturnValue([]);

      const result = await processor.processAll({ readmeOnly: true });

      expect(mockFetcher.fetchReadmesBatch).not.toHaveBeenCalled();
      expect(result.readmesFetched).toBe(0);
      expect(result.readmesFailed).toBe(0);
    });
  });

  describe('summary generation edge cases', () => {
    it('should skip nodes without README for summary generation', async () => {
      const nodes = [
        createMockCommunityNode({ nodeType: 'node1', npmReadme: '# README' }),
        createMockCommunityNode({ nodeType: 'node2', npmReadme: '' }),
        createMockCommunityNode({ nodeType: 'node3', npmReadme: null }),
      ];

      vi.mocked(mockRepository.getCommunityNodes).mockReturnValue(nodes);
      vi.mocked(mockGenerator.generateBatch).mockResolvedValue([
        { nodeType: 'node1', summary: createMockDocumentationSummary('node1') },
      ]);

      await processor.processAll({ summaryOnly: true });

      const inputs = vi.mocked(mockGenerator.generateBatch).mock.calls[0][0];
      expect(inputs).toHaveLength(1);
      expect(inputs[0].nodeType).toBe('node1');
    });

    it('should pass correct concurrency to generateBatch', async () => {
      const nodes = [
        createMockCommunityNode({ nodeType: 'node1', npmReadme: '# README' }),
      ];

      vi.mocked(mockRepository.getCommunityNodes).mockReturnValue(nodes);
      vi.mocked(mockGenerator.generateBatch).mockResolvedValue([]);

      await processor.processAll({ summaryOnly: true, llmConcurrency: 10 });

      expect(mockGenerator.generateBatch).toHaveBeenCalledWith(
        expect.any(Array),
        10,
        undefined
      );
    });

    it('should use default LLM concurrency of 3', async () => {
      const nodes = [
        createMockCommunityNode({ nodeType: 'node1', npmReadme: '# README' }),
      ];

      vi.mocked(mockRepository.getCommunityNodes).mockReturnValue(nodes);
      vi.mocked(mockGenerator.generateBatch).mockResolvedValue([]);

      await processor.processAll({ summaryOnly: true });

      expect(mockGenerator.generateBatch).toHaveBeenCalledWith(
        expect.any(Array),
        3,
        undefined
      );
    });

    it('should handle empty node list for summary generation', async () => {
      vi.mocked(mockRepository.getCommunityNodes).mockReturnValue([]);

      const result = await processor.processAll({ summaryOnly: true });

      expect(mockGenerator.testConnection).not.toHaveBeenCalled();
      expect(mockGenerator.generateBatch).not.toHaveBeenCalled();
      expect(result.summariesGenerated).toBe(0);
    });
  });

  describe('concurrency options', () => {
    it('should respect custom readmeConcurrency option', async () => {
      const nodes = [
        createMockCommunityNode({ nodeType: 'node1', npmPackageName: 'pkg1' }),
      ];

      vi.mocked(mockRepository.getCommunityNodes).mockReturnValue(nodes);
      vi.mocked(mockFetcher.fetchReadmesBatch).mockResolvedValue(new Map());

      await processor.processAll({ readmeOnly: true, readmeConcurrency: 1 });

      expect(mockFetcher.fetchReadmesBatch).toHaveBeenCalledWith(
        expect.any(Array),
        undefined,
        1
      );
    });

    it('should respect custom llmConcurrency option', async () => {
      const nodes = [
        createMockCommunityNode({ nodeType: 'node1', npmReadme: '# README' }),
      ];

      vi.mocked(mockRepository.getCommunityNodes).mockReturnValue(nodes);
      vi.mocked(mockGenerator.generateBatch).mockResolvedValue([]);

      await processor.processAll({ summaryOnly: true, llmConcurrency: 1 });

      expect(mockGenerator.generateBatch).toHaveBeenCalledWith(
        expect.any(Array),
        1,
        undefined
      );
    });
  });

  describe('progress callback propagation', () => {
    it('should pass progress callback to summary generation', async () => {
      const nodes = [
        createMockCommunityNode({ nodeType: 'node1', npmReadme: '# README' }),
      ];

      vi.mocked(mockRepository.getCommunityNodes).mockReturnValue(nodes);
      vi.mocked(mockGenerator.generateBatch).mockResolvedValue([]);

      const progressCallback = vi.fn();
      await processor.processAll({ summaryOnly: true, progressCallback });

      expect(mockGenerator.generateBatch).toHaveBeenCalledWith(
        expect.any(Array),
        expect.any(Number),
        progressCallback
      );
    });

    it('should pass progress callback to README fetching', async () => {
      const nodes = [
        createMockCommunityNode({ nodeType: 'node1', npmPackageName: 'pkg1' }),
      ];

      vi.mocked(mockRepository.getCommunityNodes).mockReturnValue(nodes);
      vi.mocked(mockFetcher.fetchReadmesBatch).mockResolvedValue(new Map());

      const progressCallback = vi.fn();
      await processor.processAll({ readmeOnly: true, progressCallback });

      expect(mockFetcher.fetchReadmesBatch).toHaveBeenCalledWith(
        expect.any(Array),
        progressCallback,
        expect.any(Number)
      );
    });
  });

  describe('documentation input preparation', () => {
    it('should prepare correct input for documentation generator', async () => {
      const nodes = [
        {
          nodeType: 'n8n-nodes-test.testNode',
          displayName: 'Test Node',
          description: 'A test node',
          npmPackageName: 'n8n-nodes-test',
          npmReadme: '# Test README\nThis is a test.',
        },
      ];

      vi.mocked(mockRepository.getCommunityNodes).mockReturnValue(nodes as any);
      vi.mocked(mockGenerator.generateBatch).mockResolvedValue([
        { nodeType: 'n8n-nodes-test.testNode', summary: createMockDocumentationSummary('test') },
      ]);

      await processor.processAll({ summaryOnly: true });

      const inputs = vi.mocked(mockGenerator.generateBatch).mock.calls[0][0];
      expect(inputs[0]).toEqual({
        nodeType: 'n8n-nodes-test.testNode',
        displayName: 'Test Node',
        description: 'A test node',
        readme: '# Test README\nThis is a test.',
        npmPackageName: 'n8n-nodes-test',
      });
    });

    it('should handle missing optional fields', async () => {
      const nodes = [
        {
          nodeType: 'node1',
          displayName: 'Node 1',
          npmReadme: '# README',
          // Missing description and npmPackageName
        },
      ];

      vi.mocked(mockRepository.getCommunityNodes).mockReturnValue(nodes as any);
      vi.mocked(mockGenerator.generateBatch).mockResolvedValue([]);

      await processor.processAll({ summaryOnly: true });

      const inputs = vi.mocked(mockGenerator.generateBatch).mock.calls[0][0];
      expect(inputs[0].description).toBeUndefined();
      expect(inputs[0].npmPackageName).toBeUndefined();
    });
  });
});
