health_local_data_convert_test.dart 11.2 KB
import 'package:doublefeel_flutter/core/services/health_raw_data_core_service.dart';
import 'package:doublefeel_flutter/data/datasource/health/health_local_data_convert.dart';
import 'package:doublefeel_flutter/pigeon/health_kit_raw_data_api.g.dart';
import 'package:flutter_test/flutter_test.dart';

void main() {
  group('LocalHealthDataConvert sleep intervals', () {
    test('keeps previous-day sleep start for next-day sleep statistics', () {
      final july15 = DateTime(2026, 7, 15);
      final july16 = DateTime(2026, 7, 16);
      final sleepStart = DateTime(2026, 7, 15, 23, 56);
      final midnight = DateTime(2026, 7, 16);
      final sleepEnd = DateTime(2026, 7, 16, 7);
      final intervals = [
        HealthKitRawDataPoint(
          dataType: 3,
          startTime: LocalHealthDataConvert.unixSeconds(sleepStart),
          endTime: LocalHealthDataConvert.unixSeconds(midnight),
        ),
        HealthKitRawDataPoint(
          dataType: 3,
          startTime: LocalHealthDataConvert.unixSeconds(midnight),
          endTime: LocalHealthDataConvert.unixSeconds(sleepEnd),
        ),
      ];

      final previousDayIntervals =
          LocalHealthDataConvert.completeSleepIntervalsForDay(
        july15,
        intervals,
      );
      final currentDayIntervals =
          LocalHealthDataConvert.completeSleepIntervalsForDay(
        july16,
        intervals,
      );
      final statistics = LocalHealthDataConvert.sleepStatistics(
        dateRangeType: 3,
        days: [july16],
        previousDays: const [],
        sleepIntervals: intervals,
        heartRate: const [],
      );

      expect(previousDayIntervals, isEmpty);
      expect(currentDayIntervals, hasLength(2));
      expect(
        currentDayIntervals.first.startTime,
        LocalHealthDataConvert.unixSeconds(sleepStart),
      );
      expect(
        statistics.asleepTimeTrendList?.single.asleepTime,
        LocalHealthDataConvert.unixSeconds(sleepStart),
      );
      expect(
        statistics.sleepTrendList?.single.totalTime,
        const Duration(hours: 7, minutes: 4).inSeconds,
      );
    });

    test('sets sleep duration to zero when sleep score is unavailable', () {
      final day = DateTime(2026, 7, 16);
      final awakeStart = DateTime(2026, 7, 16, 1);
      final awakeEnd = DateTime(2026, 7, 16, 2);
      final statistics = LocalHealthDataConvert.sleepStatistics(
        dateRangeType: 3,
        days: [day],
        previousDays: const [],
        sleepIntervals: [
          HealthKitRawDataPoint(
            dataType: 2,
            startTime: LocalHealthDataConvert.unixSeconds(awakeStart),
            endTime: LocalHealthDataConvert.unixSeconds(awakeEnd),
          ),
        ],
        heartRate: const [],
      );

      expect(statistics.sleepTrendList?.single.totalTime, 0);
      expect(statistics.sleepTrendList?.single.score, isNull);
      expect(statistics.sleepTrendList?.single.sleepEvaluate, isNull);
      expect(statistics.avgSleepDuration, isNull);
    });
  });

  group('LocalHealthDataConvert v2StressScore', () {
    test('uses median and p75 after excluding confirmed activity buffer', () {
      const userId = 1;
      const dayStart = 100000;
      const dayEnd = dayStart + Duration.secondsPerDay - 1;
      const flagsNone = HealthRawPointFlags.none();
      const workoutFlags = HealthRawPointFlags(
        isWorkout: true,
        isWorkoutRecovery: false,
        isSleepLikely: false,
        isSuspectedActivity: false,
      );
      const suspectedFlags = HealthRawPointFlags(
        isWorkout: false,
        isWorkoutRecovery: false,
        isSleepLikely: false,
        isSuspectedActivity: true,
      );

      HealthRawRealtimeStressPoint point(
        int offset,
        double value, {
        HealthRawPointFlags flags = flagsNone,
      }) {
        return HealthRawRealtimeStressPoint(
          userId: userId,
          rawEndTime: dayStart + offset,
          result: value,
          sourceStartTime: dayStart + offset,
          sourceEndTime: dayStart + offset,
          flags: flags,
        );
      }

      final score = LocalHealthDataConvert.v2StressScore(
        [
          point(600, 10),
          point(1200, 20, flags: suspectedFlags),
          point(1800, 30),
          point(2000, 40),
          point(3000, 90, flags: workoutFlags),
          point(3300, 100),
          point(4200, 80),
          point(4800, 110),
        ],
        startTime: dayStart,
        endTime: dayEnd,
      );

      expect(score.comprehensiveScore, 34);
      expect(score.state, HealthRawStressState.normal.value);
    });

    test('uses activity buffer from adjacent day', () {
      const userId = 1;
      const dayStart = 100000;
      const dayEnd = dayStart + Duration.secondsPerDay - 1;
      const workoutFlags = HealthRawPointFlags(
        isWorkout: true,
        isWorkoutRecovery: false,
        isSleepLikely: false,
        isSuspectedActivity: false,
      );

      final score = LocalHealthDataConvert.v2StressScore(
        [
          HealthRawRealtimeStressPoint(
            userId: userId,
            rawEndTime: dayStart - 300,
            result: 80,
            sourceStartTime: dayStart - 300,
            sourceEndTime: dayStart - 300,
            flags: workoutFlags,
          ),
          HealthRawRealtimeStressPoint(
            userId: userId,
            rawEndTime: dayStart + 100,
            result: 90,
            sourceStartTime: dayStart + 100,
            sourceEndTime: dayStart + 100,
          ),
          HealthRawRealtimeStressPoint(
            userId: userId,
            rawEndTime: dayStart + 1000,
            result: 20,
            sourceStartTime: dayStart + 1000,
            sourceEndTime: dayStart + 1000,
          ),
        ],
        startTime: dayStart,
        endTime: dayEnd,
      );

      expect(score.comprehensiveScore, 20);
      expect(score.state, HealthRawStressState.excellent.value);
    });
  });

  group('LocalHealthDataConvert hrvStatistics', () {
    test('uses daily comprehensive stress for trend and distributions', () {
      final currentDay = DateTime(2026, 7, 16);
      final previousDay = DateTime(2026, 7, 9);
      final currentStart = LocalHealthDataConvert.unixSeconds(currentDay);
      final previousStart = LocalHealthDataConvert.unixSeconds(previousDay);

      HealthRawHrvStressPoint hrvPoint(int time, double value) {
        return HealthRawHrvStressPoint(
          userId: 1,
          rawEndTime: time,
          rawHrv: value,
          result: value,
          sourceStartTime: time,
          sourceEndTime: time,
          state: HealthRawStressState.excellent,
          baselineHrv: 50,
          baselineAwakeHrv: 50,
          baselineSleepHrv: null,
          baselineRestingHr: 65,
        );
      }

      HealthRawRealtimeStressPoint stressPoint(int time, double value) {
        return HealthRawRealtimeStressPoint(
          userId: 1,
          rawEndTime: time,
          result: value,
          sourceStartTime: time,
          sourceEndTime: time,
        );
      }

      final statistics = LocalHealthDataConvert.hrvStatistics(
        dateRangeType: 0,
        days: [currentDay],
        previousDays: [previousDay],
        hrvPoints: [
          hrvPoint(currentStart + 600, 80),
        ],
        realtimePoints: [
          stressPoint(currentStart + 600, 10),
          stressPoint(currentStart + 1200, 80),
          stressPoint(previousStart + 600, 90),
        ],
      );

      expect(statistics.hrvTrendList?.single.hrvAverage, 80);
      expect(
        statistics.hrvTrendList?.single.state,
        HealthRawStressState.normal.value,
      );
      expect(statistics.hrvDistributionList?.single.stressId,
          HealthRawStressState.normal.value);
      expect(statistics.hrvDistributionList?.single.dayCounts, 1);
      expect(statistics.qoqHrvDistributionList?.single.stressState,
          HealthRawStressState.overload.value);
      expect(statistics.qoqHrvDistributionList?.single.dayCounts, 1);
      expect(statistics.dailyDistributionList, isNull);
    });

    test('keeps yearly months that only have comprehensive stress data', () {
      final mayDay = DateTime(2026, 5, 12);
      final mayStart = LocalHealthDataConvert.unixSeconds(mayDay);

      final statistics = LocalHealthDataConvert.hrvStatistics(
        dateRangeType: 2,
        days: LocalHealthDataConvert.rangeDays(2, 20260101),
        previousDays: const [],
        hrvPoints: const [],
        realtimePoints: [
          HealthRawRealtimeStressPoint(
            userId: 1,
            rawEndTime: mayStart + 600,
            result: 70,
            sourceStartTime: mayStart + 600,
            sourceEndTime: mayStart + 600,
          ),
          HealthRawRealtimeStressPoint(
            userId: 1,
            rawEndTime: mayStart + 1200,
            result: 80,
            sourceStartTime: mayStart + 1200,
            sourceEndTime: mayStart + 1200,
          ),
        ],
      );

      expect(statistics.hrvTrendList, hasLength(1));
      expect(statistics.hrvTrendList?.single.timeKey, 5);
      expect(statistics.hrvTrendList?.single.hrvAverage, isNull);
      expect(statistics.hrvTrendList?.single.hrAverage, 75);
      expect(
        statistics.hrvTrendList?.single.state,
        HealthRawStressState.attention.value,
      );
      expect(statistics.dailyDistributionList, hasLength(1));
      expect(statistics.dailyDistributionList?.single.date, 20260512);
      expect(
        statistics.dailyDistributionList?.map((e) => e.date),
        isNot(contains(20260101)),
      );
      expect(statistics.hrvDistributionList?.single.stressId,
          HealthRawStressState.attention.value);
    });
  });

  group('LocalHealthDataConvert activity unit conversion', () {
    test('passes through exercise and stand seconds from raw activity data',
        () {
      final day = DateTime(2026, 7, 16);
      final endTime = LocalHealthDataConvert.unixSeconds(
        day.add(const Duration(hours: 12)),
      );
      final activity = [
        HealthKitRawActivityDataPoint(
          endTime: endTime,
          activeEnergyBurned: 300,
          activeEnergyBurnedGoal: 500,
          appleMoveTime: 8000,
          appleMoveTimeGoal: 10000,
          appleExerciseTime: 1800,
          exerciseTimeGoal: 3600,
          appleStandHours: 21600,
          standHoursGoal: 43200,
        ),
      ];

      final healthData = LocalHealthDataConvert.v2HealthData(
        day: day,
        hrvPoints: const [],
        activity: activity,
        sleepingHeartRate: const [],
        restingHeartRate: const [],
        sleepIntervals: const [],
      );
      final target = LocalHealthDataConvert.v2ActivityTarget(
        userId: 1,
        activity: activity,
      );
      final statistics = LocalHealthDataConvert.activityBurnStatistics(
        days: [day],
        previousDays: const [],
        activity: activity,
        heartRate: const [],
        sleepIntervals: const [],
      );

      expect(healthData.exercise, 1800);
      expect(healthData.stand, 21600);
      expect(target.exercise, 3600);
      expect(target.stand, 43200);
      expect(statistics.totalExercise, 1800);
      expect(statistics.totalStand, 21600);
      expect(statistics.activityTargetInfo?.exercise, 3600);
      expect(statistics.activityTargetInfo?.stand, 43200);
    });
  });
}