/*
 * 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 "test_alarm_common.h"

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

#include "stubs_blob_db_sync.h"
#include "stubs_blob_db_sync_util.h"

static int s_rand = 0;

int rand(void) {
  // There are no odds
  return s_rand;
}

static ActivitySleepState s_sleep_state = ActivitySleepStateAwake;
static uint16_t s_sleep_state_seconds = 0;
static uint16_t s_last_vmc = 0;

bool activity_tracking_on(void) {
  return true;
}

bool activity_get_metric(ActivityMetric metric, uint32_t history_len, int32_t *history) {
  cl_assert_equal_i(history_len, 1);
  if (metric == ActivityMetricSleepState) {
    *history = s_sleep_state;
    return true;
  } else if (metric == ActivityMetricSleepStateSeconds) {
    *history = s_sleep_state_seconds;
    return true;
  } else if (metric == ActivityMetricLastVMC) {
    *history = s_last_vmc;
    return true;
  }

  cl_assert(false);
  return false;
}

///////////////////////////////////////////////////////////////////////////////////////////////////
//! Helper Functions

static void prv_set_time(time_t day, int hour, int minute) {
  s_current_day = day;
  s_current_hour = hour;
  s_current_minute = minute;
  rtc_set_time(day + prv_hours_and_minutes_to_seconds(hour, minute));
}

///////////////////////////////////////////////////////////////////////////////////////////////////
//! Setup

void test_alarm_smart__initialize(void) {
  s_num_timeline_adds = 0;
  s_num_timeline_removes = 0;
  s_num_alarm_events_put = 0;
  s_num_alarms_fired = 0;
  s_last_vmc = 0;
  s_rand = 0;

  // Setup time
  TimezoneInfo tz_info = {
    .tm_zone = "UTC",
  };
  time_util_update_timezone(&tz_info);
  rtc_set_timezone(&tz_info);

  // Default to Thursday
  prv_set_time(s_thursday, 0, 0);

  timeline_item_destroy(s_last_timeline_item_added);
  s_last_timeline_item_added = NULL;
  s_last_timeline_item_removed_uuid = (Uuid) {};

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

  cron_service_init();

  alarm_init();
  alarm_service_enable_alarms(true);
}

void test_alarm_smart__cleanup(void) {
  cron_service_deinit();
}

////////////////////////////////////////////////////////////////////////////////////////////////////
//! Smart alarms

#define SMART_ALARM_UPDATE_MIN (SMART_ALARM_SNOOZE_DELAY_S / SECONDS_PER_MINUTE)

void test_alarm_smart__trigger_30_min_early_awake(void) {
  AlarmId id;
  id = alarm_create(&(AlarmInfo) { .hour = 10, .minute = 30, .kind = ALARM_KIND_EVERYDAY, .is_smart = true });
  prv_assert_alarm_config(id, 10, 30, false, ALARM_KIND_EVERYDAY, s_every_day_schedule);
  cl_assert_equal_i(s_num_timeline_adds, 3);
  cl_assert_equal_i(s_num_timeline_removes, 0);

  // Set sleep status
  s_sleep_state = ActivitySleepStateAwake;
  s_sleep_state_seconds = 0;
  s_last_vmc = 0;

  time_t next_alarm_time;
  alarm_get_next_enabled_alarm(&next_alarm_time);
  cl_assert_equal_i(next_alarm_time,
                    s_current_day + 10 * SECONDS_PER_HOUR + 30 * SECONDS_PER_MINUTE);

  // Don't trigger too early
  prv_set_time(s_current_day, 9, 49);
  cron_service_wakeup();
  cl_assert_equal_i(s_num_alarms_fired, 0);
  cl_assert_equal_i(s_num_alarm_events_put, 0);

  // Trigger at the right time
  prv_set_time(s_current_day, 10, 0);
  cron_service_wakeup();
  cl_assert_equal_i(s_num_alarms_fired, 1);
  cl_assert_equal_i(s_num_alarm_events_put, 1);
  cl_assert_equal_i(s_num_timeline_adds, 6);
  cl_assert_equal_i(s_num_timeline_removes, 3);
  cl_assert_equal_i(s_last_timeline_item_added->header.timestamp, rtc_get_time());
}

void test_alarm_smart__trigger_30_min_early_vmc(void) {
  AlarmId id;
  id = alarm_create(&(AlarmInfo) { .hour = 10, .minute = 30, .kind = ALARM_KIND_EVERYDAY, .is_smart = true });
  prv_assert_alarm_config(id, 10, 30, false, ALARM_KIND_EVERYDAY, s_every_day_schedule);
  cl_assert_equal_i(s_num_timeline_adds, 3);
  cl_assert_equal_i(s_num_timeline_removes, 0);

  s_sleep_state = ActivitySleepStateLightSleep;
  s_last_vmc = 1;
  prv_set_time(s_current_day, 10, 0);
  cron_service_wakeup();
  cl_assert_equal_i(s_num_alarms_fired, 1);
  cl_assert_equal_i(s_num_alarm_events_put, 1);
  cl_assert_equal_i(s_last_timeline_item_added->header.timestamp, rtc_get_time());
}

void test_alarm_smart__dont_trigger_30_min_early_deep_sleep(void) {
  AlarmId id;
  id = alarm_create(&(AlarmInfo) { .hour = 10, .minute = 30, .kind = ALARM_KIND_EVERYDAY, .is_smart = true });
  prv_assert_alarm_config(id, 10, 30, false, ALARM_KIND_EVERYDAY, s_every_day_schedule);
  cl_assert_equal_i(s_num_timeline_adds, 3);
  cl_assert_equal_i(s_num_timeline_removes, 0);

  s_sleep_state = ActivitySleepStateRestfulSleep;
  s_sleep_state_seconds = 0;
  s_last_vmc = 0;
  prv_set_time(s_current_day, 10, 0);
  cron_service_wakeup();
  cl_assert_equal_i(s_num_alarms_fired, 1);
  cl_assert_equal_i(s_num_alarm_events_put, 0);
}

void test_alarm_smart__trigger_15_min_early_light_sleep(void) {
  AlarmId id;
  id = alarm_create(&(AlarmInfo) { .hour = 10, .minute = 30, .kind = ALARM_KIND_EVERYDAY, .is_smart = true });
  prv_assert_alarm_config(id, 10, 30, false, ALARM_KIND_EVERYDAY, s_every_day_schedule);
  cl_assert_equal_i(s_num_timeline_adds, 3);
  cl_assert_equal_i(s_num_timeline_removes, 0);

  // Begin light sleep
  s_sleep_state = ActivitySleepStateLightSleep;
  s_sleep_state_seconds = SMART_ALARM_MAX_LIGHT_SLEEP_S - 15 * SECONDS_PER_MINUTE;

  // Smart alarms are first triggered by cron at T-30min
  prv_set_time(s_current_day, 10, 0);
  cron_service_wakeup();
  cl_assert_equal_i(s_num_alarms_fired, 1);
  cl_assert_equal_i(s_num_alarm_events_put, 0);

  // Afterwards, the alarm snooze timer triggers every 5min
  const int num_checks = 3;
  for (int i = 0; i < num_checks; i++) {
    // Step forward time and increase light sleep duration
    s_sleep_state_seconds += 5 * SECONDS_PER_MINUTE;
    s_last_vmc = i == 2 ? 1 : 0;
    prv_set_time(s_current_day, 10, (i + 1) * 5);
    PBL_LOG(LOG_LEVEL_DEBUG, "Iteration #%d, sleep %d seconds", i, s_sleep_state_seconds);
    stub_new_timer_invoke(1);
    if (i < num_checks - 1) {
      // Smart alarm non-trigger checks
      cl_assert_equal_i(s_num_alarms_fired, 1);
      cl_assert_equal_i(s_num_alarm_events_put, 0);
    }
  }

  // Smart alarm trigger checks
  cl_assert_equal_i(s_num_alarms_fired, 1);
  cl_assert_equal_i(s_num_alarm_events_put, 1);
  cl_assert_equal_i(s_num_timeline_adds, 6);
  cl_assert_equal_i(s_num_timeline_removes, 3);
  cl_assert_equal_i(s_last_timeline_item_added->header.timestamp, rtc_get_time());
}

void test_alarm_smart__trigger_at_timeout(void) {
  AlarmId id;
  id = alarm_create(&(AlarmInfo) { .hour = 10, .minute = 30, .kind = ALARM_KIND_EVERYDAY, .is_smart = true });
  prv_assert_alarm_config(id, 10, 30, false, ALARM_KIND_EVERYDAY, s_every_day_schedule);
  cl_assert_equal_i(s_num_timeline_adds, 3);
  cl_assert_equal_i(s_num_timeline_removes, 0);

  // Stay in deep sleep
  s_sleep_state = ActivitySleepStateRestfulSleep;
  s_sleep_state_seconds = 0;

  // Make sure random snooze does not cause the smart alarm to go beyond the alarm time
  s_rand = 4;

  // Smart alarms are first triggered by cron at T-30min
  prv_set_time(s_current_day, 10, 0);
  cron_service_wakeup();
  cl_assert_equal_i(s_num_alarms_fired, 1);
  cl_assert_equal_i(s_num_alarm_events_put, 0);

  // Afterwards, the alarm snooze timer triggers every 5min
  const int num_checks = 6;
  for (int i = 0; i < num_checks; i++) {
    // Step forward time and increase light sleep duration
    s_sleep_state_seconds = (i + 1) * 5 * SECONDS_PER_MINUTE;
    s_last_vmc = (i == 5);
    prv_set_time(s_current_day, 10, i * 5);
    PBL_LOG(LOG_LEVEL_DEBUG, "Iteration #%d, sleep %d seconds", i, s_sleep_state_seconds);
    stub_new_timer_invoke(1);
    if (i < num_checks - 1) {
      // Smart alarm non-trigger checks
      cl_assert_equal_i(s_num_alarms_fired, 1);
      cl_assert_equal_i(s_num_alarm_events_put, 0);
    }
  }

  // Smart alarm trigger checks
  cl_assert_equal_i(s_num_alarms_fired, 1);
  cl_assert_equal_i(s_num_alarm_events_put, 1);
  cl_assert_equal_i(s_num_timeline_adds, 6);
  cl_assert_equal_i(s_num_timeline_removes, 3);
  cl_assert_equal_i(s_last_timeline_item_added->header.timestamp, rtc_get_time());
}

void test_alarm_smart__across_midnight_boundary(void) {
  prv_set_time(s_sunday, 22, 0);

  AlarmId id;
  bool monday_only[7] = {false, true, false, false, false, false, false};
  id = alarm_create(&(AlarmInfo) { .hour = 0, .minute = 15, .kind = ALARM_KIND_CUSTOM, .is_smart = true,
                                   .scheduled_days = &monday_only });
  prv_assert_alarm_config(id, 0, 15, false, ALARM_KIND_CUSTOM, monday_only);
  cl_assert_equal_i(s_num_timeline_adds, 1);
  cl_assert_equal_i(s_num_timeline_removes, 0);

  // Set sleep status
  s_sleep_state = ActivitySleepStateAwake;
  s_sleep_state_seconds = 0;

  // Don't trigger too early
  prv_set_time(s_sunday, 23, 44);
  cron_service_wakeup();
  cl_assert_equal_i(s_num_alarms_fired, 0);
  cl_assert_equal_i(s_num_alarm_events_put, 0);

  // Trigger at the right time
  prv_set_time(s_sunday, 23, 45);
  cron_service_wakeup();
  cl_assert_equal_i(s_num_alarms_fired, 1);
  cl_assert_equal_i(s_num_alarm_events_put, 1);
  cl_assert_equal_i(s_num_timeline_adds, 2);
  cl_assert_equal_i(s_num_timeline_removes, 1);
  cl_assert_equal_i(s_last_timeline_item_added->header.timestamp, rtc_get_time());
}