import 'dart:convert'; import 'package:flutter_test/flutter_test.dart'; import 'package:gametime/application/application.dart'; import 'package:gametime/domain/domain.dart'; void main() { test('Exercise requires at least one active measure', () { expect( () => Exercise( metadata: _metadata('exercise-1'), name: 'Push up', hasTimeMeasure: false, hasRepsMeasure: false, hasScoreMeasure: false, ), throwsA(isA()), ); }); test('Workout template use case rejects active measure overrides', () async { final useCase = WorkoutTemplateUseCases( templateRepository: _FakeWorkoutTemplateRepository(), programRepository: _FakeProgramRepository(), clock: _FakeClock(DateTime.utc(2026, 7, 17, 12)), ids: _FakeIds(), originDeviceId: 'device-1', ); await expectLater( useCase.overrideExerciseTargets( workoutTemplateProgramId: 'template-program-1', snapshotProgramExerciseId: 'exercise-snapshot-1', attemptedMeasureOverride: {WorkoutMeasure.reps}, ), throwsA(isA()), ); }); test( 'Workout template use case rejects target overrides for inactive measures', () async { final useCase = WorkoutTemplateUseCases( templateRepository: _FakeWorkoutTemplateRepository(), programRepository: _FakeProgramRepository(), clock: _FakeClock(DateTime.utc(2026, 7, 17, 12)), ids: _FakeIds(), originDeviceId: 'device-1', ); await expectLater( useCase.saveConfigured( name: 'Séance tir', programs: [ WorkoutTemplateProgramConfig( clientKey: 'program-1', programNameSnapshot: 'Programme tir', defaultRestSecondsSnapshot: 30, programSnapshotJson: jsonEncode({ 'exercises': [ { 'id': 'exercise-snapshot-1', 'exerciseNameSnapshot': 'Lancers francs', 'setsCount': 3, 'timeEnabled': true, 'repsEnabled': false, 'scoreEnabled': false, 'targetTimeSeconds': 60, 'targetReps': null, 'targetScore': null, }, ], }), ), ], overrides: const [ WorkoutTemplateExerciseOverrideConfig( workoutTemplateProgramClientKey: 'program-1', snapshotProgramExerciseId: 'exercise-snapshot-1', targetRepsOverride: 10, ), ], ), throwsA(isA()), ); }, ); test('Active session elapsed time is based on persisted timestamps', () { final clock = _FakeClock(DateTime.utc(2026, 7, 17, 12)); final session = ActiveWorkoutSession( metadata: _metadata('session-1'), status: ActiveWorkoutStatus.running, startedAt: clock.now(), lastPersistedAt: clock.now(), elapsedActiveMs: 0, currentProgramIndex: 0, currentExerciseIndex: 0, currentSetIndex: 0, resolvedTemplateSnapshotJson: '{}', ); clock.value = clock.value.add(const Duration(seconds: 30)); final paused = session.pause(clock.now()); expect(paused.elapsedActiveMs, 30000); clock.value = clock.value.add(const Duration(minutes: 5)); final resumed = paused.resume(clock.now()); expect(resumed.elapsedActiveMillisecondsAt(clock.now()), 30000); clock.value = clock.value.add(const Duration(seconds: 15)); expect(resumed.elapsedActiveMillisecondsAt(clock.now()), 45000); }); test('adjustRestSeconds persists adjusted rest duration', () async { final clock = _FakeClock(DateTime.utc(2026, 7, 17, 12)); final repository = _FakeActiveSessionRepository() ..restStates['rest-1'] = ActiveRestState( metadata: _metadata('rest-1'), activeWorkoutSessionId: 'session-1', afterProgramIndex: 0, afterExerciseIndex: 0, afterSetIndex: 0, plannedRestSeconds: 60, adjustedRestSeconds: 60, startedAt: clock.now(), ); final useCase = _activeUseCase(repository, clock); final adjusted = await useCase.adjustRestSeconds( restStateId: 'rest-1', deltaSeconds: -75, ); expect(adjusted.adjustedRestSeconds, 0); expect(repository.restStates['rest-1']!.adjustedRestSeconds, 0); }); test('skipRest persists skippedAt timestamp', () async { final clock = _FakeClock(DateTime.utc(2026, 7, 17, 12)); final repository = _FakeActiveSessionRepository() ..restStates['rest-1'] = ActiveRestState( metadata: _metadata('rest-1'), activeWorkoutSessionId: 'session-1', afterProgramIndex: 0, afterExerciseIndex: 0, afterSetIndex: 0, plannedRestSeconds: 60, adjustedRestSeconds: 60, startedAt: clock.now(), ); clock.value = clock.value.add(const Duration(seconds: 10)); final useCase = _activeUseCase(repository, clock); final skipped = await useCase.skipRest(restStateId: 'rest-1'); expect(skipped.skippedAt, clock.now()); expect(repository.restStates['rest-1']!.skippedAt, clock.now()); }); test('findActiveRest returns latest non-ended non-skipped rest', () async { final clock = _FakeClock(DateTime.utc(2026, 7, 17, 12)); final repository = _FakeActiveSessionRepository() ..restStates['ended'] = ActiveRestState( metadata: _metadata('ended'), activeWorkoutSessionId: 'session-1', afterProgramIndex: 0, afterExerciseIndex: 0, afterSetIndex: 0, plannedRestSeconds: 60, adjustedRestSeconds: 60, startedAt: clock.now(), endedAt: clock.now().add(const Duration(seconds: 60)), ) ..restStates['older-active'] = ActiveRestState( metadata: _metadata('older-active'), activeWorkoutSessionId: 'session-1', afterProgramIndex: 0, afterExerciseIndex: 1, afterSetIndex: 0, plannedRestSeconds: 60, adjustedRestSeconds: 60, startedAt: clock.now().add(const Duration(seconds: 30)), ) ..restStates['latest-active'] = ActiveRestState( metadata: _metadata('latest-active'), activeWorkoutSessionId: 'session-1', afterProgramIndex: 0, afterExerciseIndex: 2, afterSetIndex: 0, plannedRestSeconds: 60, adjustedRestSeconds: 60, startedAt: clock.now().add(const Duration(seconds: 90)), ) ..restStates['skipped'] = ActiveRestState( metadata: _metadata('skipped'), activeWorkoutSessionId: 'session-1', afterProgramIndex: 0, afterExerciseIndex: 3, afterSetIndex: 0, plannedRestSeconds: 60, adjustedRestSeconds: 60, startedAt: clock.now().add(const Duration(seconds: 120)), skippedAt: clock.now().add(const Duration(seconds: 125)), ); final useCase = _activeUseCase(repository, clock); final active = await useCase.findActiveRest(sessionId: 'session-1'); expect(active?.metadata.id, 'latest-active'); }); } 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 _FakeWorkoutTemplateRepository implements WorkoutTemplateRepository { @override Future findById(String id) async => null; @override Future> listActive() async => const []; @override Future replaceComposition( WorkoutTemplate template, DateTime deletedAt, ) async {} @override Future save(WorkoutTemplate template) async {} @override Future saveOverride(WorkoutTemplateExerciseOverride override) async {} @override Future saveProgram(WorkoutTemplateProgram program) async {} } final class _FakeProgramRepository implements ProgramRepository { @override Future findById(String id) async => null; @override Future> listActive() async => const []; @override Future replaceExercises(Program program, DateTime deletedAt) async {} @override Future save(Program program) async {} @override Future saveExercise(ProgramExercise exercise) async {} } ActiveWorkoutSessionUseCases _activeUseCase( _FakeActiveSessionRepository repository, _FakeClock clock, ) { return ActiveWorkoutSessionUseCases( sessionRepository: repository, templateRepository: _FakeWorkoutTemplateRepository(), clock: clock, ids: _FakeIds(), originDeviceId: 'device-1', ); } final class _FakeActiveSessionRepository implements ActiveSessionRepository { final restStates = {}; @override Future findById(String id) async => null; @override Future findOpen() async => null; @override Future findRestStateById(String id) async => restStates[id]; @override Future> listRestStates(String sessionId) async { return restStates.values .where((rest) => rest.activeWorkoutSessionId == sessionId) .toList(); } @override Future> listSetResults(String sessionId) async => const []; @override Future save(ActiveWorkoutSession session) async {} @override Future saveRestState(ActiveRestState restState) async { restStates[restState.metadata.id] = restState; } @override Future saveSetResult(ActiveSetResult result) async {} }