health_local_data_convert_test.dart 11.1 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('HealthRawDailyStressCalculator', () {
    test('uses median and p75 after excluding confirmed activity buffer', () {
      const userId = 1;
      const dayStart = 100000;
      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 dailyStress = HealthRawDailyStressCalculator.calculate(
        userId: userId,
        date: 20260716,
        dataTime: dayStart,
        realtimePoints: [
          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),
        ],
      );
      final score = LocalHealthDataConvert.v2StressScore(dailyStress);

      expect(score.comprehensiveScore, 34);
      expect(score.state, HealthRawStressState.normal.value);
      expect(dailyStress?.stressValue, 36);
    });

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

      final dailyStress = HealthRawDailyStressCalculator.calculate(
        userId: userId,
        date: 20260716,
        dataTime: dayStart,
        realtimePoints: [
          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,
          ),
        ],
      );
      final score = LocalHealthDataConvert.v2StressScore(dailyStress);

      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);

      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,
        );
      }

      HealthRawDailyStressPoint dailyStressPoint(
        DateTime day,
        int score,
      ) {
        return HealthRawDailyStressPoint(
          userId: 1,
          date: LocalHealthDataConvert.dateKey(day),
          stressValue: score.toDouble(),
          stressScore: score,
          state: healthRawRealtimeStressState(score),
          dataTime: LocalHealthDataConvert.unixSeconds(day),
        );
      }

      final statistics = LocalHealthDataConvert.hrvStatistics(
        dateRangeType: 0,
        days: [currentDay],
        previousDays: [previousDay],
        hrvPoints: [
          hrvPoint(currentStart + 600, 80),
        ],
        dailyStressPoints: [
          dailyStressPoint(currentDay, 34),
          dailyStressPoint(previousDay, 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 statistics = LocalHealthDataConvert.hrvStatistics(
        dateRangeType: 2,
        days: LocalHealthDataConvert.rangeDays(2, 20260101),
        previousDays: const [],
        hrvPoints: const [],
        dailyStressPoints: [
          HealthRawDailyStressPoint(
            userId: 1,
            date: 20260512,
            stressValue: 75,
            stressScore: 77,
            state: HealthRawStressState.attention,
            dataTime: LocalHealthDataConvert.unixSeconds(mayDay),
          ),
        ],
      );

      expect(statistics.hrvTrendList, hasLength(1));
      expect(statistics.hrvTrendList?.single.timeKey, 5);
      expect(statistics.hrvTrendList?.single.hrvAverage, isNull);
      expect(statistics.hrvTrendList?.single.hrAverage, 77);
      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 seconds and stand hours 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: 6,
          standHoursGoal: 12,
        ),
      ];

      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, 6);
      expect(target.exercise, 3600);
      expect(target.stand, 12);
      expect(statistics.totalExercise, 1800);
      expect(statistics.totalStand, 6);
      expect(statistics.activityTargetInfo?.exercise, 3600);
      expect(statistics.activityTargetInfo?.stand, 12);
    });
  });
}