import { mkdtempSync, writeFileSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import sharp from 'sharp';
import { afterAll, beforeAll, describe, expect, it, vi } from 'vitest';

// No real filesystem writes — the pipeline logic is what's under test.
vi.mock('./storage.service', () => ({
  derivativeExists: vi.fn().mockResolvedValue(false),
  writeDerivative: vi.fn().mockResolvedValue('/dev/null'),
  derivativeFsPath: (p: string) => p,
  fileSize: vi.fn().mockResolvedValue(1234),
}));

import { generateDerivatives, normalise } from './derivative.service';

let dir: string;
let landscape: string;
let portrait: string;
let small: string;

beforeAll(async () => {
  dir = mkdtempSync(join(tmpdir(), 'deriv-'));
  landscape = join(dir, 'l.png');
  portrait = join(dir, 'p.png');
  small = join(dir, 's.png');
  writeFileSync(
    landscape,
    await sharp({ create: { width: 3000, height: 2000, channels: 3, background: '#123' } })
      .png()
      .toBuffer(),
  );
  writeFileSync(
    portrait,
    await sharp({ create: { width: 1200, height: 3200, channels: 3, background: '#123' } })
      .png()
      .toBuffer(),
  );
  writeFileSync(
    small,
    await sharp({ create: { width: 500, height: 400, channels: 3, background: '#123' } })
      .jpeg()
      .toBuffer(),
  );
});

afterAll(() => vi.restoreAllMocks());

describe('normalise', () => {
  it('caps the long edge at 2400 and keeps aspect ratio', async () => {
    const n = await normalise(landscape);
    expect(n.width).toBe(2400);
    expect(n.height).toBe(1600);
    expect(n.ext).toBe('png');
  });

  it('caps a portrait on its height', async () => {
    const n = await normalise(portrait);
    expect(n.height).toBe(2400);
    expect(n.width).toBe(900);
  });

  it('never enlarges a small image and maps jpeg -> jpg', async () => {
    const n = await normalise(small);
    expect(n.width).toBe(500);
    expect(n.height).toBe(400);
    expect(n.ext).toBe('jpg');
  });

  it('rejects a non-image', async () => {
    const bad = join(dir, 'bad.png');
    writeFileSync(bad, 'definitely not an image');
    await expect(normalise(bad)).rejects.toThrow();
  });
});

describe('generateDerivatives', () => {
  it('emits AVIF+WebP for every ladder width up to the source, plus one primary JPEG', async () => {
    const n = await normalise(landscape); // 2400x1600
    const specs = await generateDerivatives(n, 'genesis', 'a'.repeat(64));

    const avif = specs.filter((s) => s.format === 'avif').map((s) => s.width);
    const webp = specs.filter((s) => s.format === 'webp').map((s) => s.width);
    const jpeg = specs.filter((s) => s.format === 'jpeg');

    expect(avif.sort((a, b) => a - b)).toEqual([640, 1024, 1600, 2400]);
    expect(webp.sort((a, b) => a - b)).toEqual([640, 1024, 1600, 2400]);
    expect(jpeg).toHaveLength(1);
    expect(jpeg[0].isPrimary).toBe(true);
    expect(jpeg[0].width).toBe(1600);
    expect(specs.every((s) => s.width <= n.width)).toBe(true);
    expect(specs.every((s) => s.height > 0 && s.bytes > 0)).toBe(true);
    expect(specs.find((s) => s.width === 640)?.path).toBe(`genesis/${'a'.repeat(64)}/640.avif`);
  });

  it('never upscales past the source width', async () => {
    const n = await normalise(small); // 500x400
    const specs = await generateDerivatives(n, 'hive', 'b'.repeat(64));
    expect(specs.map((s) => s.width).filter((w, i, arr) => arr.indexOf(w) === i)).toEqual([500]);
    expect(specs.filter((s) => s.format === 'jpeg')[0].width).toBe(500);
  });
});
