import 'dart:convert';

import 'package:test/test.dart';
import 'package:watch_bridge_contract/watch_bridge_contract.dart';

void main() {
  group('WatchCommandEnvelope', () {
    test('round-trips every command type through JSON', () {
      for (final type in WatchCommandType.values) {
        final command = WatchCommandEnvelope(
          commandId: 'command-${type.name}',
          type: type,
          sessionId: 'session-1',
          expectedRevision: 12,
          sentAtEpochMs: 1710000000000,
        );

        final decoded = WatchCommandEnvelope.fromJson(
          jsonDecode(jsonEncode(command.toJson())) as Map<String, Object?>,
        );

        expect(decoded, command);
      }
    });

    test('ignores unknown fields and falls back for missing fields', () {
      final command = WatchCommandEnvelope.fromJson({
        'type': 'pauseSession',
        'unknown': 'ignored',
      });

      expect(command.schemaVersion, watchBridgeSchemaVersion);
      expect(command.commandId, '');
      expect(command.type, WatchCommandType.pauseSession);
      expect(command.sessionId, '');
      expect(command.expectedRevision, 0);
      expect(command.sentAtEpochMs, 0);
    });
  });

  group('WatchCommandAck', () {
    test('round-trips every ack enum value by stable JSON name', () {
      for (final ack in WatchCommandAck.values) {
        final encoded = jsonEncode(ack.name);
        final decodedName = jsonDecode(encoded) as String;
        final decoded = WatchCommandAck.values.singleWhere(
          (value) => value.name == decodedName,
        );

        expect(decoded, ack);
      }
    });
  });

  group('WatchAlertEnvelope', () {
    test('round-trips every alert kind through JSON', () {
      for (final kind in WatchAlertKind.values) {
        final alert = WatchAlertEnvelope(
          alertId: 'alert-${kind.name}',
          sessionId: 'session-1',
          revision: 7,
          kind: kind,
          timerKind: WatchTimerKind.step,
          programIndex: 0,
          exerciseIndex: 1,
          setIndex: 2,
          passageIndex: 3,
          stepIndex: 4,
          scheduledForEpochMs: 1710000000700,
          expiresAtEpochMs: 1710000002700,
          pattern: kind == WatchAlertKind.countdownTick
              ? WatchAlertPattern.countdownTick
              : WatchAlertPattern.timerFinished,
        );

        final decoded = WatchAlertEnvelope.fromJson(
          jsonDecode(jsonEncode(alert.toJson())) as Map<String, Object?>,
        );

        expect(decoded, alert);
      }
    });

    test('falls back safely for absent fields', () {
      final alert = WatchAlertEnvelope.fromJson({});

      expect(alert.schemaVersion, watchBridgeSchemaVersion);
      expect(alert.alertId, '');
      expect(alert.sessionId, '');
      expect(alert.revision, 0);
      expect(alert.kind, WatchAlertKind.countdownTick);
      expect(alert.timerKind, WatchTimerKind.step);
      expect(alert.programIndex, isNull);
      expect(alert.exerciseIndex, isNull);
      expect(alert.setIndex, isNull);
      expect(alert.passageIndex, isNull);
      expect(alert.stepIndex, isNull);
      expect(alert.scheduledForEpochMs, 0);
      expect(alert.expiresAtEpochMs, 0);
      expect(alert.pattern, WatchAlertPattern.countdownTick);
    });
  });

  group('WatchSensorSummary', () {
    test('round-trips heart rate summary through JSON', () {
      const summary = WatchSensorSummary(
        sessionId: 'session-1',
        sampleCount: 12,
        minHeartRateBpm: 91,
        averageHeartRateBpm: 128.5,
        maxHeartRateBpm: 174,
        totalDistanceMeters: 842.4,
        totalCaloriesKcal: 186.2,
      );

      final decoded = WatchSensorSummary.fromJson(
        jsonDecode(jsonEncode(summary.toJson())) as Map<String, Object?>,
      );

      expect(decoded, summary);
    });

    test('falls back safely for absent fields', () {
      final summary = WatchSensorSummary.fromJson({});

      expect(summary.schemaVersion, watchBridgeSchemaVersion);
      expect(summary.sessionId, '');
      expect(summary.sampleCount, 0);
      expect(summary.minHeartRateBpm, isNull);
      expect(summary.averageHeartRateBpm, isNull);
      expect(summary.maxHeartRateBpm, isNull);
      expect(summary.totalDistanceMeters, isNull);
      expect(summary.totalCaloriesKcal, isNull);
    });

    test('accepts legacy live metric keys for summary totals', () {
      final summary = WatchSensorSummary.fromJson({
        'sessionId': 'session-1',
        'sampleCount': 4,
        'minHeartRateBpm': 91,
        'averageHeartRateBpm': 128.5,
        'maxHeartRateBpm': 174,
        'distanceMeters': 842.4,
        'caloriesKcal': 186.2,
      });

      expect(summary.totalDistanceMeters, 842.4);
      expect(summary.totalCaloriesKcal, 186.2);
    });
  });

  group('WatchSensorSample', () {
    test('round-trips live telemetry sample through JSON', () {
      const sample = WatchSensorSample(
        sampleId: 'sample-1',
        sessionId: 'session-1',
        capturedAtEpochMs: 1710000000300,
        programIndex: 0,
        exerciseIndex: 1,
        setIndex: 2,
        passageIndex: 3,
        stepIndex: 4,
        programSnapshotId: 'program-snapshot-1',
        exerciseSnapshotId: 'exercise-snapshot-1',
        stepSnapshotId: 'step-snapshot-1',
        heartRateBpm: 142,
        distanceMeters: 840.5,
        caloriesKcal: 184.2,
        sensorCollectionStatus: 'active',
      );

      final decoded = WatchSensorSample.fromJson(
        jsonDecode(jsonEncode(sample.toJson())) as Map<String, Object?>,
      );

      expect(decoded, sample);
    });

    test('falls back safely for absent fields', () {
      final sample = WatchSensorSample.fromJson({});

      expect(sample.schemaVersion, watchBridgeSchemaVersion);
      expect(sample.sampleId, isNull);
      expect(sample.sessionId, '');
      expect(sample.recordedAtEpochMs, 0);
      expect(sample.capturedAtEpochMs, 0);
      expect(sample.programIndex, isNull);
      expect(sample.exerciseIndex, isNull);
      expect(sample.setIndex, isNull);
      expect(sample.passageIndex, isNull);
      expect(sample.stepIndex, isNull);
      expect(sample.heartRateBpm, isNull);
      expect(sample.distanceMeters, isNull);
      expect(sample.caloriesKcal, isNull);
    });

    test('accepts legacy recordedAtEpochMs as captured timestamp', () {
      final sample = WatchSensorSample.fromJson({
        'sessionId': 'session-1',
        'recordedAtEpochMs': 1710000000300,
      });

      expect(sample.capturedAtEpochMs, 1710000000300);
      expect(sample.recordedAtEpochMs, 1710000000300);
    });
  });

  group('WatchSessionProjection', () {
    test('round-trips every phase, primary action, and secondary action', () {
      for (final phase in WatchSessionPhase.values) {
        for (final primaryAction in WatchPrimaryAction.values) {
          final projection = WatchSessionProjection(
            deviceSessionId: 'session-${phase.name}-${primaryAction.name}',
            revision: 4,
            projectedAtEpochMs: 1710000000100,
            expiresAtEpochMs: 1710000012100,
            phase: phase,
            phoneReachable: true,
            seriesIndex: 2,
            seriesTotal: 5,
            exerciseName: 'Pompes tempo',
            programIndex: 0,
            exerciseIndex: 1,
            setIndex: 1,
            passageIndex: 1,
            passageTotal: 3,
            stepIndex: 2,
            stepTotal: 4,
            stepName: 'Descente',
            stepType: WatchStepType.reps,
            stepTargetValue: 12,
            dominantTimer: _stepTimer(),
            secondaryTimers: [_scoreStopwatchTimer(), _setTimer()],
            primaryAction: primaryAction,
            secondaryActions: WatchSecondaryAction.values,
            nextExerciseName: 'Fentes sautees',
            statusLabel: 'Chrono etape',
            hasManualScore: true,
            currentManualScoreValue: 7.5,
            canDecrementScore: true,
            manualScoreTargetValue: 10,
            manualScoreTargetLabel: 'Cible',
            manualScoreRepsTargetValue: 12,
            manualScoreScope: WatchManualScoreScope.step,
            healthServicesExerciseTypeStrategy: const [
              'RUNNING',
              'HIGH_INTENSITY_INTERVAL_TRAINING',
              'WORKOUT',
            ],
          );

          final decoded = WatchSessionProjection.fromJson(
            jsonDecode(jsonEncode(projection.toJson())) as Map<String, Object?>,
          );

          expect(decoded, projection);
        }
      }
    });

    test('ignores unknown fields and accepts absent optional fields', () {
      final projection = WatchSessionProjection.fromJson({
        'schemaVersion': 1,
        'deviceSessionId': 'session-1',
        'revision': 9,
        'projectedAtEpochMs': 1710000000200,
        'phase': 'restPaused',
        'phoneReachable': true,
        'seriesIndex': 3,
        'seriesTotal': 5,
        'exerciseName': 'Burpees',
        'primaryAction': 'resumeSession',
        'secondaryActions': ['skipCurrentRest', 'futureAction'],
        'extra': {'ignored': true},
      });

      expect(projection.phase, WatchSessionPhase.restPaused);
      expect(projection.passageIndex, isNull);
      expect(projection.stepIndex, isNull);
      expect(projection.stepType, isNull);
      expect(projection.stepTargetValue, isNull);
      expect(projection.dominantTimer, isNull);
      expect(projection.secondaryTimers, isEmpty);
      expect(projection.primaryAction, WatchPrimaryAction.resumeSession);
      expect(projection.secondaryActions, [
        WatchSecondaryAction.skipCurrentRest,
      ]);
      expect(projection.nextExerciseName, isNull);
      expect(projection.statusLabel, isNull);
      expect(projection.hasManualScore, isFalse);
      expect(projection.currentManualScoreValue, isNull);
      expect(projection.canDecrementScore, isFalse);
      expect(projection.manualScoreTargetValue, isNull);
      expect(projection.manualScoreTargetLabel, isNull);
      expect(projection.manualScoreRepsTargetValue, isNull);
      expect(projection.manualScoreScope, isNull);
      expect(projection.healthServicesExerciseTypeStrategy, isEmpty);
    });

    test('falls back to neutral values for absent required fields', () {
      final projection = WatchSessionProjection.fromJson({});

      expect(projection.schemaVersion, watchBridgeSchemaVersion);
      expect(projection.deviceSessionId, '');
      expect(projection.revision, 0);
      expect(projection.projectedAtEpochMs, 0);
      expect(projection.expiresAtEpochMs, 0);
      expect(projection.phase, WatchSessionPhase.noActiveSession);
      expect(projection.phoneReachable, false);
      expect(projection.seriesIndex, 0);
      expect(projection.seriesTotal, 0);
      expect(projection.exerciseName, '');
      expect(projection.primaryAction, WatchPrimaryAction.none);
      expect(projection.secondaryActions, isEmpty);
      expect(projection.stepType, isNull);
      expect(projection.stepTargetValue, isNull);
      expect(projection.hasManualScore, isFalse);
      expect(projection.currentManualScoreValue, isNull);
      expect(projection.canDecrementScore, isFalse);
      expect(projection.manualScoreTargetValue, isNull);
      expect(projection.manualScoreTargetLabel, isNull);
      expect(projection.manualScoreRepsTargetValue, isNull);
      expect(projection.manualScoreScope, isNull);
      expect(projection.healthServicesExerciseTypeStrategy, isEmpty);
    });
  });

  group('WatchTimerProjection', () {
    test('round-trips every timer enum combination through JSON', () {
      for (final kind in WatchTimerKind.values) {
        for (final displayMode in WatchTimerDisplayMode.values) {
          for (final runState in WatchTimerRunState.values) {
            final timer = WatchTimerProjection(
              kind: kind,
              label: 'timer-${kind.name}',
              displayMode: displayMode,
              runState: runState,
              referenceEpochMs: 1710000000300,
              accumulatedMs: 42000,
              startedAtEpochMs: 1710000000000,
              targetMs: 90000,
            );

            final decoded = WatchTimerProjection.fromJson(
              jsonDecode(jsonEncode(timer.toJson())) as Map<String, Object?>,
            );

            expect(decoded, timer);
          }
        }
      }
    });

    test('ignores unknown fields and falls back for missing fields', () {
      final timer = WatchTimerProjection.fromJson({
        'kind': 'rest',
        'displayMode': 'countdown',
        'runState': 'paused',
        'unknown': 'ignored',
      });

      expect(timer.kind, WatchTimerKind.rest);
      expect(timer.label, '');
      expect(timer.displayMode, WatchTimerDisplayMode.countdown);
      expect(timer.runState, WatchTimerRunState.paused);
      expect(timer.referenceEpochMs, 0);
      expect(timer.accumulatedMs, 0);
      expect(timer.startedAtEpochMs, isNull);
      expect(timer.targetMs, isNull);
    });
  });
}

WatchTimerProjection _stepTimer() {
  return const WatchTimerProjection(
    kind: WatchTimerKind.step,
    label: 'Chrono etape',
    displayMode: WatchTimerDisplayMode.countdown,
    runState: WatchTimerRunState.running,
    referenceEpochMs: 1710000000000,
    accumulatedMs: 18000,
    startedAtEpochMs: 1710000000000,
    targetMs: 20000,
  );
}

WatchTimerProjection _scoreStopwatchTimer() {
  return const WatchTimerProjection(
    kind: WatchTimerKind.scoreStopwatch,
    label: 'Score chrono',
    displayMode: WatchTimerDisplayMode.elapsed,
    runState: WatchTimerRunState.running,
    referenceEpochMs: 1710000000000,
    accumulatedMs: 51000,
    startedAtEpochMs: 1710000000000,
  );
}

WatchTimerProjection _setTimer() {
  return const WatchTimerProjection(
    kind: WatchTimerKind.setTimer,
    label: 'Temps de serie',
    displayMode: WatchTimerDisplayMode.elapsed,
    runState: WatchTimerRunState.running,
    referenceEpochMs: 1710000000000,
    accumulatedMs: 102000,
    startedAtEpochMs: 1710000000000,
  );
}
