import 'dart:convert';

import 'package:flutter_test/flutter_test.dart';
import 'package:gametime/application/application.dart';
import 'package:gametime/domain/domain.dart';
import 'package:watch_bridge_contract/watch_bridge_contract.dart';

void main() {
  test('routes startCurrentExercise to active execution timers', () async {
    final env = _env(
      session: _session(timeEnabled: true),
      projection: _projection(
        primaryAction: WatchPrimaryAction.startCurrentExercise,
      ),
    );

    final ack = await env.dispatch(WatchCommandType.startCurrentExercise);

    expect(ack, WatchCommandAck.accepted);
    expect(env.repository.setTimerStates, hasLength(1));
    expect(env.projections.emitCount, 1);
  });

  test('routes pauseSession and resumeSession to session use cases', () async {
    final pauseEnv = _env(
      session: _session(timeEnabled: true),
      projection: _projection(
        phase: WatchSessionPhase.running,
        primaryAction: WatchPrimaryAction.pauseSession,
      ),
    );

    expect(
      await pauseEnv.dispatch(WatchCommandType.pauseSession),
      WatchCommandAck.accepted,
    );
    expect(pauseEnv.repository.session?.status, ActiveWorkoutStatus.paused);

    final resumeEnv = _env(
      session: _session(status: ActiveWorkoutStatus.paused, pausedAt: _now),
      projection: _projection(
        phase: WatchSessionPhase.paused,
        primaryAction: WatchPrimaryAction.resumeSession,
      ),
    );

    expect(
      await resumeEnv.dispatch(WatchCommandType.resumeSession),
      WatchCommandAck.accepted,
    );
    expect(resumeEnv.repository.session?.status, ActiveWorkoutStatus.running);
  });

  test('routes startPreparedTimedStep to step timer start', () async {
    final session = _session(
      steps: [
        _step(),
        _step(id: 'step-2', position: 1),
      ],
    );
    final env = _env(
      session: session,
      projection: _projection(
        phase: WatchSessionPhase.nextTimerReady,
        primaryAction: WatchPrimaryAction.startPreparedTimedStep,
      ),
    );
    env.repository.stepProgressStates['step-state'] = _stepState(
      sessionId: session.metadata.id,
      stepId: 'step-2',
      stepIndex: 1,
    );

    final ack = await env.dispatch(WatchCommandType.startPreparedTimedStep);

    expect(ack, WatchCommandAck.accepted);
    expect(
      env.repository.stepProgressStates['step-state']?.status,
      ActiveExerciseStepProgressStatus.runningTimer,
    );
  });

  test('routes skipCurrentStep to step use case', () async {
    final session = _session(steps: [_step()]);
    final env = _env(
      session: session,
      projection: _projection(
        secondaryActions: [WatchSecondaryAction.skipCurrentStep],
      ),
    );
    env.repository.stepProgressStates['step-state'] = _stepState(
      sessionId: session.metadata.id,
    );

    final ack = await env.dispatch(WatchCommandType.skipCurrentStep);

    expect(ack, WatchCommandAck.accepted);
    expect(env.repository.stepResults, hasLength(1));
  });

  test('routes completeCurrentStep to completed reps step', () async {
    final session = _session(
      steps: [_step(type: ExerciseStepType.reps, defaultTargetValue: 12)],
    );
    final env = _env(
      session: session,
      projection: _projection(
        stepType: WatchStepType.reps,
        stepTargetValue: 12,
      ),
    );
    env.repository.stepProgressStates['step-state'] = _stepState(
      sessionId: session.metadata.id,
    );

    final ack = await env.dispatch(WatchCommandType.completeCurrentStep);

    expect(ack, WatchCommandAck.accepted);
    expect(env.repository.stepResults.single.status, SetResultStatus.completed);
    expect(env.repository.stepResults.single.actualReps, 12);
  });

  test(
    'rejects completeCurrentStep when current projection is not reps',
    () async {
      final session = _session(steps: [_step()]);
      final env = _env(session: session, projection: _projection());
      env.repository.stepProgressStates['step-state'] = _stepState(
        sessionId: session.metadata.id,
      );

      final ack = await env.dispatch(WatchCommandType.completeCurrentStep);

      expect(ack, WatchCommandAck.rejectedNotApplicable);
      expect(env.repository.stepResults, isEmpty);
    },
  );

  test('routes skipCurrentPassage to step use case', () async {
    final session = _session(
      targetReps: 2,
      steps: [
        _step(),
        _step(id: 'step-2', position: 1),
      ],
    );
    final env = _env(
      session: session,
      projection: _projection(
        secondaryActions: [WatchSecondaryAction.skipCurrentPassage],
      ),
    );
    env.repository.stepProgressStates['step-state'] = _stepState(
      sessionId: session.metadata.id,
    );

    final ack = await env.dispatch(WatchCommandType.skipCurrentPassage);

    expect(ack, WatchCommandAck.accepted);
    expect(env.repository.stepResults, hasLength(2));
  });

  test(
    'routes finishCurrentSet and advances to next set without rest',
    () async {
      final session = _session(timeEnabled: true, setsCount: 2);
      final env = _env(
        session: session,
        projection: _projection(
          secondaryActions: [WatchSecondaryAction.finishCurrentSet],
        ),
      );
      env.repository.setTimerStates['set'] = _setTimer(
        sessionId: session.metadata.id,
      );

      final ack = await env.dispatch(WatchCommandType.finishCurrentSet);

      expect(ack, WatchCommandAck.accepted);
      expect(env.repository.results, hasLength(1));
      expect(env.repository.session?.currentSetIndex, 1);
    },
  );

  test('routes finishCurrentSet and starts rest before next set', () async {
    final session = _session(timeEnabled: true, setsCount: 2, restSeconds: 60);
    final env = _env(
      session: session,
      projection: _projection(
        secondaryActions: [WatchSecondaryAction.finishCurrentSet],
      ),
    );
    env.repository.setTimerStates['set'] = _setTimer(
      sessionId: session.metadata.id,
    );

    final ack = await env.dispatch(WatchCommandType.finishCurrentSet);

    expect(ack, WatchCommandAck.accepted);
    expect(env.repository.restStates.values.single.plannedRestSeconds, 60);
    expect(env.repository.session?.currentSetIndex, 0);
  });

  test('routes skipCurrentSet and advances once on retry duplicate', () async {
    final session = _session(timeEnabled: true, setsCount: 3);
    final env = _env(
      session: session,
      projection: _projection(
        secondaryActions: [WatchSecondaryAction.skipCurrentSet],
      ),
    );
    env.repository.setTimerStates['set'] = _setTimer(
      sessionId: session.metadata.id,
    );
    final command = _command(WatchCommandType.skipCurrentSet);

    final firstAck = await env.handler.dispatch(command);
    final retryAck = await env.handler.dispatch(command);

    expect(firstAck, WatchCommandAck.accepted);
    expect(retryAck, WatchCommandAck.acceptedNoOp);
    expect(env.repository.results, hasLength(1));
    expect(env.repository.session?.currentSetIndex, 1);
  });

  test('routes skipCurrentRest to rest skip and next position', () async {
    final session = _session(setsCount: 2);
    final env = _env(
      session: session,
      projection: _projection(
        phase: WatchSessionPhase.restRunning,
        primaryAction: WatchPrimaryAction.pauseSession,
        secondaryActions: [WatchSecondaryAction.skipCurrentRest],
      ),
    );
    env.repository.restStates['rest'] = ActiveRestState(
      metadata: _metadata('rest'),
      activeWorkoutSessionId: session.metadata.id,
      afterProgramIndex: 0,
      afterExerciseIndex: 0,
      afterSetIndex: 0,
      plannedRestSeconds: 60,
      adjustedRestSeconds: 60,
      startedAt: _now,
    );

    final ack = await env.dispatch(WatchCommandType.skipCurrentRest);

    expect(ack, WatchCommandAck.accepted);
    expect(env.repository.restStates['rest']?.skippedAt, isNotNull);
    expect(env.repository.session?.currentSetIndex, 1);
  });

  test(
    'accepts skipCurrentRest after a refresh that keeps the same revision',
    () async {
      final session = _session(setsCount: 2);
      final repository = _FakeActiveSessionRepository()..session = session;
      repository.restStates['rest'] = ActiveRestState(
        metadata: _metadata('rest'),
        activeWorkoutSessionId: session.metadata.id,
        afterProgramIndex: 0,
        afterExerciseIndex: 0,
        afterSetIndex: 0,
        plannedRestSeconds: 60,
        adjustedRestSeconds: 60,
        startedAt: _now,
      );
      final clock = _FakeClock(_now);
      final ids = _FakeIds();
      final activeUseCases = ActiveWorkoutSessionUseCases(
        sessionRepository: repository,
        templateRepository: _FakeWorkoutTemplateRepository(),
        clock: clock,
        ids: ids,
        originDeviceId: 'device-1',
      );
      final projectionUseCases = WatchCompanionProjectionUseCases(
        sessionRepository: repository,
        clock: clock,
        ids: ids,
        originDeviceId: 'device-1',
      );
      final handler = WatchCompanionCommandHandler(
        sessionRepository: repository,
        activeSessionUseCases: activeUseCases,
        stepUseCases: ActiveExerciseStepUseCases(
          sessionRepository: repository,
          clock: clock,
          ids: ids,
          originDeviceId: 'device-1',
          activeSessionUseCases: activeUseCases,
        ),
        projectionSource: projectionUseCases,
      );

      final firstProjection = await projectionUseCases.emitCurrentProjection();
      final refreshedProjection = await projectionUseCases
          .emitCurrentProjection();

      final ack = await handler.dispatch(
        _command(
          WatchCommandType.skipCurrentRest,
          expectedRevision: firstProjection.revision,
        ),
      );

      expect(refreshedProjection.revision, firstProjection.revision);
      expect(ack, WatchCommandAck.accepted);
      expect(repository.restStates['rest']?.skippedAt, isNotNull);
      expect(repository.session?.currentSetIndex, 1);

      await projectionUseCases.dispose();
    },
  );

  test(
    'routes incrementScore and decrementScore to manual score use cases',
    () async {
      final env = _env(
        session: _session(scoreEnabled: true),
        projection: _projection(hasManualScore: true),
      );

      expect(
        await env.dispatch(WatchCommandType.incrementScore),
        WatchCommandAck.accepted,
      );
      expect(env.repository.manualScoreStates.values.single.value, 1);

      expect(
        await env.dispatch(
          WatchCommandType.decrementScore,
          commandId: 'command-2',
        ),
        WatchCommandAck.accepted,
      );
      expect(env.repository.manualScoreStates.values.single.value, 0);
    },
  );

  test('routes score commands to independent current step score', () async {
    final session = _session(
      scoreEnabled: true,
      steps: [
        _step(
          hasScore: true,
          scoreLabel: 'Réussites',
          scoreUnit: 'pts',
          defaultTargetScore: 5,
        ),
      ],
    );
    final env = _env(
      session: session,
      projection: _projection(
        hasManualScore: true,
        manualScoreScope: WatchManualScoreScope.step,
      ),
    );

    expect(
      await env.dispatch(WatchCommandType.incrementScore),
      WatchCommandAck.accepted,
    );
    expect(env.repository.manualScoreStates, isEmpty);
    expect(env.repository.stepResults.single.actualScore, 1);

    expect(
      await env.dispatch(
        WatchCommandType.decrementScore,
        commandId: 'command-2',
      ),
      WatchCommandAck.accepted,
    );
    expect(env.repository.manualScoreStates, isEmpty);
    expect(env.repository.stepResults.last.actualScore, 0);
  });

  test('projects reps target for independent step manual score', () async {
    final session = _session(
      steps: [
        _step(
          type: ExerciseStepType.reps,
          defaultTargetValue: 10,
          hasScore: true,
          scoreLabel: 'Cible',
          scoreUnit: 'pts',
          defaultTargetScore: 8,
        ),
      ],
    );
    final repository = _FakeActiveSessionRepository()..session = session;
    repository.stepProgressStates['step-state'] = _stepState(
      sessionId: session.metadata.id,
    );
    final projectionUseCases = WatchCompanionProjectionUseCases(
      sessionRepository: repository,
      clock: _FakeClock(_now),
      ids: _FakeIds(),
      originDeviceId: 'device-1',
    );

    final projection = await projectionUseCases.emitCurrentProjection();

    expect(projection.hasManualScore, isTrue);
    expect(projection.manualScoreScope, WatchManualScoreScope.step);
    expect(projection.manualScoreRepsTargetValue, 10);
    expect(projection.manualScoreTargetValue, 8);
    expect(projection.manualScoreTargetLabel, 'Cible');
    expect(
      projection.expiresAtEpochMs - projection.projectedAtEpochMs,
      const Duration(seconds: 12).inMilliseconds,
    );

    await projectionUseCases.dispose();
  });

  test('decrementScore at zero is accepted no-op', () async {
    final env = _env(
      session: _session(scoreEnabled: true),
      projection: _projection(hasManualScore: true),
    );

    final ack = await env.dispatch(WatchCommandType.decrementScore);

    expect(ack, WatchCommandAck.acceptedNoOp);
    expect(env.repository.manualScoreStates.values.single.value, 0);
    expect(env.projections.emitCount, 0);
  });

  test('rejects score commands outside manual score mode', () async {
    final env = _env(
      session: _session(
        scoreEnabled: true,
        scoreInputMode: ScoreInputMode.stopwatch,
      ),
      projection: _projection(hasManualScore: false),
    );

    expect(
      await env.dispatch(WatchCommandType.incrementScore),
      WatchCommandAck.rejectedNotApplicable,
    );
    expect(env.repository.manualScoreStates, isEmpty);
  });

  test(
    'accepts repeated score commands with stale expected revisions',
    () async {
      final env = _env(
        session: _session(scoreEnabled: true),
        projection: _projection(revision: 3, hasManualScore: true),
      );

      expect(
        await env.dispatch(
          WatchCommandType.incrementScore,
          commandId: 'command-1',
          expectedRevision: 1,
        ),
        WatchCommandAck.accepted,
      );
      expect(
        await env.dispatch(
          WatchCommandType.incrementScore,
          commandId: 'command-2',
          expectedRevision: 1,
        ),
        WatchCommandAck.accepted,
      );
      expect(env.repository.manualScoreStates.values.single.value, 2);
    },
  );

  test(
    'rejects stale revision, non applicable, missing and mismatch',
    () async {
      final stale = _env(
        session: _session(),
        projection: _projection(revision: 2),
      );
      expect(
        await stale.dispatch(WatchCommandType.startCurrentExercise),
        WatchCommandAck.rejectedStaleRevision,
      );

      final nonApplicable = _env(
        session: _session(),
        projection: _projection(),
      );
      expect(
        await nonApplicable.dispatch(WatchCommandType.pauseSession),
        WatchCommandAck.rejectedNotApplicable,
      );

      final missing = _env(
        projection: _projection(
          phase: WatchSessionPhase.noActiveSession,
          deviceSessionId: '',
          primaryAction: WatchPrimaryAction.none,
        ),
      );
      expect(
        await missing.dispatch(WatchCommandType.startCurrentExercise),
        WatchCommandAck.rejectedNoActiveSession,
      );

      final mismatch = _env(
        session: _session(),
        projection: _projection(deviceSessionId: 'other-session'),
      );
      expect(
        await mismatch.dispatch(WatchCommandType.startCurrentExercise),
        WatchCommandAck.rejectedSessionMismatch,
      );
    },
  );

  test('rejects commands when no session is active', () async {
    final env = _env(
      projection: _projection(
        phase: WatchSessionPhase.noActiveSession,
        deviceSessionId: '',
        primaryAction: WatchPrimaryAction.none,
      ),
    );

    expect(
      await env.dispatch(WatchCommandType.startCurrentExercise),
      WatchCommandAck.rejectedNoActiveSession,
    );
    expect(env.repository.session, isNull);
  });
}

final _now = DateTime.utc(2026, 7, 25, 12);

_Harness _env({
  ActiveWorkoutSession? session,
  required WatchSessionProjection projection,
  _FakeWorkoutTemplateRepository? templateRepository,
}) {
  final repository = _FakeActiveSessionRepository()..session = session;
  final clock = _FakeClock(_now);
  final ids = _FakeIds();
  final activeUseCases = ActiveWorkoutSessionUseCases(
    sessionRepository: repository,
    templateRepository: templateRepository ?? _FakeWorkoutTemplateRepository(),
    clock: clock,
    ids: ids,
    originDeviceId: 'device-1',
  );
  final stepUseCases = ActiveExerciseStepUseCases(
    sessionRepository: repository,
    clock: clock,
    ids: ids,
    originDeviceId: 'device-1',
  );
  final projections = _FakeProjectionSource(projection);
  return _Harness(
    repository: repository,
    projections: projections,
    handler: WatchCompanionCommandHandler(
      sessionRepository: repository,
      activeSessionUseCases: activeUseCases,
      stepUseCases: stepUseCases,
      projectionSource: projections,
    ),
  );
}

final class _Harness {
  const _Harness({
    required this.repository,
    required this.projections,
    required this.handler,
  });

  final _FakeActiveSessionRepository repository;
  final _FakeProjectionSource projections;
  final WatchCompanionCommandHandler handler;

  Future<WatchCommandAck> dispatch(
    WatchCommandType type, {
    String commandId = 'command-1',
    int expectedRevision = 1,
  }) {
    return handler.dispatch(
      _command(type, commandId: commandId, expectedRevision: expectedRevision),
    );
  }
}

WatchCommandEnvelope _command(
  WatchCommandType type, {
  String commandId = 'command-1',
  int expectedRevision = 1,
}) {
  return WatchCommandEnvelope(
    commandId: commandId,
    type: type,
    sessionId: 'session-1',
    expectedRevision: expectedRevision,
    sentAtEpochMs: _now.millisecondsSinceEpoch,
  );
}

WatchSessionProjection _projection({
  WatchSessionPhase phase = WatchSessionPhase.ready,
  String deviceSessionId = 'session-1',
  int revision = 1,
  WatchPrimaryAction primaryAction = WatchPrimaryAction.startCurrentExercise,
  List<WatchSecondaryAction> secondaryActions = const [
    WatchSecondaryAction.finishCurrentSet,
    WatchSecondaryAction.skipCurrentSet,
  ],
  bool hasManualScore = false,
  double? currentManualScoreValue,
  bool canDecrementScore = false,
  WatchManualScoreScope? manualScoreScope,
  WatchStepType? stepType,
  int? stepTargetValue,
}) {
  return WatchSessionProjection(
    deviceSessionId: deviceSessionId,
    revision: revision,
    projectedAtEpochMs: _now.millisecondsSinceEpoch,
    phase: phase,
    phoneReachable: true,
    seriesIndex: 1,
    seriesTotal: 2,
    exerciseName: 'Squat',
    primaryAction: primaryAction,
    secondaryActions: secondaryActions,
    hasManualScore: hasManualScore,
    currentManualScoreValue: currentManualScoreValue,
    canDecrementScore: canDecrementScore,
    stepType: stepType,
    stepTargetValue: stepTargetValue,
    manualScoreScope:
        manualScoreScope ??
        (hasManualScore ? WatchManualScoreScope.series : null),
  );
}

ActiveWorkoutSession _session({
  ActiveWorkoutStatus status = ActiveWorkoutStatus.running,
  DateTime? pausedAt,
  int currentSetIndex = 0,
  int setsCount = 2,
  bool timeEnabled = false,
  int? targetReps = 10,
  int restSeconds = 0,
  bool scoreEnabled = false,
  ScoreInputMode scoreInputMode = ScoreInputMode.manual,
  List<ExerciseStep> steps = const [],
}) {
  final exerciseSnapshot = {
    'id': 'exercise-snapshot-1',
    'exerciseNameSnapshot': 'Squat',
    'setsCount': setsCount,
    'timeEnabled': timeEnabled,
    'repsEnabled': true,
    'scoreEnabled': scoreEnabled,
    'targetReps': targetReps,
    'scoreInputModeSnapshot': scoreInputMode.name,
    'restSecondsOverride': restSeconds,
    'exerciseStepsSnapshot': steps
        .map((step) => step.toSnapshotJson())
        .toList(),
    'autoStartNextTimedStepSnapshot': false,
  };
  return ActiveWorkoutSession(
    metadata: _metadata('session-1'),
    status: status,
    startedAt: _now,
    pausedAt: pausedAt,
    lastPersistedAt: _now,
    elapsedActiveMs: 0,
    currentProgramIndex: 0,
    currentExerciseIndex: 0,
    currentSetIndex: currentSetIndex,
    resolvedTemplateSnapshotJson: jsonEncode({
      'programs': [
        {
          'id': 'program-snapshot-1',
          'programNameSnapshot': 'Programme',
          'programSnapshotJson': jsonEncode({
            'exercises': [exerciseSnapshot],
          }),
        },
      ],
    }),
  );
}

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,
}) {
  return ActiveExerciseStepProgressState(
    metadata: _metadata('step-state'),
    activeWorkoutSessionId: sessionId,
    programIndex: 0,
    exerciseIndex: 0,
    setIndex: 0,
    currentPassageIndex: 0,
    currentStepIndex: stepIndex,
    currentStepSnapshotId: stepId,
    status: ActiveExerciseStepProgressStatus.stoppedTimer,
    accumulatedMs: 0,
    lastTransitionAt: _now,
  );
}

ActiveSetTimerState _setTimer({required String sessionId}) {
  return ActiveSetTimerState(
    metadata: _metadata('set'),
    activeWorkoutSessionId: sessionId,
    programIndex: 0,
    exerciseIndex: 0,
    setIndex: 0,
    status: ActiveSetTimerStatus.running,
    startedAt: _now,
    accumulatedMs: 0,
  );
}

EntityMetadata _metadata(String id) {
  return EntityMetadata(
    id: id,
    createdAt: _now,
    updatedAt: _now,
    originDeviceId: 'device-1',
  );
}

final class _FakeProjectionSource implements WatchProjectionSource {
  _FakeProjectionSource(this.projection);

  WatchSessionProjection projection;
  var emitCount = 0;

  @override
  Stream<WatchSessionProjection> get projections => const Stream.empty();

  @override
  Future<WatchSessionProjection> currentProjection() async => projection;

  @override
  Future<WatchSessionProjection> emitCurrentProjection() async {
    emitCount += 1;
    projection = WatchSessionProjection(
      deviceSessionId: projection.deviceSessionId,
      revision: projection.revision + 1,
      projectedAtEpochMs: projection.projectedAtEpochMs,
      expiresAtEpochMs: projection.expiresAtEpochMs,
      phase: projection.phase,
      phoneReachable: projection.phoneReachable,
      seriesIndex: projection.seriesIndex,
      seriesTotal: projection.seriesTotal,
      exerciseName: projection.exerciseName,
      stepType: projection.stepType,
      stepTargetValue: projection.stepTargetValue,
      primaryAction: projection.primaryAction,
      secondaryActions: projection.secondaryActions,
      hasManualScore: projection.hasManualScore,
      currentManualScoreValue: projection.currentManualScoreValue,
      canDecrementScore: projection.canDecrementScore,
      manualScoreTargetValue: projection.manualScoreTargetValue,
      manualScoreTargetLabel: projection.manualScoreTargetLabel,
      manualScoreRepsTargetValue: projection.manualScoreRepsTargetValue,
      manualScoreScope: projection.manualScoreScope,
    );
    return projection;
  }
}

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

  final 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 {
  final templates = <WorkoutTemplate>[];

  @override
  Future<WorkoutTemplate?> findById(String id) async {
    for (final template in templates) {
      if (template.metadata.id == id && template.metadata.deletedAt == null) {
        return template;
      }
    }
    return null;
  }

  @override
  Future<List<WorkoutTemplate>> listActive() async {
    return templates
        .where((template) => template.metadata.deletedAt == null)
        .toList();
  }

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

  @override
  Future<void> save(WorkoutTemplate template) async {
    templates.removeWhere((saved) => saved.metadata.id == template.metadata.id);
    templates.add(template);
  }

  @override
  Future<void> saveAll(List<WorkoutTemplate> templates) async {
    for (final template in templates) {
      await save(template);
    }
  }

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

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

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.removeWhere((item) => item.metadata.id == result.metadata.id);
    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;
  }
}
