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 = <WatchSessionProjection>[];
      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<void>.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<ExerciseStep> steps = const [],
  String? secondExerciseName,
  List<String> healthServicesExerciseTypeStrategy = const [],
  List<String> 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 = <WatchSessionProjection>[];

  @override
  Future<void> 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 = <ActiveSetResult>[];
  final restStates = <String, ActiveRestState>{};
  final setTimerStates = <String, ActiveSetTimerState>{};
  final scoreStopwatchStates = <String, ActiveScoreStopwatchState>{};
  final manualScoreStates = <String, ActiveManualScoreState>{};
  final stepProgressStates = <String, ActiveExerciseStepProgressState>{};
  final stepResults = <ActiveExerciseStepResult>[];

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

  @override
  Future<void> deleteManualScoreState({
    required String sessionId,
    required int programIndex,
    required int exerciseIndex,
    required int setIndex,
    required DateTime deletedAt,
  }) async {
    manualScoreStates.clear();
  }

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

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

  @override
  Future<ActiveExerciseStepProgressState?> 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<ActiveRestState?> findRestStateById(String id) async {
    return restStates[id];
  }

  @override
  Future<ActiveScoreStopwatchState?> 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<ActiveManualScoreState?> 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<ActiveSetTimerState?> 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<List<ActiveExerciseStepProgressState>> listExerciseStepProgressStates(
    String sessionId,
  ) async {
    return stepProgressStates.values
        .where((state) => state.activeWorkoutSessionId == sessionId)
        .toList();
  }

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

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

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

  @override
  Future<List<ActiveManualScoreState>> listManualScoreStates(
    String sessionId,
  ) async {
    return manualScoreStates.values
        .where((state) => state.activeWorkoutSessionId == sessionId)
        .toList();
  }

  @override
  Future<List<ActiveSetResult>> listSetResults(String sessionId) async {
    return results
        .where((result) => result.activeWorkoutSessionId == sessionId)
        .toList();
  }

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

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

  @override
  Future<void> saveExerciseStepProgressState(
    ActiveExerciseStepProgressState state,
  ) async {
    stepProgressStates[state.metadata.id] = state;
  }

  @override
  Future<void> saveExerciseStepResult(ActiveExerciseStepResult result) async {
    stepResults.add(result);
  }

  @override
  Future<void> saveRestState(ActiveRestState restState) async {
    restStates[restState.metadata.id] = restState;
  }

  @override
  Future<void> saveScoreStopwatchState(ActiveScoreStopwatchState state) async {
    scoreStopwatchStates[state.metadata.id] = state;
  }

  @override
  Future<void> saveManualScoreState(ActiveManualScoreState state) async {
    manualScoreStates[state.metadata.id] = state;
  }

  @override
  Future<void> saveSetResult(ActiveSetResult result) async {
    results.add(result);
  }

  @override
  Future<void> saveSetTimerState(ActiveSetTimerState state) async {
    setTimerStates[state.metadata.id] = state;
  }
}
