import 'dart:async';
import 'dart:ui' as ui;

import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:intl/date_symbol_data_local.dart';
import 'package:material_symbols_icons/symbols.dart';
import 'package:provider/provider.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:plezy/focus/key_event_utils.dart';
import 'package:plezy/i18n/strings.g.dart';
import 'package:plezy/media/media_source_info.dart';
import 'package:plezy/media/media_version.dart';
import 'package:plezy/models/shader_preset.dart';
import 'package:plezy/mpv/mpv.dart';
import 'package:plezy/services/playback_subtitle_resolver.dart';
import 'package:plezy/providers/playback_state_provider.dart';
import 'package:plezy/services/settings_service.dart';
import 'package:plezy/services/video_volume_controller.dart';
import 'package:plezy/widgets/video_controls/widgets/player_toast_indicator.dart';
import 'package:plezy/widgets/video_controls/desktop_video_controls.dart';
import 'package:plezy/widgets/video_controls/mobile_video_controls.dart';
import 'package:plezy/watch_together/providers/watch_together_provider.dart';
import 'package:plezy/widgets/video_controls/video_controls.dart';
import 'package:plezy/widgets/video_controls/models/track_controls_state.dart';
import 'package:plezy/widgets/video_controls/player_chrome_controller.dart';
import 'package:plezy/widgets/video_controls/painters/buffer_range_painter.dart';
import 'package:plezy/widgets/video_controls/widgets/mobile_skip_zones.dart';
import 'package:plezy/widgets/video_controls/widgets/skip_marker_button.dart';
import 'package:plezy/widgets/video_controls/widgets/sync_offset_control.dart';
import 'package:plezy/widgets/video_controls/widgets/timeline_slider.dart';
import 'package:plezy/widgets/video_controls/video_control_button.dart';
import 'package:plezy/widgets/video_controls/widgets/video_timeline_bar.dart';

import '../test_helpers/watch_together_fakes.dart';
import '../test_helpers/media_items.dart';
import '../test_helpers/prefs.dart';
import '../test_helpers/theme.dart';

void main() {
  TestWidgetsFlutterBinding.ensureInitialized();

  group('resolveShaderTogglePreset', () {
    test('turns shaders off when a shader is currently active', () {
      final result = resolveShaderTogglePreset(
        currentPreset: ShaderPreset.nvscalerDefault,
        savedPreset: ShaderPreset.nvscalerDefault,
        allPresets: ShaderPreset.allPresets,
      );

      expect(result, ShaderPreset.none);
    });

    test('restores the saved preset when shaders are currently off', () {
      final saved = ShaderPreset.artcnnPreset(ArtCNNModel.c4f16, ArtCNNVariant.neutral);
      final result = resolveShaderTogglePreset(
        currentPreset: ShaderPreset.none,
        savedPreset: saved,
        allPresets: ShaderPreset.allPresets,
      );

      expect(result, saved);
    });

    test('falls back to the first enabled preset when no shader is saved', () {
      final result = resolveShaderTogglePreset(
        currentPreset: ShaderPreset.none,
        savedPreset: ShaderPreset.none,
        allPresets: const [ShaderPreset.none, ShaderPreset.nvscalerDefault],
      );

      expect(result, ShaderPreset.nvscalerDefault);
    });
  });

  group('effectiveVersionQualityControls', () {
    test('clears switchable version and quality state during offline playback', () {
      final version = MediaVersion(id: 'v1', videoResolution: '1080');
      final audio = MediaAudioTrack(id: 1, languageCode: 'eng', selected: false);
      final subtitle = MediaSubtitleTrack(id: 2, languageCode: 'eng', selected: false, forced: false);

      final result = effectiveVersionQualityControls(
        isOfflinePlayback: true,
        availableVersions: [version],
        serverSupportsTranscoding: true,
        isTranscoding: true,
        sourceAudioTracks: [audio],
        selectedAudioStreamId: 1,
        sourceSubtitleTracks: [subtitle],
        selectedSubtitleChoice: const PlaybackSourceSubtitleChoice.source(2),
      );

      expect(result.canSwitch, isFalse);
      expect(result.availableVersions, isEmpty);
      expect(result.serverSupportsTranscoding, isFalse);
      expect(result.isTranscoding, isFalse);
      expect(result.sourceAudioTracks, isEmpty);
      expect(result.selectedAudioStreamId, isNull);
      expect(result.sourceSubtitleTracks, isEmpty);
      expect(result.selectedSubtitleChoice, isNull);
    });

    test('keeps switchable state during online playback', () {
      final version = MediaVersion(id: 'v1', videoResolution: '1080');
      final audio = MediaAudioTrack(id: 1, languageCode: 'eng', selected: false);
      final subtitle = MediaSubtitleTrack(id: 2, languageCode: 'eng', selected: false, forced: false);

      final result = effectiveVersionQualityControls(
        isOfflinePlayback: false,
        availableVersions: [version],
        serverSupportsTranscoding: true,
        isTranscoding: true,
        sourceAudioTracks: [audio],
        selectedAudioStreamId: 1,
        sourceSubtitleTracks: [subtitle],
        selectedSubtitleChoice: const PlaybackSourceSubtitleChoice.source(2),
      );

      expect(result.canSwitch, isTrue);
      expect(result.availableVersions, [version]);
      expect(result.serverSupportsTranscoding, isTrue);
      expect(result.isTranscoding, isTrue);
      expect(result.sourceAudioTracks, [audio]);
      expect(result.selectedAudioStreamId, 1);
      expect(result.sourceSubtitleTracks, [subtitle]);
      expect(result.selectedSubtitleChoice, const PlaybackSourceSubtitleChoice.source(2));
    });
  });

  group('selectableSourceSubtitleTracks', () {
    MediaSubtitleTrack sub(int id, {String? codec, String? key}) =>
        MediaSubtitleTrack(id: id, codec: codec, key: key, languageCode: 'eng', selected: false, forced: false);

    test('returns the full list unchanged when not transcoding', () {
      final tracks = [sub(1, codec: 'srt'), sub(2, codec: 'pgs'), sub(3, codec: 'weird')];
      expect(
        selectableSourceSubtitleTracks(
          tracks,
          isTranscoding: false,
          sidecarSourceIds: const {},
          supportsEmbeddedTranscodeSelection: false,
        ),
        tracks,
      );
    });

    test('keeps text, image and keyed tracks while transcoding', () {
      final text = sub(1, codec: 'srt');
      final image = sub(2, codec: 'pgs');
      final keyed = sub(3, codec: 'weird', key: '/library/streams/3');
      final result = selectableSourceSubtitleTracks(
        [text, image, keyed],
        isTranscoding: true,
        sidecarSourceIds: {keyed.id},
        supportsEmbeddedTranscodeSelection: true,
      );
      expect(result, [text, image, keyed]);
    });

    test('drops unsupported embedded codecs while transcoding', () {
      final text = sub(1, codec: 'ass');
      final unsupported = sub(2, codec: 'weird');
      final result = selectableSourceSubtitleTracks(
        [text, unsupported],
        isTranscoding: true,
        sidecarSourceIds: const {},
        supportsEmbeddedTranscodeSelection: true,
      );
      expect(result, [text]);
    });

    test('only offers resolved sidecars when embedded transcode selection is unsupported', () {
      final external = sub(1, codec: 'srt', key: '/Videos/item/source/Subtitles/1/Stream.srt');
      final embedded = sub(2, codec: 'srt');
      final unavailableExternal = sub(3, codec: 'srt', key: '/missing');

      final result = selectableSourceSubtitleTracks(
        [external, embedded, unavailableExternal],
        isTranscoding: true,
        sidecarSourceIds: {external.id},
        supportsEmbeddedTranscodeSelection: false,
      );

      expect(result, [external]);
    });

    test('only offers resolved file sidecars during direct play', () {
      final embedded = sub(1, codec: 'srt');
      final availableExternal = sub(2, codec: 'srt', key: '/available');
      final unavailableExternal = sub(3, codec: 'srt', key: '/missing');

      final result = selectableSourceSubtitleTracks(
        [embedded, availableExternal, unavailableExternal],
        isTranscoding: false,
        sidecarSourceIds: {availableExternal.id},
        supportsEmbeddedTranscodeSelection: false,
      );

      expect(result, [embedded, availableExternal]);
    });

    test('keeps Jellyfin external-delivery rows that remain embedded during direct play', () {
      final deliveryExternalEmbedded = MediaSubtitleTrack(
        id: 1,
        codec: 'srt',
        key: '/Videos/item/source/Subtitles/1/Stream.srt',
        usesExternalDelivery: true,
        selected: false,
        forced: false,
      );

      final result = selectableSourceSubtitleTracks(
        [deliveryExternalEmbedded],
        isTranscoding: false,
        sidecarSourceIds: const {},
        supportsEmbeddedTranscodeSelection: false,
      );

      expect(result, [deliveryExternalEmbedded]);
    });
  });

  test('findNewExternalSubtitleTrack ignores embedded and existing source rows', () {
    final embedded = MediaSubtitleTrack(id: 1, selected: false, forced: false);
    final existing = MediaSubtitleTrack(id: 2, external: true, selected: false, forced: false);
    final downloaded = MediaSubtitleTrack(id: 3, external: true, selected: false, forced: false);

    expect(findNewExternalSubtitleTrack([embedded, existing, downloaded], {1, 2}), downloaded);
  });

  test('subtitle download treats an already-selected source as applied', () {
    expect(
      subtitleDownloadApplyOutcomeFor(PlaybackSourceChangeOutcome.unchanged),
      SubtitleDownloadApplyOutcome.applied,
    );
  });

  group('shouldShowSkipMarkerButton', () {
    test('does not show before the first frame is rendered', () {
      expect(
        shouldShowSkipMarkerButton(
          hasFirstFrame: false,
          hasMarker: true,
          hasPlayNextPrompt: false,
          skipButtonDismissed: false,
          controlsVisible: true,
        ),
        isFalse,
      );
    });

    test('shows after first frame when marker is active and not dismissed', () {
      expect(
        shouldShowSkipMarkerButton(
          hasFirstFrame: true,
          hasMarker: true,
          hasPlayNextPrompt: false,
          skipButtonDismissed: false,
          controlsVisible: false,
        ),
        isTrue,
      );
    });

    test('does not show when dismissed until controls are visible again', () {
      expect(
        shouldShowSkipMarkerButton(
          hasFirstFrame: true,
          hasMarker: true,
          hasPlayNextPrompt: false,
          skipButtonDismissed: true,
          controlsVisible: false,
        ),
        isFalse,
      );
      expect(
        shouldShowSkipMarkerButton(
          hasFirstFrame: true,
          hasMarker: true,
          hasPlayNextPrompt: false,
          skipButtonDismissed: true,
          controlsVisible: true,
        ),
        isTrue,
      );
    });

    test('does not show while play next prompt is active', () {
      expect(
        shouldShowSkipMarkerButton(
          hasFirstFrame: true,
          hasMarker: true,
          hasPlayNextPrompt: true,
          skipButtonDismissed: false,
          controlsVisible: true,
        ),
        isFalse,
      );
    });
  });

  group('classifyPlayerNavigationKey', () {
    test('reserves only physical keyboard Escape for fullscreen', () {
      expect(
        classifyPlayerNavigationKey(
          _navigationKeyDown(LogicalKeyboardKey.escape, ui.KeyEventDeviceType.keyboard),
          isAppleTV: false,
        ),
        PlayerNavigationKey.physicalEscape,
      );
      expect(
        classifyPlayerNavigationKey(
          _navigationKeyDown(LogicalKeyboardKey.escape, ui.KeyEventDeviceType.gamepad),
          isAppleTV: false,
        ),
        PlayerNavigationKey.back,
      );
      expect(
        classifyPlayerNavigationKey(
          _navigationKeyDown(LogicalKeyboardKey.escape, ui.KeyEventDeviceType.directionalPad),
          isAppleTV: false,
        ),
        PlayerNavigationKey.back,
      );
    });

    test('treats tvOS keyboard Escape as semantic Back', () {
      expect(
        classifyPlayerNavigationKey(
          _navigationKeyDown(LogicalKeyboardKey.escape, ui.KeyEventDeviceType.keyboard),
          isAppleTV: true,
        ),
        PlayerNavigationKey.back,
      );
    });

    test('recognizes controller and browser Back keys', () {
      for (final key in [LogicalKeyboardKey.gameButtonB, LogicalKeyboardKey.goBack, LogicalKeyboardKey.browserBack]) {
        expect(
          classifyPlayerNavigationKey(_navigationKeyDown(key, ui.KeyEventDeviceType.gamepad), isAppleTV: false),
          PlayerNavigationKey.back,
        );
      }
    });

    test('recognizes only bare physical Backspace as player Back', () {
      final event = _navigationKeyDown(LogicalKeyboardKey.backspace, ui.KeyEventDeviceType.keyboard);

      expect(classifyPlayerNavigationKey(event, isAppleTV: false, hasModifiers: false), PlayerNavigationKey.back);
      expect(classifyPlayerNavigationKey(event, isAppleTV: false, hasModifiers: true), PlayerNavigationKey.none);
    });

    test('recognizes bare keyboard and browser Home', () {
      for (final key in [LogicalKeyboardKey.home, LogicalKeyboardKey.browserHome]) {
        expect(
          classifyPlayerNavigationKey(
            _navigationKeyDown(key, ui.KeyEventDeviceType.keyboard),
            isAppleTV: false,
            hasModifiers: false,
          ),
          PlayerNavigationKey.home,
        );
      }
    });
  });

  group('handlePlayerNavigationKeyAction', () {
    testWidgets('semantic Back activates once on key up', (tester) async {
      var actions = 0;

      final downResult = handlePlayerNavigationKeyAction(
        _keyDown(LogicalKeyboardKey.gameButtonB),
        PlayerNavigationKey.back,
        () => actions++,
      );
      final upResult = handlePlayerNavigationKeyAction(
        _keyUp(LogicalKeyboardKey.gameButtonB),
        PlayerNavigationKey.back,
        () => actions++,
      );

      expect(downResult, KeyEventResult.handled);
      expect(upResult, KeyEventResult.handled);
      expect(actions, 1);
      await tester.pump();
    });

    testWidgets('Backspace alias activates once on key up', (tester) async {
      var actions = 0;

      handlePlayerNavigationKeyAction(
        _keyDown(LogicalKeyboardKey.backspace),
        PlayerNavigationKey.back,
        () => actions++,
      );
      expect(BackKeyCoordinator.consumeIfHandled(), isTrue, reason: 'parallel route pop is suppressed on key down');
      handlePlayerNavigationKeyAction(_keyUp(LogicalKeyboardKey.backspace), PlayerNavigationKey.back, () => actions++);

      expect(actions, 1);
      await tester.pump();
    });
  });

  group('primePlayerNavigationFocusForEvent', () {
    testWidgets('claims loading-route focus on navigation key down', (tester) async {
      final playerFocus = FocusNode();
      final otherFocus = FocusNode();
      addTearDown(playerFocus.dispose);
      addTearDown(otherFocus.dispose);

      await tester.pumpWidget(
        MaterialApp(
          home: Column(
            children: [
              Focus(focusNode: playerFocus, child: const SizedBox()),
              Focus(focusNode: otherFocus, child: const SizedBox()),
            ],
          ),
        ),
      );
      otherFocus.requestFocus();
      await tester.pump();

      final primed = primePlayerNavigationFocusForEvent(
        _keyDown(LogicalKeyboardKey.gameButtonB),
        focusNode: playerFocus,
        playerReady: false,
        isCurrentRoute: true,
        isAppleTV: false,
      );
      await tester.pump();

      expect(primed, isTrue);
      expect(playerFocus.hasPrimaryFocus, isTrue);
    });

    testWidgets('does not steal focus after the player is ready', (tester) async {
      final playerFocus = FocusNode();
      final otherFocus = FocusNode();
      addTearDown(playerFocus.dispose);
      addTearDown(otherFocus.dispose);

      await tester.pumpWidget(
        MaterialApp(
          home: Column(
            children: [
              Focus(focusNode: playerFocus, child: const SizedBox()),
              Focus(focusNode: otherFocus, child: const SizedBox()),
            ],
          ),
        ),
      );
      otherFocus.requestFocus();
      await tester.pump();

      final primed = primePlayerNavigationFocusForEvent(
        _keyDown(LogicalKeyboardKey.gameButtonB),
        focusNode: playerFocus,
        playerReady: true,
        isCurrentRoute: true,
        isAppleTV: false,
      );
      await tester.pump();

      expect(primed, isFalse);
      expect(otherFocus.hasPrimaryFocus, isTrue);
    });

    testWidgets('does not steal focus from a route above the player', (tester) async {
      final playerFocus = FocusNode();
      final overlayFocus = FocusNode();
      addTearDown(playerFocus.dispose);
      addTearDown(overlayFocus.dispose);

      await tester.pumpWidget(
        MaterialApp(
          home: Column(
            children: [
              Focus(focusNode: playerFocus, child: const SizedBox()),
              Focus(focusNode: overlayFocus, child: const SizedBox()),
            ],
          ),
        ),
      );
      overlayFocus.requestFocus();
      await tester.pump();

      final primed = primePlayerNavigationFocusForEvent(
        _keyDown(LogicalKeyboardKey.gameButtonB),
        focusNode: playerFocus,
        playerReady: false,
        isCurrentRoute: false,
        isAppleTV: false,
      );
      await tester.pump();

      expect(primed, isFalse);
      expect(overlayFocus.hasPrimaryFocus, isTrue);
    });
  });

  group('PlayerNavigationCoordinator focus dispatch', () {
    PlayerNavigationCoordinator coordinatorFor(
      PlayerChromeController chromeController, {
      bool Function()? isPromptOpen,
      VoidCallback? dismissPrompt,
      bool Function()? isChromePresented,
      Future<bool> Function()? exitFullscreenIfActive,
      bool physicalEscapeExitsFullscreen = true,
      bool Function()? physicalEscapeExitsFullscreenProvider,
      VoidCallback? exitPlayer,
      VoidCallback? navigateHome,
      bool Function()? isActive,
    }) {
      return PlayerNavigationCoordinator(
        chromeController: chromeController,
        isPromptOpen: isPromptOpen ?? () => false,
        dismissPrompt: dismissPrompt ?? () {},
        isChromePresented: isChromePresented ?? () => chromeController.controlsPresented,
        exitFullscreenIfActive: exitFullscreenIfActive ?? () async => false,
        physicalEscapeExitsFullscreen: physicalEscapeExitsFullscreenProvider ?? () => physicalEscapeExitsFullscreen,
        exitPlayer: exitPlayer ?? () {},
        navigateHome: navigateHome ?? () {},
        isActive: isActive,
      );
    }

    Future<void> pumpNavigationFocus(WidgetTester tester, PlayerNavigationCoordinator coordinator) async {
      await tester.pumpWidget(
        MaterialApp(
          home: Focus(
            autofocus: true,
            onKeyEvent: (_, event) {
              final navigationKey = classifyPlayerNavigationKey(event, isAppleTV: false);
              return handlePlayerNavigationKeyAction(event, navigationKey, () => coordinator.handle(navigationKey));
            },
            child: const SizedBox.expand(),
          ),
        ),
      );
      await tester.pump();
    }

    testWidgets('one Back hides presented chrome and the next exits once after fade-out', (tester) async {
      final chromeController = PlayerChromeController();
      addTearDown(chromeController.dispose);
      var exits = 0;
      final coordinator = coordinatorFor(chromeController, exitPlayer: () => exits++);
      await pumpNavigationFocus(tester, coordinator);

      await tester.sendKeyEvent(LogicalKeyboardKey.gameButtonB);

      expect(chromeController.controlsVisible, isFalse);
      expect(chromeController.controlsPresented, isTrue);
      expect(exits, 0);

      chromeController.markControlsHidden();
      await tester.sendKeyEvent(LogicalKeyboardKey.gameButtonB);

      expect(exits, 1);
    });

    testWidgets('Back exits during pre-first-frame loading even when controls default visible', (tester) async {
      final chromeController = PlayerChromeController();
      addTearDown(chromeController.dispose);
      var exits = 0;
      final coordinator = coordinatorFor(chromeController, isChromePresented: () => false, exitPlayer: () => exits++);
      await pumpNavigationFocus(tester, coordinator);

      await tester.sendKeyEvent(LogicalKeyboardKey.gameButtonB);

      expect(chromeController.controlsVisible, isTrue);
      expect(exits, 1);
    });

    testWidgets('physical Escape outside fullscreen hides presented chrome without exiting', (tester) async {
      final chromeController = PlayerChromeController();
      addTearDown(chromeController.dispose);
      var fullscreenChecks = 0;
      var exits = 0;
      final coordinator = coordinatorFor(
        chromeController,
        exitFullscreenIfActive: () async {
          fullscreenChecks++;
          return false;
        },
        exitPlayer: () => exits++,
      );
      await pumpNavigationFocus(tester, coordinator);

      await tester.sendKeyEvent(LogicalKeyboardKey.escape);
      await tester.pump();

      expect(fullscreenChecks, 1);
      expect(chromeController.controlsVisible, isFalse);
      expect(exits, 0);
    });

    testWidgets('physical Escape preserves event-time chrome presentation across fullscreen check', (tester) async {
      final chromeController = PlayerChromeController();
      addTearDown(chromeController.dispose);
      final fullscreenResult = Completer<bool>();
      var exits = 0;
      final coordinator = coordinatorFor(
        chromeController,
        exitFullscreenIfActive: () => fullscreenResult.future,
        exitPlayer: () => exits++,
      );
      await pumpNavigationFocus(tester, coordinator);

      await tester.sendKeyEvent(LogicalKeyboardKey.escape);
      chromeController.hide();
      chromeController.markControlsHidden();
      fullscreenResult.complete(false);
      await tester.pump();

      expect(chromeController.controlsVisible, isFalse);
      expect(chromeController.controlsPresented, isFalse);
      expect(exits, 0);
    });

    testWidgets('physical Escape exits native fullscreen before chrome', (tester) async {
      final chromeController = PlayerChromeController();
      addTearDown(chromeController.dispose);
      var exits = 0;
      final coordinator = coordinatorFor(
        chromeController,
        exitFullscreenIfActive: () async => true,
        exitPlayer: () => exits++,
      );
      await pumpNavigationFocus(tester, coordinator);

      await tester.sendKeyEvent(LogicalKeyboardKey.escape);
      await tester.pump();

      expect(chromeController.controlsVisible, isTrue);
      expect(exits, 0);
    });

    testWidgets('enabling player navigation makes physical Escape preserve fullscreen', (tester) async {
      final chromeController = PlayerChromeController();
      addTearDown(chromeController.dispose);
      var physicalEscapeExitsFullscreen = true;
      var fullscreenChecks = 0;
      var exits = 0;
      final coordinator = coordinatorFor(
        chromeController,
        exitFullscreenIfActive: () async {
          fullscreenChecks++;
          return true;
        },
        physicalEscapeExitsFullscreenProvider: () => physicalEscapeExitsFullscreen,
        exitPlayer: () => exits++,
      );
      physicalEscapeExitsFullscreen = false;
      await pumpNavigationFocus(tester, coordinator);

      await tester.sendKeyEvent(LogicalKeyboardKey.escape);

      expect(fullscreenChecks, 0);
      expect(chromeController.controlsVisible, isFalse);
      expect(exits, 0);
    });

    testWidgets('macOS physical Escape stages through chrome and player without leaving fullscreen', (tester) async {
      final chromeController = PlayerChromeController();
      addTearDown(chromeController.dispose);
      var fullscreenChecks = 0;
      var exits = 0;
      final coordinator = coordinatorFor(
        chromeController,
        exitFullscreenIfActive: () async {
          fullscreenChecks++;
          return true;
        },
        physicalEscapeExitsFullscreen: false,
        exitPlayer: () => exits++,
      );
      await pumpNavigationFocus(tester, coordinator);

      await tester.sendKeyEvent(LogicalKeyboardKey.escape);

      expect(fullscreenChecks, 0);
      expect(chromeController.controlsVisible, isFalse);
      expect(exits, 0);

      chromeController.markControlsHidden();
      await tester.sendKeyEvent(LogicalKeyboardKey.escape);

      expect(fullscreenChecks, 0);
      expect(exits, 1);
    });

    testWidgets('physical Escape does nothing after its player route is disposed', (tester) async {
      final chromeController = PlayerChromeController();
      addTearDown(chromeController.dispose);
      final fullscreenResult = Completer<bool>();
      var active = true;
      var exits = 0;
      final coordinator = coordinatorFor(
        chromeController,
        exitFullscreenIfActive: () => fullscreenResult.future,
        exitPlayer: () => exits++,
        isActive: () => active,
      );
      await pumpNavigationFocus(tester, coordinator);

      await tester.sendKeyEvent(LogicalKeyboardKey.escape);
      active = false;
      fullscreenResult.complete(false);
      await tester.pump();

      expect(chromeController.controlsVisible, isTrue);
      expect(exits, 0);
    });

    testWidgets('Back closes the content strip without hiding chrome or exiting', (tester) async {
      final chromeController = PlayerChromeController()..setContentStripVisible(true);
      addTearDown(chromeController.dispose);
      var exits = 0;
      final coordinator = coordinatorFor(chromeController, exitPlayer: () => exits++);
      await pumpNavigationFocus(tester, coordinator);

      await tester.sendKeyEvent(LogicalKeyboardKey.gameButtonB);

      expect(chromeController.contentStripVisible, isFalse);
      expect(chromeController.controlsVisible, isTrue);
      expect(exits, 0);
      chromeController.cancelAutoHide();
    });

    testWidgets('Home bypasses staged Back layers', (tester) async {
      final chromeController = PlayerChromeController()..setContentStripVisible(true);
      addTearDown(chromeController.dispose);
      var promptOpen = true;
      var promptDismissals = 0;
      var homeNavigations = 0;
      final coordinator = coordinatorFor(
        chromeController,
        isPromptOpen: () => promptOpen,
        dismissPrompt: () {
          promptOpen = false;
          promptDismissals++;
        },
        navigateHome: () => homeNavigations++,
      );
      await pumpNavigationFocus(tester, coordinator);

      await tester.sendKeyEvent(LogicalKeyboardKey.home);

      expect(homeNavigations, 1);
      expect(promptDismissals, 0);
      expect(chromeController.contentStripVisible, isTrue);
      expect(chromeController.controlsVisible, isTrue);
    });

    testWidgets('global observation and focus dispatch produce one native Back action', (tester) async {
      final chromeController = PlayerChromeController();
      addTearDown(chromeController.dispose);
      var globalEvents = 0;
      var exits = 0;
      bool globalHandler(KeyEvent event) {
        if (classifyPlayerNavigationKey(event, isAppleTV: false) != PlayerNavigationKey.none) {
          globalEvents++;
        }
        return false;
      }

      HardwareKeyboard.instance.addHandler(globalHandler);
      addTearDown(() => HardwareKeyboard.instance.removeHandler(globalHandler));
      await pumpNavigationFocus(tester, coordinatorFor(chromeController, exitPlayer: () => exits++));

      await tester.sendKeyEvent(LogicalKeyboardKey.backspace);

      expect(globalEvents, 2);
      expect(chromeController.controlsVisible, isFalse);
      expect(exits, 0);
    });
  });

  group('resolvePlayerBackDisposition', () {
    test('closes a content strip before other Back behavior', () {
      expect(
        resolvePlayerBackDisposition(
          navigationKey: PlayerNavigationKey.physicalEscape,
          contentStripVisible: true,
          controlsVisible: true,
        ),
        PlayerBackDisposition.closeContentStrip,
      );
    });

    test('checks fullscreen only for physical Escape', () {
      expect(
        resolvePlayerBackDisposition(
          navigationKey: PlayerNavigationKey.physicalEscape,
          contentStripVisible: false,
          controlsVisible: false,
        ),
        PlayerBackDisposition.exitFullscreenIfActive,
      );
      expect(
        resolvePlayerBackDisposition(
          navigationKey: PlayerNavigationKey.back,
          contentStripVisible: false,
          controlsVisible: false,
        ),
        PlayerBackDisposition.exitPlayer,
      );
    });
  });

  group('shouldPhysicalEscapeExitFullscreen', () {
    test('uses fullscreen-first behavior for normal Windows and Linux navigation', () {
      expect(shouldPhysicalEscapeExitFullscreen(isMacOS: false, videoPlayerNavigationEnabled: false), isTrue);
    });

    test('preserves fullscreen when HTPC-style player navigation is enabled', () {
      expect(shouldPhysicalEscapeExitFullscreen(isMacOS: false, videoPlayerNavigationEnabled: true), isFalse);
    });

    test('preserves native fullscreen inside the macOS player', () {
      expect(shouldPhysicalEscapeExitFullscreen(isMacOS: true, videoPlayerNavigationEnabled: false), isFalse);
    });
  });

  group('SkipMarkerButton', () {
    testWidgets('tap activates skip', (tester) async {
      final focusNode = FocusNode();
      addTearDown(focusNode.dispose);
      var activateCount = 0;

      await _pumpSkipMarkerButton(
        tester,
        focusNode: focusNode,
        isAutoSkipActive: true,
        onActivate: () => activateCount++,
      );

      expect(find.text('Skip Intro (3)'), findsOneWidget);

      await tester.tap(find.byType(InkWell));
      await tester.pump();

      expect(activateCount, 1);
    });

    testWidgets('select activates skip', (tester) async {
      final focusNode = FocusNode();
      addTearDown(focusNode.dispose);
      var activateCount = 0;

      await _pumpSkipMarkerButton(
        tester,
        focusNode: focusNode,
        isAutoSkipActive: true,
        onActivate: () => activateCount++,
      );

      focusNode.requestFocus();
      await tester.pump();
      await tester.sendKeyEvent(LogicalKeyboardKey.select);
      await tester.pump();

      expect(activateCount, 1);
    });

    testWidgets('d-pad down moves focus without activating', (tester) async {
      final focusNode = FocusNode();
      addTearDown(focusNode.dispose);
      var activateCount = 0;
      var focusDownCount = 0;

      await _pumpSkipMarkerButton(
        tester,
        focusNode: focusNode,
        isAutoSkipActive: true,
        onActivate: () => activateCount++,
        onFocusDown: () => focusDownCount++,
      );

      focusNode.requestFocus();
      await tester.pump();
      await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown);
      await tester.pump();

      expect(activateCount, 0);
      expect(focusDownCount, 1);
    });

    testWidgets('tap activates when auto-skip is inactive', (tester) async {
      final focusNode = FocusNode();
      addTearDown(focusNode.dispose);
      var activateCount = 0;

      await _pumpSkipMarkerButton(
        tester,
        focusNode: focusNode,
        isAutoSkipActive: false,
        onActivate: () => activateCount++,
      );

      expect(find.text('Skip Intro'), findsOneWidget);

      await tester.tap(find.byType(InkWell));
      await tester.pump();

      expect(activateCount, 1);
    });
  });

  group('mobileSkipZoneForTap', () {
    const size = Size(1000, 600);

    test('returns backward for left skip zone', () {
      expect(mobileSkipZoneForTap(position: const Offset(100, 300), size: size), isFalse);
    });

    test('returns forward for right skip zone', () {
      expect(mobileSkipZoneForTap(position: const Offset(900, 300), size: size), isTrue);
    });

    test('returns null outside skip zones', () {
      expect(mobileSkipZoneForTap(position: const Offset(500, 300), size: size), isNull);
      expect(mobileSkipZoneForTap(position: const Offset(100, 20), size: size), isNull);
      expect(mobileSkipZoneForTap(position: const Offset(900, 580), size: size), isNull);
    });
  });

  group('play/pause callback routing', () {
    testWidgets('desktop button delegates without issuing a player command', (tester) async {
      LocaleSettings.setLocaleSync(AppLocale.en);
      await initializeDateFormatting('en');
      tester.view.physicalSize = const Size(1200, 800);
      tester.view.devicePixelRatio = 1;
      addTearDown(tester.view.reset);
      resetSharedPreferencesForTest();
      SettingsService.resetForTesting();
      final settings = await SettingsService.getInstance();
      final player = FakeSyncPlayer();
      addTearDown(player.dispose);
      final volume = VideoVolumeController(player: player, settings: settings, initialVolume: 100);
      addTearDown(volume.dispose);
      var requests = 0;

      final watchTogether = WatchTogetherProvider();
      addTearDown(watchTogether.dispose);
      await tester.pumpWidget(
        ChangeNotifierProvider<WatchTogetherProvider>.value(
          value: watchTogether,
          child: MaterialApp(
            theme: ThemeData(extensions: const [testMonoTokens]),
            home: Scaffold(
              body: SizedBox(
                width: 1000,
                height: 700,
                child: DesktopVideoControls(
                  player: player,
                  volumeController: volume,
                  metadata: testMediaItem(id: 'desktop'),
                  onPlayPause: () => requests++,
                  chapters: const [],
                  chaptersLoaded: true,
                  seekTimeSmall: 10,
                  onSeekToPreviousChapter: () {},
                  onSeekToNextChapter: () {},
                  onSeek: (_) {},
                  onSeekEnd: (_) {},
                  getReplayIcon: (_) => Icons.replay,
                  getForwardIcon: (_) => Icons.forward_10,
                  trackControlsState: const TrackControlsState(canControl: true),
                ),
              ),
            ),
          ),
        ),
      );
      await tester.pump();
      await tester.tap(find.bySemanticsLabel(t.videoControls.playButton).last);
      await tester.pump();

      expect(requests, 1);
      expect(player.commandLog.where((entry) => entry == 'play' || entry == 'pause'), isEmpty);
    });

    testWidgets('mobile button delegates and preserves chrome timer behavior', (tester) async {
      LocaleSettings.setLocaleSync(AppLocale.en);
      await initializeDateFormatting('en');
      tester.view.physicalSize = const Size(800, 1000);
      tester.view.devicePixelRatio = 1;
      addTearDown(tester.view.reset);
      resetSharedPreferencesForTest();
      SettingsService.resetForTesting();
      await SettingsService.getInstance();
      final player = FakeSyncPlayer();
      addTearDown(player.dispose);
      var requests = 0;
      var startAutoHide = 0;
      var cancelAutoHide = 0;

      final watchTogether = WatchTogetherProvider();
      addTearDown(watchTogether.dispose);
      await tester.pumpWidget(
        ChangeNotifierProvider<WatchTogetherProvider>.value(
          value: watchTogether,
          child: MaterialApp(
            theme: ThemeData(extensions: const [testMonoTokens]),
            home: Scaffold(
              body: SizedBox(
                width: 500,
                height: 800,
                child: MobileVideoControls(
                  player: player,
                  metadata: testMediaItem(id: 'mobile'),
                  chapters: const [],
                  chaptersLoaded: true,
                  seekTimeSmall: 10,
                  trackChapterControls: const SizedBox.shrink(),
                  onSeek: (_) {},
                  onSeekEnd: (_) {},
                  onPlayPause: () => requests++,
                  onStartAutoHide: () => startAutoHide++,
                  onCancelAutoHide: () => cancelAutoHide++,
                ),
              ),
            ),
          ),
        ),
      );
      await tester.pump();
      await tester.tap(find.bySemanticsLabel(t.videoControls.playButton).last);
      await tester.pump();

      expect(requests, 1);
      expect(startAutoHide, 1);
      expect(cancelAutoHide, 0);
      expect(player.commandLog.where((entry) => entry == 'play' || entry == 'pause'), isEmpty);
    });
  });

  group('TimelineSlider', () {
    testWidgets('routes keyboard input through the custom focus handler', (tester) async {
      final focusNode = FocusNode();
      addTearDown(focusNode.dispose);
      var keyEvents = 0;
      var seekEvents = 0;

      await tester.pumpWidget(
        MaterialApp(
          home: Scaffold(
            body: SizedBox(
              width: 400,
              child: TimelineSlider(
                position: const Duration(minutes: 1),
                duration: const Duration(minutes: 10),
                chapters: const [],
                chaptersLoaded: true,
                focusNode: focusNode,
                onKeyEvent: (_, event) {
                  if (event is KeyDownEvent && event.logicalKey == LogicalKeyboardKey.arrowRight) {
                    keyEvents++;
                    return KeyEventResult.handled;
                  }
                  return KeyEventResult.ignored;
                },
                onSeek: (_) => seekEvents++,
                onSeekEnd: (_) {},
              ),
            ),
          ),
        ),
      );

      focusNode.requestFocus();
      await tester.pump();
      await tester.sendKeyEvent(LogicalKeyboardKey.arrowRight);
      await tester.pump();

      expect(keyEvents, 1);
      expect(seekEvents, 0);
    });

    testWidgets('focused slider owns one adjustable semantics node', (tester) async {
      LocaleSettings.setLocaleSync(AppLocale.en);
      final semantics = tester.ensureSemantics();
      final focusNode = FocusNode(debugLabel: 'semantic_timeline');
      addTearDown(focusNode.dispose);
      final seekEnds = <Duration>[];

      await tester.pumpWidget(
        MaterialApp(
          home: Scaffold(
            body: SizedBox(
              width: 400,
              child: TimelineSlider(
                position: const Duration(minutes: 1),
                duration: const Duration(minutes: 10),
                chapters: const [],
                chaptersLoaded: true,
                focusNode: focusNode,
                onSeek: (_) {},
                onSeekEnd: seekEnds.add,
              ),
            ),
          ),
        ),
      );
      focusNode.requestFocus();
      await tester.pump();

      final finder = find.bySemanticsLabel(t.videoControls.timelineSlider);
      expect(finder, findsOneWidget);
      final node = tester.getSemantics(finder);
      final data = node.getSemanticsData();
      expect(data.label, t.videoControls.timelineSlider);
      expect(data.value, '1:00');
      expect(data.increasedValue, '1:10');
      expect(data.decreasedValue, '0:50');
      expect(data.flagsCollection.isSlider, isTrue);
      expect(data.flagsCollection.isEnabled, ui.Tristate.isTrue);
      expect(data.flagsCollection.isButton, isFalse);
      expect(data.hasAction(ui.SemanticsAction.tap), isFalse);
      expect(data.hasAction(ui.SemanticsAction.increase), isTrue);
      expect(data.hasAction(ui.SemanticsAction.decrease), isTrue);

      node.owner!.performAction(node.id, ui.SemanticsAction.increase);
      node.owner!.performAction(node.id, ui.SemanticsAction.decrease);
      expect(seekEnds, const [Duration(minutes: 1, seconds: 10), Duration(seconds: 50)]);
      semantics.dispose();
    });

    testWidgets('does not pass chapters to painter when timeline markers are hidden', (tester) async {
      await tester.pumpWidget(
        MaterialApp(
          home: Scaffold(
            body: SizedBox(
              width: 400,
              child: TimelineSlider(
                position: const Duration(minutes: 1),
                duration: const Duration(minutes: 10),
                chapters: [MediaChapter(id: 1, startTimeOffset: 300000)],
                chaptersLoaded: true,
                showChapterMarkersOnTimeline: false,
                onSeek: (_) {},
                onSeekEnd: (_) {},
              ),
            ),
          ),
        ),
      );

      final customPaint = tester.widget<CustomPaint>(
        find.byWidgetPredicate((widget) => widget is CustomPaint && widget.painter is BufferRangePainter),
      );

      expect((customPaint.painter! as BufferRangePainter).chapters, isEmpty);
    });

    testWidgets('clamps stale position beyond duration before building slider', (tester) async {
      await tester.pumpWidget(
        MaterialApp(
          home: Scaffold(
            body: SizedBox(
              width: 400,
              child: TimelineSlider(
                position: const Duration(minutes: 12),
                duration: const Duration(minutes: 10),
                chapters: const [],
                chaptersLoaded: true,
                onSeek: (_) {},
                onSeekEnd: (_) {},
              ),
            ),
          ),
        ),
      );

      final slider = tester.widget<Slider>(find.byType(Slider));

      expect(slider.value, const Duration(minutes: 10).inMilliseconds.toDouble());
      expect(slider.max, const Duration(minutes: 10).inMilliseconds.toDouble());
    });

    testWidgets('clamps stale position when duration is unknown', (tester) async {
      await tester.pumpWidget(
        MaterialApp(
          home: Scaffold(
            body: SizedBox(
              width: 400,
              child: TimelineSlider(
                position: const Duration(minutes: 12),
                duration: Duration.zero,
                chapters: const [],
                chaptersLoaded: true,
                onSeek: (_) {},
                onSeekEnd: (_) {},
              ),
            ),
          ),
        ),
      );

      final slider = tester.widget<Slider>(find.byType(Slider));

      expect(slider.value, 0.0);
      expect(slider.max, 0.0);
    });

    testWidgets('timeline bar displays pending preview position while player position is stale', (tester) async {
      final player = FakeSyncPlayer(position: const Duration(minutes: 1), duration: const Duration(minutes: 10));
      addTearDown(player.dispose);

      await tester.pumpWidget(
        MaterialApp(
          home: Scaffold(
            body: SizedBox(
              width: 400,
              child: VideoTimelineBar(
                player: player,
                chapters: const [],
                chaptersLoaded: true,
                previewPosition: const Duration(minutes: 4),
                onSeek: (_) {},
                onSeekEnd: (_) {},
              ),
            ),
          ),
        ),
      );

      final slider = tester.widget<Slider>(find.byType(Slider));
      expect(slider.value, const Duration(minutes: 4).inMilliseconds.toDouble());
    });

    Future<void> pumpScrubSlider(
      WidgetTester tester, {
      required List<Duration> seeks,
      required List<Duration> seekEnds,
      Duration duration = const Duration(minutes: 10),
      bool enabled = true,
      VoidCallback? onScrubStart,
      VoidCallback? onScrubEnd,
      Widget Function(Widget child)? wrap,
    }) async {
      Widget slider = SizedBox(
        width: 400,
        child: TimelineSlider(
          position: const Duration(minutes: 1),
          duration: duration,
          chapters: const [],
          chaptersLoaded: true,
          enabled: enabled,
          onSeek: seeks.add,
          onSeekEnd: seekEnds.add,
          onScrubStart: onScrubStart,
          onScrubEnd: onScrubEnd,
        ),
      );
      if (wrap != null) slider = wrap(slider);
      await tester.pumpWidget(
        MaterialApp(
          home: Scaffold(body: Center(child: slider)),
        ),
      );
    }

    testWidgets('touch drag survives tooltip appearance and finalizes once', (tester) async {
      final seeks = <Duration>[];
      final seekEnds = <Duration>[];
      var scrubStarts = 0;
      var scrubEnds = 0;
      await pumpScrubSlider(
        tester,
        seeks: seeks,
        seekEnds: seekEnds,
        onScrubStart: () => scrubStarts++,
        onScrubEnd: () => scrubEnds++,
      );

      // Down at the center (200/400 → 5min), drag +100px (→ 7.5min). The
      // first scrub event makes the tooltip appear; the drag must keep
      // tracking through that rebuild and finalize exactly once.
      final gesture = await tester.startGesture(tester.getCenter(find.byType(TimelineSlider)));
      await tester.pump();
      await gesture.moveBy(const Offset(50, 0));
      await tester.pump();
      await gesture.moveBy(const Offset(50, 0));
      await tester.pump();
      await gesture.up();
      await tester.pump();

      expect(seeks, isNotEmpty);
      expect(seekEnds, hasLength(1));
      expect(scrubStarts, 1);
      expect(scrubEnds, 1);
      expect(seekEnds.single.inMilliseconds, closeTo(const Duration(minutes: 7, seconds: 30).inMilliseconds, 2000));
    });

    testWidgets('keyboard input does not start a scrub lifecycle', (tester) async {
      final focusNode = FocusNode();
      addTearDown(focusNode.dispose);
      var scrubStarts = 0;
      var scrubEnds = 0;

      await tester.pumpWidget(
        MaterialApp(
          home: Scaffold(
            body: SizedBox(
              width: 400,
              child: TimelineSlider(
                position: const Duration(minutes: 1),
                duration: const Duration(minutes: 10),
                chapters: const [],
                chaptersLoaded: true,
                focusNode: focusNode,
                onKeyEvent: (_, event) => KeyEventResult.handled,
                onSeek: (_) {},
                onSeekEnd: (_) {},
                onScrubStart: () => scrubStarts++,
                onScrubEnd: () => scrubEnds++,
              ),
            ),
          ),
        ),
      );

      focusNode.requestFocus();
      await tester.pump();
      await tester.sendKeyEvent(LogicalKeyboardKey.arrowRight);
      await tester.pump();

      expect(scrubStarts, 0);
      expect(scrubEnds, 0);
    });

    testWidgets('disposing mid-drag ends the scrub lifecycle', (tester) async {
      final seeks = <Duration>[];
      final seekEnds = <Duration>[];
      var scrubStarts = 0;
      var scrubEnds = 0;
      await pumpScrubSlider(
        tester,
        seeks: seeks,
        seekEnds: seekEnds,
        onScrubStart: () => scrubStarts++,
        onScrubEnd: () => scrubEnds++,
      );

      final gesture = await tester.startGesture(tester.getCenter(find.byType(TimelineSlider)));
      await tester.pump();
      expect(scrubStarts, 1);
      expect(scrubEnds, 0);

      await tester.pumpWidget(const SizedBox.shrink());
      await tester.pump();
      await gesture.cancel();

      expect(scrubEnds, 1);
    });

    testWidgets('tap seeks to the tapped position', (tester) async {
      final seeks = <Duration>[];
      final seekEnds = <Duration>[];
      await pumpScrubSlider(tester, seeks: seeks, seekEnds: seekEnds);

      final topLeft = tester.getTopLeft(find.byType(TimelineSlider));
      final size = tester.getSize(find.byType(TimelineSlider));
      final gesture = await tester.startGesture(Offset(topLeft.dx + size.width * 0.75, topLeft.dy + size.height / 2));
      await tester.pump();
      await gesture.up();
      await tester.pump();

      expect(seekEnds, hasLength(1));
      expect(seekEnds.single.inMilliseconds, closeTo(const Duration(minutes: 7, seconds: 30).inMilliseconds, 2000));
    });

    testWidgets('drag starting on the slider is never stolen by ancestor recognizers', (tester) async {
      final seeks = <Duration>[];
      final seekEnds = <Duration>[];
      var verticalDragUpdates = 0;
      var longPresses = 0;
      await pumpScrubSlider(
        tester,
        seeks: seeks,
        seekEnds: seekEnds,
        wrap: (child) => GestureDetector(
          behavior: HitTestBehavior.translucent,
          onVerticalDragUpdate: (_) => verticalDragUpdates++,
          onLongPressStart: (_) => longPresses++,
          child: child,
        ),
      );

      // Press-aim-drag: hold past the long-press deadline, then drag with a
      // vertical-dominant start. Without the eager claim, the long-press or
      // the vertical recognizer wins and the scrub is eaten.
      final gesture = await tester.startGesture(tester.getCenter(find.byType(TimelineSlider)));
      await tester.pump(const Duration(milliseconds: 600));
      for (var i = 0; i < 4; i++) {
        await gesture.moveBy(const Offset(8, 12));
        await tester.pump();
      }
      await gesture.moveBy(const Offset(50, 0));
      await tester.pump();
      await gesture.up();
      await tester.pump();

      expect(seekEnds, hasLength(1));
      expect(verticalDragUpdates, 0);
      expect(longPresses, 0);
    });

    testWidgets('ignores input when disabled', (tester) async {
      final seeks = <Duration>[];
      final seekEnds = <Duration>[];
      await pumpScrubSlider(tester, seeks: seeks, seekEnds: seekEnds, enabled: false);

      final gesture = await tester.startGesture(tester.getCenter(find.byType(TimelineSlider)));
      await tester.pump();
      await gesture.moveBy(const Offset(50, 0));
      await tester.pump();
      await gesture.up();
      await tester.pump();

      expect(seeks, isEmpty);
      expect(seekEnds, isEmpty);
    });

    testWidgets('ignores input when duration is unknown', (tester) async {
      final seeks = <Duration>[];
      final seekEnds = <Duration>[];
      await pumpScrubSlider(tester, seeks: seeks, seekEnds: seekEnds, duration: Duration.zero);

      final gesture = await tester.startGesture(tester.getCenter(find.byType(TimelineSlider)));
      await tester.pump();
      await gesture.moveBy(const Offset(50, 0));
      await tester.pump();
      await gesture.up();
      await tester.pump();

      expect(seeks, isEmpty);
      expect(seekEnds, isEmpty);
    });

    testWidgets('second finger is ignored mid-drag', (tester) async {
      final seeks = <Duration>[];
      final seekEnds = <Duration>[];
      await pumpScrubSlider(tester, seeks: seeks, seekEnds: seekEnds);

      final center = tester.getCenter(find.byType(TimelineSlider));
      final first = await tester.startGesture(center);
      await tester.pump();
      final seeksAfterDown = seeks.length;

      final second = await tester.startGesture(center + const Offset(100, 0));
      await tester.pump();
      await second.moveBy(const Offset(-80, 0));
      await tester.pump();
      expect(seeks.length, seeksAfterDown, reason: 'second pointer must not drive the scrub');

      await first.moveBy(const Offset(40, 0));
      await tester.pump();
      await first.up();
      await second.up();
      await tester.pump();

      // 240/400 of 10min → 6min: follows the first pointer only.
      expect(seekEnds, hasLength(1));
      expect(seekEnds.single.inMilliseconds, closeTo(const Duration(minutes: 6).inMilliseconds, 2000));
    });
  });

  group('shouldSkipDuplicateTimelineSeek', () {
    test('skips a matching final seek', () {
      expect(
        shouldSkipDuplicateTimelineSeek(
          lastDispatchedSeek: const Duration(minutes: 7, seconds: 30),
          finalSeek: const Duration(minutes: 7, seconds: 30),
        ),
        isTrue,
      );
    });

    test('does not skip when no matching seek was already dispatched', () {
      expect(
        shouldSkipDuplicateTimelineSeek(
          lastDispatchedSeek: const Duration(minutes: 7),
          finalSeek: const Duration(minutes: 7, seconds: 30),
        ),
        isFalse,
      );
      expect(
        shouldSkipDuplicateTimelineSeek(lastDispatchedSeek: null, finalSeek: const Duration(minutes: 7, seconds: 30)),
        isFalse,
      );
    });
  });

  group('shouldStartHiddenDirectionalSeek', () {
    test('accepts only the initial key-down event', () {
      expect(shouldStartHiddenDirectionalSeek(_keyDown(LogicalKeyboardKey.arrowRight)), isTrue);
      expect(
        shouldStartHiddenDirectionalSeek(
          const KeyRepeatEvent(
            physicalKey: PhysicalKeyboardKey.arrowRight,
            logicalKey: LogicalKeyboardKey.arrowRight,
            timeStamp: Duration.zero,
          ),
        ),
        isFalse,
      );
      expect(shouldStartHiddenDirectionalSeek(_keyUp(LogicalKeyboardKey.arrowRight)), isFalse);
    });
  });

  group('subtitle visibility', () {
    testWidgets('rolls back a failed latest toggle to the preceding successful mutation', (tester) async {
      LocaleSettings.setLocaleSync(AppLocale.en);
      await initializeDateFormatting('en');
      tester.view.physicalSize = const Size(1200, 800);
      tester.view.devicePixelRatio = 1;
      addTearDown(tester.view.reset);
      resetSharedPreferencesForTest();
      SettingsService.resetForTesting();
      final settings = await SettingsService.getInstance();
      final firstWrite = Completer<void>();
      final secondWrite = Completer<void>();
      final player = _FakeSubtitleVisibilityPlayer(writes: [firstWrite, secondWrite]);
      final volume = VideoVolumeController(player: player, settings: settings, initialVolume: 100);
      final playbackState = PlaybackStateProvider();
      final watchTogether = WatchTogetherProvider();
      final chrome = PlayerChromeController();
      final toast = PlayerToastController();
      addTearDown(volume.dispose);
      addTearDown(playbackState.dispose);
      addTearDown(watchTogether.dispose);
      addTearDown(chrome.dispose);
      addTearDown(toast.dispose);

      await tester.pumpWidget(
        MultiProvider(
          providers: [
            ChangeNotifierProvider<PlaybackStateProvider>.value(value: playbackState),
            ChangeNotifierProvider<WatchTogetherProvider>.value(value: watchTogether),
          ],
          child: MaterialApp(
            theme: ThemeData(platform: TargetPlatform.macOS, extensions: const [testMonoTokens]),
            home: Scaffold(
              body: SizedBox(
                width: 1200,
                height: 800,
                child: PlexVideoControls(
                  player: player,
                  volumeController: volume,
                  metadata: testMediaItem(id: 'subtitle-visibility'),
                  toastController: toast,
                  canNavigateMediaItems: false,
                  chromeController: chrome,
                  isLive: true,
                ),
              ),
            ),
          ),
        ),
      );
      await tester.pump();
      expect(find.byIcon(Symbols.subtitles_rounded), findsOneWidget);

      await tester.sendKeyEvent(LogicalKeyboardKey.keyS);
      await tester.pump();
      expect(player.propertyValues, ['no']);
      expect(find.byIcon(Symbols.subtitles_off_rounded), findsOneWidget);

      await tester.sendKeyEvent(LogicalKeyboardKey.keyS);
      await tester.pump();
      expect(player.propertyValues, ['no'], reason: 'the latest toggle must wait for the in-flight native write');
      expect(find.byIcon(Symbols.subtitles_rounded), findsOneWidget);

      firstWrite.complete();
      await tester.pump();
      await tester.pump();
      expect(player.propertyValues, ['no', 'yes']);

      secondWrite.completeError(PlatformException(code: 'SET_PROPERTY_FAILED'));
      await tester.pump();
      await tester.pump();

      expect(tester.takeException(), isNull);
      expect(find.byIcon(Symbols.subtitles_off_rounded), findsOneWidget);
      chrome.cancelAutoHide();
      await tester.pumpWidget(const SizedBox.shrink());
    });
  });

  group('SyncOffsetControl', () {
    testWidgets('uses 100ms slider steps without rendering tick marks', (tester) async {
      LocaleSettings.setLocaleSync(AppLocale.en);

      await tester.pumpWidget(
        MaterialApp(
          theme: ThemeData(extensions: const [testMonoTokens]),
          home: Scaffold(
            body: SizedBox(
              width: 700,
              child: SyncOffsetControl(
                player: _FakeSyncPlayer(),
                propertyName: 'sub-delay',
                initialOffset: 0,
                labelText: 'Subtitles',
                onOffsetChanged: (_) async {},
                compact: true,
              ),
            ),
          ),
        ),
      );

      final slider = tester.widget<Slider>(find.byType(Slider));
      final sliderTheme = tester.widget<SliderTheme>(
        find.ancestor(of: find.byType(Slider), matching: find.byType(SliderTheme)).first,
      );

      expect(slider.min, -60000);
      expect(slider.max, 60000);
      expect(slider.divisions, 1200);
      expect((slider.max - slider.min) / slider.divisions!, 100);
      expect(sliderTheme.data.tickMarkShape, same(SliderTickMarkShape.noTickMark));
    });

    testWidgets('reconciles a failed current write without persisting it', (tester) async {
      final propertyWrite = Completer<void>();
      final persistedOffsets = <int>[];
      final player = _FakeSyncPlayer(onSetProperty: (_, _) => propertyWrite.future);

      await tester.pumpWidget(
        MaterialApp(
          theme: ThemeData(extensions: const [testMonoTokens]),
          home: Scaffold(
            body: SizedBox(
              width: 700,
              child: SyncOffsetControl(
                player: player,
                propertyName: 'sub-delay',
                initialOffset: 500,
                labelText: 'Subtitles',
                onOffsetChanged: (offset) async => persistedOffsets.add(offset),
                compact: true,
              ),
            ),
          ),
        ),
      );

      tester.widget<Slider>(find.byType(Slider)).onChanged!(600);
      await tester.pump();
      tester.widget<Slider>(find.byType(Slider)).onChangeEnd!(600);
      await tester.pump();
      expect(tester.widget<Slider>(find.byType(Slider)).value, 600);
      expect(persistedOffsets, isEmpty);

      propertyWrite.completeError(PlatformException(code: 'SET_PROPERTY_FAILED'));
      await tester.pump();
      await tester.pump();

      expect(tester.takeException(), isNull);
      expect(tester.widget<Slider>(find.byType(Slider)).value, 500);
      expect(persistedOffsets, isEmpty);
    });

    testWidgets('ignores stale failure and persists the latest accepted offset once', (tester) async {
      final staleWrite = Completer<void>();
      final persistedOffsets = <int>[];
      var writeCount = 0;
      final player = _FakeSyncPlayer(
        onSetProperty: (_, _) {
          writeCount++;
          return writeCount == 1 ? staleWrite.future : Future<void>.value();
        },
      );

      await tester.pumpWidget(
        MaterialApp(
          theme: ThemeData(extensions: const [testMonoTokens]),
          home: Scaffold(
            body: SizedBox(
              width: 700,
              child: SyncOffsetControl(
                player: player,
                propertyName: 'audio-delay',
                initialOffset: 0,
                labelText: 'Audio',
                onOffsetChanged: (offset) async => persistedOffsets.add(offset),
                compact: true,
              ),
            ),
          ),
        ),
      );

      tester.widget<Slider>(find.byType(Slider)).onChanged!(100);
      await tester.pump();
      tester.widget<Slider>(find.byType(Slider)).onChangeEnd!(100);
      await tester.pump();

      tester.widget<Slider>(find.byType(Slider)).onChanged!(200);
      await tester.pump();
      tester.widget<Slider>(find.byType(Slider)).onChangeEnd!(200);
      await tester.pump();
      await tester.pump();
      expect(persistedOffsets, isEmpty);

      staleWrite.completeError(PlatformException(code: 'SET_PROPERTY_FAILED'));
      await tester.pump();
      await tester.pump();
      await tester.pump();

      expect(tester.takeException(), isNull);
      expect(tester.widget<Slider>(find.byType(Slider)).value, 200);
      expect(persistedOffsets, [200]);
    });

    testWidgets('rolls back a failed latest write to the preceding successful offset', (tester) async {
      final firstWrite = Completer<void>();
      final secondWrite = Completer<void>();
      final propertyValues = <String>[];
      final persistedOffsets = <int>[];
      final player = _FakeSyncPlayer(
        onSetProperty: (_, value) {
          propertyValues.add(value);
          return propertyValues.length == 1 ? firstWrite.future : secondWrite.future;
        },
      );

      await tester.pumpWidget(
        MaterialApp(
          theme: ThemeData(extensions: const [testMonoTokens]),
          home: Scaffold(
            body: SizedBox(
              width: 700,
              child: SyncOffsetControl(
                player: player,
                propertyName: 'sub-delay',
                initialOffset: 0,
                labelText: 'Subtitles',
                onOffsetChanged: (offset) async {
                  persistedOffsets.add(offset);
                },
                compact: true,
              ),
            ),
          ),
        ),
      );

      tester.widget<Slider>(find.byType(Slider)).onChanged!(100);
      tester.widget<Slider>(find.byType(Slider)).onChangeEnd!(100);
      await tester.pump();
      expect(propertyValues, ['0.1']);

      tester.widget<Slider>(find.byType(Slider)).onChanged!(200);
      tester.widget<Slider>(find.byType(Slider)).onChangeEnd!(200);
      await tester.pump();
      expect(propertyValues, ['0.1']);
      expect(tester.widget<Slider>(find.byType(Slider)).value, 200);

      firstWrite.complete();
      await tester.pump();
      await tester.pump();
      expect(propertyValues, ['0.1', '0.2']);
      expect(persistedOffsets, [100]);

      secondWrite.completeError(PlatformException(code: 'SET_PROPERTY_FAILED'));
      await tester.pump();
      await tester.pump();

      expect(tester.takeException(), isNull);
      expect(tester.widget<Slider>(find.byType(Slider)).value, 100);
      expect(persistedOffsets, [100]);
    });

    testWidgets('persists an accepted offset after the control is disposed', (tester) async {
      final propertyWrite = Completer<void>();
      final persistedOffsets = <int>[];
      final player = _FakeSyncPlayer(onSetProperty: (_, _) => propertyWrite.future);

      await tester.pumpWidget(
        MaterialApp(
          theme: ThemeData(extensions: const [testMonoTokens]),
          home: Scaffold(
            body: SizedBox(
              width: 700,
              child: SyncOffsetControl(
                player: player,
                propertyName: 'sub-delay',
                initialOffset: 0,
                labelText: 'Subtitles',
                onOffsetChanged: (offset) async => persistedOffsets.add(offset),
                compact: true,
              ),
            ),
          ),
        ),
      );

      tester.widget<Slider>(find.byType(Slider)).onChanged!(100);
      tester.widget<Slider>(find.byType(Slider)).onChangeEnd!(100);
      await tester.pumpWidget(const SizedBox.shrink());
      propertyWrite.complete();
      await tester.pump();

      expect(persistedOffsets, [100]);
    });
  });

  group('VideoControlButton semantics', () {
    testWidgets('exposes one operable node with value and checked state', (tester) async {
      final semantics = tester.ensureSemantics();
      final focusNode = FocusNode(debugLabel: 'semantic_video_control');
      addTearDown(focusNode.dispose);
      var activations = 0;

      await tester.pumpWidget(
        MaterialApp(
          home: Scaffold(
            body: VideoControlButton(
              icon: Icons.settings,
              tooltip: 'Player settings',
              semanticValue: '720p',
              checked: true,
              focusNode: focusNode,
              onPressed: () => activations++,
            ),
          ),
        ),
      );

      final finder = find.bySemanticsLabel('Player settings');
      expect(finder, findsOneWidget);
      final data = tester.getSemantics(finder).getSemanticsData();
      expect(data.value, '720p');
      expect(data.flagsCollection.isButton, isTrue);
      expect(data.flagsCollection.isChecked, ui.CheckedState.isTrue);
      expect(data.hasAction(ui.SemanticsAction.tap), isTrue);

      final node = tester.getSemantics(finder);
      node.owner!.performAction(node.id, ui.SemanticsAction.tap);
      await tester.pump();
      expect(activations, 1);
      semantics.dispose();
    });

    testWidgets('keeps disabled controls discoverable without a tap action', (tester) async {
      final semantics = tester.ensureSemantics();

      await tester.pumpWidget(
        const MaterialApp(
          home: Scaffold(
            body: VideoControlButton(icon: Icons.skip_next, semanticLabel: 'Next item', onPressed: null),
          ),
        ),
      );

      final data = tester.getSemantics(find.bySemanticsLabel('Next item')).getSemanticsData();
      expect(data.flagsCollection.isButton, isTrue);
      expect(data.flagsCollection.isEnabled, ui.Tristate.isFalse);
      expect(data.hasAction(ui.SemanticsAction.tap), isFalse);
      semantics.dispose();
    });
  });
}

KeyDownEvent _keyDown(LogicalKeyboardKey key) {
  return KeyDownEvent(physicalKey: PhysicalKeyboardKey.escape, logicalKey: key, timeStamp: Duration.zero);
}

KeyUpEvent _keyUp(LogicalKeyboardKey key) {
  return KeyUpEvent(physicalKey: PhysicalKeyboardKey.escape, logicalKey: key, timeStamp: Duration.zero);
}

KeyDownEvent _navigationKeyDown(LogicalKeyboardKey key, ui.KeyEventDeviceType deviceType) {
  return KeyDownEvent(
    physicalKey: PhysicalKeyboardKey.escape,
    logicalKey: key,
    timeStamp: Duration.zero,
    deviceType: deviceType,
  );
}

Future<void> _pumpSkipMarkerButton(
  WidgetTester tester, {
  required FocusNode focusNode,
  required bool isAutoSkipActive,
  required VoidCallback onActivate,
  VoidCallback? onFocusDown,
}) {
  return tester.pumpWidget(
    MaterialApp(
      theme: ThemeData(extensions: const [testMonoTokens]),
      home: Scaffold(
        body: Center(
          child: SkipMarkerButton(
            marker: MediaMarker(id: 1, type: 'intro', startTimeOffset: 10000, endTimeOffset: 45000),
            playerDuration: const Duration(minutes: 20),
            hasNextEpisode: false,
            isAutoSkipActive: isAutoSkipActive,
            shouldShowAutoSkip: true,
            autoSkipDelay: 5,
            autoSkipProgress: 0.4,
            focusNode: focusNode,
            onActivate: onActivate,
            onFocusDown: onFocusDown ?? () {},
          ),
        ),
      ),
    ),
  );
}

class _FakeSyncPlayer implements Player {
  _FakeSyncPlayer({this.onSetProperty});

  final Future<void> Function(String name, String value)? onSetProperty;

  @override
  PlayerState get state => PlayerState();

  @override
  Future<void> setProperty(String name, String value) {
    return onSetProperty?.call(name, value) ?? Future<void>.value();
  }

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

class _FakeSubtitleVisibilityPlayer implements Player {
  _FakeSubtitleVisibilityPlayer({required this.writes});

  final List<Completer<void>> writes;
  final List<String> propertyValues = [];

  @override
  String get playerType => 'mpv';

  @override
  PlayerState get state => PlayerState(
    duration: const Duration(minutes: 45),
    seekable: true,
    tracks: const Tracks(
      subtitle: [SubtitleTrack(id: 'subtitle-1', language: 'eng')],
    ),
    track: const TrackSelection(
      subtitle: SubtitleTrack(id: 'subtitle-1', language: 'eng'),
    ),
  );

  @override
  PlayerStreams get streams => PlayerStreams(
    playing: const Stream<bool>.empty(),
    completed: const Stream<bool>.empty(),
    buffering: const Stream<bool>.empty(),
    position: const Stream<Duration>.empty(),
    duration: const Stream<Duration>.empty(),
    seekable: const Stream<bool>.empty(),
    buffer: const Stream<Duration>.empty(),
    volume: const Stream<double>.empty(),
    rate: const Stream<double>.empty(),
    tracks: const Stream<Tracks>.empty(),
    track: const Stream<TrackSelection>.empty(),
    log: const Stream<PlayerLog>.empty(),
    error: const Stream<PlayerError>.empty(),
    audioDevice: const Stream<AudioDevice>.empty(),
    audioDevices: const Stream<List<AudioDevice>>.empty(),
    bufferRanges: const Stream<List<BufferRange>>.empty(),
    playbackRestart: const Stream<void>.empty(),
    backendSwitched: const Stream<void>.empty(),
  );

  @override
  Future<void> setProperty(String name, String value) {
    expect(name, 'sub-visibility');
    propertyValues.add(value);
    return writes[propertyValues.length - 1].future;
  }

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