import 'dart:convert';
import 'dart:io';

import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:gametime/application/application.dart';
import 'package:gametime/domain/domain.dart';
import 'package:gametime/presentation/presentation.dart';

void main() {
  testWidgets('le temps affiché est recalculé depuis les horodatages', (
    tester,
  ) async {
    final clock = _FakeClock(DateTime.utc(2026, 7, 17, 12, 0, 15));
    final session = ActiveWorkoutSession(
      metadata: _metadata('session-1'),
      sourceWorkoutTemplateId: 'template-1',
      status: ActiveWorkoutStatus.running,
      startedAt: DateTime.utc(2026, 7, 17, 11, 59),
      lastPersistedAt: DateTime.utc(2026, 7, 17, 12),
      elapsedActiveMs: 30000,
      currentProgramIndex: 0,
      currentExerciseIndex: 0,
      currentSetIndex: 0,
      resolvedTemplateSnapshotJson: _sessionSnapshot(),
    );

    await tester.pumpWidget(
      MaterialApp(
        home: WorkoutExecutionScreen(
          initialSession: session,
          activeUseCases: _activeUseCases(
            _FakeActiveSessionRepository(),
            clock,
          ),
          closeUseCase: _closeUseCase(_FakeActiveSessionRepository(), clock),
          historyUseCases: _historyUseCases(clock),
          workoutTemplateUseCases: _workoutTemplateUseCases(),
        ),
      ),
    );

    expect(find.text('Squat'), findsOneWidget);
    expect(find.text('Temps de série : 00:45'), findsOneWidget);
    expect(find.widgetWithText(FilledButton, 'Démarrer'), findsOneWidget);
  });

  testWidgets('les répétitions sont initialisées avec la cible', (
    tester,
  ) async {
    final clock = _FakeClock(DateTime.utc(2026, 7, 17, 12));
    final session = ActiveWorkoutSession(
      metadata: _metadata('session-1'),
      sourceWorkoutTemplateId: 'template-1',
      status: ActiveWorkoutStatus.running,
      startedAt: DateTime.utc(2026, 7, 17, 12),
      lastPersistedAt: DateTime.utc(2026, 7, 17, 12),
      elapsedActiveMs: 0,
      currentProgramIndex: 0,
      currentExerciseIndex: 0,
      currentSetIndex: 0,
      resolvedTemplateSnapshotJson: _sessionSnapshot(targetReps: 8),
    );

    await tester.pumpWidget(
      MaterialApp(
        home: WorkoutExecutionScreen(
          initialSession: session,
          activeUseCases: _activeUseCases(
            _FakeActiveSessionRepository(),
            clock,
          ),
          closeUseCase: _closeUseCase(_FakeActiveSessionRepository(), clock),
          historyUseCases: _historyUseCases(clock),
          workoutTemplateUseCases: _workoutTemplateUseCases(),
        ),
      ),
    );

    expect(find.text('Répétitions'), findsOneWidget);
    expect(find.text('8'), findsOneWidget);
  });

  testWidgets('les références de temps sont formatées sans valeurs brutes', (
    tester,
  ) async {
    final clock = _FakeClock(DateTime.utc(2026, 7, 17, 12));
    final repository = _FakeActiveSessionRepository();
    final performanceRepository = _FakeExercisePerformanceReferenceRepository(
      last: WorkoutHistorySetPerformance(
        workoutHistoryId: 'history-1',
        startedAt: DateTime.utc(2026, 7, 12),
        setIndex: 0,
        exerciseNameSnapshot: 'Squat',
        scoreInputModeSnapshot: ScoreInputMode.stopwatch,
        actualTimeMs: 72000,
        actualScoreTimeMs: 45100,
      ),
      record: WorkoutHistoryMetricPerformance(
        workoutHistoryId: 'history-2',
        startedAt: DateTime.utc(2026, 7, 15),
        setIndex: 0,
        exerciseNameSnapshot: 'Squat',
        metric: PerformanceMetric.score,
        scoreInputModeSnapshot: ScoreInputMode.stopwatch,
        actualScoreTimeMs: 42800,
      ),
    );
    final session = ActiveWorkoutSession(
      metadata: _metadata('session-1'),
      sourceWorkoutTemplateId: 'template-1',
      status: ActiveWorkoutStatus.running,
      startedAt: DateTime.utc(2026, 7, 17, 12),
      lastPersistedAt: DateTime.utc(2026, 7, 17, 12),
      elapsedActiveMs: 0,
      currentProgramIndex: 0,
      currentExerciseIndex: 0,
      currentSetIndex: 0,
      resolvedTemplateSnapshotJson: _sessionSnapshot(
        sourceExerciseId: 'exercise-source-1',
        scoreInputMode: ScoreInputMode.stopwatch,
        targetTimeSeconds: 300,
      ),
    );
    repository.session = session;

    await tester.pumpWidget(
      MaterialApp(
        home: WorkoutExecutionScreen(
          initialSession: session,
          activeUseCases: _activeUseCases(repository, clock),
          closeUseCase: _closeUseCase(repository, clock),
          historyUseCases: _historyUseCases(clock),
          workoutTemplateUseCases: _workoutTemplateUseCases(),
          performanceReferenceUseCase: ExercisePerformanceReferenceUseCase(
            repository: performanceRepository,
          ),
        ),
      ),
    );
    await tester.pump();
    await tester.pump();

    expect(find.text('Temps de série : 05:00'), findsOneWidget);
    expect(find.text('1:12 · 00:45.1'), findsOneWidget);
    expect(find.text('00:42.8'), findsOneWidget);
    expect(find.textContaining('72000'), findsNothing);
    expect(find.textContaining('42800'), findsNothing);
  });

  testWidgets('les mesures cumulées respectent la hiérarchie visuelle', (
    tester,
  ) async {
    final scoreController = TextEditingController();
    addTearDown(scoreController.dispose);

    await tester.pumpWidget(
      MaterialApp(
        home: Scaffold(
          body: SetMeasureInput(
            exercise: const ExecutionExercise(
              id: 'exercise-1',
              name: 'Squat',
              setsCount: 3,
              timeEnabled: true,
              repsEnabled: true,
              scoreEnabled: true,
              restSeconds: 0,
              scoreUnit: 'kg',
            ),
            reps: 0,
            scoreController: scoreController,
            onRepsChanged: (_) {},
          ),
        ),
      ),
    );

    final timeY = tester.getTopLeft(find.text('Temps')).dy;
    final repsY = tester.getTopLeft(find.text('Répétitions')).dy;
    final scoreY = tester.getTopLeft(find.text('Score (kg)')).dy;

    expect(timeY, lessThan(repsY));
    expect(repsY, lessThan(scoreY));
  });

  testWidgets('aucun repos ne s’affiche après la toute dernière série', (
    tester,
  ) async {
    final plan = WorkoutExecutionPlan.fromSession(
      ActiveWorkoutSession(
        metadata: _metadata('session-1'),
        sourceWorkoutTemplateId: 'template-1',
        status: ActiveWorkoutStatus.running,
        startedAt: DateTime.utc(2026, 7, 17, 12),
        lastPersistedAt: DateTime.utc(2026, 7, 17, 12),
        elapsedActiveMs: 0,
        currentProgramIndex: 0,
        currentExerciseIndex: 0,
        currentSetIndex: 0,
        resolvedTemplateSnapshotJson: _sessionSnapshot(
          setsCount: 1,
          restSeconds: 60,
        ),
      ),
    );
    const position = ExecutionPosition(
      programIndex: 0,
      exerciseIndex: 0,
      setIndex: 0,
    );

    await tester.pumpWidget(
      Directionality(
        textDirection: TextDirection.ltr,
        child: Text(
          plan.shouldShowRestAfter(position) ? 'Repos' : 'Pas de repos',
        ),
      ),
    );

    expect(find.text('Pas de repos'), findsOneWidget);
  });

  testWidgets('le bouton médias est absent sans média', (tester) async {
    final clock = _FakeClock(DateTime.utc(2026, 7, 17, 12));
    final session = ActiveWorkoutSession(
      metadata: _metadata('session-1'),
      sourceWorkoutTemplateId: 'template-1',
      status: ActiveWorkoutStatus.running,
      startedAt: DateTime.utc(2026, 7, 17, 12),
      lastPersistedAt: DateTime.utc(2026, 7, 17, 12),
      elapsedActiveMs: 0,
      currentProgramIndex: 0,
      currentExerciseIndex: 0,
      currentSetIndex: 0,
      resolvedTemplateSnapshotJson: _sessionSnapshot(),
    );

    await tester.pumpWidget(
      MaterialApp(
        home: WorkoutExecutionScreen(
          initialSession: session,
          activeUseCases: _activeUseCases(
            _FakeActiveSessionRepository(),
            clock,
          ),
          closeUseCase: _closeUseCase(_FakeActiveSessionRepository(), clock),
          historyUseCases: _historyUseCases(clock),
          workoutTemplateUseCases: _workoutTemplateUseCases(),
        ),
      ),
    );

    expect(find.text('Voir médias'), findsNothing);
    expect(find.byTooltip('Voir les médias de l’exercice'), findsNothing);
  });

  testWidgets('le bouton médias ouvre la sheet si médias présents', (
    tester,
  ) async {
    final imageFile = (await tester.runAsync(_createTestImageFile))!;
    addTearDown(() async {
      await tester.runAsync(() async {
        await imageFile.parent.delete(recursive: true);
      });
    });
    final clock = _FakeClock(DateTime.utc(2026, 7, 17, 12));
    final session = ActiveWorkoutSession(
      metadata: _metadata('session-1'),
      sourceWorkoutTemplateId: 'template-1',
      status: ActiveWorkoutStatus.running,
      startedAt: DateTime.utc(2026, 7, 17, 12),
      lastPersistedAt: DateTime.utc(2026, 7, 17, 12),
      elapsedActiveMs: 0,
      currentProgramIndex: 0,
      currentExerciseIndex: 0,
      currentSetIndex: 0,
      resolvedTemplateSnapshotJson: _sessionSnapshot(
        imageMediaIds: const ['image-1'],
        videoMediaId: 'video-1',
      ),
    );

    await tester.runAsync(() async {
      await tester.pumpWidget(
        MaterialApp(
          home: WorkoutExecutionScreen(
            initialSession: session,
            activeUseCases: _activeUseCases(
              _FakeActiveSessionRepository(),
              clock,
            ),
            closeUseCase: _closeUseCase(_FakeActiveSessionRepository(), clock),
            historyUseCases: _historyUseCases(clock),
            workoutTemplateUseCases: _workoutTemplateUseCases(),
            mediaAssetLoader: (id) async {
              if (id == 'image-1') {
                return MediaAsset(
                  metadata: _metadata(id),
                  kind: MediaKind.image,
                  localUri: imageFile.uri.toString(),
                );
              }
              if (id == 'video-1') {
                return MediaAsset(
                  metadata: _metadata(id),
                  kind: MediaKind.video,
                  localUri: imageFile.uri.toString(),
                );
              }
              return null;
            },
            videoMediaBuilder: (context, asset) => SizedBox(
              height: 260,
              child: Center(child: Text('Lecteur vidéo ${asset.metadata.id}')),
            ),
          ),
        ),
      );
    });

    expect(find.byTooltip('Voir les médias de l’exercice'), findsOneWidget);

    await tester.tap(find.byTooltip('Voir les médias de l’exercice'));
    await tester.runAsync(() async {
      await tester.pumpAndSettle();
    });

    expect(find.text('Médias de l’exercice'), findsOneWidget);
    expect(find.text('Images'), findsOneWidget);
    expect(find.text('Vidéo'), findsOneWidget);
    expect(find.text('1/1'), findsOneWidget);
    expect(
      find.byKey(const ValueKey('exercise-image-image-1')),
      findsOneWidget,
    );
    expect(find.text('Image 1'), findsNothing);

    await tester.tap(find.text('Vidéo'));
    await tester.pumpAndSettle();

    expect(find.text('Lecteur vidéo video-1'), findsOneWidget);
  });

  testWidgets('la galerie médias navigue entre les images', (tester) async {
    final imageFile = (await tester.runAsync(_createTestImageFile))!;
    addTearDown(() async {
      await tester.runAsync(() async {
        await imageFile.parent.delete(recursive: true);
      });
    });
    final clock = _FakeClock(DateTime.utc(2026, 7, 17, 12));
    final session = ActiveWorkoutSession(
      metadata: _metadata('session-1'),
      sourceWorkoutTemplateId: 'template-1',
      status: ActiveWorkoutStatus.running,
      startedAt: DateTime.utc(2026, 7, 17, 12),
      lastPersistedAt: DateTime.utc(2026, 7, 17, 12),
      elapsedActiveMs: 0,
      currentProgramIndex: 0,
      currentExerciseIndex: 0,
      currentSetIndex: 0,
      resolvedTemplateSnapshotJson: _sessionSnapshot(
        imageMediaIds: const ['image-1', 'image-2', 'image-3'],
      ),
    );

    await tester.runAsync(() async {
      await tester.pumpWidget(
        MaterialApp(
          home: WorkoutExecutionScreen(
            initialSession: session,
            activeUseCases: _activeUseCases(
              _FakeActiveSessionRepository(),
              clock,
            ),
            closeUseCase: _closeUseCase(_FakeActiveSessionRepository(), clock),
            historyUseCases: _historyUseCases(clock),
            workoutTemplateUseCases: _workoutTemplateUseCases(),
            mediaAssetLoader: (id) async => MediaAsset(
              metadata: _metadata(id),
              kind: MediaKind.image,
              localUri: imageFile.uri.toString(),
            ),
          ),
        ),
      );
    });

    await tester.tap(find.byTooltip('Voir les médias de l’exercice'));
    await tester.runAsync(() async {
      await tester.pumpAndSettle();
    });

    expect(find.text('1/3'), findsOneWidget);

    await tester.drag(find.byType(PageView), const Offset(-400, 0));
    await tester.runAsync(() async {
      await tester.pumpAndSettle();
    });

    expect(find.text('2/3'), findsOneWidget);
    expect(
      find.byKey(const ValueKey('exercise-image-image-2')),
      findsOneWidget,
    );
  });

  testWidgets('le chrono score démarre et s’arrête avec la bonne durée', (
    tester,
  ) async {
    await tester.binding.setSurfaceSize(const Size(400, 1400));
    addTearDown(() => tester.binding.setSurfaceSize(null));

    final clock = _FakeClock(DateTime.utc(2026, 7, 17, 12));
    final repository = _FakeActiveSessionRepository();
    final session = ActiveWorkoutSession(
      metadata: _metadata('session-1'),
      sourceWorkoutTemplateId: 'template-1',
      status: ActiveWorkoutStatus.running,
      startedAt: DateTime.utc(2026, 7, 17, 12),
      lastPersistedAt: DateTime.utc(2026, 7, 17, 12),
      elapsedActiveMs: 0,
      currentProgramIndex: 0,
      currentExerciseIndex: 0,
      currentSetIndex: 0,
      resolvedTemplateSnapshotJson: _sessionSnapshot(
        scoreInputMode: ScoreInputMode.stopwatch,
      ),
    );
    repository.session = session;

    await tester.pumpWidget(
      MaterialApp(
        home: WorkoutExecutionScreen(
          initialSession: session,
          activeUseCases: _activeUseCases(repository, clock),
          closeUseCase: _closeUseCase(repository, clock),
          historyUseCases: _historyUseCases(clock),
          workoutTemplateUseCases: _workoutTemplateUseCases(),
        ),
      ),
    );

    expect(find.text('Chrono score'), findsOneWidget);
    expect(find.text('00:00.0'), findsOneWidget);

    await tester.tap(find.widgetWithText(FilledButton, 'Lancer'));
    await tester.pump();
    clock.value = DateTime.utc(2026, 7, 17, 12, 0, 0, 100);
    await tester.pump(const Duration(milliseconds: 100));

    expect(find.text('00:00.1'), findsOneWidget);

    clock.value = DateTime.utc(2026, 7, 17, 12, 0, 0, 900);
    await tester.pump(const Duration(milliseconds: 100));

    expect(find.text('00:00.9'), findsOneWidget);

    clock.value = DateTime.utc(2026, 7, 17, 12, 0, 1, 200);
    await tester.pump(const Duration(seconds: 1));

    expect(find.text('00:01.2'), findsOneWidget);

    await tester.tap(find.widgetWithText(FilledButton, 'Arrêter'));
    await tester.pump();

    expect(find.text('00:01.2'), findsOneWidget);
    expect(
      repository.scoreStopwatchStates.single.status,
      ActiveScoreStopwatchStatus.stopped,
    );
  });

  testWidgets('terminer sans chrono démarré affiche une confirmation', (
    tester,
  ) async {
    await tester.binding.setSurfaceSize(const Size(400, 1400));
    addTearDown(() => tester.binding.setSurfaceSize(null));

    final clock = _FakeClock(DateTime.utc(2026, 7, 17, 12));
    final repository = _FakeActiveSessionRepository();
    final session = ActiveWorkoutSession(
      metadata: _metadata('session-1'),
      sourceWorkoutTemplateId: 'template-1',
      status: ActiveWorkoutStatus.running,
      startedAt: DateTime.utc(2026, 7, 17, 12),
      lastPersistedAt: DateTime.utc(2026, 7, 17, 12),
      elapsedActiveMs: 0,
      currentProgramIndex: 0,
      currentExerciseIndex: 0,
      currentSetIndex: 0,
      resolvedTemplateSnapshotJson: _sessionSnapshot(
        scoreInputMode: ScoreInputMode.stopwatch,
      ),
    );
    repository.session = session;

    await tester.pumpWidget(
      MaterialApp(
        home: WorkoutExecutionScreen(
          initialSession: session,
          activeUseCases: _activeUseCases(repository, clock),
          closeUseCase: _closeUseCase(repository, clock),
          historyUseCases: _historyUseCases(clock),
          workoutTemplateUseCases: _workoutTemplateUseCases(),
        ),
      ),
    );

    await tester.tap(find.text('Terminer la série'));
    await tester.pumpAndSettle();

    expect(find.text('Chrono non lancé'), findsOneWidget);
    expect(
      find.text('Tu n’as pas démarré le chrono de cette série.'),
      findsOneWidget,
    );
    expect(find.widgetWithText(TextButton, 'Démarrer'), findsOneWidget);
    expect(find.text('Terminer sans chrono'), findsOneWidget);
    expect(repository.results, isEmpty);
  });

  testWidgets('terminer avec chrono en cours l’arrête et enregistre', (
    tester,
  ) async {
    await tester.binding.setSurfaceSize(const Size(400, 1400));
    addTearDown(() => tester.binding.setSurfaceSize(null));

    final clock = _FakeClock(DateTime.utc(2026, 7, 17, 12));
    final repository = _FakeActiveSessionRepository();
    final session = ActiveWorkoutSession(
      metadata: _metadata('session-1'),
      sourceWorkoutTemplateId: 'template-1',
      status: ActiveWorkoutStatus.running,
      startedAt: DateTime.utc(2026, 7, 17, 12),
      lastPersistedAt: DateTime.utc(2026, 7, 17, 12),
      elapsedActiveMs: 0,
      currentProgramIndex: 0,
      currentExerciseIndex: 0,
      currentSetIndex: 0,
      resolvedTemplateSnapshotJson: _sessionSnapshot(
        setsCount: 2,
        scoreInputMode: ScoreInputMode.stopwatch,
      ),
    );
    repository.session = session;

    await tester.pumpWidget(
      MaterialApp(
        home: WorkoutExecutionScreen(
          initialSession: session,
          activeUseCases: _activeUseCases(repository, clock),
          closeUseCase: _closeUseCase(repository, clock),
          historyUseCases: _historyUseCases(clock),
          workoutTemplateUseCases: _workoutTemplateUseCases(),
        ),
      ),
    );

    await tester.tap(find.widgetWithText(FilledButton, 'Démarrer').first);
    await tester.pump();
    clock.value = DateTime.utc(2026, 7, 17, 12, 0, 2, 500);
    await tester.pump(const Duration(seconds: 1));
    await tester.tap(find.text('Terminer la série'));
    await tester.pumpAndSettle();

    expect(repository.results.single.actualScoreTimeMs, 2500);
    expect(
      repository.scoreStopwatchStates.single.status,
      ActiveScoreStopwatchStatus.stopped,
    );
    expect(repository.session?.currentSetIndex, 1);
  });

  testWidgets('un repos actif est repris avec le temps restant recalculé', (
    tester,
  ) async {
    final clock = _FakeClock(DateTime.utc(2026, 7, 17, 12));
    final repository = _FakeActiveSessionRepository();
    final session = ActiveWorkoutSession(
      metadata: _metadata('session-1'),
      sourceWorkoutTemplateId: 'template-1',
      status: ActiveWorkoutStatus.running,
      startedAt: DateTime.utc(2026, 7, 17, 12),
      lastPersistedAt: DateTime.utc(2026, 7, 17, 12),
      elapsedActiveMs: 0,
      currentProgramIndex: 0,
      currentExerciseIndex: 0,
      currentSetIndex: 0,
      resolvedTemplateSnapshotJson: _sessionSnapshot(
        setsCount: 2,
        restSeconds: 60,
      ),
    );
    repository.session = session;
    repository.restStates.add(
      ActiveRestState(
        metadata: _metadata('rest-1'),
        activeWorkoutSessionId: 'session-1',
        afterProgramIndex: 0,
        afterExerciseIndex: 0,
        afterSetIndex: 0,
        plannedRestSeconds: 60,
        adjustedRestSeconds: 60,
        startedAt: DateTime.now().toUtc().subtract(const Duration(seconds: 15)),
      ),
    );

    await tester.pumpWidget(
      MaterialApp(
        home: WorkoutExecutionScreen(
          initialSession: session,
          activeUseCases: _activeUseCases(repository, clock),
          closeUseCase: _closeUseCase(repository, clock),
          historyUseCases: _historyUseCases(clock),
          workoutTemplateUseCases: _workoutTemplateUseCases(),
        ),
      ),
    );
    await tester.pump();

    expect(find.text('Repos avant la prochaine série'), findsOneWidget);
    expect(find.text('00:45'), findsOneWidget);
    expect(find.text('Série 2 / 2'), findsOneWidget);

    await tester.tap(find.text('+15 s'));
    await tester.pump();

    expect(find.text('01:00'), findsOneWidget);
    expect(repository.restStates.single.adjustedRestSeconds, 75);

    await tester.tap(find.text('Ignorer le repos'));
    await tester.pump();

    expect(repository.restStates.single.skippedAt, isNotNull);
  });

  testWidgets('un repos en pause est repris sans compter la durée de pause', (
    tester,
  ) async {
    final clock = _FakeClock(DateTime.utc(2026, 7, 17, 12));
    final repository = _FakeActiveSessionRepository();
    final session = ActiveWorkoutSession(
      metadata: _metadata('session-1'),
      sourceWorkoutTemplateId: 'template-1',
      status: ActiveWorkoutStatus.running,
      startedAt: DateTime.utc(2026, 7, 17, 12),
      lastPersistedAt: DateTime.utc(2026, 7, 17, 12),
      elapsedActiveMs: 0,
      currentProgramIndex: 0,
      currentExerciseIndex: 0,
      currentSetIndex: 0,
      resolvedTemplateSnapshotJson: _sessionSnapshot(
        setsCount: 2,
        restSeconds: 60,
      ),
    );
    repository.session = session;
    final now = DateTime.now().toUtc();
    repository.restStates.add(
      ActiveRestState(
        metadata: _metadata('rest-1'),
        activeWorkoutSessionId: 'session-1',
        afterProgramIndex: 0,
        afterExerciseIndex: 0,
        afterSetIndex: 0,
        plannedRestSeconds: 60,
        adjustedRestSeconds: 60,
        startedAt: now.subtract(const Duration(minutes: 10, seconds: 15)),
        pausedAt: now.subtract(const Duration(minutes: 10)),
      ),
    );

    await tester.pumpWidget(
      MaterialApp(
        home: WorkoutExecutionScreen(
          initialSession: session,
          activeUseCases: _activeUseCases(repository, clock),
          closeUseCase: _closeUseCase(repository, clock),
          historyUseCases: _historyUseCases(clock),
          workoutTemplateUseCases: _workoutTemplateUseCases(),
        ),
      ),
    );
    await tester.pump();

    expect(find.text('Repos avant la prochaine série'), findsOneWidget);
    expect(find.text('00:45'), findsOneWidget);
  });

  testWidgets('la mesure temps enregistre une durée réelle de série', (
    tester,
  ) async {
    await tester.binding.setSurfaceSize(const Size(400, 1400));
    addTearDown(() => tester.binding.setSurfaceSize(null));

    final clock = _FakeClock(DateTime.utc(2026, 7, 17, 12));
    final repository = _FakeActiveSessionRepository();
    final session = ActiveWorkoutSession(
      metadata: _metadata('session-1'),
      sourceWorkoutTemplateId: 'template-1',
      status: ActiveWorkoutStatus.running,
      startedAt: DateTime.utc(2026, 7, 17, 12),
      lastPersistedAt: DateTime.utc(2026, 7, 17, 12),
      elapsedActiveMs: 0,
      currentProgramIndex: 0,
      currentExerciseIndex: 0,
      currentSetIndex: 0,
      resolvedTemplateSnapshotJson: _sessionSnapshot(
        setsCount: 2,
        restSeconds: 0,
      ),
    );
    repository.session = session;

    await tester.pumpWidget(
      MaterialApp(
        home: WorkoutExecutionScreen(
          initialSession: session,
          activeUseCases: _activeUseCases(repository, clock),
          closeUseCase: _closeUseCase(repository, clock),
          historyUseCases: _historyUseCases(clock),
          workoutTemplateUseCases: _workoutTemplateUseCases(),
        ),
      ),
    );
    await tester.tap(find.widgetWithText(FilledButton, 'Démarrer').first);
    await tester.pump();
    clock.value = DateTime.utc(2026, 7, 17, 12, 0, 2);
    await tester.pump(const Duration(seconds: 2));
    await tester.tap(find.text('Terminer la série'));
    await tester.pump();

    expect(repository.results.single.actualTimeMs, 2000);
  });

  testWidgets(
    'terminer la série 2 avec temps répétitions et score manuel avance',
    (tester) async {
      await tester.binding.setSurfaceSize(const Size(400, 1400));
      addTearDown(() => tester.binding.setSurfaceSize(null));

      final clock = _FakeClock(DateTime.utc(2026, 7, 17, 12));
      final repository = _FakeActiveSessionRepository();
      final session = ActiveWorkoutSession(
        metadata: _metadata('session-1'),
        sourceWorkoutTemplateId: 'template-1',
        status: ActiveWorkoutStatus.running,
        startedAt: DateTime.utc(2026, 7, 17, 12),
        lastPersistedAt: DateTime.utc(2026, 7, 17, 12),
        elapsedActiveMs: 0,
        currentProgramIndex: 0,
        currentExerciseIndex: 0,
        currentSetIndex: 1,
        resolvedTemplateSnapshotJson: _sessionSnapshot(
          setsCount: 3,
          restSeconds: 0,
          targetTimeSeconds: 30,
          targetReps: 5,
          targetScore: 10,
          scoreUnit: 'points',
        ),
      );
      repository.session = session;

      await tester.pumpWidget(
        MaterialApp(
          home: WorkoutExecutionScreen(
            initialSession: session,
            activeUseCases: _activeUseCases(repository, clock),
            closeUseCase: _closeUseCase(repository, clock),
            historyUseCases: _historyUseCases(clock),
            workoutTemplateUseCases: _workoutTemplateUseCases(),
          ),
        ),
      );

      expect(find.text('Programme jambes'), findsOneWidget);
      expect(find.text('2 / 3'), findsOneWidget);
      expect(find.text('Squat'), findsOneWidget);
      expect(find.text('Temps de série : 00:30'), findsOneWidget);

      await tester.tap(find.widgetWithText(FilledButton, 'Démarrer').first);
      await tester.pump();

      for (var i = 0; i < 2; i++) {
        await tester.tap(find.byIcon(Icons.remove));
        await tester.pump();
      }
      await tester.enterText(
        find.widgetWithText(TextField, 'Score (points)'),
        '5',
      );
      await tester.ensureVisible(find.text('Terminer la série'));
      await tester.pumpAndSettle();
      await tester.tap(find.text('Terminer la série'));
      await tester.pumpAndSettle();

      expect(repository.results.single.setIndex, 1);
      expect(repository.results.single.actualReps, 3);
      expect(repository.results.single.actualScore, 5);
      expect(repository.session?.currentSetIndex, 2);
      expect(find.text('Programme jambes'), findsOneWidget);
      expect(find.text('3 / 3'), findsOneWidget);
      expect(find.text('5'), findsOneWidget);
    },
  );

  testWidgets('la flèche de retour met la séance active en pause', (
    tester,
  ) async {
    final clock = _FakeClock(DateTime.utc(2026, 7, 17, 12));
    final repository = _FakeActiveSessionRepository();
    final session = ActiveWorkoutSession(
      metadata: _metadata('session-1'),
      sourceWorkoutTemplateId: 'template-1',
      status: ActiveWorkoutStatus.running,
      startedAt: DateTime.utc(2026, 7, 17, 12),
      lastPersistedAt: DateTime.utc(2026, 7, 17, 12),
      elapsedActiveMs: 0,
      currentProgramIndex: 0,
      currentExerciseIndex: 0,
      currentSetIndex: 0,
      resolvedTemplateSnapshotJson: _sessionSnapshot(
        setsCount: 2,
        restSeconds: 0,
      ),
    );
    repository.session = session;

    await tester.pumpWidget(
      MaterialApp(
        home: WorkoutExecutionScreen(
          initialSession: session,
          activeUseCases: _activeUseCases(repository, clock),
          closeUseCase: _closeUseCase(repository, clock),
          historyUseCases: _historyUseCases(clock),
          workoutTemplateUseCases: _workoutTemplateUseCases(),
        ),
      ),
    );

    await tester.tap(find.byTooltip('Mettre en pause'));
    await tester.pumpAndSettle();

    expect(find.text('Reprendre'), findsOneWidget);
    expect(repository.session?.status, ActiveWorkoutStatus.paused);
  });

  testWidgets('le plan de séance affiche les états des séries', (tester) async {
    final clock = _FakeClock(DateTime.utc(2026, 7, 17, 12));
    final repository = _FakeActiveSessionRepository();
    final session = ActiveWorkoutSession(
      metadata: _metadata('session-1'),
      sourceWorkoutTemplateId: 'template-1',
      status: ActiveWorkoutStatus.running,
      startedAt: DateTime.utc(2026, 7, 17, 12),
      lastPersistedAt: DateTime.utc(2026, 7, 17, 12),
      elapsedActiveMs: 0,
      currentProgramIndex: 0,
      currentExerciseIndex: 0,
      currentSetIndex: 2,
      resolvedTemplateSnapshotJson: _sessionSnapshot(
        setsCount: 4,
        restSeconds: 0,
      ),
    );
    repository.session = session;
    repository.results.add(
      ActiveSetResult(
        metadata: _metadata('result-1'),
        activeWorkoutSessionId: 'session-1',
        programSnapshotId: 'template-program-1',
        exerciseSnapshotId: 'exercise-1',
        programIndex: 0,
        exerciseIndex: 0,
        setIndex: 0,
        actualReps: 10,
        actualScore: 7,
        scoreUnitSnapshot: 'paniers',
      ),
    );

    await tester.pumpWidget(
      MaterialApp(
        home: WorkoutExecutionScreen(
          initialSession: session,
          activeUseCases: _activeUseCases(repository, clock),
          closeUseCase: _closeUseCase(repository, clock),
          historyUseCases: _historyUseCases(clock),
          workoutTemplateUseCases: _workoutTemplateUseCases(),
        ),
      ),
    );

    await tester.tap(find.byTooltip('Voir le plan'));
    await tester.pumpAndSettle();

    expect(find.text('Plan de séance'), findsOneWidget);
    expect(find.text('Terminée'), findsOneWidget);
    expect(find.text('10 reps · 7 paniers'), findsOneWidget);
    expect(find.text('Passée'), findsOneWidget);
    expect(find.text('Aucun résultat'), findsOneWidget);
    expect(find.text('En cours'), findsOneWidget);
    expect(find.text('À faire'), findsOneWidget);

    await tester.tap(find.text('En cours'));
    await tester.pumpAndSettle();

    expect(find.text('Plan de séance'), findsNothing);
  });

  testWidgets('le plan affiche le score chrono formaté avec objectif', (
    tester,
  ) async {
    final clock = _FakeClock(DateTime.utc(2026, 7, 17, 12));
    final repository = _FakeActiveSessionRepository();
    final session = ActiveWorkoutSession(
      metadata: _metadata('session-1'),
      sourceWorkoutTemplateId: 'template-1',
      status: ActiveWorkoutStatus.running,
      startedAt: DateTime.utc(2026, 7, 17, 12),
      lastPersistedAt: DateTime.utc(2026, 7, 17, 12),
      elapsedActiveMs: 0,
      currentProgramIndex: 0,
      currentExerciseIndex: 0,
      currentSetIndex: 1,
      resolvedTemplateSnapshotJson: _sessionSnapshot(
        setsCount: 2,
        restSeconds: 0,
        scoreInputMode: ScoreInputMode.stopwatch,
        targetScoreTimeMs: 45000,
      ),
    );
    repository.session = session;
    repository.results.add(
      ActiveSetResult(
        metadata: _metadata('result-1'),
        activeWorkoutSessionId: 'session-1',
        programSnapshotId: 'template-program-1',
        exerciseSnapshotId: 'exercise-1',
        programIndex: 0,
        exerciseIndex: 0,
        setIndex: 0,
        actualScoreTimeMs: 38700,
        scoreInputModeSnapshot: ScoreInputMode.stopwatch,
      ),
    );

    await tester.pumpWidget(
      MaterialApp(
        home: WorkoutExecutionScreen(
          initialSession: session,
          activeUseCases: _activeUseCases(repository, clock),
          closeUseCase: _closeUseCase(repository, clock),
          historyUseCases: _historyUseCases(clock),
          workoutTemplateUseCases: _workoutTemplateUseCases(),
        ),
      ),
    );

    await tester.tap(find.byTooltip('Voir le plan'));
    await tester.pumpAndSettle();

    expect(find.text('Score chrono 00:38.7 / objectif 00:45'), findsOneWidget);
    expect(find.textContaining('Temps réalisé 00:38.7'), findsNothing);
  });

  testWidgets('le plan affiche le résumé de séquence d’une série', (
    tester,
  ) async {
    final clock = _FakeClock(DateTime.utc(2026, 7, 17, 12));
    final repository = _FakeActiveSessionRepository();
    final session = ActiveWorkoutSession(
      metadata: _metadata('session-1'),
      sourceWorkoutTemplateId: 'template-1',
      status: ActiveWorkoutStatus.running,
      startedAt: DateTime.utc(2026, 7, 17, 12),
      lastPersistedAt: DateTime.utc(2026, 7, 17, 12),
      elapsedActiveMs: 0,
      currentProgramIndex: 0,
      currentExerciseIndex: 0,
      currentSetIndex: 0,
      resolvedTemplateSnapshotJson: _sessionSnapshot(
        setsCount: 2,
        targetReps: 2,
        exerciseSteps: [
          _step('step-1', 0, 'Gauche', ExerciseStepType.reps, 4),
          _step('step-2', 1, 'Droite', ExerciseStepType.reps, 4),
        ],
      ),
    );
    repository.session = session;
    repository.stepResults.add(
      ActiveExerciseStepResult(
        metadata: _metadata('step-result-1'),
        activeWorkoutSessionId: 'session-1',
        programSnapshotId: 'template-program-1',
        exerciseSnapshotId: 'exercise-1',
        programIndex: 0,
        exerciseIndex: 0,
        setIndex: 0,
        passageIndex: 0,
        stepIndex: 0,
        stepSnapshotId: 'step-1',
        stepNameSnapshot: 'Gauche',
        stepTypeSnapshot: ExerciseStepType.reps,
        targetValueSnapshot: 4,
        hasScoreSnapshot: false,
        status: SetResultStatus.completed,
        actualReps: 4,
      ),
    );

    await tester.pumpWidget(
      MaterialApp(
        home: WorkoutExecutionScreen(
          initialSession: session,
          activeUseCases: _activeUseCases(repository, clock),
          stepUseCases: _stepUseCases(repository, clock),
          stepAudioCuePlayer: _FakeStepAudioCuePlayer(),
          closeUseCase: _closeUseCase(repository, clock),
          historyUseCases: _historyUseCases(clock),
          workoutTemplateUseCases: _workoutTemplateUseCases(),
        ),
      ),
    );

    await tester.tap(find.byTooltip('Voir le plan'));
    await tester.pumpAndSettle();

    expect(find.text('Séquence : 1/2 étapes · Passage 1/2'), findsOneWidget);
  });

  testWidgets(
    'modifier une série passée la termine sans changer la position courante',
    (tester) async {
      final clock = _FakeClock(DateTime.utc(2026, 7, 17, 12));
      final repository = _FakeActiveSessionRepository();
      final session = ActiveWorkoutSession(
        metadata: _metadata('session-1'),
        sourceWorkoutTemplateId: 'template-1',
        status: ActiveWorkoutStatus.running,
        startedAt: DateTime.utc(2026, 7, 17, 12),
        lastPersistedAt: DateTime.utc(2026, 7, 17, 12),
        elapsedActiveMs: 0,
        currentProgramIndex: 0,
        currentExerciseIndex: 0,
        currentSetIndex: 2,
        resolvedTemplateSnapshotJson: _sessionSnapshot(
          setsCount: 4,
          restSeconds: 0,
        ),
      );
      repository.session = session;

      await tester.pumpWidget(
        MaterialApp(
          home: WorkoutExecutionScreen(
            initialSession: session,
            activeUseCases: _activeUseCases(repository, clock),
            closeUseCase: _closeUseCase(repository, clock),
            historyUseCases: _historyUseCases(clock),
            workoutTemplateUseCases: _workoutTemplateUseCases(),
          ),
        ),
      );

      await tester.tap(find.byTooltip('Voir le plan'));
      await tester.pumpAndSettle();
      await tester.tap(find.widgetWithText(ListTile, 'Série 2'));
      await tester.pumpAndSettle();

      expect(find.text('Modifier la série'), findsOneWidget);

      await tester.enterText(_textFieldWithLabel('Répétitions'), '12');
      await tester.ensureVisible(find.text('Enregistrer le résultat'));
      await tester.tap(find.text('Enregistrer le résultat'));
      await tester.pumpAndSettle();

      expect(repository.session?.currentSetIndex, 2);
      expect(repository.results.single.status, SetResultStatus.completed);
      expect(repository.results.single.actualReps, 12);
      expect(find.text('Série mise à jour'), findsWidgets);
      expect(find.text('12 reps'), findsOneWidget);

      await tester.tap(find.byTooltip('Fermer'));
      await tester.pumpAndSettle();

      expect(find.text('Programme jambes'), findsOneWidget);
      expect(find.text('3 / 4'), findsOneWidget);
    },
  );

  testWidgets(
    'toucher la série courante dans le plan revient à l’écran principal',
    (tester) async {
      final clock = _FakeClock(DateTime.utc(2026, 7, 17, 12));
      final repository = _FakeActiveSessionRepository();
      final session = ActiveWorkoutSession(
        metadata: _metadata('session-1'),
        sourceWorkoutTemplateId: 'template-1',
        status: ActiveWorkoutStatus.running,
        startedAt: DateTime.utc(2026, 7, 17, 12),
        lastPersistedAt: DateTime.utc(2026, 7, 17, 12),
        elapsedActiveMs: 0,
        currentProgramIndex: 0,
        currentExerciseIndex: 0,
        currentSetIndex: 1,
        resolvedTemplateSnapshotJson: _sessionSnapshot(
          setsCount: 3,
          restSeconds: 0,
        ),
      );
      repository.session = session;

      await tester.pumpWidget(
        MaterialApp(
          home: WorkoutExecutionScreen(
            initialSession: session,
            activeUseCases: _activeUseCases(repository, clock),
            closeUseCase: _closeUseCase(repository, clock),
            historyUseCases: _historyUseCases(clock),
            workoutTemplateUseCases: _workoutTemplateUseCases(),
          ),
        ),
      );

      await tester.tap(find.byTooltip('Voir le plan'));
      await tester.pumpAndSettle();
      await tester.tap(find.text('En cours'));
      await tester.pumpAndSettle();

      expect(find.text('Plan de séance'), findsNothing);
      expect(find.text('Modifier la série'), findsNothing);
      expect(find.text('Programme jambes'), findsOneWidget);
      expect(find.text('2 / 3'), findsOneWidget);
    },
  );

  testWidgets('le module séquence s’affiche pour un exercice avec étapes', (
    tester,
  ) async {
    await tester.binding.setSurfaceSize(const Size(400, 1600));
    addTearDown(() => tester.binding.setSurfaceSize(null));

    final clock = _FakeClock(DateTime.utc(2026, 7, 17, 12));
    final repository = _FakeActiveSessionRepository();
    final session = ActiveWorkoutSession(
      metadata: _metadata('session-1'),
      sourceWorkoutTemplateId: 'template-1',
      status: ActiveWorkoutStatus.running,
      startedAt: DateTime.utc(2026, 7, 17, 12),
      lastPersistedAt: DateTime.utc(2026, 7, 17, 12),
      elapsedActiveMs: 0,
      currentProgramIndex: 0,
      currentExerciseIndex: 0,
      currentSetIndex: 0,
      resolvedTemplateSnapshotJson: _sessionSnapshot(
        targetReps: 10,
        exerciseSteps: [
          _step('step-1', 0, 'Dribble main droite', ExerciseStepType.time, 10),
          _step('step-2', 1, 'Pompes', ExerciseStepType.reps, 8),
        ],
      ),
    );
    repository.session = session;

    await tester.pumpWidget(
      MaterialApp(
        home: WorkoutExecutionScreen(
          initialSession: session,
          activeUseCases: _activeUseCases(repository, clock),
          stepUseCases: _stepUseCases(repository, clock),
          stepAudioCuePlayer: _FakeStepAudioCuePlayer(),
          closeUseCase: _closeUseCase(repository, clock),
          historyUseCases: _historyUseCases(clock),
          workoutTemplateUseCases: _workoutTemplateUseCases(),
        ),
      ),
    );
    await tester.pump();

    expect(find.byType(ListView), findsNothing);
    expect(find.text('SÉQUENCE'), findsOneWidget);
    expect(find.text('Passage 1 / 10'), findsOneWidget);
    expect(find.text('ÉTAPE 1 / 2'), findsOneWidget);
    expect(find.text('Dribble main droite'), findsOneWidget);
    expect(find.text('00:10'), findsOneWidget);
  });

  testWidgets('l’écran actif avec séquence longue reste sans scroll', (
    tester,
  ) async {
    await tester.binding.setSurfaceSize(const Size(400, 900));
    addTearDown(() => tester.binding.setSurfaceSize(null));

    final clock = _FakeClock(DateTime.utc(2026, 7, 17, 12));
    final repository = _FakeActiveSessionRepository();
    final session = ActiveWorkoutSession(
      metadata: _metadata('session-1'),
      sourceWorkoutTemplateId: 'template-1',
      status: ActiveWorkoutStatus.running,
      startedAt: DateTime.utc(2026, 7, 17, 12),
      lastPersistedAt: DateTime.utc(2026, 7, 17, 12),
      elapsedActiveMs: 0,
      currentProgramIndex: 0,
      currentExerciseIndex: 0,
      currentSetIndex: 0,
      resolvedTemplateSnapshotJson: _sessionSnapshot(
        setsCount: 4,
        workoutName: 'Séance tirs du soir très complète',
        programName:
            'Programme finition longue avec nom volontairement interminable',
        exerciseName: 'Dribble combo finition contact côté faible très long',
        targetReps: 10,
        scoreUnit: 'paniers',
        exerciseSteps: [
          _step(
            'step-1',
            0,
            'Dribble main droite enchaîné avec finition longue',
            ExerciseStepType.time,
            90,
          ),
          _step('step-2', 1, 'Appuis explosifs', ExerciseStepType.reps, 8),
          _step('step-3', 2, 'Tir après contact', ExerciseStepType.reps, 6),
        ],
      ),
    );
    repository.session = session;

    await tester.pumpWidget(
      MaterialApp(
        home: WorkoutExecutionScreen(
          initialSession: session,
          activeUseCases: _activeUseCases(repository, clock),
          stepUseCases: _stepUseCases(repository, clock),
          stepAudioCuePlayer: _FakeStepAudioCuePlayer(),
          closeUseCase: _closeUseCase(repository, clock),
          historyUseCases: _historyUseCases(clock),
          workoutTemplateUseCases: _workoutTemplateUseCases(),
        ),
      ),
    );
    await tester.pump();

    expect(tester.takeException(), isNull);
    expect(find.byType(ListView), findsNothing);
    expect(find.text('SÉRIE'), findsOneWidget);
    expect(find.text('Passage 1 / 10'), findsOneWidget);
    expect(find.text('Objectif : 1:30'), findsOneWidget);
    expect(find.widgetWithText(FilledButton, 'Démarrer'), findsOneWidget);
    expect(find.widgetWithText(FilledButton, 'Lancer'), findsOneWidget);
    expect(find.textContaining('Passages réalisés'), findsNothing);
  });

  testWidgets('une étape répétitions peut être validée', (tester) async {
    await tester.binding.setSurfaceSize(const Size(400, 1600));
    addTearDown(() => tester.binding.setSurfaceSize(null));

    final clock = _FakeClock(DateTime.utc(2026, 7, 17, 12));
    final repository = _FakeActiveSessionRepository();
    final session = ActiveWorkoutSession(
      metadata: _metadata('session-1'),
      sourceWorkoutTemplateId: 'template-1',
      status: ActiveWorkoutStatus.running,
      startedAt: DateTime.utc(2026, 7, 17, 12),
      lastPersistedAt: DateTime.utc(2026, 7, 17, 12),
      elapsedActiveMs: 0,
      currentProgramIndex: 0,
      currentExerciseIndex: 0,
      currentSetIndex: 0,
      resolvedTemplateSnapshotJson: _sessionSnapshot(
        targetReps: 1,
        exerciseSteps: [_step('step-1', 0, 'Pompes', ExerciseStepType.reps, 8)],
      ),
    );
    repository.session = session;

    await tester.pumpWidget(
      MaterialApp(
        home: WorkoutExecutionScreen(
          initialSession: session,
          activeUseCases: _activeUseCases(repository, clock),
          stepUseCases: _stepUseCases(repository, clock),
          stepAudioCuePlayer: _FakeStepAudioCuePlayer(),
          closeUseCase: _closeUseCase(repository, clock),
          historyUseCases: _historyUseCases(clock),
          workoutTemplateUseCases: _workoutTemplateUseCases(),
        ),
      ),
    );
    await tester.pump();

    expect(find.text('8'), findsWidgets);
    await tester.tap(find.text('Étape suivante'));
    await tester.pump();

    expect(repository.stepResults.single.actualReps, 8);
    expect(find.text('Séquence terminée'), findsWidgets);
  });

  testWidgets('une étape temps affiche un décompte recalculé', (tester) async {
    await tester.binding.setSurfaceSize(const Size(400, 1600));
    addTearDown(() => tester.binding.setSurfaceSize(null));

    final clock = _FakeClock(DateTime.utc(2026, 7, 17, 12));
    final repository = _FakeActiveSessionRepository();
    final audio = _FakeStepAudioCuePlayer();
    final session = ActiveWorkoutSession(
      metadata: _metadata('session-1'),
      sourceWorkoutTemplateId: 'template-1',
      status: ActiveWorkoutStatus.running,
      startedAt: DateTime.utc(2026, 7, 17, 12),
      lastPersistedAt: DateTime.utc(2026, 7, 17, 12),
      elapsedActiveMs: 0,
      currentProgramIndex: 0,
      currentExerciseIndex: 0,
      currentSetIndex: 0,
      resolvedTemplateSnapshotJson: _sessionSnapshot(
        targetReps: 1,
        exerciseSteps: [
          _step('step-1', 0, 'Gainage', ExerciseStepType.time, 5),
        ],
      ),
    );
    repository.session = session;

    await tester.pumpWidget(
      MaterialApp(
        home: WorkoutExecutionScreen(
          initialSession: session,
          activeUseCases: _activeUseCases(repository, clock),
          stepUseCases: _stepUseCases(repository, clock),
          stepAudioCuePlayer: audio,
          closeUseCase: _closeUseCase(repository, clock),
          historyUseCases: _historyUseCases(clock),
          workoutTemplateUseCases: _workoutTemplateUseCases(),
        ),
      ),
    );
    await tester.pump();

    expect(find.text('00:05'), findsOneWidget);
    await tester.tap(find.widgetWithText(FilledButton, 'Lancer'));
    await tester.pump();
    clock.value = DateTime.utc(2026, 7, 17, 12, 0, 2);
    await tester.pump(const Duration(milliseconds: 100));

    expect(find.text('00:03'), findsOneWidget);
    expect(audio.shortBeeps, 1);
  });

  testWidgets(
    'terminer sans démarrer la première étape temps affiche une confirmation',
    (tester) async {
      await tester.binding.setSurfaceSize(const Size(400, 1600));
      addTearDown(() => tester.binding.setSurfaceSize(null));

      final clock = _FakeClock(DateTime.utc(2026, 7, 17, 12));
      final repository = _FakeActiveSessionRepository();
      final audio = _FakeStepAudioCuePlayer();
      final session = ActiveWorkoutSession(
        metadata: _metadata('session-1'),
        sourceWorkoutTemplateId: 'template-1',
        status: ActiveWorkoutStatus.running,
        startedAt: DateTime.utc(2026, 7, 17, 12),
        lastPersistedAt: DateTime.utc(2026, 7, 17, 12),
        elapsedActiveMs: 0,
        currentProgramIndex: 0,
        currentExerciseIndex: 0,
        currentSetIndex: 0,
        resolvedTemplateSnapshotJson: _sessionSnapshot(
          timeEnabled: false,
          scoreEnabled: false,
          targetReps: 1,
          exerciseSteps: [
            _step('step-1', 0, 'Gainage', ExerciseStepType.time, 5),
          ],
        ),
      );
      repository.session = session;

      await tester.pumpWidget(
        MaterialApp(
          home: WorkoutExecutionScreen(
            initialSession: session,
            activeUseCases: _activeUseCases(repository, clock),
            stepUseCases: _stepUseCases(repository, clock),
            stepAudioCuePlayer: audio,
            closeUseCase: _closeUseCase(repository, clock),
            historyUseCases: _historyUseCases(clock),
            workoutTemplateUseCases: _workoutTemplateUseCases(),
          ),
        ),
      );
      await tester.pump();

      await tester.tap(find.text('Terminer la série'));
      await tester.pumpAndSettle();

      expect(find.text('Chrono non lancé'), findsOneWidget);
      expect(
        find.text('Tu n’as pas démarré le chrono de cette série.'),
        findsOneWidget,
      );
      expect(find.widgetWithText(TextButton, 'Démarrer'), findsOneWidget);
      expect(find.text('Terminer sans chrono'), findsOneWidget);
      expect(repository.results, isEmpty);
    },
  );

  testWidgets('deux étapes temps consécutives s’enchaînent automatiquement', (
    tester,
  ) async {
    await tester.binding.setSurfaceSize(const Size(400, 1600));
    addTearDown(() => tester.binding.setSurfaceSize(null));

    final clock = _FakeClock(DateTime.utc(2026, 7, 17, 12));
    final repository = _FakeActiveSessionRepository();
    final audio = _FakeStepAudioCuePlayer();
    final session = ActiveWorkoutSession(
      metadata: _metadata('session-1'),
      sourceWorkoutTemplateId: 'template-1',
      status: ActiveWorkoutStatus.running,
      startedAt: DateTime.utc(2026, 7, 17, 12),
      lastPersistedAt: DateTime.utc(2026, 7, 17, 12),
      elapsedActiveMs: 0,
      currentProgramIndex: 0,
      currentExerciseIndex: 0,
      currentSetIndex: 0,
      resolvedTemplateSnapshotJson: _sessionSnapshot(
        targetReps: 1,
        exerciseSteps: [
          _step('step-1', 0, 'Gauche', ExerciseStepType.time, 1),
          _step('step-2', 1, 'Droite', ExerciseStepType.time, 2),
        ],
      ),
    );
    repository.session = session;

    await tester.pumpWidget(
      MaterialApp(
        home: WorkoutExecutionScreen(
          initialSession: session,
          activeUseCases: _activeUseCases(repository, clock),
          stepUseCases: _stepUseCases(repository, clock),
          stepAudioCuePlayer: audio,
          closeUseCase: _closeUseCase(repository, clock),
          historyUseCases: _historyUseCases(clock),
          workoutTemplateUseCases: _workoutTemplateUseCases(),
        ),
      ),
    );
    await tester.pump();

    await tester.tap(find.widgetWithText(FilledButton, 'Lancer'));
    await tester.pump();
    clock.value = DateTime.utc(2026, 7, 17, 12, 0, 1, 200);
    await tester.pump(const Duration(milliseconds: 200));
    await tester.pump();

    expect(repository.stepResults.single.stepIndex, 0);
    expect(repository.stepResults.single.status, SetResultStatus.completed);
    expect(find.text('ÉTAPE 2 / 2'), findsOneWidget);
    expect(find.text('Droite'), findsOneWidget);
    expect(find.text('00:02'), findsOneWidget);
    expect(
      repository.stepProgressStates.single.status,
      ActiveExerciseStepProgressStatus.runningTimer,
    );
    expect(audio.longBeeps, 1);
  });

  testWidgets(
    'chrono suivant prêt attend un démarrage manuel quand l’enchaînement est désactivé',
    (tester) async {
      await tester.binding.setSurfaceSize(const Size(400, 1600));
      addTearDown(() => tester.binding.setSurfaceSize(null));

      final clock = _FakeClock(DateTime.utc(2026, 7, 17, 12));
      final repository = _FakeActiveSessionRepository();
      final session = ActiveWorkoutSession(
        metadata: _metadata('session-1'),
        sourceWorkoutTemplateId: 'template-1',
        status: ActiveWorkoutStatus.running,
        startedAt: DateTime.utc(2026, 7, 17, 12),
        lastPersistedAt: DateTime.utc(2026, 7, 17, 12),
        elapsedActiveMs: 0,
        currentProgramIndex: 0,
        currentExerciseIndex: 0,
        currentSetIndex: 0,
        resolvedTemplateSnapshotJson: _sessionSnapshot(
          targetReps: 1,
          autoStartNextTimedStep: false,
          exerciseSteps: [
            _step('step-1', 0, 'Gauche', ExerciseStepType.time, 1),
            _step('step-2', 1, 'Droite', ExerciseStepType.time, 2),
          ],
        ),
      );
      repository.session = session;

      await tester.pumpWidget(
        MaterialApp(
          home: WorkoutExecutionScreen(
            initialSession: session,
            activeUseCases: _activeUseCases(repository, clock),
            stepUseCases: _stepUseCases(repository, clock),
            stepAudioCuePlayer: _FakeStepAudioCuePlayer(),
            closeUseCase: _closeUseCase(repository, clock),
            historyUseCases: _historyUseCases(clock),
            workoutTemplateUseCases: _workoutTemplateUseCases(),
          ),
        ),
      );
      await tester.pump();

      await tester.tap(find.widgetWithText(FilledButton, 'Lancer'));
      await tester.pump();
      clock.value = DateTime.utc(2026, 7, 17, 12, 0, 1, 200);
      await tester.pump(const Duration(milliseconds: 200));
      await tester.pump();

      expect(repository.stepResults.single.stepIndex, 0);
      expect(find.text('ÉTAPE 2 / 2'), findsOneWidget);
      expect(find.text('Droite'), findsOneWidget);
      expect(find.text('00:02'), findsOneWidget);
      expect(find.text('Chrono prêt'), findsOneWidget);
      expect(find.widgetWithText(FilledButton, 'Lancer'), findsOneWidget);
      expect(
        repository.stepProgressStates.single.status,
        ActiveExerciseStepProgressStatus.stoppedTimer,
      );
      expect(repository.stepProgressStates.single.startedAt, isNull);

      await tester.tap(find.widgetWithText(FilledButton, 'Lancer'));
      await tester.pump();

      expect(
        repository.stepProgressStates.single.status,
        ActiveExerciseStepProgressStatus.runningTimer,
      );
      expect(repository.stepProgressStates.single.startedAt, clock.value);
    },
  );

  testWidgets('la séquence passe au deuxième passage si répétitions > 1', (
    tester,
  ) async {
    await tester.binding.setSurfaceSize(const Size(400, 1600));
    addTearDown(() => tester.binding.setSurfaceSize(null));

    final clock = _FakeClock(DateTime.utc(2026, 7, 17, 12));
    final repository = _FakeActiveSessionRepository();
    final session = ActiveWorkoutSession(
      metadata: _metadata('session-1'),
      sourceWorkoutTemplateId: 'template-1',
      status: ActiveWorkoutStatus.running,
      startedAt: DateTime.utc(2026, 7, 17, 12),
      lastPersistedAt: DateTime.utc(2026, 7, 17, 12),
      elapsedActiveMs: 0,
      currentProgramIndex: 0,
      currentExerciseIndex: 0,
      currentSetIndex: 0,
      resolvedTemplateSnapshotJson: _sessionSnapshot(
        targetReps: 2,
        exerciseSteps: [_step('step-1', 0, 'Pompes', ExerciseStepType.reps, 4)],
      ),
    );
    repository.session = session;

    await tester.pumpWidget(
      MaterialApp(
        home: WorkoutExecutionScreen(
          initialSession: session,
          activeUseCases: _activeUseCases(repository, clock),
          stepUseCases: _stepUseCases(repository, clock),
          stepAudioCuePlayer: _FakeStepAudioCuePlayer(),
          closeUseCase: _closeUseCase(repository, clock),
          historyUseCases: _historyUseCases(clock),
          workoutTemplateUseCases: _workoutTemplateUseCases(),
        ),
      ),
    );
    await tester.pump();
    await tester.tap(find.text('Étape suivante'));
    await tester.pump();

    expect(find.text('Passage 2 / 2'), findsOneWidget);
    expect(find.textContaining('Passages réalisés'), findsNothing);
  });

  testWidgets('terminer la série reste utilisable avant la fin de séquence', (
    tester,
  ) async {
    await tester.binding.setSurfaceSize(const Size(400, 1800));
    addTearDown(() => tester.binding.setSurfaceSize(null));

    final clock = _FakeClock(DateTime.utc(2026, 7, 17, 12));
    final repository = _FakeActiveSessionRepository();
    final session = ActiveWorkoutSession(
      metadata: _metadata('session-1'),
      sourceWorkoutTemplateId: 'template-1',
      status: ActiveWorkoutStatus.running,
      startedAt: DateTime.utc(2026, 7, 17, 12),
      lastPersistedAt: DateTime.utc(2026, 7, 17, 12),
      elapsedActiveMs: 0,
      currentProgramIndex: 0,
      currentExerciseIndex: 0,
      currentSetIndex: 0,
      resolvedTemplateSnapshotJson: _sessionSnapshot(
        setsCount: 2,
        restSeconds: 0,
        targetReps: 2,
        exerciseSteps: [_step('step-1', 0, 'Pompes', ExerciseStepType.reps, 4)],
      ),
    );
    repository.session = session;

    await tester.pumpWidget(
      MaterialApp(
        home: WorkoutExecutionScreen(
          initialSession: session,
          activeUseCases: _activeUseCases(repository, clock),
          stepUseCases: _stepUseCases(repository, clock),
          stepAudioCuePlayer: _FakeStepAudioCuePlayer(),
          closeUseCase: _closeUseCase(repository, clock),
          historyUseCases: _historyUseCases(clock),
          workoutTemplateUseCases: _workoutTemplateUseCases(),
        ),
      ),
    );
    await tester.pump();

    await tester.ensureVisible(find.text('Terminer la série'));
    await tester.tap(find.text('Terminer la série'));
    await tester.pumpAndSettle();

    expect(repository.results.single.setIndex, 0);
    expect(repository.session?.currentSetIndex, 1);
  });

  testWidgets('les trois niveaux de skip de séquence fonctionnent', (
    tester,
  ) async {
    await tester.binding.setSurfaceSize(const Size(400, 1800));
    addTearDown(() => tester.binding.setSurfaceSize(null));

    final clock = _FakeClock(DateTime.utc(2026, 7, 17, 12));
    final repository = _FakeActiveSessionRepository();
    final session = ActiveWorkoutSession(
      metadata: _metadata('session-1'),
      sourceWorkoutTemplateId: 'template-1',
      status: ActiveWorkoutStatus.running,
      startedAt: DateTime.utc(2026, 7, 17, 12),
      lastPersistedAt: DateTime.utc(2026, 7, 17, 12),
      elapsedActiveMs: 0,
      currentProgramIndex: 0,
      currentExerciseIndex: 0,
      currentSetIndex: 0,
      resolvedTemplateSnapshotJson: _sessionSnapshot(
        targetReps: 2,
        exerciseSteps: [
          _step('step-1', 0, 'Gauche', ExerciseStepType.reps, 3),
          _step('step-2', 1, 'Droite', ExerciseStepType.reps, 3),
        ],
      ),
    );
    repository.session = session;

    await tester.pumpWidget(
      MaterialApp(
        home: WorkoutExecutionScreen(
          initialSession: session,
          activeUseCases: _activeUseCases(repository, clock),
          stepUseCases: _stepUseCases(repository, clock),
          stepAudioCuePlayer: _FakeStepAudioCuePlayer(),
          closeUseCase: _closeUseCase(repository, clock),
          historyUseCases: _historyUseCases(clock),
          workoutTemplateUseCases: _workoutTemplateUseCases(),
        ),
      ),
    );
    await tester.pump();

    await tester.tap(find.text('Passer l’étape'));
    await tester.pumpAndSettle();
    await tester.tap(find.widgetWithText(FilledButton, 'Passer l’étape'));
    await tester.pumpAndSettle();

    expect(repository.stepResults.single.stepIndex, 0);
    expect(repository.stepResults.single.status, SetResultStatus.skipped);

    await tester.tap(find.byTooltip('Plus d’actions'));
    await tester.pumpAndSettle();
    await tester.tap(find.text('Passer ce passage'));
    await tester.pumpAndSettle();
    await tester.tap(find.widgetWithText(FilledButton, 'Passer ce passage'));
    await tester.pumpAndSettle();

    expect(find.text('Passage 2 / 2'), findsOneWidget);

    await tester.tap(find.byTooltip('Plus d’actions'));
    await tester.pumpAndSettle();
    await tester.tap(find.text('Passer la séquence'));
    await tester.pumpAndSettle();
    await tester.tap(find.widgetWithText(FilledButton, 'Passer la séquence'));
    await tester.pumpAndSettle();

    expect(find.text('Séquence terminée'), findsWidgets);
  });
}

Finder _textFieldWithLabel(String label) {
  return find.byWidgetPredicate(
    (widget) => widget is TextField && widget.decoration?.labelText == label,
    description: 'TextField with label "$label"',
  );
}

Future<File> _createTestImageFile() async {
  final directory = await Directory.systemTemp.createTemp('gametime-media-');
  final file = File('${directory.path}/image.png');
  const transparentPng =
      'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJ'
      'AAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==';
  await file.writeAsBytes(base64Decode(transparentPng));
  return file;
}

ActiveWorkoutSessionUseCases _activeUseCases(
  _FakeActiveSessionRepository repository,
  _FakeClock clock,
) {
  return ActiveWorkoutSessionUseCases(
    sessionRepository: repository,
    templateRepository: _FakeWorkoutTemplateRepository(),
    clock: clock,
    ids: _FakeIds(),
    originDeviceId: 'device-1',
  );
}

CloseWorkoutSessionUseCase _closeUseCase(
  _FakeActiveSessionRepository repository,
  _FakeClock clock,
) {
  return CloseWorkoutSessionUseCase(
    sessionRepository: repository,
    historyRepository: _FakeWorkoutHistoryRepository(),
    clock: clock,
    ids: _FakeIds(),
    originDeviceId: 'device-1',
  );
}

ActiveExerciseStepUseCases _stepUseCases(
  _FakeActiveSessionRepository repository,
  _FakeClock clock,
) {
  return ActiveExerciseStepUseCases(
    sessionRepository: repository,
    clock: clock,
    ids: _FakeIds(),
    originDeviceId: 'device-1',
  );
}

WorkoutHistoryUseCases _historyUseCases(_FakeClock clock) {
  return WorkoutHistoryUseCases(
    repository: _FakeWorkoutHistoryRepository(),
    clock: clock,
  );
}

WorkoutTemplateUseCases _workoutTemplateUseCases() {
  return WorkoutTemplateUseCases(
    templateRepository: _FakeWorkoutTemplateRepository(),
    programRepository: _FakeProgramRepository(),
    clock: _FakeClock(DateTime.utc(2026, 7, 17)),
    ids: _FakeIds(),
    originDeviceId: 'device-1',
  );
}

String _sessionSnapshot({
  int setsCount = 3,
  int restSeconds = 0,
  bool timeEnabled = true,
  bool repsEnabled = true,
  bool scoreEnabled = true,
  int targetTimeSeconds = 45,
  int targetReps = 10,
  double targetScore = 80,
  int? targetScoreTimeMs,
  String scoreUnit = 'kg',
  ScoreInputMode scoreInputMode = ScoreInputMode.manual,
  List<String> imageMediaIds = const [],
  List<Map<String, Object?>> exerciseSteps = const [],
  bool autoStartNextTimedStep = true,
  String? videoMediaId,
  String? sourceExerciseId,
  String workoutName = 'Séance jambes',
  String programName = 'Programme jambes',
  String exerciseName = 'Squat',
}) {
  return jsonEncode({
    'name': workoutName,
    'programs': [
      {
        'id': 'template-program-1',
        'programNameSnapshot': programName,
        'programSnapshotJson': jsonEncode({
          'exercises': [
            {
              'id': 'exercise-1',
              'sourceExerciseId': sourceExerciseId,
              'exerciseNameSnapshot': exerciseName,
              'setsCount': setsCount,
              'timeEnabled': timeEnabled,
              'repsEnabled': repsEnabled,
              'scoreEnabled': scoreEnabled,
              'targetTimeSeconds': targetTimeSeconds,
              'targetReps': targetReps,
              'targetScore': targetScore,
              'targetScoreTimeMs': targetScoreTimeMs,
              'scoreInputModeSnapshot': scoreInputMode.name,
              'scoreLabelSnapshot': 'Score',
              'scoreUnitSnapshot': scoreUnit,
              'restSecondsOverride': restSeconds,
              'exerciseImageMediaIdsSnapshot': imageMediaIds,
              'imageMediaIdsSnapshot': imageMediaIds,
              'exerciseVideoMediaIdSnapshot': videoMediaId,
              'videoMediaIdSnapshot': videoMediaId,
              'exerciseStepsSnapshot': exerciseSteps,
              'autoStartNextTimedStepSnapshot': autoStartNextTimedStep,
            },
          ],
        }),
      },
    ],
    'overrides': const [],
  });
}

Map<String, Object?> _step(
  String id,
  int position,
  String name,
  ExerciseStepType type,
  int defaultTargetValue, {
  bool hasScore = false,
  ScoreInputMode scoreInputMode = ScoreInputMode.manual,
  String? scoreLabel,
  String? scoreUnit,
  double? defaultTargetScore,
  int? defaultTargetScoreTimeMs,
}) {
  return {
    'id': id,
    'position': position,
    'name': name,
    'type': type.name,
    'defaultTargetValue': defaultTargetValue,
    'hasScore': hasScore,
    'scoreInputMode': scoreInputMode.name,
    'scoreLabel': scoreLabel,
    'scoreUnit': scoreUnit,
    'defaultTargetScore': defaultTargetScore,
    'defaultTargetScoreTimeMs': defaultTargetScoreTimeMs,
  };
}

EntityMetadata _metadata(String id) {
  return EntityMetadata(
    id: id,
    createdAt: DateTime.utc(2026, 7, 17),
    updatedAt: DateTime.utc(2026, 7, 17),
    originDeviceId: 'device-1',
  );
}

final class _FakeClock implements Clock {
  _FakeClock(this.value);

  DateTime value;

  @override
  DateTime now() => value;
}

final class _FakeIds implements IdGenerator {
  var _next = 0;

  @override
  String newId() {
    _next += 1;
    return 'id-$_next';
  }
}

final class _FakeStepAudioCuePlayer implements ExerciseStepAudioCuePlayer {
  var shortBeeps = 0;
  var longBeeps = 0;
  var disposed = false;

  @override
  Future<void> playShortCountdownBeep() async {
    shortBeeps += 1;
  }

  @override
  Future<void> playLongCompletionBeep() async {
    longBeeps += 1;
  }

  @override
  Future<void> dispose() async {
    disposed = true;
  }
}

final class _FakeExercisePerformanceReferenceRepository
    implements ExercisePerformanceReferenceRepository {
  const _FakeExercisePerformanceReferenceRepository({this.last, this.record});

  final WorkoutHistorySetPerformance? last;
  final WorkoutHistoryMetricPerformance? record;

  @override
  Future<bool> hasAnyCompletedHistoryForExercise(String exerciseId) async {
    return true;
  }

  @override
  Future<WorkoutHistorySetPerformance?> findLatestSetPerformance({
    required String exerciseId,
    required ActivePerformanceMeasures activeMeasures,
    required int currentSetIndex,
  }) async {
    return last;
  }

  @override
  Future<WorkoutHistoryMetricPerformance?> findBestMetricPerformance({
    required String exerciseId,
    required PerformanceMetric metric,
    required ScoreInputMode scoreInputMode,
  }) async {
    return record;
  }
}

final class _FakeActiveSessionRepository implements ActiveSessionRepository {
  ActiveWorkoutSession? session;
  final results = <ActiveSetResult>[];
  final restStates = <ActiveRestState>[];
  final scoreStopwatchStates = <ActiveScoreStopwatchState>[];
  final setTimerStates = <ActiveSetTimerState>[];
  final stepProgressStates = <ActiveExerciseStepProgressState>[];
  final stepResults = <ActiveExerciseStepResult>[];

  @override
  Future<ActiveWorkoutSession?> findById(String id) async {
    return session?.metadata.id == id ? session : null;
  }

  @override
  Future<ActiveWorkoutSession?> findOpen() async => session;

  @override
  Future<ActiveRestState?> findRestStateById(String id) async {
    for (final restState in restStates) {
      if (restState.metadata.id == id) {
        return restState;
      }
    }
    return null;
  }

  @override
  Future<ActiveScoreStopwatchState?> findScoreStopwatchState({
    required String sessionId,
    required int programIndex,
    required int exerciseIndex,
    required int setIndex,
  }) async {
    for (final state in scoreStopwatchStates) {
      if (state.activeWorkoutSessionId == sessionId &&
          state.programIndex == programIndex &&
          state.exerciseIndex == exerciseIndex &&
          state.setIndex == setIndex &&
          state.metadata.deletedAt == null) {
        return state;
      }
    }
    return null;
  }

  @override
  Future<ActiveSetTimerState?> findSetTimerState({
    required String sessionId,
    required int programIndex,
    required int exerciseIndex,
    required int setIndex,
  }) async {
    for (final state in setTimerStates) {
      if (state.activeWorkoutSessionId == sessionId &&
          state.programIndex == programIndex &&
          state.exerciseIndex == exerciseIndex &&
          state.setIndex == setIndex &&
          state.metadata.deletedAt == null) {
        return state;
      }
    }
    return null;
  }

  @override
  Future<ActiveExerciseStepProgressState?> findExerciseStepProgressState({
    required String sessionId,
    required int programIndex,
    required int exerciseIndex,
    required int setIndex,
  }) async {
    for (final state in stepProgressStates) {
      if (state.activeWorkoutSessionId == sessionId &&
          state.programIndex == programIndex &&
          state.exerciseIndex == exerciseIndex &&
          state.setIndex == setIndex &&
          state.metadata.deletedAt == null) {
        return state;
      }
    }
    return null;
  }

  @override
  Future<List<ActiveRestState>> listRestStates(String sessionId) async {
    return restStates
        .where((restState) => restState.activeWorkoutSessionId == sessionId)
        .toList();
  }

  @override
  Future<List<ActiveScoreStopwatchState>> listScoreStopwatchStates(
    String sessionId,
  ) async {
    return scoreStopwatchStates
        .where((state) => state.activeWorkoutSessionId == sessionId)
        .toList();
  }

  @override
  Future<List<ActiveSetResult>> listSetResults(String sessionId) async {
    return results;
  }

  @override
  Future<List<ActiveSetTimerState>> listSetTimerStates(String sessionId) async {
    return setTimerStates
        .where((state) => state.activeWorkoutSessionId == sessionId)
        .toList();
  }

  @override
  Future<List<ActiveExerciseStepProgressState>> listExerciseStepProgressStates(
    String sessionId,
  ) async {
    return stepProgressStates
        .where((state) => state.activeWorkoutSessionId == sessionId)
        .toList();
  }

  @override
  Future<List<ActiveExerciseStepResult>> listExerciseStepResults(
    String sessionId,
  ) async {
    return stepResults
        .where((result) => result.activeWorkoutSessionId == sessionId)
        .toList();
  }

  @override
  Future<void> save(ActiveWorkoutSession session) async {
    this.session = session;
  }

  @override
  Future<void> saveRestState(ActiveRestState restState) async {
    final index = restStates.indexWhere(
      (saved) => saved.metadata.id == restState.metadata.id,
    );
    if (index == -1) {
      restStates.add(restState);
    } else {
      restStates[index] = restState;
    }
  }

  @override
  Future<void> saveScoreStopwatchState(ActiveScoreStopwatchState state) async {
    final index = scoreStopwatchStates.indexWhere(
      (saved) => saved.metadata.id == state.metadata.id,
    );
    if (index == -1) {
      scoreStopwatchStates.add(state);
    } else {
      scoreStopwatchStates[index] = state;
    }
  }

  @override
  Future<void> saveSetTimerState(ActiveSetTimerState state) async {
    final index = setTimerStates.indexWhere(
      (saved) =>
          saved.activeWorkoutSessionId == state.activeWorkoutSessionId &&
          saved.programIndex == state.programIndex &&
          saved.exerciseIndex == state.exerciseIndex &&
          saved.setIndex == state.setIndex,
    );
    if (index == -1) {
      setTimerStates.add(state);
    } else {
      setTimerStates[index] = state;
    }
  }

  @override
  Future<void> saveExerciseStepProgressState(
    ActiveExerciseStepProgressState state,
  ) async {
    final index = stepProgressStates.indexWhere(
      (saved) =>
          saved.activeWorkoutSessionId == state.activeWorkoutSessionId &&
          saved.programIndex == state.programIndex &&
          saved.exerciseIndex == state.exerciseIndex &&
          saved.setIndex == state.setIndex,
    );
    if (index == -1) {
      stepProgressStates.add(state);
    } else {
      stepProgressStates[index] = state;
    }
  }

  @override
  Future<void> saveExerciseStepResult(ActiveExerciseStepResult result) async {
    final index = stepResults.indexWhere(
      (saved) =>
          saved.activeWorkoutSessionId == result.activeWorkoutSessionId &&
          saved.programIndex == result.programIndex &&
          saved.exerciseIndex == result.exerciseIndex &&
          saved.setIndex == result.setIndex &&
          saved.passageIndex == result.passageIndex &&
          saved.stepIndex == result.stepIndex,
    );
    if (index == -1) {
      stepResults.add(result);
    } else {
      stepResults[index] = result;
    }
  }

  @override
  Future<void> deleteScoreStopwatchState({
    required String sessionId,
    required int programIndex,
    required int exerciseIndex,
    required int setIndex,
    required DateTime deletedAt,
  }) async {
    scoreStopwatchStates.removeWhere(
      (state) =>
          state.activeWorkoutSessionId == sessionId &&
          state.programIndex == programIndex &&
          state.exerciseIndex == exerciseIndex &&
          state.setIndex == setIndex,
    );
  }

  @override
  Future<void> saveSetResult(ActiveSetResult result) async {
    final index = results.indexWhere(
      (saved) =>
          saved.activeWorkoutSessionId == result.activeWorkoutSessionId &&
          saved.programIndex == result.programIndex &&
          saved.exerciseIndex == result.exerciseIndex &&
          saved.setIndex == result.setIndex,
    );
    if (index == -1) {
      results.add(result);
    } else {
      results[index] = result;
    }
  }
}

final class _FakeWorkoutTemplateRepository
    implements WorkoutTemplateRepository {
  @override
  Future<WorkoutTemplate?> findById(String id) async => null;

  @override
  Future<List<WorkoutTemplate>> listActive() async => const [];

  @override
  Future<void> replaceComposition(
    WorkoutTemplate template,
    DateTime deletedAt,
  ) async {}

  @override
  Future<void> save(WorkoutTemplate template) async {}

  @override
  Future<void> saveOverride(WorkoutTemplateExerciseOverride override) async {}

  @override
  Future<void> saveProgram(WorkoutTemplateProgram program) async {}
}

final class _FakeWorkoutHistoryRepository implements WorkoutHistoryRepository {
  @override
  Future<void> delete(String id, DateTime deletedAt) async {}

  @override
  Future<WorkoutHistory?> findById(String id) async => null;

  @override
  Future<List<WorkoutHistory>> listActive() async => const [];

  @override
  Future<void> save(WorkoutHistory history) async {}

  @override
  Future<void> saveSetResult(WorkoutHistorySetResult result) async {}

  @override
  Future<void> saveStepResult(WorkoutHistoryStepResult result) async {}
}

final class _FakeProgramRepository implements ProgramRepository {
  @override
  Future<void> replaceExercises(Program program, DateTime deletedAt) async {}

  @override
  Future<Program?> findById(String id) async => null;

  @override
  Future<List<Program>> listActive() async => const [];

  @override
  Future<void> save(Program program) async {}

  @override
  Future<void> saveExercise(ProgramExercise exercise) async {}
}
