// src/services/receiptService.server.test.ts
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import type { Logger } from 'pino';
import type { Job } from 'bullmq';
import type { ReceiptJobData } from '../types/job-data';
import { createMockLogger } from '../tests/utils/mockLogger';
import type {
  ReceiptStatus,
  ReceiptItemStatus,
  ReceiptProcessingStep,
  ReceiptProcessingStatus,
  OcrProvider,
  ReceiptProcessingLogRecord,
} from '../types/expiry';

// Mock dependencies
vi.mock('./db/index.db', () => ({
  receiptRepo: {
    createReceipt: vi.fn(),
    getReceiptById: vi.fn(),
    getReceipts: vi.fn(),
    updateReceipt: vi.fn(),
    deleteReceipt: vi.fn(),
    logProcessingStep: vi.fn(),
    detectStoreFromText: vi.fn(),
    addReceiptItems: vi.fn(),
    incrementRetryCount: vi.fn(),
    getReceiptItems: vi.fn(),
    updateReceiptItem: vi.fn(),
    getUnaddedReceiptItems: vi.fn(),
    getProcessingLogs: vi.fn(),
    getProcessingStats: vi.fn(),
    getReceiptsNeedingProcessing: vi.fn(),
    addStorePattern: vi.fn(),
    getActiveStorePatterns: vi.fn(),
  },
}));

vi.mock('../config/env', () => ({
  isAiConfigured: false,
  config: {
    gemini: {
      apiKey: undefined,
    },
  },
}));

vi.mock('./aiService.server', () => ({
  aiService: {
    extractItemsFromReceiptImage: vi.fn(),
  },
}));

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

vi.mock('node:fs/promises', () => ({
  default: {
    access: vi.fn(),
  },
}));

// Import after mocks are set up
import {
  createReceipt,
  getReceiptById,
  getReceipts,
  deleteReceipt,
  processReceipt,
  getReceiptItems,
  updateReceiptItem,
  getUnaddedItems,
  getProcessingLogs,
  getProcessingStats,
  getReceiptsNeedingProcessing,
  addStorePattern,
  getActiveStorePatterns,
  processReceiptJob,
} from './receiptService.server';

import { receiptRepo } from './db/index.db';

// Helper to create mock processing log record
function createMockProcessingLogRecord(
  overrides: Partial<ReceiptProcessingLogRecord> = {},
): ReceiptProcessingLogRecord {
  return {
    log_id: 1,
    receipt_id: 1,
    processing_step: 'upload' as ReceiptProcessingStep,
    status: 'completed' as ReceiptProcessingStatus,
    provider: null,
    duration_ms: null,
    tokens_used: null,
    cost_cents: null,
    input_data: null,
    output_data: null,
    error_message: null,
    created_at: new Date().toISOString(),
    ...overrides,
  };
}

// Helper to create mock store pattern row
interface StoreReceiptPatternRow {
  pattern_id: number;
  store_id: number;
  pattern_type: string;
  pattern_value: string;
  priority: number;
  is_active: boolean;
  created_at: string;
  updated_at: string;
}

function createMockStorePatternRow(
  overrides: Partial<StoreReceiptPatternRow> = {},
): StoreReceiptPatternRow {
  return {
    pattern_id: 1,
    store_id: 1,
    pattern_type: 'name',
    pattern_value: 'WALMART',
    priority: 0,
    is_active: true,
    created_at: new Date().toISOString(),
    updated_at: new Date().toISOString(),
    ...overrides,
  };
}

describe('receiptService.server', () => {
  let mockLogger: Logger;

  beforeEach(() => {
    vi.clearAllMocks();
    mockLogger = createMockLogger();
  });

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

  describe('createReceipt', () => {
    it('should create a new receipt and log upload step', async () => {
      const mockReceipt = {
        receipt_id: 1,
        user_id: 'user-1',
        store_id: null,
        receipt_image_url: '/uploads/receipt.jpg',
        transaction_date: null,
        total_amount_cents: null,
        status: 'pending' as ReceiptStatus,
        raw_text: null,
        store_confidence: null,
        ocr_provider: null,
        error_details: null,
        retry_count: 0,
        ocr_confidence: null,
        currency: 'USD',
        created_at: new Date().toISOString(),
        processed_at: null,
        updated_at: new Date().toISOString(),
      };

      vi.mocked(receiptRepo.createReceipt).mockResolvedValueOnce(mockReceipt);
      vi.mocked(receiptRepo.logProcessingStep).mockResolvedValueOnce(
        createMockProcessingLogRecord(),
      );

      const result = await createReceipt('user-1', '/uploads/receipt.jpg', mockLogger);

      expect(result.receipt_id).toBe(1);
      expect(receiptRepo.createReceipt).toHaveBeenCalledWith(
        {
          user_id: 'user-1',
          receipt_image_url: '/uploads/receipt.jpg',
          store_id: undefined,
          transaction_date: undefined,
        },
        mockLogger,
      );
      expect(receiptRepo.logProcessingStep).toHaveBeenCalledWith(
        1,
        'upload',
        'completed',
        mockLogger,
        expect.any(Object),
      );
    });

    it('should create receipt with optional store ID and transaction date', async () => {
      const mockReceipt = {
        receipt_id: 2,
        user_id: 'user-1',
        store_id: 5,
        receipt_image_url: '/uploads/receipt2.jpg',
        transaction_date: '2024-01-15',
        total_amount_cents: null,
        status: 'pending' as ReceiptStatus,
        raw_text: null,
        store_confidence: null,
        ocr_provider: null,
        error_details: null,
        retry_count: 0,
        ocr_confidence: null,
        currency: 'USD',
        created_at: new Date().toISOString(),
        processed_at: null,
        updated_at: new Date().toISOString(),
      };

      vi.mocked(receiptRepo.createReceipt).mockResolvedValueOnce(mockReceipt);
      vi.mocked(receiptRepo.logProcessingStep).mockResolvedValueOnce(
        createMockProcessingLogRecord(),
      );

      const result = await createReceipt('user-1', '/uploads/receipt2.jpg', mockLogger, {
        storeId: 5,
        transactionDate: '2024-01-15',
      });

      expect(result.store_id).toBe(5);
      expect(result.transaction_date).toBe('2024-01-15');
    });
  });

  describe('getReceiptById', () => {
    it('should return receipt by ID', async () => {
      const mockReceipt = {
        receipt_id: 1,
        user_id: 'user-1',
        store_id: null,
        receipt_image_url: '/uploads/receipt.jpg',
        transaction_date: null,
        total_amount_cents: null,
        status: 'pending' as ReceiptStatus,
        raw_text: null,
        store_confidence: null,
        ocr_provider: null,
        error_details: null,
        retry_count: 0,
        ocr_confidence: null,
        currency: 'USD',
        created_at: new Date().toISOString(),
        processed_at: null,
        updated_at: new Date().toISOString(),
      };

      vi.mocked(receiptRepo.getReceiptById).mockResolvedValueOnce(mockReceipt);

      const result = await getReceiptById(1, 'user-1', mockLogger);

      expect(result.receipt_id).toBe(1);
      expect(receiptRepo.getReceiptById).toHaveBeenCalledWith(1, 'user-1', mockLogger);
    });
  });

  describe('getReceipts', () => {
    it('should return paginated receipts for user', async () => {
      const mockReceipts = {
        receipts: [
          {
            receipt_id: 1,
            user_id: 'user-1',
            store_id: null,
            receipt_image_url: '/uploads/receipt1.jpg',
            transaction_date: null,
            total_amount_cents: null,
            status: 'completed' as ReceiptStatus,
            raw_text: null,
            store_confidence: null,
            ocr_provider: null,
            error_details: null,
            retry_count: 0,
            ocr_confidence: null,
            currency: 'USD',
            created_at: new Date().toISOString(),
            processed_at: new Date().toISOString(),
            updated_at: new Date().toISOString(),
          },
        ],
        total: 1,
      };

      vi.mocked(receiptRepo.getReceipts).mockResolvedValueOnce(mockReceipts);

      const result = await getReceipts({ user_id: 'user-1', limit: 10, offset: 0 }, mockLogger);

      expect(result.receipts).toHaveLength(1);
      expect(result.total).toBe(1);
    });

    it('should filter by status', async () => {
      vi.mocked(receiptRepo.getReceipts).mockResolvedValueOnce({ receipts: [], total: 0 });

      await getReceipts({ user_id: 'user-1', status: 'completed' }, mockLogger);

      expect(receiptRepo.getReceipts).toHaveBeenCalledWith(
        { user_id: 'user-1', status: 'completed' },
        mockLogger,
      );
    });
  });

  describe('deleteReceipt', () => {
    it('should delete receipt', async () => {
      vi.mocked(receiptRepo.deleteReceipt).mockResolvedValueOnce(undefined);

      await deleteReceipt(1, 'user-1', mockLogger);

      expect(receiptRepo.deleteReceipt).toHaveBeenCalledWith(1, 'user-1', mockLogger);
    });
  });

  describe('processReceipt', () => {
    it('should process receipt and return items when AI not configured', async () => {
      const mockReceipt = {
        receipt_id: 1,
        user_id: 'user-1',
        store_id: null,
        receipt_image_url: '/uploads/receipt.jpg',
        transaction_date: null,
        total_amount_cents: null,
        status: 'pending' as ReceiptStatus,
        raw_text: null,
        store_confidence: null,
        ocr_provider: null,
        error_details: null,
        retry_count: 0,
        ocr_confidence: null,
        currency: 'USD',
        created_at: new Date().toISOString(),
        processed_at: null,
        updated_at: new Date().toISOString(),
      };

      const mockUpdatedReceipt = { ...mockReceipt, status: 'processing' };
      const mockCompletedReceipt = { ...mockReceipt, status: 'completed' };

      vi.mocked(receiptRepo.updateReceipt)
        .mockResolvedValueOnce(mockUpdatedReceipt as any) // status: processing
        .mockResolvedValueOnce({ ...mockUpdatedReceipt, raw_text: '[AI not configured]' } as any) // raw_text update
        .mockResolvedValueOnce(mockCompletedReceipt as any); // status: completed

      vi.mocked(receiptRepo.logProcessingStep).mockResolvedValue(createMockProcessingLogRecord());
      vi.mocked(receiptRepo.detectStoreFromText).mockResolvedValueOnce(null);
      vi.mocked(receiptRepo.addReceiptItems).mockResolvedValueOnce([]);

      const result = await processReceipt(1, mockLogger);

      expect(result.receipt.status).toBe('completed');
      expect(receiptRepo.updateReceipt).toHaveBeenCalledWith(
        1,
        { status: 'processing' },
        expect.any(Object),
      );
    });

    it('should detect store from receipt text', async () => {
      const mockReceipt = {
        receipt_id: 2,
        user_id: 'user-1',
        store_id: null,
        receipt_image_url: '/uploads/receipt.jpg',
        transaction_date: null,
        total_amount_cents: null,
        status: 'pending' as ReceiptStatus,
        raw_text: null,
        store_confidence: null,
        ocr_provider: null,
        error_details: null,
        retry_count: 0,
        ocr_confidence: null,
        currency: 'USD',
        created_at: new Date().toISOString(),
        processed_at: null,
        updated_at: new Date().toISOString(),
      };

      vi.mocked(receiptRepo.updateReceipt).mockResolvedValue({
        ...mockReceipt,
        status: 'completed' as ReceiptStatus,
      } as any);
      vi.mocked(receiptRepo.logProcessingStep).mockResolvedValue(createMockProcessingLogRecord());
      vi.mocked(receiptRepo.detectStoreFromText).mockResolvedValueOnce({
        store_id: 10,
        confidence: 0.9,
      });
      vi.mocked(receiptRepo.addReceiptItems).mockResolvedValueOnce([]);

      await processReceipt(2, mockLogger);

      expect(receiptRepo.updateReceipt).toHaveBeenCalledWith(
        2,
        expect.objectContaining({ store_id: 10, store_confidence: 0.9 }),
        expect.any(Object),
      );
    });

    it('should handle processing errors', async () => {
      vi.mocked(receiptRepo.updateReceipt).mockRejectedValueOnce(new Error('DB error'));
      vi.mocked(receiptRepo.incrementRetryCount).mockResolvedValueOnce(1);
      vi.mocked(receiptRepo.logProcessingStep).mockResolvedValue(createMockProcessingLogRecord());

      await expect(processReceipt(1, mockLogger)).rejects.toThrow('DB error');

      expect(receiptRepo.incrementRetryCount).toHaveBeenCalledWith(1, expect.any(Object));
    });
  });

  describe('getReceiptItems', () => {
    it('should return receipt items', async () => {
      const mockItems = [
        {
          receipt_item_id: 1,
          receipt_id: 1,
          raw_item_description: 'MILK 2%',
          quantity: 1,
          price_paid_cents: 399,
          master_item_id: null,
          product_id: null,
          status: 'unmatched' as ReceiptItemStatus,
          line_number: 1,
          match_confidence: null,
          is_discount: false,
          unit_price_cents: null,
          unit_type: null,
          added_to_pantry: false,
          pantry_item_id: null,
          upc_code: null,
          created_at: new Date().toISOString(),
          updated_at: new Date().toISOString(),
        },
      ];

      vi.mocked(receiptRepo.getReceiptItems).mockResolvedValueOnce(mockItems);

      const result = await getReceiptItems(1, mockLogger);

      expect(result).toHaveLength(1);
      expect(result[0].raw_item_description).toBe('MILK 2%');
    });
  });

  describe('updateReceiptItem', () => {
    it('should update receipt item', async () => {
      const mockUpdatedItem = {
        receipt_item_id: 1,
        receipt_id: 1,
        raw_item_description: 'MILK 2%',
        quantity: 2,
        price_paid_cents: 399,
        master_item_id: 5,
        product_id: null,
        status: 'matched' as ReceiptItemStatus,
        line_number: 1,
        match_confidence: 0.95,
        is_discount: false,
        unit_price_cents: null,
        unit_type: null,
        added_to_pantry: false,
        pantry_item_id: null,
        upc_code: null,
        created_at: new Date().toISOString(),
        updated_at: new Date().toISOString(),
      };

      vi.mocked(receiptRepo.updateReceiptItem).mockResolvedValueOnce(mockUpdatedItem);

      const result = await updateReceiptItem(
        1,
        { master_item_id: 5, status: 'matched' as ReceiptItemStatus, match_confidence: 0.95 },
        mockLogger,
      );

      expect(result.quantity).toBe(2);
      expect(result.master_item_id).toBe(5);
      expect(result.status).toBe('matched');
    });
  });

  describe('getUnaddedItems', () => {
    it('should return items not yet added to pantry', async () => {
      const mockItems = [
        {
          receipt_item_id: 1,
          receipt_id: 1,
          raw_item_description: 'BREAD',
          quantity: 1,
          price_paid_cents: 299,
          master_item_id: null,
          product_id: null,
          status: 'unmatched' as ReceiptItemStatus,
          line_number: 1,
          match_confidence: null,
          is_discount: false,
          unit_price_cents: null,
          unit_type: null,
          added_to_pantry: false,
          pantry_item_id: null,
          upc_code: null,
          created_at: new Date().toISOString(),
          updated_at: new Date().toISOString(),
        },
      ];

      vi.mocked(receiptRepo.getUnaddedReceiptItems).mockResolvedValueOnce(mockItems);

      const result = await getUnaddedItems(1, mockLogger);

      expect(result).toHaveLength(1);
      expect(result[0].added_to_pantry).toBe(false);
    });
  });

  describe('getProcessingLogs', () => {
    it('should return processing logs for receipt', async () => {
      const mockLogs = [
        {
          log_id: 1,
          receipt_id: 1,
          processing_step: 'upload' as ReceiptProcessingStep,
          status: 'completed' as ReceiptProcessingStatus,
          provider: 'internal' as OcrProvider,
          duration_ms: 50,
          tokens_used: null,
          cost_cents: null,
          input_data: null,
          output_data: null,
          error_message: null,
          created_at: new Date().toISOString(),
        },
      ];

      vi.mocked(receiptRepo.getProcessingLogs).mockResolvedValueOnce(mockLogs);

      const result = await getProcessingLogs(1, mockLogger);

      expect(result).toHaveLength(1);
      expect(result[0].processing_step).toBe('upload');
    });
  });

  describe('getProcessingStats', () => {
    it('should return processing statistics', async () => {
      const mockStats = {
        total_receipts: 100,
        completed: 85,
        failed: 10,
        pending: 5,
        avg_processing_time_ms: 2500,
        total_cost_cents: 0,
      };

      vi.mocked(receiptRepo.getProcessingStats).mockResolvedValueOnce(mockStats);

      const result = await getProcessingStats(mockLogger);

      expect(result.total_receipts).toBe(100);
      expect(result.completed).toBe(85);
    });

    it('should filter by date range', async () => {
      const mockStats = {
        total_receipts: 20,
        completed: 18,
        failed: 2,
        pending: 0,
        avg_processing_time_ms: 2000,
        total_cost_cents: 0,
      };

      vi.mocked(receiptRepo.getProcessingStats).mockResolvedValueOnce(mockStats);

      await getProcessingStats(mockLogger, {
        fromDate: '2024-01-01',
        toDate: '2024-01-31',
      });

      expect(receiptRepo.getProcessingStats).toHaveBeenCalledWith(mockLogger, {
        fromDate: '2024-01-01',
        toDate: '2024-01-31',
      });
    });
  });

  describe('getReceiptsNeedingProcessing', () => {
    it('should return pending receipts for processing', async () => {
      const mockReceipts = [
        {
          receipt_id: 1,
          user_id: 'user-1',
          store_id: null,
          receipt_image_url: '/uploads/receipt.jpg',
          transaction_date: null,
          total_amount_cents: null,
          status: 'pending' as ReceiptStatus,
          raw_text: null,
          store_confidence: null,
          ocr_provider: null,
          error_details: null,
          retry_count: 0,
          ocr_confidence: null,
          currency: 'USD',
          created_at: new Date().toISOString(),
          processed_at: null,
          updated_at: new Date().toISOString(),
        },
      ];

      vi.mocked(receiptRepo.getReceiptsNeedingProcessing).mockResolvedValueOnce(mockReceipts);

      const result = await getReceiptsNeedingProcessing(10, mockLogger);

      expect(result).toHaveLength(1);
      expect(result[0].status).toBe('pending');
    });
  });

  describe('addStorePattern', () => {
    it('should add store pattern', async () => {
      vi.mocked(receiptRepo.addStorePattern).mockResolvedValueOnce(createMockStorePatternRow());

      await addStorePattern(1, 'name', 'WALMART', mockLogger, { priority: 1 });

      expect(receiptRepo.addStorePattern).toHaveBeenCalledWith(1, 'name', 'WALMART', mockLogger, {
        priority: 1,
      });
    });
  });

  describe('getActiveStorePatterns', () => {
    it('should return active store patterns', async () => {
      const mockPatterns = [
        createMockStorePatternRow({
          pattern_id: 1,
          store_id: 1,
          pattern_type: 'name',
          pattern_value: 'WALMART',
        }),
      ];

      vi.mocked(receiptRepo.getActiveStorePatterns).mockResolvedValueOnce(mockPatterns);

      const result = await getActiveStorePatterns(mockLogger);

      expect(result).toHaveLength(1);
    });
  });

  describe('processReceiptJob', () => {
    it('should process receipt job successfully', async () => {
      const mockReceipt = {
        receipt_id: 1,
        user_id: 'user-1',
        store_id: null,
        receipt_image_url: '/uploads/receipt.jpg',
        transaction_date: null,
        total_amount_cents: null,
        status: 'pending' as ReceiptStatus,
        raw_text: null,
        store_confidence: null,
        ocr_provider: null,
        error_details: null,
        retry_count: 0,
        ocr_confidence: null,
        currency: 'USD',
        created_at: new Date().toISOString(),
        processed_at: null,
        updated_at: new Date().toISOString(),
      };

      vi.mocked(receiptRepo.getReceiptById).mockResolvedValueOnce(mockReceipt);
      vi.mocked(receiptRepo.updateReceipt).mockResolvedValue({
        ...mockReceipt,
        status: 'completed' as ReceiptStatus,
      } as any);
      vi.mocked(receiptRepo.logProcessingStep).mockResolvedValue(createMockProcessingLogRecord());
      vi.mocked(receiptRepo.detectStoreFromText).mockResolvedValueOnce(null);
      vi.mocked(receiptRepo.addReceiptItems).mockResolvedValueOnce([]);

      const mockJob = {
        id: 'job-1',
        data: {
          receiptId: 1,
          userId: 'user-1',
          meta: { requestId: 'req-1' },
        },
        attemptsMade: 0,
      } as Job<ReceiptJobData>;

      const result = await processReceiptJob(mockJob, mockLogger);

      expect(result.success).toBe(true);
      expect(result.receiptId).toBe(1);
    });

    it('should skip already completed receipts', async () => {
      const mockReceipt = {
        receipt_id: 1,
        user_id: 'user-1',
        store_id: null,
        receipt_image_url: '/uploads/receipt.jpg',
        transaction_date: null,
        total_amount_cents: null,
        status: 'completed' as ReceiptStatus,
        raw_text: 'Previous text',
        store_confidence: null,
        ocr_provider: null,
        error_details: null,
        retry_count: 0,
        ocr_confidence: null,
        currency: 'USD',
        created_at: new Date().toISOString(),
        processed_at: new Date().toISOString(),
        updated_at: new Date().toISOString(),
      };

      vi.mocked(receiptRepo.getReceiptById).mockResolvedValueOnce(mockReceipt);

      const mockJob = {
        id: 'job-2',
        data: {
          receiptId: 1,
          userId: 'user-1',
        },
        attemptsMade: 0,
      } as Job<ReceiptJobData>;

      const result = await processReceiptJob(mockJob, mockLogger);

      expect(result.success).toBe(true);
      expect(result.itemsFound).toBe(0);
      expect(receiptRepo.updateReceipt).not.toHaveBeenCalled();
    });

    it('should handle job processing errors', async () => {
      const mockReceipt = {
        receipt_id: 1,
        user_id: 'user-1',
        store_id: null,
        receipt_image_url: '/uploads/receipt.jpg',
        transaction_date: null,
        total_amount_cents: null,
        status: 'pending' as ReceiptStatus,
        raw_text: null,
        store_confidence: null,
        ocr_provider: null,
        error_details: null,
        retry_count: 0,
        ocr_confidence: null,
        currency: 'USD',
        created_at: new Date().toISOString(),
        processed_at: null,
        updated_at: new Date().toISOString(),
      };

      vi.mocked(receiptRepo.getReceiptById).mockResolvedValueOnce(mockReceipt);
      vi.mocked(receiptRepo.updateReceipt)
        .mockRejectedValueOnce(new Error('Processing failed'))
        .mockResolvedValueOnce({ ...mockReceipt, status: 'failed' } as any);
      vi.mocked(receiptRepo.incrementRetryCount).mockResolvedValueOnce(1);
      vi.mocked(receiptRepo.logProcessingStep).mockResolvedValue(createMockProcessingLogRecord());

      const mockJob = {
        id: 'job-3',
        data: {
          receiptId: 1,
          userId: 'user-1',
        },
        attemptsMade: 1,
      } as Job<ReceiptJobData>;

      await expect(processReceiptJob(mockJob, mockLogger)).rejects.toThrow('Processing failed');

      expect(receiptRepo.updateReceipt).toHaveBeenCalledWith(
        1,
        expect.objectContaining({ status: 'failed' }),
        expect.any(Object),
      );
    });

    it('should handle error when updating receipt status fails after processing error', async () => {
      const mockReceipt = {
        receipt_id: 1,
        user_id: 'user-1',
        store_id: null,
        receipt_image_url: '/uploads/receipt.jpg',
        transaction_date: null,
        total_amount_cents: null,
        status: 'pending' as ReceiptStatus,
        raw_text: null,
        store_confidence: null,
        ocr_provider: null,
        error_details: null,
        retry_count: 0,
        ocr_confidence: null,
        currency: 'USD',
        created_at: new Date().toISOString(),
        processed_at: null,
        updated_at: new Date().toISOString(),
      };

      // First call returns receipt, then processReceipt calls it internally
      vi.mocked(receiptRepo.getReceiptById).mockResolvedValueOnce(mockReceipt);

      // All updateReceipt calls fail
      vi.mocked(receiptRepo.updateReceipt).mockRejectedValue(new Error('Database unavailable'));

      vi.mocked(receiptRepo.incrementRetryCount).mockResolvedValueOnce(1);
      vi.mocked(receiptRepo.logProcessingStep).mockResolvedValue(createMockProcessingLogRecord());

      const mockJob = {
        id: 'job-4',
        data: {
          receiptId: 1,
          userId: 'user-1',
        },
        attemptsMade: 1,
      } as Job<ReceiptJobData>;

      // When all updateReceipt calls fail, the error is propagated
      await expect(processReceiptJob(mockJob, mockLogger)).rejects.toThrow('Database unavailable');
    });
  });

  // Test internal logic patterns used in the service
  describe('receipt text parsing patterns', () => {
    // These test the regex patterns and logic used in parseReceiptText

    it('should match price pattern at end of line', () => {
      const pricePattern = /\$?(\d+)\.(\d{2})\s*$/;

      expect('MILK 2% $4.99'.match(pricePattern)).toBeTruthy();
      expect('BREAD 2.49'.match(pricePattern)).toBeTruthy();
      expect('Item Name $12.00'.match(pricePattern)).toBeTruthy();
      expect('No price here'.match(pricePattern)).toBeNull();
    });

    it('should match quantity pattern', () => {
      const quantityPattern = /^(\d+)\s*[@xX]/;

      expect('2 @ $3.99 APPLES'.match(quantityPattern)?.[1]).toBe('2');
      expect('3x Bananas'.match(quantityPattern)?.[1]).toBe('3');
      expect('5X ITEM'.match(quantityPattern)?.[1]).toBe('5');
      expect('Regular Item'.match(quantityPattern)).toBeNull();
    });

    it('should identify discount lines', () => {
      const isDiscount = (line: string) =>
        line.includes('-') || line.toLowerCase().includes('discount');

      expect(isDiscount('COUPON DISCOUNT -$2.00')).toBe(true);
      expect(isDiscount('MEMBER DISCOUNT')).toBe(true);
      expect(isDiscount('-$1.50')).toBe(true);
      expect(isDiscount('Regular Item $4.99')).toBe(false);
    });
  });

  describe('receipt header/footer detection patterns', () => {
    // Test the isHeaderOrFooter logic
    const skipPatterns = [
      'thank you',
      'thanks for',
      'visit us',
      'total',
      'subtotal',
      'tax',
      'change',
      'cash',
      'credit',
      'debit',
      'visa',
      'mastercard',
      'approved',
      'transaction',
      'terminal',
      'receipt',
      'store #',
      'date:',
      'time:',
      'cashier',
    ];

    const isHeaderOrFooter = (line: string): boolean => {
      const lowercaseLine = line.toLowerCase();
      return skipPatterns.some((pattern) => lowercaseLine.includes(pattern));
    };

    it('should skip thank you lines', () => {
      expect(isHeaderOrFooter('THANK YOU FOR SHOPPING')).toBe(true);
      expect(isHeaderOrFooter('Thanks for visiting!')).toBe(true);
    });

    it('should skip total/subtotal lines', () => {
      expect(isHeaderOrFooter('SUBTOTAL $45.99')).toBe(true);
      expect(isHeaderOrFooter('TOTAL $49.99')).toBe(true);
      expect(isHeaderOrFooter('TAX $3.00')).toBe(true);
    });

    it('should skip payment method lines', () => {
      expect(isHeaderOrFooter('VISA **** 1234')).toBe(true);
      expect(isHeaderOrFooter('MASTERCARD APPROVED')).toBe(true);
      expect(isHeaderOrFooter('CASH TENDERED')).toBe(true);
      expect(isHeaderOrFooter('CREDIT CARD')).toBe(true);
      expect(isHeaderOrFooter('DEBIT $50.00')).toBe(true);
    });

    it('should skip store info lines', () => {
      expect(isHeaderOrFooter('Store #1234')).toBe(true);
      expect(isHeaderOrFooter('DATE: 01/15/2024')).toBe(true);
      expect(isHeaderOrFooter('TIME: 14:30')).toBe(true);
      expect(isHeaderOrFooter('Cashier: John')).toBe(true);
    });

    it('should allow regular item lines', () => {
      expect(isHeaderOrFooter('MILK 2% $4.99')).toBe(false);
      expect(isHeaderOrFooter('BREAD WHOLE WHEAT')).toBe(false);
      expect(isHeaderOrFooter('BANANAS 2.5LB')).toBe(false);
    });
  });

  describe('receipt metadata extraction patterns', () => {
    // Test the extractReceiptMetadata logic

    it('should extract total amount from different formats', () => {
      const totalPatterns = [
        /total[:\s]+\$?(\d+)\.(\d{2})/i,
        /grand total[:\s]+\$?(\d+)\.(\d{2})/i,
        /amount due[:\s]+\$?(\d+)\.(\d{2})/i,
      ];

      const extractTotal = (text: string): number | undefined => {
        for (const pattern of totalPatterns) {
          const match = text.match(pattern);
          if (match) {
            return parseInt(match[1], 10) * 100 + parseInt(match[2], 10);
          }
        }
        return undefined;
      };

      expect(extractTotal('TOTAL: $45.99')).toBe(4599);
      expect(extractTotal('Grand Total $123.00')).toBe(12300);
      expect(extractTotal('AMOUNT DUE: 78.50')).toBe(7850);
      expect(extractTotal('No total here')).toBeUndefined();
    });

    it('should extract date from MM/DD/YYYY format', () => {
      const datePattern = /(\d{1,2})\/(\d{1,2})\/(\d{2,4})/;

      const match1 = '01/15/2024'.match(datePattern);
      expect(match1?.[1]).toBe('01');
      expect(match1?.[2]).toBe('15');
      expect(match1?.[3]).toBe('2024');

      const match2 = '1/5/24'.match(datePattern);
      expect(match2?.[1]).toBe('1');
      expect(match2?.[2]).toBe('5');
      expect(match2?.[3]).toBe('24');
    });

    it('should extract date from YYYY-MM-DD format', () => {
      const datePattern = /(\d{4})-(\d{2})-(\d{2})/;

      const match = '2024-01-15'.match(datePattern);
      expect(match?.[1]).toBe('2024');
      expect(match?.[2]).toBe('01');
      expect(match?.[3]).toBe('15');
    });

    it('should convert 2-digit years to 4-digit years', () => {
      const convertYear = (year: number): number => {
        if (year < 100) {
          return year + 2000;
        }
        return year;
      };

      expect(convertYear(24)).toBe(2024);
      expect(convertYear(99)).toBe(2099);
      expect(convertYear(2024)).toBe(2024);
    });
  });

  describe('OCR extraction edge cases', () => {
    // These test the logic in performOcrExtraction

    it('should determine if URL is local path', () => {
      const isLocalPath = (url: string) => !url.startsWith('http');

      expect(isLocalPath('/uploads/receipt.jpg')).toBe(true);
      expect(isLocalPath('./images/receipt.png')).toBe(true);
      expect(isLocalPath('https://example.com/receipt.jpg')).toBe(false);
      expect(isLocalPath('http://localhost/receipt.jpg')).toBe(false);
    });

    it('should determine MIME type from extension', () => {
      const mimeTypeMap: Record<string, string> = {
        '.jpg': 'image/jpeg',
        '.jpeg': 'image/jpeg',
        '.png': 'image/png',
        '.gif': 'image/gif',
        '.webp': 'image/webp',
      };

      const getMimeType = (ext: string) => mimeTypeMap[ext] || 'image/jpeg';

      expect(getMimeType('.jpg')).toBe('image/jpeg');
      expect(getMimeType('.jpeg')).toBe('image/jpeg');
      expect(getMimeType('.png')).toBe('image/png');
      expect(getMimeType('.gif')).toBe('image/gif');
      expect(getMimeType('.webp')).toBe('image/webp');
      expect(getMimeType('.unknown')).toBe('image/jpeg');
    });

    it('should format extracted items as text', () => {
      const extractedItems = [
        { raw_item_description: 'MILK 2%', price_paid_cents: 499 },
        { raw_item_description: 'BREAD', price_paid_cents: 299 },
      ];

      const textLines = extractedItems.map(
        (item) => `${item.raw_item_description} - $${(item.price_paid_cents / 100).toFixed(2)}`,
      );

      expect(textLines).toEqual(['MILK 2% - $4.99', 'BREAD - $2.99']);
    });
  });
});
