import 'package:flutter/material.dart';
import 'package:plezy/media/ids.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:plezy/media/media_backend.dart';
import 'package:plezy/media/media_item.dart';
import 'package:plezy/media/media_kind.dart';
import 'package:plezy/media/media_server_client.dart';
import 'package:plezy/media/play_queue.dart';
import 'package:plezy/models/plex/play_queue_response.dart';
import 'package:plezy/providers/multi_server_provider.dart';
import 'package:plezy/providers/playback_state_provider.dart';
import 'package:plezy/services/data_aggregation_service.dart';
import 'package:plezy/services/episode_navigation_service.dart';
import 'package:plezy/services/multi_server_manager.dart';
import 'package:provider/provider.dart';
import '../test_helpers/media_items.dart';
import '../test_helpers/multi_server_fixtures.dart';

MediaItem _meta(String id, {String? title}) =>
    testMediaItem(id: id, backend: MediaBackend.plex, kind: MediaKind.episode, title: title ?? 'Episode $id');

MediaItem _jfEpisode(String id, {required String seriesId, ServerId? serverId}) => testMediaItem(
  id: id,
  backend: MediaBackend.jellyfin,
  kind: MediaKind.episode,
  title: 'Episode $id',
  serverId: serverId ?? ServerId('srv-jf'),
  grandparentId: seriesId,
);

MediaItem _jfMovie(String id) => testMediaItem(
  id: id,
  backend: MediaBackend.jellyfin,
  kind: MediaKind.movie,
  title: 'Movie $id',
  serverId: ServerId('srv-jf'),
);

PlexMediaItem _plexMovie(String id, int playQueueItemId) =>
    PlexMediaItem(id: id, kind: MediaKind.movie, title: 'Movie $id', playQueueItemId: playQueueItemId);

MediaItem _plexEpisode(String id, {required String seriesId, int? viewCount}) => testMediaItem(
  id: id,
  backend: MediaBackend.plex,
  kind: MediaKind.episode,
  title: 'Episode $id',
  serverId: 'srv-plex',
  grandparentId: seriesId,
  viewCount: viewCount,
);

/// MultiServerManager subclass that returns a pre-supplied client without
/// going through the production add-connection flow. The base class doesn't
/// expose a way to inject clients into its private `_clients` map, so we
/// override the lookup directly.
class _StubManager extends MultiServerManager {
  _StubManager(this._client);
  final MediaServerClient? _client;
  @override
  MediaServerClient? getClient(String _) => _client;
}

/// Recording client whose `fetchClientSideEpisodeQueue` is observable —
/// callers can assert it was (or wasn't) hit.
class _RecordingClient implements MediaServerClient {
  _RecordingClient({required this.seriesEpisodes, this.clientBackend = MediaBackend.jellyfin, this.fetchError});
  final List<MediaItem> seriesEpisodes;
  final MediaBackend clientBackend;
  final Object? fetchError;
  final List<String> seriesQueueCalls = [];

  @override
  Future<List<MediaItem>?> fetchClientSideEpisodeQueue(String seriesId) async {
    seriesQueueCalls.add(seriesId);
    final error = fetchError;
    if (error != null) throw error;
    return seriesEpisodes;
  }

  @override
  MediaBackend get backend => clientBackend;

  @override
  dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation);
}

class _ProbeWidget extends StatefulWidget {
  const _ProbeWidget({required this.metadata, required this.onResult});

  final MediaItem metadata;
  final void Function(AdjacentEpisodes) onResult;

  @override
  State<_ProbeWidget> createState() => _ProbeWidgetState();
}

class _ProbeWidgetState extends State<_ProbeWidget> {
  @override
  void initState() {
    super.initState();
    WidgetsBinding.instance.addPostFrameCallback((_) async {
      if (!mounted) return;
      final svc = EpisodeNavigationService();
      final result = await svc.loadAdjacentEpisodes(context: context, metadata: widget.metadata);
      widget.onResult(result);
    });
  }

  @override
  Widget build(BuildContext context) =>
      const Directionality(textDirection: TextDirection.ltr, child: SizedBox.shrink());
}

void main() {
  TestWidgetsFlutterBinding.ensureInitialized();

  group('loadAdjacentEpisodes', () {
    testWidgets('returns unavailable when no play queue is active for a standalone movie', (tester) async {
      final playback = PlaybackStateProvider();
      addTearDown(playback.dispose);
      final manager = _StubManager(null);
      final serverProvider = testMultiServerProvider(manager);
      addTearDown(serverProvider.dispose);

      AdjacentEpisodes? result;
      await tester.pumpWidget(
        MultiProvider(
          providers: [
            ChangeNotifierProvider<PlaybackStateProvider>.value(value: playback),
            ChangeNotifierProvider<MultiServerProvider>.value(value: serverProvider),
          ],
          child: _ProbeWidget(metadata: _jfMovie('42'), onResult: (r) => result = r),
        ),
      );
      await tester.pump();
      await tester.pump();

      expect(result, isNotNull);
      expect(result!.nextStatus, QueueNavigationStatus.unavailable);
      expect(result!.previousStatus, QueueNavigationStatus.unavailable);
      expect(result!.hasNext, isFalse);
      expect(result!.hasPrevious, isFalse);
      expect(playback.isQueueActive, isFalse);
    });

    testWidgets('preserves adjacency for a movie in an active local queue', (tester) async {
      final previous = _jfMovie('movie-1');
      final current = _jfMovie('movie-2');
      final next = _jfMovie('movie-3');
      final playback = PlaybackStateProvider();
      addTearDown(playback.dispose);
      playback.setPlaybackFromLocalQueue(
        LocalPlayQueue(
          id: 'jellyfin:playlist-movies',
          items: [previous, current, next],
          currentIndex: 1,
          backendId: MediaBackend.jellyfin.id,
        ),
        contextKey: 'playlist-movies',
      );
      final client = _RecordingClient(seriesEpisodes: const []);
      final manager = _StubManager(client);
      final serverProvider = MultiServerProvider(manager, DataAggregationService(manager));
      addTearDown(serverProvider.dispose);

      AdjacentEpisodes? result;
      await tester.pumpWidget(
        MultiProvider(
          providers: [
            ChangeNotifierProvider<PlaybackStateProvider>.value(value: playback),
            ChangeNotifierProvider<MultiServerProvider>.value(value: serverProvider),
          ],
          child: _ProbeWidget(metadata: current, onResult: (r) => result = r),
        ),
      );
      await tester.pump();
      await tester.pump();

      expect(client.seriesQueueCalls, isEmpty);
      expect(playback.isQueueActive, isTrue);
      expect(playback.shuffleContextKey, 'playlist-movies');
      expect(playback.currentQueueItem, same(current));
      expect(playback.currentPlayQueueItemID, 1);
      expect(playback.loadedItems, [previous, current, next]);
      expect(result, isNotNull);
      expect(result!.nextStatus, QueueNavigationStatus.found);
      expect(result!.previousStatus, QueueNavigationStatus.found);
      expect(result!.next, same(next));
      expect(result!.previous, same(previous));
      expect(result!.hasNext, isTrue);
      expect(result!.hasPrevious, isTrue);
    });

    testWidgets('preserves a local movie collection queue for a current-item clone', (tester) async {
      final previous = _jfMovie('movie-1');
      final storedCurrent = _jfMovie('movie-2');
      final next = _jfMovie('movie-3');
      final playback = PlaybackStateProvider();
      addTearDown(playback.dispose);
      playback.setPlaybackFromLocalQueue(
        LocalPlayQueue(
          id: 'jellyfin:collection-movies',
          items: [previous, storedCurrent, next],
          currentIndex: 1,
          backendId: MediaBackend.jellyfin.id,
        ),
        contextKey: 'collection-movies',
      );
      final client = _RecordingClient(seriesEpisodes: const []);
      final manager = _StubManager(client);
      final serverProvider = MultiServerProvider(manager, DataAggregationService(manager));
      addTearDown(serverProvider.dispose);
      final currentClone = storedCurrent.copyWith(viewOffsetMs: 42);

      AdjacentEpisodes? result;
      await tester.pumpWidget(
        MultiProvider(
          providers: [
            ChangeNotifierProvider<PlaybackStateProvider>.value(value: playback),
            ChangeNotifierProvider<MultiServerProvider>.value(value: serverProvider),
          ],
          child: _ProbeWidget(metadata: currentClone, onResult: (r) => result = r),
        ),
      );
      await tester.pump();
      await tester.pump();

      expect(client.seriesQueueCalls, isEmpty);
      expect(playback.isQueueActive, isTrue);
      expect(playback.shuffleContextKey, 'collection-movies');
      expect(playback.currentQueueItem, same(storedCurrent));
      expect(playback.currentPlayQueueItemID, 1);
      expect(playback.loadedItems, [previous, storedCurrent, next]);
      expect(result, isNotNull);
      expect(result!.next, same(next));
      expect(result!.previous, same(previous));
      expect(result!.hasNext, isTrue);
      expect(result!.hasPrevious, isTrue);
    });

    testWidgets('extends a Plex movie queue window for adjacency', (tester) async {
      final previous = _plexMovie('movie-1', 1001);
      final current = _plexMovie('movie-2', 1002);
      final next = _plexMovie('movie-3', 1003);
      final playback = PlaybackStateProvider();
      addTearDown(playback.dispose);
      await playback.setPlaybackFromPlayQueue(
        PlayQueueResponse(
          playQueueID: 77,
          playQueueSelectedItemID: 1002,
          playQueueShuffled: false,
          playQueueTotalCount: 3,
          playQueueVersion: 1,
          items: [current],
        ),
        'collection-movies',
      );
      var windowFetches = 0;
      String? requestedCenter;
      playback.setPlayQueueWindowFetcher((playQueueId, {center, window = 50}) async {
        windowFetches++;
        requestedCenter = center;
        expect(playQueueId, 77);
        return PlayQueueResponse(
          playQueueID: playQueueId,
          playQueueSelectedItemID: 1002,
          playQueueShuffled: false,
          playQueueTotalCount: 3,
          playQueueVersion: 2,
          items: [previous, current, next],
        );
      });
      final manager = _StubManager(null);
      final serverProvider = MultiServerProvider(manager, DataAggregationService(manager));
      addTearDown(serverProvider.dispose);

      AdjacentEpisodes? result;
      await tester.pumpWidget(
        MultiProvider(
          providers: [
            ChangeNotifierProvider<PlaybackStateProvider>.value(value: playback),
            ChangeNotifierProvider<MultiServerProvider>.value(value: serverProvider),
          ],
          child: _ProbeWidget(metadata: current, onResult: (r) => result = r),
        ),
      );
      await tester.pump();
      await tester.pump();

      expect(windowFetches, 1);
      expect(requestedCenter, '1002');
      expect(playback.isQueueActive, isTrue);
      expect(playback.playQueueId, 77);
      expect(playback.currentQueueItem, same(current));
      expect(playback.currentPlayQueueItemID, 1002);
      expect(playback.loadedItems, [previous, current, next]);
      expect(result, isNotNull);
      expect(result!.nextStatus, QueueNavigationStatus.found);
      expect(result!.previousStatus, QueueNavigationStatus.found);
      expect(result!.next, same(next));
      expect(result!.previous, same(previous));
      expect(result!.hasNext, isTrue);
      expect(result!.hasPrevious, isTrue);
    });

    testWidgets('does not use an unrelated active queue for a standalone movie', (tester) async {
      final queuedPrevious = _jfMovie('queued-1');
      final queuedCurrent = _jfMovie('queued-2');
      final unrelated = _jfMovie('unrelated');
      final playback = PlaybackStateProvider();
      addTearDown(playback.dispose);
      playback.setPlaybackFromLocalQueue(
        LocalPlayQueue(
          id: 'jellyfin:stale-playlist',
          items: [queuedPrevious, queuedCurrent],
          currentIndex: 1,
          backendId: MediaBackend.jellyfin.id,
        ),
        contextKey: 'stale-playlist',
      );
      final client = _RecordingClient(seriesEpisodes: const []);
      final manager = _StubManager(client);
      final serverProvider = MultiServerProvider(manager, DataAggregationService(manager));
      addTearDown(serverProvider.dispose);

      AdjacentEpisodes? result;
      await tester.pumpWidget(
        MultiProvider(
          providers: [
            ChangeNotifierProvider<PlaybackStateProvider>.value(value: playback),
            ChangeNotifierProvider<MultiServerProvider>.value(value: serverProvider),
          ],
          child: _ProbeWidget(metadata: unrelated, onResult: (r) => result = r),
        ),
      );
      await tester.pump();
      await tester.pump();

      expect(client.seriesQueueCalls, isEmpty);
      expect(playback.isQueueActive, isTrue);
      expect(playback.shuffleContextKey, 'stale-playlist');
      expect(playback.currentQueueItem, same(queuedCurrent));
      expect(playback.currentPlayQueueItemID, 1);
      expect(playback.loadedItems, [queuedPrevious, queuedCurrent]);
      expect(result, isNotNull);
      expect(result!.nextStatus, QueueNavigationStatus.failed);
      expect(result!.previousStatus, QueueNavigationStatus.failed);
      expect(result!.hasNext, isFalse);
      expect(result!.hasPrevious, isFalse);
    });

    testWidgets('catches downstream exceptions and reports failed adjacency', (tester) async {
      // Required providers are absent, so context.read throws. The service
      // converts the exception into an explicit failed result.
      AdjacentEpisodes? result;
      await tester.pumpWidget(_ProbeWidget(metadata: _meta('42'), onResult: (r) => result = r));
      await tester.pump();
      await tester.pump();

      expect(result, isNotNull);
      expect(result!.hasNext, isFalse);
      expect(result!.hasPrevious, isFalse);
      expect(result!.nextStatus, QueueNavigationStatus.failed);
    });

    testWidgets('preserves an active playlist/collection queue against series rebuild', (tester) async {
      // Reproduces the bug where playing an episode from a Jellyfin playlist
      // had next/prev walking the show's episodes instead of the playlist —
      // [_ensureLocalEpisodeQueue] used to overwrite the launcher-set queue
      // unconditionally. The guard now bails out when contextKey is set to
      // anything other than the seriesId.
      final ep1 = _jfEpisode('ep1', seriesId: 'series-A');
      final ep2 = _jfEpisode('ep2', seriesId: 'series-B');
      final ep3 = _jfEpisode('ep3', seriesId: 'series-A');

      final playback = PlaybackStateProvider();
      addTearDown(playback.dispose);
      playback.setPlaybackFromLocalQueue(
        LocalPlayQueue(
          id: 'jellyfin:playlist-X',
          items: [ep1, ep2, ep3],
          currentIndex: 1,
          backendId: MediaBackend.jellyfin.id,
        ),
        contextKey: 'playlist-X',
      );

      // Stub client returns fake series episodes that *include* ep2 — without
      // the guard, the service would replace the playlist queue with this
      // list and prev/next would point at sibling-X / sibling-Y.
      final client = _RecordingClient(
        seriesEpisodes: [
          _jfEpisode('sibling-X', seriesId: 'series-B'),
          ep2,
          _jfEpisode('sibling-Y', seriesId: 'series-B'),
        ],
      );
      final manager = _StubManager(client);
      final serverProvider = testMultiServerProvider(manager);
      addTearDown(serverProvider.dispose);

      AdjacentEpisodes? result;
      await tester.pumpWidget(
        MultiProvider(
          providers: [
            ChangeNotifierProvider<PlaybackStateProvider>.value(value: playback),
            ChangeNotifierProvider<MultiServerProvider>.value(value: serverProvider),
          ],
          child: _ProbeWidget(metadata: ep2, onResult: (r) => result = r),
        ),
      );
      await tester.pump();
      await tester.pump();

      // Guard short-circuits before the wire fetch.
      expect(client.seriesQueueCalls, isEmpty);
      // Queue items unchanged — still the playlist's three episodes.
      expect(playback.loadedItems.map((e) => e.id), ['ep1', 'ep2', 'ep3']);
      // Prev/next walk the playlist, not the series.
      expect(result, isNotNull);
      expect(result!.next?.id, 'ep3');
      expect(result!.previous?.id, 'ep1');
    });

    testWidgets('builds a Plex local fallback queue with watched episodes', (tester) async {
      final ep1 = _plexEpisode('ep1', seriesId: 'series-P', viewCount: 1);
      final ep2 = _plexEpisode('ep2', seriesId: 'series-P', viewCount: 1);
      final ep3 = _plexEpisode('ep3', seriesId: 'series-P', viewCount: 1);
      final playback = PlaybackStateProvider();
      addTearDown(playback.dispose);
      final client = _RecordingClient(seriesEpisodes: [ep1, ep2, ep3], clientBackend: MediaBackend.plex);
      final manager = _StubManager(client);
      final serverProvider = testMultiServerProvider(manager);
      addTearDown(serverProvider.dispose);

      AdjacentEpisodes? result;
      await tester.pumpWidget(
        MultiProvider(
          providers: [
            ChangeNotifierProvider<PlaybackStateProvider>.value(value: playback),
            ChangeNotifierProvider<MultiServerProvider>.value(value: serverProvider),
          ],
          child: _ProbeWidget(metadata: ep2, onResult: (r) => result = r),
        ),
      );
      await tester.pump();
      await tester.pump();

      expect(client.seriesQueueCalls, ['series-P']);
      expect(playback.loadedItems.map((item) => item.id), ['ep1', 'ep2', 'ep3']);
      expect(result!.nextStatus, QueueNavigationStatus.found);
      expect(result!.next?.id, 'ep3');
      expect(result!.previous?.id, 'ep1');
    });

    testWidgets('distinguishes a fallback fetch failure from the end of a series', (tester) async {
      final current = _plexEpisode('ep2', seriesId: 'series-P');
      final playback = PlaybackStateProvider();
      addTearDown(playback.dispose);
      final client = _RecordingClient(
        seriesEpisodes: const [],
        clientBackend: MediaBackend.plex,
        fetchError: StateError('network unavailable'),
      );
      final manager = _StubManager(client);
      final serverProvider = testMultiServerProvider(manager);
      addTearDown(serverProvider.dispose);

      AdjacentEpisodes? result;
      await tester.pumpWidget(
        MultiProvider(
          providers: [
            ChangeNotifierProvider<PlaybackStateProvider>.value(value: playback),
            ChangeNotifierProvider<MultiServerProvider>.value(value: serverProvider),
          ],
          child: _ProbeWidget(metadata: current, onResult: (r) => result = r),
        ),
      );
      await tester.pump();
      await tester.pump();

      expect(result!.nextStatus, QueueNavigationStatus.failed);
      expect(result!.isEndConfirmed, isFalse);
      expect(playback.isQueueActive, isFalse);
    });

    testWidgets('confirms the end only after loading a queue containing the current episode', (tester) async {
      final ep1 = _plexEpisode('ep1', seriesId: 'series-P', viewCount: 1);
      final ep2 = _plexEpisode('ep2', seriesId: 'series-P', viewCount: 1);
      final playback = PlaybackStateProvider();
      addTearDown(playback.dispose);
      final client = _RecordingClient(seriesEpisodes: [ep1, ep2], clientBackend: MediaBackend.plex);
      final manager = _StubManager(client);
      final serverProvider = testMultiServerProvider(manager);
      addTearDown(serverProvider.dispose);

      AdjacentEpisodes? result;
      await tester.pumpWidget(
        MultiProvider(
          providers: [
            ChangeNotifierProvider<PlaybackStateProvider>.value(value: playback),
            ChangeNotifierProvider<MultiServerProvider>.value(value: serverProvider),
          ],
          child: _ProbeWidget(metadata: ep2, onResult: (r) => result = r),
        ),
      );
      await tester.pump();
      await tester.pump();

      expect(result!.nextStatus, QueueNavigationStatus.boundary);
      expect(result!.isEndConfirmed, isTrue);
      expect(result!.next, isNull);
    });
  });

  // ===========================================================
  // loadAdjacentEpisodes: shuffled same-series queue (#1466)
  // ===========================================================

  group('loadAdjacentEpisodes with a shuffled same-series queue', () {
    // Mirrors JellyfinSequentialLauncher.launchShuffledShow: the full series
    // episode list, locally shuffled, published with contextKey == seriesId.
    // The regression under test: _ensureLocalEpisodeQueue used to rebuild a
    // sequential series queue whenever contextKey == seriesId, so shuffle
    // held for exactly one episode (#1466).
    final ep1 = _jfEpisode('ep1', seriesId: 'series-A');
    final ep2 = _jfEpisode('ep2', seriesId: 'series-A');
    final ep3 = _jfEpisode('ep3', seriesId: 'series-A');
    final ep4 = _jfEpisode('ep4', seriesId: 'series-A');
    final ep5 = _jfEpisode('ep5', seriesId: 'series-A');
    final shuffledOrder = [ep3, ep1, ep5, ep2, ep4];
    const shuffledIds = ['ep3', 'ep1', 'ep5', 'ep2', 'ep4'];

    // Stub server answers with the sequential list, so an (unwanted) queue
    // rebuild is observable both via seriesQueueCalls and via reordered
    // loadedItems.
    (PlaybackStateProvider, _RecordingClient, MultiServerProvider) buildShuffledSession() {
      final playback = PlaybackStateProvider();
      addTearDown(playback.dispose);
      playback.setPlaybackFromLocalQueue(
        LocalPlayQueue(
          id: 'jellyfin:series-A',
          items: shuffledOrder,
          currentIndex: 0,
          shuffled: true,
          backendId: MediaBackend.jellyfin.id,
        ),
        contextKey: 'series-A',
      );
      final client = _RecordingClient(seriesEpisodes: [ep1, ep2, ep3, ep4, ep5]);
      final manager = _StubManager(client);
      final serverProvider = testMultiServerProvider(manager);
      addTearDown(serverProvider.dispose);
      return (playback, client, serverProvider);
    }

    Future<AdjacentEpisodes?> probe(
      WidgetTester tester,
      PlaybackStateProvider playback,
      MultiServerProvider serverProvider,
      MediaItem metadata,
    ) async {
      AdjacentEpisodes? result;
      await tester.pumpWidget(
        MultiProvider(
          providers: [
            ChangeNotifierProvider<PlaybackStateProvider>.value(value: playback),
            ChangeNotifierProvider<MultiServerProvider>.value(value: serverProvider),
          ],
          child: _ProbeWidget(metadata: metadata, onResult: (r) => result = r),
        ),
      );
      await tester.pump();
      await tester.pump();
      return result;
    }

    testWidgets('preserves the shuffled order instead of rebuilding sequentially', (tester) async {
      final (playback, client, serverProvider) = buildShuffledSession();

      final result = await probe(tester, playback, serverProvider, ep3);

      expect(client.seriesQueueCalls, isEmpty);
      expect(playback.loadedItems.map((e) => e.id), shuffledIds);
      expect(playback.isShuffleActive, isTrue);
      expect(playback.shuffleContextKey, 'series-A');
      expect(result!.next?.id, 'ep1');
      expect(result.previous, isNull);
    });

    testWidgets('continues the shuffled order after advancing to the next episode', (tester) async {
      final (playback, client, serverProvider) = buildShuffledSession();
      // What _reloadMediaInPlace does when the player swaps to the next item.
      playback.setCurrentItem(ep1);

      final result = await probe(tester, playback, serverProvider, ep1);

      expect(client.seriesQueueCalls, isEmpty);
      expect(playback.isShuffleActive, isTrue);
      expect(result!.next?.id, 'ep5');
      expect(result.previous?.id, 'ep3');
    });

    testWidgets('same-episode clone from a source switch does not clobber the queue', (tester) async {
      final (playback, client, serverProvider) = buildShuffledSession();

      // _switchPlaybackSource reloads with a copyWith clone of the playing
      // item; MediaItem compares by identity, so queue membership misses and
      // only the cursor-globalKey gate keeps the queue alive.
      final result = await probe(tester, playback, serverProvider, ep3.copyWith(viewOffsetMs: 42));

      expect(client.seriesQueueCalls, isEmpty);
      expect(playback.loadedItems.map((e) => e.id), shuffledIds);
      expect(playback.isShuffleActive, isTrue);
      expect(result!.next?.id, 'ep1');
    });

    testWidgets('still builds a sequential series queue when no queue is active', (tester) async {
      // Direct episode tap with no launcher: the preserve gates must not get
      // in the way of the normal series-queue build.
      final playback = PlaybackStateProvider();
      addTearDown(playback.dispose);
      final client = _RecordingClient(seriesEpisodes: [ep1, ep2, ep3, ep4, ep5]);
      final manager = _StubManager(client);
      final serverProvider = testMultiServerProvider(manager);
      addTearDown(serverProvider.dispose);

      final result = await probe(tester, playback, serverProvider, ep3);

      expect(client.seriesQueueCalls, ['series-A']);
      expect(playback.loadedItems.map((e) => e.id), ['ep1', 'ep2', 'ep3', 'ep4', 'ep5']);
      expect(playback.isShuffleActive, isFalse);
      expect(result!.next?.id, 'ep4');
      expect(result.previous?.id, 'ep2');
    });
  });
}
