// src/utils/imageProcessor.test.ts
import { describe, it, expect, vi, beforeEach } from 'vitest';
import type { Logger } from 'pino';
import path from 'path';

// --- Hoisted Mocks ---
const mocks = vi.hoisted(() => {
  // Create a chainable mock for the sharp library
  const toFile = vi.fn().mockResolvedValue({ info: 'mocked' });

  // Chain for generateFlyerIcon
  const webp = vi.fn(() => ({ toFile }));
  const resize = vi.fn(() => ({ webp }));

  // Chain for processAndSaveImage
  const png = vi.fn(() => ({ toFile }));
  const jpeg = vi.fn(() => ({ png }));
  const withMetadata = vi.fn(() => ({ jpeg }));

  const sharpInstance = { resize, withMetadata };

  // Mock the sharp function and attach static properties required by the implementation
  const sharp = vi.fn(() => sharpInstance);
  Object.assign(sharp, { fit: { cover: 'cover' } });

  return {
    sharp: sharp,
    resize,
    webp,
    withMetadata,
    jpeg,
    png,
    toFile,
    mkdir: vi.fn().mockResolvedValue(undefined),
  };
});

// --- Mock Modules ---
vi.mock('sharp', () => ({
  __esModule: true,
  default: mocks.sharp,
}));

vi.mock('node:fs/promises', () => ({
  default: {
    mkdir: mocks.mkdir,
  },
}));

// Helper to create a type-safe mock logger
const createMockLogger = (): Logger =>
  ({
    info: vi.fn(),
    error: vi.fn(),
    warn: vi.fn(),
    debug: vi.fn(),
    fatal: vi.fn(),
    trace: vi.fn(),
    silent: vi.fn(),
    child: vi.fn(() => createMockLogger()), // Return a new mock for child loggers
    level: 'info',
  }) as unknown as Logger;

const logger = createMockLogger();

vi.mock('../services/logger.server', () => ({ logger }));

// --- Import the function to be tested ---
import { generateFlyerIcon, processAndSaveImage } from './imageProcessor';

describe('generateFlyerIcon', () => {
  beforeEach(() => {
    vi.clearAllMocks();
  });

  it('should process the image and return the correct icon filename', async () => {
    const sourceImagePath = '/path/to/flyer image (1).jpg';
    const iconsDirectory = '/path/to/icons';

    const result = await generateFlyerIcon(sourceImagePath, iconsDirectory, logger);

    // Check that the icons directory was created
    expect(mocks.mkdir).toHaveBeenCalledWith(iconsDirectory, { recursive: true });

    // Check that sharp was called with the correct source
    expect(mocks.sharp).toHaveBeenCalledWith(sourceImagePath, { failOn: 'none' });

    // Check the processing chain
    expect(mocks.resize).toHaveBeenCalledWith({ width: 128, height: 128, fit: 'inside' });
    expect(mocks.webp).toHaveBeenCalledWith({ quality: 75 });
    expect(mocks.toFile).toHaveBeenCalledWith('/path/to/icons/icon-flyer-image-1.webp');

    // Check the returned filename
    expect(result).toBe('icon-flyer-image-1.webp');
  });

  it('should throw an error if sharp fails to process the image', async () => {
    const sharpError = new Error('Invalid image buffer');
    mocks.toFile.mockRejectedValue(sharpError);

    await expect(
      generateFlyerIcon('/path/to/bad-image.jpg', '/path/to/icons', logger), // This was a duplicate, fixed.
    ).rejects.toThrow('Failed to generate icon for /path/to/bad-image.jpg.');
    expect(logger.error).toHaveBeenCalledWith(
      { err: sharpError, sourcePath: '/path/to/bad-image.jpg', outputPath: '/path/to/icons/icon-bad-image.webp' },
      'An error occurred during icon generation.',
    );
  });
});

describe('processAndSaveImage', () => {
  beforeEach(() => {
    vi.clearAllMocks();
    // Ensure toFile is in a resolved state
    mocks.toFile.mockResolvedValue({ info: 'mocked' });
  });

  it('should process the image, strip metadata, and return the new filename', async () => {
    const sourcePath = '/tmp/upload/original.jpg';
    const destinationDir = '/var/www/images';
    const originalFileName = 'original.jpg';

    const result = await processAndSaveImage(sourcePath, destinationDir, originalFileName, logger);

    // Check that the destination directory was created
    expect(mocks.mkdir).toHaveBeenCalledWith(destinationDir, { recursive: true });

    // Check that sharp was called with the correct source
    expect(mocks.sharp).toHaveBeenCalledWith(sourcePath, { failOn: 'none' });

    // Check the processing chain
    expect(mocks.withMetadata).toHaveBeenCalledWith({});
    expect(mocks.jpeg).toHaveBeenCalledWith({ quality: 85, mozjpeg: true });
    expect(mocks.png).toHaveBeenCalledWith({ compressionLevel: 8, quality: 85 });
    expect(mocks.toFile).toHaveBeenCalledWith(expect.stringContaining(path.join(destinationDir, 'original-')));

    // Check the returned filename format (original-timestamp.jpg)
    expect(result).toMatch(/^original-\d+\.jpg$/);
  });

  it('should throw an error if sharp fails to process the image', async () => {
    const sharpError = new Error('Processing failed');
    mocks.toFile.mockRejectedValueOnce(sharpError);

    await expect(
      processAndSaveImage('/path/img.jpg', '/dest', 'img.jpg', logger),
    ).rejects.toThrow('Failed to process image img.jpg.');

    expect(logger.error).toHaveBeenCalledWith(
      expect.objectContaining({ err: sharpError, sourcePath: '/path/img.jpg' }),
      'An error occurred during image processing and saving.',
    );
  });
});
