// src/services/barcodeService.server.test.ts
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import type { Logger } from 'pino';
import type { Job } from 'bullmq';
import type { BarcodeDetectionJobData } from '../types/job-data';
import { createMockLogger } from '../tests/utils/mockLogger';

// Unmock the barcodeService module so we can test the real implementation
// The global test setup mocks this to prevent zxing-wasm issues, but we need the real module here
vi.unmock('./barcodeService.server');

// Mock dependencies
vi.mock('zxing-wasm/reader', () => ({
  readBarcodesFromImageData: vi.fn(),
}));

vi.mock('sharp', () => {
  const mockSharp = vi.fn(() => ({
    metadata: vi.fn().mockResolvedValue({ width: 100, height: 100 }),
    ensureAlpha: vi.fn().mockReturnThis(),
    raw: vi.fn().mockReturnThis(),
    toBuffer: vi.fn().mockResolvedValue({
      data: new Uint8Array(100 * 100 * 4),
      info: { width: 100, height: 100 },
    }),
    grayscale: vi.fn().mockReturnThis(),
    normalize: vi.fn().mockReturnThis(),
    sharpen: vi.fn().mockReturnThis(),
    toFile: vi.fn().mockResolvedValue(undefined),
  }));
  return { default: mockSharp };
});

vi.mock('node:fs/promises', () => ({
  default: {
    readFile: vi.fn().mockResolvedValue(Buffer.from('mock image data')),
  },
}));

vi.mock('./db/index.db', () => ({
  upcRepo: {
    updateScanWithDetectedCode: vi.fn().mockResolvedValue(undefined),
  },
}));

// Import after mocks are set up
import {
  detectBarcode,
  isValidUpcFormat,
  calculateUpcCheckDigit,
  validateUpcCheckDigit,
  processBarcodeDetectionJob,
  detectMultipleBarcodes,
  enhanceImageForDetection,
} from './barcodeService.server';

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

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

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

  describe('detectBarcode', () => {
    it('should detect a valid UPC-A barcode from image', async () => {
      const { readBarcodesFromImageData } = await import('zxing-wasm/reader');
      vi.mocked(readBarcodesFromImageData).mockResolvedValueOnce([
        { text: '012345678905', format: 'UPC-A' },
      ] as any);

      const result = await detectBarcode('/path/to/image.jpg', mockLogger);

      expect(result.detected).toBe(true);
      expect(result.upc_code).toBe('012345678905');
      expect(result.format).toBe('UPC-A');
      expect(result.confidence).toBe(0.95);
      expect(result.error).toBeNull();
    });

    it('should detect a valid UPC-E barcode from image', async () => {
      const { readBarcodesFromImageData } = await import('zxing-wasm/reader');
      vi.mocked(readBarcodesFromImageData).mockResolvedValueOnce([
        { text: '01234567', format: 'UPC-E' },
      ] as any);

      const result = await detectBarcode('/path/to/image.jpg', mockLogger);

      expect(result.detected).toBe(true);
      expect(result.upc_code).toBe('01234567');
      expect(result.format).toBe('UPC-E');
    });

    it('should detect a valid EAN-13 barcode from image', async () => {
      const { readBarcodesFromImageData } = await import('zxing-wasm/reader');
      vi.mocked(readBarcodesFromImageData).mockResolvedValueOnce([
        { text: '5901234123457', format: 'EAN-13' },
      ] as any);

      const result = await detectBarcode('/path/to/image.jpg', mockLogger);

      expect(result.detected).toBe(true);
      expect(result.upc_code).toBe('5901234123457');
      expect(result.format).toBe('EAN-13');
    });

    it('should detect a valid EAN-8 barcode from image', async () => {
      const { readBarcodesFromImageData } = await import('zxing-wasm/reader');
      vi.mocked(readBarcodesFromImageData).mockResolvedValueOnce([
        { text: '96385074', format: 'EAN-8' },
      ] as any);

      const result = await detectBarcode('/path/to/image.jpg', mockLogger);

      expect(result.detected).toBe(true);
      expect(result.upc_code).toBe('96385074');
      expect(result.format).toBe('EAN-8');
    });

    it('should return detected: false when no barcode found', async () => {
      const { readBarcodesFromImageData } = await import('zxing-wasm/reader');
      vi.mocked(readBarcodesFromImageData).mockResolvedValueOnce([]);

      const result = await detectBarcode('/path/to/image.jpg', mockLogger);

      expect(result.detected).toBe(false);
      expect(result.upc_code).toBeNull();
      expect(result.confidence).toBeNull();
      expect(result.format).toBeNull();
      expect(result.error).toBeNull();
    });

    it('should return error when image dimensions cannot be determined', async () => {
      const sharp = (await import('sharp')).default;
      vi.mocked(sharp).mockReturnValueOnce({
        metadata: vi.fn().mockResolvedValue({}),
        ensureAlpha: vi.fn().mockReturnThis(),
        raw: vi.fn().mockReturnThis(),
        toBuffer: vi.fn(),
      } as any);

      const result = await detectBarcode('/path/to/image.jpg', mockLogger);

      expect(result.detected).toBe(false);
      expect(result.error).toBe('Could not determine image dimensions');
    });

    it('should handle errors during detection gracefully', async () => {
      const { readBarcodesFromImageData } = await import('zxing-wasm/reader');
      vi.mocked(readBarcodesFromImageData).mockRejectedValueOnce(new Error('Detection failed'));

      const result = await detectBarcode('/path/to/image.jpg', mockLogger);

      expect(result.detected).toBe(false);
      expect(result.error).toBe('Detection failed');
    });

    it('should map unknown barcode formats to "unknown"', async () => {
      const { readBarcodesFromImageData } = await import('zxing-wasm/reader');
      vi.mocked(readBarcodesFromImageData).mockResolvedValueOnce([
        { text: '12345678', format: 'SomeFutureFormat' },
      ] as any);

      const result = await detectBarcode('/path/to/image.jpg', mockLogger);

      expect(result.detected).toBe(true);
      expect(result.format).toBe('unknown');
    });

    it('should calculate lower confidence when text is empty', async () => {
      const { readBarcodesFromImageData } = await import('zxing-wasm/reader');
      vi.mocked(readBarcodesFromImageData).mockResolvedValueOnce([
        { text: '', format: 'UPC-A' },
      ] as any);

      const result = await detectBarcode('/path/to/image.jpg', mockLogger);

      expect(result.detected).toBe(true);
      expect(result.confidence).toBe(0.5);
    });
  });

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

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

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

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

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

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

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

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

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

  describe('calculateUpcCheckDigit', () => {
    it('should calculate correct check digit for valid 11-digit code', () => {
      // UPC-A: 01234567890 has check digit 5
      expect(calculateUpcCheckDigit('01234567890')).toBe(5);
    });

    it('should return null for code with wrong length', () => {
      expect(calculateUpcCheckDigit('1234567890')).toBeNull(); // 10 digits
      expect(calculateUpcCheckDigit('123456789012')).toBeNull(); // 12 digits
    });

    it('should return null for code with non-numeric characters', () => {
      expect(calculateUpcCheckDigit('0123456789A')).toBeNull();
    });

    it('should handle all zeros', () => {
      // 00000000000 should produce a valid check digit
      const checkDigit = calculateUpcCheckDigit('00000000000');
      expect(typeof checkDigit).toBe('number');
      expect(checkDigit).toBeGreaterThanOrEqual(0);
      expect(checkDigit).toBeLessThanOrEqual(9);
    });
  });

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

    it('should return false for UPC-A with incorrect check digit', () => {
      expect(validateUpcCheckDigit('012345678901')).toBe(false);
    });

    it('should return false for code with wrong length', () => {
      expect(validateUpcCheckDigit('01234567890')).toBe(false); // 11 digits
      expect(validateUpcCheckDigit('0123456789012')).toBe(false); // 13 digits
    });

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

  describe('processBarcodeDetectionJob', () => {
    it('should process job and update scan record when barcode detected', async () => {
      const { readBarcodesFromImageData } = await import('zxing-wasm/reader');
      const { upcRepo } = await import('./db/index.db');

      vi.mocked(readBarcodesFromImageData).mockResolvedValueOnce([
        { text: '012345678905', format: 'UPC-A' },
      ] as any);

      const mockJob = {
        id: 'job-1',
        data: {
          scanId: 123,
          imagePath: '/path/to/barcode.jpg',
          userId: 'user-1',
          meta: { requestId: 'req-1' },
        },
      } as Job<BarcodeDetectionJobData>;

      const result = await processBarcodeDetectionJob(mockJob, mockLogger);

      expect(result.detected).toBe(true);
      expect(result.upc_code).toBe('012345678905');
      expect(upcRepo.updateScanWithDetectedCode).toHaveBeenCalledWith(
        123,
        '012345678905',
        0.95,
        expect.any(Object),
      );
    });

    it('should not update scan record when no barcode detected', async () => {
      const { readBarcodesFromImageData } = await import('zxing-wasm/reader');
      const { upcRepo } = await import('./db/index.db');

      vi.mocked(readBarcodesFromImageData).mockResolvedValueOnce([]);

      const mockJob = {
        id: 'job-2',
        data: {
          scanId: 456,
          imagePath: '/path/to/no-barcode.jpg',
          userId: 'user-2',
        },
      } as Job<BarcodeDetectionJobData>;

      const result = await processBarcodeDetectionJob(mockJob, mockLogger);

      expect(result.detected).toBe(false);
      expect(upcRepo.updateScanWithDetectedCode).not.toHaveBeenCalled();
    });

    it('should return error result when job processing fails', async () => {
      const { readBarcodesFromImageData } = await import('zxing-wasm/reader');
      vi.mocked(readBarcodesFromImageData).mockRejectedValueOnce(new Error('Processing error'));

      const mockJob = {
        id: 'job-3',
        data: {
          scanId: 789,
          imagePath: '/path/to/error.jpg',
          userId: 'user-3',
        },
      } as Job<BarcodeDetectionJobData>;

      const result = await processBarcodeDetectionJob(mockJob, mockLogger);

      expect(result.detected).toBe(false);
      expect(result.error).toBe('Processing error');
    });
  });

  describe('detectMultipleBarcodes', () => {
    it('should detect multiple barcodes in an image', async () => {
      const { readBarcodesFromImageData } = await import('zxing-wasm/reader');
      vi.mocked(readBarcodesFromImageData).mockResolvedValueOnce([
        { text: '012345678905', format: 'UPC-A' },
        { text: '5901234123457', format: 'EAN-13' },
        { text: '96385074', format: 'EAN-8' },
      ] as any);

      const results = await detectMultipleBarcodes('/path/to/multi.jpg', mockLogger);

      expect(results).toHaveLength(3);
      expect(results[0].upc_code).toBe('012345678905');
      expect(results[0].format).toBe('UPC-A');
      expect(results[1].upc_code).toBe('5901234123457');
      expect(results[1].format).toBe('EAN-13');
      expect(results[2].upc_code).toBe('96385074');
      expect(results[2].format).toBe('EAN-8');
    });

    it('should return empty array when no barcodes detected', async () => {
      const { readBarcodesFromImageData } = await import('zxing-wasm/reader');
      vi.mocked(readBarcodesFromImageData).mockResolvedValueOnce([]);

      const results = await detectMultipleBarcodes('/path/to/no-codes.jpg', mockLogger);

      expect(results).toEqual([]);
    });

    it('should return empty array on error', async () => {
      const { readBarcodesFromImageData } = await import('zxing-wasm/reader');
      vi.mocked(readBarcodesFromImageData).mockRejectedValueOnce(
        new Error('Multi-detection failed'),
      );

      const results = await detectMultipleBarcodes('/path/to/error.jpg', mockLogger);

      expect(results).toEqual([]);
    });
  });

  describe('enhanceImageForDetection', () => {
    it('should enhance image and return new path', async () => {
      const result = await enhanceImageForDetection('/path/to/image.jpg', mockLogger);

      expect(result).toBe('/path/to/image-enhanced.jpg');
    });

    it('should handle different file extensions', async () => {
      const result = await enhanceImageForDetection('/path/to/image.png', mockLogger);

      expect(result).toBe('/path/to/image-enhanced.png');
    });

    it('should return original path on enhancement failure', async () => {
      const sharp = (await import('sharp')).default;
      vi.mocked(sharp).mockReturnValueOnce({
        grayscale: vi.fn().mockReturnThis(),
        normalize: vi.fn().mockReturnThis(),
        sharpen: vi.fn().mockReturnThis(),
        toFile: vi.fn().mockRejectedValue(new Error('Enhancement failed')),
      } as any);

      const result = await enhanceImageForDetection('/path/to/image.jpg', mockLogger);

      expect(result).toBe('/path/to/image.jpg');
    });
  });
});
