// src/services/db/receipt.db.test.ts
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import type { Logger } from 'pino';
import { createMockLogger } from '../../tests/utils/mockLogger';
import { ReceiptRepository } from './receipt.db';
import { NotFoundError } from './errors.db';

// Create mock pool
const mockQuery = vi.fn();
const mockPool = {
  query: mockQuery,
};

describe('ReceiptRepository', () => {
  let repo: ReceiptRepository;
  let mockLogger: Logger;

  beforeEach(() => {
    vi.clearAllMocks();
    mockLogger = createMockLogger();
    repo = new ReceiptRepository(mockPool);
  });

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

  // ============================================================================
  // RECEIPTS
  // ============================================================================

  describe('createReceipt', () => {
    it('should create a new receipt successfully', async () => {
      const receiptRow = {
        receipt_id: 1,
        user_id: 'user-1',
        store_id: 5,
        receipt_image_url: '/uploads/receipts/receipt-1.jpg',
        transaction_date: '2024-01-15',
        total_amount_cents: null,
        status: 'pending',
        raw_text: null,
        store_confidence: null,
        ocr_provider: null,
        error_details: null,
        retry_count: 0,
        ocr_confidence: null,
        currency: 'CAD',
        created_at: new Date().toISOString(),
        processed_at: null,
        updated_at: new Date().toISOString(),
      };

      mockQuery.mockResolvedValueOnce({
        rows: [receiptRow],
      });

      const result = await repo.createReceipt(
        {
          user_id: 'user-1',
          receipt_image_url: '/uploads/receipts/receipt-1.jpg',
          store_id: 5,
          transaction_date: '2024-01-15',
        },
        mockLogger,
      );

      expect(result.receipt_id).toBe(1);
      expect(result.user_id).toBe('user-1');
      expect(result.status).toBe('pending');
      expect(mockQuery).toHaveBeenCalledWith(
        expect.stringContaining('INSERT INTO public.receipts'),
        ['user-1', '/uploads/receipts/receipt-1.jpg', 5, '2024-01-15'],
      );
    });

    it('should create receipt without optional fields', async () => {
      const receiptRow = {
        receipt_id: 2,
        user_id: 'user-1',
        store_id: null,
        receipt_image_url: '/uploads/receipts/receipt-2.jpg',
        transaction_date: null,
        total_amount_cents: null,
        status: 'pending',
        raw_text: null,
        store_confidence: null,
        ocr_provider: null,
        error_details: null,
        retry_count: 0,
        ocr_confidence: null,
        currency: 'CAD',
        created_at: new Date().toISOString(),
        processed_at: null,
        updated_at: new Date().toISOString(),
      };

      mockQuery.mockResolvedValueOnce({
        rows: [receiptRow],
      });

      const result = await repo.createReceipt(
        {
          user_id: 'user-1',
          receipt_image_url: '/uploads/receipts/receipt-2.jpg',
        },
        mockLogger,
      );

      expect(result.store_id).toBeNull();
      expect(result.transaction_date).toBeNull();
    });

    it('should throw on database error', async () => {
      mockQuery.mockRejectedValueOnce(new Error('DB connection failed'));

      await expect(
        repo.createReceipt(
          {
            user_id: 'user-1',
            receipt_image_url: '/uploads/receipts/receipt-1.jpg',
          },
          mockLogger,
        ),
      ).rejects.toThrow();
    });
  });

  describe('getReceiptById', () => {
    it('should return receipt when found', async () => {
      const receiptRow = {
        receipt_id: 1,
        user_id: 'user-1',
        store_id: 5,
        receipt_image_url: '/uploads/receipts/receipt-1.jpg',
        transaction_date: '2024-01-15',
        total_amount_cents: 5499,
        status: 'completed',
        raw_text: 'Store Name\nItem 1 $10.00',
        store_confidence: 0.95,
        ocr_provider: 'gemini',
        error_details: null,
        retry_count: 0,
        ocr_confidence: 0.9,
        currency: 'CAD',
        created_at: new Date().toISOString(),
        processed_at: new Date().toISOString(),
        updated_at: new Date().toISOString(),
      };

      mockQuery.mockResolvedValueOnce({
        rowCount: 1,
        rows: [receiptRow],
      });

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

      expect(result.receipt_id).toBe(1);
      expect(result.status).toBe('completed');
      expect(result.store_confidence).toBe(0.95);
      expect(mockQuery).toHaveBeenCalledWith(
        expect.stringContaining('WHERE receipt_id = $1 AND user_id = $2'),
        [1, 'user-1'],
      );
    });

    it('should throw NotFoundError when receipt not found', async () => {
      mockQuery.mockResolvedValueOnce({
        rowCount: 0,
        rows: [],
      });

      await expect(repo.getReceiptById(999, 'user-1', mockLogger)).rejects.toThrow(NotFoundError);
    });
  });

  describe('getReceipts', () => {
    it('should return paginated receipts', async () => {
      // Count query
      mockQuery.mockResolvedValueOnce({
        rows: [{ count: '10' }],
      });

      // Data query
      mockQuery.mockResolvedValueOnce({
        rows: [
          {
            receipt_id: 1,
            user_id: 'user-1',
            store_id: 5,
            receipt_image_url: '/uploads/receipts/receipt-1.jpg',
            transaction_date: '2024-01-15',
            total_amount_cents: 5499,
            status: 'completed',
            raw_text: null,
            store_confidence: null,
            ocr_provider: null,
            error_details: null,
            retry_count: 0,
            ocr_confidence: null,
            currency: 'CAD',
            created_at: new Date().toISOString(),
            processed_at: null,
            updated_at: new Date().toISOString(),
          },
        ],
      });

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

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

    it('should filter by status', async () => {
      mockQuery.mockResolvedValueOnce({ rows: [{ count: '5' }] });
      mockQuery.mockResolvedValueOnce({ rows: [] });

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

      expect(mockQuery).toHaveBeenCalledWith(
        expect.stringContaining('status = $2'),
        expect.any(Array),
      );
    });

    it('should filter by store_id', async () => {
      mockQuery.mockResolvedValueOnce({ rows: [{ count: '3' }] });
      mockQuery.mockResolvedValueOnce({ rows: [] });

      await repo.getReceipts({ user_id: 'user-1', store_id: 5 }, mockLogger);

      expect(mockQuery).toHaveBeenCalledWith(
        expect.stringContaining('store_id = $2'),
        expect.any(Array),
      );
    });

    it('should filter by date range', async () => {
      mockQuery.mockResolvedValueOnce({ rows: [{ count: '2' }] });
      mockQuery.mockResolvedValueOnce({ rows: [] });

      await repo.getReceipts(
        {
          user_id: 'user-1',
          from_date: '2024-01-01',
          to_date: '2024-01-31',
        },
        mockLogger,
      );

      expect(mockQuery).toHaveBeenCalledWith(
        expect.stringContaining('created_at >= $2'),
        expect.any(Array),
      );
    });
  });

  describe('updateReceipt', () => {
    it('should update receipt successfully', async () => {
      const updatedRow = {
        receipt_id: 1,
        user_id: 'user-1',
        store_id: 5,
        receipt_image_url: '/uploads/receipts/receipt-1.jpg',
        transaction_date: '2024-01-15',
        total_amount_cents: 5499,
        status: 'completed',
        raw_text: 'Extracted text here',
        store_confidence: 0.95,
        ocr_provider: 'gemini',
        error_details: null,
        retry_count: 0,
        ocr_confidence: 0.88,
        currency: 'CAD',
        created_at: new Date().toISOString(),
        processed_at: new Date().toISOString(),
        updated_at: new Date().toISOString(),
      };

      mockQuery.mockResolvedValueOnce({
        rowCount: 1,
        rows: [updatedRow],
      });

      const result = await repo.updateReceipt(
        1,
        {
          status: 'completed',
          raw_text: 'Extracted text here',
          ocr_provider: 'gemini',
          ocr_confidence: 0.88,
          total_amount_cents: 5499,
        },
        mockLogger,
      );

      expect(result.status).toBe('completed');
      expect(result.ocr_confidence).toBe(0.88);
      expect(mockQuery).toHaveBeenCalledWith(
        expect.stringContaining('UPDATE public.receipts SET'),
        expect.any(Array),
      );
    });

    it('should throw NotFoundError when receipt not found', async () => {
      mockQuery.mockResolvedValueOnce({
        rowCount: 0,
        rows: [],
      });

      await expect(repo.updateReceipt(999, { status: 'completed' }, mockLogger)).rejects.toThrow(
        NotFoundError,
      );
    });
  });

  describe('incrementRetryCount', () => {
    it('should increment retry count successfully', async () => {
      mockQuery.mockResolvedValueOnce({
        rowCount: 1,
        rows: [{ retry_count: 2 }],
      });

      const result = await repo.incrementRetryCount(1, mockLogger);

      expect(result).toBe(2);
      expect(mockQuery).toHaveBeenCalledWith(
        expect.stringContaining('retry_count = retry_count + 1'),
        [1],
      );
    });

    it('should throw NotFoundError when receipt not found', async () => {
      mockQuery.mockResolvedValueOnce({
        rowCount: 0,
        rows: [],
      });

      await expect(repo.incrementRetryCount(999, mockLogger)).rejects.toThrow(NotFoundError);
    });
  });

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

      mockQuery.mockResolvedValueOnce({
        rowCount: 2,
        rows: receipts,
      });

      const result = await repo.getReceiptsNeedingProcessing(3, 10, mockLogger);

      expect(result).toHaveLength(2);
      expect(mockQuery).toHaveBeenCalledWith(
        expect.stringContaining("status = 'pending' OR (status = 'failed' AND retry_count < $1)"),
        [3, 10],
      );
    });
  });

  describe('deleteReceipt', () => {
    it('should delete receipt successfully', async () => {
      mockQuery.mockResolvedValueOnce({
        rowCount: 1,
      });

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

      expect(mockQuery).toHaveBeenCalledWith(
        expect.stringContaining('DELETE FROM public.receipts'),
        [1, 'user-1'],
      );
    });

    it('should throw NotFoundError when receipt not found or unauthorized', async () => {
      mockQuery.mockResolvedValueOnce({
        rowCount: 0,
      });

      await expect(repo.deleteReceipt(999, 'user-1', mockLogger)).rejects.toThrow(NotFoundError);
    });
  });

  // ============================================================================
  // RECEIPT ITEMS
  // ============================================================================

  describe('addReceiptItems', () => {
    it('should add multiple receipt items', async () => {
      const itemRows = [
        {
          receipt_item_id: 1,
          receipt_id: 1,
          raw_item_description: 'Milk 2L',
          quantity: 1,
          price_paid_cents: 499,
          master_item_id: null,
          product_id: null,
          status: 'pending',
          line_number: 1,
          match_confidence: null,
          is_discount: false,
          unit_price_cents: 499,
          unit_type: null,
          added_to_pantry: false,
          pantry_item_id: null,
          upc_code: null,
          created_at: new Date().toISOString(),
          updated_at: new Date().toISOString(),
        },
        {
          receipt_item_id: 2,
          receipt_id: 1,
          raw_item_description: 'Bread',
          quantity: 2,
          price_paid_cents: 598,
          master_item_id: null,
          product_id: null,
          status: 'pending',
          line_number: 2,
          match_confidence: null,
          is_discount: false,
          unit_price_cents: 299,
          unit_type: null,
          added_to_pantry: false,
          pantry_item_id: null,
          upc_code: null,
          created_at: new Date().toISOString(),
          updated_at: new Date().toISOString(),
        },
      ];

      mockQuery.mockResolvedValueOnce({
        rowCount: 2,
        rows: itemRows,
      });

      const result = await repo.addReceiptItems(
        [
          {
            receipt_id: 1,
            raw_item_description: 'Milk 2L',
            price_paid_cents: 499,
            line_number: 1,
            unit_price_cents: 499,
          },
          {
            receipt_id: 1,
            raw_item_description: 'Bread',
            quantity: 2,
            price_paid_cents: 598,
            line_number: 2,
            unit_price_cents: 299,
          },
        ],
        mockLogger,
      );

      expect(result).toHaveLength(2);
      expect(result[0].raw_item_description).toBe('Milk 2L');
      expect(result[1].quantity).toBe(2);
    });

    it('should return empty array for empty input', async () => {
      const result = await repo.addReceiptItems([], mockLogger);

      expect(result).toHaveLength(0);
      expect(mockQuery).not.toHaveBeenCalled();
    });

    it('should handle discount items', async () => {
      const itemRow = {
        receipt_item_id: 1,
        receipt_id: 1,
        raw_item_description: 'DISCOUNT -$1.00',
        quantity: 1,
        price_paid_cents: -100,
        master_item_id: null,
        product_id: null,
        status: 'pending',
        line_number: 5,
        match_confidence: null,
        is_discount: true,
        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(),
      };

      mockQuery.mockResolvedValueOnce({
        rowCount: 1,
        rows: [itemRow],
      });

      const result = await repo.addReceiptItems(
        [
          {
            receipt_id: 1,
            raw_item_description: 'DISCOUNT -$1.00',
            price_paid_cents: -100,
            line_number: 5,
            is_discount: true,
          },
        ],
        mockLogger,
      );

      expect(result[0].is_discount).toBe(true);
      expect(result[0].price_paid_cents).toBe(-100);
    });
  });

  describe('getReceiptItems', () => {
    it('should return receipt items ordered by line number', async () => {
      const itemRows = [
        {
          receipt_item_id: 1,
          receipt_id: 1,
          raw_item_description: 'Item 1',
          quantity: 1,
          price_paid_cents: 100,
          master_item_id: null,
          product_id: null,
          status: 'pending',
          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(),
        },
        {
          receipt_item_id: 2,
          receipt_id: 1,
          raw_item_description: 'Item 2',
          quantity: 1,
          price_paid_cents: 200,
          master_item_id: null,
          product_id: null,
          status: 'pending',
          line_number: 2,
          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(),
        },
      ];

      mockQuery.mockResolvedValueOnce({
        rows: itemRows,
      });

      const result = await repo.getReceiptItems(1, mockLogger);

      expect(result).toHaveLength(2);
      expect(mockQuery).toHaveBeenCalledWith(
        expect.stringContaining('ORDER BY line_number ASC NULLS LAST'),
        [1],
      );
    });
  });

  describe('updateReceiptItem', () => {
    it('should update receipt item with product match', async () => {
      const updatedRow = {
        receipt_item_id: 1,
        receipt_id: 1,
        raw_item_description: 'Milk 2L',
        quantity: 1,
        price_paid_cents: 499,
        master_item_id: 100,
        product_id: 50,
        status: 'matched',
        line_number: 1,
        match_confidence: 0.95,
        is_discount: false,
        unit_price_cents: 499,
        unit_type: null,
        added_to_pantry: false,
        pantry_item_id: null,
        upc_code: null,
        created_at: new Date().toISOString(),
        updated_at: new Date().toISOString(),
      };

      mockQuery.mockResolvedValueOnce({
        rowCount: 1,
        rows: [updatedRow],
      });

      const result = await repo.updateReceiptItem(
        1,
        {
          status: 'matched',
          master_item_id: 100,
          product_id: 50,
          match_confidence: 0.95,
        },
        mockLogger,
      );

      expect(result.status).toBe('matched');
      expect(result.match_confidence).toBe(0.95);
    });

    it('should update receipt item with pantry linkage', async () => {
      const updatedRow = {
        receipt_item_id: 1,
        receipt_id: 1,
        raw_item_description: 'Milk 2L',
        quantity: 1,
        price_paid_cents: 499,
        master_item_id: 100,
        product_id: 50,
        status: 'matched',
        line_number: 1,
        match_confidence: 0.95,
        is_discount: false,
        unit_price_cents: 499,
        unit_type: null,
        added_to_pantry: true,
        pantry_item_id: 25,
        upc_code: null,
        created_at: new Date().toISOString(),
        updated_at: new Date().toISOString(),
      };

      mockQuery.mockResolvedValueOnce({
        rowCount: 1,
        rows: [updatedRow],
      });

      const result = await repo.updateReceiptItem(
        1,
        {
          added_to_pantry: true,
          pantry_item_id: 25,
        },
        mockLogger,
      );

      expect(result.added_to_pantry).toBe(true);
      expect(result.pantry_item_id).toBe(25);
    });

    it('should throw NotFoundError when item not found', async () => {
      mockQuery.mockResolvedValueOnce({
        rowCount: 0,
        rows: [],
      });

      await expect(repo.updateReceiptItem(999, { status: 'matched' }, mockLogger)).rejects.toThrow(
        NotFoundError,
      );
    });
  });

  describe('getUnaddedReceiptItems', () => {
    it('should return items not added to pantry excluding discounts', async () => {
      const itemRows = [
        {
          receipt_item_id: 1,
          receipt_id: 1,
          raw_item_description: 'Milk 2L',
          quantity: 1,
          price_paid_cents: 499,
          master_item_id: null,
          product_id: null,
          status: 'pending',
          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(),
        },
      ];

      mockQuery.mockResolvedValueOnce({
        rows: itemRows,
      });

      const result = await repo.getUnaddedReceiptItems(1, mockLogger);

      expect(result).toHaveLength(1);
      expect(mockQuery).toHaveBeenCalledWith(
        expect.stringContaining('added_to_pantry = false'),
        [1],
      );
      expect(mockQuery).toHaveBeenCalledWith(expect.stringContaining('is_discount = false'), [1]);
    });
  });

  // ============================================================================
  // PROCESSING LOG
  // ============================================================================

  describe('logProcessingStep', () => {
    it('should log processing step with all options', async () => {
      const logRecord = {
        log_id: 1,
        receipt_id: 1,
        processing_step: 'ocr_extraction',
        status: 'completed',
        provider: 'gemini',
        duration_ms: 1500,
        tokens_used: 1000,
        cost_cents: 5,
        input_data: null,
        output_data: JSON.stringify({ text: 'Extracted text' }),
        error_message: null,
        created_at: new Date().toISOString(),
      };

      mockQuery.mockResolvedValueOnce({
        rows: [logRecord],
      });

      const result = await repo.logProcessingStep(1, 'ocr_extraction', 'completed', mockLogger, {
        provider: 'gemini',
        durationMs: 1500,
        tokensUsed: 1000,
        costCents: 5,
        outputData: { text: 'Extracted text' },
      });

      expect(result.processing_step).toBe('ocr_extraction');
      expect(result.status).toBe('completed');
      expect(mockQuery).toHaveBeenCalledWith(
        expect.stringContaining('INSERT INTO public.receipt_processing_log'),
        expect.any(Array),
      );
    });

    it('should log processing step with error', async () => {
      const logRecord = {
        log_id: 2,
        receipt_id: 1,
        processing_step: 'ocr_extraction',
        status: 'failed',
        provider: 'gemini',
        duration_ms: 500,
        tokens_used: null,
        cost_cents: null,
        input_data: null,
        output_data: null,
        error_message: 'OCR service unavailable',
        created_at: new Date().toISOString(),
      };

      mockQuery.mockResolvedValueOnce({
        rows: [logRecord],
      });

      const result = await repo.logProcessingStep(1, 'ocr_extraction', 'failed', mockLogger, {
        provider: 'gemini',
        durationMs: 500,
        errorMessage: 'OCR service unavailable',
      });

      expect(result.status).toBe('failed');
      expect(result.error_message).toBe('OCR service unavailable');
    });
  });

  describe('getProcessingLogs', () => {
    it('should return processing logs for receipt', async () => {
      const logs = [
        {
          log_id: 1,
          receipt_id: 1,
          processing_step: 'upload',
          status: 'success',
          provider: null,
          duration_ms: 100,
          tokens_used: null,
          cost_cents: null,
          input_data: null,
          output_data: null,
          error_message: null,
          created_at: new Date().toISOString(),
        },
        {
          log_id: 2,
          receipt_id: 1,
          processing_step: 'ocr',
          status: 'success',
          provider: 'gemini',
          duration_ms: 1500,
          tokens_used: 1000,
          cost_cents: 5,
          input_data: null,
          output_data: null,
          error_message: null,
          created_at: new Date().toISOString(),
        },
      ];

      mockQuery.mockResolvedValueOnce({
        rows: logs,
      });

      const result = await repo.getProcessingLogs(1, mockLogger);

      expect(result).toHaveLength(2);
      expect(mockQuery).toHaveBeenCalledWith(
        expect.stringContaining('ORDER BY created_at ASC'),
        [1],
      );
    });
  });

  describe('getProcessingStats', () => {
    it('should return processing statistics', async () => {
      // Receipt stats query
      mockQuery.mockResolvedValueOnce({
        rows: [
          {
            total_receipts: '100',
            completed: '80',
            failed: '10',
            pending: '10',
          },
        ],
      });

      // Processing log stats query
      mockQuery.mockResolvedValueOnce({
        rows: [
          {
            avg_duration_ms: '1500.5',
            total_cost_cents: '500',
          },
        ],
      });

      const result = await repo.getProcessingStats(mockLogger);

      expect(result.total_receipts).toBe(100);
      expect(result.completed).toBe(80);
      expect(result.failed).toBe(10);
      expect(result.pending).toBe(10);
      expect(result.avg_processing_time_ms).toBe(1500.5);
      expect(result.total_cost_cents).toBe(500);
    });

    it('should filter by date range', async () => {
      mockQuery.mockResolvedValueOnce({
        rows: [{ total_receipts: '50', completed: '40', failed: '5', pending: '5' }],
      });
      mockQuery.mockResolvedValueOnce({
        rows: [{ avg_duration_ms: '1200', total_cost_cents: '250' }],
      });

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

      expect(mockQuery).toHaveBeenCalledWith(
        expect.stringContaining('created_at >= $1'),
        expect.any(Array),
      );
    });
  });

  // ============================================================================
  // STORE RECEIPT PATTERNS
  // ============================================================================

  describe('getActiveStorePatterns', () => {
    it('should return active patterns ordered by priority', async () => {
      const patterns = [
        {
          pattern_id: 1,
          store_id: 5,
          pattern_type: 'header_regex',
          pattern_value: 'LOBLAWS',
          priority: 100,
          is_active: true,
          created_at: new Date().toISOString(),
          updated_at: new Date().toISOString(),
        },
        {
          pattern_id: 2,
          store_id: 5,
          pattern_type: 'phone_number',
          pattern_value: '416-555-1234',
          priority: 50,
          is_active: true,
          created_at: new Date().toISOString(),
          updated_at: new Date().toISOString(),
        },
      ];

      mockQuery.mockResolvedValueOnce({
        rows: patterns,
      });

      const result = await repo.getActiveStorePatterns(mockLogger);

      expect(result).toHaveLength(2);
      expect(mockQuery).toHaveBeenCalledWith(expect.stringContaining('is_active = true'));
      expect(mockQuery).toHaveBeenCalledWith(expect.stringContaining('ORDER BY priority DESC'));
    });
  });

  describe('addStorePattern', () => {
    it('should add new store pattern', async () => {
      const pattern = {
        pattern_id: 1,
        store_id: 5,
        pattern_type: 'header_regex',
        pattern_value: 'METRO',
        priority: 100,
        is_active: true,
        created_at: new Date().toISOString(),
        updated_at: new Date().toISOString(),
      };

      mockQuery.mockResolvedValueOnce({
        rows: [pattern],
      });

      const result = await repo.addStorePattern(5, 'header_regex', 'METRO', mockLogger, {
        priority: 100,
      });

      expect(result.store_id).toBe(5);
      expect(result.pattern_type).toBe('header_regex');
      expect(result.priority).toBe(100);
    });

    it('should use default priority when not specified', async () => {
      const pattern = {
        pattern_id: 2,
        store_id: 5,
        pattern_type: 'phone_number',
        pattern_value: '416-555-0000',
        priority: 0,
        is_active: true,
        created_at: new Date().toISOString(),
        updated_at: new Date().toISOString(),
      };

      mockQuery.mockResolvedValueOnce({
        rows: [pattern],
      });

      const result = await repo.addStorePattern(5, 'phone_number', '416-555-0000', mockLogger);

      expect(result.priority).toBe(0);
    });
  });

  describe('deactivateStorePattern', () => {
    it('should deactivate pattern successfully', async () => {
      mockQuery.mockResolvedValueOnce({
        rowCount: 1,
      });

      await repo.deactivateStorePattern(1, mockLogger);

      expect(mockQuery).toHaveBeenCalledWith(expect.stringContaining('SET is_active = false'), [1]);
    });

    it('should throw NotFoundError when pattern not found', async () => {
      mockQuery.mockResolvedValueOnce({
        rowCount: 0,
      });

      await expect(repo.deactivateStorePattern(999, mockLogger)).rejects.toThrow(NotFoundError);
    });
  });

  describe('detectStoreFromText', () => {
    it('should detect store from receipt text using patterns', async () => {
      const patterns = [
        {
          pattern_id: 1,
          store_id: 5,
          pattern_type: 'header_regex',
          pattern_value: 'LOBLAWS',
          priority: 100,
          is_active: true,
          created_at: new Date().toISOString(),
          updated_at: new Date().toISOString(),
        },
        {
          pattern_id: 2,
          store_id: 5,
          pattern_type: 'phone_number',
          pattern_value: '416-555-1234',
          priority: 50,
          is_active: true,
          created_at: new Date().toISOString(),
          updated_at: new Date().toISOString(),
        },
      ];

      mockQuery.mockResolvedValueOnce({
        rows: patterns,
      });

      const result = await repo.detectStoreFromText(
        'LOBLAWS\n123 Main St\nPhone: 416-555-1234\nItem 1 $10.00',
        mockLogger,
      );

      expect(result).not.toBeNull();
      expect(result?.store_id).toBe(5);
      expect(result?.confidence).toBeGreaterThan(0);
    });

    it('should return null when no patterns match', async () => {
      const patterns = [
        {
          pattern_id: 1,
          store_id: 5,
          pattern_type: 'header_regex',
          pattern_value: 'LOBLAWS',
          priority: 100,
          is_active: true,
          created_at: new Date().toISOString(),
          updated_at: new Date().toISOString(),
        },
      ];

      mockQuery.mockResolvedValueOnce({
        rows: patterns,
      });

      const result = await repo.detectStoreFromText('METRO\n123 Main St', mockLogger);

      expect(result).toBeNull();
    });

    it('should handle regex pattern matching', async () => {
      const patterns = [
        {
          pattern_id: 1,
          store_id: 5,
          pattern_type: 'header_regex',
          pattern_value: 'LOBLAWS.*SUPERSTORE',
          priority: 100,
          is_active: true,
          created_at: new Date().toISOString(),
          updated_at: new Date().toISOString(),
        },
      ];

      mockQuery.mockResolvedValueOnce({
        rows: patterns,
      });

      const result = await repo.detectStoreFromText(
        'LOBLAWS GREAT FOOD SUPERSTORE\nItem 1 $10.00',
        mockLogger,
      );

      expect(result).not.toBeNull();
      expect(result?.store_id).toBe(5);
    });

    it('should handle invalid regex gracefully', async () => {
      const patterns = [
        {
          pattern_id: 1,
          store_id: 5,
          pattern_type: 'header_regex',
          pattern_value: '[invalid(regex',
          priority: 100,
          is_active: true,
          created_at: new Date().toISOString(),
          updated_at: new Date().toISOString(),
        },
        {
          pattern_id: 2,
          store_id: 5,
          pattern_type: 'phone_number',
          pattern_value: '416-555-1234',
          priority: 50,
          is_active: true,
          created_at: new Date().toISOString(),
          updated_at: new Date().toISOString(),
        },
      ];

      mockQuery.mockResolvedValueOnce({
        rows: patterns,
      });

      // Should not throw and should still match the phone number pattern
      const result = await repo.detectStoreFromText('Phone: 416-555-1234', mockLogger);

      expect(result).not.toBeNull();
      expect(result?.store_id).toBe(5);
    });

    it('should select best match when multiple stores match', async () => {
      const patterns = [
        // Store 5 patterns
        {
          pattern_id: 1,
          store_id: 5,
          pattern_type: 'header_regex',
          pattern_value: 'LOBLAWS',
          priority: 100,
          is_active: true,
          created_at: new Date().toISOString(),
          updated_at: new Date().toISOString(),
        },
        // Store 6 patterns (more matches)
        {
          pattern_id: 2,
          store_id: 6,
          pattern_type: 'header_regex',
          pattern_value: 'METRO',
          priority: 100,
          is_active: true,
          created_at: new Date().toISOString(),
          updated_at: new Date().toISOString(),
        },
        {
          pattern_id: 3,
          store_id: 6,
          pattern_type: 'phone_number',
          pattern_value: '416-555-9999',
          priority: 50,
          is_active: true,
          created_at: new Date().toISOString(),
          updated_at: new Date().toISOString(),
        },
        {
          pattern_id: 4,
          store_id: 6,
          pattern_type: 'address_fragment',
          pattern_value: '456 Oak Ave',
          priority: 25,
          is_active: true,
          created_at: new Date().toISOString(),
          updated_at: new Date().toISOString(),
        },
      ];

      mockQuery.mockResolvedValueOnce({
        rows: patterns,
      });

      const result = await repo.detectStoreFromText(
        'METRO\n456 Oak Ave\nPhone: 416-555-9999\nItem 1 $10.00',
        mockLogger,
      );

      // Should match store 6 because it has more matches
      expect(result).not.toBeNull();
      expect(result?.store_id).toBe(6);
    });
  });
});
