import 'dart:convert'; import 'package:gametime/application/application.dart'; import 'package:gametime/domain/domain.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:watch_bridge_contract/watch_bridge_contract.dart'; void main() { test('projects noActiveSession without an open session', () async { final projector = _projector(_FakeActiveSessionRepository(), _clock()); final projection = await projector.project(revision: 1); expect(projection.phase, WatchSessionPhase.noActiveSession); expect(projection.primaryAction, WatchPrimaryAction.none); expect(projection.phoneReachable, isTrue); }); test('projects ready at the first set before timers start', () async { final repository = _FakeActiveSessionRepository() ..session = _session( currentSetIndex: 0, timeEnabled: true, targetTimeSeconds: 20, steps: [_step(defaultTargetValue: 20)], ); final projector = _projector(repository, _clock()); final projection = await projector.project(revision: 1); expect(projection.phase, WatchSessionPhase.ready); expect(projection.seriesIndex, 1); expect(projection.seriesTotal, 2); expect(projection.exerciseName, 'Squat'); expect(projection.stepIndex, 1); expect(projection.stepTotal, 1); expect(projection.dominantTimer?.kind, WatchTimerKind.step); expect(projection.dominantTimer?.runState, WatchTimerRunState.stopped); expect(projection.dominantTimer?.targetMs, 20000); expect(projection.primaryAction, WatchPrimaryAction.startCurrentExercise); }); test('projects reps step target for direct watch validation', () async { final repository = _FakeActiveSessionRepository() ..session = _session( steps: [_step(type: ExerciseStepType.reps, defaultTargetValue: 12)], ); final projector = _projector(repository, _clock()); final projection = await projector.project(revision: 1); expect(projection.stepType, WatchStepType.reps); expect(projection.stepTargetValue, 12); expect(projection.dominantTimer, isNull); }); test('projects session-level Health Services exercise strategy', () async { final repository = _FakeActiveSessionRepository() ..session = _session( healthServicesExerciseTypeStrategy: const ['WORKOUT'], secondExerciseName: 'Dribble', secondExerciseHealthServicesExerciseTypeStrategy: const [ 'RUNNING', 'HIGH_INTENSITY_INTERVAL_TRAINING', 'WORKOUT', ], ); final projector = _projector(repository, _clock()); final projection = await projector.project(revision: 1); expect(projection.healthServicesExerciseTypeStrategy, [ 'WORKOUT', 'RUNNING', 'HIGH_INTENSITY_INTERVAL_TRAINING', ]); }); test('projects running with step timer before stopwatch score', () async { final now = DateTime.utc(2026, 7, 25, 12); final session = _session( timeEnabled: true, scoreEnabled: true, scoreInputMode: ScoreInputMode.stopwatch, steps: [_step(defaultTargetValue: 30)], ); final repository = _FakeActiveSessionRepository() ..session = session ..stepProgressStates['step-state'] = _stepState( sessionId: session.metadata.id, status: ActiveExerciseStepProgressStatus.runningTimer, startedAt: now.subtract(const Duration(seconds: 5)), ) ..scoreStopwatchStates['score'] = _scoreStopwatch( sessionId: session.metadata.id, startedAt: now.subtract(const Duration(seconds: 4)), ) ..setTimerStates['set'] = _setTimer( sessionId: session.metadata.id, startedAt: now.subtract(const Duration(seconds: 6)), ); final projector = _projector(repository, _clock(now)); final projection = await projector.project(revision: 1); expect(projection.phase, WatchSessionPhase.running); expect(projection.dominantTimer?.kind, WatchTimerKind.step); expect(projection.dominantTimer?.accumulatedMs, 5000); expect( projection.dominantTimer?.startedAtEpochMs, now.millisecondsSinceEpoch, ); expect(projection.secondaryTimers.map((timer) => timer.kind), [ WatchTimerKind.scoreStopwatch, ]); expect(projection.primaryAction, WatchPrimaryAction.pauseSession); expect( projection.secondaryActions, contains(WatchSecondaryAction.finishCurrentSet), ); }); test( 'projects duration and stopwatch score with stopwatch timer visible', () async { final now = DateTime.utc(2026, 7, 25, 12); final session = _session( timeEnabled: true, scoreEnabled: true, scoreInputMode: ScoreInputMode.stopwatch, ); final repository = _FakeActiveSessionRepository() ..session = session ..scoreStopwatchStates['score'] = _scoreStopwatch( sessionId: session.metadata.id, startedAt: now.subtract(const Duration(seconds: 4)), ) ..setTimerStates['set'] = _setTimer( sessionId: session.metadata.id, startedAt: now.subtract(const Duration(seconds: 6)), ); final projector = _projector(repository, _clock(now)); final projection = await projector.project(revision: 1); expect(projection.phase, WatchSessionPhase.running); expect(projection.dominantTimer?.kind, WatchTimerKind.scoreStopwatch); expect(projection.secondaryTimers, isEmpty); expect(projection.primaryAction, WatchPrimaryAction.pauseSession); expect( projection.secondaryActions, contains(WatchSecondaryAction.finishCurrentSet), ); }, ); test( 'keeps set timer out of display timers while preserving phase', () async { final now = DateTime.utc(2026, 7, 25, 12); final session = _session(timeEnabled: true, repsEnabled: false); final repository = _FakeActiveSessionRepository() ..session = session ..setTimerStates['set'] = _setTimer( sessionId: session.metadata.id, startedAt: now.subtract(const Duration(seconds: 6)), ); final projector = _projector(repository, _clock(now)); final projection = await projector.project(revision: 1); expect(projection.phase, WatchSessionPhase.running); expect(projection.dominantTimer, isNull); expect(projection.secondaryTimers, isEmpty); expect(projection.primaryAction, WatchPrimaryAction.pauseSession); }, ); test('projects manual score state for the current set', () async { final session = _session(scoreEnabled: true, targetScore: 8); final repository = _FakeActiveSessionRepository() ..session = session ..manualScoreStates['manual-score'] = ActiveManualScoreState( metadata: _metadata('manual-score'), activeWorkoutSessionId: session.metadata.id, programIndex: 0, exerciseIndex: 0, setIndex: 0, value: 3, updatedAt: DateTime.utc(2026, 7, 25, 12), ); final projector = _projector(repository, _clock()); final projection = await projector.project(revision: 1); expect(projection.hasManualScore, isTrue); expect(projection.currentManualScoreValue, 3); expect(projection.canDecrementScore, isTrue); expect(projection.manualScoreTargetValue, 8); expect(projection.manualScoreTargetLabel, 'Cible'); expect(projection.manualScoreScope, WatchManualScoreScope.series); }); test( 'projects independent current step manual score before series score', () async { final session = _session( scoreEnabled: true, targetScore: 8, steps: [ _step( hasScore: true, scoreLabel: 'Réussites', scoreUnit: 'pts', defaultTargetScore: 5, ), ], ); final repository = _FakeActiveSessionRepository() ..session = session ..stepProgressStates['step-state'] = _stepState( sessionId: session.metadata.id, status: ActiveExerciseStepProgressStatus.waitingManual, ) ..stepResults.add( ActiveExerciseStepResult( metadata: _metadata('step-score'), activeWorkoutSessionId: session.metadata.id, programSnapshotId: 'program-snapshot-1', exerciseSnapshotId: 'exercise-snapshot-1', programIndex: 0, exerciseIndex: 0, setIndex: 0, passageIndex: 0, stepIndex: 0, stepSnapshotId: 'step-1', stepNameSnapshot: 'Step 1', stepTypeSnapshot: ExerciseStepType.time, targetValueSnapshot: 1, hasScoreSnapshot: true, scoreInputModeSnapshot: ScoreInputMode.manual, scoreLabelSnapshot: 'Réussites', scoreUnitSnapshot: 'pts', targetScoreSnapshot: 5, status: SetResultStatus.completed, actualScore: 2, ), ); final projector = _projector(repository, _clock()); final projection = await projector.project(revision: 1); expect(projection.hasManualScore, isTrue); expect(projection.currentManualScoreValue, 2); expect(projection.manualScoreTargetValue, 5); expect(projection.manualScoreScope, WatchManualScoreScope.step); }, ); test('does not project manual score controls for stopwatch score', () async { final repository = _FakeActiveSessionRepository() ..session = _session( scoreEnabled: true, scoreInputMode: ScoreInputMode.stopwatch, ); final projector = _projector(repository, _clock()); final projection = await projector.project(revision: 1); expect(projection.hasManualScore, isFalse); expect(projection.currentManualScoreValue, isNull); expect(projection.canDecrementScore, isFalse); expect(projection.manualScoreTargetValue, isNull); }); test('projects paused after a running session is paused', () async { final now = DateTime.utc(2026, 7, 25, 12); final session = _session( status: ActiveWorkoutStatus.paused, pausedAt: now, timeEnabled: true, steps: [_step()], ); final repository = _FakeActiveSessionRepository() ..session = session ..stepProgressStates['step-state'] = _stepState( sessionId: session.metadata.id, status: ActiveExerciseStepProgressStatus.pausedTimer, accumulatedMs: 5000, lastTransitionAt: now, ); final projector = _projector(repository, _clock(now)); final projection = await projector.project(revision: 2); expect(projection.phase, WatchSessionPhase.paused); expect(projection.primaryAction, WatchPrimaryAction.resumeSession); expect(projection.statusLabel, 'Séance en pause'); expect(projection.dominantTimer?.runState, WatchTimerRunState.paused); }); test( 'projects nextTimerReady after an elapsed timer with chaining disabled', () async { final now = DateTime.utc(2026, 7, 25, 12); final session = _session( autoStartNextTimedStepSnapshot: false, steps: [ _step(id: 'step-1'), _step(id: 'step-2', position: 1), ], ); final repository = _FakeActiveSessionRepository() ..session = session ..stepProgressStates['step-state'] = _stepState( sessionId: session.metadata.id, stepId: 'step-1', status: ActiveExerciseStepProgressStatus.runningTimer, startedAt: now.subtract(const Duration(milliseconds: 1500)), ); final projector = _projector(repository, _clock(now)); final projection = await projector.project(revision: 3); expect(projection.phase, WatchSessionPhase.nextTimerReady); expect(projection.stepIndex, 2); expect(projection.stepName, 'Step 2'); expect(projection.statusLabel, 'Chrono suivant prêt'); expect( projection.primaryAction, WatchPrimaryAction.startPreparedTimedStep, ); expect(projection.dominantTimer?.runState, WatchTimerRunState.stopped); expect(repository.stepResults, hasLength(1)); }, ); test( 'projects nextTimerReady for a stopped timed step after watch skip with chaining enabled', () async { final now = DateTime.utc(2026, 7, 25, 12); final session = _session( autoStartNextTimedStepSnapshot: true, steps: [ _step(id: 'step-1'), _step(id: 'step-2', position: 1), ], ); final repository = _FakeActiveSessionRepository() ..session = session ..stepProgressStates['step-state'] = _stepState( sessionId: session.metadata.id, stepId: 'step-2', stepIndex: 1, status: ActiveExerciseStepProgressStatus.stoppedTimer, lastTransitionAt: now, ); final projector = _projector(repository, _clock(now)); final projection = await projector.project(revision: 4); expect(projection.phase, WatchSessionPhase.nextTimerReady); expect(projection.stepIndex, 2); expect(projection.stepName, 'Step 2'); expect(projection.statusLabel, 'Chrono suivant prêt'); expect( projection.primaryAction, WatchPrimaryAction.startPreparedTimedStep, ); expect(projection.dominantTimer?.runState, WatchTimerRunState.stopped); }, ); test('projects restRunning after finishing a set with rest', () async { final now = DateTime.utc(2026, 7, 25, 12); final session = _session(currentSetIndex: 1); final repository = _FakeActiveSessionRepository() ..session = session ..restStates['rest'] = ActiveRestState( metadata: _metadata('rest'), activeWorkoutSessionId: session.metadata.id, afterProgramIndex: 0, afterExerciseIndex: 0, afterSetIndex: 0, plannedRestSeconds: 60, adjustedRestSeconds: 60, startedAt: now.subtract(const Duration(seconds: 10)), ); final projector = _projector(repository, _clock(now)); final projection = await projector.project(revision: 4); expect(projection.phase, WatchSessionPhase.restRunning); expect(projection.primaryAction, WatchPrimaryAction.pauseSession); expect(projection.secondaryActions, [WatchSecondaryAction.skipCurrentRest]); expect(projection.dominantTimer?.kind, WatchTimerKind.rest); expect(projection.dominantTimer?.targetMs, 60000); expect(projection.dominantTimer?.accumulatedMs, 10000); expect(projection.nextExerciseName, 'Squat'); }); test( 'projects restRunning next exercise when rest precedes another exercise', () async { final now = DateTime.utc(2026, 7, 25, 12); final session = _session( currentExerciseIndex: 1, currentSetIndex: 0, secondExerciseName: 'Fentes', ); final repository = _FakeActiveSessionRepository() ..session = session ..restStates['rest'] = ActiveRestState( metadata: _metadata('rest'), activeWorkoutSessionId: session.metadata.id, afterProgramIndex: 0, afterExerciseIndex: 0, afterSetIndex: 1, plannedRestSeconds: 60, adjustedRestSeconds: 60, startedAt: now.subtract(const Duration(seconds: 10)), ); final projector = _projector(repository, _clock(now)); final projection = await projector.project(revision: 4); expect(projection.phase, WatchSessionPhase.restRunning); expect(projection.exerciseName, 'Fentes'); expect(projection.nextExerciseName, 'Fentes'); }, ); test('projects restPaused', () async { final now = DateTime.utc(2026, 7, 25, 12); final session = _session(status: ActiveWorkoutStatus.paused, pausedAt: now); final repository = _FakeActiveSessionRepository() ..session = session ..restStates['rest'] = ActiveRestState( metadata: _metadata('rest'), activeWorkoutSessionId: session.metadata.id, afterProgramIndex: 0, afterExerciseIndex: 0, afterSetIndex: 0, plannedRestSeconds: 60, adjustedRestSeconds: 60, startedAt: now.subtract(const Duration(seconds: 15)), pausedAt: now, ); final projector = _projector(repository, _clock(now)); final projection = await projector.project(revision: 5); expect(projection.phase, WatchSessionPhase.restPaused); expect(projection.primaryAction, WatchPrimaryAction.resumeSession); expect(projection.dominantTimer?.runState, WatchTimerRunState.paused); }); test( 'projects betweenSetsReady after rest ends before the next set', () async { final now = DateTime.utc(2026, 7, 25, 12); final session = _session(currentSetIndex: 1); final repository = _FakeActiveSessionRepository() ..session = session ..restStates['rest'] = ActiveRestState( metadata: _metadata('rest'), activeWorkoutSessionId: session.metadata.id, afterProgramIndex: 0, afterExerciseIndex: 0, afterSetIndex: 0, plannedRestSeconds: 60, adjustedRestSeconds: 60, startedAt: now.subtract(const Duration(seconds: 60)), endedAt: now, ); final projector = _projector(repository, _clock(now)); final projection = await projector.project(revision: 6); expect(projection.phase, WatchSessionPhase.betweenSetsReady); expect(projection.statusLabel, isNull); expect(projection.primaryAction, WatchPrimaryAction.startCurrentExercise); }, ); test( 'projects nextExerciseName between sets when exercise changes', () async { final session = _session( currentExerciseIndex: 1, currentSetIndex: 0, secondExerciseName: 'Fentes', ); final repository = _FakeActiveSessionRepository()..session = session; final projector = _projector(repository, _clock()); final projection = await projector.project(revision: 7); expect(projection.phase, WatchSessionPhase.betweenSetsReady); expect(projection.exerciseName, 'Fentes'); expect(projection.nextExerciseName, 'Fentes'); }, ); test( 'keeps revision stable when refresh only changes volatile timing fields', () async { final now = DateTime.utc(2026, 7, 25, 12); final clock = _clock(now); final session = _session(steps: [_step(defaultTargetValue: 30)]); final repository = _FakeActiveSessionRepository() ..session = session ..stepProgressStates['step-state'] = _stepState( sessionId: session.metadata.id, status: ActiveExerciseStepProgressStatus.runningTimer, startedAt: now, ); final publisher = _FakeWatchProjectionPublisher(); final useCases = WatchCompanionProjectionUseCases( sessionRepository: repository, clock: clock, ids: _FakeIds(), originDeviceId: 'device-1', publisher: publisher, ); final emitted = []; final subscription = useCases.projections.listen(emitted.add); final first = await useCases.emitCurrentProjection(); clock.value = now.add(const Duration(seconds: 2)); final second = await useCases.emitCurrentProjection(); await Future.delayed(Duration.zero); expect(first.revision, 1); expect(second.revision, 1); expect(second.projectedAtEpochMs, greaterThan(first.projectedAtEpochMs)); expect(second.expiresAtEpochMs, greaterThan(first.expiresAtEpochMs)); expect( second.dominantTimer?.accumulatedMs, greaterThan(first.dominantTimer?.accumulatedMs ?? 0), ); expect(emitted.map((projection) => projection.revision), [1, 1]); expect(publisher.published.map((projection) => projection.revision), [ 1, 1, ]); await subscription.cancel(); await useCases.dispose(); }, ); test('increments revision when session command state changes', () async { final repository = _FakeActiveSessionRepository() ..session = _session(setsCount: 2); final publisher = _FakeWatchProjectionPublisher(); final useCases = WatchCompanionProjectionUseCases( sessionRepository: repository, clock: _clock(), ids: _FakeIds(), originDeviceId: 'device-1', publisher: publisher, ); final first = await useCases.emitCurrentProjection(); repository.session = repository.session!.copyWith(currentSetIndex: 1); final second = await useCases.emitCurrentProjection(); expect(first.revision, 1); expect(second.revision, 2); expect(second.seriesIndex, 2); expect(publisher.published.map((projection) => projection.revision), [ 1, 2, ]); await useCases.dispose(); }); } WatchSessionProjectionProjector _projector( _FakeActiveSessionRepository repository, _FakeClock clock, ) { return WatchSessionProjectionProjector( sessionRepository: repository, clock: clock, ids: _FakeIds(), originDeviceId: 'device-1', ); } _FakeClock _clock([DateTime? now]) { return _FakeClock(now ?? DateTime.utc(2026, 7, 25, 12)); } ActiveWorkoutSession _session({ ActiveWorkoutStatus status = ActiveWorkoutStatus.running, DateTime? pausedAt, int currentExerciseIndex = 0, int currentSetIndex = 0, int setsCount = 2, bool timeEnabled = false, bool repsEnabled = true, bool scoreEnabled = false, int? targetTimeSeconds, double? targetScore, ScoreInputMode scoreInputMode = ScoreInputMode.manual, bool? autoStartNextTimedStepSnapshot = true, List steps = const [], String? secondExerciseName, List healthServicesExerciseTypeStrategy = const [], List secondExerciseHealthServicesExerciseTypeStrategy = const [], }) { final exerciseSnapshot = { 'id': 'exercise-snapshot-1', 'exerciseNameSnapshot': 'Squat', 'setsCount': setsCount, 'timeEnabled': timeEnabled, 'repsEnabled': repsEnabled, 'scoreEnabled': scoreEnabled, 'targetTimeSeconds': targetTimeSeconds, 'targetReps': repsEnabled ? setsCount : null, 'targetScore': targetScore, 'scoreInputModeSnapshot': scoreInputMode.name, 'exerciseStepsSnapshot': steps .map((step) => step.toSnapshotJson()) .toList(), 'autoStartNextTimedStepSnapshot': ?autoStartNextTimedStepSnapshot, 'healthServicesExerciseTypeStrategy': healthServicesExerciseTypeStrategy, }; final secondExerciseSnapshot = secondExerciseName == null ? null : { 'id': 'exercise-snapshot-2', 'exerciseNameSnapshot': secondExerciseName, 'setsCount': 1, 'timeEnabled': false, 'repsEnabled': true, 'scoreEnabled': false, 'targetReps': 1, 'scoreInputModeSnapshot': ScoreInputMode.manual.name, 'exerciseStepsSnapshot': const [], 'autoStartNextTimedStepSnapshot': true, 'healthServicesExerciseTypeStrategy': secondExerciseHealthServicesExerciseTypeStrategy, }; return ActiveWorkoutSession( metadata: _metadata('session-1'), status: status, startedAt: DateTime.utc(2026, 7, 25, 12), pausedAt: pausedAt, lastPersistedAt: DateTime.utc(2026, 7, 25, 12), elapsedActiveMs: 0, currentProgramIndex: 0, currentExerciseIndex: currentExerciseIndex, currentSetIndex: currentSetIndex, resolvedTemplateSnapshotJson: jsonEncode({ 'programs': [ { 'id': 'program-snapshot-1', 'programNameSnapshot': 'Programme', 'programSnapshotJson': jsonEncode({ 'exercises': [exerciseSnapshot, ?secondExerciseSnapshot], }), }, ], }), ); } ExerciseStep _step({ String id = 'step-1', int position = 0, ExerciseStepType type = ExerciseStepType.time, int defaultTargetValue = 1, bool hasScore = false, String? scoreLabel, String? scoreUnit, double? defaultTargetScore, }) { return ExerciseStep( id: id, position: position, name: 'Step ${position + 1}', type: type, defaultTargetValue: defaultTargetValue, hasScore: hasScore, scoreLabel: scoreLabel, scoreUnit: scoreUnit, defaultTargetScore: defaultTargetScore, ); } ActiveExerciseStepProgressState _stepState({ required String sessionId, String stepId = 'step-1', int stepIndex = 0, ActiveExerciseStepProgressStatus status = ActiveExerciseStepProgressStatus.stoppedTimer, DateTime? startedAt, int accumulatedMs = 0, DateTime? lastTransitionAt, }) { return ActiveExerciseStepProgressState( metadata: _metadata('step-state'), activeWorkoutSessionId: sessionId, programIndex: 0, exerciseIndex: 0, setIndex: 0, currentPassageIndex: 0, currentStepIndex: stepIndex, currentStepSnapshotId: stepId, status: status, startedAt: startedAt, accumulatedMs: accumulatedMs, lastTransitionAt: lastTransitionAt ?? DateTime.utc(2026, 7, 25, 12), ); } ActiveScoreStopwatchState _scoreStopwatch({ required String sessionId, required DateTime startedAt, }) { return ActiveScoreStopwatchState( metadata: _metadata('score'), activeWorkoutSessionId: sessionId, programIndex: 0, exerciseIndex: 0, setIndex: 0, status: ActiveScoreStopwatchStatus.running, startedAt: startedAt, accumulatedMs: 0, ); } ActiveSetTimerState _setTimer({ required String sessionId, required DateTime startedAt, }) { return ActiveSetTimerState( metadata: _metadata('set'), activeWorkoutSessionId: sessionId, programIndex: 0, exerciseIndex: 0, setIndex: 0, status: ActiveSetTimerStatus.running, startedAt: startedAt, accumulatedMs: 0, ); } EntityMetadata _metadata(String id) { return EntityMetadata( id: id, createdAt: DateTime.utc(2026, 7, 25, 12), updatedAt: DateTime.utc(2026, 7, 25, 12), originDeviceId: 'device-1', ); } final class _FakeWatchProjectionPublisher implements WatchProjectionPublisher { final published = []; @override Future publish( WatchSessionProjection projection, { bool urgent = true, }) async { published.add(projection); } } 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 _FakeActiveSessionRepository implements ActiveSessionRepository { ActiveWorkoutSession? session; final results = []; final restStates = {}; final setTimerStates = {}; final scoreStopwatchStates = {}; final manualScoreStates = {}; final stepProgressStates = {}; final stepResults = []; @override Future deleteScoreStopwatchState({ required String sessionId, required int programIndex, required int exerciseIndex, required int setIndex, required DateTime deletedAt, }) async { scoreStopwatchStates.clear(); } @override Future deleteManualScoreState({ required String sessionId, required int programIndex, required int exerciseIndex, required int setIndex, required DateTime deletedAt, }) async { manualScoreStates.clear(); } @override Future findById(String id) async { return session?.metadata.id == id ? session : null; } @override Future findOpen() async => session; @override Future findExerciseStepProgressState({ required String sessionId, required int programIndex, required int exerciseIndex, required int setIndex, }) async { return stepProgressStates.values.where((state) { return state.activeWorkoutSessionId == sessionId && state.programIndex == programIndex && state.exerciseIndex == exerciseIndex && state.setIndex == setIndex; }).firstOrNull; } @override Future findRestStateById(String id) async { return restStates[id]; } @override Future findScoreStopwatchState({ required String sessionId, required int programIndex, required int exerciseIndex, required int setIndex, }) async { return scoreStopwatchStates.values.where((state) { return state.activeWorkoutSessionId == sessionId && state.programIndex == programIndex && state.exerciseIndex == exerciseIndex && state.setIndex == setIndex; }).firstOrNull; } @override Future findManualScoreState({ required String sessionId, required int programIndex, required int exerciseIndex, required int setIndex, }) async { return manualScoreStates.values.where((state) { return state.activeWorkoutSessionId == sessionId && state.programIndex == programIndex && state.exerciseIndex == exerciseIndex && state.setIndex == setIndex; }).firstOrNull; } @override Future findSetTimerState({ required String sessionId, required int programIndex, required int exerciseIndex, required int setIndex, }) async { return setTimerStates.values.where((state) { return state.activeWorkoutSessionId == sessionId && state.programIndex == programIndex && state.exerciseIndex == exerciseIndex && state.setIndex == setIndex; }).firstOrNull; } @override Future> listExerciseStepProgressStates( String sessionId, ) async { return stepProgressStates.values .where((state) => state.activeWorkoutSessionId == sessionId) .toList(); } @override Future> listExerciseStepResults( String sessionId, ) async { return stepResults .where((result) => result.activeWorkoutSessionId == sessionId) .toList(); } @override Future> listRestStates(String sessionId) async { return restStates.values .where((state) => state.activeWorkoutSessionId == sessionId) .toList(); } @override Future> listScoreStopwatchStates( String sessionId, ) async { return scoreStopwatchStates.values .where((state) => state.activeWorkoutSessionId == sessionId) .toList(); } @override Future> listManualScoreStates( String sessionId, ) async { return manualScoreStates.values .where((state) => state.activeWorkoutSessionId == sessionId) .toList(); } @override Future> listSetResults(String sessionId) async { return results .where((result) => result.activeWorkoutSessionId == sessionId) .toList(); } @override Future> listSetTimerStates(String sessionId) async { return setTimerStates.values .where((state) => state.activeWorkoutSessionId == sessionId) .toList(); } @override Future save(ActiveWorkoutSession session) async { this.session = session; } @override Future saveExerciseStepProgressState( ActiveExerciseStepProgressState state, ) async { stepProgressStates[state.metadata.id] = state; } @override Future saveExerciseStepResult(ActiveExerciseStepResult result) async { stepResults.add(result); } @override Future saveRestState(ActiveRestState restState) async { restStates[restState.metadata.id] = restState; } @override Future saveScoreStopwatchState(ActiveScoreStopwatchState state) async { scoreStopwatchStates[state.metadata.id] = state; } @override Future saveManualScoreState(ActiveManualScoreState state) async { manualScoreStates[state.metadata.id] = state; } @override Future saveSetResult(ActiveSetResult result) async { results.add(result); } @override Future saveSetTimerState(ActiveSetTimerState state) async { setTimerStates[state.metadata.id] = state; } }