/*
 * 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 "applib/accel_service.h"
#include "applib/data_logging.h"
#include "drivers/ambient_light.h"
#include "drivers/rtc.h"
#include "services/common/regular_timer.h"
#include "services/common/battery/battery_state.h"
#include "services/normal/activity/activity.h"
#include "services/normal/activity/activity_private.h"
#include "services/normal/activity/activity_algorithm.h"
#include "services/normal/activity/kraepelin/activity_algorithm_kraepelin.h"
#include "services/normal/activity/kraepelin/kraepelin_algorithm.h"
#include "services/normal/data_logging/data_logging_service.h"
#include "services/normal/filesystem/pfs.h"
#include "services/normal/settings/settings_file.h"
#include "services/common/system_task.h"
#include "util/math.h"
#include "util/size.h"

#include <stdint.h>
#include <string.h>
#include <applib/health_service.h>
#include <services/normal/activity/kraepelin/activity_algorithm_kraepelin.h>

#include "clar.h"

// Stubs
#include "stubs_analytics.h"
#include "stubs_freertos.h"
#include "stubs_hexdump.h"
#include "stubs_hr_util.h"
#include "stubs_logging.h"
#include "stubs_mutex.h"
#include "stubs_passert.h"
#include "stubs_prompt.h"
#include "stubs_sleep.h"
#include "stubs_task_watchdog.h"

// Fakes
#include "fake_accel_service.h"
#include "fake_new_timer.h"
#include "fake_rtc.h"
#include "fake_spi_flash.h"
#include "fake_system_task.h"


#define ASSERT_EQUAL_I(i1,i2,file,line) \
        clar__assert_equal_i((i1),(i2),file,line,#i1 " != " #i2, 1)

// Globals
AccelSamplingRate s_sample_rate;

static bool s_dls_created;
static DataLoggingSession *s_dls_session = (DataLoggingSession *)1;

// Logged items
static bool s_capture_dls_records = true;
static int s_num_dls_records;
static AlgMinuteDLSRecord s_dls_records[100];

// Which step count to return from kalg_analyze_samples()
static uint16_t s_alg_next_steps;

// Which vmc and orientation to return from kalg_minute_stats
static uint16_t s_alg_next_vmc;
static uint8_t s_alg_next_orientation;
static uint8_t s_alg_next_light;
static bool s_alg_next_plugged_in;

static struct tm s_start_time_tm = {
  .tm_hour = 17,
  .tm_mday = 1,
  .tm_mon = 0,
  .tm_year = 115
};


// ============================================================================================
// Misc stubs
uint32_t ambient_light_get_light_level(void) {
  return s_alg_next_light << 4;
}

AmbientLightLevel ambient_light_level_to_enum(uint32_t light_level) {
  // Just return a predictable result to validate the unit tests
  return (light_level / ALG_RAW_LIGHT_SENSOR_DIVIDE_BY) % AMBIENT_LIGHT_LEVEL_ENUM_COUNT;
}

BatteryChargeState battery_get_charge_state(void) {
  BatteryChargeState state = {
    .charge_percent = 50,
    .is_charging = s_alg_next_plugged_in,
    .is_plugged = s_alg_next_plugged_in,
  };
  return state;
}

void kalg_enable_activity_tracking(KAlgState *kalg_state, bool enable) {}

bool activity_tracking_on(void) {
  return true;
}

// ------------------------------------------------------------------------------------
// Return true if the given activity type is a sleep activity
bool activity_sessions_prv_is_sleep_activity(ActivitySessionType activity_type) {
  switch (activity_type) {
    case ActivitySessionType_Sleep:
    case ActivitySessionType_RestfulSleep:
    case ActivitySessionType_Nap:
    case ActivitySessionType_RestfulNap:
      return true;
    case ActivitySessionType_Walk:
    case ActivitySessionType_Run:
    case ActivitySessionType_Open:
      return false;
    case ActivitySessionType_None:
    case ActivitySessionTypeCount:
      break;
  }
  WTF;
}

// ------------------------------------------------------------------------------------
uint16_t s_activity_sessions_count;
ActivitySession s_activity_sessions[ACTIVITY_MAX_ACTIVITY_SESSIONS_COUNT];
void activity_sessions_prv_add_activity_session(ActivitySession *session) {
  // If this is a duplicate activity, ignore it
  ActivitySession *stored_session = s_activity_sessions;
  for (uint16_t i = 0; i < s_activity_sessions_count; i++, stored_session++) {
    if ((session->type == stored_session->type)
        && (session->start_utc == stored_session->start_utc)) {
      return;
    }
  }

  // If no more room, fail
  if (s_activity_sessions_count >= ACTIVITY_MAX_ACTIVITY_SESSIONS_COUNT) {
    PBL_LOG(LOG_LEVEL_WARNING, "No more room for additional activities");
    return;
  }

  // Add this activity in
  s_activity_sessions[s_activity_sessions_count++] = *session;
}

// ------------------------------------------------------------------------------------
void activity_sessions_prv_delete_activity_session(ActivitySession *session) {
}


// =============================================================================================
// Data logging stubs
DataLoggingResult dls_log(DataLoggingSession *logging_session, const void *data,
                          uint32_t num_items) {
  if (!s_capture_dls_records) {
    return DATA_LOGGING_SUCCESS;
  }
  cl_assert(s_dls_created);
  cl_assert_equal_p(logging_session, s_dls_session);

  AlgMinuteDLSRecord *records = (AlgMinuteDLSRecord *)data;
  for (int i = 0; i < num_items; i++) {
    cl_assert(s_num_dls_records < ARRAY_LENGTH(s_dls_records));
    s_dls_records[s_num_dls_records++] = records[i];
  }

  return DATA_LOGGING_SUCCESS;
}

DataLoggingSession *dls_create(uint32_t tag, DataLoggingItemType item_type, uint16_t item_size,
                               bool buffered, bool resume, const Uuid *uuid) {
  s_dls_created = true;
  cl_assert_equal_i(item_size, sizeof(AlgMinuteDLSRecord));

  return s_dls_session;
}

void dls_send_all_sessions(void) {
}


// ============================================================================================
// Activity service stubs
// --------------------------------------------------------------------------------------------
// Values to return from activity_private_get_.*()
static uint32_t s_activity_next_distance_mm;
static uint32_t s_activity_next_active_calories;
static uint32_t s_activity_next_resting_calories;
static uint32_t s_activity_next_heart_rate_bpm;
static uint32_t s_activity_next_heart_rate_zone;
static uint32_t s_activity_next_heart_rate_heart_rate_total_weight_x100;

uint32_t activity_metrics_prv_get_steps(void) {
  return 0;
}


uint32_t activity_metrics_prv_get_distance_mm(void) {
  return s_activity_next_distance_mm;
}


// --------------------------------------------------------------------------------------------
uint32_t activity_metrics_prv_get_resting_calories(void) {
  return s_activity_next_resting_calories;
}


// --------------------------------------------------------------------------------------------
uint32_t activity_metrics_prv_get_active_calories(void) {
  return s_activity_next_active_calories;
}

HRZone activity_metrics_prv_get_hr_zone(void) {
  return s_activity_next_heart_rate_zone;
}

void activity_metrics_prv_get_median_hr_bpm(int32_t *median, int32_t *total_weight) {
  if (median) {
    *median = s_activity_next_heart_rate_bpm;
  }
  if (total_weight) {
    *total_weight = s_activity_next_heart_rate_heart_rate_total_weight_x100;
  }

}

void activity_metrics_prv_reset_hr_stats(void) {
  s_activity_next_heart_rate_bpm = 0;
  s_activity_next_heart_rate_zone = 0;
}


// =============================================================================================
// Algorithm stubs
uint32_t kalg_state_size(void) {
  return 1;
}

bool kalg_init(KAlgState *state, KAlgStatsCallback stats_cb) {
  return true;
}

uint32_t kalg_analyze_samples(KAlgState *state, AccelRawData *data, uint32_t num_samples,
                              uint32_t *consumed_samples) {
  *consumed_samples = 0;
  return s_alg_next_steps;
}

void kalg_minute_stats(KAlgState *state, uint16_t *vmc, uint8_t *orientation, bool *still) {
  *vmc = s_alg_next_vmc;
  *orientation = s_alg_next_orientation;
  *still = false;
}

void kalg_set_weight(KAlgState *state, uint32_t grams) {
}

void kalg_activities_update(KAlgState *state, time_t utc_now, uint16_t steps, uint16_t vmc,
                            uint8_t orientation, bool plugged_in, uint32_t resting_calories,
                            uint32_t active_calories, uint32_t distance_mm, bool shutting_down,
                            KAlgActivitySessionCallback sessions_cb, void *context) {
}

time_t kalg_activity_last_processed_time(KAlgState *state, KAlgActivityType activity) {
  return rtc_get_time();
}

// Set these to simulate a sleep session (that should result in zeroing out any steps taken)
static time_t s_kalg_sleep_start_utc;
static uint16_t s_kalg_sleep_m;

void kalg_get_sleep_stats(KAlgState *alg_state, KAlgOngoingSleepStats *stats) {
  time_t now = rtc_get_time();
  if (s_kalg_sleep_start_utc == 0 || now < s_kalg_sleep_start_utc + SECONDS_PER_HOUR) {
    // We are before the requested sleep time
    *stats = (KAlgOngoingSleepStats) { };
  } else {
    // We are somewhere after the start of sleep
    time_t sleep_end = s_kalg_sleep_start_utc + s_kalg_sleep_m * SECONDS_PER_MINUTE;
    if (now < sleep_end + KALG_MAX_UNCERTAIN_SLEEP_M) {
      // Still haven't detected the end of sleep, the last KALG_MAX_UNCERTAIN_SLEEP_M minutes are
      // uncertain
      *stats = (KAlgOngoingSleepStats) {
        .sleep_start_utc = s_kalg_sleep_start_utc,
        .sleep_len_m = (now - s_kalg_sleep_start_utc) / SECONDS_PER_MINUTE
                       - KALG_MAX_UNCERTAIN_SLEEP_M,
        .uncertain_start_utc = now - KALG_MAX_UNCERTAIN_SLEEP_M * SECONDS_PER_MINUTE,
      };
    } else {
      // The sleep was in the past and has ended
      *stats = (KAlgOngoingSleepStats) {
        .sleep_start_utc = s_kalg_sleep_start_utc,
        .sleep_len_m = s_kalg_sleep_m,
        .uncertain_start_utc = 0,
      };
    }
  }
}


// --------------------------------------------------------------------------------------------
// Create sample data for testing sleep and data logging
static void prv_create_test_data(uint32_t num_minutes, AlgMinuteDLSSample *minute_data) {
  uint16_t next_vmc = 0;
  uint8_t next_orient = 1;
  uint8_t next_light = 2;
  uint16_t next_active_calories = 3;
  uint16_t next_resting_calories = 4;
  uint16_t next_distance_cm = 5;
  uint8_t next_heart_rate_bpm = 6;
  uint8_t next_heart_rate_heart_rate_total_weight_x100 = 7;
  uint8_t next_heart_rate_zone = 8;

  bool next_plugged_in = false;
  for (int i = 0; i < num_minutes; i++) {
    minute_data[i] = (AlgMinuteDLSSample) { };
    minute_data[i].base.steps = i;
    minute_data[i].base.vmc = next_vmc++;
    if (next_vmc == 65533) {
      // Make sure combinations of vmc/orient are mostly unique, so don't wrap at the same
      // module 256 boundary.
      next_vmc = 0;
    }
    minute_data[i].base.orientation = next_orient++;
    minute_data[i].base.light = next_light++;
    minute_data[i].base.plugged_in = next_plugged_in;
    next_plugged_in = !next_plugged_in;

    minute_data[i].active_calories = next_active_calories++;
    minute_data[i].resting_calories = next_resting_calories++;
    minute_data[i].distance_cm = next_distance_cm++;
    minute_data[i].base.active = (minute_data[i].base.steps >= ACTIVITY_ACTIVE_MINUTE_MIN_STEPS)
                                 ? 1 : 0;
    minute_data[i].heart_rate_bpm = next_heart_rate_bpm++;
    minute_data[i].heart_rate_total_weight_x100 = next_heart_rate_heart_rate_total_weight_x100++;
    minute_data[i].heart_rate_zone = next_heart_rate_zone++;
  }
}


// --------------------------------------------------------------------------------------------
// Feed in sleep data
static void prv_feed_minute_data(uint32_t num_minutes, AlgMinuteDLSSample *minute_data,
                                 bool simulate_bg_delays) {
  // Call the minute handler, which computes the minute stats and saves them to data logging
  // as well as the sleep PFS file.
  for (int i = 0; i < num_minutes; i++) {
    fake_rtc_increment_time(SECONDS_PER_MINUTE);
    s_alg_next_steps = minute_data[i].base.steps;
    AccelRawData samples[100] = { };
    uint64_t timestamp = 0;
    // Calling activity_algorithm_handle_accel() on our stub algorithm gives it the step
    // counts for this minute
    activity_algorithm_handle_accel(samples, s_sample_rate, timestamp);

    // Are we simulating delays in KernelBG processing?
    int delay = 0;
    if (simulate_bg_delays) {
      delay = (((i / ALG_MINUTES_PER_FILE_RECORD) + 1) % 30);
      rtc_set_time(rtc_get_time() + delay);
    }
    s_alg_next_vmc = minute_data[i].base.vmc;
    s_alg_next_orientation = minute_data[i].base.orientation;
    s_alg_next_light = minute_data[i].base.light;
    s_alg_next_plugged_in = minute_data[i].base.plugged_in;
    s_activity_next_distance_mm += minute_data[i].distance_cm * 10;
    s_activity_next_resting_calories += minute_data[i].resting_calories;
    s_activity_next_active_calories += minute_data[i].active_calories;
    s_activity_next_heart_rate_bpm = minute_data[i].heart_rate_bpm;
    s_activity_next_heart_rate_heart_rate_total_weight_x100 = minute_data[i].heart_rate_total_weight_x100;
    s_activity_next_heart_rate_zone = minute_data[i].heart_rate_zone;
    AlgMinuteRecord minute_record = {};
    activity_algorithm_minute_handler(rtc_get_time(), &minute_record);
    if (simulate_bg_delays) {
      rtc_set_time(rtc_get_time() - delay);
    }
  }
}


// =============================================================================================
// Start of unit tests
void test_activity_algorithm_kraepelin__initialize(void) {
  time_t utc_sec = mktime(&s_start_time_tm);
  fake_rtc_init(100 /*initial_ticks*/, utc_sec);

  fake_spi_flash_init(0, 0x1000000);
  pfs_init(false);
  pfs_format(false);

  // Init the algorithm
  s_activity_next_resting_calories = 0;
  s_activity_next_distance_mm = 0;
  s_activity_next_active_calories = 0;
  s_activity_next_heart_rate_bpm = 0;
  s_activity_next_heart_rate_zone = 0;
  s_kalg_sleep_start_utc = 0;
  s_kalg_sleep_m = 0;
  activity_algorithm_init(&s_sample_rate);
}


// ---------------------------------------------------------------------------------------
void test_activity_algorithm_kraepelin__cleanup(void) {
  fake_system_task_callbacks_invoke_pending();
  activity_algorithm_deinit();
}


// ---------------------------------------------------------------------------------------
// Test to make sure that the minute data gets sent to data logging correctly
void test_activity_algorithm_kraepelin__data_logging_test(void) {
  const int k_num_records = 2;
  const int num_minutes = k_num_records * ALG_MINUTES_PER_DLS_RECORD;

  // The test data
  AlgMinuteDLSSample minute_data[num_minutes];
  prv_create_test_data(num_minutes, minute_data);

  // Call the minute handler, which computes the minute stats and saves them to data logging
  // as well as the minute data settings file.
  prv_feed_minute_data(num_minutes, minute_data, false /*simulate_bg_delays*/);

  // Make sure the correct data got saved to data logging
  cl_assert_equal_i(s_num_dls_records, 2);
  for (int j = 0; j < k_num_records; j++) {
    cl_assert_equal_i(s_dls_records[j].hdr.version, ALG_DLS_MINUTES_RECORD_VERSION);
    for (int i = 0; i < ALG_MINUTES_PER_DLS_RECORD; i++) {
      cl_assert_equal_m(&s_dls_records[j].samples[i],
                        &minute_data[(j * ALG_MINUTES_PER_DLS_RECORD) + i],
                        sizeof(minute_data[i]));
    }
  }
}


// ------------------------------------------------------------------------------------
static void prv_assert_minute_data(HealthMinuteData *actual, AlgMinuteDLSSample *expected) {
  cl_assert_equal_i(actual->steps, expected->base.steps);
  cl_assert_equal_i(actual->orientation, expected->base.orientation);
  cl_assert_equal_i(actual->vmc, expected->base.vmc);
  cl_assert_equal_i(actual->light, ambient_light_level_to_enum(
    expected->base.light * ALG_RAW_LIGHT_SENSOR_DIVIDE_BY));
  cl_assert_equal_i(actual->heart_rate_bpm, expected->heart_rate_bpm);

}

// ---------------------------------------------------------------------------------------
// Test to make sure that when we re-boot we correctly get the saved minute data
void test_activity_algorithm_kraepelin__minute_data_after_boot(void) {
  const int num_minutes = 4 * MINUTES_PER_HOUR;
  time_t start_utc = rtc_get_time();

  // The test data
  AlgMinuteDLSSample minute_data[num_minutes];
  prv_create_test_data(num_minutes, minute_data);

  // Write first half of the data
  prv_feed_minute_data(num_minutes / 2, minute_data, false /*simulate_bg_delays*/);

  // Now, simulate a reboot, re-initialize of the algorithm. This will trigger a re-read
  // of the sleep data file
  activity_algorithm_deinit();
  activity_algorithm_init(&s_sample_rate);

  // Write the rest of the data
  int start_idx = num_minutes / 2;
  prv_feed_minute_data(num_minutes / 2, minute_data + start_idx, false /*simulate_bg_delays*/);

  // Retrieve all the minute data and verify the contents
  HealthMinuteData retrieve[num_minutes];
  uint32_t num_records = num_minutes;
  time_t start = start_utc;
  activity_algorithm_get_minute_history(retrieve, &num_records, &start);
  cl_assert_equal_i(num_records, num_minutes);
  cl_assert_equal_i(start, start_utc);
  for (int i = 0; i < num_minutes; i++) {
    prv_assert_minute_data(&retrieve[i], &minute_data[i]);
  }
}


// ---------------------------------------------------------------------------------------
// Test to make sure that the minute data file gets compacted correctly. If we write more than
// ALG_MINUTE_DATA_FILE_LEN worth of data to the sleep file, it's size should be capped at
// ALG_MINUTE_DATA_FILE_LEN and we should be able to successfully read back the most recent
// data we wrote.
void test_activity_algorithm_kraepelin__sleep_data_compaction_test(void) {
  const int num_minutes = ALG_SLEEP_HISTORY_HOURS_FOR_TODAY * MINUTES_PER_HOUR;

  // The test data
  AlgMinuteDLSSample minute_data[num_minutes];
  prv_create_test_data(num_minutes, minute_data);

  // Fill with garbage for more than ALG_MINUTE_DATA_FILE_LEN to force us to
  // chop off old data.
  s_capture_dls_records = false;
  uint32_t max_minutes = ALG_MINUTE_DATA_FILE_LEN * 3 / 2 / sizeof(AlgMinuteFileSample);
  // Make sure it's a multiple of ALG_MINUTES_PER_RECORD
  max_minutes = (max_minutes / ALG_MINUTES_PER_FILE_RECORD) * ALG_MINUTES_PER_FILE_RECORD;
  for (int i = 0; i < max_minutes; i++) {
    fake_rtc_increment_time(SECONDS_PER_MINUTE);
    s_alg_next_steps = 0x1234;
    AccelRawData samples[100] = { };
    uint64_t timestamp = 0;
    activity_algorithm_handle_accel(samples, s_sample_rate, timestamp);

    s_alg_next_vmc = 0x11;
    s_alg_next_orientation = 0x22;
    AlgMinuteRecord minute_record = {};
    activity_algorithm_minute_handler(rtc_get_time(), &minute_record);
  }

  // Get the size of the sleep data and make sure it is within the expected range
  uint32_t num_records;
  uint32_t data_bytes;
  uint32_t minutes;
  bool success = activity_algorithm_minute_file_info(false /*compact_first*/, &num_records,
                                                     &data_bytes, &minutes);
  cl_assert(success);
  cl_assert(data_bytes < ALG_MINUTE_DATA_FILE_LEN && data_bytes > ALG_MINUTE_DATA_FILE_LEN / 2);
  cl_assert(minutes < ALG_MINUTE_FILE_MAX_ENTRIES * ALG_MINUTES_PER_FILE_RECORD);
  cl_assert(minutes > ALG_MINUTE_FILE_MAX_ENTRIES * ALG_MINUTES_PER_FILE_RECORD / 2);


  // Now, put in our expected data
  // Call the minute handler, which computes the minute stats and saves them to data logging
  // as well as the sleep PFS file.
  time_t start_of_data_utc = rtc_get_time();
  prv_feed_minute_data(num_minutes, minute_data, false /*simulate_bg_delays*/);

  // Retrieve the minute data now
  HealthMinuteData retrieve[num_minutes];
  num_records = num_minutes;
  time_t start = start_of_data_utc;  // starting just past the minute to test that &start gets updated
  activity_algorithm_get_minute_history(retrieve, &num_records, &start);
  cl_assert_equal_i(num_records, num_minutes);
  cl_assert_equal_i(start, start_of_data_utc);
  for (int i = 0; i < num_minutes; i++) {
    prv_assert_minute_data(&retrieve[i], &minute_data[i]);
  }
}


// ---------------------------------------------------------------------------------------
// Test that the call to retrieve minute history from flash works correctly
void test_activity_algorithm_kraepelin__get_flash_minute_history(void) {
  const int num_minutes = 4 * MINUTES_PER_HOUR;
  time_t start_utc = rtc_get_time();

  // Let's start time not on a 15 minute boundary to aggravate the get_minute logic
  start_utc += 7 * SECONDS_PER_MINUTE;
  rtc_set_time(start_utc);

  // The test data
  AlgMinuteDLSSample minute_data[num_minutes];
  prv_create_test_data(num_minutes, minute_data);

  // Call the minute handler, which computes the minute stats and saves them to data logging
  // as well as to the sleep PFS file.
  prv_feed_minute_data(num_minutes, minute_data, false /*simulate_bg_delays*/);

  // Retrieve all of the minute data at once
  HealthMinuteData retrieve[num_minutes * 2];
  uint32_t num_records = num_minutes;
  time_t start = start_utc + 5;  // starting just past the minute to test that &start gets updated
  activity_algorithm_get_minute_history(retrieve, &num_records, &start);
  cl_assert_equal_i(num_records, num_minutes);
  cl_assert_equal_i(start, start_utc);
  for (int i = 0; i < num_minutes; i++) {
    prv_assert_minute_data(&retrieve[i], &minute_data[i]);
  }

  // Retrieve, trying to start from a lot farther back, it should return the UTC of the first
  // record available. Also ask for more than what is available
  num_records = num_minutes * 2;
  start = start_utc - SECONDS_PER_DAY;
  activity_algorithm_get_minute_history(retrieve, &num_records, &start);
  cl_assert_equal_i(num_records, num_minutes);
  cl_assert_equal_i(start, start_utc);
  for (int i = 0; i < num_minutes; i++) {
    prv_assert_minute_data(&retrieve[i], &minute_data[i]);
  }

  // Retrieve a little (10 minutes) at a time
  int num_records_left = num_minutes;
  int num_records_found = 0;
  start = start_utc;
  while (num_records_left) {
    uint32_t chunk;
    chunk = MIN(10, num_records_left);
    time_t first_ts = start;
    activity_algorithm_get_minute_history(&retrieve[num_records_found], &chunk, &first_ts);
    cl_assert_equal_i(start, first_ts);
    num_records_left -= chunk;
    num_records_found += chunk;
    start += chunk * SECONDS_PER_MINUTE;
  }
  cl_assert_equal_i(num_records_found, num_minutes);
  for (int i = 0; i < num_minutes; i++) {
    prv_assert_minute_data(&retrieve[i], &minute_data[i]);
  }
}

// ---------------------------------------------------------------------------------------
// Test that retrieving the most recent minute history works correctly. This test insures that
// we correctly include the minute history that has not yet been saved to flash
void test_activity_algorithm_kraepelin__get_ram_minute_history(void) {
  const int num_minutes = 1 * MINUTES_PER_HOUR;
  time_t start_utc = rtc_get_time();

  // Let's start time not on a 15 minute boundary to aggravate the get_minute logic
  start_utc += 7 * SECONDS_PER_MINUTE;
  rtc_set_time(start_utc);

  // The test data
  AlgMinuteDLSSample minute_data[num_minutes];
  prv_create_test_data(num_minutes, minute_data);

  // Call the minute handler to feed in enough data to write to flash. This computes the minute
  // stats and saves them to data logging as well as to the sleep PFS file.
  prv_feed_minute_data(ALG_MINUTES_PER_FILE_RECORD, minute_data, false /*simulate_bg_delays*/);
  uint32_t next_write_minute_idx = ALG_MINUTES_PER_FILE_RECORD;

  // Once a minute, retrieve the last ALG_MINUTES_PER_RECORD minutes of data. We should
  // get ALG_MINUTES_PER_RECORD records each time. We know that the activity algorithm code only
  // writes a new minute data record to flash once every ALG_MINUTES_PER_RECORD minutes, but
  // the records that are not yet saved to flash should be correctly retrieved from RAM.
  time_t oldest_to_fetch = rtc_get_time() - (ALG_MINUTES_PER_FILE_RECORD * SECONDS_PER_MINUTE);
  uint32_t next_read_minute_idx = 0;
  for (int i = 0; i < ALG_MINUTES_PER_FILE_RECORD; i++, oldest_to_fetch += SECONDS_PER_MINUTE,
    next_read_minute_idx++, next_write_minute_idx++) {

    // Ask for the last ALG_MINUTES_PER_RECORD minutes of data
    uint32_t num_records = ALG_MINUTES_PER_FILE_RECORD;
    time_t start = oldest_to_fetch;
    HealthMinuteData received_records[ALG_MINUTES_PER_FILE_RECORD];
    activity_algorithm_get_minute_history(received_records, &num_records, &start);

    cl_assert_equal_i(num_records, ALG_MINUTES_PER_FILE_RECORD);
    cl_assert_equal_i(start, oldest_to_fetch);

    printf("\nReceived %d minute records", (int)num_records);
    for (int j = 0; j < num_records; j++) {
      printf("\nRecord:%d, steps: %d", j, (int)received_records[j].steps);
    }

    // Verify the contents of the records
    for (int j = 0; j < num_records; j++) {
      prv_assert_minute_data(&received_records[j], &minute_data[next_read_minute_idx + j]);
    }

    // Advance another minute. It doesn't matter what data we feed in
    prv_feed_minute_data(1, minute_data + next_write_minute_idx, false /*simulate_bg_delays*/);
  }

  // Let's add data for a partial minute and make sure that gets returned
  const int exp_steps = 23;
  oldest_to_fetch = rtc_get_time() - SECONDS_PER_MINUTE;
  fake_rtc_increment_time(30); // 30 seconds
  s_alg_next_steps = exp_steps;
  AccelRawData samples[100] = { };
  uint64_t timestamp = 0;
  // Calling activity_algorithm_handle_accel() on our stub algorithm registers the new steps
  // counts for this minute
  activity_algorithm_handle_accel(samples, s_sample_rate, timestamp);

  // Fetch the last whole minute plus this partial minute
  time_t start = oldest_to_fetch;
  uint32_t num_records = 2;
  HealthMinuteData received_records[num_records];
  activity_algorithm_get_minute_history(received_records, &num_records, &start);
  cl_assert_equal_i(num_records, 2);
  cl_assert_equal_i(start, oldest_to_fetch);
  prv_assert_minute_data(&received_records[0],
                         &minute_data[next_read_minute_idx + ALG_MINUTES_PER_FILE_RECORD - 1]);
  cl_assert_equal_i(received_records[1].steps, exp_steps);
}


// ---------------------------------------------------------------------------------------
// Test the logic that detects naps. This logic is performed by the
// prv_sleep_sessions_post_process() method.
void test_activity_algorithm_kraepelin__sleep_post_process(void) {
  // NOTE: All tests by default start at 5pm. Let's advance time to 9pm to give us more
  // time to test the various nap scenarios
  time_t now_utc = rtc_get_time();
  now_utc += 4 * SECONDS_PER_HOUR;
  rtc_set_time(now_utc);
  time_t start_of_today = time_util_get_midnight_of(now_utc);

  { // Create a 2 hour session at 1pm ==> should be a nap
    ActivitySession sessions[] = {
      {
        .start_utc = start_of_today + (13 * SECONDS_PER_HOUR),  // 1pm
        .length_min = 2 * MINUTES_PER_HOUR,
        .type = ActivitySessionType_Sleep,
      },
      {
        .start_utc = start_of_today + (13 * SECONDS_PER_HOUR) + (15 * SECONDS_PER_MINUTE), // 1:15pm
        .length_min = 20,
        .type = ActivitySessionType_RestfulSleep,
      },
    };

    uint16_t session_entries = ARRAY_LENGTH(sessions);
    activity_algorithm_post_process_sleep_sessions(session_entries, sessions);
    cl_assert_equal_i(sessions[0].type, ActivitySessionType_Nap);
    cl_assert_equal_i(sessions[1].type, ActivitySessionType_RestfulNap);
  }

  { // Create a 4 hour session at 1pm ==> should be regular sleep
    ActivitySession sessions[] = {
      {
        .start_utc = start_of_today + (13 * (SECONDS_PER_HOUR)),  // 1pm
        .length_min = 4 * MINUTES_PER_HOUR,
        .type = ActivitySessionType_Sleep,
      },
    };

    uint16_t session_entries = ARRAY_LENGTH(sessions);
    activity_algorithm_post_process_sleep_sessions(session_entries, sessions);
    cl_assert_equal_i(sessions[0].type, ActivitySessionType_Sleep);
  }

  { // Create two 2 hour sessions, they should both be considered as separate naps
    ActivitySession sessions[] = {
      {
        .start_utc = start_of_today + (13 * SECONDS_PER_HOUR),  // 1pm
        .length_min = 2 * MINUTES_PER_HOUR,
        .type = ActivitySessionType_Sleep,
      },
      {
        .start_utc = start_of_today + (17 * SECONDS_PER_HOUR), // 5pm
        .length_min = 2 * MINUTES_PER_HOUR,
        .type = ActivitySessionType_Sleep,
      },
    };

    uint16_t session_entries = ARRAY_LENGTH(sessions);
    activity_algorithm_post_process_sleep_sessions(session_entries, sessions);
    cl_assert_equal_i(sessions[0].type, ActivitySessionType_Nap);
    cl_assert_equal_i(sessions[1].type, ActivitySessionType_Nap);
  }

  { // Create a 2 hour session that ends after 9pm ==> should be regular sleep
    ActivitySession sessions[] = {
      {
        .start_utc = start_of_today + (20 * SECONDS_PER_HOUR),  // 8pm
        .length_min = 2 * MINUTES_PER_HOUR,
        .type = ActivitySessionType_Sleep,
      },
    };

    uint16_t session_entries = ARRAY_LENGTH(sessions);
    activity_algorithm_post_process_sleep_sessions(session_entries, sessions);
    cl_assert_equal_i(sessions[0].type, ActivitySessionType_Sleep);
  }

  { // Create a 2 hour session that starts before 12pm ==> should be regular sleep
    ActivitySession sessions[] = {
      {
        .start_utc = start_of_today + (11 * SECONDS_PER_HOUR),  // 11am
        .length_min = 2 * MINUTES_PER_HOUR,
        .type = ActivitySessionType_Sleep,
      },
    };

    uint16_t session_entries = ARRAY_LENGTH(sessions);
    activity_algorithm_post_process_sleep_sessions(session_entries, sessions);
    cl_assert_equal_i(sessions[0].type, ActivitySessionType_Sleep);
  }

  { // Create a 2 hour session that is still on-going - should register as normal sleep
    time_t sleep_start_utc = now_utc - (2 * SECONDS_PER_HOUR);
    ActivitySession sessions[] = {
      {
        .start_utc = sleep_start_utc,
        .length_min = 2 * MINUTES_PER_HOUR,
        .type = ActivitySessionType_Sleep,
        .ongoing = true,
      },
      {
        .start_utc = sleep_start_utc + (15 * SECONDS_PER_MINUTE),
        .length_min = 20,
        .type = ActivitySessionType_RestfulSleep,
        .ongoing = true,
      },
    };
    uint16_t session_entries = ARRAY_LENGTH(sessions);
    activity_algorithm_post_process_sleep_sessions(session_entries, sessions);
    cl_assert_equal_i(sessions[0].type, ActivitySessionType_Sleep);
    cl_assert_equal_i(sessions[1].type, ActivitySessionType_RestfulSleep);
  }

  { // Create a 2h 39m  session that starts at 11:59pm ==> should be regular sleep
    ActivitySession sessions[] = {
      {
        .start_utc = start_of_today - (1 * SECONDS_PER_MINUTE),  // 11:59pm
        .length_min = (2 * MINUTES_PER_HOUR) + 39,
        .type = ActivitySessionType_Sleep,
      },
    };

    uint16_t session_entries = ARRAY_LENGTH(sessions);
    activity_algorithm_post_process_sleep_sessions(session_entries, sessions);
    cl_assert_equal_i(sessions[0].type, ActivitySessionType_Sleep);
  }

}

// ---------------------------------------------------------------------------------------
// Test to make sure we don't get steps counted while sleeping
void test_activity_algorithm_kraepelin__steps_during_sleep(void) {
  const int num_minutes = 120;

  // The test data
  AlgMinuteDLSSample minute_data[num_minutes];
  prv_create_test_data(num_minutes, minute_data);

  // Zero out the first hour of data. The sleep algorithm takes an hour to figure out that
  // you are sleeping, so it has no chance of zeroing out all the steps in that first hour.
  for (int i = 0; i < 60; i++) {
    minute_data[i].base.steps = 0;
  }

  // -------------------------------------------------------------------------------
  // Set to not sleeping
  s_kalg_sleep_start_utc = 0;
  s_kalg_sleep_m = 0;

  activity_algorithm_metrics_changed_notification();
  uint16_t steps_awake_60m;
  uint16_t steps_awake_100m;
  uint16_t steps_awake_120m;

  // Call the minute handler, which should zero out steps that occur while sleeping
  prv_feed_minute_data(60, &minute_data[0], false /*simulate_bg_delays*/);
  activity_algorithm_get_steps(&steps_awake_60m);

  prv_feed_minute_data(40, &minute_data[60], false /*simulate_bg_delays*/);
  activity_algorithm_get_steps(&steps_awake_100m);

  prv_feed_minute_data(20, &minute_data[100], false /*simulate_bg_delays*/);
  activity_algorithm_get_steps(&steps_awake_120m);

  // We should get steps counted while not sleeping
  printf("\nWhile awake: ");
  printf("\n  Counted %d steps first 60m", steps_awake_60m);
  printf("\n  Counted %d steps next 40m", steps_awake_100m - steps_awake_60m);
  printf("\n  Counted %d steps last 20m", steps_awake_120m - steps_awake_100m);
  printf("\n  Total: %d\n", steps_awake_120m);

  // Compute the expected number of steps
  int exp_steps = 0;
  for (int i = 0; i < num_minutes; i++) {
    exp_steps += minute_data[i].base.steps;
  }
  cl_assert_equal_i(steps_awake_120m, exp_steps);


  // -------------------------------------------------------------------------------
  // Try again while sleeping
  time_t start_utc = rtc_get_time();

  // Set to sleeping for the first 100 minutes
  s_kalg_sleep_start_utc = start_utc;
  s_kalg_sleep_m = 100;

  activity_algorithm_metrics_changed_notification();
  uint16_t steps_asleep_60m;
  uint16_t steps_asleep_100m;
  uint16_t steps_asleep_120m;

  // Call the minute handler, which should zero out steps that occur while sleeping
  prv_feed_minute_data(60, &minute_data[0], false /*simulate_bg_delays*/);
  activity_algorithm_get_steps(&steps_asleep_60m);

  prv_feed_minute_data(40, &minute_data[60], false /*simulate_bg_delays*/);
  activity_algorithm_get_steps(&steps_asleep_100m);

  prv_feed_minute_data(20, &minute_data[100], false /*simulate_bg_delays*/);
  activity_algorithm_get_steps(&steps_asleep_120m);

  // We should get steps counted while not sleeping
  printf("\nWhile asleep in the first 100m: ");
  printf("\n  Counted %d steps first 60m", steps_asleep_60m);
  printf("\n  Counted %d steps next 40m", steps_asleep_100m - steps_asleep_60m);
  printf("\n  Counted %d steps last 20m", steps_asleep_120m - steps_asleep_100m);
  printf("\n  Total: %d\n", steps_asleep_120m);

  // We should only get the steps counted from the last 20 minutes after waking
  cl_assert_equal_i(steps_asleep_120m, steps_awake_120m - steps_awake_100m);
}


// ---------------------------------------------------------------------------------------
// Test to make sure that the minute data we save has no steps during sleep
void test_activity_algorithm_kraepelin__minute_data_steps_during_sleep(void) {
  const int num_minutes = 120;

  // The test data
  AlgMinuteDLSSample minute_data[num_minutes];
  prv_create_test_data(num_minutes, minute_data);

  // Zero out the first hour of data. The sleep algorithm takes an hour to figure out that
  // you are sleeping, so it has no chance of zeroing out all the steps in that first hour.
  for (int i = 0; i < 60; i++) {
    minute_data[i].base.steps = 0;
  }

  time_t start_utc = rtc_get_time();

  // Set to sleeping for the first 100 minutes
  s_kalg_sleep_start_utc = start_utc;
  s_kalg_sleep_m = 100;

  // Write the data out
  prv_feed_minute_data(num_minutes, minute_data, false /*simulate_bg_delays*/);

  // Retrieve all the minute data and verify the contents
  HealthMinuteData retrieve[num_minutes];
  uint32_t num_records = num_minutes;
  time_t start = start_utc;
  activity_algorithm_get_minute_history(retrieve, &num_records, &start);
  cl_assert_equal_i(num_records, num_minutes);
  cl_assert_equal_i(start, start_utc);
  for (int i = 0; i < num_minutes; i++) {
    // If this is during the sleep period, steps should be 0
    if (i < 100) {
      cl_assert_equal_i(retrieve[i].steps, 0);
    } else {
      cl_assert_equal_i(retrieve[i].steps, minute_data[i].base.steps);
    }
    cl_assert_equal_i(retrieve[i].orientation, minute_data[i].base.orientation);
    cl_assert_equal_i(retrieve[i].vmc, minute_data[i].base.vmc);
    cl_assert_equal_i(retrieve[i].light, ambient_light_level_to_enum(
      minute_data[i].base.light * ALG_RAW_LIGHT_SENSOR_DIVIDE_BY));
    cl_assert_equal_i(retrieve[i].heart_rate_bpm, minute_data[i].heart_rate_bpm);
  }
}