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

#include "applib/ui/app_window_stack.h"
#include "applib/ui/window.h"
#include "applib/ui/window_manager.h"
#include "applib/ui/window_stack.h"
#include "applib/ui/window_stack_private.h"
#include "kernel/ui/modals/modal_manager.h"

#include "applib/connection_service_private.h"
#include "applib/battery_state_service_private.h"
#include "applib/tick_timer_service_private.h"

#include "clar.h"

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

#include "stubs_accel_service.h"
#include "stubs_app_state.h"
#include "stubs_app_timer.h"
#include "stubs_ble_app_support.h"
#include "stubs_event_service_client.h"
#include "stubs_fonts.h"
#include "stubs_freertos.h"
#include "stubs_gbitmap.h"
#include "stubs_graphics.h"
#include "stubs_graphics_context.h"
#include "stubs_heap.h"
#include "stubs_logging.h"
#include "stubs_passert.h"
#include "stubs_persist.h"
#include "stubs_plugin_service.h"
#include "stubs_print.h"
#include "stubs_process_manager.h"
#include "stubs_prompt.h"
#include "stubs_queue.h"
#include "stubs_resources.h"
#include "stubs_syscalls.h"
#include "stubs_unobstructed_area.h"

// Fakes
////////////////////////////////////

#include "fake_events.h"
#include "fake_pbl_malloc.h"
#include "fake_pebble_tasks.h"
#include "fake_animation.h"

#include <stdlib.h>
#include <stdio.h>
#include <string.h>

// Static Variables
////////////////////////////////////

static int16_t s_load_count = 0;
static int16_t s_unload_count = 0;
static int16_t s_appear_count = 0;
static int16_t s_disappear_count = 0;

static Window *s_last_click_configured_window;

static bool s_app_idle = false;

// Overrides
////////////////////////////////////

void battery_state_service_state_init(BatteryStateServiceState *state) {
  return;
}

void connection_service_state_init(ConnectionServiceState *state) {
}

void tick_timer_service_state_init(TickTimerServiceState *state) {
  return;
}

void framebuffer_clear(FrameBuffer* f) {
  return;
}

void launcher_task_add_callback(void (*callback)(void *data), void *data) {
  callback(data);
}

void app_idle_timeout_pause(void) {
  s_app_idle = true;
}

void app_idle_timeout_resume(void) {
  s_app_idle = false;
}

bool app_install_id_from_app_db(AppInstallId id) {
  return false;
}

void framebuffer_dirty_all(FrameBuffer *f) {
  return;
}

void framebuffer_mark_dirty_rect(FrameBuffer *f, GRect rect) {
  return;
}

bool layer_is_status_bar_layer(Layer *layer) {
  return false;
}

void status_bar_layer_render(GContext *ctx, const GRect *bounds, void *config) {
  return;
}

GDrawState graphics_context_get_drawing_state(GContext* ctx) {
  GDrawState state;
  memset(&state, 0, sizeof(GDrawState));
  return state;
}

void graphics_context_set_drawing_state(GContext* ctx, GDrawState draw_state) {
  return;
}

bool compositor_is_animating(void) {
  return false;
}

void *compositor_modal_transition_to_modal_get(bool dest) {
  return NULL;
}

void compositor_modal_render_ready(void) {
}

void compositor_transition_cancel(void) {
}

bool sys_app_is_watchface(void) {
  return false;
}

void click_manager_init(ClickManager *click_manager) {
  return;
}

void click_manager_clear(ClickManager *click_manager) {
  return;
}

void click_manager_reset(ClickManager *click_manager) {
  return;
}

void watchface_reset_click_manager(void) {
  return;
}

Animation *window_transition_default_pop_create_animation(WindowTransitioningContext *context) {
  window_transition_context_disappear(context);
  window_transition_context_appear(context);
  return animation_create();
}

const WindowTransitionImplementation window_transition_default_pop_implementation = {
  .create_animation = window_transition_default_pop_create_animation,
};

const WindowTransitionImplementation *window_transition_get_default_pop_implementation() {
  return &window_transition_default_pop_implementation;
}

Animation *window_transition_default_push_create_animation(WindowTransitioningContext *context) {
  window_transition_context_disappear(context);
  window_transition_context_appear(context);
  return animation_create();
}

const WindowTransitionImplementation window_transition_default_push_implementation = {
  .create_animation = window_transition_default_push_create_animation,
};

const WindowTransitionImplementation *window_transition_get_default_push_implementation() {
  return &window_transition_default_push_implementation;
}

Animation *window_transition_none_create_animation(WindowTransitioningContext *context) {
  window_transition_context_disappear(context);
  window_transition_context_appear(context);
  return animation_create();
}

const WindowTransitionImplementation g_window_transition_none_implementation = {
  .create_animation = window_transition_none_create_animation,
};

void compositor_transition(const CompositorTransition *type) {
  Window *window = modal_manager_get_top_window();
  if (window) {
    GContext ctx;
    memset(&ctx, 0, sizeof(GContext));
    modal_manager_render(&ctx);
  }
}

void app_click_config_setup_with_window(ClickManager *click_manager, struct Window *window) {
  s_last_click_configured_window = window;
}

// Helpers
////////////////////////////////////
static int16_t prv_get_load_unload_count(void) {
  return s_load_count - s_unload_count;
}

static int16_t prv_get_appear_disappear_count(void) {
  return s_appear_count - s_disappear_count;
}

static void prv_reset_counts(void) {
  s_load_count = 0;
  s_unload_count = 0;
  s_appear_count = 0;
  s_disappear_count = 0;
}

static void prv_click_config_provider(void *context) {
  return;
}

static void prv_window_appear(Window *window) {
  cl_check(window);
  cl_assert_equal_i(window->on_screen, true);
  s_appear_count++;
  cl_check(s_appear_count >= 1);
}

static void prv_window_disappear(Window *window) {
  cl_check(window);
  cl_assert_equal_i(window->on_screen, false);
  s_disappear_count++;
  cl_check(s_appear_count >= 0);
}

static void prv_window_load(Window *window) {
  cl_check(window);
  cl_assert_equal_i(window->on_screen, true);
  s_load_count++;
}

static void prv_window_unload(Window *window) {
  cl_check(window);
  cl_assert_equal_i(window->on_screen, false);
  s_unload_count++;
  window_destroy(window);
}

static void prv_push_window_load(Window *window) {
  prv_window_load(window);
  Window *new_window = window_create();
  window_set_window_handlers(new_window, &(WindowHandlers){
    .load = prv_window_load,
    .unload = prv_window_unload
  });

  cl_check(window->parent_window_stack);
  cl_assert_equal_i(window->on_screen, true);
  cl_assert_equal_i(window->is_loaded, false);

  window_stack_push(window->parent_window_stack, new_window, true);
}

static void prv_pop_window_load(Window *window) {
  prv_window_load(window);

  cl_check(window->parent_window_stack);
  cl_assert_equal_i(window->on_screen, true);
  cl_assert_equal_i(window->is_loaded, false);

  window_stack_pop(window->parent_window_stack, true);
}

static void prv_push_window_unload(Window *window) {
  WindowStack *stack = window->parent_window_stack;

  prv_window_unload(window);

  Window *new_window = window_create();
  window_set_window_handlers(new_window, &(WindowHandlers){
    .load = prv_window_load,
    .unload = prv_window_unload,
    .appear = prv_window_appear
  });

  cl_check(stack);
  cl_check(new_window);

  window_stack_push(stack, new_window, true);
}

static void prv_pop_window_unload(Window *window) {
  window_stack_remove(window, true);
  prv_window_unload(window);
}

// Setup and Teardown
////////////////////////////////////

void test_window_stack__initialize(void) {
  s_last_click_configured_window = NULL;

  WindowStack *stack = app_state_get_window_stack();
  *stack = (WindowStack) {};

  modal_manager_reset();

  prv_reset_counts();

  stub_pebble_tasks_set_current(PebbleTask_KernelMain);
}

void test_window_stack__cleanup(void) {
  stub_pebble_tasks_set_current(PebbleTask_App);

  app_window_stack_pop_all(false);

  stub_pebble_tasks_set_current(PebbleTask_KernelMain);

  modal_manager_pop_all();

  fake_animation_cleanup();

  cl_assert_equal_i(fake_pbl_malloc_num_net_allocs(), 0);
}

// Tests
////////////////////////////////////

void test_window_stack__basic_app_push(void) {
  Window *window = window_create();

  stub_pebble_tasks_set_current(PebbleTask_App);

  cl_check(app_state_get_window_stack());
  cl_check(!app_state_get_window_stack()->list_head);

  app_window_stack_push(window, true);

  cl_assert_equal_i(app_window_stack_count(), 1);

  app_window_stack_pop(true);

  cl_assert_equal_i(app_window_stack_count(), 0);

  window_destroy(window);
}

void test_window_stack__basic_modal_push(void) {
  Window *window = window_create();
  WindowStack *window_stack =
      modal_manager_get_window_stack(ModalPriorityGeneric);

  cl_check(window_stack);
  cl_check(!window_stack->list_head);

  window_stack_push(window_stack, window, true);

  cl_assert_equal_i(window_stack_count(window_stack), 1);

  window_stack_pop(window_stack, true);

  cl_assert_equal_i(window_stack_count(window_stack), 0);

  window_destroy(window);
}

void test_window_stack__basic_window_pop(void) {
  Window *window1 = window_create();
  Window *window2 = window_create();

  // Switch to app state to push windows to the Application window stack
  stub_pebble_tasks_set_current(PebbleTask_App);

  cl_check(app_state_get_window_stack());
  cl_check(!app_state_get_window_stack()->list_head);

  app_window_stack_push(window1, true);

  cl_assert_equal_i(app_window_stack_count(), 1);
  cl_assert_equal_i(window1->on_screen, true);

  app_window_stack_push(window2, true);

  cl_assert_equal_i(app_window_stack_count(), 2);
  cl_assert_equal_i(window1->on_screen, false);
  cl_assert_equal_i(window2->on_screen, true);

  app_window_stack_pop(true);

  cl_assert_equal_i(window1->on_screen, true);
  cl_assert_equal_i(window2->on_screen, false);

  app_window_stack_pop(true);

  cl_assert_equal_i(window1->on_screen, false);

  window_destroy(window1);
  window_destroy(window2);
}

void test_window_stack__basic_window_pop_under(void) {
  Window *window1 = window_create();
  Window *window2 = window_create();

  // Switch to app state to push windows to the Application window stack
  stub_pebble_tasks_set_current(PebbleTask_App);

  cl_check(app_state_get_window_stack());
  cl_check(!app_state_get_window_stack()->list_head);

  app_window_stack_push(window1, true);

  cl_assert_equal_i(app_window_stack_count(), 1);
  cl_assert_equal_i(window1->on_screen, true);

  app_window_stack_push(window2, true);

  cl_assert_equal_i(app_window_stack_count(), 2);
  cl_assert_equal_i(window1->on_screen, false);
  cl_assert_equal_i(window2->on_screen, true);

  app_window_stack_remove(window1, false);

  cl_assert_equal_i(app_window_stack_count(), 1);
  cl_assert_equal_i(window2->on_screen, true);
  cl_assert_equal_i(window1->on_screen, false);

  app_window_stack_remove(window2, false);

  cl_assert_equal_i(app_window_stack_count(), 0);
  cl_assert_equal_i(window2->on_screen, false);
  cl_assert_equal_i(window1->on_screen, false);

  window_destroy(window1);
  window_destroy(window2);
}

void test_window_stack__pop_all(void) {
  WindowStack *stack = modal_manager_get_window_stack(ModalPriorityGeneric);
  Window *windows[3];

  for (uint8_t idx = 0; idx < 3; idx++) {
    windows[idx] = window_create();
  }

  window_stack_push(stack, windows[0], true);
  cl_assert_equal_i(window_stack_count(stack), 1);
  cl_assert_equal_i(windows[0]->on_screen, true);

  window_stack_push(stack, windows[1], true);
  cl_assert_equal_i(window_stack_count(stack), 2);
  cl_assert_equal_i(windows[0]->on_screen, false);
  cl_assert_equal_i(windows[1]->on_screen, true);

  window_stack_push(stack, windows[2], true);
  cl_assert_equal_i(window_stack_count(stack), 3);
  cl_assert_equal_i(windows[0]->on_screen, false);
  cl_assert_equal_i(windows[1]->on_screen, false);
  cl_assert_equal_i(windows[2]->on_screen, true);

  window_stack_pop_all(stack, true);

  cl_assert_equal_i(window_stack_count(stack), 0);
  cl_assert_equal_i(windows[0]->on_screen, false);
  cl_assert_equal_i(windows[1]->on_screen, false);
  cl_assert_equal_i(windows[2]->on_screen, false);

  for (uint8_t idx = 0; idx < 3; idx++) {
    window_destroy(windows[idx]);
  }
}

void test_window_stack__insert_next(void) {
  Window *window1 = window_create();
  Window *window2 = window_create();

  stub_pebble_tasks_set_current(PebbleTask_App);

  cl_check(app_state_get_window_stack());
  cl_check(!app_state_get_window_stack()->list_head);

  app_window_stack_push(window1, true);

  cl_assert_equal_i(app_window_stack_count(), 1);
  cl_assert_equal_i(window1->on_screen, true);

  app_window_stack_insert_next(window2);

  cl_assert_equal_i(app_window_stack_count(), 2);
  cl_assert_equal_i(window1->on_screen, true);
  cl_assert_equal_i(window2->on_screen, false);

  app_window_stack_pop(true);

  cl_assert_equal_i(app_window_stack_count(), 1);
  cl_assert_equal_i(window1->on_screen, false);
  cl_assert_equal_i(window2->on_screen, true);

  app_window_stack_pop(true);

  cl_assert_equal_i(app_window_stack_count(), 0);
  cl_assert_equal_i(window2->on_screen, false);

  window_destroy(window1);
  window_destroy(window2);
}

// Description:
// During the push of a window, we push another window in the load handler of
// the window being pushed.  This causes the loading window to disappaer from
// the screen (before it even appeared) and become subverted by the new window.
void test_window_stack__push_during_window_load(void) {
  Window *window = window_create();
  window_set_window_handlers(window, &(WindowHandlers){
    .load = prv_push_window_load,
    .unload = prv_window_unload,
    .appear = prv_window_appear,
    .disappear = prv_window_disappear
  });

  WindowStack *stack = app_state_get_window_stack();

  // Switch to the app state to push a window
  stub_pebble_tasks_set_current(PebbleTask_App);

  cl_check(stack);
  cl_assert_equal_i(window_stack_count(stack), 0);

  window_stack_push(stack, window, true);

  cl_assert_equal_i(window_stack_count(stack), 2);
  cl_assert_equal_i(prv_get_load_unload_count(), 2);
  cl_assert_equal_i(prv_get_appear_disappear_count(), 0);

  cl_check(((WindowStackItem *)stack->list_head)->window != window);

  window_stack_pop_all(stack, false);

  cl_assert_equal_i(window_stack_count(stack), 0);
  cl_assert_equal_i(prv_get_load_unload_count(), 0);
  cl_assert_equal_i(prv_get_appear_disappear_count(), 0);
}

// Description:
// This test ensures that when we push windows onto modal window stacks, that
// only the appropriate window is visible at a given time.
void test_window_stack__modal_priority(void) {
  Window *windows[NumModalPriorities];
  WindowStack *window_stacks[NumModalPriorities];
  ModalPriority idx = ModalPriorityInvalid;

  do {
    ++idx;
    windows[idx] = window_create();
    window_set_window_handlers(windows[idx], &(WindowHandlers) {
      .unload = prv_window_unload
    });
    window_stacks[idx] = modal_manager_get_window_stack(idx);
  } while (idx < NumModalPriorities - 1);

  idx = ModalPriorityInvalid;

  do {
    ++idx;
    window_stack_push(window_stacks[idx], windows[idx], false);

    cl_assert_equal_i(window_stack_count(window_stacks[idx]), 1);
    cl_assert_equal_i(windows[idx]->on_screen, true);

    // All windows below the current priority should now not be on the screen
    // as the modal has subverted them.
    ModalPriority sub_idx = idx;
    do {
      sub_idx--;
      if (sub_idx == ModalPriorityInvalid) {
        break;
      }
      cl_assert_equal_i(window_stack_count(window_stacks[sub_idx]), 1);
      cl_assert_equal_i(windows[sub_idx]->on_screen, false);
    } while (true);
  } while (idx < NumModalPriorities - 1);
}

void test_window_stack__modal_properties_transparent(void) {
  const int num_windows_per_stack = 2;
  Window *windows[NumModalPriorities][num_windows_per_stack];
  WindowStack *window_stacks[NumModalPriorities];

  for (ModalPriority idx = ModalPriorityMin; idx < NumModalPriorities; idx++) {
    window_stacks[idx] = modal_manager_get_window_stack(idx);
    for (int i = 0; i < num_windows_per_stack; i++) {
      windows[idx][i] = window_create();
    }
  }

  // The following checks use integer priorities to clearly indicate stack order
  // We check to make sure the first occurrence of integer values are less than NumModalPriorities

  // Test: No top window does not result in Exists
  // Test: No top window results in Transparent and Unfocused
  modal_manager_event_loop_upkeep();
  cl_assert_equal_i(modal_manager_get_properties(),
                    ModalProperty_Transparent | ModalProperty_Unfocused);

  // Add priority 0 (discreet) opaque window 0
  cl_assert(NumModalPriorities > 0);
  window_stack_push(window_stacks[0], windows[0][0], false);

  // A discreet window just went on-screen
  // Test: Discreet windows have no compositor transition
  modal_manager_event_loop_upkeep();
  cl_assert_equal_i(modal_manager_get_properties(),
                    ModalProperty_Exists | ModalProperty_RenderRequested);

  window_stack_remove(windows[0][0], false);

  // Add priority 2 opaque window 0
  cl_assert(NumModalPriorities > 2);
  window_stack_push(window_stacks[2], windows[2][0], false);

  // An opaque window just went on-screen
  // Test: A top window results in Exists
  // Test: One opaque top window removes Transparent and Unfocused
  modal_manager_event_loop_upkeep();
  cl_assert_equal_i(modal_manager_get_properties(),
                    ModalProperty_Exists | ModalProperty_CompositorTransitions |
                    ModalProperty_RenderRequested);
  cl_assert_equal_i(windows[2][0]->on_screen, true);
  cl_assert_equal_i(windows[2][0]->is_click_configured, true);
  cl_assert_equal_p(s_last_click_configured_window, windows[2][0]);

  // Add priority 2 transparent window 1
  window_set_transparent(windows[2][1], true);
  window_stack_push(window_stacks[2], windows[2][1], false);

  // A transparent window is now the top window
  // Test: Opaque windows that are not the top window have no affect on transparency
  // Test: One transparent top window results in Transparent
  modal_manager_event_loop_upkeep();
  cl_assert_equal_i(modal_manager_get_properties(),
                    ModalProperty_Exists | ModalProperty_CompositorTransitions |
                    ModalProperty_RenderRequested | ModalProperty_Transparent);
  // Checks are listed from top to bottom
  cl_assert_equal_i(windows[2][1]->on_screen, true);
  cl_assert_equal_i(windows[2][1]->is_click_configured, true);
  cl_assert_equal_i(windows[2][0]->on_screen, false);
  cl_assert_equal_i(windows[2][0]->is_click_configured, false);
  cl_assert_equal_p(s_last_click_configured_window, windows[2][1]);

  // Add priority 3 opaque window 0
  cl_assert(NumModalPriorities > 3);
  window_stack_push(window_stacks[3], windows[3][0], false);

  // An opaque top window of a different stack is now obstructing the transparent top window
  // Top here throughout means that it is the top window of the window stack it is in
  // Test: An opaque top window above a transparent top window removes Transparent
  //       i.e. A transparent top window below an opaque top window does not result in Transparent
  modal_manager_event_loop_upkeep();
  cl_assert_equal_i(modal_manager_get_properties(),
                    ModalProperty_Exists | ModalProperty_CompositorTransitions |
                    ModalProperty_RenderRequested);
  cl_assert_equal_i(windows[3][0]->on_screen, true);
  cl_assert_equal_i(windows[3][0]->is_click_configured, true);
  cl_assert_equal_i(windows[2][1]->on_screen, false);
  cl_assert_equal_i(windows[2][1]->is_click_configured, false);
  cl_assert_equal_p(s_last_click_configured_window, windows[3][0]);

  // Add priority 3 transparent window 1
  window_set_transparent(windows[3][1], true);
  window_stack_push(window_stacks[3], windows[3][1], false);

  // A transparent window is now the top window, and there is another transparent window below
  // Test: Multiple transparent top windows result in Transparent
  modal_manager_event_loop_upkeep();
  cl_assert_equal_i(modal_manager_get_properties(),
                    ModalProperty_Exists | ModalProperty_CompositorTransitions |
                    ModalProperty_RenderRequested | ModalProperty_Transparent);
  cl_assert_equal_i(windows[3][1]->on_screen, true);
  cl_assert_equal_i(windows[3][1]->is_click_configured, true);
  cl_assert_equal_i(windows[3][0]->on_screen, false);
  cl_assert_equal_i(windows[3][0]->is_click_configured, false);
  cl_assert_equal_i(windows[2][1]->on_screen, true);
  cl_assert_equal_i(windows[2][1]->is_click_configured, false);
  cl_assert_equal_p(s_last_click_configured_window, windows[3][1]);

  // Add priority 1 opaque window 0
  cl_assert(NumModalPriorities > 1);
  window_stack_push(window_stacks[1], windows[1][0], false);

  // An opaque top window is now below two transparent top windows
  // Test: An opaque top window below a transparent top window removes Transparent
  //       i.e. A transparent top window above an opaque top window does not result in Transparent
  modal_manager_event_loop_upkeep();
  cl_assert_equal_i(modal_manager_get_properties(),
                    ModalProperty_Exists | ModalProperty_CompositorTransitions |
                    ModalProperty_RenderRequested);
  cl_assert_equal_i(windows[3][1]->on_screen, true);
  cl_assert_equal_i(windows[3][1]->is_click_configured, true);
  cl_assert_equal_i(windows[2][1]->on_screen, true);
  cl_assert_equal_i(windows[2][1]->is_click_configured, false);
  cl_assert_equal_i(windows[1][0]->on_screen, true);
  cl_assert_equal_i(windows[1][0]->is_click_configured, false);
  cl_assert_equal_p(s_last_click_configured_window, windows[3][1]);

  for (ModalPriority idx = ModalPriorityMin; idx < NumModalPriorities; idx++) {
    for (int i = 0; i < num_windows_per_stack; i++) {
      window_stack_remove(windows[idx][i], false);
      window_destroy(windows[idx][i]);
    }
  }
}

void test_window_stack__modal_properties_unfocused(void) {
  const int num_windows_per_stack = 2;
  Window *windows[NumModalPriorities][num_windows_per_stack];
  WindowStack *window_stacks[NumModalPriorities];

  for (ModalPriority idx = ModalPriorityMin; idx < NumModalPriorities; idx++) {
    window_stacks[idx] = modal_manager_get_window_stack(idx);
    for (int i = 0; i < num_windows_per_stack; i++) {
      windows[idx][i] = window_create();
    }
  }

  // The following checks use integer priorities to clearly indicate stack order
  // We check to make sure the first occurrence of integer values are less than NumModalPriorities

  // Test: No top window does not result in Exists
  // Test: No top window results in Transparent and Unfocused
  modal_manager_event_loop_upkeep();
  cl_assert_equal_i(modal_manager_get_properties(),
                    ModalProperty_Transparent | ModalProperty_Unfocused);

  // Add priority 2 opaque window 0
  cl_assert(NumModalPriorities > 2);
  window_stack_push(window_stacks[2], windows[2][0], false);

  // Add priority 2 unfocusable window 1
  window_set_focusable(windows[2][1], false);
  window_stack_push(window_stacks[2], windows[2][1], false);

  // An unfocusable window is now the top window
  // Test: Opaque windows that are not the top window have no affect on unfocusable
  // Test: One unfocusable top window results in Unfocused
  modal_manager_event_loop_upkeep();
  cl_assert_equal_i(modal_manager_get_properties(),
                    ModalProperty_Exists | ModalProperty_CompositorTransitions |
                    ModalProperty_RenderRequested | ModalProperty_Unfocused);
  // Checks are listed from top to bottom
  cl_assert_equal_i(windows[2][1]->on_screen, true);
  cl_assert_equal_i(windows[2][1]->is_click_configured, false);
  cl_assert_equal_i(windows[2][0]->on_screen, false);
  cl_assert_equal_i(windows[2][0]->is_click_configured, false);
  cl_assert_equal_p(s_last_click_configured_window, windows[2][0]);

  // Add priority 3 opaque window 0
  cl_assert(NumModalPriorities > 3);
  window_stack_push(window_stacks[3], windows[3][0], false);

  // An opaque top window of a different stack is now obstructing the unfocusable top window
  // Top here throughout means that it is the top window of the window stack it is in
  // Test: An opaque top window above a unfocusable top window removes Unfocusable
  //       i.e. A unfocusable top window below an opaque top window does not result in Unfocusable
  modal_manager_event_loop_upkeep();
  cl_assert_equal_i(modal_manager_get_properties(),
                    ModalProperty_Exists | ModalProperty_CompositorTransitions |
                    ModalProperty_RenderRequested);
  cl_assert_equal_i(windows[3][0]->on_screen, true);
  cl_assert_equal_i(windows[3][0]->is_click_configured, true);
  cl_assert_equal_i(windows[2][1]->on_screen, false);
  cl_assert_equal_i(windows[2][1]->is_click_configured, false);
  cl_assert_equal_p(s_last_click_configured_window, windows[3][0]);

  // Add priority 3 unfocusable window 1
  window_set_focusable(windows[3][1], false);
  window_stack_push(window_stacks[3], windows[3][1], false);

  // A unfocusable window is now the top window, and there is another unfocusable window below
  // Test: Multiple unfocusable top windows result in Unfocusable
  modal_manager_event_loop_upkeep();
  cl_assert_equal_i(modal_manager_get_properties(),
                    ModalProperty_Exists | ModalProperty_CompositorTransitions |
                    ModalProperty_RenderRequested | ModalProperty_Unfocused);
  cl_assert_equal_i(windows[3][1]->on_screen, true);
  cl_assert_equal_i(windows[3][1]->is_click_configured, false);
  cl_assert_equal_i(windows[3][0]->on_screen, false);
  cl_assert_equal_i(windows[3][0]->is_click_configured, false);
  cl_assert_equal_i(windows[2][1]->on_screen, false);
  cl_assert_equal_i(windows[2][1]->is_click_configured, false);
  cl_assert_equal_p(s_last_click_configured_window, windows[3][0]);

  // Add priority 1 opaque window 0
  cl_assert(NumModalPriorities > 1);
  window_stack_push(window_stacks[1], windows[1][0], false);

  // An opaque top window is now below two unfocusable top windows
  // Test: An opaque top window below a unfocusable top window removes Unfocusable
  //       i.e. A unfocusable top window above an opaque top window does not result in Unfocusable
  modal_manager_event_loop_upkeep();
  cl_assert_equal_i(modal_manager_get_properties(),
                    ModalProperty_Exists | ModalProperty_CompositorTransitions |
                    ModalProperty_RenderRequested);
  cl_assert_equal_i(windows[3][1]->on_screen, true);
  cl_assert_equal_i(windows[3][1]->is_click_configured, false);
  cl_assert_equal_i(windows[2][1]->on_screen, false);
  cl_assert_equal_i(windows[2][1]->is_click_configured, false);
  cl_assert_equal_i(windows[1][0]->on_screen, false);
  cl_assert_equal_i(windows[1][0]->is_click_configured, true);
  cl_assert_equal_p(s_last_click_configured_window, windows[1][0]);

  for (ModalPriority idx = ModalPriorityMin; idx < NumModalPriorities; idx++) {
    for (int i = 0; i < num_windows_per_stack; i++) {
      window_stack_remove(windows[idx][i], false);
      window_destroy(windows[idx][i]);
    }
  }
}

void test_window_stack__modal_properties_enable_disable(void) {
  // Enable all modals
  modal_manager_set_min_priority(ModalPriorityMin);

  Window *window1 = window_create();
  modal_window_push(window1, ModalPriorityGeneric, false);

  modal_manager_event_loop_upkeep();
  cl_assert_equal_i(modal_manager_get_properties(),
                    ModalProperty_Exists | ModalProperty_CompositorTransitions |
                    ModalProperty_RenderRequested);

  // Disable all modals
  modal_manager_set_min_priority(ModalPriorityMax);

  modal_manager_event_loop_upkeep();
  cl_assert_equal_i(modal_manager_get_properties(),
                    ModalProperty_Transparent | ModalProperty_Unfocused);

  // Re-enable all modals
  modal_manager_set_min_priority(ModalPriorityMin);

  modal_manager_event_loop_upkeep();
  cl_assert_equal_i(modal_manager_get_properties(),
                    ModalProperty_Exists | ModalProperty_CompositorTransitions);
}

// Description:
// This test ensures that when we push a window onto the modal window stack, then
// we push another window onto the modal window stack at a lower priority, then
// pushing the first at a lower priority than the second will bring the second onto
// the screen and subvert the first.
void test_window_stack__modal_reprioritize(void) {
  Window *window1 = window_create();
  Window *window2 = window_create();
  uint8_t base_priority = ModalPriorityDiscreet + 3;

  window_set_click_config_provider(window1, prv_click_config_provider);

  window_set_click_config_provider(window2, prv_click_config_provider);

  modal_window_push(window1, base_priority, false);

  cl_assert_equal_i(window1->on_screen, true);

  modal_window_push(window2, base_priority - 1, false);

  cl_assert_equal_i(window1->on_screen, true);
  cl_assert_equal_i(window2->on_screen, false);

  modal_window_push(window1, base_priority - 2, false);

  modal_manager_event_loop_upkeep();

  cl_assert_equal_i(window1->on_screen, false);
  cl_assert_equal_i(window2->on_screen, true);

  window_stack_remove(window2, false);

  modal_manager_event_loop_upkeep();

  cl_assert_equal_i(window2->on_screen, false);
  cl_assert_equal_i(window1->on_screen, true);

  window_stack_remove(window1, true);

  cl_assert_equal_i(window1->on_screen, false);

  window_destroy(window1);
  window_destroy(window2);
}

// Description:
// This test ensures that we are able to work with both the modal window stacks
// and the application stack at the same time.
void test_window_stack__modal_and_app(void) {
  Window *window1 = window_create();
  Window *window2 = window_create();

  WindowStack *app_stack = app_state_get_window_stack();
  WindowStack *modal_stack = modal_manager_get_window_stack(ModalPriorityGeneric);

  cl_check(app_stack);
  cl_check(modal_stack);

  cl_check(!app_stack->list_head);
  cl_check(!modal_stack->list_head);

  // Switch to the app state to push a window
  stub_pebble_tasks_set_current(PebbleTask_App);

  window_stack_push(app_stack, window1, true);

  cl_assert_equal_i(window_stack_count(app_stack), 1);
  cl_assert_equal_i(window1->on_screen, true);
  cl_assert_equal_p(s_last_click_configured_window, window1);

  // Switch to the kernel to push a modal window
  stub_pebble_tasks_set_current(PebbleTask_KernelMain);

  window_stack_push(modal_stack, window2, true);

  cl_assert_equal_i(window_stack_count(modal_stack), 1);
  cl_assert_equal_i(window2->on_screen, true);
  cl_assert_equal_p(s_last_click_configured_window, window2);

  // Switch to modal happens via the compositor
  compositor_transition(NULL);
  // Call the upkeep function so the change in state is handled
  modal_manager_event_loop_upkeep();

  // The app is now obstructed by an opaque modal, it should be idle
  cl_assert_equal_b(s_app_idle, true);

  // Assert that the window pushed onto the app stack has lost focus
  // We do this by checking the last event, which should have been a focus
  // lost event.
  PebbleEvent event = fake_event_get_last();
  cl_assert_equal_i(event.type, PEBBLE_APP_WILL_CHANGE_FOCUS_EVENT);
  cl_assert_equal_i(event.app_focus.in_focus, false);

  cl_assert_equal_i(window_stack_count(app_stack), 1);

  // Pop the modal window off the stack
  window_stack_remove(window2, true);

  cl_assert_equal_i(window_stack_count(modal_stack), 0);
  cl_assert_equal_i(window2->on_screen, false);

  // Switch to app happens via the compositor
  compositor_transition(NULL);
  // Call the upkeep function so the change in state is handled
  modal_manager_event_loop_upkeep();

  // The app is unobstructed, it should not be idle
  cl_assert_equal_b(s_app_idle, false);

  // Assert that the window pushed onto the app stack has regained focus,
  // this is also done by checking the last event.
  event = fake_event_get_last();

  cl_assert_equal_i(event.type, PEBBLE_APP_WILL_CHANGE_FOCUS_EVENT);
  cl_assert_equal_i(event.app_focus.in_focus, true);

  cl_assert_equal_i(window1->on_screen, true);

  window_stack_remove(window1, true);

  cl_assert_equal_i(window1->on_screen, false);
  cl_assert_equal_i(window_stack_count(app_stack), 0);

  window_destroy(window1);
  window_destroy(window2);
}

// Tests modal and app transitions with a transparent modal window
void test_window_stack__transparent_modal_and_app(void) {
  Window *window1 = window_create();
  Window *window2 = window_create();
  cl_assert_equal_b(window_is_transparent(window2), false);
  window_set_transparent(window2, true);
  cl_assert_equal_b(window_is_transparent(window2), true);

  WindowStack *app_stack = app_state_get_window_stack();
  WindowStack *modal_stack = modal_manager_get_window_stack(ModalPriorityGeneric);

  cl_check(app_stack);
  cl_check(modal_stack);

  cl_check(!app_stack->list_head);
  cl_check(!modal_stack->list_head);

  // Switch to the app state to push a window
  stub_pebble_tasks_set_current(PebbleTask_App);

  window_stack_push(app_stack, window1, true);

  cl_assert_equal_i(window_stack_count(app_stack), 1);
  cl_assert_equal_i(window1->on_screen, true);
  cl_assert_equal_p(s_last_click_configured_window, window1);

  // Switch to the kernel to push a modal window
  stub_pebble_tasks_set_current(PebbleTask_KernelMain);

  window_stack_push(modal_stack, window2, true);

  cl_assert_equal_i(window_stack_count(modal_stack), 1);
  cl_assert_equal_i(window2->on_screen, true);
  cl_assert_equal_p(s_last_click_configured_window, window2);

  // Switch to modal happens via the compositor
  compositor_transition(NULL);
  // Call the upkeep function so the change in state is handled
  modal_manager_event_loop_upkeep();

  // The app is now obstructed by a transparent modal, it should remain active
  cl_assert_equal_b(s_app_idle, false);

  // Assert that the window pushed onto the app stack has lost focus
  // We do this by checking the last event, which should have been a focus
  // lost event.
  PebbleEvent event = fake_event_get_last();
  cl_assert_equal_i(event.type, PEBBLE_APP_WILL_CHANGE_FOCUS_EVENT);
  cl_assert_equal_i(event.app_focus.in_focus, false);

  cl_assert_equal_i(window_stack_count(app_stack), 1);

  // Pop the modal window off the stack
  window_stack_remove(window2, true);

  cl_assert_equal_i(window_stack_count(modal_stack), 0);
  cl_assert_equal_i(window2->on_screen, false);

  // Switch to app happens via the compositor
  compositor_transition(NULL);
  // Call the upkeep function so the change in state is handled
  modal_manager_event_loop_upkeep();

  // The app is unobstructed, it should remain active
  cl_assert_equal_b(s_app_idle, false);

  // Assert that the window pushed onto the app stack has regained focus,
  // this is also done by checking the last event.
  event = fake_event_get_last();

  cl_assert_equal_i(event.type, PEBBLE_APP_WILL_CHANGE_FOCUS_EVENT);
  cl_assert_equal_i(event.app_focus.in_focus, true);

  cl_assert_equal_i(window1->on_screen, true);

  window_stack_remove(window1, true);

  cl_assert_equal_i(window1->on_screen, false);
  cl_assert_equal_i(window_stack_count(app_stack), 0);

  window_destroy(window1);
  window_destroy(window2);
}

// Tests modal and app transitions with an unfocusable modal window
void test_window_stack__unfocusable_modal_and_app(void) {
  Window *window1 = window_create();
  Window *window2 = window_create();
  cl_assert_equal_b(window_is_focusable(window2), true);
  window_set_focusable(window2, false);
  cl_assert_equal_b(window_is_focusable(window2), false);

  WindowStack *app_stack = app_state_get_window_stack();
  WindowStack *modal_stack = modal_manager_get_window_stack(ModalPriorityGeneric);

  cl_check(app_stack);
  cl_check(modal_stack);

  cl_check(!app_stack->list_head);
  cl_check(!modal_stack->list_head);

  // Switch to the app state to push a window
  stub_pebble_tasks_set_current(PebbleTask_App);

  window_stack_push(app_stack, window1, true);

  cl_assert_equal_i(window_stack_count(app_stack), 1);
  cl_assert_equal_i(window1->on_screen, true);
  cl_assert_equal_p(s_last_click_configured_window, window1);

  // Switch to the kernel to push a modal window
  stub_pebble_tasks_set_current(PebbleTask_KernelMain);

  window_stack_push(modal_stack, window2, true);

  cl_assert_equal_i(window_stack_count(modal_stack), 1);
  cl_assert_equal_i(window2->on_screen, true);
  cl_assert_equal_p(s_last_click_configured_window, window1);

  // Switch to modal happens via the compositor
  compositor_transition(NULL);
  // Call the upkeep function so the change in state is handled
  modal_manager_event_loop_upkeep();

  // The app is now obstructed by a unfocusable modal, it should remain active
  cl_assert_equal_b(s_app_idle, false);

  // The app should retain focus
  PebbleEvent event = fake_event_get_last();
  cl_assert_equal_i(event.type, PEBBLE_APP_WILL_CHANGE_FOCUS_EVENT);
  cl_assert_equal_i(event.app_focus.in_focus, true);

  cl_assert_equal_i(window_stack_count(app_stack), 1);

  // Pop the modal window off the stack
  window_stack_remove(window2, true);

  cl_assert_equal_i(window_stack_count(modal_stack), 0);
  cl_assert_equal_i(window2->on_screen, false);

  // Switch to app happens via the compositor
  compositor_transition(NULL);
  // Call the upkeep function so the change in state is handled
  modal_manager_event_loop_upkeep();

  // The app is unobstructed, it should remain active
  cl_assert_equal_b(s_app_idle, false);

  // Assert that the window pushed onto the app stack has remained focused
  event = fake_event_get_last();

  cl_assert_equal_i(event.type, PEBBLE_APP_WILL_CHANGE_FOCUS_EVENT);
  cl_assert_equal_i(event.app_focus.in_focus, true);

  cl_assert_equal_i(window1->on_screen, true);

  window_stack_remove(window1, true);

  cl_assert_equal_i(window1->on_screen, false);
  cl_assert_equal_i(window_stack_count(app_stack), 0);

  window_destroy(window1);
  window_destroy(window2);
}

// Description:
// This test ensures that the flow of adding a window to the window stack is followed
// correctly.  That is, we add the window to the window stack, its load handler is
// called, it calls to set the click config, and the click config is set properly.
void test_window_stack__window_flow(void) {
  Window *window = window_create();

  window_set_window_handlers(window, &(WindowHandlers){
    .load = prv_window_load,
    .unload = prv_window_unload,
    .appear = prv_window_appear,
    .disappear = prv_window_disappear
  });

  window_set_click_config_provider(window, prv_click_config_provider);

  cl_check(window->is_waiting_for_click_config);

  WindowStack *stack = app_state_get_window_stack();

  cl_check(stack);
  cl_check(!stack->list_head);

  // Switch to the app state to push the window
  stub_pebble_tasks_set_current(PebbleTask_App);

  window_stack_push(stack, window, true);

  cl_assert_equal_i(window->on_screen, 1);
  cl_assert_equal_i(window_stack_count(stack), 1);

  // Ensure the load handler was called
  cl_assert_equal_i(prv_get_load_unload_count(), 1);

  // Ensure the appear handler was called
  cl_assert_equal_i(prv_get_appear_disappear_count(), 1);

  // Ensure the click config handler was called
  cl_assert_equal_i(window->is_waiting_for_click_config, false);

  window_stack_pop(stack, false);

  cl_assert_equal_i(window_stack_count(stack), 0);

  // Ensure the disappear handler was called
  cl_assert_equal_i(prv_get_appear_disappear_count(), 0);

  // Ensure the unload handler was called
  cl_assert_equal_i(prv_get_load_unload_count(), 0);
}

void test_window_stack__dump(void) {
  WindowStack *stack = app_state_get_window_stack();
  Window *window1 = window_create();
  Window *window2 = window_create();
  Window *window3 = window_create();
  window1->debug_name = "Window1";
  window2->debug_name = "Window2";
  window3->debug_name = "Window3";

  window_stack_push(stack, window1, true);
  window_stack_push(stack, window2, true);
  window_stack_push(stack, window3, true);

  WindowStackDump *dump;
  size_t stack_depth = window_stack_dump(stack, &dump);
  cl_assert_equal_i(stack_depth, 3);
  cl_assert_equal_p(dump[0].addr, window3);
  cl_assert_equal_s(dump[0].name, "Window3");
  cl_assert_equal_p(dump[1].addr, window2);
  cl_assert_equal_s(dump[1].name, "Window2");
  cl_assert_equal_p(dump[2].addr, window1);
  cl_assert_equal_s(dump[2].name, "Window1");
  kernel_free(dump);
}

void test_window_stack__pop_all_modals(void) {
  Window *windows[NumModalPriorities];
  WindowStack *window_stacks[NumModalPriorities];

  for (ModalPriority idx = ModalPriorityMin; idx < NumModalPriorities; idx++) {
    window_stacks[idx] = modal_manager_get_window_stack(idx);
    windows[idx] = window_create();
    window_stack_push(window_stacks[idx], windows[idx], true);
    // Modals are visible as we push from lowest priority to top most
    cl_assert_equal_b(windows[idx]->on_screen, true);
  }

  // Only the top modal is visible
  modal_manager_event_loop_upkeep();
  for (ModalPriority idx = ModalPriorityMin; idx < NumModalPriorities; idx++) {
    cl_assert_equal_b(windows[idx]->on_screen, (idx == NumModalPriorities - 1));
  }

  // Pop all modals
  modal_manager_pop_all();
  modal_manager_event_loop_upkeep();

  _Static_assert(ModalPriorityMin == ModalPriorityDiscreet,
                 "Update the test to handle priorities below discreet.");
  // Discreet should not be popped
  cl_assert_equal_b(windows[ModalPriorityDiscreet]->on_screen, true);

  // All other modals should be popped
  for (ModalPriority idx = ModalPriorityDiscreet + 1; idx < NumModalPriorities; idx++) {
    cl_assert_equal_b(windows[idx]->on_screen, false);
  }

}

// Edge Case Tests
////////////////////////////////////

// Description:
// During the load handler of a window, we pop it.
void test_window_stack__pop_during_window_load(void) {
  Window *window = window_create();
  window_set_window_handlers(window, &(WindowHandlers){
    .load = prv_pop_window_load,
    .unload = prv_window_unload
  });

  WindowStack *stack = app_state_get_window_stack();

  // Switch to the app state to push a window
  stub_pebble_tasks_set_current(PebbleTask_App);

  cl_check(stack);
  cl_assert_equal_i(window_stack_count(stack), 0);

  window_stack_push(stack, window, true);

  cl_assert_equal_i(window_stack_count(stack), 0);

  // We popped the window off the screen, but the unload handler
  // should not have been called for it, as it hasn't finished
  // unloading.
  cl_assert_equal_i(prv_get_load_unload_count(), 1);
}

// Description:
// In this test, we push a window during the unload handler of a window.
void test_window_stack__push_during_window_unload(void) {
  Window *window = window_create();

  window_set_window_handlers(window, &(WindowHandlers){
    .load = prv_window_load,
    .unload = prv_push_window_unload
  });

  WindowStack *stack = app_state_get_window_stack();

  // Switch to the app state to push a window
  stub_pebble_tasks_set_current(PebbleTask_App);

  cl_check(stack);
  cl_assert_equal_i(window_stack_count(stack), 0);

  window_stack_push(stack, window, true);

  cl_assert_equal_i(window_stack_count(stack), 1);
  cl_assert_equal_i(window->on_screen, true);
  cl_assert_equal_i(prv_get_load_unload_count(), 1);

  window_stack_pop(stack, true);

  cl_assert_equal_i(window_stack_count(stack), 1);
  cl_assert_equal_i(prv_get_load_unload_count(), 1);
}

// Description:
// In this test we push two windows that push windows during their unload handlers.
// We want to verify that those two windows stay on the stack after calling
// `window_stack_pop_all`.
void test_window_stack__push_during_window_unload_multiple(void) {
  Window *window1 = window_create();
  Window *window2 = window_create();

  window_set_window_handlers(window1, &(WindowHandlers){
    .load = prv_window_load,
    .unload = prv_push_window_unload,
    .appear = prv_window_appear,
    .disappear = prv_window_disappear
  });

  window_set_window_handlers(window2, &(WindowHandlers){
    .load = prv_window_load,
    .unload = prv_push_window_unload,
    .appear = prv_window_appear,
    .disappear = prv_window_disappear
  });

  WindowStack *stack = app_state_get_window_stack();

  // Switch to the app state to push a window
  stub_pebble_tasks_set_current(PebbleTask_App);

  cl_check(stack);
  cl_assert_equal_i(window_stack_count(stack), 0);

  window_stack_push(stack, window1, true);
  window_stack_push(stack, window2, true);

  cl_assert_equal_i(window_stack_count(stack), 2);
  cl_assert_equal_i(prv_get_load_unload_count(), 2);
  cl_assert_equal_i(prv_get_appear_disappear_count(), 1);

  window_stack_pop_all(stack, true);

  cl_assert_equal_i(window_stack_count(stack), 2);
  cl_assert_equal_i(prv_get_load_unload_count(), 2);
  cl_assert_equal_i(prv_get_appear_disappear_count(), 2);
  cl_assert_equal_i(s_disappear_count, 2);
  cl_assert_equal_i(s_appear_count, 4);

  window_stack_pop_all(stack, true);

  cl_assert_equal_i(window_stack_count(stack), 0);
  cl_assert_equal_i(prv_get_load_unload_count(), 0);
}

// Edge Case
// Description:
// During the unload handler of a window, we try to pop it.
void test_window_stack__pop_during_window_unload(void) {
  Window *window = window_create();

  window_set_window_handlers(window, &(WindowHandlers){
    .load = prv_window_load,
    .unload = prv_pop_window_unload,
    .appear = prv_window_appear,
    .disappear = prv_window_disappear
  });

  WindowStack *stack = app_state_get_window_stack();

  // Switch to the app state to push a window
  stub_pebble_tasks_set_current(PebbleTask_App);

  cl_check(stack);
  cl_assert_equal_i(window_stack_count(stack), 0);

  window_stack_push(stack, window, true);

  cl_assert_equal_b(animation_is_scheduled(fake_animation_get_first_animation()), true);
  cl_assert_equal_i(window_stack_count(stack), 1);
  cl_assert_equal_i(prv_get_load_unload_count(), 1);
  cl_assert_equal_i(prv_get_appear_disappear_count(), 1);

  window_stack_remove(window, true);

  cl_assert_equal_i(window_stack_count(stack), 0);
  cl_assert_equal_i(prv_get_load_unload_count(), 0);
  cl_assert_equal_i(prv_get_appear_disappear_count(), 0);
  // FIXME: PBL-25460
  //  cl_assert_equal_b(animation_is_scheduled(fake_animation_get_first_animation()), false);
}

// Push two windows back to back, before the first transition completes. This should cancel the
// first transition and instead run the second transition.
void test_window_stack__double_animated_push(void) {
  Window *window1 = window_create();
  Window *window2 = window_create();

  WindowStack *stack = app_state_get_window_stack();

  window_stack_push(stack, window1, true);
  Animation *first = fake_animation_get_first_animation();

  cl_assert(animation_is_scheduled(first));

  window_stack_push(stack, window2, true);
  Animation *second = fake_animation_get_next_animation(first);
  cl_assert(!animation_is_scheduled(first));
  cl_assert(animation_is_scheduled(second));
}
