import 'package:flutter_test/flutter_test.dart'; import 'package:smplayer/core/contracts/library.dart'; import 'package:smplayer/features/aggregated/aggregated_grouping.dart'; GlobalSearchServerResult _server( String id, String name, List items, ) => GlobalSearchServerResult( serverId: id, serverName: name, serverUrl: 'http://$id', token: 'tok-$id', userId: 'user-$id', items: items, ); EmbyRawItem _item( String id, String name, { String? type, Map? providerIds, String? lastPlayedDate, int? playbackPositionTicks, int? runtimeTicks, bool? played, }) => EmbyRawItem( Id: id, Name: name, Type: type, RunTimeTicks: runtimeTicks, UserData: playbackPositionTicks != null || played != null ? EmbyRawUserData( PlaybackPositionTicks: playbackPositionTicks, Played: played, ) : null, extra: { if (providerIds != null) 'ProviderIds': providerIds, if (lastPlayedDate != null) 'UserData': {'LastPlayedDate': lastPlayedDate}, }, ); void main() { group('flatten – default behavior (dedup off)', () { test('returns all items from all servers', () { final results = [ _server('s1', 'S1', [_item('a', 'Movie A')]), _server('s2', 'S2', [_item('b', 'Movie B')]), ]; final entries = flatten(results); expect(entries.length, 2); }); test('respects serverFilter', () { final results = [ _server('s1', 'S1', [_item('a', 'A')]), _server('s2', 'S2', [_item('b', 'B')]), ]; final entries = flatten(results, serverFilter: 's1'); expect(entries.length, 1); expect(entries.first.source.serverId, 's1'); }); }); group('flatten – dedup on, serverFilter null', () { test('collapses same TMDB Movie to newest LastPlayedDate', () { final results = [ _server('s1', 'S1', [ _item( 'a', 'Movie', type: 'Movie', providerIds: {'Tmdb': '100'}, lastPlayedDate: '2026-06-20T10:00:00Z', ), ]), _server('s2', 'S2', [ _item( 'b', 'Movie', type: 'Movie', providerIds: {'Tmdb': '100'}, lastPlayedDate: '2026-06-24T15:00:00Z', ), ]), ]; final entries = flatten(results, deduplicateAcrossServers: true); expect(entries.length, 1); expect(entries.first.source.serverId, 's2'); expect(entries.first.sources.map((source) => source.source.serverId), [ 's2', 's1', ]); }); test('preserves each server item and its independent playback state', () { final results = [ _server('s1', 'S1', [ _item( 'a', 'Movie', type: 'Movie', providerIds: {'Tmdb': '100'}, lastPlayedDate: '2026-06-20T10:00:00Z', playbackPositionTicks: 100, runtimeTicks: 1000, ), ]), _server('s2', 'S2', [ _item( 'b', 'Movie', type: 'Movie', providerIds: {'Tmdb': '100'}, lastPlayedDate: '2026-06-24T15:00:00Z', playbackPositionTicks: 800, runtimeTicks: 1000, ), ]), ]; final entry = flatten(results, deduplicateAcrossServers: true).single; expect(entry.sources, hasLength(2)); expect(entry.sources[0].item.UserData?.PlaybackPositionTicks, 800); expect(entry.sources[1].item.UserData?.PlaybackPositionTicks, 100); expect(entry.sources[0].item.RunTimeTicks, 1000); expect(entry.sources[1].item.RunTimeTicks, 1000); }); test('keeps input order when sources have equal LastPlayedDate', () { final results = [ _server('s1', 'S1', [ _item( 'a', 'Movie', type: 'Movie', providerIds: {'Tmdb': '100'}, lastPlayedDate: '2026-06-24T15:00:00Z', ), ]), _server('s2', 'S2', [ _item( 'b', 'Movie', type: 'Movie', providerIds: {'Tmdb': '100'}, lastPlayedDate: '2026-06-24T15:00:00Z', ), ]), ]; final entry = flatten(results, deduplicateAcrossServers: true).single; expect(entry.sources.map((source) => source.source.serverId), [ 's1', 's2', ]); }); }); group('flatten – dedup on + serverFilter non-null', () { test('skips dedup when server filter is active', () { final results = [ _server('s1', 'S1', [ _item('a', 'Movie', type: 'Movie', providerIds: {'Tmdb': '100'}), ]), _server('s2', 'S2', [ _item('b', 'Movie', type: 'Movie', providerIds: {'Tmdb': '100'}), ]), ]; final entries = flatten( results, serverFilter: 's1', deduplicateAcrossServers: true, ); expect(entries.length, 1); expect(entries.first.source.serverId, 's1'); expect(entries.first.sources, hasLength(1)); }); }); group('no ProviderIds – never deduped', () { test('items without ProviderIds stay separate', () { final results = [ _server('s1', 'S1', [_item('a', 'Same Name', type: 'Movie')]), _server('s2', 'S2', [_item('b', 'Same Name', type: 'Movie')]), ]; final entries = flatten(results, deduplicateAcrossServers: true); expect(entries.length, 2); }); }); group('Type namespace isolation', () { test('Movie and Series with same tmdb id stay separate', () { final results = [ _server('s1', 'S1', [ _item('a', 'Title', type: 'Movie', providerIds: {'Tmdb': '100'}), ]), _server('s2', 'S2', [ _item('b', 'Title', type: 'Series', providerIds: {'Tmdb': '100'}), ]), ]; final entries = flatten(results, deduplicateAcrossServers: true); expect(entries.length, 2); }); }); group('ProviderIds key case insensitivity', () { test('Tmdb vs tmdb vs TMDB all match', () { final results = [ _server('s1', 'S1', [ _item( 'a', 'M', type: 'Movie', providerIds: {'Tmdb': '99'}, lastPlayedDate: '2026-06-20T00:00:00Z', ), ]), _server('s2', 'S2', [ _item( 'b', 'M', type: 'Movie', providerIds: {'tmdb': '99'}, lastPlayedDate: '2026-06-21T00:00:00Z', ), ]), _server('s3', 'S3', [ _item( 'c', 'M', type: 'Movie', providerIds: {'TMDB': '99'}, lastPlayedDate: '2026-06-22T00:00:00Z', ), ]), ]; final entries = flatten(results, deduplicateAcrossServers: true); expect(entries.length, 1); expect(entries.first.source.serverId, 's3'); }); }); group('null LastPlayedDate handling', () { test('null loses to valid date', () { final results = [ _server('s1', 'S1', [ _item('a', 'M', type: 'Movie', providerIds: {'Tmdb': '1'}), ]), _server('s2', 'S2', [ _item( 'b', 'M', type: 'Movie', providerIds: {'Tmdb': '1'}, lastPlayedDate: '2026-06-01T00:00:00Z', ), ]), ]; final entries = flatten(results, deduplicateAcrossServers: true); expect(entries.length, 1); expect(entries.first.source.serverId, 's2'); }); test('both null keeps first encountered', () { final results = [ _server('s1', 'S1', [ _item('a', 'M', type: 'Movie', providerIds: {'Tmdb': '1'}), ]), _server('s2', 'S2', [ _item('b', 'M', type: 'Movie', providerIds: {'Tmdb': '1'}), ]), ]; final entries = flatten(results, deduplicateAcrossServers: true); expect(entries.length, 1); expect(entries.first.source.serverId, 's1'); }); }); group('transitive matching', () { test('A(tmdb:1) B(tmdb:1,imdb:tt2) C(imdb:tt2) collapse to one', () { final results = [ _server('s1', 'S1', [ _item( 'a', 'M', type: 'Movie', providerIds: {'Tmdb': '1'}, lastPlayedDate: '2026-06-20T00:00:00Z', ), ]), _server('s2', 'S2', [ _item( 'b', 'M', type: 'Movie', providerIds: {'Tmdb': '1', 'Imdb': 'tt2'}, lastPlayedDate: '2026-06-22T00:00:00Z', ), ]), _server('s3', 'S3', [ _item( 'c', 'M', type: 'Movie', providerIds: {'Imdb': 'tt2'}, lastPlayedDate: '2026-06-21T00:00:00Z', ), ]), ]; final entries = flatten(results, deduplicateAcrossServers: true); expect(entries.length, 1); expect(entries.first.source.serverId, 's2'); }); }); }