// src/services/upcService.server.test.ts
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import type { Logger } from 'pino';
import { createMockLogger } from '../tests/utils/mockLogger';
import type { UpcScanSource, UpcExternalLookupRecord, UpcExternalSource } from '../types/upc';

// Mock dependencies
vi.mock('./db/index.db', () => ({
  upcRepo: {
    recordScan: vi.fn(),
    findProductByUpc: vi.fn(),
    findExternalLookup: vi.fn(),
    upsertExternalLookup: vi.fn(),
    linkUpcToProduct: vi.fn(),
    getScanHistory: vi.fn(),
    getUserScanStats: vi.fn(),
    getScanById: vi.fn(),
  },
}));

vi.mock('../config/env', () => ({
  config: {
    upc: {
      upcItemDbApiKey: undefined,
      barcodeLookupApiKey: undefined,
    },
  },
  isUpcItemDbConfigured: false,
  isBarcodeLookupConfigured: false,
}));

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

// Mock global fetch
const mockFetch = vi.fn();
global.fetch = mockFetch;

// Import after mocks are set up
import {
  isValidUpcCode,
  normalizeUpcCode,
  detectBarcodeFromImage,
  lookupExternalUpc,
  scanUpc,
  lookupUpc,
  linkUpcToProduct,
  getScanHistory,
  getScanStats,
  getScanById,
} from './upcService.server';

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

// Helper to create mock UpcExternalLookupRecord
function createMockExternalLookupRecord(
  overrides: Partial<UpcExternalLookupRecord> = {},
): UpcExternalLookupRecord {
  return {
    lookup_id: 1,
    upc_code: '012345678905',
    product_name: null,
    brand_name: null,
    category: null,
    description: null,
    image_url: null,
    external_source: 'openfoodfacts' as UpcExternalSource,
    lookup_data: null,
    lookup_successful: false,
    created_at: new Date().toISOString(),
    updated_at: new Date().toISOString(),
    ...overrides,
  };
}

// Helper to create mock ProductRow (from db layer - matches upc.db.ts)
interface ProductRow {
  product_id: number;
  name: string;
  brand_id: number | null;
  category_id: number | null;
  description: string | null;
  size: string | null;
  upc_code: string | null;
  master_item_id: number | null;
  created_at: string;
  updated_at: string;
}

function createMockProductRow(overrides: Partial<ProductRow> = {}): ProductRow {
  return {
    product_id: 1,
    name: 'Test Product',
    brand_id: null,
    category_id: null,
    description: null,
    size: null,
    upc_code: '012345678905',
    master_item_id: null,
    created_at: new Date().toISOString(),
    updated_at: new Date().toISOString(),
    ...overrides,
  };
}

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

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

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

  describe('isValidUpcCode', () => {
    it('should return true for valid 12-digit UPC-A', () => {
      expect(isValidUpcCode('012345678905')).toBe(true);
    });

    it('should return true for valid 8-digit UPC-E', () => {
      expect(isValidUpcCode('01234567')).toBe(true);
    });

    it('should return true for valid 13-digit EAN-13', () => {
      expect(isValidUpcCode('5901234123457')).toBe(true);
    });

    it('should return true for valid 8-digit EAN-8', () => {
      expect(isValidUpcCode('96385074')).toBe(true);
    });

    it('should return true for valid 14-digit GTIN-14', () => {
      expect(isValidUpcCode('00012345678905')).toBe(true);
    });

    it('should return false for code with less than 8 digits', () => {
      expect(isValidUpcCode('1234567')).toBe(false);
    });

    it('should return false for code with more than 14 digits', () => {
      expect(isValidUpcCode('123456789012345')).toBe(false);
    });

    it('should return false for code with non-numeric characters', () => {
      expect(isValidUpcCode('01234567890A')).toBe(false);
    });

    it('should return false for empty string', () => {
      expect(isValidUpcCode('')).toBe(false);
    });
  });

  describe('normalizeUpcCode', () => {
    it('should remove spaces from UPC code', () => {
      expect(normalizeUpcCode('012 345 678 905')).toBe('012345678905');
    });

    it('should remove dashes from UPC code', () => {
      expect(normalizeUpcCode('012-345-678-905')).toBe('012345678905');
    });

    it('should remove mixed spaces and dashes', () => {
      expect(normalizeUpcCode('012-345 678-905')).toBe('012345678905');
    });

    it('should return unchanged if no spaces or dashes', () => {
      expect(normalizeUpcCode('012345678905')).toBe('012345678905');
    });
  });

  describe('detectBarcodeFromImage', () => {
    it('should return not implemented error', async () => {
      const result = await detectBarcodeFromImage('base64imagedata', mockLogger);

      expect(result.detected).toBe(false);
      expect(result.upc_code).toBeNull();
      expect(result.error).toBe(
        'Barcode detection from images is not yet implemented. Please use manual entry.',
      );
    });
  });

  describe('lookupExternalUpc', () => {
    it('should return product info from Open Food Facts on success', async () => {
      mockFetch.mockResolvedValueOnce({
        ok: true,
        json: async () => ({
          status: 1,
          product: {
            product_name: 'Test Product',
            brands: 'Test Brand',
            categories_tags: ['en:snacks'],
            ingredients_text: 'Test ingredients',
            image_url: 'https://example.com/image.jpg',
          },
        }),
      });

      const result = await lookupExternalUpc('012345678905', mockLogger);

      expect(result).not.toBeNull();
      expect(result?.name).toBe('Test Product');
      expect(result?.brand).toBe('Test Brand');
      expect(result?.source).toBe('openfoodfacts');
    });

    it('should return null when Open Food Facts returns status 0', async () => {
      mockFetch.mockResolvedValueOnce({
        ok: true,
        json: async () => ({
          status: 0,
          product: null,
        }),
      });

      const result = await lookupExternalUpc('012345678905', mockLogger);

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

    it('should return null when Open Food Facts request fails', async () => {
      mockFetch.mockResolvedValueOnce({
        ok: false,
        status: 500,
      });

      const result = await lookupExternalUpc('012345678905', mockLogger);

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

    it('should return null on network error', async () => {
      mockFetch.mockRejectedValueOnce(new Error('Network error'));

      const result = await lookupExternalUpc('012345678905', mockLogger);

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

    it('should use generic_name when product_name is missing', async () => {
      mockFetch.mockResolvedValueOnce({
        ok: true,
        json: async () => ({
          status: 1,
          product: {
            generic_name: 'Generic Product Name',
            brands: null,
          },
        }),
      });

      const result = await lookupExternalUpc('012345678905', mockLogger);

      expect(result?.name).toBe('Generic Product Name');
    });
  });

  describe('scanUpc', () => {
    it('should scan with manual entry and return product from database', async () => {
      const mockProduct = {
        product_id: 1,
        name: 'Test Product',
        brand: 'Test Brand',
        category: 'Snacks',
        description: null,
        size: '100g',
        upc_code: '012345678905',
        image_url: null,
        master_item_id: null,
      };

      vi.mocked(upcRepo.findProductByUpc).mockResolvedValueOnce(mockProduct);
      vi.mocked(upcRepo.recordScan).mockResolvedValueOnce({
        scan_id: 1,
        user_id: 'user-1',
        upc_code: '012345678905',
        product_id: 1,
        scan_source: 'manual_entry',
        scan_confidence: 1.0,
        raw_image_path: null,
        lookup_successful: true,
        created_at: new Date().toISOString(),
        updated_at: new Date().toISOString(),
      });

      const result = await scanUpc(
        'user-1',
        { upc_code: '012345678905', scan_source: 'manual_entry' },
        mockLogger,
      );

      expect(result.upc_code).toBe('012345678905');
      expect(result.product).toEqual(mockProduct);
      expect(result.lookup_successful).toBe(true);
      expect(result.is_new_product).toBe(false);
      expect(result.confidence).toBe(1.0);
    });

    it('should scan with manual entry and perform external lookup when not in database', async () => {
      vi.mocked(upcRepo.findProductByUpc).mockResolvedValueOnce(null);
      vi.mocked(upcRepo.findExternalLookup).mockResolvedValueOnce(null);
      vi.mocked(upcRepo.upsertExternalLookup).mockResolvedValueOnce(
        createMockExternalLookupRecord(),
      );
      vi.mocked(upcRepo.recordScan).mockResolvedValueOnce({
        scan_id: 2,
        user_id: 'user-1',
        upc_code: '012345678905',
        product_id: null,
        scan_source: 'manual_entry',
        scan_confidence: 1.0,
        raw_image_path: null,
        lookup_successful: true,
        created_at: new Date().toISOString(),
        updated_at: new Date().toISOString(),
      });

      mockFetch.mockResolvedValueOnce({
        ok: true,
        json: async () => ({
          status: 1,
          product: {
            product_name: 'External Product',
            brands: 'External Brand',
          },
        }),
      });

      const result = await scanUpc(
        'user-1',
        { upc_code: '012345678905', scan_source: 'manual_entry' },
        mockLogger,
      );

      expect(result.product).toBeNull();
      expect(result.external_lookup).not.toBeNull();
      expect(result.external_lookup?.name).toBe('External Product');
      expect(result.is_new_product).toBe(true);
    });

    it('should use cached external lookup when available', async () => {
      vi.mocked(upcRepo.findProductByUpc).mockResolvedValueOnce(null);
      vi.mocked(upcRepo.findExternalLookup).mockResolvedValueOnce({
        lookup_id: 1,
        upc_code: '012345678905',
        product_name: 'Cached Product',
        brand_name: 'Cached Brand',
        category: 'Cached Category',
        description: null,
        image_url: null,
        external_source: 'openfoodfacts',
        lookup_data: null,
        lookup_successful: true,
        created_at: new Date().toISOString(),
        updated_at: new Date().toISOString(),
      });
      vi.mocked(upcRepo.recordScan).mockResolvedValueOnce({
        scan_id: 3,
        user_id: 'user-1',
        upc_code: '012345678905',
        product_id: null,
        scan_source: 'manual_entry',
        scan_confidence: 1.0,
        raw_image_path: null,
        lookup_successful: true,
        created_at: new Date().toISOString(),
        updated_at: new Date().toISOString(),
      });

      const result = await scanUpc(
        'user-1',
        { upc_code: '012345678905', scan_source: 'manual_entry' },
        mockLogger,
      );

      expect(result.external_lookup?.name).toBe('Cached Product');
      expect(mockFetch).not.toHaveBeenCalled();
    });

    it('should throw error for invalid UPC code format', async () => {
      await expect(
        scanUpc('user-1', { upc_code: 'invalid', scan_source: 'manual_entry' }, mockLogger),
      ).rejects.toThrow('Invalid UPC code format. UPC codes must be 8-14 digits.');
    });

    it('should throw error when neither upc_code nor image_base64 provided', async () => {
      await expect(
        scanUpc('user-1', { scan_source: 'manual_entry' } as any, mockLogger),
      ).rejects.toThrow('Either upc_code or image_base64 must be provided.');
    });

    it('should record failed scan when image detection fails', async () => {
      vi.mocked(upcRepo.recordScan).mockResolvedValueOnce({
        scan_id: 4,
        user_id: 'user-1',
        upc_code: 'DETECTION_FAILED',
        product_id: null,
        scan_source: 'image_upload',
        scan_confidence: 0,
        raw_image_path: null,
        lookup_successful: false,
        created_at: new Date().toISOString(),
        updated_at: new Date().toISOString(),
      });

      const result = await scanUpc(
        'user-1',
        { image_base64: 'base64data', scan_source: 'image_upload' },
        mockLogger,
      );

      expect(result.lookup_successful).toBe(false);
      expect(result.confidence).toBe(0);
    });
  });

  describe('lookupUpc', () => {
    it('should return product from database when found', async () => {
      const mockProduct = {
        product_id: 1,
        name: 'Test Product',
        brand: 'Test Brand',
        category: 'Snacks',
        description: null,
        size: '100g',
        upc_code: '012345678905',
        image_url: null,
        master_item_id: null,
      };

      vi.mocked(upcRepo.findProductByUpc).mockResolvedValueOnce(mockProduct);

      const result = await lookupUpc({ upc_code: '012345678905' }, mockLogger);

      expect(result.found).toBe(true);
      expect(result.product).toEqual(mockProduct);
      expect(result.from_cache).toBe(false);
    });

    it('should return cached external lookup when available', async () => {
      vi.mocked(upcRepo.findProductByUpc).mockResolvedValueOnce(null);
      vi.mocked(upcRepo.findExternalLookup).mockResolvedValueOnce({
        lookup_id: 1,
        upc_code: '012345678905',
        product_name: 'Cached Product',
        brand_name: null,
        category: null,
        description: null,
        image_url: null,
        external_source: 'openfoodfacts',
        lookup_data: null,
        lookup_successful: true,
        created_at: new Date().toISOString(),
        updated_at: new Date().toISOString(),
      });

      const result = await lookupUpc({ upc_code: '012345678905' }, mockLogger);

      expect(result.found).toBe(true);
      expect(result.from_cache).toBe(true);
      expect(result.external_lookup?.name).toBe('Cached Product');
    });

    it('should return cached unsuccessful lookup', async () => {
      vi.mocked(upcRepo.findProductByUpc).mockResolvedValueOnce(null);
      vi.mocked(upcRepo.findExternalLookup).mockResolvedValueOnce({
        lookup_id: 1,
        upc_code: '012345678905',
        product_name: null,
        brand_name: null,
        category: null,
        description: null,
        image_url: null,
        external_source: 'unknown',
        lookup_data: null,
        lookup_successful: false,
        created_at: new Date().toISOString(),
        updated_at: new Date().toISOString(),
      });

      const result = await lookupUpc({ upc_code: '012345678905' }, mockLogger);

      expect(result.found).toBe(false);
      expect(result.from_cache).toBe(true);
    });

    it('should perform fresh external lookup when force_refresh is true', async () => {
      vi.mocked(upcRepo.findProductByUpc).mockResolvedValueOnce(null);
      vi.mocked(upcRepo.upsertExternalLookup).mockResolvedValueOnce(
        createMockExternalLookupRecord(),
      );

      mockFetch.mockResolvedValueOnce({
        ok: true,
        json: async () => ({
          status: 1,
          product: {
            product_name: 'Fresh External Product',
            brands: 'Fresh Brand',
          },
        }),
      });

      const result = await lookupUpc({ upc_code: '012345678905', force_refresh: true }, mockLogger);

      expect(result.from_cache).toBe(false);
      expect(result.external_lookup?.name).toBe('Fresh External Product');
      expect(upcRepo.findExternalLookup).not.toHaveBeenCalled();
    });

    it('should throw error for invalid UPC code', async () => {
      await expect(lookupUpc({ upc_code: 'invalid' }, mockLogger)).rejects.toThrow(
        'Invalid UPC code format. UPC codes must be 8-14 digits.',
      );
    });

    it('should normalize UPC code before lookup', async () => {
      vi.mocked(upcRepo.findProductByUpc).mockResolvedValueOnce(null);
      vi.mocked(upcRepo.findExternalLookup).mockResolvedValueOnce(null);
      vi.mocked(upcRepo.upsertExternalLookup).mockResolvedValueOnce(
        createMockExternalLookupRecord(),
      );

      mockFetch.mockResolvedValueOnce({
        ok: true,
        json: async () => ({ status: 0 }),
      });

      const result = await lookupUpc({ upc_code: '012-345-678-905' }, mockLogger);

      expect(result.upc_code).toBe('012345678905');
    });
  });

  describe('linkUpcToProduct', () => {
    it('should link UPC code to product successfully', async () => {
      vi.mocked(upcRepo.linkUpcToProduct).mockResolvedValueOnce(createMockProductRow());

      await linkUpcToProduct(1, '012345678905', mockLogger);

      expect(upcRepo.linkUpcToProduct).toHaveBeenCalledWith(1, '012345678905', mockLogger);
    });

    it('should throw error for invalid UPC code', async () => {
      await expect(linkUpcToProduct(1, 'invalid', mockLogger)).rejects.toThrow(
        'Invalid UPC code format. UPC codes must be 8-14 digits.',
      );
    });

    it('should normalize UPC code before linking', async () => {
      vi.mocked(upcRepo.linkUpcToProduct).mockResolvedValueOnce(createMockProductRow());

      await linkUpcToProduct(1, '012-345-678-905', mockLogger);

      expect(upcRepo.linkUpcToProduct).toHaveBeenCalledWith(1, '012345678905', mockLogger);
    });
  });

  describe('getScanHistory', () => {
    it('should return paginated scan history', async () => {
      const mockHistory = {
        scans: [
          {
            scan_id: 1,
            user_id: 'user-1',
            upc_code: '012345678905',
            product_id: 1,
            scan_source: 'manual_entry' as UpcScanSource,
            scan_confidence: 1.0,
            raw_image_path: null,
            lookup_successful: true,
            created_at: new Date().toISOString(),
            updated_at: new Date().toISOString(),
          },
        ],
        total: 1,
      };

      vi.mocked(upcRepo.getScanHistory).mockResolvedValueOnce(mockHistory);

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

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

    it('should filter by scan source', async () => {
      vi.mocked(upcRepo.getScanHistory).mockResolvedValueOnce({ scans: [], total: 0 });

      await getScanHistory({ user_id: 'user-1', scan_source: 'image_upload' }, mockLogger);

      expect(upcRepo.getScanHistory).toHaveBeenCalledWith(
        { user_id: 'user-1', scan_source: 'image_upload' },
        mockLogger,
      );
    });

    it('should filter by date range', async () => {
      vi.mocked(upcRepo.getScanHistory).mockResolvedValueOnce({ scans: [], total: 0 });

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

      expect(upcRepo.getScanHistory).toHaveBeenCalledWith(
        {
          user_id: 'user-1',
          from_date: '2024-01-01',
          to_date: '2024-01-31',
        },
        mockLogger,
      );
    });
  });

  describe('getScanStats', () => {
    it('should return user scan statistics', async () => {
      const mockStats = {
        total_scans: 100,
        successful_lookups: 80,
        unique_products: 50,
        scans_today: 5,
        scans_this_week: 20,
      };

      vi.mocked(upcRepo.getUserScanStats).mockResolvedValueOnce(mockStats);

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

      expect(result).toEqual(mockStats);
      expect(upcRepo.getUserScanStats).toHaveBeenCalledWith('user-1', mockLogger);
    });
  });

  describe('getScanById', () => {
    it('should return scan record by ID', async () => {
      const mockScan = {
        scan_id: 1,
        user_id: 'user-1',
        upc_code: '012345678905',
        product_id: 1,
        scan_source: 'manual_entry' as UpcScanSource,
        scan_confidence: 1.0,
        raw_image_path: null,
        lookup_successful: true,
        created_at: new Date().toISOString(),
        updated_at: new Date().toISOString(),
      };

      vi.mocked(upcRepo.getScanById).mockResolvedValueOnce(mockScan);

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

      expect(result).toEqual(mockScan);
      expect(upcRepo.getScanById).toHaveBeenCalledWith(1, 'user-1', mockLogger);
    });
  });

  describe('lookupExternalUpc - additional coverage', () => {
    it('should use image_front_url as fallback when image_url is missing', async () => {
      mockFetch.mockResolvedValueOnce({
        ok: true,
        json: async () => ({
          status: 1,
          product: {
            product_name: 'Test Product',
            brands: 'Test Brand',
            image_url: null,
            image_front_url: 'https://example.com/front.jpg',
          },
        }),
      });

      const result = await lookupExternalUpc('012345678905', mockLogger);

      expect(result?.image_url).toBe('https://example.com/front.jpg');
    });

    it('should return Unknown Product when both product_name and generic_name are missing', async () => {
      mockFetch.mockResolvedValueOnce({
        ok: true,
        json: async () => ({
          status: 1,
          product: {
            brands: 'Test Brand',
            // No product_name or generic_name
          },
        }),
      });

      const result = await lookupExternalUpc('012345678905', mockLogger);

      expect(result?.name).toBe('Unknown Product');
    });

    it('should handle category without en: prefix', async () => {
      mockFetch.mockResolvedValueOnce({
        ok: true,
        json: async () => ({
          status: 1,
          product: {
            product_name: 'Test Product',
            categories_tags: ['snacks'], // No en: prefix
          },
        }),
      });

      const result = await lookupExternalUpc('012345678905', mockLogger);

      expect(result?.category).toBe('snacks');
    });

    it('should handle non-Error thrown in catch block', async () => {
      mockFetch.mockRejectedValueOnce('String error');

      const result = await lookupExternalUpc('012345678905', mockLogger);

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

  describe('scanUpc - additional coverage', () => {
    it('should not set external_lookup when cached lookup was unsuccessful', async () => {
      vi.mocked(upcRepo.findProductByUpc).mockResolvedValueOnce(null);
      vi.mocked(upcRepo.findExternalLookup).mockResolvedValueOnce({
        lookup_id: 1,
        upc_code: '012345678905',
        product_name: null,
        brand_name: null,
        category: null,
        description: null,
        image_url: null,
        external_source: 'unknown',
        lookup_data: null,
        lookup_successful: false,
        created_at: new Date().toISOString(),
        updated_at: new Date().toISOString(),
      });
      vi.mocked(upcRepo.recordScan).mockResolvedValueOnce({
        scan_id: 5,
        user_id: 'user-1',
        upc_code: '012345678905',
        product_id: null,
        scan_source: 'manual_entry',
        scan_confidence: 1.0,
        raw_image_path: null,
        lookup_successful: false,
        created_at: new Date().toISOString(),
        updated_at: new Date().toISOString(),
      });

      const result = await scanUpc(
        'user-1',
        { upc_code: '012345678905', scan_source: 'manual_entry' },
        mockLogger,
      );

      expect(result.external_lookup).toBeNull();
      expect(result.lookup_successful).toBe(false);
      expect(mockFetch).not.toHaveBeenCalled();
    });

    it('should cache unsuccessful external lookup result', async () => {
      vi.mocked(upcRepo.findProductByUpc).mockResolvedValueOnce(null);
      vi.mocked(upcRepo.findExternalLookup).mockResolvedValueOnce(null);
      vi.mocked(upcRepo.upsertExternalLookup).mockResolvedValueOnce(
        createMockExternalLookupRecord(),
      );
      vi.mocked(upcRepo.recordScan).mockResolvedValueOnce({
        scan_id: 6,
        user_id: 'user-1',
        upc_code: '012345678905',
        product_id: null,
        scan_source: 'manual_entry',
        scan_confidence: 1.0,
        raw_image_path: null,
        lookup_successful: false,
        created_at: new Date().toISOString(),
        updated_at: new Date().toISOString(),
      });

      // External lookup returns nothing
      mockFetch.mockResolvedValueOnce({
        ok: true,
        json: async () => ({ status: 0, product: null }),
      });

      const result = await scanUpc(
        'user-1',
        { upc_code: '012345678905', scan_source: 'manual_entry' },
        mockLogger,
      );

      expect(result.external_lookup).toBeNull();
      expect(upcRepo.upsertExternalLookup).toHaveBeenCalledWith(
        '012345678905',
        'unknown',
        false,
        expect.anything(),
        {},
      );
    });
  });

  describe('lookupUpc - additional coverage', () => {
    it('should cache unsuccessful external lookup and return found=false', async () => {
      vi.mocked(upcRepo.findProductByUpc).mockResolvedValueOnce(null);
      vi.mocked(upcRepo.findExternalLookup).mockResolvedValueOnce(null);
      vi.mocked(upcRepo.upsertExternalLookup).mockResolvedValueOnce(
        createMockExternalLookupRecord(),
      );

      // External lookup returns nothing
      mockFetch.mockResolvedValueOnce({
        ok: true,
        json: async () => ({ status: 0, product: null }),
      });

      const result = await lookupUpc({ upc_code: '012345678905' }, mockLogger);

      expect(result.found).toBe(false);
      expect(result.from_cache).toBe(false);
      expect(result.external_lookup).toBeNull();
    });

    it('should use custom max_cache_age_hours', async () => {
      vi.mocked(upcRepo.findProductByUpc).mockResolvedValueOnce(null);
      vi.mocked(upcRepo.findExternalLookup).mockResolvedValueOnce(null);
      vi.mocked(upcRepo.upsertExternalLookup).mockResolvedValueOnce(
        createMockExternalLookupRecord(),
      );

      mockFetch.mockResolvedValueOnce({
        ok: true,
        json: async () => ({ status: 0, product: null }),
      });

      await lookupUpc({ upc_code: '012345678905', max_cache_age_hours: 24 }, mockLogger);

      expect(upcRepo.findExternalLookup).toHaveBeenCalledWith(
        '012345678905',
        24,
        expect.anything(),
      );
    });
  });
});

/**
 * Tests for UPC Item DB and Barcode Lookup APIs when configured.
 * These require separate describe blocks to re-mock the config module.
 */
describe('upcService.server - with API keys configured', () => {
  let mockLogger: Logger;
  const mockFetch = vi.fn();

  beforeEach(async () => {
    vi.clearAllMocks();
    vi.resetModules();
    global.fetch = mockFetch;
    mockFetch.mockReset();

    // Re-mock with API keys configured
    vi.doMock('../config/env', () => ({
      config: {
        upc: {
          upcItemDbApiKey: 'test-upcitemdb-key',
          barcodeLookupApiKey: 'test-barcodelookup-key',
        },
      },
      isUpcItemDbConfigured: true,
      isBarcodeLookupConfigured: true,
    }));

    vi.doMock('./db/index.db', () => ({
      upcRepo: {
        recordScan: vi.fn(),
        findProductByUpc: vi.fn(),
        findExternalLookup: vi.fn(),
        upsertExternalLookup: vi.fn(),
        linkUpcToProduct: vi.fn(),
        getScanHistory: vi.fn(),
        getUserScanStats: vi.fn(),
        getScanById: vi.fn(),
      },
    }));

    mockLogger = createMockLogger();
  });

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

  describe('lookupExternalUpc with UPC Item DB', () => {
    it('should return product from UPC Item DB when Open Food Facts has no result', async () => {
      // Open Food Facts returns nothing
      mockFetch
        .mockResolvedValueOnce({
          ok: true,
          json: async () => ({ status: 0, product: null }),
        })
        // UPC Item DB returns product
        .mockResolvedValueOnce({
          ok: true,
          json: async () => ({
            code: 'OK',
            items: [
              {
                title: 'UPC Item DB Product',
                brand: 'UPC Brand',
                category: 'Electronics',
                description: 'A test product',
                images: ['https://example.com/upcitemdb.jpg'],
              },
            ],
          }),
        });

      const { lookupExternalUpc } = await import('./upcService.server');
      const result = await lookupExternalUpc('012345678905', mockLogger);

      expect(result).not.toBeNull();
      expect(result?.name).toBe('UPC Item DB Product');
      expect(result?.brand).toBe('UPC Brand');
      expect(result?.source).toBe('upcitemdb');
    });

    it('should handle UPC Item DB rate limit (429)', async () => {
      // Open Food Facts returns nothing
      mockFetch
        .mockResolvedValueOnce({
          ok: true,
          json: async () => ({ status: 0, product: null }),
        })
        // UPC Item DB rate limit
        .mockResolvedValueOnce({
          ok: false,
          status: 429,
        })
        // Barcode Lookup also returns nothing
        .mockResolvedValueOnce({
          ok: false,
          status: 404,
        });

      const { lookupExternalUpc } = await import('./upcService.server');
      const result = await lookupExternalUpc('012345678905', mockLogger);

      expect(result).toBeNull();
      expect(mockLogger.warn).toHaveBeenCalledWith(
        { upcCode: '012345678905' },
        'UPC Item DB rate limit exceeded',
      );
    });

    it('should handle UPC Item DB network error', async () => {
      // Open Food Facts returns nothing
      mockFetch
        .mockResolvedValueOnce({
          ok: true,
          json: async () => ({ status: 0, product: null }),
        })
        // UPC Item DB network error
        .mockRejectedValueOnce(new Error('Network error'))
        // Barcode Lookup also errors
        .mockRejectedValueOnce(new Error('Network error'));

      const { lookupExternalUpc } = await import('./upcService.server');
      const result = await lookupExternalUpc('012345678905', mockLogger);

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

    it('should handle UPC Item DB empty items array', async () => {
      // Open Food Facts returns nothing
      mockFetch
        .mockResolvedValueOnce({
          ok: true,
          json: async () => ({ status: 0, product: null }),
        })
        // UPC Item DB returns empty items
        .mockResolvedValueOnce({
          ok: true,
          json: async () => ({ code: 'OK', items: [] }),
        })
        // Barcode Lookup also returns nothing
        .mockResolvedValueOnce({
          ok: false,
          status: 404,
        });

      const { lookupExternalUpc } = await import('./upcService.server');
      const result = await lookupExternalUpc('012345678905', mockLogger);

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

    it('should return Unknown Product when UPC Item DB item has no title', async () => {
      // Open Food Facts returns nothing
      mockFetch
        .mockResolvedValueOnce({
          ok: true,
          json: async () => ({ status: 0, product: null }),
        })
        // UPC Item DB returns item without title
        .mockResolvedValueOnce({
          ok: true,
          json: async () => ({
            code: 'OK',
            items: [{ brand: 'Some Brand' }],
          }),
        });

      const { lookupExternalUpc } = await import('./upcService.server');
      const result = await lookupExternalUpc('012345678905', mockLogger);

      expect(result?.name).toBe('Unknown Product');
      expect(result?.source).toBe('upcitemdb');
    });
  });

  describe('lookupExternalUpc with Barcode Lookup', () => {
    it('should return product from Barcode Lookup when other APIs have no result', async () => {
      // Open Food Facts returns nothing
      mockFetch
        .mockResolvedValueOnce({
          ok: true,
          json: async () => ({ status: 0, product: null }),
        })
        // UPC Item DB returns nothing
        .mockResolvedValueOnce({
          ok: true,
          json: async () => ({ code: 'OK', items: [] }),
        })
        // Barcode Lookup returns product
        .mockResolvedValueOnce({
          ok: true,
          json: async () => ({
            products: [
              {
                title: 'Barcode Lookup Product',
                brand: 'BL Brand',
                category: 'Food',
                description: 'A barcode lookup product',
                images: ['https://example.com/barcodelookup.jpg'],
              },
            ],
          }),
        });

      const { lookupExternalUpc } = await import('./upcService.server');
      const result = await lookupExternalUpc('012345678905', mockLogger);

      expect(result).not.toBeNull();
      expect(result?.name).toBe('Barcode Lookup Product');
      expect(result?.source).toBe('barcodelookup');
    });

    it('should handle Barcode Lookup rate limit (429)', async () => {
      // Open Food Facts returns nothing
      mockFetch
        .mockResolvedValueOnce({
          ok: true,
          json: async () => ({ status: 0, product: null }),
        })
        // UPC Item DB returns nothing
        .mockResolvedValueOnce({
          ok: true,
          json: async () => ({ code: 'OK', items: [] }),
        })
        // Barcode Lookup rate limit
        .mockResolvedValueOnce({
          ok: false,
          status: 429,
        });

      const { lookupExternalUpc } = await import('./upcService.server');
      const result = await lookupExternalUpc('012345678905', mockLogger);

      expect(result).toBeNull();
      expect(mockLogger.warn).toHaveBeenCalledWith(
        { upcCode: '012345678905' },
        'Barcode Lookup rate limit exceeded',
      );
    });

    it('should handle Barcode Lookup 404 response', async () => {
      // Open Food Facts returns nothing
      mockFetch
        .mockResolvedValueOnce({
          ok: true,
          json: async () => ({ status: 0, product: null }),
        })
        // UPC Item DB returns nothing
        .mockResolvedValueOnce({
          ok: true,
          json: async () => ({ code: 'OK', items: [] }),
        })
        // Barcode Lookup 404
        .mockResolvedValueOnce({
          ok: false,
          status: 404,
        });

      const { lookupExternalUpc } = await import('./upcService.server');
      const result = await lookupExternalUpc('012345678905', mockLogger);

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

    it('should use product_name fallback when title is missing in Barcode Lookup', async () => {
      // Open Food Facts returns nothing
      mockFetch
        .mockResolvedValueOnce({
          ok: true,
          json: async () => ({ status: 0, product: null }),
        })
        // UPC Item DB returns nothing
        .mockResolvedValueOnce({
          ok: true,
          json: async () => ({ code: 'OK', items: [] }),
        })
        // Barcode Lookup with product_name instead of title
        .mockResolvedValueOnce({
          ok: true,
          json: async () => ({
            products: [
              {
                product_name: 'Product Name Fallback',
                brand: 'BL Brand',
              },
            ],
          }),
        });

      const { lookupExternalUpc } = await import('./upcService.server');
      const result = await lookupExternalUpc('012345678905', mockLogger);

      expect(result?.name).toBe('Product Name Fallback');
    });

    it('should handle Barcode Lookup network error', async () => {
      // Open Food Facts returns nothing
      mockFetch
        .mockResolvedValueOnce({
          ok: true,
          json: async () => ({ status: 0, product: null }),
        })
        // UPC Item DB returns nothing
        .mockResolvedValueOnce({
          ok: true,
          json: async () => ({ code: 'OK', items: [] }),
        })
        // Barcode Lookup network error
        .mockRejectedValueOnce(new Error('Network error'));

      const { lookupExternalUpc } = await import('./upcService.server');
      const result = await lookupExternalUpc('012345678905', mockLogger);

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

    it('should handle non-Error thrown in Barcode Lookup', async () => {
      // Open Food Facts returns nothing
      mockFetch
        .mockResolvedValueOnce({
          ok: true,
          json: async () => ({ status: 0, product: null }),
        })
        // UPC Item DB returns nothing
        .mockResolvedValueOnce({
          ok: true,
          json: async () => ({ code: 'OK', items: [] }),
        })
        // Barcode Lookup throws non-Error
        .mockRejectedValueOnce('String error thrown');

      const { lookupExternalUpc } = await import('./upcService.server');
      const result = await lookupExternalUpc('012345678905', mockLogger);

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