/**
 * Tests for imageFormat helpers — pure functions + the sharp-wrapper
 * fallback path. We don't actually transcode in tests (sharp may not be
 * installed in CI yet) — `transcodeIfNeeded` has clean fallbacks for
 * "already in target format" and "unknown input" that we can pin.
 */

import { describe, test, expect, beforeEach } from '@jest/globals';
import {
  SUPPORTED_FORMATS,
  DEFAULT_FORMAT,
  normalizeFormat,
  extensionFor,
  resolveTargetFormat,
  detectFormatFromBytes,
  transcodeIfNeeded,
  ensureExtension,
  _resetSharpCacheForTests,
} from '../imageFormat.js';

beforeEach(() => { _resetSharpCacheForTests(); });

describe('constants', () => {
  test('default format is webp', () => {
    expect(DEFAULT_FORMAT).toBe('webp');
  });
  test('supported formats include png, jpeg, webp', () => {
    expect(SUPPORTED_FORMATS).toEqual(expect.arrayContaining(['png', 'jpeg', 'webp']));
  });
});

describe('normalizeFormat', () => {
  test.each([
    ['png', 'png'], ['.png', 'png'], ['PNG', 'png'],
    ['jpg', 'jpeg'], ['.jpg', 'jpeg'], ['jpeg', 'jpeg'], ['.jpeg', 'jpeg'], ['JPG', 'jpeg'],
    ['webp', 'webp'], ['.webp', 'webp'], ['WEBP', 'webp'], ['  WebP  ', 'webp'],
  ])('"%s" → %s', (input, expected) => {
    expect(normalizeFormat(input)).toBe(expected);
  });
  test.each([null, undefined, '', 'gif', 'bmp', 'random', 42])('rejects "%p" → null', (input) => {
    expect(normalizeFormat(input)).toBe(null);
  });
});

describe('extensionFor', () => {
  test('png → "png"', () => { expect(extensionFor('png')).toBe('png'); });
  test('jpeg → "jpg" (the more common file extension)', () => { expect(extensionFor('jpeg')).toBe('jpg'); });
  test('webp → "webp"', () => { expect(extensionFor('webp')).toBe('webp'); });
  test('unknown → "webp" fallback (defensive)', () => { expect(extensionFor('xyz')).toBe('webp'); });
});

describe('resolveTargetFormat', () => {
  test('outputPath extension wins when present', () => {
    const r = resolveTargetFormat({ outputPath: 'images/foo.png', outputType: 'webp' });
    expect(r).toEqual({ format: 'png', source: 'extension' });
  });

  test('outputType used when outputPath has no extension', () => {
    const r = resolveTargetFormat({ outputPath: 'images/foo', outputType: 'png' });
    expect(r).toEqual({ format: 'png', source: 'outputType' });
  });

  test('outputType used when no outputPath', () => {
    const r = resolveTargetFormat({ outputType: 'webp' });
    expect(r).toEqual({ format: 'webp', source: 'outputType' });
  });

  test('default webp when neither given', () => {
    const r = resolveTargetFormat({});
    expect(r).toEqual({ format: 'webp', source: 'default' });
  });

  test('outputType "jpg" canonicalizes to jpeg', () => {
    expect(resolveTargetFormat({ outputType: 'jpg' }).format).toBe('jpeg');
  });

  test('invalid extension falls through to outputType / default', () => {
    expect(resolveTargetFormat({ outputPath: 'foo.tiff', outputType: 'png' })).toEqual({
      format: 'png', source: 'outputType',
    });
    expect(resolveTargetFormat({ outputPath: 'foo.tiff' })).toEqual({
      format: 'webp', source: 'default',
    });
  });

  test('outputPath extension wins even over a contradictory outputType', () => {
    // An agent that says outputPath: "x.webp" + outputType: "png" probably
    // typo'd one; the path is the clearer signal — honor it.
    expect(resolveTargetFormat({ outputPath: 'foo.webp', outputType: 'png' })).toEqual({
      format: 'webp', source: 'extension',
    });
  });
});

describe('detectFormatFromBytes', () => {
  test('PNG signature', () => {
    const buf = Buffer.from([0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, 0,0,0,0]);
    expect(detectFormatFromBytes(buf)).toBe('png');
  });
  test('JPEG signature', () => {
    const buf = Buffer.from([0xFF, 0xD8, 0xFF, 0xE0, 0,0,0,0,0,0,0,0]);
    expect(detectFormatFromBytes(buf)).toBe('jpeg');
  });
  test('WebP signature (RIFF...WEBP)', () => {
    const buf = Buffer.from([
      0x52, 0x49, 0x46, 0x46,    // RIFF
      0,0,0,0,                   // size (4 bytes)
      0x57, 0x45, 0x42, 0x50,    // WEBP
    ]);
    expect(detectFormatFromBytes(buf)).toBe('webp');
  });
  test('GIF signature', () => {
    const buf = Buffer.concat([Buffer.from('GIF89a'), Buffer.alloc(8)]);
    expect(detectFormatFromBytes(buf)).toBe('gif');
  });
  test('BMP signature', () => {
    const buf = Buffer.from([0x42, 0x4D, 0,0,0,0,0,0,0,0,0,0]);
    expect(detectFormatFromBytes(buf)).toBe('bmp');
  });
  test('unknown bytes → null', () => {
    expect(detectFormatFromBytes(Buffer.from([0,0,0,0,0,0,0,0,0,0,0,0]))).toBe(null);
  });
  test('too-small buffer → null (defensive)', () => {
    expect(detectFormatFromBytes(Buffer.from([0x89]))).toBe(null);
  });
  test('non-buffer → null', () => {
    expect(detectFormatFromBytes(null)).toBe(null);
    expect(detectFormatFromBytes('string')).toBe(null);
    expect(detectFormatFromBytes(undefined)).toBe(null);
  });
});

describe('ensureExtension', () => {
  test('appends extension when missing', () => {
    expect(ensureExtension('images/foo', 'webp')).toBe('images/foo.webp');
    expect(ensureExtension('images/foo', 'png')).toBe('images/foo.png');
    expect(ensureExtension('images/foo', 'jpeg')).toBe('images/foo.jpg');
  });
  test('leaves existing valid extension alone', () => {
    expect(ensureExtension('images/foo.webp', 'png')).toBe('images/foo.webp');
    expect(ensureExtension('images/foo.png', 'webp')).toBe('images/foo.png');
  });
  test('appends when extension is unknown (e.g. .bak)', () => {
    expect(ensureExtension('foo.bak', 'webp')).toBe('foo.bak.webp');
  });
  test('null/empty → returned as-is', () => {
    expect(ensureExtension('', 'webp')).toBe('');
    expect(ensureExtension(null, 'webp')).toBe(null);
  });
});

describe('transcodeIfNeeded — short-circuits without invoking sharp', () => {
  test('input format already matches target → returns input bytes unchanged', async () => {
    const png = Buffer.from([0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, 0,0,0,0]);
    const r = await transcodeIfNeeded(png, 'png');
    expect(r.transcoded).toBe(false);
    expect(r.reason).toBe('already-target');
    expect(r.buffer).toBe(png);
    expect(r.inputFormat).toBe('png');
  });

  test('unknown input format → writes bytes as-is, reason "unknown-input"', async () => {
    const garbage = Buffer.alloc(64, 0x00);
    const r = await transcodeIfNeeded(garbage, 'webp');
    expect(r.transcoded).toBe(false);
    expect(r.reason).toBe('unknown-input');
    expect(r.buffer).toBe(garbage);
  });

  test('GIF input → reason "unsupported-input" (we don\'t transcode animations)', async () => {
    const gif = Buffer.concat([Buffer.from('GIF89a'), Buffer.alloc(8)]);
    const r = await transcodeIfNeeded(gif, 'webp');
    expect(r.transcoded).toBe(false);
    expect(r.reason).toBe('unsupported-input');
    expect(r.inputFormat).toBe('gif');
    expect(r.buffer).toBe(gif);
  });
});

describe('transcodeIfNeeded — sharp-unavailable fallback', () => {
  // Sharp may legitimately fail to load on platforms without prebuilt
  // binaries. The module's design is to NEVER fail the render — fall
  // back to writing raw bytes. We can't easily simulate sharp failure
  // without complex module mocking, so the contract is pinned by the
  // existing reason-code structure (also documented in the helper's
  // JSDoc). When sharp IS available + transcode succeeds, the result
  // shape is { transcoded: true, ... } — covered by integration tests
  // that run with sharp present.

  test('result shape is documented in the function — every failure path returns a buffer', async () => {
    // Exercise once with PNG → WebP. Two outcomes are valid:
    //   - sharp loaded → transcoded: true, reason undefined
    //   - sharp unavailable → transcoded: false, reason 'sharp-unavailable'
    // EITHER way, .buffer is non-null and .outputFormat is 'webp'.
    const png = Buffer.from([0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, 0,0,0,0]);
    const r = await transcodeIfNeeded(png, 'webp');
    expect(Buffer.isBuffer(r.buffer)).toBe(true);
    expect(r.outputFormat).toBe('webp');
    expect(r.inputFormat).toBe('png');
    if (!r.transcoded) {
      // Acceptable failure modes for this 12-byte fake buffer:
      //  - sharp-unavailable (CI / weird host)
      //  - transcode-failed  (sharp is loaded but rejects 12-byte fake PNG)
      //  - unknown-input     (some readers may not recognize a header-only buffer)
      expect(['sharp-unavailable', 'transcode-failed', 'unknown-input']).toContain(r.reason);
    }
  });
});
