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

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

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

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

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

  describe('findProductByUpc', () => {
    it('should return product when found', async () => {
      mockQuery.mockResolvedValueOnce({
        rowCount: 1,
        rows: [
          {
            product_id: 1,
            name: 'Test Product',
            description: 'A test product',
            size: '500g',
            upc_code: '012345678905',
            master_item_id: 5,
            brand_name: 'Test Brand',
            category_name: 'Snacks',
            image_url: null,
          },
        ],
      });

      const result = await repo.findProductByUpc('012345678905', mockLogger);

      expect(result).not.toBeNull();
      expect(result?.product_id).toBe(1);
      expect(result?.name).toBe('Test Product');
      expect(result?.brand).toBe('Test Brand');
      expect(result?.category).toBe('Snacks');
      expect(mockQuery).toHaveBeenCalledWith(expect.stringContaining('WHERE p.upc_code = $1'), [
        '012345678905',
      ]);
    });

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

      const result = await repo.findProductByUpc('999999999999', mockLogger);

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

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

      await expect(repo.findProductByUpc('012345678905', mockLogger)).rejects.toThrow();
    });
  });

  describe('linkUpcToProduct', () => {
    it('should link UPC to product successfully', async () => {
      mockQuery.mockResolvedValueOnce({
        rowCount: 1,
        rows: [
          {
            product_id: 1,
            name: 'Test Product',
            brand_id: 1,
            category_id: 1,
            description: null,
            size: null,
            upc_code: '012345678905',
            master_item_id: null,
            created_at: new Date().toISOString(),
            updated_at: new Date().toISOString(),
          },
        ],
      });

      const result = await repo.linkUpcToProduct(1, '012345678905', mockLogger);

      expect(result.upc_code).toBe('012345678905');
      expect(mockQuery).toHaveBeenCalledWith(
        expect.stringContaining('UPDATE public.products SET upc_code = $1'),
        ['012345678905', 1],
      );
    });

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

      await expect(repo.linkUpcToProduct(999, '012345678905', mockLogger)).rejects.toThrow(
        NotFoundError,
      );
    });
  });

  describe('recordScan', () => {
    it('should record a scan successfully', async () => {
      const scanRecord = {
        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(),
      };

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

      const result = await repo.recordScan('user-1', '012345678905', 'manual_entry', mockLogger, {
        productId: 1,
        scanConfidence: 1.0,
        lookupSuccessful: true,
      });

      expect(result.scan_id).toBe(1);
      expect(result.upc_code).toBe('012345678905');
      expect(result.lookup_successful).toBe(true);
    });

    it('should record scan with default options', async () => {
      const scanRecord = {
        scan_id: 2,
        user_id: 'user-1',
        upc_code: '012345678905',
        product_id: null,
        scan_source: 'image_upload',
        scan_confidence: null,
        raw_image_path: null,
        lookup_successful: false,
        created_at: new Date().toISOString(),
        updated_at: new Date().toISOString(),
      };

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

      const result = await repo.recordScan('user-1', '012345678905', 'image_upload', mockLogger);

      expect(result.product_id).toBeNull();
      expect(result.lookup_successful).toBe(false);
    });
  });

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

      // Data query
      mockQuery.mockResolvedValueOnce({
        rows: [
          {
            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 repo.getScanHistory(
        { user_id: 'user-1', limit: 10, offset: 0 },
        mockLogger,
      );

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

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

      await repo.getScanHistory({ user_id: 'user-1', lookup_successful: true }, mockLogger);

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

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

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

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

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

      await repo.getScanHistory(
        {
          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('getScanById', () => {
    it('should return scan record when found', async () => {
      const scanRecord = {
        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(),
      };

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

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

      expect(result.scan_id).toBe(1);
      expect(result.user_id).toBe('user-1');
    });

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

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

  describe('findExternalLookup', () => {
    it('should return cached lookup when found and not expired', async () => {
      const lookupRecord = {
        lookup_id: 1,
        upc_code: '012345678905',
        product_name: 'External Product',
        brand_name: 'External Brand',
        category: 'Snacks',
        description: null,
        image_url: null,
        external_source: 'openfoodfacts',
        lookup_data: null,
        lookup_successful: true,
        created_at: new Date().toISOString(),
        updated_at: new Date().toISOString(),
      };

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

      const result = await repo.findExternalLookup('012345678905', 168, mockLogger);

      expect(result).not.toBeNull();
      expect(result?.product_name).toBe('External Product');
    });

    it('should return null when lookup not cached', async () => {
      mockQuery.mockResolvedValueOnce({
        rowCount: 0,
        rows: [],
      });

      const result = await repo.findExternalLookup('999999999999', 168, mockLogger);

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

  describe('upsertExternalLookup', () => {
    it('should insert new external lookup', async () => {
      const lookupRecord = {
        lookup_id: 1,
        upc_code: '012345678905',
        product_name: 'New Product',
        brand_name: 'New Brand',
        category: 'Food',
        description: 'A description',
        image_url: 'https://example.com/image.jpg',
        external_source: 'openfoodfacts',
        lookup_data: null,
        lookup_successful: true,
        created_at: new Date().toISOString(),
        updated_at: new Date().toISOString(),
      };

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

      const result = await repo.upsertExternalLookup(
        '012345678905',
        'openfoodfacts',
        true,
        mockLogger,
        {
          productName: 'New Product',
          brandName: 'New Brand',
          category: 'Food',
          description: 'A description',
          imageUrl: 'https://example.com/image.jpg',
        },
      );

      expect(result.product_name).toBe('New Product');
      expect(mockQuery).toHaveBeenCalledWith(
        expect.stringContaining('ON CONFLICT (upc_code) DO UPDATE'),
        expect.any(Array),
      );
    });

    it('should update existing external lookup on conflict', async () => {
      const updatedRecord = {
        lookup_id: 1,
        upc_code: '012345678905',
        product_name: 'Updated Product',
        brand_name: 'Updated Brand',
        category: null,
        description: null,
        image_url: null,
        external_source: 'upcitemdb',
        lookup_data: null,
        lookup_successful: true,
        created_at: new Date().toISOString(),
        updated_at: new Date().toISOString(),
      };

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

      const result = await repo.upsertExternalLookup(
        '012345678905',
        'upcitemdb',
        true,
        mockLogger,
        {
          productName: 'Updated Product',
          brandName: 'Updated Brand',
        },
      );

      expect(result.product_name).toBe('Updated Product');
      expect(result.external_source).toBe('upcitemdb');
    });
  });

  describe('getExternalLookupByUpc', () => {
    it('should return lookup without cache expiry check', async () => {
      const lookupRecord = {
        lookup_id: 1,
        upc_code: '012345678905',
        product_name: '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(),
      };

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

      const result = await repo.getExternalLookupByUpc('012345678905', mockLogger);

      expect(result?.product_name).toBe('Product');
      expect(mockQuery).toHaveBeenCalledWith(expect.not.stringContaining('interval'), [
        '012345678905',
      ]);
    });

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

      const result = await repo.getExternalLookupByUpc('999999999999', mockLogger);

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

  describe('deleteOldExternalLookups', () => {
    it('should delete old lookups and return count', async () => {
      mockQuery.mockResolvedValueOnce({
        rowCount: 5,
      });

      const deleted = await repo.deleteOldExternalLookups(30, mockLogger);

      expect(deleted).toBe(5);
      expect(mockQuery).toHaveBeenCalledWith(expect.stringContaining("interval '1 day'"), [30]);
    });

    it('should return 0 when no records deleted', async () => {
      mockQuery.mockResolvedValueOnce({
        rowCount: 0,
      });

      const deleted = await repo.deleteOldExternalLookups(30, mockLogger);

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

  describe('getUserScanStats', () => {
    it('should return user scan statistics', async () => {
      mockQuery.mockResolvedValueOnce({
        rows: [
          {
            total_scans: '100',
            successful_lookups: '80',
            unique_products: '50',
            scans_today: '5',
            scans_this_week: '25',
          },
        ],
      });

      const stats = await repo.getUserScanStats('user-1', mockLogger);

      expect(stats.total_scans).toBe(100);
      expect(stats.successful_lookups).toBe(80);
      expect(stats.unique_products).toBe(50);
      expect(stats.scans_today).toBe(5);
      expect(stats.scans_this_week).toBe(25);
    });
  });

  describe('updateScanWithDetectedCode', () => {
    it('should update scan with detected code', async () => {
      mockQuery.mockResolvedValueOnce({
        rowCount: 1,
      });

      await repo.updateScanWithDetectedCode(1, '012345678905', 0.95, mockLogger);

      expect(mockQuery).toHaveBeenCalledWith(
        expect.stringContaining('UPDATE public.upc_scan_history'),
        [1, '012345678905', 0.95],
      );
    });

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

      await expect(
        repo.updateScanWithDetectedCode(999, '012345678905', 0.95, mockLogger),
      ).rejects.toThrow(NotFoundError);
    });
  });
});
