// src/services/flyerFileHandler.server.test.ts
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { Job } from 'bullmq';
import type { Dirent } from 'node:fs';
import sharp from 'sharp';
import { FlyerFileHandler, ICommandExecutor, IFileSystem } from './flyerFileHandler.server';
import { ImageConversionError, PdfConversionError, UnsupportedFileTypeError } from './processingErrors';
import { logger } from './logger.server';
import type { FlyerJobData } from '../types/job-data';

// Mock dependencies
vi.mock('sharp', () => {
  const mockSharpInstance = {
    jpeg: vi.fn().mockReturnThis(),
    png: vi.fn().mockReturnThis(),
    toFile: vi.fn().mockResolvedValue({}),
  };
  return {
    __esModule: true,
    default: vi.fn(() => mockSharpInstance),
  };
});

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

const createMockJob = (data: Partial<FlyerJobData>): Job<FlyerJobData> => {
  return {
    id: 'job-1',
    data: {
      filePath: '/tmp/flyer.jpg',
      originalFileName: 'flyer.jpg',
      checksum: 'checksum-123',
      ...data,
    },
    updateProgress: vi.fn(),
  } as unknown as Job<FlyerJobData>;
};

describe('FlyerFileHandler', () => {
  let service: FlyerFileHandler;
  let mockFs: IFileSystem;
  let mockExec: ICommandExecutor;

  beforeEach(() => {
    vi.clearAllMocks();

    mockFs = {
      readdir: vi.fn().mockResolvedValue([]),
      unlink: vi.fn(),
    };
    mockExec = vi.fn().mockResolvedValue({ stdout: 'success', stderr: '' });

    service = new FlyerFileHandler(mockFs, mockExec);
  });

  it('should convert a PDF and return image paths', async () => {
    const job = createMockJob({ filePath: '/tmp/flyer.pdf' });
    vi.mocked(mockFs.readdir).mockResolvedValue([
      { name: 'flyer-1.jpg' },
      { name: 'flyer-2.jpg' },
    ] as Dirent[]);

    const { imagePaths, createdImagePaths } = await service.prepareImageInputs(
      '/tmp/flyer.pdf',
      job,
      logger,
    );

    expect(mockExec).toHaveBeenCalledWith('pdftocairo -jpeg -r 150 "/tmp/flyer.pdf" "/tmp/flyer"');
    expect(imagePaths).toHaveLength(2);
    expect(imagePaths[0].path).toContain('flyer-1.jpg');
    expect(createdImagePaths).toHaveLength(2);
  });

  it('should throw PdfConversionError if PDF conversion yields no images', async () => {
    const job = createMockJob({ filePath: '/tmp/flyer.pdf' });
    vi.mocked(mockFs.readdir).mockResolvedValue([]); // No images found

    await expect(service.prepareImageInputs('/tmp/flyer.pdf', job, logger)).rejects.toThrow(
      PdfConversionError,
    );
  });

  it('should convert convertible image types to PNG', async () => {
    const job = createMockJob({ filePath: '/tmp/flyer.gif' });
    const mockSharpInstance = sharp('/tmp/flyer.gif');
    vi.mocked(mockSharpInstance.toFile).mockResolvedValue({} as any);

    const { imagePaths, createdImagePaths } = await service.prepareImageInputs(
      '/tmp/flyer.gif',
      job,
      logger,
    );

    expect(sharp).toHaveBeenCalledWith('/tmp/flyer.gif');
    expect(mockSharpInstance.png).toHaveBeenCalled();
    expect(mockSharpInstance.toFile).toHaveBeenCalledWith('/tmp/flyer-converted.png');
    expect(imagePaths).toEqual([{ path: '/tmp/flyer-converted.png', mimetype: 'image/png' }]);
    expect(createdImagePaths).toEqual(['/tmp/flyer-converted.png']);
  });

  it('should throw UnsupportedFileTypeError for unsupported types', async () => {
    const job = createMockJob({ filePath: '/tmp/document.txt' });
    await expect(service.prepareImageInputs('/tmp/document.txt', job, logger)).rejects.toThrow(
      UnsupportedFileTypeError,
    );
  });

  describe('Image Processing', () => {
    it('should process a JPEG to strip EXIF data', async () => {
      const job = createMockJob({ filePath: '/tmp/flyer.jpg' });
      const mockSharpInstance = sharp('/tmp/flyer.jpg');
      vi.mocked(mockSharpInstance.toFile).mockResolvedValue({} as any);

      const { imagePaths, createdImagePaths } = await service.prepareImageInputs(
        '/tmp/flyer.jpg',
        job,
        logger,
      );

      expect(sharp).toHaveBeenCalledWith('/tmp/flyer.jpg');
      expect(mockSharpInstance.jpeg).toHaveBeenCalledWith({ quality: 90 });
      expect(mockSharpInstance.toFile).toHaveBeenCalledWith('/tmp/flyer-processed.jpeg');
      expect(imagePaths).toEqual([{ path: '/tmp/flyer-processed.jpeg', mimetype: 'image/jpeg' }]);
      expect(createdImagePaths).toEqual(['/tmp/flyer-processed.jpeg']);
    });

    it('should process a PNG to strip metadata', async () => {
      const job = createMockJob({ filePath: '/tmp/flyer.png' });
      const mockSharpInstance = sharp('/tmp/flyer.png');
      vi.mocked(mockSharpInstance.toFile).mockResolvedValue({} as any);

      const { imagePaths, createdImagePaths } = await service.prepareImageInputs(
        '/tmp/flyer.png',
        job,
        logger,
      );

      expect(sharp).toHaveBeenCalledWith('/tmp/flyer.png');
      expect(mockSharpInstance.png).toHaveBeenCalledWith({ quality: 90 });
      expect(mockSharpInstance.toFile).toHaveBeenCalledWith('/tmp/flyer-processed.png');
      expect(imagePaths).toEqual([{ path: '/tmp/flyer-processed.png', mimetype: 'image/png' }]);
      expect(createdImagePaths).toEqual(['/tmp/flyer-processed.png']);
    });

    it('should handle other supported image types (e.g. webp) directly without processing', async () => {
      const job = createMockJob({ filePath: '/tmp/flyer.webp' });
      const { imagePaths, createdImagePaths } = await service.prepareImageInputs(
        '/tmp/flyer.webp',
        job,
        logger,
      );

      expect(imagePaths).toEqual([{ path: '/tmp/flyer.webp', mimetype: 'image/webp' }]);
      expect(createdImagePaths).toEqual([]);
      expect(sharp).not.toHaveBeenCalled();
    });

    it('should throw ImageConversionError if sharp fails during JPEG processing', async () => {
      const job = createMockJob({ filePath: '/tmp/flyer.jpg' });
      const sharpError = new Error('Sharp failed');
      const mockSharpInstance = sharp('/tmp/flyer.jpg');
      vi.mocked(mockSharpInstance.toFile).mockRejectedValue(sharpError);

      await expect(service.prepareImageInputs('/tmp/flyer.jpg', job, logger)).rejects.toThrow(ImageConversionError);
    });

    it('should throw ImageConversionError if sharp fails during PNG processing', async () => {
      const job = createMockJob({ filePath: '/tmp/flyer.png' });
      const sharpError = new Error('Sharp failed');
      const mockSharpInstance = sharp('/tmp/flyer.png');
      vi.mocked(mockSharpInstance.toFile).mockRejectedValue(sharpError);

      await expect(service.prepareImageInputs('/tmp/flyer.png', job, logger)).rejects.toThrow(ImageConversionError);
    });
  });
});