import 'dart:typed_data'; import 'package:cxyz_android/services/ntfy_notifier.dart'; import 'package:cxyz_android/services/ntfy_settings.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:image/image.dart' as img; void main() { group('NtfyNotifier.formatBytes', () { test('formats B KB MB', () { expect(NtfyNotifier.formatBytes(500), '500 B'); expect(NtfyNotifier.formatBytes(1536), '1.5 KB'); expect(NtfyNotifier.formatBytes(5 * 1024 * 1024), '5.0 MB'); }); }); group('payload titles and body', () { test('completed title is scannable', () { expect( NtfyNotifier.completedTitle('One Piece', 'Chapter 1100'), 'Downloaded · One Piece · Chapter 1100', ); }); test('failed title includes series and chapter', () { expect( NtfyNotifier.failedTitle('One Piece', 'Chapter 1100'), 'Download failed · One Piece · Chapter 1100', ); }); test('drained title includes counts', () { expect( NtfyNotifier.drainedTitle(ok: 5, failed: 1), 'Queue finished · 5 downloaded · 1 failed', ); }); test('completed body includes sizes and savings', () { final body = NtfyNotifier.completedBody( imageCount: 36, rawBytes: 10 * 1024 * 1024, cbzBytes: 8 * 1024 * 1024, ); expect(body, contains('Pages: 36')); expect(body, contains('Images:')); expect(body, contains('CBZ:')); expect(body, contains('saved 20%')); }); test('completed body appends note', () { final body = NtfyNotifier.completedBody( imageCount: 1, rawBytes: 100, cbzBytes: 90, note: 'Attachment skipped (15 MB limit)', ); expect(body, contains('Attachment skipped (15 MB limit)')); }); }); group('NtfyAttachmentMode', () { test('fromStorage defaults to none', () { expect(NtfyAttachmentMode.fromStorage(null), NtfyAttachmentMode.none); expect(NtfyAttachmentMode.fromStorage('nope'), NtfyAttachmentMode.none); expect( NtfyAttachmentMode.fromStorage('firstImage'), NtfyAttachmentMode.firstImage, ); }); }); group('preview resize', () { test('resizePreviewIsolate shrinks large image under 250 KB', () { final big = img.Image(width: 2000, height: 3000); img.fill(big, color: img.ColorRgb8(40, 80, 120)); // Add noise so JPEG does not compress to near-zero. for (var y = 0; y < big.height; y += 17) { for (var x = 0; x < big.width; x += 13) { big.setPixelRgb(x, y, (x * 3) % 255, (y * 5) % 255, 90); } } final raw = Uint8List.fromList(img.encodeJpg(big, quality: 95)); expect(raw.length, greaterThan(250 * 1024)); final out = resizePreviewForTest(raw); expect(out, isNotNull); expect(out!.length, lessThanOrEqualTo(250 * 1024)); final decoded = img.decodeImage(out)!; final longest = decoded.width > decoded.height ? decoded.width : decoded.height; expect(longest, lessThanOrEqualTo(720)); }); }); group('encodeNtfyHeader', () { test('leaves plain ASCII unchanged', () { expect(encodeNtfyHeader('Downloaded Chapter 1'), 'Downloaded Chapter 1'); }); test('RFC 2047-encodes middle-dot titles', () { final encoded = encodeNtfyHeader('Downloaded · Series · Ch 1'); expect(encoded.startsWith('=?UTF-8?B?'), isTrue); expect(encoded.endsWith('?='), isTrue); expect(encoded.codeUnits.every((c) => c >= 0x20 && c <= 0x7e), isTrue); }); test('RFC 2047-encodes multiline message', () { final encoded = encodeNtfyHeader('Pages: 3\nImages: 1 MB\nCBZ: 0.9 MB'); expect(encoded.startsWith('=?UTF-8?B?'), isTrue); expect(encoded.contains('\n'), isFalse); }); }); }