import 'dart:io'; import 'package:flutter_test/flutter_test.dart'; import 'package:spotiflac_android/models/settings.dart'; import 'package:spotiflac_android/screens/queue_library_refresh_policy.dart'; import 'package:spotiflac_android/services/library_database.dart'; void main() { const empty = QueueLibraryCounts( allTrackCount: 0, albumCount: 0, singleTrackCount: 0, ); const cached = QueueLibraryCounts( allTrackCount: 20, albumCount: 2, singleTrackCount: 3, ); const fallback = QueueLibraryCounts( allTrackCount: 15, albumCount: 1, singleTrackCount: 4, ); test('keeps cached counts when a download refresh returns empty', () { final resolved = resolveQueueLibraryCountsSnapshot( current: empty, cached: cached, activeDownloadFallback: fallback, ); expect(identical(resolved, cached), isTrue); }); test('uses in-memory history when no page cache exists yet', () { final resolved = resolveQueueLibraryCountsSnapshot( current: empty, activeDownloadFallback: fallback, ); expect(identical(resolved, fallback), isTrue); }); test('allows a legitimate empty refresh without an active fallback', () { final resolved = resolveQueueLibraryCountsSnapshot( current: empty, cached: cached, ); expect(identical(resolved, empty), isTrue); expect( shouldRetainQueueLibraryPageSnapshot( currentIsEmpty: true, cachedHasContent: true, activeDownloadFallbackAvailable: false, ), isFalse, ); }); test('retains a non-empty page only while its fallback is available', () { expect( shouldRetainQueueLibraryPageSnapshot( currentIsEmpty: true, cachedHasContent: true, activeDownloadFallbackAvailable: true, ), isTrue, ); expect( shouldRetainQueueLibraryPageSnapshot( currentIsEmpty: false, cachedHasContent: true, activeDownloadFallbackAvailable: true, ), isFalse, ); }); test('completion bridge prefers distinct finalized path candidates', () { expect( resolveCompletionBridgePlayableCandidates( historyFilePath: ' /music/final.flac ', completedItemFilePath: '/music/completed.flac', ), ['/music/final.flac', '/music/completed.flac'], ); expect( resolveCompletionBridgePlayableCandidates( historyFilePath: ' /music/final.flac ', completedItemFilePath: '/music/final.flac', ), ['/music/final.flac'], ); expect( resolveCompletionBridgePlayablePath( historyFilePath: ' ', completedItemFilePath: ' /music/completed.flac ', ), '/music/completed.flac', ); expect( resolveCompletionBridgePlayablePath( historyFilePath: null, completedItemFilePath: '', ), isNull, ); }); test( 'completion bridge survives partial album cancellation until landing', () { expect( shouldRetainCompletionBridge( isRequeued: false, hasActiveDownloads: false, libraryRowLanded: false, hasHistoryItem: true, expired: true, ), isTrue, ); expect( shouldRetainCompletionBridge( isRequeued: false, hasActiveDownloads: false, libraryRowLanded: true, hasHistoryItem: true, expired: true, ), isFalse, ); }, ); test('unpersisted completion bridge retains only its short grace period', () { expect( shouldRetainCompletionBridge( isRequeued: false, hasActiveDownloads: false, libraryRowLanded: false, hasHistoryItem: false, expired: false, ), isTrue, ); expect( shouldRetainCompletionBridge( isRequeued: false, hasActiveDownloads: false, libraryRowLanded: false, hasHistoryItem: false, expired: true, ), isFalse, ); }); test('completion bridge fallback follows the current label mode', () { expect( buildCompletionBridgeFallbackQualityLabel( mode: AppSettings.libraryQualityLabelFileFormat, completedItemFilePath: '/music/completed.flac', storedQuality: '24-bit/96kHz', ), 'FLAC', ); expect( buildCompletionBridgeFallbackQualityLabel( mode: AppSettings.libraryQualityLabelBitDepthOnly, completedItemFilePath: '/music/completed.flac', storedQuality: '24-bit/96kHz', ), '24-bit', ); expect( buildCompletionBridgeFallbackQualityLabel( mode: AppSettings.libraryQualityLabelBitrate, completedItemFilePath: '/music/completed.flac', storedQuality: '24-bit/96kHz', ), isNull, ); }); test( 'completion probe falls back from stale history to completed path', () async { final checkedPaths = []; final probe = CompletionBridgePlayableProbeCache( pathExists: (path) async { checkedPaths.add(path); return path == '/music/completed.flac'; }, retryDelays: const [], ); addTearDown(probe.dispose); final result = probe.listenable( historyFilePath: '/music/stale.flac', completedItemFilePath: '/music/completed.flac', ); expect(result.value.status, CompletionBridgePlayableStatus.checking); await Future.delayed(const Duration(milliseconds: 10)); expect(checkedPaths, ['/music/stale.flac', '/music/completed.flac']); expect(result.value.status, CompletionBridgePlayableStatus.playable); expect(result.value.path, '/music/completed.flac'); }, ); test('library file cache hides transient publication misses', () async { var checks = 0; final cache = LibraryFileAvailabilityCache( pathExists: (_) async => ++checks >= 3, normalizePath: (path) => path ?? '', retryDelays: const [Duration.zero, Duration.zero], ); addTearDown(cache.dispose); final availability = cache.listenable('/music/new.flac'); final publishedValues = []; availability.addListener(() => publishedValues.add(availability.value)); expect(availability.value, isTrue); await Future.delayed(const Duration(milliseconds: 20)); expect(checks, 3); expect(availability.value, isTrue); expect(publishedValues, isNot(contains(false))); }); test('library file cache rechecks a stale confirmed miss', () async { var exists = false; final cache = LibraryFileAvailabilityCache( pathExists: (_) async => exists, normalizePath: (path) => path ?? '', retryDelays: const [], missingRecheckAfter: Duration.zero, ); addTearDown(cache.dispose); final availability = cache.listenable('/music/late.flac'); await Future.delayed(const Duration(milliseconds: 10)); expect(availability.value, isFalse); exists = true; expect(identical(cache.listenable('/music/late.flac'), availability), true); await Future.delayed(const Duration(milliseconds: 10)); expect(availability.value, isTrue); }); test('completion probe heals the normal library file cache', () async { final availabilityCache = LibraryFileAvailabilityCache( pathExists: (_) async => false, normalizePath: (path) => path ?? '', retryDelays: const [], ); addTearDown(availabilityCache.dispose); final availability = availabilityCache.listenable( 'content://downloads/final.flac', ); await Future.delayed(const Duration(milliseconds: 10)); expect(availability.value, isFalse); final completionProbe = CompletionBridgePlayableProbeCache( pathExists: (_) async => true, onPlayable: availabilityCache.markExists, retryDelays: const [], ); addTearDown(completionProbe.dispose); completionProbe.listenable( completedItemFilePath: 'content://downloads/final.flac', ); await Future.delayed(const Duration(milliseconds: 10)); expect(availability.value, isTrue); }); test( 'download completion explicitly refreshes cached missing paths', () async { var exists = false; final cache = LibraryFileAvailabilityCache( pathExists: (_) async => exists, normalizePath: (path) => path ?? '', retryDelays: const [], ); addTearDown(cache.dispose); final availability = cache.listenable('/music/reused.flac'); await Future.delayed(const Duration(milliseconds: 10)); expect(availability.value, isFalse); exists = true; cache.refreshForPath('/music/reused.flac'); expect(availability.value, isTrue); await Future.delayed(const Duration(milliseconds: 10)); expect(availability.value, isTrue); }, ); test( 'completion probe retries publication before reporting missing', () async { var checks = 0; final probe = CompletionBridgePlayableProbeCache( pathExists: (_) async => ++checks >= 3, retryDelays: const [Duration.zero, Duration.zero], ); addTearDown(probe.dispose); final result = probe.listenable( completedItemFilePath: 'content://downloads/final.flac', ); await Future.delayed(const Duration(milliseconds: 20)); expect(checks, 3); expect(result.value.status, CompletionBridgePlayableStatus.playable); expect(result.value.path, 'content://downloads/final.flac'); }, ); test('completion probe can refresh a cached missing destination', () async { var exists = false; final probe = CompletionBridgePlayableProbeCache( pathExists: (_) async => exists, retryDelays: const [], ); addTearDown(probe.dispose); final result = probe.listenable( completedItemFilePath: '/music/reused.flac', ); await Future.delayed(const Duration(milliseconds: 10)); expect(result.value.status, CompletionBridgePlayableStatus.missing); exists = true; probe.refreshForPath('/music/reused.flac'); expect(result.value.status, CompletionBridgePlayableStatus.checking); await Future.delayed(const Duration(milliseconds: 10)); expect(result.value.status, CompletionBridgePlayableStatus.playable); expect(result.value.path, '/music/reused.flac'); }); test( 'completion probe does not evict an actively listened notifier', () async { final probe = CompletionBridgePlayableProbeCache( pathExists: (_) async => true, retryDelays: const [], maxEntries: 2, ); addTearDown(probe.dispose); final first = probe.listenable( completedItemFilePath: '/music/first.flac', ); void listener() {} first.addListener(listener); final second = probe.listenable( completedItemFilePath: '/music/second.flac', ); probe.listenable(completedItemFilePath: '/music/third.flac'); await Future.delayed(const Duration(milliseconds: 10)); expect( identical( first, probe.listenable(completedItemFilePath: '/music/first.flac'), ), isTrue, ); expect( identical( second, probe.listenable(completedItemFilePath: '/music/second.flac'), ), isFalse, ); expect(() => first.removeListener(listener), returnsNormally); }, ); test('completion bridge cards probe before showing Play or missing', () { final source = File( 'lib/screens/queue_tab_collection_items.dart', ).readAsStringSync(); final gridStart = source.indexOf('Widget _buildBridgeGridItem('); final listStart = source.indexOf('Widget _buildBridgeListItem('); final badgeStart = source.indexOf('Widget _buildLibraryQualityBadge('); expect(gridStart, greaterThanOrEqualTo(0)); expect(listStart, greaterThan(gridStart)); expect(badgeStart, greaterThan(listStart)); final gridSource = source.substring(gridStart, listStart); final listSource = source.substring(listStart, badgeStart); expect(gridSource, contains('resolveCompletionBridgePlayablePath(')); expect(gridSource, contains('_completionBridgePlayableProbe.listenable(')); expect(gridSource, contains('CompletionBridgePlayableStatus.checking')); expect(gridSource, contains('semanticsLabel:')); expect(gridSource, contains('queueCheckingDownloadedFile')); expect(gridSource, contains('TrackGridPlayButton(')); expect(listSource, contains('resolveCompletionBridgePlayablePath(')); expect(listSource, contains('_completionBridgePlayableProbe.listenable(')); expect(listSource, contains('CompletionBridgePlayableStatus.checking')); expect(listSource, contains('semanticsLabel:')); expect(listSource, contains('queueCheckingDownloadedFile')); expect(listSource, contains('Icons.play_arrow')); }); }