// src/services/expiryService.server.test.ts
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import type { Logger } from 'pino';
import type { Job } from 'bullmq';
import type { ExpiryAlertJobData } from '../types/job-data';
import { createMockLogger } from '../tests/utils/mockLogger';
import type {
  InventorySource,
  StorageLocation,
  ExpiryStatus,
  ExpiryRangeSource,
  AlertMethod,
  UserInventoryItem,
  ReceiptStatus,
  ReceiptItemStatus,
  ExpiryAlertLogRecord,
  ExpiryAlertType,
} from '../types/expiry';

// Mock dependencies
vi.mock('./db/index.db', () => ({
  expiryRepo: {
    addInventoryItem: vi.fn(),
    updateInventoryItem: vi.fn(),
    markAsConsumed: vi.fn(),
    deleteInventoryItem: vi.fn(),
    getInventoryItemById: vi.fn(),
    getInventory: vi.fn(),
    getExpiringItems: vi.fn(),
    getExpiredItems: vi.fn(),
    getExpiryRangeForItem: vi.fn(),
    getExpiryRanges: vi.fn(),
    addExpiryRange: vi.fn(),
    getUserAlertSettings: vi.fn(),
    upsertAlertSettings: vi.fn(),
    getUsersWithExpiringItems: vi.fn(),
    logAlert: vi.fn(),
    markAlertSent: vi.fn(),
    getRecipesForExpiringItems: vi.fn(),
  },
  receiptRepo: {
    getReceiptById: vi.fn(),
    getReceiptItems: vi.fn(),
    updateReceiptItem: vi.fn(),
  },
}));

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

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

// Import after mocks are set up
import {
  addInventoryItem,
  updateInventoryItem,
  markItemConsumed,
  deleteInventoryItem,
  getInventoryItemById,
  getInventory,
  getExpiringItemsGrouped,
  getExpiringItems,
  getExpiredItems,
  calculateExpiryDate,
  getExpiryRanges,
  addExpiryRange,
  getAlertSettings,
  updateAlertSettings,
  processExpiryAlerts,
  addItemsFromReceipt,
  getRecipeSuggestionsForExpiringItems,
  processExpiryAlertJob,
} from './expiryService.server';

import { expiryRepo, receiptRepo } from './db/index.db';
import * as emailService from './emailService.server';

// Helper to create mock alert log record
function createMockAlertLogRecord(
  overrides: Partial<ExpiryAlertLogRecord> = {},
): ExpiryAlertLogRecord {
  return {
    alert_log_id: 1,
    user_id: 'user-1',
    pantry_item_id: null,
    alert_type: 'expiring_soon' as ExpiryAlertType,
    alert_method: 'email' as AlertMethod,
    item_name: 'Test Item',
    expiry_date: null,
    days_until_expiry: null,
    sent_at: new Date().toISOString(),
    ...overrides,
  };
}

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

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

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

  describe('addInventoryItem', () => {
    it('should add item to inventory without expiry date', async () => {
      const mockItem: UserInventoryItem = {
        inventory_id: 1,
        user_id: 'user-1',
        product_id: null,
        master_item_id: null,
        item_name: 'Milk',
        quantity: 1,
        unit: 'gallon',
        purchase_date: null,
        expiry_date: null,
        source: 'manual',
        location: 'fridge',
        notes: null,
        is_consumed: false,
        consumed_at: null,
        expiry_source: null,
        receipt_item_id: null,
        pantry_location_id: null,
        notification_sent_at: null,
        created_at: new Date().toISOString(),
        updated_at: new Date().toISOString(),
        days_until_expiry: null,
        expiry_status: 'unknown',
      };

      vi.mocked(expiryRepo.addInventoryItem).mockResolvedValueOnce(mockItem);

      const result = await addInventoryItem(
        'user-1',
        { item_name: 'Milk', quantity: 1, source: 'manual', location: 'fridge' },
        mockLogger,
      );

      expect(result.inventory_id).toBe(1);
      expect(result.item_name).toBe('Milk');
    });

    it('should calculate expiry date when purchase date and location provided', async () => {
      const mockItem: UserInventoryItem = {
        inventory_id: 2,
        user_id: 'user-1',
        product_id: null,
        master_item_id: 5,
        item_name: 'Milk',
        quantity: 1,
        unit: 'gallon',
        purchase_date: '2024-01-15',
        expiry_date: '2024-01-22', // calculated
        source: 'manual',
        location: 'fridge',
        notes: null,
        is_consumed: false,
        consumed_at: null,
        expiry_source: 'calculated',
        receipt_item_id: null,
        pantry_location_id: null,
        notification_sent_at: null,
        created_at: new Date().toISOString(),
        updated_at: new Date().toISOString(),
        days_until_expiry: 7,
        expiry_status: 'fresh',
      };

      vi.mocked(expiryRepo.getExpiryRangeForItem).mockResolvedValueOnce({
        expiry_range_id: 1,
        master_item_id: 5,
        category_id: null,
        item_pattern: null,
        storage_location: 'fridge',
        min_days: 5,
        max_days: 10,
        typical_days: 7,
        notes: null,
        source: 'usda',
        created_at: new Date().toISOString(),
        updated_at: new Date().toISOString(),
      });

      vi.mocked(expiryRepo.addInventoryItem).mockResolvedValueOnce(mockItem);

      const result = await addInventoryItem(
        'user-1',
        {
          item_name: 'Milk',
          master_item_id: 5,
          quantity: 1,
          source: 'manual',
          location: 'fridge',
          purchase_date: '2024-01-15',
        },
        mockLogger,
      );

      expect(result.expiry_date).toBe('2024-01-22');
    });
  });

  describe('updateInventoryItem', () => {
    it('should update inventory item', async () => {
      const mockUpdatedItem: UserInventoryItem = {
        inventory_id: 1,
        user_id: 'user-1',
        product_id: null,
        master_item_id: null,
        item_name: 'Milk',
        quantity: 2, // updated
        unit: 'gallon',
        purchase_date: null,
        expiry_date: '2024-01-25',
        source: 'manual',
        location: 'fridge',
        notes: 'Almost gone',
        is_consumed: false,
        consumed_at: null,
        expiry_source: null,
        receipt_item_id: null,
        pantry_location_id: null,
        notification_sent_at: null,
        created_at: new Date().toISOString(),
        updated_at: new Date().toISOString(),
        days_until_expiry: 5,
        expiry_status: 'expiring_soon',
      };

      vi.mocked(expiryRepo.updateInventoryItem).mockResolvedValueOnce(mockUpdatedItem);

      const result = await updateInventoryItem(
        1,
        'user-1',
        { quantity: 2, notes: 'Almost gone' },
        mockLogger,
      );

      expect(result.quantity).toBe(2);
      expect(result.notes).toBe('Almost gone');
    });
  });

  describe('markItemConsumed', () => {
    it('should mark item as consumed', async () => {
      vi.mocked(expiryRepo.markAsConsumed).mockResolvedValueOnce(undefined);

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

      expect(expiryRepo.markAsConsumed).toHaveBeenCalledWith(1, 'user-1', mockLogger);
    });
  });

  describe('deleteInventoryItem', () => {
    it('should delete inventory item', async () => {
      vi.mocked(expiryRepo.deleteInventoryItem).mockResolvedValueOnce(undefined);

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

      expect(expiryRepo.deleteInventoryItem).toHaveBeenCalledWith(1, 'user-1', mockLogger);
    });
  });

  describe('getInventoryItemById', () => {
    it('should return inventory item by ID', async () => {
      const mockItem: UserInventoryItem = {
        inventory_id: 1,
        user_id: 'user-1',
        product_id: null,
        master_item_id: null,
        item_name: 'Eggs',
        quantity: 12,
        unit: null,
        purchase_date: null,
        expiry_date: null,
        source: 'manual',
        location: 'fridge',
        notes: null,
        is_consumed: false,
        consumed_at: null,
        expiry_source: null,
        receipt_item_id: null,
        pantry_location_id: null,
        notification_sent_at: null,
        created_at: new Date().toISOString(),
        updated_at: new Date().toISOString(),
        days_until_expiry: null,
        expiry_status: 'unknown',
      };

      vi.mocked(expiryRepo.getInventoryItemById).mockResolvedValueOnce(mockItem);

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

      expect(result.item_name).toBe('Eggs');
    });
  });

  describe('getInventory', () => {
    it('should return paginated inventory', async () => {
      const mockInventory = {
        items: [
          {
            inventory_id: 1,
            user_id: 'user-1',
            product_id: null,
            master_item_id: null,
            item_name: 'Butter',
            quantity: 1,
            unit: null,
            purchase_date: null,
            expiry_date: null,
            source: 'manual' as InventorySource,
            location: 'fridge' as StorageLocation,
            notes: null,
            is_consumed: false,
            consumed_at: null,
            expiry_source: null,
            receipt_item_id: null,
            pantry_location_id: null,
            notification_sent_at: null,
            created_at: new Date().toISOString(),
            updated_at: new Date().toISOString(),
            days_until_expiry: null,
            expiry_status: 'unknown' as ExpiryStatus,
          },
        ],
        total: 1,
      };

      vi.mocked(expiryRepo.getInventory).mockResolvedValueOnce(mockInventory);

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

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

    it('should filter by location', async () => {
      vi.mocked(expiryRepo.getInventory).mockResolvedValueOnce({ items: [], total: 0 });

      await getInventory({ user_id: 'user-1', location: 'freezer' }, mockLogger);

      expect(expiryRepo.getInventory).toHaveBeenCalledWith(
        { user_id: 'user-1', location: 'freezer' },
        mockLogger,
      );
    });
  });

  describe('getExpiringItemsGrouped', () => {
    it('should return items grouped by expiry urgency', async () => {
      const expiringItems = [
        createMockInventoryItem({ days_until_expiry: 0 }), // today
        createMockInventoryItem({ days_until_expiry: 3 }), // this week
        createMockInventoryItem({ days_until_expiry: 15 }), // this month
      ];
      const expiredItems = [createMockInventoryItem({ days_until_expiry: -2 })];

      vi.mocked(expiryRepo.getExpiringItems).mockResolvedValueOnce(expiringItems);
      vi.mocked(expiryRepo.getExpiredItems).mockResolvedValueOnce(expiredItems);

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

      expect(result.expiring_today).toHaveLength(1);
      expect(result.expiring_this_week).toHaveLength(1);
      expect(result.expiring_this_month).toHaveLength(1);
      expect(result.already_expired).toHaveLength(1);
      expect(result.counts.total).toBe(4);
    });
  });

  describe('getExpiringItems', () => {
    it('should return items expiring within specified days', async () => {
      const mockItems = [createMockInventoryItem({ days_until_expiry: 5 })];
      vi.mocked(expiryRepo.getExpiringItems).mockResolvedValueOnce(mockItems);

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

      expect(result).toHaveLength(1);
      expect(expiryRepo.getExpiringItems).toHaveBeenCalledWith('user-1', 7, mockLogger);
    });
  });

  describe('getExpiredItems', () => {
    it('should return expired items', async () => {
      const mockItems = [createMockInventoryItem({ days_until_expiry: -3 })];
      vi.mocked(expiryRepo.getExpiredItems).mockResolvedValueOnce(mockItems);

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

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

  describe('calculateExpiryDate', () => {
    it('should calculate expiry date based on storage location', async () => {
      vi.mocked(expiryRepo.getExpiryRangeForItem).mockResolvedValueOnce({
        expiry_range_id: 1,
        master_item_id: null,
        category_id: 1,
        item_pattern: null,
        storage_location: 'fridge',
        min_days: 7,
        max_days: 14,
        typical_days: 10,
        notes: null,
        source: 'usda',
        created_at: new Date().toISOString(),
        updated_at: new Date().toISOString(),
      });

      const result = await calculateExpiryDate(
        {
          item_name: 'Cheese',
          storage_location: 'fridge',
          purchase_date: '2024-01-15',
        },
        mockLogger,
      );

      expect(result).toBe('2024-01-25'); // 10 days after purchase
    });

    it('should return null when no expiry range found', async () => {
      vi.mocked(expiryRepo.getExpiryRangeForItem).mockResolvedValueOnce(null);

      const result = await calculateExpiryDate(
        {
          item_name: 'Unknown Item',
          storage_location: 'pantry',
          purchase_date: '2024-01-15',
        },
        mockLogger,
      );

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

  describe('getExpiryRanges', () => {
    it('should return paginated expiry ranges', async () => {
      const mockRanges = {
        ranges: [
          {
            expiry_range_id: 1,
            master_item_id: null,
            category_id: 1,
            item_pattern: null,
            storage_location: 'fridge' as StorageLocation,
            min_days: 7,
            max_days: 14,
            typical_days: 10,
            notes: null,
            source: 'usda' as ExpiryRangeSource,
            created_at: new Date().toISOString(),
            updated_at: new Date().toISOString(),
          },
        ],
        total: 1,
      };

      vi.mocked(expiryRepo.getExpiryRanges).mockResolvedValueOnce(mockRanges);

      const result = await getExpiryRanges({}, mockLogger);

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

  describe('addExpiryRange', () => {
    it('should add new expiry range', async () => {
      const mockRange = {
        expiry_range_id: 2,
        master_item_id: null,
        category_id: 2,
        item_pattern: null,
        storage_location: 'freezer' as StorageLocation,
        min_days: 30,
        max_days: 90,
        typical_days: 60,
        notes: 'Best stored in back of freezer',
        source: 'manual' as ExpiryRangeSource,
        created_at: new Date().toISOString(),
        updated_at: new Date().toISOString(),
      };

      vi.mocked(expiryRepo.addExpiryRange).mockResolvedValueOnce(mockRange);

      const result = await addExpiryRange(
        {
          category_id: 2,
          storage_location: 'freezer',
          min_days: 30,
          max_days: 90,
          typical_days: 60,
          notes: 'Best stored in back of freezer',
        },
        mockLogger,
      );

      expect(result.typical_days).toBe(60);
    });
  });

  describe('getAlertSettings', () => {
    it('should return user alert settings', async () => {
      const mockSettings = [
        {
          expiry_alert_id: 1,
          user_id: 'user-1',
          days_before_expiry: 3,
          alert_method: 'email' as AlertMethod,
          is_enabled: true,
          last_alert_sent_at: null,
          created_at: new Date().toISOString(),
          updated_at: new Date().toISOString(),
        },
      ];

      vi.mocked(expiryRepo.getUserAlertSettings).mockResolvedValueOnce(mockSettings);

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

      expect(result).toHaveLength(1);
      expect(result[0].alert_method).toBe('email');
    });
  });

  describe('updateAlertSettings', () => {
    it('should update alert settings', async () => {
      const mockUpdatedSettings = {
        expiry_alert_id: 1,
        user_id: 'user-1',
        days_before_expiry: 5,
        alert_method: 'email' as AlertMethod,
        is_enabled: true,
        last_alert_sent_at: null,
        created_at: new Date().toISOString(),
        updated_at: new Date().toISOString(),
      };

      vi.mocked(expiryRepo.upsertAlertSettings).mockResolvedValueOnce(mockUpdatedSettings);

      const result = await updateAlertSettings(
        'user-1',
        'email',
        { days_before_expiry: 5 },
        mockLogger,
      );

      expect(result.days_before_expiry).toBe(5);
    });
  });

  describe('processExpiryAlerts', () => {
    it('should process alerts for users with expiring items', async () => {
      vi.mocked(expiryRepo.getUsersWithExpiringItems).mockResolvedValueOnce([
        {
          user_id: 'user-1',
          email: 'user1@example.com',
          alert_method: 'email' as AlertMethod,
          days_before_expiry: 3,
        },
      ]);

      vi.mocked(expiryRepo.getExpiringItems).mockResolvedValueOnce([
        createMockInventoryItem({ days_until_expiry: 2 }),
      ]);

      vi.mocked(emailService.sendEmail).mockResolvedValueOnce(undefined);
      vi.mocked(expiryRepo.logAlert).mockResolvedValue(createMockAlertLogRecord());
      vi.mocked(expiryRepo.markAlertSent).mockResolvedValue(undefined);

      const alertsSent = await processExpiryAlerts(mockLogger);

      expect(alertsSent).toBe(1);
    });

    it('should skip users with no expiring items', async () => {
      vi.mocked(expiryRepo.getUsersWithExpiringItems).mockResolvedValueOnce([
        {
          user_id: 'user-1',
          email: 'user1@example.com',
          alert_method: 'email' as AlertMethod,
          days_before_expiry: 3,
        },
      ]);

      vi.mocked(expiryRepo.getExpiringItems).mockResolvedValueOnce([]);

      const alertsSent = await processExpiryAlerts(mockLogger);

      expect(alertsSent).toBe(0);
    });
  });

  describe('addItemsFromReceipt', () => {
    it('should add items from receipt to inventory', async () => {
      const mockReceipt = {
        receipt_id: 1,
        user_id: 'user-1',
        store_id: null,
        receipt_image_url: '/uploads/receipt.jpg',
        transaction_date: '2024-01-15',
        total_amount_cents: 2500,
        status: 'completed' as ReceiptStatus,
        raw_text: 'test 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(),
      };

      const mockReceiptItems = [
        {
          receipt_item_id: 1,
          receipt_id: 1,
          raw_item_description: 'MILK 2%',
          quantity: 1,
          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.getReceiptById).mockResolvedValueOnce(mockReceipt);
      vi.mocked(receiptRepo.getReceiptItems).mockResolvedValueOnce(mockReceiptItems);
      vi.mocked(expiryRepo.addInventoryItem).mockResolvedValueOnce(
        createMockInventoryItem({ inventory_id: 10 }),
      );
      vi.mocked(receiptRepo.updateReceiptItem).mockResolvedValueOnce(mockReceiptItems[0] as any);

      const result = await addItemsFromReceipt(
        'user-1',
        1,
        [{ receipt_item_id: 1, location: 'fridge', include: true }],
        mockLogger,
      );

      expect(result).toHaveLength(1);
      expect(receiptRepo.updateReceiptItem).toHaveBeenCalledWith(
        1,
        expect.objectContaining({ added_to_pantry: true }),
        expect.any(Object),
      );
    });

    it('should skip items with include: false', async () => {
      const mockReceipt = {
        receipt_id: 1,
        user_id: 'user-1',
        store_id: null,
        receipt_image_url: '/uploads/receipt.jpg',
        transaction_date: '2024-01-15',
        total_amount_cents: 2500,
        status: 'completed' as ReceiptStatus,
        raw_text: 'test 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 result = await addItemsFromReceipt(
        'user-1',
        1,
        [{ receipt_item_id: 1, include: false }],
        mockLogger,
      );

      expect(result).toHaveLength(0);
      expect(expiryRepo.addInventoryItem).not.toHaveBeenCalled();
    });
  });

  describe('getRecipeSuggestionsForExpiringItems', () => {
    it('should return recipes using expiring items', async () => {
      const expiringItems = [
        createMockInventoryItem({ master_item_id: 5, days_until_expiry: 2 }),
        createMockInventoryItem({ master_item_id: 10, days_until_expiry: 4 }),
      ];

      const mockRecipes = {
        recipes: [
          {
            recipe_id: 1,
            recipe_name: 'Quick Breakfast',
            description: 'Easy breakfast recipe',
            prep_time_minutes: 10,
            cook_time_minutes: 15,
            servings: 2,
            photo_url: null,
            matching_master_item_ids: [5],
            match_count: 1,
          },
        ],
        total: 1,
      };

      vi.mocked(expiryRepo.getExpiringItems).mockResolvedValueOnce(expiringItems);
      vi.mocked(expiryRepo.getRecipesForExpiringItems).mockResolvedValueOnce(mockRecipes);

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

      expect(result.recipes).toHaveLength(1);
      expect(result.recipes[0].matching_items).toHaveLength(1);
      expect(result.considered_items).toHaveLength(2);
    });

    it('should return empty results when no expiring items', async () => {
      vi.mocked(expiryRepo.getExpiringItems).mockResolvedValueOnce([]);

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

      expect(result.recipes).toHaveLength(0);
      expect(result.total).toBe(0);
    });
  });

  describe('processExpiryAlertJob', () => {
    it('should process user-specific alert job', async () => {
      vi.mocked(expiryRepo.getUserAlertSettings).mockResolvedValueOnce([
        {
          expiry_alert_id: 1,
          user_id: 'user-1',
          days_before_expiry: 7,
          alert_method: 'email' as AlertMethod,
          is_enabled: true,
          last_alert_sent_at: null,
          created_at: new Date().toISOString(),
          updated_at: new Date().toISOString(),
        },
      ]);

      vi.mocked(expiryRepo.getExpiringItems).mockResolvedValueOnce([
        createMockInventoryItem({ days_until_expiry: 3 }),
      ]);

      vi.mocked(expiryRepo.logAlert).mockResolvedValue(createMockAlertLogRecord());
      vi.mocked(expiryRepo.upsertAlertSettings).mockResolvedValue({
        expiry_alert_id: 1,
        user_id: 'user-1',
        days_before_expiry: 7,
        alert_method: 'email' as AlertMethod,
        is_enabled: true,
        last_alert_sent_at: new Date().toISOString(),
        created_at: new Date().toISOString(),
        updated_at: new Date().toISOString(),
      });

      const mockJob = {
        id: 'job-1',
        data: {
          alertType: 'user_specific' as const,
          userId: 'user-1',
          daysAhead: 7,
          meta: { requestId: 'req-1' },
        },
      } as Job<ExpiryAlertJobData>;

      const result = await processExpiryAlertJob(mockJob, mockLogger);

      expect(result.success).toBe(true);
      expect(result.alertsSent).toBe(1);
      expect(result.usersNotified).toBe(1);
    });

    it('should process daily check job for all users', async () => {
      vi.mocked(expiryRepo.getUsersWithExpiringItems).mockResolvedValueOnce([
        {
          user_id: 'user-1',
          email: 'user1@example.com',
          alert_method: 'email' as AlertMethod,
          days_before_expiry: 7,
        },
        {
          user_id: 'user-2',
          email: 'user2@example.com',
          alert_method: 'email' as AlertMethod,
          days_before_expiry: 7,
        },
      ]);

      vi.mocked(expiryRepo.getUserAlertSettings)
        .mockResolvedValueOnce([
          {
            expiry_alert_id: 1,
            user_id: 'user-1',
            days_before_expiry: 7,
            alert_method: 'email' as AlertMethod,
            is_enabled: true,
            last_alert_sent_at: null,
            created_at: new Date().toISOString(),
            updated_at: new Date().toISOString(),
          },
        ])
        .mockResolvedValueOnce([
          {
            expiry_alert_id: 2,
            user_id: 'user-2',
            days_before_expiry: 7,
            alert_method: 'email' as AlertMethod,
            is_enabled: true,
            last_alert_sent_at: null,
            created_at: new Date().toISOString(),
            updated_at: new Date().toISOString(),
          },
        ]);

      vi.mocked(expiryRepo.getExpiringItems)
        .mockResolvedValueOnce([createMockInventoryItem({ days_until_expiry: 3 })])
        .mockResolvedValueOnce([createMockInventoryItem({ days_until_expiry: 5 })]);

      vi.mocked(expiryRepo.logAlert).mockResolvedValue(createMockAlertLogRecord());
      vi.mocked(expiryRepo.upsertAlertSettings).mockResolvedValue({
        expiry_alert_id: 1,
        user_id: 'user-1',
        days_before_expiry: 7,
        alert_method: 'email' as AlertMethod,
        is_enabled: true,
        last_alert_sent_at: new Date().toISOString(),
        created_at: new Date().toISOString(),
        updated_at: new Date().toISOString(),
      });

      const mockJob = {
        id: 'job-2',
        data: {
          alertType: 'daily_check' as const,
          daysAhead: 7,
        },
      } as Job<ExpiryAlertJobData>;

      const result = await processExpiryAlertJob(mockJob, mockLogger);

      expect(result.success).toBe(true);
      expect(result.usersNotified).toBe(2);
    });

    it('should handle job processing errors', async () => {
      vi.mocked(expiryRepo.getUserAlertSettings).mockRejectedValueOnce(new Error('DB error'));

      const mockJob = {
        id: 'job-3',
        data: {
          alertType: 'user_specific' as const,
          userId: 'user-1',
        },
      } as Job<ExpiryAlertJobData>;

      await expect(processExpiryAlertJob(mockJob, mockLogger)).rejects.toThrow('DB error');
    });
  });
});

// Helper function to create mock inventory items
function createMockInventoryItem(
  overrides: Partial<{
    inventory_id: number;
    master_item_id: number | null;
    days_until_expiry: number | null;
  }>,
): UserInventoryItem {
  const daysUntilExpiry = overrides.days_until_expiry ?? 5;
  const expiryStatus: ExpiryStatus =
    daysUntilExpiry !== null && daysUntilExpiry < 0
      ? 'expired'
      : daysUntilExpiry !== null && daysUntilExpiry <= 7
        ? 'expiring_soon'
        : 'fresh';
  return {
    inventory_id: overrides.inventory_id ?? 1,
    user_id: 'user-1',
    product_id: null,
    master_item_id: overrides.master_item_id ?? null,
    item_name: 'Test Item',
    quantity: 1,
    unit: null,
    purchase_date: null,
    expiry_date: '2024-01-25',
    source: 'manual' as InventorySource,
    location: 'fridge' as StorageLocation,
    notes: null,
    is_consumed: false,
    consumed_at: null,
    expiry_source: null,
    receipt_item_id: null,
    pantry_location_id: null,
    notification_sent_at: null,
    created_at: new Date().toISOString(),
    updated_at: new Date().toISOString(),
    days_until_expiry: daysUntilExpiry,
    expiry_status: expiryStatus,
  };
}
