/* SPDX-FileCopyrightText: 2024 Google LLC */
/* SPDX-License-Identifier: Apache-2.0 */

#include "clar.h"

#include "applib/health_service_private.h"
#include "services/normal/activity/activity.h"
#include "shell/prefs_syscalls.h"
#include "util/size.h"


// Stubs
#include "stubs_app_manager.h"
#include "stubs_logging.h"
#include "stubs_passert.h"
#include "stubs_pbl_malloc.h"
#include "stubs_worker_manager.h"

// Fakes
#include "fake_rtc.h"
#include "fake_pbl_std.h"

static HealthServiceState s_health_service;

// -----------------------------------
// T_STATIC functions from health_service.c
bool prv_calculate_time_range(time_t time_start, time_t time_end,
                              HealthServiceTimeRange *range);

void prv_adjust_value_boundaries(HealthValue *values, size_t num_values,
                                 const HealthServiceTimeRange *range);

bool prv_activity_session_matches(const ActivitySession *session, HealthActivityMask mask,
                                  time_t time_start, time_t time_end);

int64_t prv_session_compare(const ActivitySession *a, const ActivitySession *b,
                            HealthIterationDirection direction);

void prv_health_event_handler(PebbleEvent *e, void *context);


// -----------------------------------
// Stubs
AppInstallId app_get_app_id(void) {
  return 1;
}

HealthServiceState *app_state_get_health_service_state(void) {
  return &s_health_service;
}

PebbleTask pebble_task_get_current(void) {
  return PebbleTask_App;
}

HealthServiceState *worker_state_get_health_service_state(void) {
  cl_fail("should never be called");
  return NULL;
}

void sys_send_pebble_event_to_kernel(PebbleEvent* event) {}

HRMSessionRef sys_hrm_manager_get_app_subscription(AppInstallId app_id) {
  return HRM_INVALID_SESSION_REF;
}

static bool s_activity_prefs_heart_rate_enabled;
bool sys_activity_prefs_heart_rate_is_enabled(void) {
  return s_activity_prefs_heart_rate_enabled;
}

bool sys_hrm_manager_get_subscription_info(HRMSessionRef session, AppInstallId *app_id,
                                           uint32_t *update_interval_s, uint16_t *expire_s,
                                           HRMFeature *features) {
  return false;
}


typedef struct {
  struct {
    ActivityMetric metric;
    uint32_t history_len;
  } in;
  struct {
    HealthValue history[ACTIVITY_HISTORY_DAYS];
    bool result;
  } out;
} sys_activity_get_metric_values;

// Activity Metric Overrides
// Allows one to specify a return value for a specific metric
static struct {
  bool overridden;
  int32_t value;
} s_metric_overrides[ActivityMetricNumMetrics];

static bool prv_handle_override(ActivityMetric metric, uint32_t history_len, int32_t *history) {
  if (!s_metric_overrides[metric].overridden) {
    return false;
  }
  cl_assert_equal_i(1, history_len);
  *history = s_metric_overrides[metric].value;
  return true;
}

static void prv_override_metric(ActivityMetric metric, int32_t value) {
  s_metric_overrides[metric].value = value;
  s_metric_overrides[metric].overridden = true;
}
// End override code


static sys_activity_get_metric_values s_sys_activity_get_metric_values;

// mock that simply copies values from static vars and stores args for later inspection
bool sys_activity_get_metric(ActivityMetric metric, uint32_t history_len, int32_t *history) {
  cl_assert(history_len <= ARRAY_LENGTH(s_sys_activity_get_metric_values.out.history));

  s_sys_activity_get_metric_values.in.metric = metric;
  s_sys_activity_get_metric_values.in.history_len = history_len;

  // Check if this value is in our overrides. If it is return it and quit.
  if (prv_handle_override(metric, history_len, history)) {
    return true;
  }

  // yes, actual implementation can handle this
  if (history) {
    for (uint32_t i = 0; i < history_len; i++) {
      history[i] = s_sys_activity_get_metric_values.out.history[i];
    }
  }

  return s_sys_activity_get_metric_values.out.result;
}

void event_service_client_subscribe(EventServiceInfo * service_info) {}
void event_service_client_unsubscribe(EventServiceInfo * service_info) {}

static UnitsDistance s_units_distance_result;

UnitsDistance sys_shell_prefs_get_units_distance(void) {
  return s_units_distance_result;
}

typedef struct {
  struct {
    ActivitySession sessions[30];
    uint32_t num_sessions;
    bool result;
  } out;
} sys_activity_get_sessions_values;

static sys_activity_get_sessions_values s_sys_activity_get_sessions_values;

bool sys_activity_get_sessions(uint32_t *num_sessions, ActivitySession *sessions) {
  cl_assert(num_sessions);
  cl_assert(sessions);
  cl_assert(s_sys_activity_get_sessions_values.out.num_sessions <=
            ARRAY_LENGTH(s_sys_activity_get_sessions_values.out.sessions));

  for (uint32_t i = 0;
       i < MIN(*num_sessions,
               s_sys_activity_get_sessions_values.out.num_sessions);
       i++) {
    sessions[i] = s_sys_activity_get_sessions_values.out.sessions[i];
  }

  *num_sessions = s_sys_activity_get_sessions_values.out.num_sessions;
  return s_sys_activity_get_sessions_values.out.result;
}


typedef struct {
  struct {
    uint16_t day_of_week;
  } in;
  struct {
    ActivityMetricAverages averages;
    bool result;
  } out;
} sys_activity_get_step_averages_values;

static sys_activity_get_step_averages_values s_sys_activity_get_step_averages_values_weekday;
static sys_activity_get_step_averages_values s_sys_activity_get_step_averages_values_weekend;

bool sys_activity_get_step_averages(uint16_t day_of_week, ActivityMetricAverages *averages) {
  cl_assert(averages);
  if (day_of_week == Sunday || day_of_week == Saturday) {
    s_sys_activity_get_step_averages_values_weekend.in.day_of_week = day_of_week;
    memcpy(averages, &s_sys_activity_get_step_averages_values_weekend.out.averages,
           sizeof(*averages));
    return s_sys_activity_get_step_averages_values_weekend.out.result;
  } else {
    s_sys_activity_get_step_averages_values_weekday.in.day_of_week = day_of_week;
    memcpy(averages, &s_sys_activity_get_step_averages_values_weekday.out.averages,
           sizeof(*averages));
    return s_sys_activity_get_step_averages_values_weekday.out.result;
  }
}

typedef struct {
  HealthActivity activity;
  time_t time_start;
  time_t time_end;
  void *context;
} HealthActivityCBData;

static HealthActivityCBData s_prv_activity_cb__args[100];
static uint32_t s_prv_activity_cb__call_count;
static uint32_t s_prv_activity_cb__false_at_call_no = UINT32_MAX;

bool prv_activity_cb(HealthActivity activity,
                     time_t time_start, time_t time_end,
                     void *context) {
  s_prv_activity_cb__args[s_prv_activity_cb__call_count] = (HealthActivityCBData) {
    .activity = activity, .time_start = time_start, .time_end = time_end, .context = context,
  };
  s_prv_activity_cb__call_count++;

  cl_assert(s_prv_activity_cb__call_count <= s_prv_activity_cb__false_at_call_no);
  return s_prv_activity_cb__call_count < s_prv_activity_cb__false_at_call_no;
}

typedef struct {
  HealthMinuteData records[MINUTES_PER_DAY];
  uint32_t num_records;
  time_t utc_start;
  bool result;
  bool asserts;
} sys_activity_get_minute_history_out_values;

typedef struct {
  uint32_t num_records;
  time_t utc_start;
} sys_activity_get_minute_history_in_values;

typedef struct {
  // Each time sys_activity_get_minute_history is called, we return the next stage of data
  int stage;
  sys_activity_get_minute_history_in_values in[4];
  sys_activity_get_minute_history_out_values out[4];
} sys_activity_get_minute_history_values;

static sys_activity_get_minute_history_values s_sys_activity_get_minute_history_values;


bool sys_activity_get_minute_history(HealthMinuteData *minute_data, uint32_t *num_records,
                                     time_t *utc_start) {
  int stage = s_sys_activity_get_minute_history_values.stage++;
  cl_assert(stage < ARRAY_LENGTH(s_sys_activity_get_minute_history_values.out));
  sys_activity_get_minute_history_out_values *out =
      &s_sys_activity_get_minute_history_values.out[stage];

  cl_assert_equal_b(out->asserts, false);
  cl_assert(minute_data);
  cl_assert(num_records);
  cl_assert(utc_start);

  s_sys_activity_get_minute_history_values.in[stage].num_records = *num_records;
  s_sys_activity_get_minute_history_values.in[stage].utc_start = *utc_start;

  if (!out->result) {
    return false;
  }

  *num_records = MIN(out->num_records, *num_records);
  *utc_start = out->utc_start;
  for (uint32_t i = 0; i < *num_records; i++) {
    minute_data[i] = out->records[i];
  }

  return true;
}

static bool s_activity_sessions_ongoing[ActivitySessionTypeCount];

bool sys_activity_sessions_is_session_type_ongoing(ActivitySessionType type) {
  return s_activity_sessions_ongoing[type];
}


/////////////////////////////////////////

void test_health__initialize(void) {
  TimezoneInfo tz_info = {
    .tm_zone = "UTC",
    .tm_gmtoff = 0,
  };
  time_util_update_timezone(&tz_info);

  s_health_service = (HealthServiceState){};
  time_t utc_sec = 1451293942; // some constant time for this test
    // Mon, 28 Dec 2015 09:12:22 GMT
    // => 22+12*60+9*60*60 = 33142 seconds into this day
    // =>   24*60*60-33142 = 53258 seconds remaining this day
  fake_rtc_init(100 /*initial_ticks*/, utc_sec);

  s_sys_activity_get_metric_values = (sys_activity_get_metric_values) {
    .out.result = true,
    .in.metric = (ActivityMetric)-1,
  };

  memset(s_prv_activity_cb__args, 0, sizeof(s_prv_activity_cb__args));
  memset(s_metric_overrides, 0, sizeof(s_metric_overrides));
  s_prv_activity_cb__call_count = 0;
  s_prv_activity_cb__false_at_call_no = UINT32_MAX;

  s_sys_activity_get_minute_history_values = (sys_activity_get_minute_history_values) {
    // as all these values need to be configured in the test, we assert per default
    .out[0].asserts = true,
  };

  s_activity_prefs_heart_rate_enabled = true;
}

void test_health__sum_today_returns_0_on_failure(void) {
  s_sys_activity_get_metric_values.out.result = false;
  s_sys_activity_get_metric_values.out.history[0] = 456;
  HealthValue result = health_service_sum_today(HealthMetricStepCount);
  cl_assert_equal_i(0, result);
}

void test_health__sum_today(void) {
  s_sys_activity_get_metric_values.out.history[0] = 123;
  s_sys_activity_get_metric_values.out.history[1] = 456;
  HealthValue result = health_service_sum_today(HealthMetricStepCount);
  cl_assert_equal_i(123, result);
  cl_assert_equal_i(s_sys_activity_get_metric_values.in.metric, ActivityMetricStepCount);
  cl_assert_equal_i(s_sys_activity_get_metric_values.in.history_len, ACTIVITY_HISTORY_DAYS);
}

#define cl_assert_equal_range(a, b) \
  do { \
    HealthServiceTimeRange r_a = (a); \
    HealthServiceTimeRange r_b = (b); \
    bool success = memcmp(&r_a, &r_b, sizeof(r_a)) == 0; \
    if (!success) { \
      char error_msg[256] = {0}; \
      snprintf(error_msg, sizeof(error_msg), \
          "HealthServiceInternalTimeRange equal\n" \
          "    a: {last_day_idx:%d, num_days:%d, seconds_first_day:%d, seconds_last_day:%d, " \
                  "seconds_total_last_day: %d}\n" \
          "    b: {last_day_idx:%d, num_days:%d, seconds_first_day:%d, seconds_last_day:%d," \
                  "seconds_total_last_day: %d}\n", \
              (int)r_a.last_day_idx, (int)r_a.num_days, \
              (int)r_a.seconds_first_day, (int)r_a.seconds_last_day, \
              (int)r_a.seconds_total_last_day, \
              (int)r_b.last_day_idx, (int)r_b.num_days, \
              (int)r_b.seconds_first_day, (int)r_b.seconds_last_day, \
              (int)r_b.seconds_total_last_day); \
      clar__assert(0, __FILE__, __LINE__, "Expression is not true: ", error_msg, 1); \
    } \
  } while(0);


void test_health__range_to_day_id(void) {
  const time_t now = rtc_get_time();

  bool result;
  HealthServiceTimeRange range;

  // today
  result = prv_calculate_time_range(time_util_get_midnight_of(now), now, &range);
  cl_assert(result);
  cl_assert_equal_range(range, ((HealthServiceTimeRange){
    .last_day_idx = 0,
    .num_days = 1,
    .seconds_first_day = 33142,
    .seconds_last_day = 33142,
    .seconds_total_last_day = 33142,
  }));

  // yesterday
  result = prv_calculate_time_range(
    time_util_get_midnight_of(now - SECONDS_PER_DAY), time_util_get_midnight_of(now), &range);
  cl_assert(result);
  cl_assert_equal_range(range, ((HealthServiceTimeRange){
    .last_day_idx = 1,
    .num_days = 1,
    .seconds_first_day = 86400,
    .seconds_last_day = 86400,
    .seconds_total_last_day = 86400,
  }));

  // some time yesterday + today
  result = prv_calculate_time_range(now - SECONDS_PER_DAY, now, &range);
  cl_assert(result);
  cl_assert_equal_range(range, ((HealthServiceTimeRange){
    .last_day_idx = 0,
    .num_days = 2,
    .seconds_first_day = 53258,
    .seconds_last_day = 33142,
    .seconds_total_last_day = 33142,
  }));
}

void test_health__range_to_day_id_respects_local_time(void) {
  const time_t now = rtc_get_time();

  bool result;
  HealthServiceTimeRange range;

  // some time yesterday + today - as if UTC == localtime
  result = prv_calculate_time_range(now - SECONDS_PER_DAY, now, &range);
  cl_assert(result);
  cl_assert_equal_range(range, ((HealthServiceTimeRange){
    .last_day_idx = 0,
    .num_days = 2,
    .seconds_first_day = 53258,
    .seconds_last_day = 33142,
    .seconds_total_last_day = 33142,
  }));

  // shifted one hour
  time_t utc_to_local_delta = SECONDS_PER_HOUR;
  TimezoneInfo tz_info = {
    .tm_zone = "FOO",
    .tm_gmtoff = utc_to_local_delta,
  };
  time_util_update_timezone(&tz_info);

  result = prv_calculate_time_range(now - SECONDS_PER_DAY, now, &range);
  cl_assert(result);
  cl_assert_equal_range(range, ((HealthServiceTimeRange){
    .last_day_idx = 0,
    .num_days = 2,
    .seconds_first_day = 53258 - utc_to_local_delta,
    .seconds_last_day = 33142 + utc_to_local_delta,
    .seconds_total_last_day = 33142 + utc_to_local_delta,
  }));
}

void test_health__range_to_day_id_rejects_invalid_values(void) {
  const time_t now = rtc_get_time();
  bool result;

  // check that we *can* return success
  result = prv_calculate_time_range(now - 10, now, NULL);
  cl_assert_equal_b(result, true);

  // in the future
  result = prv_calculate_time_range(now + 10, now + 20, NULL);
  cl_assert_equal_b(result, false);

  // too far in the past
  result = prv_calculate_time_range(
    now - (ACTIVITY_HISTORY_DAYS + 10) * SECONDS_PER_DAY,
    now - (ACTIVITY_HISTORY_DAYS + 2) * SECONDS_PER_DAY, NULL);
  cl_assert_equal_b(result, false);


  // start after end
  result = prv_calculate_time_range(now - 100, now - 200, NULL);
  cl_assert_equal_b(result, false);
}

void test_health__range_to_day_id_clamps_values(void) {
  const time_t now = rtc_get_time();
  bool result;
  HealthServiceTimeRange range;

  // clamps value that goes into the future
  result = prv_calculate_time_range(now - 10, now + 11, &range);
  cl_assert_equal_b(result, true);
  cl_assert_equal_range(range, ((HealthServiceTimeRange){
    .last_day_idx = 0,
    .num_days = 1,
    .seconds_first_day = 10,
    .seconds_last_day = 10,
    .seconds_total_last_day = 33142,
  }));

  // clamps value that goes into the future
  const time_t first_valid_time =
    time_util_get_midnight_of(now - (ACTIVITY_HISTORY_DAYS - 1) * SECONDS_PER_DAY);
  result = prv_calculate_time_range(first_valid_time - 12, first_valid_time + 13,
                                    &range);
  cl_assert_equal_b(result, true);
  cl_assert_equal_range(range, ((HealthServiceTimeRange){
    .last_day_idx = ACTIVITY_HISTORY_DAYS - 1,
    .num_days = 1,
    .seconds_first_day = 13,
    .seconds_last_day = 13,
    .seconds_total_last_day = 86400,
  }));
}

void test_health__sum_full_days(void) {
  // use values structured as binary mask so we can detect if we sum up currect days
  s_sys_activity_get_metric_values.out.history[0] = 1000;
  s_sys_activity_get_metric_values.out.history[1] = 2000;
  s_sys_activity_get_metric_values.out.history[2] = 4000;
  s_sys_activity_get_metric_values.out.history[3] = 8000;
  s_sys_activity_get_metric_values.out.history[4] = 16000;

  const time_t now = rtc_get_time();
  HealthValue result;
  // today until now
  result = health_service_sum(HealthMetricStepCount,
                              time_util_get_midnight_of(now),
                              now);
  cl_assert_equal_i(result, 1000);
  cl_assert_equal_i(s_sys_activity_get_metric_values.in.history_len, ACTIVITY_HISTORY_DAYS);

  // today into future
  result = health_service_sum(HealthMetricStepCount,
                              time_util_get_midnight_of(now),
                              now + 12345);
  cl_assert_equal_i(result, 1000);

  // yesterday
  result = health_service_sum(HealthMetricStepCount,
                              time_util_get_midnight_of(now) - SECONDS_PER_DAY,
                              time_util_get_midnight_of(now));
  cl_assert_equal_i(result, 2000);

  // yesterday and today
  result = health_service_sum(HealthMetricStepCount,
                              time_util_get_midnight_of(now) - SECONDS_PER_DAY,
                              now);
  cl_assert_equal_i(result, 1000 + 2000);
}

void test_health__process_range(void) {
  HealthValue values[4] = {1000, 1000, 1000, 1000};
  HealthServiceTimeRange range = {
    .num_days = 3,
    .seconds_first_day = SECONDS_PER_DAY / 10,
    .seconds_last_day = SECONDS_PER_DAY / 5,
    .seconds_total_last_day = SECONDS_PER_DAY,
  };

  // make sure we treat first and last day correctly (last == idx 0)
  prv_adjust_value_boundaries(values, ARRAY_LENGTH(values), &range);
  cl_assert_equal_i(values[0], 1000 / 5);
  cl_assert_equal_i(values[1], 1000);
  cl_assert_equal_i(values[2], 1000 / 10);

  // ensure we look at seconds_total_last_day
  values[0] = 1000;
  values[2] = 1000;
  range.seconds_total_last_day = SECONDS_PER_DAY / 4;
  prv_adjust_value_boundaries(values, ARRAY_LENGTH(values), &range);
  cl_assert_equal_i(values[0], 4 * 1000 / 5);
  cl_assert_equal_i(values[1], 1000);
  cl_assert_equal_i(values[2], 1000 / 10);

  // ensure we don't calculate a single day multiple times
  values[0] = 1000;
  values[2] = 1000;
  range.num_days = 1;
  prv_adjust_value_boundaries(values, ARRAY_LENGTH(values), &range);
  cl_assert_equal_i(values[0], 4 * 1000 / 5);
  cl_assert_equal_i(values[1], 1000);
  cl_assert_equal_i(values[2], 1000);

  // ensure we can handle smaller array than range - nothing will be processed
  values[0] = 1000;
  range.num_days = 2;
  prv_adjust_value_boundaries(values, 1, &range);
  cl_assert_equal_i(values[0], 1000);
  cl_assert_equal_i(values[1], 1000);
  cl_assert_equal_i(values[2], 1000);

  // ensure we can handle empty sets
  values[0] = 1000;
  prv_adjust_value_boundaries(values, 0, &range);
  cl_assert_equal_i(values[0], 1000);
  cl_assert_equal_i(values[1], 1000);
  cl_assert_equal_i(values[2], 1000);

  // ensure we can handle empty ranges
  range.num_days = 0;
  prv_adjust_value_boundaries(values, ARRAY_LENGTH(values), &range);
  cl_assert_equal_i(values[0], 1000);
  cl_assert_equal_i(values[1], 1000);
  cl_assert_equal_i(values[2], 1000);

  // ensure we correctly handle the day index
  range = (HealthServiceTimeRange){
    .num_days = 3,
    .last_day_idx = 1,
    .seconds_first_day = SECONDS_PER_DAY / 10,
    .seconds_last_day = SECONDS_PER_DAY / 5,
    .seconds_total_last_day = SECONDS_PER_DAY,
  };
  prv_adjust_value_boundaries(values, ARRAY_LENGTH(values), &range);
  cl_assert_equal_i(values[0], 1000);
  cl_assert_equal_i(values[1], 1000 / 5);
  cl_assert_equal_i(values[2], 1000);
  cl_assert_equal_i(values[3], 1000 / 10);
}

void test_health__sum_fraction_days(void) {
  // use values structured as binary mask so we can detect if we sum up currect days
  s_sys_activity_get_metric_values.out.history[0] = 1000;
  s_sys_activity_get_metric_values.out.history[1] = 2000;
  s_sys_activity_get_metric_values.out.history[2] = 4000;
  s_sys_activity_get_metric_values.out.history[3] = 8000;
  s_sys_activity_get_metric_values.out.history[4] = 16000;

  const time_t now = rtc_get_time();
  HealthValue result;
  // 3/4 of yesterday
  result = health_service_sum(HealthMetricStepCount,
                              time_util_get_midnight_of(now) - SECONDS_PER_DAY,
                              time_util_get_midnight_of(now) - SECONDS_PER_DAY / 4);
  cl_assert_equal_i(result, 1500);
  cl_assert_equal_i(s_sys_activity_get_metric_values.in.history_len, ACTIVITY_HISTORY_DAYS);

  // 1/2 of today's captured seconds so far
  result = health_service_sum(HealthMetricStepCount,
                              time_util_get_midnight_of(now),
                              (time_util_get_midnight_of(now) + now) / 2);
  cl_assert_equal_i(result, 500);
}

void test_health__cache(void) {
  cl_assert_equal_p(s_health_service.cache, NULL);
  health_service_events_subscribe(NULL, NULL);
  HealthServiceCache *const cache = s_health_service.cache;
  cl_assert(cache != NULL);

  // cache is preserved
  health_service_events_subscribe(NULL, NULL);
  cl_assert_equal_p(s_health_service.cache, cache);

  health_service_events_unsubscribe();
  cl_assert_equal_p(s_health_service.cache, NULL);

  // multiple unsubscribe/empty cache doesn't cause a problem
  health_service_events_unsubscribe();
  cl_assert_equal_p(s_health_service.cache, NULL);
}

void test_health__metric_accessible(void) {
  const time_t now = rtc_get_time();

  HealthServiceAccessibilityMask accessible;
  // all value in the future
  accessible = health_service_metric_accessible(HealthMetricStepCount, now + 10, now + 20);
  cl_assert_equal_i(accessible, HealthServiceAccessibilityMaskNotAvailable);

  // normal value is available
  accessible = health_service_metric_accessible(HealthMetricStepCount, now - 10, now);
  cl_assert_equal_i(accessible, HealthServiceAccessibilityMaskAvailable);

  // values that partly are in unsupported range are available
  accessible = health_service_metric_accessible(HealthMetricStepCount, now - 10, now + 20);
  cl_assert_equal_i(accessible, HealthServiceAccessibilityMaskAvailable);

  // if all values are -1, data is not available
  s_sys_activity_get_metric_values.out.history[0] = -1;
  s_sys_activity_get_metric_values.out.history[1] = -1;
  accessible = health_service_metric_accessible(HealthMetricStepCount,
                                                now - SECONDS_PER_DAY, now);
  cl_assert_equal_i(accessible, HealthServiceAccessibilityMaskNotAvailable);

  // if some values are >= 0, data is not available (day at idx 2)
  accessible = health_service_metric_accessible(HealthMetricStepCount,
                                                now - 2 * SECONDS_PER_DAY, now);
  cl_assert_equal_i(accessible, HealthServiceAccessibilityMaskAvailable);
}

void test_health__metric_hr_accessible(void) {
  const time_t now = rtc_get_time();

  HealthServiceAccessibilityMask accessible;
  // all value in the future
  accessible = health_service_metric_accessible(HealthMetricHeartRateBPM, now + 10, now + 20);
  cl_assert_equal_i(accessible, HealthServiceAccessibilityMaskNotAvailable);

  // normal value is available
  accessible = health_service_metric_accessible(HealthMetricHeartRateBPM, now - 10, now);
  cl_assert_equal_i(accessible, HealthServiceAccessibilityMaskAvailable);

  // values that partly are in unsupported range are available
  accessible = health_service_metric_accessible(HealthMetricHeartRateBPM, now - 10, now + 20);
  cl_assert_equal_i(accessible, HealthServiceAccessibilityMaskAvailable);

  // HR has a limit of two hours. Make sure if we are within that range it's available
  accessible = health_service_metric_accessible(HealthMetricHeartRateBPM,
                                                now - 2 * SECONDS_PER_HOUR, now);
  cl_assert_equal_i(accessible, HealthServiceAccessibilityMaskAvailable);
}

void test_health__metric_hr_averaged_accessible(void) {
  const time_t now = rtc_get_time();

  typedef struct {
    const char *desc;
    struct {
      HealthMetric metric;
      time_t time_start;
      time_t time_end;
      HealthServiceTimeScope scope;
      bool hr_disabled;
    } in;
    struct {
      HealthServiceAccessibilityMask accessible;
    } out;
  } TestInputOutput;

  const TestInputOutput tests[] = {
    {
      .desc = "Valid time range with ScopeOnce",
      .in =  { HealthMetricHeartRateBPM, now - 10, now, HealthServiceTimeScopeOnce },
      .out = { HealthServiceAccessibilityMaskAvailable }
    },
    {
      .desc = "Valid time range with ScopeWeekly",
      .in =  { HealthMetricHeartRateBPM, now - 10, now, HealthServiceTimeScopeWeekly },
      .out = { HealthServiceAccessibilityMaskNotSupported }
    },
    {
      .desc = "Valid time range with ScopeDailyWeekdayOrWeekend",
      .in =  { HealthMetricHeartRateBPM, now - 10, now, HealthServiceTimeScopeDailyWeekdayOrWeekend },
      .out = { HealthServiceAccessibilityMaskNotSupported }
    },
    {
      .desc = "Valid time range with ScopeDaily",
      .in =  { HealthMetricHeartRateBPM, now - 10, now, HealthServiceTimeScopeDaily },
      .out = { HealthServiceAccessibilityMaskNotSupported }
    },
    {
      .desc = "Invalid future time range with ScopeOnce",
      .in =  { HealthMetricHeartRateBPM, now + 10, now + 20, HealthServiceTimeScopeOnce },
      .out = { HealthServiceAccessibilityMaskNotAvailable }
    },
    {
      .desc = "Time range that goes further back into history than BPM supports",
      .in =  { HealthMetricHeartRateBPM, now - 3 * SECONDS_PER_HOUR, now, HealthServiceTimeScopeOnce },
      .out = { HealthServiceAccessibilityMaskNotSupported }
    },
    {
      .desc = "Time range that goes further back into history than BPM supports",
      .in =  { HealthMetricHeartRateBPM, now - 3 * SECONDS_PER_HOUR, now - 1 * SECONDS_PER_HOUR, HealthServiceTimeScopeOnce },
      .out = { HealthServiceAccessibilityMaskNotSupported }
    },
    {
      .desc = "HR Disabled. Return NoPermission",
      .in =  { HealthMetricHeartRateBPM, now - 10, now, HealthServiceTimeScopeOnce, true},
      .out = { HealthServiceAccessibilityMaskNoPermission }
    },
  };

  // Run all the tests
  HealthServiceAccessibilityMask accessible;
  for (int i = 0; i < ARRAY_LENGTH(tests); i++) {
    const TestInputOutput *test = &tests[i];
    s_activity_prefs_heart_rate_enabled = !test->in.hr_disabled;
    accessible = health_service_metric_averaged_accessible(test->in.metric,
                                                           test->in.time_start,
                                                           test->in.time_end,
                                                           test->in.scope);
    PBL_LOG(LOG_LEVEL_DEBUG, "%s\nMetric: %d, start: %d, end: %d, Scope: %d",
            test->desc, (int)test->in.metric, (int)test->in.time_start,
            (int)test->in.time_end, (int)test->in.scope);
    cl_assert_equal_i(accessible, tests[i].out.accessible);
  }
}

void test_health__metric_hr_aggregate_averaged_accessible(void) {
  const time_t now = rtc_get_time();

  typedef struct {
    const char *desc;
    struct {
      HealthMetric metric;
      time_t time_start;
      time_t time_end;
      HealthAggregation aggregation;
      HealthServiceTimeScope scope;
      bool hr_disabled;
    } in;
    struct {
      HealthServiceAccessibilityMask accessible;
    } out;
  } TestInputOutput;

  const TestInputOutput tests[] = {
    {
      .desc = "Valid time range with ScopeDaily and Sum. Should be NotSupported",
      .in =  { HealthMetricHeartRateBPM, now - 10, now, HealthAggregationSum,
               HealthServiceTimeScopeDaily },
      .out = { HealthServiceAccessibilityMaskNotSupported }
    },
    {
      .desc = "Valid time range with ScopeDaily and Avg. Not available because Daily",
      .in =  { HealthMetricHeartRateBPM, now - 10, now, HealthAggregationAvg,
               HealthServiceTimeScopeDaily },
      .out = { HealthServiceAccessibilityMaskNotSupported }
    },
    {
      .desc = "Valid time range with ScopeOnce and Min. Available",
      .in =  { HealthMetricHeartRateBPM, now - 10, now, HealthAggregationMin,
               HealthServiceTimeScopeOnce },
      .out = { HealthServiceAccessibilityMaskAvailable }
    },
    {
      .desc = "Valid time range with ScopeOnce and Max. Available",
      .in =  { HealthMetricHeartRateBPM, now - 10, now, HealthAggregationMax,
               HealthServiceTimeScopeOnce },
      .out = { HealthServiceAccessibilityMaskAvailable }
    },
    {
      .desc = "Valid time range with ScopeDaily and Max. NotSupported",
      .in =  { HealthMetricHeartRateBPM, now - 10, now, HealthAggregationMax,
               HealthServiceTimeScopeDaily },
      .out = { HealthServiceAccessibilityMaskNotSupported }
    },
    {
      .desc = "Invalid time range with ScopeOnce and Max. NotSupported",
      .in =  { HealthMetricHeartRateBPM, now - 3 * SECONDS_PER_HOUR, now - 2 * SECONDS_PER_HOUR,
               HealthAggregationMax, HealthServiceTimeScopeOnce },
      .out = { HealthServiceAccessibilityMaskNotSupported }
    },
    {
      .desc = "HR Disabled. Return NoPermission",
      .in =  { HealthMetricHeartRateBPM, now - 10, now, HealthAggregationMax,
               HealthServiceTimeScopeOnce, true },
      .out = { HealthServiceAccessibilityMaskNoPermission }
    },
    {
      .desc = "Time range that goes further back into history than BPM supports",
      .in =  { HealthMetricHeartRateBPM, now - 3 * SECONDS_PER_HOUR, now - 1 * SECONDS_PER_HOUR,
               HealthAggregationAvg, HealthServiceTimeScopeOnce },
      .out = { HealthServiceAccessibilityMaskNotSupported }
    },
  };

  // Run all the tests
  HealthServiceAccessibilityMask accessible;
  for (int i = 0; i < ARRAY_LENGTH(tests); i++) {
    const TestInputOutput *test = &tests[i];
    s_activity_prefs_heart_rate_enabled = !test->in.hr_disabled;
    accessible = health_service_metric_aggregate_averaged_accessible(test->in.metric,
                                                                     test->in.time_start,
                                                                     test->in.time_end,
                                                                     test->in.aggregation,
                                                                     test->in.scope);
    PBL_LOG(LOG_LEVEL_DEBUG, "%s\nMetric: %d, start: %d, end: %d, Aggregation: %d, Scope: %d",
            test->desc, (int)test->in.metric, (int)test->in.time_start,
            (int)test->in.time_end, (int)test->in.aggregation, (int)test->in.scope);
    cl_assert_equal_i(accessible, tests[i].out.accessible);
  }
}

void test_health__sleep_session_matches(void) {
  const time_t now = rtc_get_time();
  ActivitySession session = {
    .type = ActivitySessionType_Sleep,
    .start_utc = now - (10 * SECONDS_PER_MINUTE),
    .length_min = 10,
  };
  bool (*fun)(const ActivitySession *, HealthActivityMask, time_t, time_t) =
    prv_activity_session_matches;

  // mask none matches nothing
  cl_assert_equal_b(false, fun(&session, HealthActivityNone, now - (10 * SECONDS_PER_MINUTE),
                               now));

  // mask restful doesn't match
  cl_assert_equal_b(false, fun(&session, HealthActivityRestfulSleep,
                               now - (10 * SECONDS_PER_MINUTE), now));

  // exact time range matches
  cl_assert_equal_b(true, fun(&session, HealthActivityMaskAll, now - (10 * SECONDS_PER_MINUTE),
                              now));

  // too large time range matches
  cl_assert_equal_b(true, fun(&session, HealthActivityMaskAll, now - (20 * SECONDS_PER_MINUTE),
                              now + (10 * SECONDS_PER_MINUTE)));

  // range before doesn't match, even if it touches
  cl_assert_equal_b(false, fun(&session, HealthActivityMaskAll, now - (20 * SECONDS_PER_MINUTE),
                               now - (10 * SECONDS_PER_MINUTE)));

  // range after doesn't match, even if it touches
  cl_assert_equal_b(false, fun(&session, HealthActivityMaskAll, now,
                               now + (10 * SECONDS_PER_MINUTE)));

  // range that starts before matches
  cl_assert_equal_b(true, fun(&session, HealthActivityMaskAll, now - (20 * SECONDS_PER_MINUTE),
                              now - (9 * SECONDS_PER_MINUTE)));

  // range that ends after matches
  cl_assert_equal_b(true, fun(&session, HealthActivityMaskAll, now - (1 * SECONDS_PER_MINUTE),
                              now + (10 * SECONDS_PER_MINUTE)));
}

void test_health__any_activity_accessible(void) {
  const time_t now = rtc_get_time();
  HealthServiceAccessibilityMask accessible;

  // empty mask => not available
  accessible = health_service_any_activity_accessible(HealthActivityNone,
                                                      now - (10 * SECONDS_PER_MINUTE), now);
  cl_assert_equal_i(accessible, HealthServiceAccessibilityMaskNotAvailable);

  accessible = health_service_any_activity_accessible(HealthActivityMaskAll,
                                                      now - (10 * SECONDS_PER_MINUTE), now);
  cl_assert_equal_i(accessible, HealthServiceAccessibilityMaskAvailable);

  // too far in the past
  accessible = health_service_any_activity_accessible(HealthActivityMaskAll,
                                                      now - 10 * SECONDS_PER_DAY,
                                                      now - 9 * SECONDS_PER_DAY);
  cl_assert_equal_i(accessible, HealthServiceAccessibilityMaskNotAvailable);

  // range far into to past and future
  accessible = health_service_any_activity_accessible(HealthActivityMaskAll,
                                                      now - 10 * SECONDS_PER_DAY,
                                                      now + 10 * SECONDS_PER_DAY);
  cl_assert_equal_i(accessible, HealthServiceAccessibilityMaskAvailable);
}

void test_health__activities_iterate(void) {
  const time_t now = rtc_get_time();

  // start from oldest to most-recent - this is more or less an arbitrary order
  s_sys_activity_get_sessions_values.out.sessions[6] = (ActivitySession){
    .type = ActivitySessionType_Open,
    .start_utc = now - (95 * SECONDS_PER_MINUTE),
    .length_min = 15, // end = -80
  };
  s_sys_activity_get_sessions_values.out.sessions[5] = (ActivitySession){
    .type = ActivitySessionType_Run,
    .start_utc = now - (80 * SECONDS_PER_MINUTE),
    .length_min = 15, // end = -65
  };
  s_sys_activity_get_sessions_values.out.sessions[4] = (ActivitySession){
    .type = ActivitySessionType_Walk,
    .start_utc = now - (65 * SECONDS_PER_MINUTE),
    .length_min = 15, // end = -50
  };
  s_sys_activity_get_sessions_values.out.sessions[3] = (ActivitySession){
    .type = ActivitySessionType_Sleep,
    .start_utc = now - (50 * SECONDS_PER_MINUTE),
    .length_min = 20, // end = -30
  };
  s_sys_activity_get_sessions_values.out.sessions[2] = (ActivitySession){
    .type = ActivitySessionType_RestfulSleep,
    .start_utc = now - (45 * SECONDS_PER_MINUTE),
    .length_min = 10, // end = -35
  };
  s_sys_activity_get_sessions_values.out.sessions[1] = (ActivitySession){
    .type = ActivitySessionType_Sleep,
    .start_utc = now - (20 * SECONDS_PER_MINUTE),
    .length_min = 10, // end = -10
  };
  s_sys_activity_get_sessions_values.out.sessions[0] = (ActivitySession){
    .type = ActivitySessionType_RestfulSleep,
    .start_utc = now - (18 * SECONDS_PER_MINUTE),
    .length_min = 5, // end = -13
  };
  // oldest to most-recent (looking at each session's start): 3, 2, 1, 0
  // most-recent to oldest (looking at each session's end): 1, 0, 3, 2

  const int num_sleep_sessions = 4;
  const int num_restfulsleep_sessions = 2;
  const int num_run_sessions = 1;
  const int num_walk_sessions = 1;
  const int num_open_sessions = 1;
  const int num_sessions = num_sleep_sessions + num_restfulsleep_sessions +
                           num_run_sessions + num_walk_sessions + num_open_sessions;

  // result from mocked sys_activity_get_sessions_values is still false
  health_service_activities_iterate(HealthActivityMaskAll, now - (100 * SECONDS_PER_MINUTE), now,
                                    HealthIterationDirectionPast, prv_activity_cb, NULL);
  cl_assert_equal_i(0, s_prv_activity_cb__call_count);

  s_sys_activity_get_sessions_values.out.result = true;
  // result from mocked sys_activity_get_sessions_values is still 0 sessions
  health_service_activities_iterate(HealthActivityMaskAll, now - (100 * SECONDS_PER_MINUTE), now,
                                    HealthIterationDirectionPast, prv_activity_cb, NULL);
  cl_assert_equal_i(0, s_prv_activity_cb__call_count);

  // respect mask for RestfulSleep
  s_prv_activity_cb__call_count = 0;
  s_sys_activity_get_sessions_values.out.num_sessions = 7;
  health_service_activities_iterate(HealthActivityRestfulSleep, now - (100 * SECONDS_PER_MINUTE),
                                    now, HealthIterationDirectionPast, prv_activity_cb, NULL);
  cl_assert_equal_i(num_restfulsleep_sessions, s_prv_activity_cb__call_count);
  cl_assert_equal_b(s_prv_activity_cb__args[0].activity, HealthActivityRestfulSleep);

  // respect mask for Run/Walk
  s_prv_activity_cb__call_count = 0;
  s_sys_activity_get_sessions_values.out.num_sessions = 7;
  health_service_activities_iterate(HealthActivityRun | HealthActivityWalk |
                                    HealthActivityOpenWorkout,
                                    now - (100 * SECONDS_PER_MINUTE),
                                    now, HealthIterationDirectionPast, prv_activity_cb, NULL);
  cl_assert_equal_i(num_run_sessions + num_walk_sessions + num_open_sessions,
                    s_prv_activity_cb__call_count);
  cl_assert_equal_b(s_prv_activity_cb__args[0].activity, HealthActivityRun);

  // respect range
  s_prv_activity_cb__call_count = 0;
  s_sys_activity_get_sessions_values.out.num_sessions = 7;
  health_service_activities_iterate(HealthActivitySleep, now - (15 * SECONDS_PER_MINUTE), now,
                                    HealthIterationDirectionPast, prv_activity_cb, NULL);
  cl_assert_equal_i(1, s_prv_activity_cb__call_count);
  cl_assert_equal_b(s_prv_activity_cb__args[0].activity, HealthActivitySleep);

  // order direction past
  s_prv_activity_cb__call_count = 0;
  health_service_activities_iterate(HealthActivityMaskAll, now - (200 * SECONDS_PER_MINUTE), now,
                                    HealthIterationDirectionPast, prv_activity_cb, NULL);
  cl_assert_equal_i(7, s_prv_activity_cb__call_count);
  cl_assert_equal_i(s_prv_activity_cb__args[0].time_start,
                    s_sys_activity_get_sessions_values.out.sessions[1].start_utc);
  cl_assert_equal_i(s_prv_activity_cb__args[3].time_start,
                    s_sys_activity_get_sessions_values.out.sessions[2].start_utc);

  // order direction future
  s_prv_activity_cb__call_count = 0;
  health_service_activities_iterate(HealthActivityMaskAll, now - (200 * SECONDS_PER_MINUTE), now,
                                    HealthIterationDirectionFuture, prv_activity_cb, NULL);
  cl_assert_equal_i(7, s_prv_activity_cb__call_count);
  cl_assert_equal_i(s_prv_activity_cb__args[0].time_start,
                    s_sys_activity_get_sessions_values.out.sessions[6].start_utc);
  cl_assert_equal_i(s_prv_activity_cb__args[6].time_start,
                    s_sys_activity_get_sessions_values.out.sessions[0].start_utc);
}

void test_health__peek_current_activities(void) {
  HealthActivityMask activities;
  activities = health_service_peek_current_activities();
  cl_assert_equal_i(activities, HealthActivityNone);
  cl_assert_equal_i(s_sys_activity_get_metric_values.in.history_len, 1);
  cl_assert_equal_i(s_sys_activity_get_metric_values.in.metric, ActivityMetricSleepState);

  s_sys_activity_get_metric_values.out.history[0] = ActivitySleepStateLightSleep;
  activities = health_service_peek_current_activities();
  cl_assert_equal_i(activities, HealthActivitySleep);

  s_sys_activity_get_metric_values.out.history[0] = ActivitySleepStateRestfulSleep;
  activities = health_service_peek_current_activities();
  cl_assert_equal_i(activities, HealthActivitySleep | HealthActivityRestfulSleep);

  s_sys_activity_get_metric_values.out.history[0] = ActivitySleepStateAwake;
  s_activity_sessions_ongoing[ActivitySessionType_Run] = true;
  s_activity_sessions_ongoing[ActivitySessionType_Walk] = true;
  s_activity_sessions_ongoing[ActivitySessionType_Open] = true;
  activities = health_service_peek_current_activities();
  cl_assert_equal_i(activities, HealthActivityRun | HealthActivityWalk | HealthActivityOpenWorkout);
}

void test_health__session_compare(void) {
  const time_t now = rtc_get_time();

  // both start at the same time
  cl_assert(0 == prv_session_compare(
    &(ActivitySession) {.start_utc = now, .length_min = 10},
    &(ActivitySession) {.start_utc = now, .length_min = 5},
    HealthIterationDirectionFuture));

  // a starts earlier
  cl_assert(0 > prv_session_compare(
    &(ActivitySession) {.start_utc = now, .length_min = 10},
    &(ActivitySession) {.start_utc = now + (2 * SECONDS_PER_MINUTE), .length_min = 5},
    HealthIterationDirectionFuture));

  // b starts earlier
  cl_assert(0 < prv_session_compare(
    &(ActivitySession) {.start_utc = now, .length_min = 10},
    &(ActivitySession) {.start_utc = now - (2 * SECONDS_PER_MINUTE), .length_min = 5},
    HealthIterationDirectionFuture));

  // both end at the same time
  cl_assert(0 == prv_session_compare(
    &(ActivitySession) {.start_utc = now, .length_min = 10},
    &(ActivitySession) {.start_utc = now + (5 * SECONDS_PER_MINUTE), .length_min = 5},
    HealthIterationDirectionPast));

  // a ends later
  cl_assert(0 > prv_session_compare(
    &(ActivitySession) {.start_utc = now, .length_min = 10},
    &(ActivitySession) {.start_utc = now + (2 * SECONDS_PER_MINUTE), .length_min = 5},
    HealthIterationDirectionPast));

  // b ends later
  cl_assert(0 < prv_session_compare(
    &(ActivitySession) {.start_utc = now, .length_min = 5},
    &(ActivitySession) {.start_utc = now + (2 * SECONDS_PER_MINUTE), .length_min = 5},
    HealthIterationDirectionPast));
}

void test_health__get_minute_history_edge_case_args(void) {
  const time_t now = rtc_get_time();

  HealthMinuteData data[5] = {};
  uint32_t written;

  // null pointer
  time_t time_start = now - 10 * 60 - 30;
  time_t time_end = now - 20;
  written = health_service_get_minute_history(NULL, ARRAY_LENGTH(data), &time_start, &time_end);
  cl_assert_equal_i(0, written);

  // empty boundary
  written = health_service_get_minute_history(data, 0, &time_start, &time_end);
  cl_assert_equal_i(0, written);

  // empty start
  written = health_service_get_minute_history(data, ARRAY_LENGTH(data), NULL, &time_end);
  cl_assert_equal_i(0, written);

  // empty end before start
  time_t early_end = time_start - 20 * SECONDS_PER_MINUTE;
  written = health_service_get_minute_history(data, ARRAY_LENGTH(data), &time_start, &early_end);
  cl_assert_equal_i(0, written);

  // empty end works just fine
  s_sys_activity_get_minute_history_values = (sys_activity_get_minute_history_values) {
    .out[0] = {
      .num_records = 2,
      .result = true,
    },
  };
  written = health_service_get_minute_history(data, ARRAY_LENGTH(data), &time_start, NULL);
  cl_assert_equal_i(2, written);
}

void test_health__get_minute_history(void) {
  const time_t now = rtc_get_time();

  HealthMinuteData data[5] = {};
  uint32_t written;
  s_sys_activity_get_minute_history_values = (sys_activity_get_minute_history_values) {
    .out[0] = {
      .num_records = 3,
      .result = true,
      .utc_start = now - 10 * SECONDS_PER_MINUTE,
      .records = {
        {.is_invalid = false, .steps = 1},
        {.is_invalid = true, .steps = 2},
        {.is_invalid = false, .steps = 3},
      },
    },
  };

  // pass time that's not exactly on a boundary
  time_t time_start = now - 10 * SECONDS_PER_MINUTE - 30;
  time_t time_end = now - 20;
  written = health_service_get_minute_history(data, ARRAY_LENGTH(data), &time_start, &time_end);
  cl_assert_equal_i(3, written);
  cl_assert_equal_i(now - 10 * SECONDS_PER_MINUTE, time_start);
  cl_assert_equal_i(time_start + written * SECONDS_PER_MINUTE, time_end);
  cl_assert_equal_i(1, data[0].steps);
  cl_assert_equal_i(2, data[1].steps);
  cl_assert_equal_i(3, data[2].steps);

  // if internal sys_activity returns false, no records were written
  s_sys_activity_get_minute_history_values.stage = 0;
  s_sys_activity_get_minute_history_values.out[0].result = false;
  written = health_service_get_minute_history(data, ARRAY_LENGTH(data), &time_start, &time_end);
  cl_assert_equal_i(0, written);
}

void test_health__get_minute_history_respects_time_end(void) {
  s_sys_activity_get_minute_history_values.stage = 0;
  s_sys_activity_get_minute_history_values.out[0].asserts = false;

  HealthMinuteData data[5] = {};

  // pass time that's not exactly on a boundary
  time_t time_start;
  time_t time_end;

  ////////////
  // start time on boundary
  const time_t time_on_boundary = (rtc_get_time() / 60 * 60) - 10 * 60;

  // respects time_end, 2.5 minutes => 3 records
  time_start = time_on_boundary;
  time_end = time_start + (5 * SECONDS_PER_MINUTE / 2);
  health_service_get_minute_history(data, ARRAY_LENGTH(data), &time_start, &time_end);
  cl_assert_equal_i(3, s_sys_activity_get_minute_history_values.in[0].num_records);

  // respects time_end, 1 minute => 1 records
  s_sys_activity_get_minute_history_values.stage = 0;
  time_start = time_on_boundary;
  time_end = time_start + SECONDS_PER_MINUTE;
  health_service_get_minute_history(data, ARRAY_LENGTH(data), &time_start, &time_end);
  cl_assert_equal_i(1, s_sys_activity_get_minute_history_values.in[0].num_records);

  // respects time_end == time_start => 0 records
  s_sys_activity_get_minute_history_values.stage = 0;
  time_start = time_on_boundary;
  time_end = time_start;
  health_service_get_minute_history(data, ARRAY_LENGTH(data), &time_start, &time_end);
  cl_assert_equal_i(0, s_sys_activity_get_minute_history_values.in[0].num_records);

  ////////////
  // start time almost on the next minute
  s_sys_activity_get_minute_history_values.stage = 0;
  const time_t time_almost_next_minute = time_on_boundary + 59;
  // respects time_end, 2.5 minutes => 3 records
  time_start = time_almost_next_minute;
  time_end = time_start + (5 * SECONDS_PER_MINUTE / 2);
  health_service_get_minute_history(data, ARRAY_LENGTH(data), &time_start, &time_end);
  cl_assert_equal_i(4, s_sys_activity_get_minute_history_values.in[0].num_records);

  // respects time_end, 1 minute => 1 records
  s_sys_activity_get_minute_history_values.stage = 0;
  time_start = time_almost_next_minute;
  time_end = time_start + SECONDS_PER_MINUTE;
  health_service_get_minute_history(data, ARRAY_LENGTH(data), &time_start, &time_end);
  cl_assert_equal_i(2, s_sys_activity_get_minute_history_values.in[0].num_records);

  // respects time_end == time_start => 0 records
  s_sys_activity_get_minute_history_values.stage = 0;
  time_start = time_almost_next_minute;
  time_end = time_start;
  health_service_get_minute_history(data, ARRAY_LENGTH(data), &time_start, &time_end);
  cl_assert_equal_i(1, s_sys_activity_get_minute_history_values.in[0].num_records);
}

void test_health__get_yesterdays_sleep_activity(void) {
  HealthValue start_sec;
  HealthValue end_sec;
  // our mock setup doesn't provide configurations for multiple calls
  // as health_service_private_get_yesterdays_sleep_activity() calls the function twice, both
  // values will have the same value

  s_sys_activity_get_metric_values.out.history[0] = 123;
  bool success = health_service_private_get_yesterdays_sleep_activity(&start_sec, &end_sec);
  cl_assert(success);
  cl_assert_equal_i(123, start_sec);
  cl_assert_equal_i(123, end_sec);
  cl_assert_equal_i(1, s_sys_activity_get_metric_values.in.history_len);
  cl_assert_equal_i(ActivityMetricSleepExitAtSeconds, s_sys_activity_get_metric_values.in.metric);
}

// Test that health_service_sum_averaged() returns the correct result when asked to get the
// average daily value of a metric
void test_health__avg_full_days(void) {
  // Get the current time and day
  const time_t now = rtc_get_time();
  struct tm local_tm;
  localtime_r(&now, &local_tm);
  DayInWeek day_in_week = local_tm.tm_wday;

  // ----------------------------------------
  // Let's fill in some known data for the daily totals and accumulate the totals and counts
  // for each day of the week.
  int day_totals[DAYS_PER_WEEK] = {};
  int day_counts[DAYS_PER_WEEK] = {};
  for (int i = 0; i < ACTIVITY_HISTORY_DAYS; i++, day_in_week--) {
    day_in_week = positive_modulo(day_in_week, DAYS_PER_WEEK);
    s_sys_activity_get_metric_values.out.history[i] = 1000 + (i * 50);

    // Day 0 is not included in the stats
    if (i == 0) {
      continue;
    }

    // Increment totals for each day of the week
    day_totals[day_in_week] += s_sys_activity_get_metric_values.out.history[i];
    day_counts[day_in_week] += 1;
  }

  // ----------------------------------------
  // Compute expected values
  int exp_weekly = day_totals[local_tm.tm_wday] / day_counts[local_tm.tm_wday];

  int exp_daily = 0;
  int count = 0;
  for (int i = 0; i < DAYS_PER_WEEK; i++) {
    exp_daily += day_totals[i];
    count += day_counts[i];
  }
  exp_daily /= count;

  int exp_weekend = (day_totals[Sunday] + day_totals[Saturday])
                    / (day_counts[Sunday] + day_counts[Saturday]);

  int exp_weekday = 0;
  count = 0;
  for (int i = Monday; i <= Friday; i++) {
    exp_weekday += day_totals[i];
    count += day_counts[i];
  }
  exp_weekday /= count;


  // ----------------------------------------
  // Compute each type of daily average using the API and compare to expected
  HealthValue result;
  result = health_service_sum_averaged(HealthMetricStepCount,
                                       time_util_get_midnight_of(now) - SECONDS_PER_DAY,
                                       time_util_get_midnight_of(now), HealthServiceTimeScopeDaily);
  cl_assert_equal_i(result, exp_daily);

  // All of our tests set "now" to Mon, 28 Dec 2015 09:12:22 GMT, so yesteday was a Sunday
  result = health_service_sum_averaged(HealthMetricStepCount,
                                       time_util_get_midnight_of(now) - SECONDS_PER_DAY,
                                       time_util_get_midnight_of(now),
                                       HealthServiceTimeScopeDailyWeekdayOrWeekend);
  cl_assert_equal_i(result, exp_weekend);

  // All of our tests set "now" to Mon, 28 Dec 2015 09:12:22 GMT, so today is a weekday
  result = health_service_sum_averaged(HealthMetricStepCount,
                                       time_util_get_midnight_of(now),
                                       time_util_get_midnight_of(now) + SECONDS_PER_DAY,
                                       HealthServiceTimeScopeDailyWeekdayOrWeekend);
  cl_assert_equal_i(result, exp_weekday);

  // Average weekly value
  result = health_service_sum_averaged(HealthMetricStepCount,
                                       time_util_get_midnight_of(now),
                                       time_util_get_midnight_of(now) + SECONDS_PER_DAY,
                                       HealthServiceTimeScopeWeekly);
  cl_assert_equal_i(result, exp_weekly);

  // Average weekly 48hr avg
  result = health_service_sum_averaged(HealthMetricStepCount,
                                       time_util_get_midnight_of(now),
                                       time_util_get_midnight_of(now) + 2 * SECONDS_PER_DAY,
                                       HealthServiceTimeScopeWeekly);
  cl_assert_equal_i(result, 2 *exp_weekly);

}

// Return the sum of a bunch of step average chunks that cover the given time range. The time
// range is given in the minute offsets from midnight. This logic is written to produce the
// same results as implemented in health_data_steps_get_current_average() of health_data.c
// Once the Health app is updated to also use the Health API, we can change this logic freely.
static uint32_t prv_averages_sum(uint32_t minute_start_idx, uint32_t minute_end_idx,
                                 const ActivityMetricAverages *avgs) {
  cl_assert(minute_start_idx < MINUTES_PER_DAY);
  cl_assert(minute_end_idx < MINUTES_PER_DAY);

  const int k_minutes_per_step_avg = MINUTES_PER_DAY / ACTIVITY_NUM_METRIC_AVERAGES;
  uint32_t chunk_start_idx = minute_start_idx / k_minutes_per_step_avg;
  uint32_t chunk_end_idx = minute_end_idx / k_minutes_per_step_avg;
  uint32_t sum = 0;
  for (int i = chunk_start_idx; i < chunk_end_idx; i++) {
    sum += avgs->average[i];
  }
  return sum;
}

// Test that health_service_sum_averaged() returns the correct result when asked to get the
// intraday average of a metric.
void test_health__avg_partial_days(void) {
  // Get the current time and day
  const time_t now = rtc_get_time();
  struct tm local_tm;
  localtime_r(&now, &local_tm);
  DayInWeek day_in_week = local_tm.tm_wday;

  // Our _initialize should set us to Monday, 9am UTC
  cl_assert_equal_i(day_in_week, Monday);

  // ----------------------------------
  // Let's fill in known data for the 15-minute step averages
  s_sys_activity_get_step_averages_values_weekday = (sys_activity_get_step_averages_values){
    .out.result = true,
  };
  s_sys_activity_get_step_averages_values_weekend = (sys_activity_get_step_averages_values){
    .out.result = true,
  };

  for (int i = 0;
       i < ARRAY_LENGTH(s_sys_activity_get_step_averages_values_weekday.out.averages.average);
       i++) {
    s_sys_activity_get_step_averages_values_weekday.out.averages.average[i] = i * 10;
    s_sys_activity_get_step_averages_values_weekend.out.averages.average[i] = i * 5;
  }


  // Let's fill in daily totals that will be used when 15-minute averages are not available
  // (i.e. for metrics other than step averages)
  const int k_daily_total = 960;
  for (int i = 0; i < ACTIVITY_HISTORY_DAYS; i++, day_in_week--) {
    s_sys_activity_get_metric_values.out.history[i] = k_daily_total;
  }

  // ---
  // Compute weekday step average from midnight to 9am. This should use the 15-minute
  // step averages that we stuffed in.
  uint32_t exp_value = prv_averages_sum(0, 9 * MINUTES_PER_HOUR,
      &s_sys_activity_get_step_averages_values_weekday.out.averages);

  time_t start_of_today = time_start_of_today();
  HealthValue value = health_service_sum_averaged(HealthMetricStepCount, start_of_today,
                                                  start_of_today + (9 * SECONDS_PER_HOUR),
                                                  HealthServiceTimeScopeDailyWeekdayOrWeekend);
  cl_assert_equal_i(value, exp_value);


  // ---
  // Compute weekday HealthMetricActiveSeconds from midnight to 9am. This should use the daily
  // totals since we don't have 15-minute averages maintained for this metric
  exp_value = (k_daily_total * 9 * MINUTES_PER_HOUR) / MINUTES_PER_DAY;

  start_of_today = time_start_of_today();
  value = health_service_sum_averaged(HealthMetricActiveSeconds, start_of_today,
                                                  start_of_today + (9 * SECONDS_PER_HOUR),
                                                  HealthServiceTimeScopeDailyWeekdayOrWeekend);
  cl_assert_equal_i(value, exp_value);


  // ---
  // Compute weekend step average from 4am to 9am. This should use the 15-minute
  // step averages that we stuffed in.
  exp_value = prv_averages_sum(4 * MINUTES_PER_HOUR, 9 * MINUTES_PER_HOUR,
                       &s_sys_activity_get_step_averages_values_weekend.out.averages);

  // Since "today" is Monday, going back 24 hours puts us on a weekend
  time_t start_time = time_start_of_today() - SECONDS_PER_DAY
                    + (4 * SECONDS_PER_HOUR);
  value = health_service_sum_averaged(HealthMetricStepCount, start_time,
                                                  start_time + (5 * SECONDS_PER_HOUR),
                                                  HealthServiceTimeScopeDailyWeekdayOrWeekend);
  cl_assert_equal_i(value, exp_value);

  // ---
  // Compute weekend HealthMetricActiveSeconds average from 4am to 9am. This should use the
  // daily totals since we don't havce 15-minute averages maintained for this metric
  exp_value = (k_daily_total * 5 * MINUTES_PER_HOUR) / MINUTES_PER_DAY;

  // Since "today" is Monday, going back 24 hours puts us on a weekend
  value = health_service_sum_averaged(HealthMetricActiveSeconds, start_time,
                                      start_time + (5 * SECONDS_PER_HOUR),
                                      HealthServiceTimeScopeDailyWeekdayOrWeekend);
  cl_assert_equal_i(value, exp_value);


  // ---
  // Compute daily step average from midnight to 9am. This should use the 15-minute
  // step averages that we stuffed in.
  exp_value = 5 * prv_averages_sum(0, 9 * MINUTES_PER_HOUR,
                        &s_sys_activity_get_step_averages_values_weekday.out.averages);
  exp_value += 2 * prv_averages_sum(0, 9 * MINUTES_PER_HOUR,
                        &s_sys_activity_get_step_averages_values_weekend.out.averages);
  exp_value /= 7;

  start_of_today = time_start_of_today();
  value = health_service_sum_averaged(HealthMetricStepCount, start_of_today,
                                                  start_of_today + (9 * SECONDS_PER_HOUR),
                                                  HealthServiceTimeScopeDaily);
  cl_assert_equal_i(value, exp_value);
}

void test_health__get_measurement_system_for_display(void) {
  MeasurementSystem actual =
    health_service_get_measurement_system_for_display(HealthMetricSleepSeconds);
  cl_assert_equal_i(actual, MeasurementSystemUnknown);

  s_units_distance_result = UnitsDistance_Miles;
  actual = health_service_get_measurement_system_for_display(HealthMetricWalkedDistanceMeters);
  cl_assert_equal_i(actual, MeasurementSystemImperial);

  s_units_distance_result = UnitsDistance_KM;
  actual = health_service_get_measurement_system_for_display(HealthMetricWalkedDistanceMeters);
  cl_assert_equal_i(actual, MeasurementSystemMetric);
}

void test_health__peek_current_value(void) {
  const time_t now_utc = rtc_get_time();

  // Set the return value to a valid time (Less than HS_MAX_AGE_HR_SAMPLE from the current time)
  prv_override_metric(ActivityMetricHeartRateFilteredUpdatedTimeUTC, now_utc);

  s_sys_activity_get_metric_values.out.history[0] = 123;
  s_sys_activity_get_metric_values.out.history[1] = 456;
  HealthValue result = health_service_peek_current_value(HealthMetricHeartRateBPM);
  cl_assert_equal_i(123, result);
  const ActivityMetric IN_METRIC = ActivityMetricHeartRateFilteredBPM;
  cl_assert_equal_i(s_sys_activity_get_metric_values.in.metric, IN_METRIC);
  cl_assert_equal_i(s_sys_activity_get_metric_values.in.history_len, 1);

  // This is the equivalent to `peek_value` with HeartRateBPM. Make sure it is equal.
  result = health_service_aggregate_averaged(HealthMetricHeartRateBPM,
                                             now_utc, now_utc,
                                             HealthAggregationAvg, HealthServiceTimeScopeOnce);
  cl_assert_equal_i(123, result);

  // This is the equivalent to `peek_value` with HeartRateBPM. Make sure it is equal.
  result = health_service_aggregate_averaged(HealthMetricHeartRateBPM,
                                             now_utc - 60, now_utc - 60,
                                             HealthAggregationAvg, HealthServiceTimeScopeOnce);
  cl_assert_equal_i(123, result);

  // The function call is the equivalent to `peek_value` with HeartRateBPM except for the
  // time stamps, make sure it returns 0 because we haven't filled in that data)
  result = health_service_aggregate_averaged(HealthMetricHeartRateBPM,
                                             now_utc - 61, now_utc - 61,
                                             HealthAggregationAvg, HealthServiceTimeScopeOnce);
  cl_assert_equal_i(0, result);

  // Set the return value to an invalid time (More than HS_MAX_AGE_HR_SAMPLE from the current time)
  prv_override_metric(ActivityMetricHeartRateFilteredUpdatedTimeUTC,
                      rtc_get_time() - 20 * SECONDS_PER_MINUTE);
  result = health_service_peek_current_value(HealthMetricHeartRateBPM);
  cl_assert_equal_i(0, result);

  // Asking for a cumulative metric should return error (0)
  result = health_service_peek_current_value(HealthMetricStepCount);
  cl_assert_equal_i(0, result);
}


static void prv_update_stats(HealthServiceStats *stats, HealthValue value) {
  stats->sum += value;
  stats->min = MIN(value, stats->min);
  stats->max = MAX(value, stats->max);
  stats->count++;
  stats->avg = stats->sum / stats->count;
}

// Test that health_service_aggregate_averaged() returns the correct result when asked to get the
// aggregates of rate metrics (like heart rate). For these metrics, only min, max, and avg are
// valid aggregation functions. The sum function is only applicable to cumulative metrics and is
// tested above in test_health__sum_full_days().

// DISBLAED because the firmware doesn't actually store daily history of HRM values.
void DISABLED_test_health__min_max_avg_full_days(void) {
  // Get the current time and day
  const time_t now = rtc_get_time();
  const time_t yeserday_utc = now - SECONDS_PER_DAY;

  struct tm local_tm;
  localtime_r(&now, &local_tm);
  DayInWeek todays_day_in_week = local_tm.tm_wday;

  localtime_r(&yeserday_utc, &local_tm);
  DayInWeek yesterday_day_in_week = local_tm.tm_wday;
  bool yesterday_was_weekend = (yesterday_day_in_week == Sunday)
                               || (yesterday_day_in_week == Saturday);

  PBL_LOG(LOG_LEVEL_DEBUG, "yesterday day in week: %d", yesterday_day_in_week);

  // ----------------------------------------
  // Let's fill in some known data for the daily totals and accumulate the stats
  HealthServiceStats weekly_stats = {
    .min = INT32_MAX,
    .max = INT32_MIN,
  };
  HealthServiceStats daily_stats = {
    .min = INT32_MAX,
    .max = INT32_MIN,
  };
  HealthServiceStats weekday_stats = {
    .min = INT32_MAX,
    .max = INT32_MIN,
  };
  HealthServiceStats weekend_stats = {
    .min = INT32_MAX,
    .max = INT32_MIN,
  };
  HealthServiceStats yesterday_stats = {
    .min = INT32_MAX,
    .max = INT32_MIN,
  };

  DayInWeek day_in_week = todays_day_in_week;
  for (int i = 0; i < ACTIVITY_HISTORY_DAYS; i++, day_in_week--) {
    day_in_week = positive_modulo(day_in_week, DAYS_PER_WEEK);
    HealthValue value = 1000 + (i * 50);
    s_sys_activity_get_metric_values.out.history[i] = value;

    PBL_LOG(LOG_LEVEL_DEBUG, "Day #%d, day_of_week: %d, value: %"PRIi32" ", i, day_in_week,
             value);
    // Day 0 is not included in the stats
    if (i == 0) {
      continue;
    }

    // Store if this is yesterday
    if (i == 1) {
      yesterday_stats = (HealthServiceStats) {
        .max = value,
        .min = value,
        .avg = value,
        .sum = value,
        .count = 1,
      };
    }

    // Update stats
    if (day_in_week == yesterday_day_in_week) {
      prv_update_stats(&weekly_stats, value);
      PBL_LOG(LOG_LEVEL_DEBUG, "Updating weekly stats with %"PRIi32": sum: %"PRIi32", "
               "avg: %"PRIi32" ", value, weekly_stats.sum, weekly_stats.avg);
    }
    if (day_in_week == Sunday || day_in_week == Saturday) {
      prv_update_stats(&weekend_stats, value);
    } else {
      prv_update_stats(&weekday_stats, value);
    }
    prv_update_stats(&daily_stats, value);
  }


  // ----------------------------------------
  // Compute each combination of agg/stat and compare to expected
  for (HealthAggregation agg = HealthAggregationSum; agg <= HealthAggregationMax; agg++) {
    for (HealthServiceTimeScope scope = HealthServiceTimeScopeOnce;
         scope <= HealthServiceTimeScopeDaily; scope++) {

      // Figure out the expected value
      HealthServiceStats *stats = NULL;
      char *scope_str;
      switch (scope) {
        case HealthServiceTimeScopeOnce:
          stats = &yesterday_stats;
          scope_str = "once";
          break;
        case HealthServiceTimeScopeWeekly:
          stats = &weekly_stats;
          scope_str = "weekly";
          break;
        case HealthServiceTimeScopeDailyWeekdayOrWeekend:
          stats = yesterday_was_weekend ? &weekend_stats : &weekday_stats;
          scope_str = "weekday/weekend";
          break;
        case HealthServiceTimeScopeDaily:
          stats = &daily_stats;
          scope_str = "daily";
          break;
      }

      HealthValue exp_value = 0;
      char *agg_str;
      switch (agg) {
        case HealthAggregationSum:
          exp_value = 0;    // error case
          agg_str = "sum";
          break;
        case HealthAggregationAvg:
          exp_value = stats->avg;
          agg_str = "avg";
          break;
        case HealthAggregationMin:
          exp_value = stats->min;
          agg_str = "min";
          break;
        case HealthAggregationMax:
          exp_value = stats->max;
          agg_str = "max";
          break;
      }

      // Get the value. Since we are computing min, max, avg and we only store 1 value per day
      // in our history, passing in a time range less than a day should produce the same result
      // as passing in a full day
      time_t time_start = time_util_get_midnight_of(now) - SECONDS_PER_DAY;
      time_t time_end = time_start + 12 * SECONDS_PER_HOUR;  // partial day

      // Heart rate should return error if asked for min/max with scope since we can't
      // compute that.
      if (scope != HealthServiceTimeScopeOnce
        && (agg == HealthAggregationMax || agg == HealthAggregationMin)) {
        exp_value = 0;
      }

      HealthValue result;
      result = health_service_aggregate_averaged(HealthMetricHeartRateBPM,
                                                 time_start, time_end, agg, scope);
      PBL_LOG(LOG_LEVEL_INFO, "Testing %-16s %-16s exp_value: %5"PRIi32", act_value: "
              "%5"PRIi32" " , scope_str, agg_str, exp_value, result);

      if (scope != HealthServiceTimeScopeOnce) {
        // Only test scoped results. Non-scoped results for heart rate are computed using
        // minute data and verified in the test_health__heart_rate_scope_once()
        cl_assert_equal_i(result, exp_value);
      }
    }
  }
}

// --------------------------------------------------------------------------------------
// Test calls to compute heart rate stats with scope once. This ends up being implemented using
// the minute history
void test_health__heart_rate_scope_once(void) {
  const time_t now = rtc_get_time();

  const time_t time_start = now - 2 * SECONDS_PER_HOUR;
  const time_t time_end = now;

  // ----------------------------------------------------------------
  // Put in our minute history
  HealthServiceCache *cache = NULL;
  unsigned num_minutes_per_call = ARRAY_LENGTH(cache->minute_data);
  s_sys_activity_get_minute_history_values = (sys_activity_get_minute_history_values) {
    .out[0] = {
      .num_records = num_minutes_per_call,
      .result = true,
      .utc_start = time_start,
    },
    .out[1] = {
      .num_records = num_minutes_per_call,
      .result = true,
      .utc_start = time_start,
    },
  };
  int32_t min_value = INT32_MAX;
  int32_t max_value = INT32_MIN;
  int32_t sum = 0;
  int32_t count = 0;
  uint8_t value = 50;
  for (unsigned i = 0; i < num_minutes_per_call; i++) {
    min_value = MIN(min_value, value);
    max_value = MAX(max_value, value);
    sum += value;
    count++;
    s_sys_activity_get_minute_history_values.out[0].records[i].heart_rate_bpm = value;
    value++;
    if (value > 200) {
      value = 50;
    }
  }
  for (unsigned i = 0; i < num_minutes_per_call; i++) {
    min_value = MIN(min_value, value);
    max_value = MAX(max_value, value);
    sum += value;
    count++;
    s_sys_activity_get_minute_history_values.out[1].records[i].heart_rate_bpm = value;
    value++;
    if (value > 200) {
      value = 50;
    }
  }
  int32_t avg_value = ROUND(sum, count);

  // Test each aggregation
  for (HealthAggregation agg = HealthAggregationAvg; agg <= HealthAggregationMax; agg++) {

    s_sys_activity_get_minute_history_values.stage = 0;
    if (agg == HealthAggregationMin) {
      PBL_LOG(LOG_LEVEL_DEBUG, "foo");
    }
    HealthValue result = health_service_aggregate_averaged(HealthMetricHeartRateBPM,
                                                           time_start, time_end,
                                                           agg, HealthServiceTimeScopeOnce);

    cl_assert_equal_i(s_sys_activity_get_minute_history_values.in[0].utc_start, time_start);
    cl_assert_equal_i(s_sys_activity_get_minute_history_values.in[0].num_records,
                      num_minutes_per_call);
    cl_assert_equal_i(s_sys_activity_get_minute_history_values.in[1].utc_start,
                      time_start + SECONDS_PER_HOUR);
    cl_assert_equal_i(s_sys_activity_get_minute_history_values.in[1].num_records,
                      num_minutes_per_call);

    if (agg == HealthAggregationAvg) {
      cl_assert_equal_i(result, avg_value);
    } else if (agg == HealthAggregationMin) {
      cl_assert_equal_i(result, min_value);
    } else if (agg == HealthAggregationMax) {
      cl_assert_equal_i(result, max_value);
    } else {
      cl_assert(false);
    }
  }

}


// --------------------------------------------------------------------------------------
int s_metric_alert_count;
static void prv_test_event_handler(HealthEventType event, void *context) {
  if (event == HealthEventMetricAlert) {
    s_metric_alert_count++;
  }
}

// --------------------------------------------------------------------------------------
// Test the health metric alert generation
void test_health__metric_alert_generation(void) {
  health_service_events_subscribe(prv_test_event_handler, NULL);
  s_sys_activity_get_metric_values.out.result = true;

  PebbleEvent event = {
    .type = PEBBLE_HEALTH_SERVICE_EVENT,
    .health_event.type = HealthEventHeartRateUpdate,
  };

  // Process some events with various heart rates, should not get an alert
  s_metric_alert_count = 0;
  for (int i = 50; i < 60; i++) {
    s_sys_activity_get_metric_values.out.history[0] = i;
    prv_health_event_handler(&event, NULL);
  }
  // Should not get any metric alerts because none registered
  cl_assert_equal_i(s_metric_alert_count, 0);


  // Create an alert for heart rate 65
  HealthMetricAlert *alert = health_service_register_metric_alert(HealthMetricHeartRateBPM, 65);
  for (int i = 60; i < 70; i++) {
    s_sys_activity_get_metric_values.out.history[0] = i;
    prv_health_event_handler(&event, NULL);
  }
  // One alert on the way up
  cl_assert_equal_i(s_metric_alert_count, 1);

  for (int i = 70; i >= 60; i--) {
    s_sys_activity_get_metric_values.out.history[0] = i;
    PebbleEvent event = {
      .type = PEBBLE_HEALTH_SERVICE_EVENT,
      .health_event.type = HealthEventHeartRateUpdate,
    };
    prv_health_event_handler(&event, NULL);
  }
  // One alert on the way down
  cl_assert_equal_i(s_metric_alert_count, 2);


  // Remove the alert
  s_metric_alert_count = 0;
  health_service_cancel_metric_alert(alert);
  for (int i = 60; i < 70; i++) {
    s_sys_activity_get_metric_values.out.history[0] = i;
    prv_health_event_handler(&event, NULL);
  }
  // Should not get an alert
  cl_assert_equal_i(s_metric_alert_count, 0);
}

// --------------------------------------------------------------------------------------
// Test the health metric alert registration
void test_health__metric_alert_registration(void) {
  health_service_events_subscribe(prv_test_event_handler, NULL);

  time_t now = rtc_get_time();
  HealthServiceAccessibilityMask accessible = health_service_metric_aggregate_averaged_accessible(
    HealthMetricHeartRateBPM, now, now, HealthAggregationAvg, HealthServiceTimeScopeOnce);
  cl_assert(accessible & HealthServiceAccessibilityMaskAvailable);

  // Create an alert for heart rate
  HealthMetricAlert *alert = health_service_register_metric_alert(HealthMetricHeartRateBPM, 65);
  cl_assert(alert != NULL);

  // If we try to register another for heart rate, it should fail
  HealthMetricAlert *fail_alert = health_service_register_metric_alert(HealthMetricHeartRateBPM,
                                                                       65);
  cl_assert(fail_alert == NULL);

  // Cancel the original
  health_service_cancel_metric_alert(alert);

  // Should be able to register another now
  alert = health_service_register_metric_alert(HealthMetricHeartRateBPM, 65);
  cl_assert(alert != NULL);
}

