// @vitest-environment jsdom
import { describe, it, expect } from 'vitest';
import type { ImageLoader } from './imageUtils';
import {
	base64ByteLength,
	compressToTarget,
	UndecodableImageError,
	resizeAvatarImage,
	compressGarmentImage,
} from './imageUtils';

describe( 'base64ByteLength', () => {
	it( 'returns the decoded byte length of a data URL payload (no padding)', () => {
		// "AAAA" is 4 base64 chars -> 3 decoded bytes.
		expect( base64ByteLength( 'data:image/png;base64,AAAA' ) ).toBe( 3 );
	} );

	it( 'accounts for base64 padding', () => {
		expect( base64ByteLength( 'data:image/jpeg;base64,AA==' ) ).toBe( 1 );
		expect( base64ByteLength( 'data:image/jpeg;base64,AAA=' ) ).toBe( 2 );
	} );
} );

// A deterministic fake loader: `bytesFor` decides the encoded size so we can
// drive the descent without a real canvas. The encoded dataUrl embeds the
// chosen dimensions/quality so tests can assert what was produced.
function fakeLoader( spec: {
	width: number;
	height: number;
	hasAlpha: boolean;
	bytesFor: ( w: number, h: number, format: 'jpeg' | 'png', q: number ) => number;
} ): ImageLoader {
	return async () => ( {
		width: spec.width,
		height: spec.height,
		hasAlpha: spec.hasAlpha,
		encode: ( w, h, format, q ) => ( {
			dataUrl: `data:image/${ format };base64,${ w }x${ h }@${ q }`,
			bytes: spec.bytesFor( w, h, format, q ),
		} ),
	} );
}

const OPTS = {
	maxWidth: 768,
	maxHeight: 1024,
	targetBytes: 1_500_000,
	startQuality: 0.85,
	minQuality: 0.6,
	qualityStep: 0.1,
	minDimension: 512,
	dimensionStep: 0.85,
};

describe( 'compressToTarget', () => {
	it( 'caps dimensions and returns immediately when already under target (JPEG for opaque)', async () => {
		// 1536x768 opaque -> capRatio 0.5 -> 768x384; small bytes -> no descent.
		const loader = fakeLoader( { width: 1536, height: 768, hasAlpha: false, bytesFor: () => 1000 } );
		const out = await compressToTarget( 'x', OPTS, loader );
		expect( out ).toBe( 'data:image/jpeg;base64,768x384@0.85' );
	} );

	it( 'lowers JPEG quality until under target before shrinking dimensions', async () => {
		const loader = fakeLoader( {
			width: 1536, height: 768, hasAlpha: false,
			bytesFor: ( _w, _h, _f, q ) => ( q > 0.8 ? 2_000_000 : 1_000_000 ),
		} );
		const out = await compressToTarget( 'x', OPTS, loader );
		expect( out ).toBe( 'data:image/jpeg;base64,768x384@0.75' );
	} );

	it( 'shrinks dimensions when quality alone cannot reach the target', async () => {
		// bytes depend only on width; > 600px never fits, <= 600px fits.
		const loader = fakeLoader( {
			width: 1536, height: 768, hasAlpha: false,
			bytesFor: ( w ) => ( w > 600 ? 2_000_000 : 1_000_000 ),
		} );
		const out = await compressToTarget( 'x', OPTS, loader );
		// 768 -> *0.85=653 -> *0.85=555 (<=600) at first quality 0.85.
		expect( out ).toBe( 'data:image/jpeg;base64,555x277@0.85' );
	} );

	it( 'uses PNG and dimension-only descent for images with alpha', async () => {
		const loader = fakeLoader( {
			width: 1536, height: 768, hasAlpha: true,
			bytesFor: ( w ) => ( w > 600 ? 2_000_000 : 1_000_000 ),
		} );
		const out = await compressToTarget( 'x', OPTS, loader );
		expect( out ).toBe( 'data:image/png;base64,555x277@1' );
	} );

	it( 'never refuses for size: returns best-effort at the floor', async () => {
		const loader = fakeLoader( { width: 1536, height: 768, hasAlpha: false, bytesFor: () => 9_000_000 } );
		const out = await compressToTarget( 'x', OPTS, loader );
		// Floor: long edge clamped to minDimension 512 -> 512x256.
		expect( out ).toBe( 'data:image/jpeg;base64,512x256@0.6' );
	} );

	it( 'does not upscale a small source', async () => {
		const loader = fakeLoader( { width: 300, height: 300, hasAlpha: false, bytesFor: () => 1000 } );
		const out = await compressToTarget( 'x', OPTS, loader );
		expect( out ).toBe( 'data:image/jpeg;base64,300x300@0.85' );
	} );

	it( 'rejects with UndecodableImageError when the loader cannot decode', async () => {
		const loader: ImageLoader = async () => {
			throw new UndecodableImageError();
		};
		await expect( compressToTarget( 'x', OPTS, loader ) ).rejects.toBeInstanceOf( UndecodableImageError );
	} );
} );

describe( 'wrappers apply per-image dimension caps', () => {
	const opaqueBig = ( ): ImageLoader =>
		fakeLoader( { width: 4000, height: 4000, hasAlpha: false, bytesFor: () => 1000 } );

	it( 'resizeAvatarImage caps within 768x1024', async () => {
		const out = await resizeAvatarImage( 'x', opaqueBig() );
		expect( out ).toBe( 'data:image/jpeg;base64,768x768@0.85' );
	} );

	it( 'compressGarmentImage caps within 1024x1024', async () => {
		const out = await compressGarmentImage( 'x', opaqueBig() );
		expect( out ).toBe( 'data:image/jpeg;base64,1024x1024@0.85' );
	} );
} );
