/*
 * Copyright 2024 Google LLC
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

#include "clar.h"

#include "services/normal/activity/activity.h"
#include "services/normal/activity/activity_calculators.h"
#include "services/normal/activity/workout_service.h"
#include "services/common/hrm/hrm_manager.h"
#include "process_management/app_install_types.h"
#include "util/time/time.h"
#include "util/units.h"

// ---------------------------------------------------------------------------------------
#include "stubs_activity_insights.h"
#include "stubs_evented_timer.h"
#include "stubs_logging.h"
#include "stubs_pbl_malloc.h"
#include "stubs_regular_timer.h"

#include "fake_rtc.h"
#include "fake_mutex.h"

extern void prv_abandon_workout_timer_callback(void *data);
extern void prv_abandoned_notification_timer_callback(void *data);
extern void prv_workout_timer_cb(void *data);
extern bool workout_service_get_avg_hr(int32_t *avg_hr_out);
extern bool workout_service_get_current_workout_hr_zone_time(int32_t *hr_zone_time_s_out);
extern void workout_service_get_active_kcalories(int32_t *active);
extern void workout_service_reset(void);

// Stubs
///////////////////////////////////////////////////////////

static ActivitySession s_saved_session;
void activity_sessions_prv_add_activity_session(ActivitySession *session) {
  s_saved_session = *session;
}
void activity_sessions_prv_delete_activity_session(ActivitySession *session) {}
void activity_algorithm_enable_activity_tracking(bool enable) {}

bool activity_get_sessions(uint32_t *session_entries, ActivitySession *sessions) {
  return false;
}

uint8_t activity_prefs_get_age_years(void) {
  return 30; // This is our current default
}

ActivityGender activity_prefs_get_gender(void) {
  return ActivityGenderMale;
}

uint16_t activity_prefs_get_weight_dag(void) {
  return 7539;
}

uint16_t activity_prefs_get_height_mm(void) {
  return 1900;
}

uint8_t activity_prefs_heart_get_elevated_hr(void) {
  return 100;
}

uint8_t activity_prefs_heart_get_zone1_threshold(void) {
  return 130;
}

uint8_t activity_prefs_heart_get_zone2_threshold(void) {
  return 154;
}

uint8_t activity_prefs_heart_get_zone3_threshold(void) {
  return 172;
}

AppInstallId app_get_app_id(void) {
  return 0;
}

// ---------------------------------------------------------------------------------------

static bool s_hrm_subscribed;
static uint32_t s_hrm_expiration;
HRMSessionRef sys_hrm_manager_app_subscribe(AppInstallId app_id, uint32_t update_interval_s,
                                            uint16_t expire_s, HRMFeature features) {
  s_hrm_subscribed = true;
  s_hrm_expiration = expire_s;
  return 1;
}

bool sys_hrm_manager_unsubscribe(HRMSessionRef ref) {
  s_hrm_subscribed = false;
  s_hrm_expiration = 0;
  return true;
}

uint32_t time_get_uptime_seconds(void) {
  return SECONDS_PER_DAY + rtc_get_time();
}

static uint32_t s_total_step_count;
bool activity_get_metric(ActivityMetric metric, uint32_t history_len, int32_t *history) {
  if (metric != ActivityMetricStepCount) {
    return false;
  }
  *history = s_total_step_count;
  return true;
}

static bool s_abandoned_workout_notification_sent;
void workout_utils_send_abandoned_workout_notification() {
  s_abandoned_workout_notification_sent = true;
}

// ---------------------------------------------------------------------------------------
static void prv_inc_steps_and_put_event(int steps) {
  s_total_step_count += steps;

  PebbleEvent event = {
    .type = PEBBLE_HEALTH_SERVICE_EVENT,
    .health_event = {
      .type = HealthEventMovementUpdate,
      .data.movement_update.steps = s_total_step_count,
    },
  };
  event_put(&event);

  workout_service_health_event_handler(&event.health_event);
}

// ---------------------------------------------------------------------------------------
static void prv_put_bpm_event(int bpm, HRMQuality quality) {
  PebbleEvent event = {
    .type = PEBBLE_HEALTH_SERVICE_EVENT,
    .health_event = {
      .type = HealthEventHeartRateUpdate,
      .data.heart_rate_update.current_bpm = bpm,
      .data.heart_rate_update.quality = quality,
    },
  };
  event_put(&event);

  workout_service_health_event_handler(&event.health_event);
}

static void prv_inc_time(int seconds) {
  fake_rtc_increment_time(seconds);
  prv_workout_timer_cb(NULL);
}

// ---------------------------------------------------------------------------------------
void test_workout_service__initialize(void) {
  fake_rtc_init(0, 0);

  workout_service_reset();

  // Start the step value for the day at X
  s_total_step_count = 5000;
  s_hrm_expiration = 0;
  s_hrm_subscribed = false;
  s_abandoned_workout_notification_sent = false;

  const bool assert_all_unlocked = true;
  fake_mutex_reset(assert_all_unlocked);
  workout_service_init();
}

void test_workout_service__cleanup(void) {
  workout_service_stop_workout();

  const bool assert_all_unlocked = true;
  fake_mutex_reset(assert_all_unlocked);
}

// ---------------------------------------------------------------------------------------
// Tests
// ---------------------------------------------------------------------------------------
void test_workout_service__basic(void) {
  cl_assert(workout_service_start_workout(ActivitySessionType_Run));

  int32_t steps, duration_s, distance_m, bpm;
  HRZone hr_zone;

  cl_assert(workout_service_get_current_workout_info(&steps, &duration_s, &distance_m,
                                                     &bpm, &hr_zone));
  cl_assert_equal_i(steps, 0);
  cl_assert_equal_i(duration_s, 0);
  cl_assert_equal_i(distance_m, 0);
  cl_assert_equal_i(bpm, 0);
  cl_assert_equal_i(hr_zone, 0);

  // Get some step info
  prv_inc_time(5 * SECONDS_PER_MINUTE);
  prv_inc_steps_and_put_event(900 /* 180 steps per min * 5 mins */);
  cl_assert(workout_service_get_current_workout_info(&steps, &duration_s, &distance_m,
                                                     &bpm, &hr_zone));
  cl_assert_equal_i(steps, 900);
  cl_assert_equal_i(5900, s_total_step_count);
  cl_assert_equal_i(distance_m, 1201 /* 1.2km in 5 mins is reasonable */);
  cl_assert_equal_i(duration_s, 5 * SECONDS_PER_MINUTE);

  // Get some HR info
  prv_put_bpm_event(100, HRMQuality_Good);
  cl_assert(workout_service_get_current_workout_info(&steps, &duration_s, &distance_m,
                                                     &bpm, &hr_zone));
  cl_assert_equal_i(bpm, 100);
  cl_assert_equal_i(hr_zone, 0);

  // Get some more step info
  prv_inc_time(5 * SECONDS_PER_MINUTE);
  prv_inc_steps_and_put_event(900 /* 180 steps per min * 5 mins */);
  cl_assert(workout_service_get_current_workout_info(&steps, &duration_s, &distance_m,
                                                     &bpm, &hr_zone));
  cl_assert_equal_i(steps, 1800);
  cl_assert_equal_i(6800, s_total_step_count);
  cl_assert_equal_i(distance_m, 2402 /* 2.4km in 10 mins is reasonable */);
  cl_assert_equal_i(duration_s, 10 * SECONDS_PER_MINUTE);

  // Get some more HR info
  prv_inc_time(10);
  prv_put_bpm_event(180, HRMQuality_Good);
  cl_assert(workout_service_get_current_workout_info(&steps, &duration_s, &distance_m,
                                                     &bpm, &hr_zone));
  cl_assert_equal_i(bpm, 180);
  cl_assert_equal_i(hr_zone, 3);
  cl_assert_equal_i(duration_s, 10 * SECONDS_PER_MINUTE + 10);

  cl_assert(workout_service_stop_workout());
  cl_assert(!workout_service_get_current_workout_info(&steps, &duration_s, &distance_m,
                                                      &bpm, &hr_zone));
}

// ---------------------------------------------------------------------------------------
void test_workout_service__takeover_activity_session(void) {
  int32_t steps, duration_s, distance_m, bpm, active_kcalories;
  HRZone hr_zone;

  // We have a session that started at utc=10 and length_min of 10. Increment the time
  // so that makes sense
  prv_inc_time(610);

  // Start a new session
  ActivitySession session = {
    .start_utc = 10,
    .length_min = 10,
    .type = ActivitySessionType_Run,
    .ongoing = true,
    .manual = false,
    .step_data = {
      .steps = 2000,
      .active_kcalories = 200,
      .resting_kcalories = 100,
      .distance_meters = 1600,
    },
  };

  cl_assert(workout_service_takeover_activity_session(&session));

  cl_assert(workout_service_get_current_workout_info(&steps, &duration_s, &distance_m,
                                                     &bpm, &hr_zone));
  cl_assert_equal_i(steps, 2000);
  cl_assert_equal_i(duration_s, 600);
  cl_assert_equal_i(distance_m, 1600);

  // Get the active calories, should be same as session
  workout_service_get_active_kcalories(&active_kcalories);
  cl_assert_equal_i(200, active_kcalories);

  // Add some time and steps
  prv_inc_time(600); /* 10 mins */
  prv_inc_steps_and_put_event(1800 /* 180 steps per min * 10 mins */);

  // Grab the new distance and active_kcalories
  int32_t new_active_kcalories, new_distance_m;
  cl_assert(workout_service_get_current_workout_info(&steps, &duration_s, &new_distance_m,
                                                     &bpm, &hr_zone));

  // Compute our own version of active_kcalories
  const int32_t distance_delta_m = (new_distance_m - distance_m);
  const int32_t calculated_active_kcalories
      = ROUND(activity_private_compute_active_calories(distance_delta_m * MM_PER_METER, 600 * MS_PER_SECOND),
              ACTIVITY_CALORIES_PER_KCAL);

  // Make sure that the new_active_kcalories has increased
  workout_service_get_active_kcalories(&new_active_kcalories);
  cl_assert(active_kcalories < new_active_kcalories);

  // Double check that we calculated the same value that the workout service got
  cl_assert_equal_i(calculated_active_kcalories, new_active_kcalories - active_kcalories);

  workout_service_stop_workout();

  // Pull out the saved session and make sure it matches our numbers
  ActivitySession *stored_session = &s_saved_session;
  cl_assert_equal_i(stored_session->start_utc, session.start_utc);
  cl_assert_equal_i(stored_session->length_min, 20);
  cl_assert_equal_i(stored_session->step_data.steps, 3800);
  cl_assert_equal_i(stored_session->step_data.active_kcalories, 200 + calculated_active_kcalories);
  cl_assert_equal_i(stored_session->step_data.resting_kcalories,
                    ROUND(activity_private_compute_resting_calories(stored_session->length_min),
                          ACTIVITY_CALORIES_PER_KCAL));
  cl_assert_equal_b(stored_session->manual, true);
  cl_assert(stored_session->step_data.distance_meters > 2000);
}

// ---------------------------------------------------------------------------------------
void test_workout_service__pause_resume(void) {
  int32_t steps, duration_s, distance_m, bpm;
  HRZone hr_zone;

  cl_assert(workout_service_start_workout(ActivitySessionType_Run));
  prv_inc_time(10);
  prv_inc_steps_and_put_event(10);
  prv_put_bpm_event(100, HRMQuality_Good);

  cl_assert(workout_service_get_current_workout_info(&steps, &duration_s, &distance_m,
                                                     &bpm, &hr_zone));
  cl_assert_equal_i(steps, 10);
  cl_assert_equal_i(duration_s, 10);
  cl_assert_equal_i(bpm, 100);

  cl_assert(workout_service_pause_workout(true));

  // Get some new data but out stats shouldn't change (except HR)
  prv_inc_time(10);
  prv_inc_steps_and_put_event(10);
  prv_put_bpm_event(110, HRMQuality_Good);
  cl_assert(workout_service_get_current_workout_info(&steps, &duration_s, &distance_m,
                                                     &bpm, &hr_zone));
  cl_assert_equal_i(steps, 10);
  cl_assert_equal_i(duration_s, 10);
  cl_assert_equal_i(bpm, 110);

  // Get some more new data and out stats still shouldn't change (except HR)
  prv_inc_time(10);
  prv_inc_steps_and_put_event(10);
  prv_put_bpm_event(190, HRMQuality_Good);
  cl_assert(workout_service_get_current_workout_info(&steps, &duration_s, &distance_m,
                                                     &bpm, &hr_zone));
  cl_assert_equal_i(steps, 10);
  cl_assert_equal_i(duration_s, 10);
  cl_assert_equal_i(bpm, 190);

  // Resume the workout and get more data (it updates this time)
  cl_assert(workout_service_pause_workout(false));
  prv_inc_time(10);
  prv_inc_steps_and_put_event(10);
  prv_put_bpm_event(80, HRMQuality_Good);
  cl_assert(workout_service_get_current_workout_info(&steps, &duration_s, &distance_m,
                                                     &bpm, &hr_zone));
  cl_assert_equal_i(steps, 20);
  cl_assert_equal_i(duration_s, 20);
  cl_assert_equal_i(bpm, 80);

  cl_assert(workout_service_pause_workout(true));
  cl_assert(workout_service_pause_workout(true));
  cl_assert(workout_service_get_current_workout_info(&steps, &duration_s, &distance_m,
                                                     &bpm, &hr_zone));
  cl_assert_equal_i(steps, 20);
  cl_assert_equal_i(duration_s, 20);
  cl_assert_equal_i(bpm, 80);

  prv_inc_time(10);
  prv_inc_steps_and_put_event(10);
  prv_put_bpm_event(117, HRMQuality_Good);

  cl_assert(workout_service_get_current_workout_info(&steps, &duration_s, &distance_m,
                                                     &bpm, &hr_zone));
  cl_assert_equal_i(steps, 20);
  cl_assert_equal_i(duration_s, 20);
  cl_assert_equal_i(bpm, 117);

  cl_assert(workout_service_pause_workout(false));
  cl_assert(workout_service_pause_workout(false));

  prv_inc_time(10);
  prv_inc_steps_and_put_event(10);
  prv_put_bpm_event(113, HRMQuality_Good);

  cl_assert(workout_service_get_current_workout_info(&steps, &duration_s, &distance_m,
                                                     &bpm, &hr_zone));
  cl_assert_equal_i(steps, 30);
  cl_assert_equal_i(duration_s, 30);
  cl_assert_equal_i(bpm, 113);

  cl_assert(workout_service_stop_workout());
}

// ---------------------------------------------------------------------------------------
void test_workout_service__expire_hr_reading(void) {
  int32_t steps, duration_s, distance_m, bpm;
  HRZone hr_zone;

  cl_assert(workout_service_start_workout(ActivitySessionType_Run));
  prv_put_bpm_event(100, HRMQuality_Good);

  cl_assert(workout_service_get_current_workout_info(&steps, &duration_s, &distance_m,
                                                     &bpm, &hr_zone));
  cl_assert_equal_i(bpm, 100);

  // Time forward X seconds
  prv_inc_time(30);

  cl_assert(workout_service_get_current_workout_info(&steps, &duration_s, &distance_m,
                                                     &bpm, &hr_zone));
  cl_assert_equal_i(bpm, 100);

  // Time forward X seconds
  prv_inc_time(30);

  cl_assert(workout_service_get_current_workout_info(&steps, &duration_s, &distance_m,
                                                     &bpm, &hr_zone));
  // HR Reading has expired. Should return 0
  cl_assert_equal_i(bpm, 0);

  cl_assert(workout_service_stop_workout());
}

// ---------------------------------------------------------------------------------------
void test_workout_service__receive_offwrist_reading(void) {
  int32_t steps, duration_s, distance_m, bpm;
  HRZone hr_zone;

  cl_assert(workout_service_start_workout(ActivitySessionType_Run));

  // Put a good quality reading. Verify it was accepted.
  prv_put_bpm_event(100, HRMQuality_Good);
  cl_assert(workout_service_get_current_workout_info(&steps, &duration_s, &distance_m,
                                                     &bpm, &hr_zone));
  cl_assert_equal_i(bpm, 100);

  // Put an OffWrist reading. Verify we received it.
  prv_put_bpm_event(50, HRMQuality_OffWrist);
  cl_assert(workout_service_get_current_workout_info(&steps, &duration_s, &distance_m,
                                                     &bpm, &hr_zone));
  cl_assert_equal_i(bpm, 0);
  cl_assert_equal_i(hr_zone, HRZone_Zone0);

  // Put a good quality reading. Verify it was accepted.
  prv_put_bpm_event(100, HRMQuality_Good);
  cl_assert(workout_service_get_current_workout_info(&steps, &duration_s, &distance_m,
                                                     &bpm, &hr_zone));
  cl_assert_equal_i(bpm, 100);

  cl_assert(workout_service_stop_workout());
}

// ---------------------------------------------------------------------------------------
void test_workout_service__working_out_past_midnight(void) {
  int32_t steps, duration_s, distance_m, bpm;
  HRZone hr_zone;

  cl_assert(workout_service_start_workout(ActivitySessionType_Run));

  // Start with only 10 steps and make sure it updates.
  prv_inc_steps_and_put_event(10);
  cl_assert(workout_service_get_current_workout_info(&steps, &duration_s, &distance_m,
                                                     &bpm, &hr_zone));
  cl_assert_equal_i(steps, 10);

  // Increment a lot so we can make sure that the wrap around of midnight works.
  prv_inc_steps_and_put_event(1000);
  cl_assert(workout_service_get_current_workout_info(&steps, &duration_s, &distance_m,
                                                     &bpm, &hr_zone));
  cl_assert_equal_i(steps, 1010);

  // Pretend we wrap around midnight. Midnight will reset the step count.
  s_total_step_count = 0;

  // Put some steps and make sure we don't delete or freak out, and append the new step count.
  prv_inc_steps_and_put_event(50);
  cl_assert(workout_service_get_current_workout_info(&steps, &duration_s, &distance_m,
                                                     &bpm, &hr_zone));
  cl_assert_equal_i(steps, 1060);
}

// ---------------------------------------------------------------------------------------
// Open the app and close the app. Make sure the HR monitor turns off instantly
void test_workout_service__app_open_close_no_workout(void) {
  // Put some time into the clock
  prv_inc_time(1 * SECONDS_PER_MINUTE);

  // Open the app, confirm that we are now subscribed with no end in sight
  workout_service_frontend_opened();
  cl_assert_equal_b(s_hrm_subscribed, true);
  cl_assert_equal_i(s_hrm_expiration, 0);

  workout_service_frontend_closed();
  cl_assert_equal_b(s_hrm_subscribed, false);
  cl_assert_equal_i(s_hrm_expiration, 0);
}

// ---------------------------------------------------------------------------------------
// Open the app, start a workout, close the app. Make sure the HR stays on for
// WORKOUT_ACTIVE_HR_SUBSCRIPTION_TS_EXPIRE
void test_workout_service__app_open_close_active_workout(void) {
  // Put some time into the clock
  prv_inc_time(1 * SECONDS_PER_MINUTE);

  // Open the app, confirm that we are now subscribed with no end in sight
  workout_service_frontend_opened();
  cl_assert_equal_b(s_hrm_subscribed, true);
  cl_assert_equal_i(s_hrm_expiration, 0);

  cl_assert(workout_service_start_workout(ActivitySessionType_Run));

  workout_service_frontend_closed();
  cl_assert_equal_b(s_hrm_subscribed, true);
  cl_assert_equal_i(s_hrm_expiration, SECONDS_PER_HOUR);
}

// ---------------------------------------------------------------------------------------
// Open the app, start a workout, 30s, stop the workout. Make sure the HR turns off instantly
// since workout wasn't valid (too short)
void test_workout_service__app_open_close_ended_invalid_workout(void) {
  // Put some time into the clock
  prv_inc_time(1 * SECONDS_PER_MINUTE);

  // Open the app, confirm that we are now subscribed with no end in sight
  workout_service_frontend_opened();
  cl_assert_equal_b(s_hrm_subscribed, true);
  cl_assert_equal_i(s_hrm_expiration, 0);

  prv_inc_time(1 * SECONDS_PER_MINUTE);
  cl_assert(workout_service_start_workout(ActivitySessionType_Run));
  // Workout of 30 seconds duration. Should be invalid, not keep HR on after leaving
  prv_inc_time(30);
  cl_assert(workout_service_stop_workout());

  workout_service_frontend_closed();
  cl_assert_equal_b(s_hrm_subscribed, false);
  cl_assert_equal_i(s_hrm_expiration, 0);
}

// ---------------------------------------------------------------------------------------
// Open the app, start a workout, 120s, stop the workout, 120s, close app.
// Make sure the HR stays on for WORKOUT_ENDED_HR_SUBSCRIPTION_TS_EXPIRE - 120s
void test_workout_service__app_open_close_valid_workout(void) {
  // Put some time into the clock
  prv_inc_time(1 * SECONDS_PER_MINUTE);

  // Open the app, confirm that we are now subscribed with no end in sight
  workout_service_frontend_opened();
  cl_assert_equal_b(s_hrm_subscribed, true);
  cl_assert_equal_i(s_hrm_expiration, 0);

  prv_inc_time(1 * SECONDS_PER_MINUTE);
  cl_assert(workout_service_start_workout(ActivitySessionType_Run));
  // Workout of 120 seconds duration. Should be valid
  prv_inc_time(2 * SECONDS_PER_MINUTE);
  cl_assert(workout_service_stop_workout());

  prv_inc_time(2 * SECONDS_PER_MINUTE);

  workout_service_frontend_closed();
  cl_assert_equal_b(s_hrm_subscribed, true);
  cl_assert_equal_i(s_hrm_expiration, 8 * SECONDS_PER_MINUTE);
}

// ---------------------------------------------------------------------------------------
// Open the app, start a workout, 120s, stop the workout, 20 min, close app.
// Make sure the HR turns off right after we leave the app.
void test_workout_service__app_open_wait_close_valid_workout(void) {
  // Put some time into the clock
  prv_inc_time(1 * SECONDS_PER_MINUTE);

  // Open the app, confirm that we are now subscribed with no end in sight
  workout_service_frontend_opened();
  cl_assert_equal_b(s_hrm_subscribed, true);
  cl_assert_equal_i(s_hrm_expiration, 0);

  prv_inc_time(1 * SECONDS_PER_MINUTE);
  cl_assert(workout_service_start_workout(ActivitySessionType_Run));
  // Workout of 120 seconds duration. Should be valid
  prv_inc_time(2 * SECONDS_PER_MINUTE);
  cl_assert(workout_service_stop_workout());

  // Wait 20 minutes. By this time, as soon as we leave the app, we should turn off the HR device.
  prv_inc_time(20 * SECONDS_PER_MINUTE);

  workout_service_frontend_closed();
  cl_assert_equal_b(s_hrm_subscribed, false);
  cl_assert_equal_i(s_hrm_expiration, 0);
}

// ---------------------------------------------------------------------------------------
void test_workout_service__heart_rate_zone_time(void) {
  const int ZONE_0_HR = 100;
  const int ZONE_1_HR = 140;
  const int ZONE_2_HR = 160;
  const int ZONE_3_HR = 180;

  int32_t hr_zone_time_s[HRZoneCount];

  cl_assert(workout_service_start_workout(ActivitySessionType_Run));
  cl_assert(workout_service_get_current_workout_hr_zone_time(hr_zone_time_s));
  cl_assert_equal_i(hr_zone_time_s[HRZone_Zone0], 0);
  cl_assert_equal_i(hr_zone_time_s[HRZone_Zone1], 0);
  cl_assert_equal_i(hr_zone_time_s[HRZone_Zone2], 0);
  cl_assert_equal_i(hr_zone_time_s[HRZone_Zone3], 0);

  prv_put_bpm_event(ZONE_1_HR, HRMQuality_Good);
  prv_inc_time(10);
  prv_put_bpm_event(ZONE_1_HR, HRMQuality_Good);
  cl_assert(workout_service_get_current_workout_hr_zone_time(hr_zone_time_s));
  cl_assert_equal_i(hr_zone_time_s[HRZone_Zone0], 0);
  cl_assert_equal_i(hr_zone_time_s[HRZone_Zone1], 10);
  cl_assert_equal_i(hr_zone_time_s[HRZone_Zone2], 0);
  cl_assert_equal_i(hr_zone_time_s[HRZone_Zone3], 0);

  prv_inc_time(10);
  prv_put_bpm_event(ZONE_2_HR, HRMQuality_Good);
  cl_assert(workout_service_get_current_workout_hr_zone_time(hr_zone_time_s));
  cl_assert_equal_i(hr_zone_time_s[HRZone_Zone0], 0);
  cl_assert_equal_i(hr_zone_time_s[HRZone_Zone1], 10);
  cl_assert_equal_i(hr_zone_time_s[HRZone_Zone2], 10);
  cl_assert_equal_i(hr_zone_time_s[HRZone_Zone3], 0);

  prv_inc_time(10);
  prv_put_bpm_event(ZONE_3_HR, HRMQuality_Good);
  cl_assert(workout_service_get_current_workout_hr_zone_time(hr_zone_time_s));
  cl_assert_equal_i(hr_zone_time_s[HRZone_Zone0], 0);
  cl_assert_equal_i(hr_zone_time_s[HRZone_Zone1], 10);
  cl_assert_equal_i(hr_zone_time_s[HRZone_Zone2], 10);
  cl_assert_equal_i(hr_zone_time_s[HRZone_Zone3], 10);

  prv_inc_time(10);
  prv_put_bpm_event(ZONE_0_HR, HRMQuality_Good);
  cl_assert(workout_service_get_current_workout_hr_zone_time(hr_zone_time_s));
  cl_assert_equal_i(hr_zone_time_s[HRZone_Zone0], 10);
  cl_assert_equal_i(hr_zone_time_s[HRZone_Zone1], 10);
  cl_assert_equal_i(hr_zone_time_s[HRZone_Zone2], 10);
  cl_assert_equal_i(hr_zone_time_s[HRZone_Zone3], 10);

  prv_inc_time(10);
  prv_put_bpm_event(ZONE_1_HR, HRMQuality_Good);
  workout_service_get_current_workout_hr_zone_time(hr_zone_time_s);
  cl_assert_equal_i(hr_zone_time_s[HRZone_Zone0], 10);
  cl_assert_equal_i(hr_zone_time_s[HRZone_Zone1], 20);
  cl_assert_equal_i(hr_zone_time_s[HRZone_Zone2], 10);
  cl_assert_equal_i(hr_zone_time_s[HRZone_Zone3], 10);

  // Pause the workout. Don't accumulate time in zones
  cl_assert(workout_service_pause_workout(true));

  prv_inc_time(10);
  prv_put_bpm_event(ZONE_3_HR, HRMQuality_Good);
  cl_assert(workout_service_get_current_workout_hr_zone_time(hr_zone_time_s));
  cl_assert_equal_i(hr_zone_time_s[HRZone_Zone0], 10);
  cl_assert_equal_i(hr_zone_time_s[HRZone_Zone1], 20);
  cl_assert_equal_i(hr_zone_time_s[HRZone_Zone2], 10);
  cl_assert_equal_i(hr_zone_time_s[HRZone_Zone3], 10);

  prv_inc_time(10);
  prv_put_bpm_event(ZONE_1_HR, HRMQuality_Good);
  cl_assert(workout_service_get_current_workout_hr_zone_time(hr_zone_time_s));
  cl_assert_equal_i(hr_zone_time_s[HRZone_Zone0], 10);
  cl_assert_equal_i(hr_zone_time_s[HRZone_Zone1], 20);
  cl_assert_equal_i(hr_zone_time_s[HRZone_Zone2], 10);
  cl_assert_equal_i(hr_zone_time_s[HRZone_Zone3], 10);

  // Unpause workout
  cl_assert(workout_service_pause_workout(false));
  prv_inc_time(10);
  prv_put_bpm_event(ZONE_3_HR, HRMQuality_Good);
  cl_assert(workout_service_get_current_workout_hr_zone_time(hr_zone_time_s));
  cl_assert_equal_i(hr_zone_time_s[HRZone_Zone0], 10);
  cl_assert_equal_i(hr_zone_time_s[HRZone_Zone1], 20);
  cl_assert_equal_i(hr_zone_time_s[HRZone_Zone2], 10);
  cl_assert_equal_i(hr_zone_time_s[HRZone_Zone3], 20);

  prv_inc_time(10);
  prv_put_bpm_event(ZONE_2_HR, HRMQuality_Good);
  cl_assert(workout_service_get_current_workout_hr_zone_time(hr_zone_time_s));
  cl_assert_equal_i(hr_zone_time_s[HRZone_Zone0], 10);
  cl_assert_equal_i(hr_zone_time_s[HRZone_Zone1], 20);
  cl_assert_equal_i(hr_zone_time_s[HRZone_Zone2], 20);
  cl_assert_equal_i(hr_zone_time_s[HRZone_Zone3], 20);

  cl_assert(!workout_service_get_current_workout_hr_zone_time(NULL));
  cl_assert(workout_service_stop_workout());
  cl_assert(!workout_service_get_current_workout_hr_zone_time(hr_zone_time_s));
}

// ---------------------------------------------------------------------------------------
void test_workout_service__avg_hr(void) {
  int32_t avg_hr;
  cl_assert(!workout_service_get_avg_hr(&avg_hr));

  cl_assert(workout_service_start_workout(ActivitySessionType_Run));

  prv_put_bpm_event(140, HRMQuality_Good);
  prv_put_bpm_event(140, HRMQuality_Good);
  cl_assert(workout_service_get_avg_hr(&avg_hr));
  cl_assert_equal_i(avg_hr, 140);

  prv_put_bpm_event(160, HRMQuality_Good);
  prv_put_bpm_event(160, HRMQuality_Good);
  cl_assert(workout_service_get_avg_hr(&avg_hr));
  cl_assert_equal_i(avg_hr, 150);

  cl_assert(workout_service_pause_workout(true));

  prv_put_bpm_event(200, HRMQuality_Good);
  prv_put_bpm_event(200, HRMQuality_Good);
  cl_assert(workout_service_get_avg_hr(&avg_hr));
  cl_assert_equal_i(avg_hr, 150);

  cl_assert(workout_service_pause_workout(false));

  prv_put_bpm_event(180, HRMQuality_Good);
  prv_put_bpm_event(180, HRMQuality_Good);
  cl_assert(workout_service_get_avg_hr(&avg_hr));
  cl_assert_equal_i(avg_hr, 160);
}

// ---------------------------------------------------------------------------------------
// Open the app, start a workout, close app.
// 55 min - Make sure the notification was sent.
// 60 min - Make sure the workout was ended.
void test_workout_service__abandon_workout(void) {
  // Put some time into the clock
  prv_inc_time(1 * SECONDS_PER_MINUTE);

  // Open the app and wait a minute
  workout_service_frontend_opened();
  prv_inc_time(1 * SECONDS_PER_MINUTE);

  // Start workout and workout for 5 minutes
  cl_assert(workout_service_start_workout(ActivitySessionType_Run));
  prv_inc_time(5 * SECONDS_PER_MINUTE);

  // Close app and wait 30 minutes
  workout_service_frontend_closed();
  prv_inc_time(30 * SECONDS_PER_MINUTE);

  // Make sure notification is not sent yet and the workout is still ongoing
  cl_assert_equal_b(s_abandoned_workout_notification_sent, false);
  cl_assert_equal_b(workout_service_is_workout_ongoing(), true);

  // Wait 25 minutes, call evented timer callback and make sure the notification is sent
  prv_inc_time(25 * SECONDS_PER_MINUTE);
  prv_abandoned_notification_timer_callback(NULL);
  cl_assert_equal_b(s_abandoned_workout_notification_sent, true);

  // Wait 5 minutes, call evented timer callback and make sure the workout was ended
  prv_inc_time(5 * SECONDS_PER_MINUTE);
  prv_abandon_workout_timer_callback(NULL);
  cl_assert_equal_b(workout_service_is_workout_ongoing(), false);
}
