import 'dart:convert';

import 'package:drift/drift.dart' as drift;
import 'package:drift/native.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:gametime/application/application.dart';
import 'package:gametime/domain/domain.dart';
import 'package:gametime/infrastructure/local/local.dart' as local;

void main() {
  late local.AppDatabase database;
  late local.DriftExerciseRepository exerciseRepository;
  late local.DriftProgramRepository programRepository;
  late local.DriftActiveSessionRepository activeRepository;
  late local.DriftWorkoutTemplateRepository templateRepository;
  late local.DriftWorkoutHistoryRepository historyRepository;

  setUp(() {
    database = local.AppDatabase(NativeDatabase.memory());
    exerciseRepository = local.DriftExerciseRepository(database);
    programRepository = local.DriftProgramRepository(database);
    activeRepository = local.DriftActiveSessionRepository(database);
    templateRepository = local.DriftWorkoutTemplateRepository(database);
    historyRepository = local.DriftWorkoutHistoryRepository(database);
  });

  tearDown(() async {
    await database.close();
  });

  test('exercise mutations are written to change log', () async {
    final now = DateTime.utc(2026, 7, 17, 12);
    final exercise = Exercise(
      metadata: _metadata('exercise-1', now),
      name: 'Squat',
      hasTimeMeasure: false,
      hasRepsMeasure: true,
      hasScoreMeasure: false,
      defaultTargetReps: 10,
    );

    await exerciseRepository.save(exercise);
    await exerciseRepository.save(
      exercise.copyWith(
        metadata: _metadata(
          'exercise-1',
          now.add(const Duration(seconds: 1)),
          1,
        ),
        name: 'Front squat',
      ),
    );
    await exerciseRepository.save(
      exercise
          .copyWith(
            metadata: _metadata(
              'exercise-1',
              now.add(const Duration(seconds: 2)),
              2,
            ),
            name: 'Front squat',
          )
          .archive(now.add(const Duration(seconds: 2))),
    );

    final changes =
        await (database.select(database.changeLogEntries)
              ..where((table) => table.entityId.equals('exercise-1'))
              ..orderBy([
                (table) => drift.OrderingTerm.asc(table.localRevision),
              ]))
            .get();

    expect(changes.map((change) => change.operation), [
      'insert',
      'update',
      'update',
    ]);
    expect(changes.every((change) => change.entityType == 'Exercise'), isTrue);
  });

  test('exercise repository accepts zero manual default score', () async {
    final exercise = Exercise(
      metadata: _metadata('exercise-score-zero', DateTime.utc(2026, 7, 17, 12)),
      name: 'Score nul',
      hasTimeMeasure: false,
      hasRepsMeasure: false,
      hasScoreMeasure: true,
      scoreLabel: 'Score',
      scoreUnit: 'pts',
      defaultTargetScore: 0,
    );

    await exerciseRepository.save(exercise);

    final restored = await exerciseRepository.findById(exercise.metadata.id);
    expect(restored, isNotNull);
    expect(restored!.defaultTargetScore, 0);
  });

  test(
    'step chaining settings round-trip through drift repositories',
    () async {
      final now = DateTime.utc(2026, 7, 17, 12);
      final exercise = Exercise(
        metadata: _metadata('exercise-chain-1', now),
        name: 'Circuit',
        hasTimeMeasure: false,
        hasRepsMeasure: true,
        hasScoreMeasure: false,
        defaultTargetReps: 10,
        autoStartNextTimedStep: false,
      );
      await exerciseRepository.save(exercise);

      final restoredExercise = await exerciseRepository.findById(
        exercise.metadata.id,
      );
      expect(restoredExercise!.autoStartNextTimedStep, isFalse);

      final programExercise = _programExercise(
        'program-exercise-chain-1',
        now,
        programId: 'program-chain-1',
        position: 0,
        autoStartNextTimedStepSnapshot: false,
        autoStartNextTimedStepOverride: true,
      );
      await programRepository.save(
        Program(
          metadata: _metadata('program-chain-1', now),
          name: 'Programme',
          defaultRestSeconds: 60,
          exercises: [programExercise],
        ),
      );

      final restoredProgram = await programRepository.findById(
        'program-chain-1',
      );
      expect(
        restoredProgram!.exercises.single.autoStartNextTimedStepSnapshot,
        isFalse,
      );
      expect(
        restoredProgram.exercises.single.autoStartNextTimedStepOverride,
        isTrue,
      );

      final templateProgram = WorkoutTemplateProgram(
        metadata: _metadata('template-program-chain-1', now),
        workoutTemplateId: 'template-chain-1',
        sourceProgramId: 'program-chain-1',
        position: 0,
        programNameSnapshot: 'Programme',
        defaultRestSecondsSnapshot: 60,
        programSnapshotJson: jsonEncode({
          'exercises': [programExercise.toSnapshotJson()],
        }),
      );
      await templateRepository.save(
        WorkoutTemplate(
          metadata: _metadata('template-chain-1', now),
          name: 'Séance',
          programs: [templateProgram],
          overrides: [
            WorkoutTemplateExerciseOverride(
              metadata: _metadata('override-chain-1', now),
              workoutTemplateProgramId: templateProgram.metadata.id,
              snapshotProgramExerciseId: programExercise.metadata.id,
            ),
            WorkoutTemplateExerciseOverride(
              metadata: _metadata('override-chain-2', now),
              workoutTemplateProgramId: templateProgram.metadata.id,
              snapshotProgramExerciseId: 'program-exercise-chain-2',
              autoStartNextTimedStepOverride: false,
            ),
          ],
        ),
      );

      final restoredTemplate = await templateRepository.findById(
        'template-chain-1',
      );
      expect(
        restoredTemplate!.overrides
            .singleWhere(
              (override) => override.metadata.id == 'override-chain-1',
            )
            .autoStartNextTimedStepOverride,
        isNull,
      );
      expect(
        restoredTemplate.overrides
            .singleWhere(
              (override) => override.metadata.id == 'override-chain-2',
            )
            .autoStartNextTimedStepOverride,
        isFalse,
      );
    },
  );

  test('exercise repository round-trips configured steps', () async {
    final now = DateTime.utc(2026, 7, 17, 12);
    final exercise = Exercise(
      metadata: _metadata('exercise-steps-1', now),
      name: 'Burpee complexe',
      hasTimeMeasure: false,
      hasRepsMeasure: true,
      hasScoreMeasure: false,
      defaultTargetReps: 1,
      steps: [
        ExerciseStep(
          id: 'step-1',
          position: 0,
          name: 'Planche',
          type: ExerciseStepType.time,
          defaultTargetValue: 20,
        ),
        ExerciseStep(
          id: 'step-2',
          position: 1,
          name: 'Sauts',
          type: ExerciseStepType.reps,
          defaultTargetValue: 8,
          hasScore: true,
          scoreLabel: 'Amplitude',
          scoreUnit: 'pts',
          defaultTargetScore: 0,
        ),
        ExerciseStep(
          id: 'step-3',
          position: 2,
          name: 'Sprint final',
          type: ExerciseStepType.time,
          defaultTargetValue: 10,
          hasScore: true,
          scoreInputMode: ScoreInputMode.stopwatch,
          defaultTargetScoreTimeMs: 12000,
        ),
      ],
    );

    await exerciseRepository.save(exercise);

    final restored = await exerciseRepository.findById(exercise.metadata.id);

    expect(restored, isNotNull);
    expect(restored!.steps, hasLength(3));
    expect(restored.steps.map((step) => step.name), [
      'Planche',
      'Sauts',
      'Sprint final',
    ]);
    expect(restored.steps[1].defaultTargetScore, 0);
    expect(restored.steps[2].scoreInputMode, ScoreInputMode.stopwatch);
    expect(restored.steps[2].defaultTargetScoreTimeMs, 12000);
  });

  test(
    'program exercise step snapshot is preserved when source exercise changes',
    () async {
      final now = DateTime.utc(2026, 7, 17, 12);
      final source = Exercise(
        metadata: _metadata('exercise-source-1', now),
        name: 'Complexe original',
        hasTimeMeasure: false,
        hasRepsMeasure: true,
        hasScoreMeasure: false,
        defaultTargetReps: 1,
        steps: [
          ExerciseStep(
            id: 'step-original-1',
            position: 0,
            name: 'Phase originale A',
            type: ExerciseStepType.time,
            defaultTargetValue: 30,
          ),
          ExerciseStep(
            id: 'step-original-2',
            position: 1,
            name: 'Phase originale B',
            type: ExerciseStepType.reps,
            defaultTargetValue: 12,
          ),
        ],
      );
      await exerciseRepository.save(source);
      await programRepository.save(
        Program(
          metadata: _metadata('program-steps-1', now),
          name: 'Programme étapes',
          defaultRestSeconds: 60,
        ),
      );
      final useCase = ProgramUseCases(
        programRepository: programRepository,
        exerciseRepository: exerciseRepository,
        templateRepository: templateRepository,
        clock: _FakeClock(now),
        ids: _FakeIds(),
        originDeviceId: 'device-1',
      );

      final snapshot = await useCase.addExercise(
        programId: 'program-steps-1',
        exerciseId: source.metadata.id,
        position: 0,
        setsCount: 1,
        enabledMeasures: const {WorkoutMeasure.reps},
      );
      await exerciseRepository.save(
        source.copyWith(
          metadata: _metadata(
            source.metadata.id,
            now.add(const Duration(minutes: 1)),
            1,
          ),
          steps: [
            ExerciseStep(
              id: 'step-updated-1',
              position: 0,
              name: 'Phase modifiée',
              type: ExerciseStepType.time,
              defaultTargetValue: 45,
            ),
          ],
        ),
      );

      final restoredProgram = await programRepository.findById(
        'program-steps-1',
      );
      final restoredSnapshot = restoredProgram!.exercises.single;
      final snapshotJson = snapshot.toSnapshotJson();

      expect(restoredSnapshot.exerciseStepsSnapshot, hasLength(2));
      expect(
        restoredSnapshot.exerciseStepsSnapshot.first.name,
        'Phase originale A',
      );
      expect(
        restoredSnapshot.exerciseStepsSnapshot.last.type,
        ExerciseStepType.reps,
      );
      expect(
        (snapshotJson['exerciseStepsSnapshot'] as List).map(
          (step) => (step as Map)['name'],
        ),
        ['Phase originale A', 'Phase originale B'],
      );
    },
  );

  test(
    'removing a program exercise writes a soft delete change log entry',
    () async {
      final now = DateTime.utc(2026, 7, 17, 12);
      final first = _programExercise('program-exercise-1', now, position: 0);
      final second = _programExercise('program-exercise-2', now, position: 1);
      await programRepository.save(
        Program(
          metadata: _metadata('program-1', now),
          name: 'Jambes',
          defaultRestSeconds: 60,
          exercises: [first, second],
        ),
      );

      final deletedAt = now.add(const Duration(minutes: 1));
      await programRepository.replaceExercises(
        Program(
          metadata: _metadata('program-1', deletedAt, 1),
          name: 'Jambes',
          defaultRestSeconds: 60,
          exercises: [first],
        ),
        deletedAt,
      );

      final deletedRow = await (database.select(
        database.programExercises,
      )..where((table) => table.id.equals('program-exercise-2'))).getSingle();
      final change =
          await (database.select(database.changeLogEntries)..where(
                (table) =>
                    table.entityType.equals('ProgramExercise') &
                    table.entityId.equals('program-exercise-2') &
                    table.operation.equals('softDelete'),
              ))
              .getSingleOrNull();

      expect(deletedRow.deletedAt?.toUtc(), deletedAt);
      expect(deletedRow.syncState, 'deleted');
      expect(deletedRow.localRevision, 1);
      expect(change, isNotNull);
      expect(change!.localRevision, 1);
    },
  );

  test(
    'running session elapsed time survives repository reconstruction',
    () async {
      final persistedAt = DateTime.utc(2026, 7, 17, 12);
      final session = ActiveWorkoutSession(
        metadata: _metadata('session-1', persistedAt),
        sourceWorkoutTemplateId: null,
        status: ActiveWorkoutStatus.running,
        startedAt: persistedAt.subtract(const Duration(seconds: 30)),
        lastPersistedAt: persistedAt,
        elapsedActiveMs: 30000,
        currentProgramIndex: 0,
        currentExerciseIndex: 0,
        currentSetIndex: 0,
        resolvedTemplateSnapshotJson: _resolvedSnapshot(),
      );

      await activeRepository.save(session);

      final afterAppKillRepository = local.DriftActiveSessionRepository(
        database,
      );
      final afterAppKillUseCases = ActiveWorkoutSessionUseCases(
        sessionRepository: afterAppKillRepository,
        templateRepository: templateRepository,
        clock: _FakeClock(persistedAt.add(const Duration(seconds: 20))),
        ids: _FakeIds(),
        originDeviceId: 'device-1',
      );

      final restored = await afterAppKillRepository.findOpen();

      expect(restored, isNotNull);
      expect(afterAppKillUseCases.elapsedActiveMilliseconds(restored!), 50000);
    },
  );

  test(
    'closing a session stores autonomous history rows with set snapshots',
    () async {
      final now = DateTime.utc(2026, 7, 17, 12);
      final template = WorkoutTemplate(
        metadata: _metadata('template-1', now),
        name: 'Séance jambes',
      );
      await templateRepository.save(template);

      final session = ActiveWorkoutSession(
        metadata: _metadata('session-1', now),
        sourceWorkoutTemplateId: template.metadata.id,
        status: ActiveWorkoutStatus.running,
        startedAt: now.subtract(const Duration(minutes: 10)),
        lastPersistedAt: now,
        elapsedActiveMs: 600000,
        currentProgramIndex: 0,
        currentExerciseIndex: 0,
        currentSetIndex: 0,
        resolvedTemplateSnapshotJson: _resolvedSnapshot(),
      );
      await activeRepository.save(session);
      await activeRepository.saveSetResult(
        ActiveSetResult(
          metadata: _metadata('active-result-1', now),
          activeWorkoutSessionId: session.metadata.id,
          programSnapshotId: 'program-snapshot-1',
          exerciseSnapshotId: 'exercise-snapshot-1',
          programIndex: 0,
          exerciseIndex: 0,
          setIndex: 0,
          actualTimeMs: 45000,
          actualReps: 10,
          actualScore: 80,
          scoreLabelSnapshot: 'Charge',
          scoreUnitSnapshot: 'kg',
          completedAt: now,
        ),
      );

      final closeUseCase = CloseWorkoutSessionUseCase(
        sessionRepository: activeRepository,
        historyRepository: historyRepository,
        clock: _FakeClock(now.add(const Duration(seconds: 5))),
        ids: _FakeIds(),
        originDeviceId: 'device-1',
      );

      final history = await closeUseCase.close(
        sessionId: session.metadata.id,
        nameSnapshot: 'Séance jambes',
        completed: true,
      );
      await templateRepository.save(
        WorkoutTemplate(
          metadata: template.metadata.markDeleted(now),
          name: template.name,
        ),
      );

      final restored = await historyRepository.findById(history.metadata.id);

      expect(restored, isNotNull);
      expect(restored!.nameSnapshot, 'Séance jambes');
      expect(restored.results, hasLength(1));
      expect(restored.results.single.programNameSnapshot, 'Programme jambes');
      expect(restored.results.single.exerciseNameSnapshot, 'Squat');
      expect(restored.results.single.actualTimeMs, 45000);
      expect(restored.results.single.actualReps, 10);
      expect(restored.results.single.actualScore, 80);
      expect(restored.results.single.scoreUnitSnapshot, 'kg');
    },
  );
}

String _resolvedSnapshot() {
  return jsonEncode({
    'name': 'Séance jambes',
    'programs': [
      {
        'id': 'program-snapshot-1',
        'programNameSnapshot': 'Programme jambes',
        'programSnapshotJson': jsonEncode({
          'exercises': [
            {
              'id': 'exercise-snapshot-1',
              'exerciseNameSnapshot': 'Squat',
              'setsCount': 1,
              'timeEnabled': true,
              'repsEnabled': true,
              'scoreEnabled': true,
              'targetTimeSeconds': 45,
              'targetReps': 10,
              'targetScore': 80,
              'scoreLabelSnapshot': 'Charge',
              'scoreUnitSnapshot': 'kg',
              'restSecondsOverride': 0,
            },
          ],
        }),
      },
    ],
    'overrides': const [],
  });
}

EntityMetadata _metadata(String id, DateTime now, [int localRevision = 0]) {
  return EntityMetadata(
    id: id,
    createdAt: now,
    updatedAt: now,
    originDeviceId: 'device-1',
    localRevision: localRevision,
  );
}

ProgramExercise _programExercise(
  String id,
  DateTime now, {
  String programId = 'program-1',
  required int position,
  bool autoStartNextTimedStepSnapshot = true,
  bool? autoStartNextTimedStepOverride,
}) {
  return ProgramExercise(
    metadata: _metadata(id, now),
    programId: programId,
    position: position,
    exerciseNameSnapshot: 'Exercise $position',
    autoStartNextTimedStepSnapshot: autoStartNextTimedStepSnapshot,
    autoStartNextTimedStepOverride: autoStartNextTimedStepOverride,
    availableTimeSnapshot: false,
    availableRepsSnapshot: true,
    availableScoreSnapshot: false,
    setsCount: 1,
    timeEnabled: false,
    repsEnabled: true,
    scoreEnabled: false,
  );
}

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';
  }
}
