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

#include "services/normal/wakeup.h"
#include "syscall/syscall.h"
#include "flash_region/flash_region.h"
#include "services/normal/filesystem/pfs.h"
#include "services/normal/settings/settings_file.h"
#include "services/common/event_service.h"
#include "process_management/app_install_manager.h"

#include "clar.h"

// Fakes
//////////////////////////////////////////////////////////
#include "fake_app_manager.h"
#include "fake_rtc.h"
#include "fake_new_timer.h"
#include "fake_pbl_malloc.h"
#include "fake_spi_flash.h"
#include "fake_system_task.h"
#include "fake_time.h"

#include "stubs_analytics.h"
#include "stubs_events.h"
#include "stubs_language_ui.h"
#include "stubs_logging.h"
#include "stubs_print.h"
#include "stubs_prompt.h"
#include "stubs_serial.h"
#include "stubs_passert.h"
#include "stubs_pebble_process_md.h"
#include "stubs_rand_ptr.h"
#include "stubs_sleep.h"
#include "stubs_mutex.h"
#include "stubs_hexdump.h"
#include "stubs_task_watchdog.h"
#include "stubs_compiled_with_legacy2_sdk.h"
#include "stubs_memory_layout.h"

#define TEST_UUID UuidMake(0xF9, 0xC6, 0xEB, 0xE4, 0x06, 0xCD, 0x46, 0xF1, 0xB1, 0x51, 0x24, 0x08, 0x74, 0xD2, 0x07, 0x73)


// Stubs
////////////////////////////////////
//int g_pbl_log_level = 0;
//void pbl_log(uint8_t level, const char* src_filename, int src_line_number, const char* fmt, ...) {}
int time_util_get_num_hours(int hours, bool is24h) {return 0;}

bool sys_clock_is_24h_style(void) {return false;}

void event_service_init(PebbleEventType type, EventServiceAddSubscriberCallback start_cb,
                        EventServiceRemoveSubscriberCallback stop_cb) {}

static bool s_popup_occurred;
void wakeup_popup_window(uint8_t missed_apps_count, uint8_t *missed_apps_banks) {
  s_popup_occurred = true;
}

static PebbleProcessMd s_test_app_md = { .uuid = TEST_UUID };

bool clock_is_timezone_set(void) {
  return false;
}


// Tests
///////////////////////////////////////////////////////////
void test_wakeup__initialize(void) {
  // Wednesday (the 1st) at 00:00
  // date -d "2014/01/01 00:00:00" "+%s" ==> 1388563200
  fake_rtc_init(0, 1388563200);

  // Init fake filesystem used to load/store wakeup events
  fake_spi_flash_init(0, 0x1000000); //from test_settings_file.c
  pfs_init(false);

  stub_pebble_tasks_set_current(PebbleTask_KernelBackground);

  // Reset variable due to previous callbacks
  s_popup_occurred = false;

  wakeup_init();
  wakeup_enable(true);
}

void test_wakeup__cleanup(void) {}

void test_wakeup__basic_checks(void) {
  WakeupId wakeup_id = 0;
  cl_assert_equal_i(sys_get_time(), 1388563200);

  sys_wakeup_cancel_all_for_app();

  // Schedule a wakeup in 10 seconds
  wakeup_id = sys_wakeup_schedule(sys_get_time() + 10, 0, false);
  cl_assert_equal_i(sys_wakeup_query(wakeup_id), sys_get_time() + 10);

  // Cancel wakeup event
  sys_wakeup_delete(wakeup_id);
  cl_assert_equal_i(sys_wakeup_query(wakeup_id), E_DOES_NOT_EXIST);

  // Schedule again
  wakeup_id = sys_wakeup_schedule(sys_get_time() + 10, 0, false);
  cl_assert_equal_i(sys_wakeup_query(wakeup_id), sys_get_time() + 10);

  // Cancel all wakeup events
  sys_wakeup_cancel_all_for_app();
  cl_assert_equal_i(sys_wakeup_query(wakeup_id), E_DOES_NOT_EXIST);
}

void test_wakeup__max_events(void) {
  WakeupId wakeup_id = 0;
  sys_wakeup_cancel_all_for_app();

  // Schedule 8 (max), at 1 minute offsets, then fail on 9th
  for (int i = 1; i <= MAX_WAKEUP_EVENTS_PER_APP; i++) {
    wakeup_id = sys_wakeup_schedule(sys_get_time() + (i * WAKEUP_EVENT_WINDOW), 0, false);
    cl_assert_equal_i(sys_wakeup_query(wakeup_id), sys_get_time() + (i * WAKEUP_EVENT_WINDOW));
  }

  // Test that the 9th wakeup event fails to schedule (E_DOES_NOT_EXIST)
  wakeup_id = sys_wakeup_schedule(sys_get_time() + ((MAX_WAKEUP_EVENTS_PER_APP + 1) * WAKEUP_EVENT_WINDOW), 0, false);
  cl_assert_equal_i(sys_wakeup_query(wakeup_id), E_DOES_NOT_EXIST);
}

void test_wakeup__gap(void) {
  WakeupId wakeup_id = 0;
  sys_wakeup_cancel_all_for_app();

  // Schedule 1 event in a minute
  wakeup_id = sys_wakeup_schedule(sys_get_time() + WAKEUP_EVENT_WINDOW, 0, false);
  cl_assert_equal_i(sys_wakeup_query(wakeup_id), sys_get_time() + WAKEUP_EVENT_WINDOW);

  // Test that another event < 1 minute away fails to schedule (E_DOES_NOT_EXIST)
  wakeup_id = sys_wakeup_schedule(sys_get_time() + WAKEUP_EVENT_WINDOW + 59, 0, false);
  cl_assert_equal_i(sys_wakeup_query(wakeup_id), E_DOES_NOT_EXIST);

  // Test that another event < 1 minute away fails to schedule (E_DOES_NOT_EXIST)
  wakeup_id = sys_wakeup_schedule(sys_get_time() + 1, 0, false);
  cl_assert_equal_i(sys_wakeup_query(wakeup_id), E_DOES_NOT_EXIST);
}

// work around system_task_add_callback
extern void wakeup_dispatcher_system_task(void *data);

void test_wakeup__out_of_order_schedule(void) {
  const time_t start_time = sys_get_time();
  sys_wakeup_cancel_all_for_app();

  // Schedule a wakeup for 10 windows into the future
  time_t late_event = start_time + WAKEUP_EVENT_WINDOW * 10;
  WakeupId late_wakeup_id = sys_wakeup_schedule(late_event, 0, false);
  cl_assert_equal_i(sys_wakeup_query(late_wakeup_id), late_event);

  // Schedule a wakeup for 5 windows into the future
  time_t early_event = start_time + WAKEUP_EVENT_WINDOW * 5;
  WakeupId early_wakeup_id = sys_wakeup_schedule(early_event, 0, false);
  cl_assert_equal_i(sys_wakeup_query(early_wakeup_id), early_event);

  cl_assert_equal_i(early_wakeup_id, wakeup_get_next_scheduled());

  // Set time 5 minutes into the future, early_event should fire
  rtc_set_time(early_event);

  // Force wakeup to check for current wakeup event.
  wakeup_enable(false);
  wakeup_enable(true);

  // Simulate the firing of the early event
  stub_new_timer_fire(wakeup_get_current());
  wakeup_dispatcher_system_task((void *)(uintptr_t)early_wakeup_id);

  // Make sure early_wakeup_id not scheduled
  cl_assert_equal_i(sys_wakeup_query(early_wakeup_id), E_DOES_NOT_EXIST);
  cl_assert_equal_i(sys_wakeup_query(late_wakeup_id), late_event);

  // Make sure that the next scheduled timer is now the late wakeup id.
  cl_assert_equal_i(late_wakeup_id, wakeup_get_next_scheduled());

  // Set time 10 minutes into the future, late_event should fire
  rtc_set_time(late_event);

  // Force wakeup to check for current wakeup event.
  wakeup_enable(false);
  wakeup_enable(true);

  // Simulate the firing of the late event
  stub_new_timer_fire(wakeup_get_current());
  wakeup_dispatcher_system_task((void *)(uintptr_t)late_wakeup_id);

  // There should now be no scheduled wakeups
  cl_assert_equal_i(sys_wakeup_query(late_wakeup_id), E_DOES_NOT_EXIST);
}

void test_wakeup__time_jump(void) {
  sys_wakeup_cancel_all_for_app();

  // Schedule 1 event in a minute
  time_t first_event = sys_get_time() + WAKEUP_EVENT_WINDOW;
  WakeupId first_wakeup_id = sys_wakeup_schedule(first_event, 0, false);
  cl_assert_equal_i(sys_wakeup_query(first_wakeup_id), first_event);

  TimerID first_timer = wakeup_get_current();

  // Schedule another a minute away
  time_t second_event = sys_get_time() + WAKEUP_EVENT_WINDOW * 2;
  WakeupId second_wakeup_id = sys_wakeup_schedule(second_event, 0, false);
  cl_assert_equal_i(sys_wakeup_query(second_wakeup_id), second_event);

  TimerID test_timer = wakeup_get_current();

  // Wakeup should still return the first event as scheduled
  cl_assert_equal_i(first_timer, test_timer);


  // Schedule another in the future
  time_t third_event = sys_get_time() + WAKEUP_EVENT_WINDOW * 3;
  WakeupId third_wakeup_id = sys_wakeup_schedule(third_event, 0, false);
  cl_assert_equal_i(sys_wakeup_query(third_wakeup_id), third_event);

  // Schedule another in the future
  time_t fourth_event = sys_get_time() + WAKEUP_EVENT_WINDOW * 4;
  WakeupId fourth_wakeup_id = sys_wakeup_schedule(fourth_event, 0, false);
  cl_assert_equal_i(sys_wakeup_query(fourth_wakeup_id), fourth_event);

  // Jump to the future right before the 3rd event
  rtc_set_time(sys_get_time() + 170);

  // Force wakeup to check for current wakeup event
  wakeup_enable(false);
  wakeup_enable(true);

  // fire the first wakeup event, as it is still current
  stub_new_timer_fire(wakeup_get_current());
  wakeup_dispatcher_system_task((void *)(uintptr_t)first_wakeup_id);

  // The current timer should be the second event, even though it is in the past, and should
  // have a WAKEUP_CATCHUP_WINDOW second gap scheduled
  TimerID gap_timer = wakeup_get_current();
  cl_assert_equal_i(stub_new_timer_timeout(gap_timer) / 1000, WAKEUP_CATCHUP_WINDOW);

  stub_new_timer_fire(wakeup_get_current());
  wakeup_dispatcher_system_task((void *)(uintptr_t)second_wakeup_id);

  // The current timer should be the third event, with a WAKEUP_CATCHUP_WINDOW second gap again (catchup)
  gap_timer = wakeup_get_current();
  cl_assert_equal_i(stub_new_timer_timeout(gap_timer) / 1000, WAKEUP_CATCHUP_WINDOW);

  rtc_set_time(third_event); // manually move time forward to after third event
  stub_new_timer_fire(wakeup_get_current());
  wakeup_dispatcher_system_task((void *)(uintptr_t)third_wakeup_id);

  // Catchup should be finished, gap should be back to >= WAKEUP_CATCHUP_WINDOW seconds
  gap_timer = wakeup_get_current();

  cl_assert_equal_b((stub_new_timer_timeout(gap_timer) / 1000) > WAKEUP_CATCHUP_WINDOW, true);
}

void test_wakeup__handle_clock_change_not_scheduled(void) {
  // Test clock change without wakeup event scheduled
  wakeup_handle_clock_change();
  
  // Make sure no wakeup event is scheduled
  cl_assert_equal_i(sys_wakeup_query(wakeup_get_next_scheduled()), E_DOES_NOT_EXIST);
  // There should be no wakeup event missed or popup displayed
  cl_assert_equal_b(s_popup_occurred, false);
}

void test_wakeup__handle_clock_change_scheduled_jump(void) {
  // Schedule event timer 1 minute away with notifying on missed event
  time_t first_event = sys_get_time() + WAKEUP_EVENT_WINDOW;
  WakeupId first_wakeup_id = sys_wakeup_schedule(first_event, 0, true);
  cl_assert_equal_i(sys_wakeup_query(first_wakeup_id), first_event);
  
  TimerID first_timer = wakeup_get_current();
  
  // Jump 30 seconds in the future
  uint32_t initial_timeout = stub_new_timer_timeout(first_timer);
  uint32_t time_jump_seconds = 30;
  rtc_set_time(sys_get_time() + time_jump_seconds);

  // Notify clock change and record change in new timer
  wakeup_handle_clock_change();
  uint32_t final_timeout = stub_new_timer_timeout(first_timer);

  // Compare to expected value for new timer
  cl_assert_equal_i(final_timeout, initial_timeout - time_jump_seconds * 1000);
  // There should be no wakeup event missed or popup displayed
  cl_assert_equal_b(s_popup_occurred, false);

  // Jump the remainder plus an offset (missing the event)
  rtc_set_time(sys_get_time() + final_timeout / 1000 + time_jump_seconds);
  wakeup_handle_clock_change();

  // There should be a missed wakeup event and a popup displayed
  cl_assert_equal_b(s_popup_occurred, true);
  // Make sure the wakeup event is no longer scheduled
  cl_assert_equal_i(sys_wakeup_query(first_timer), E_DOES_NOT_EXIST);
}