// src/services/flyerAiProcessor.server.test.ts
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { FlyerAiProcessor } from './flyerAiProcessor.server';
import { AiDataValidationError } from './processingErrors';
import { logger } from './logger.server'; // Keep this import for the logger instance
import type { AIService } from './aiService.server';
import type { PersonalizationRepository } from './db/personalization.db';
import type { FlyerJobData } from '../types/job-data';

vi.mock('./logger.server', () => ({
  logger: {
    info: vi.fn(),
    error: vi.fn(),
    warn: vi.fn(),
    debug: vi.fn(),
    child: vi.fn().mockReturnThis(),
  },
}));

const createMockJobData = (data: Partial<FlyerJobData>): FlyerJobData => ({
  filePath: '/tmp/flyer.jpg',
  originalFileName: 'flyer.jpg',
  checksum: 'checksum-123',
  ...data,
});

describe('FlyerAiProcessor', () => {
  let service: FlyerAiProcessor;
  let mockAiService: AIService;
  let mockPersonalizationRepo: PersonalizationRepository;

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

    mockAiService = {
      extractCoreDataFromFlyerImage: vi.fn(),
    } as unknown as AIService;
    mockPersonalizationRepo = {
      getAllMasterItems: vi.fn().mockResolvedValue([]),
    } as unknown as PersonalizationRepository;

    service = new FlyerAiProcessor(mockAiService, mockPersonalizationRepo);
  });

  it('should call AI service and return validated data on success', async () => {
    const jobData = createMockJobData({});
    const mockAiResponse = {
      store_name: 'AI Store',
      valid_from: '2024-01-01',
      valid_to: '2024-01-07',
      store_address: '123 AI St',
      // FIX: Add an item to pass the new "must have items" quality check.
      items: [
        {
          item: 'Test Item',
          price_display: '$1.99',
          price_in_cents: 199,
          // ADDED to satisfy ExtractedFlyerItem type
          quantity: 'each',
          category_name: 'Grocery',
        },
      ],
    };
    vi.mocked(mockAiService.extractCoreDataFromFlyerImage).mockResolvedValue(mockAiResponse);

    const result = await service.extractAndValidateData([], jobData, logger);

    expect(mockAiService.extractCoreDataFromFlyerImage).toHaveBeenCalledTimes(1);
    expect(mockPersonalizationRepo.getAllMasterItems).toHaveBeenCalledTimes(1);
    expect(result.data).toEqual(mockAiResponse);
    expect(result.needsReview).toBe(false);
  });

  it('should throw AiDataValidationError if AI response has incorrect data structure', async () => {
    const jobData = createMockJobData({});
    // Mock AI to return a structurally invalid response (e.g., items is not an array)
    const invalidResponse = {
      store_name: 'Invalid Store',
      items: 'not-an-array',
      valid_from: null,
      valid_to: null,
      store_address: null,
    };
    vi.mocked(mockAiService.extractCoreDataFromFlyerImage).mockResolvedValue(invalidResponse as any);

    await expect(service.extractAndValidateData([], jobData, logger)).rejects.toThrow(
      AiDataValidationError,
    );
  });

  it('should pass validation even if store_name is missing', async () => {
    const jobData = createMockJobData({});
    const mockAiResponse = {
      store_name: null, // Missing store name
      items: [{ item: 'Test Item', price_display: '$1.99', price_in_cents: 199, quantity: 'each', category_name: 'Grocery' }],
      // ADDED to satisfy AiFlyerDataSchema
      valid_from: null,
      valid_to: null,
      store_address: null,
    };
    vi.mocked(mockAiService.extractCoreDataFromFlyerImage).mockResolvedValue(mockAiResponse as any);
    const { logger } = await import('./logger.server');

    const result = await service.extractAndValidateData([], jobData, logger);

    // It should not throw, but return the data and log a warning.
    expect(result.data).toEqual(mockAiResponse);
    expect(result.needsReview).toBe(true);
    expect(logger.warn).toHaveBeenCalledWith(expect.any(Object), expect.stringContaining('missing a store name. The transformer will use a fallback. Flagging for review.'));
  });

  it('should pass validation even if items array is empty', async () => {
    const jobData = createMockJobData({});
    const mockAiResponse = {
      store_name: 'Test Store',
      items: [], // Empty items array
      // ADDED to satisfy AiFlyerDataSchema
      valid_from: null,
      valid_to: null,
      store_address: null,
    };
    vi.mocked(mockAiService.extractCoreDataFromFlyerImage).mockResolvedValue(mockAiResponse);
    const { logger } = await import('./logger.server');

    const result = await service.extractAndValidateData([], jobData, logger);
    expect(result.data).toEqual(mockAiResponse);
    expect(result.needsReview).toBe(true);
    expect(logger.warn).toHaveBeenCalledWith(expect.any(Object), expect.stringContaining('contains no items. The flyer will be saved with an item_count of 0. Flagging for review.'));
  });

  describe('Batching Logic', () => {
    it('should process images in batches and merge the results correctly', async () => {
      // Arrange
      const jobData = createMockJobData({});
      // 5 images, with BATCH_SIZE = 4, should result in 2 batches.
      const imagePaths = [
        { path: 'page1.jpg', mimetype: 'image/jpeg' },
        { path: 'page2.jpg', mimetype: 'image/jpeg' },
        { path: 'page3.jpg', mimetype: 'image/jpeg' },
        { path: 'page4.jpg', mimetype: 'image/jpeg' },
        { path: 'page5.jpg', mimetype: 'image/jpeg' },
      ];

      const mockAiResponseBatch1 = {
        store_name: 'Batch 1 Store',
        valid_from: '2025-01-01',
        valid_to: '2025-01-07',
        store_address: '123 Batch St',
        items: [
          { item: 'Item A', price_display: '$1', price_in_cents: 100, quantity: '1', category_name: 'Cat A', master_item_id: 1 },
          { item: 'Item B', price_display: '$2', price_in_cents: 200, quantity: '1', category_name: 'Cat B', master_item_id: 2 },
        ],
      };

      const mockAiResponseBatch2 = {
        store_name: 'Batch 2 Store', // This should be ignored in the merge
        valid_from: null,
        valid_to: null,
        store_address: null,
        items: [
          { item: 'Item C', price_display: '$3', price_in_cents: 300, quantity: '1', category_name: 'Cat C', master_item_id: 3 },
        ],
      };

      // Mock the AI service to return different results for each batch call
      vi.mocked(mockAiService.extractCoreDataFromFlyerImage)
        .mockResolvedValueOnce(mockAiResponseBatch1)
        .mockResolvedValueOnce(mockAiResponseBatch2);

      // Act
      const result = await service.extractAndValidateData(imagePaths, jobData, logger);

      // Assert
      // 1. AI service was called twice (for 2 batches)
      expect(mockAiService.extractCoreDataFromFlyerImage).toHaveBeenCalledTimes(2);

      // 2. Check the arguments for each call
      expect(mockAiService.extractCoreDataFromFlyerImage).toHaveBeenNthCalledWith(1, imagePaths.slice(0, 4), [], undefined, undefined, logger);
      expect(mockAiService.extractCoreDataFromFlyerImage).toHaveBeenNthCalledWith(2, imagePaths.slice(4, 5), [], undefined, undefined, logger);

      // 3. Check the merged data
      expect(result.data.store_name).toBe('Batch 1 Store'); // Metadata from the first batch
      expect(result.data.valid_from).toBe('2025-01-01');
      expect(result.data.valid_to).toBe('2025-01-07');
      expect(result.data.store_address).toBe('123 Batch St');

      // 4. Check that items from both batches are merged
      expect(result.data.items).toHaveLength(3);
      expect(result.data.items).toEqual(expect.arrayContaining([
        expect.objectContaining({ item: 'Item A' }),
        expect.objectContaining({ item: 'Item B' }),
        expect.objectContaining({ item: 'Item C' }),
      ]));

      // 5. Check that the job is not flagged for review
      expect(result.needsReview).toBe(false);
    });

    it('should fill in missing metadata from subsequent batches', async () => {
      // Arrange
      const jobData = createMockJobData({});
      const imagePaths = [
        { path: 'page1.jpg', mimetype: 'image/jpeg' }, { path: 'page2.jpg', mimetype: 'image/jpeg' }, { path: 'page3.jpg', mimetype: 'image/jpeg' }, { path: 'page4.jpg', mimetype: 'image/jpeg' }, { path: 'page5.jpg', mimetype: 'image/jpeg' },
      ];

      const mockAiResponseBatch1 = { store_name: null, valid_from: '2025-01-01', valid_to: '2025-01-07', store_address: null, items: [{ item: 'Item A', price_display: '$1', price_in_cents: 100, quantity: '1', category_name: 'Cat A', master_item_id: 1 }] };
      const mockAiResponseBatch2 = { store_name: 'Batch 2 Store', valid_from: '2025-01-02', valid_to: null, store_address: '456 Subsequent St', items: [{ item: 'Item C', price_display: '$3', price_in_cents: 300, quantity: '1', category_name: 'Cat C', master_item_id: 3 }] };

      vi.mocked(mockAiService.extractCoreDataFromFlyerImage)
        .mockResolvedValueOnce(mockAiResponseBatch1)
        .mockResolvedValueOnce(mockAiResponseBatch2);

      // Act
      const result = await service.extractAndValidateData(imagePaths, jobData, logger);

      // Assert
      expect(result.data.store_name).toBe('Batch 2 Store'); // Filled from batch 2
      expect(result.data.valid_from).toBe('2025-01-01'); // Kept from batch 1
      expect(result.data.valid_to).toBe('2025-01-07'); // Kept from batch 1
      expect(result.data.store_address).toBe('456 Subsequent St'); // Filled from batch 2
      expect(result.data.items).toHaveLength(2);
    });
  });
});