// SPDX-License-Identifier: AGPL-3.0-or-later

import {describe, expect, it} from 'vitest';
import {buildProcessedMediaObject, stripNonJpegImageMetadataForUpload} from './StorageObjectHelpers';

const PNG_SIGNATURE = new Uint8Array([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]);

function concatBytes(chunks: ReadonlyArray<Uint8Array>): Uint8Array {
	const totalLength = chunks.reduce((sum, chunk) => sum + chunk.length, 0);
	const out = new Uint8Array(totalLength);
	let offset = 0;
	for (const chunk of chunks) {
		out.set(chunk, offset);
		offset += chunk.length;
	}
	return out;
}

function textBytes(value: string): Uint8Array {
	const encoded = new TextEncoder().encode(value);
	const out = new Uint8Array(encoded.length);
	out.set(encoded);
	return out;
}

function chunk(type: string, data: Uint8Array<ArrayBufferLike> = new Uint8Array()): Uint8Array {
	const out = new Uint8Array(12 + data.length);
	out[0] = (data.length >>> 24) & 0xff;
	out[1] = (data.length >>> 16) & 0xff;
	out[2] = (data.length >>> 8) & 0xff;
	out[3] = data.length & 0xff;
	out.set(textBytes(type), 4);
	out.set(data, 8);
	return out;
}

function chunkNames(data: Uint8Array): Array<string> {
	const names: Array<string> = [];
	let offset = PNG_SIGNATURE.length;
	while (offset + 12 <= data.length) {
		const length =
			((data[offset]! << 24) | (data[offset + 1]! << 16) | (data[offset + 2]! << 8) | data[offset + 3]!) >>> 0;
		names.push(String.fromCharCode(data[offset + 4]!, data[offset + 5]!, data[offset + 6]!, data[offset + 7]!));
		offset += 12 + length;
	}
	return names;
}

function pngWithPrivateChunks(): Uint8Array {
	return concatBytes([
		PNG_SIGNATURE,
		chunk('IHDR', new Uint8Array(13)),
		chunk('tEXt', textBytes('GPS=1,2')),
		chunk('prIv', textBytes('private metadata')),
		chunk('IDAT', new Uint8Array([1, 2, 3])),
		chunk('IEND'),
	]);
}

describe('stripNonJpegImageMetadataForUpload', () => {
	it('strips PNG metadata and unknown ancillary chunks while preserving image chunks', async () => {
		const input = pngWithPrivateChunks();
		const stripped = await stripNonJpegImageMetadataForUpload(input, 'image/png');
		expect(stripped.contentType).toBe('image/png');
		expect(chunkNames(stripped.body)).toEqual(['IHDR', 'IDAT', 'IEND']);
	});
	it('preserves APNG animation chunks', async () => {
		const input = concatBytes([
			PNG_SIGNATURE,
			chunk('IHDR', new Uint8Array(13)),
			chunk('acTL', new Uint8Array(8)),
			chunk('fcTL', new Uint8Array(26)),
			chunk('tEXt', textBytes('camera=private')),
			chunk('fdAT', new Uint8Array([1, 2, 3])),
			chunk('IDAT', new Uint8Array([4, 5, 6])),
			chunk('IEND'),
		]);
		const stripped = await stripNonJpegImageMetadataForUpload(input, 'image/apng');
		expect(stripped.contentType).toBe('image/apng');
		expect(chunkNames(stripped.body)).toEqual(['IHDR', 'acTL', 'fcTL', 'fdAT', 'IDAT', 'IEND']);
	});
});

describe('buildProcessedMediaObject', () => {
	it('leaves non-media objects for plain copy', async () => {
		await expect(buildProcessedMediaObject(textBytes('plain text'), 'text/plain')).resolves.toBeNull();
	});
});
