import { ConfigService } from '@nestjs/config';
import { mkdtemp, readFile, rm, writeFile } from 'fs/promises';
import { ImageProcessingService } from './image-processing.service';

jest.mock('fs/promises', () => ({ mkdtemp: jest.fn(), readFile: jest.fn(), rm: jest.fn(), writeFile: jest.fn() }));

type ImageInternals = {
  ensureFfmpegAvailable(): Promise<boolean>;
  runFfmpeg(args: string[]): Promise<void>;
};

const internals = (service: ImageProcessingService): ImageInternals =>
  service as unknown as ImageInternals;

describe('ImageProcessingService', () => {
  const values: Record<string, unknown> = {};
  const config = { get: jest.fn((key: string) => values[key]) } as unknown as ConfigService;

  beforeEach(() => {
    jest.clearAllMocks();
    Object.keys(values).forEach((key) => delete values[key]);
    (mkdtemp as jest.Mock).mockResolvedValue('/tmp/image-work');
    (writeFile as jest.Mock).mockResolvedValue(undefined);
    (readFile as jest.Mock).mockImplementation(async (path: string) => Buffer.from(path));
    (rm as jest.Mock).mockResolvedValue(undefined);
  });

  it.each([
    [{ originalname: 'photo.PNG', mimetype: '', size: 1, buffer: Buffer.from('x') }, '.png', 'image/png'],
    [{ mimetype: 'image/webp', size: 1, buffer: Buffer.from('x') }, '.webp', 'image/webp'],
    [{ mimetype: 'image/gif', size: 1, buffer: Buffer.from('x') }, '.gif', 'image/gif'],
    [{ mimetype: 'unknown', size: 1, buffer: Buffer.from('x') }, '.jpg', 'unknown'],
  ])('returns original when disabled %#', async (file, extension, contentType) => {
    const result = await new ImageProcessingService(config).processForResponsiveDelivery(file);
    expect(result).toEqual({ primaryVariantName: 'original', variants: [expect.objectContaining({ relativePath: `original${extension}`, contentType })] });
  });

  it('falls back when ffmpeg is unavailable', async () => {
    values['imageProcessing.enabled'] = true;
    const service = new ImageProcessingService(config);
    jest.spyOn(internals(service), 'ensureFfmpegAvailable').mockResolvedValue(false);
    const result = await service.processForResponsiveDelivery({ originalname: 'x.jpg', mimetype: 'image/jpeg', size: 1, buffer: Buffer.from('x') });
    expect(result.primaryVariantName).toBe('original');
    expect(mkdtemp).not.toHaveBeenCalled();
  });

  it('creates low, medium, and high WebP variants', async () => {
    Object.assign(values, {
      'imageProcessing.enabled': true, 'imageProcessing.lowWidth': 320,
      'imageProcessing.mediumWidth': 640, 'imageProcessing.highWidth': 1024,
      'imageProcessing.quality': 70,
    });
    const service = new ImageProcessingService(config);
    jest.spyOn(internals(service), 'ensureFfmpegAvailable').mockResolvedValue(true);
    const ffmpeg = jest.spyOn(internals(service), 'runFfmpeg').mockResolvedValue(undefined);
    const result = await service.processForResponsiveDelivery({ originalname: 'x.jpg', mimetype: 'image/jpeg', size: 1, buffer: Buffer.from('x') });
    expect(result.primaryVariantName).toBe('medium');
    expect(result.variants.map((item) => item.name)).toEqual(['original', 'low', 'medium', 'high']);
    expect(ffmpeg).toHaveBeenCalledTimes(3);
    expect(ffmpeg).toHaveBeenCalledWith(expect.arrayContaining([expect.stringContaining('min(320,iw)'), '70']));
    expect(rm).toHaveBeenCalledWith('/tmp/image-work', { recursive: true, force: true });
  });

  it('returns the original and cleans up after conversion errors', async () => {
    values['videoProcessing.enabled'] = true;
    const service = new ImageProcessingService(config);
    jest.spyOn(internals(service), 'ensureFfmpegAvailable').mockResolvedValue(true);
    (writeFile as jest.Mock).mockRejectedValue(new Error('disk full'));
    const result = await service.processForResponsiveDelivery({ size: 1, buffer: Buffer.from('x') });
    expect(result.primaryVariantName).toBe('original');
    expect(rm).toHaveBeenCalled();
  });

  it('probes ffmpeg once and caches both success and failure', async () => {
    values['imageProcessing.ffmpegPath'] = ' custom-ffmpeg ';
    const success = new ImageProcessingService(config);
    const successRun = jest.spyOn(internals(success), 'runFfmpeg').mockResolvedValue(undefined);
    await expect((success as never as { ensureFfmpegAvailable(): Promise<boolean> }).ensureFfmpegAvailable()).resolves.toBe(true);
    await (success as never as { ensureFfmpegAvailable(): Promise<boolean> }).ensureFfmpegAvailable();
    expect(successRun).toHaveBeenCalledTimes(1);

    const failure = new ImageProcessingService(config);
    const failRun = jest.spyOn(internals(failure), 'runFfmpeg').mockRejectedValue('missing');
    await expect((failure as never as { ensureFfmpegAvailable(): Promise<boolean> }).ensureFfmpegAvailable()).resolves.toBe(false);
    await (failure as never as { ensureFfmpegAvailable(): Promise<boolean> }).ensureFfmpegAvailable();
    expect(failRun).toHaveBeenCalledTimes(1);
  });
});
