import 'dart:typed_data'; import 'package:cardash_launcher/data/models/jellyfin_library.dart'; import 'package:cardash_launcher/data/models/jellyfin_models.dart'; import 'package:cardash_launcher/data/services/jellyfin_library_service.dart'; import 'package:flutter_test/flutter_test.dart'; import 'jellyfin_fakes.dart'; void main() { test( 'recently played dedupes tracks by album and takes the newest five', () async { final gateway = FakeJellyfinLibraryGateway() ..played = [ track( 'a2', albumId: 'album-a', albumName: 'Album A', artist: 'Artist A', lastPlayed: DateTime.utc(2026, 10, 31, 12), ), track( 'a1', albumId: 'album-a', albumName: 'Album A', artist: 'Artist A', lastPlayed: DateTime.utc(2026, 10, 31, 11), ), track('b1', albumId: 'album-b', albumName: 'Album B', artist: 'B'), track('c1', albumId: 'album-c', albumName: 'Album C', artist: 'C'), track('d1', albumId: 'album-d', albumName: 'Album D', artist: 'D'), track('e1', albumId: 'album-e', albumName: 'Album E', artist: 'E'), track('f1', albumId: 'album-f', albumName: 'Album F', artist: 'F'), ]; final service = JellyfinLibraryService( connectionProvider: () => testJellyfinConnection, gateway: gateway, ); final albums = await service.latestPlayed(); expect(albums.map((album) => album.id), [ 'album-a', 'album-b', 'album-c', 'album-d', 'album-e', ]); expect(albums.first.name, 'Album A'); expect(albums.first.lastPlayed, DateTime.utc(2026, 10, 31, 12)); expect(gateway.playedCalls, 1); }, ); test('latest added delegates to the album query', () async { final gateway = FakeJellyfinLibraryGateway() ..added = [album('one'), album('two')]; final service = JellyfinLibraryService( connectionProvider: () => testJellyfinConnection, gateway: gateway, ); final albums = await service.latestAdded(limit: 2); expect(albums.map((entry) => entry.id), ['one', 'two']); expect(gateway.addedCalls, 1); }); test('genre families are cached and can be forced', () async { final gateway = FakeJellyfinLibraryGateway() ..genres = const ['Metal', 'Jazz', 'Рок']; final service = JellyfinLibraryService( connectionProvider: () => testJellyfinConnection, gateway: gateway, ); final first = await service.genreFamilies(); expect(first.map((family) => family.label), ['Metal', 'Rock', 'Jazz']); expect(gateway.genreCalls, 1); await service.genreFamilies(); expect(gateway.genreCalls, 1); await service.genreFamilies(force: true); expect(gateway.genreCalls, 2); }); test('random albums expand only the selected families', () async { final gateway = FakeJellyfinLibraryGateway() ..genres = const ['Metal', 'Melodic Metal', 'Метъл', 'Jazz', 'Рок'] ..random = [album('r1'), album('r2')]; final service = JellyfinLibraryService( connectionProvider: () => testJellyfinConnection, gateway: gateway, ); final albums = await service.randomAlbums( selections: const [ JellyfinGenreSelection(id: 'metal', label: 'Metal'), JellyfinGenreSelection(id: 'jazz', label: 'Jazz'), ], ); expect(albums.map((entry) => entry.id), ['r1', 'r2']); expect( gateway.lastRandomGenres, containsAll(['Metal', 'Melodic Metal', 'Метъл', 'Jazz']), ); expect(gateway.lastRandomGenres, isNot(contains('Рок'))); }); test('no selected genres means no random request', () async { final gateway = FakeJellyfinLibraryGateway(); final service = JellyfinLibraryService( connectionProvider: () => testJellyfinConnection, gateway: gateway, ); final albums = await service.randomAlbums(selections: const []); expect(albums, isEmpty); expect(gateway.randomCalls, 0); }); test('stream URL carries no token and uses the universal endpoint', () { final gateway = FakeJellyfinLibraryGateway(); final service = JellyfinLibraryService( connectionProvider: () => testJellyfinConnection, gateway: gateway, ); final url = service.streamUrl( track('a1', mediaSourceId: 'source-a1'), maxStreamingBitrate: 320000, ); final text = url.toString(); expect(text, contains('Audio/a1/universal')); expect(text, contains('MediaSourceId=source-a1')); expect(text, contains('MaxStreamingBitrate=320000')); expect(text, contains('EnableRedirection=true')); expect(text, isNot(contains('test-token'))); expect(text, isNot(contains('api_key'))); }); test('buildQueue produces a native queue with artwork and auth', () async { final gateway = FakeJellyfinLibraryGateway() ..tracks = { 'album-a': [ track('a1', duration: const Duration(seconds: 60)), track('a2', duration: const Duration(seconds: 120)), ], } ..artwork = Uint8List.fromList([1, 2, 3]); final service = JellyfinLibraryService( connectionProvider: () => testJellyfinConnection, gateway: gateway, ); final queue = await service.buildQueue( album('album-a', imageTag: 'tag-a'), playSessionId: 'session-1', ); expect(queue, isNotNull); expect(queue!.items.length, 2); expect(queue.items.first.trackId, 'a1'); expect(queue.items.first.durationMs, 60000); expect(queue.items.first.url, contains('Audio/a1/universal')); expect(queue.playSessionId, 'session-1'); expect(queue.authorization, contains('Token="test-token"')); expect(queue.artwork, [1, 2, 3]); expect(queue.items.first.url, isNot(contains('test-token'))); }); test( 'buildQueue returns null for an album with no playable tracks', () async { final gateway = FakeJellyfinLibraryGateway(); final service = JellyfinLibraryService( connectionProvider: () => testJellyfinConnection, gateway: gateway, ); final queue = await service.buildQueue(album('empty')); expect(queue, isNull); }, ); test( 'image URL and headers are available without exposing the token in the URL', () { final gateway = FakeJellyfinLibraryGateway(); final service = JellyfinLibraryService( connectionProvider: () => testJellyfinConnection, gateway: gateway, ); final url = service.imageUrl('album-a', 'tag-a', maxHeight: 300); expect(url, isNotNull); expect(url.toString(), contains('Items/album-a/Images/Primary')); expect(url.toString(), contains('maxHeight=300')); expect(url.toString(), contains('tag=tag-a')); expect(url.toString(), isNot(contains('test-token'))); expect( service.imageHeaders['Authorization'], contains('Token="test-token"'), ); }, ); test('a missing connection fails without touching the gateway', () async { final gateway = FakeJellyfinLibraryGateway(); final service = JellyfinLibraryService( connectionProvider: () => null, gateway: gateway, ); expect(service.isConfigured, isFalse); await expectLater( service.latestPlayed(), throwsA(isA()), ); expect(gateway.playedCalls, 0); }); }