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

#include "clar.h"


#include "applib/ui/menu_layer.h"
#include "flash_region/flash_region.h"
#include "process_management/app_install_manager.h"
#include "process_management/app_menu_data_source.h"
#include "resource/resource.h"
#include "resource/resource_storage.h"
#include "resource/resource_storage_file.h"
#include "services/common/system_task.h"
#include "services/normal/app_cache.h"
#include "services/normal/blob_db/app_db.h"
#include "services/normal/filesystem/pfs.h"
#include "util/build_id.h"
#include "util/size.h"
#include "fixtures/load_test_resources.h"

// access it directly just to test things out
#include "shell/system_app_registry_list.auto.h"

#include <stdio.h>
#include <sys/stat.h>
#include <sys/types.h>

// Stub Includes
////////////////////////////////////
#include "stubs_activity.h"
#include "stubs_analytics.h"
#include "stubs_app_custom_icon.h"
#include "stubs_app_fetch_endpoint.h"
#include "stubs_app_manager.h"
#include "stubs_app_state.h"
#include "stubs_bootbits.h"
#include "stubs_build_id.h"
#include "stubs_comm_session.h"
#include "stubs_event_loop.h"
#include "stubs_event_service_client.h"
#include "stubs_events.h"
#include "stubs_fonts.h"
#include "stubs_gbitmap.h"
#include "stubs_graphics.h"
#include "stubs_graphics.h"
#include "stubs_graphics_context.h"
#include "stubs_heap.h"
#include "stubs_hexdump.h"
#include "stubs_i18n.h"
#include "stubs_kino_reel.h"
#include "stubs_logging.h"
#include "stubs_memory_layout.h"
#include "stubs_menu_layer.h"
#include "stubs_mutex.h"
#include "stubs_passert.h"
#include "stubs_pbl_malloc.h"
#include "stubs_pebble_tasks.h"
#include "stubs_persist.h"
#include "stubs_pin_db.h"
#include "stubs_process_loader.h"
#include "stubs_process_manager.h"
#include "stubs_process_manager.h"
#include "stubs_prompt.h"
#include "stubs_put_bytes.h"
#include "stubs_queue.h"
#include "stubs_quick_launch.h"
#include "stubs_rand_ptr.h"
#include "stubs_serial.h"
#include "stubs_shell_prefs.h"
#include "stubs_sleep.h"
#include "stubs_system_task.h"
#include "stubs_task_watchdog.h"
#include "stubs_watchface.h"
#include "stubs_worker_manager.h"

// Fake Includes
////////////////////////////////////
#include "fake_spi_flash.h"

const uint32_t g_num_file_resource_stores = 0;
const FileResourceData g_file_resource_stores[] = {};

#define APP_REGISTRY_FIXTURE_PATH "app_registry"

#define BG_COUNTER_APP_NAME "Background Counter"
#define MENU_LAYER_APP_NAME "MenuLayerName"
#define BIG_TIME_APP_NAME "Big Time"

#define BG_COUNTER_APP_ID 1
#define MENU_LAYER_APP_ID 2
#define BIG_TIME_APP_ID 3

// background counter
static const AppDBEntry bg_counter_app = {
  .name = BG_COUNTER_APP_NAME,
  .uuid = {0x1e, 0xb1, 0xd3, 0x9b, 0x56, 0x98, 0x48, 0x44,
           0xb3, 0x94, 0x1f, 0x87, 0xb6, 0xbe, 0xae, 0x67},
  .info_flags = PROCESS_INFO_HAS_WORKER | PROCESS_INFO_STANDARD_APP,
  .icon_resource_id = 0,
  .app_version = {
    .major = 1,
    .minor = 0,
  },
  .sdk_version = {
    .major = 5,
    .minor = 13,
  },
  .app_face_bg_color = {0},
  .template_id = 0,
};

// menu layer
static const AppDBEntry menu_layer_app = {
  .name = MENU_LAYER_APP_NAME,
  .uuid = {0xb8, 0x26, 0x2e, 0x08, 0x57, 0xe9, 0x4e, 0x58,
           0x88, 0x02, 0x45, 0xfd, 0xfe, 0xe0, 0xac, 0x77},
  .info_flags = PROCESS_INFO_STANDARD_APP,
  .icon_resource_id = 0,
  .app_version = {
    .major = 2,
    .minor = 0,
  },
  .sdk_version = {
    .major = 5,
    .minor = 13,
  },
  .app_face_bg_color = {0},
  .template_id = 0,
};

// big time
static const AppDBEntry big_time_app = {
  .name = BIG_TIME_APP_NAME,
  .uuid = {0xaf, 0xcc, 0x68, 0x76, 0x8f, 0x84, 0x44, 0xe0,
           0xbb, 0x8b, 0x02, 0x3f, 0xfb, 0x2d, 0x7c, 0x2c},
  .info_flags = PROCESS_INFO_WATCH_FACE,
  .icon_resource_id = 0,
  .app_version = {
    .major = 6,
    .minor = 0,
  },
  .sdk_version = {
    .major = 5,
    .minor = 17,
  },
  .app_face_bg_color = {0},
  .template_id = 0,
};

AppInstallId bg_counter_app_id;
AppInstallId menu_layer_app_id;
AppInstallId big_time_app_id;

// Fakes
////////////////////////////////////
uint32_t time_get_uptime_seconds(void) {
  return rtc_get_time();
}

// Tests
////////////////////////////////////
static MenuLayer menu_layer;
static AppMenuDataSource data_source;

static bool app_filter_callback(struct AppMenuDataSource *source, AppInstallEntry *entry) {
  if (app_install_entry_is_hidden(entry)) {
    return false;
  }
  if (app_install_entry_is_watchface(entry)) {
    return false; // Only apps
  }
  return true;
}

static bool watchface_filter_callback(struct AppMenuDataSource *source, AppInstallEntry *entry) {
  if (app_install_entry_is_hidden(entry)) {
    return false;
  }
  if (!app_install_entry_is_watchface(entry)) {
    return false; // Only watchfaces
  }
  return true;
}

static bool everything_filter_callback(struct AppMenuDataSource *source, AppInstallEntry *entry) {
  return true;
}

void test_app_menu_data_source__initialize(void) {
  fake_spi_flash_init(0, 0x1000000);

  pfs_init(false);
  pfs_format(false);

  app_install_manager_init();
  app_db_init();
  app_cache_init();

  load_resource_fixture_in_flash(RESOURCES_FIXTURE_PATH, SYSTEM_RESOURCES_FIXTURE_NAME, false);
  resource_init();

  // simulate installing bg_counter_app on flash
  app_db_insert((uint8_t *)&bg_counter_app.uuid, sizeof(Uuid),
                (uint8_t *)&bg_counter_app, sizeof(AppDBEntry));
  bg_counter_app_id = app_db_get_install_id_for_uuid(&bg_counter_app.uuid);
  app_cache_add_entry(bg_counter_app_id, 10701);
  cl_assert_equal_i(BG_COUNTER_APP_ID, bg_counter_app_id);

  // simulate installing menu_layer_app on flash
  app_db_insert((uint8_t *)&menu_layer_app.uuid, sizeof(Uuid),
                (uint8_t *)&menu_layer_app, sizeof(AppDBEntry));
  menu_layer_app_id = app_db_get_install_id_for_uuid(&menu_layer_app.uuid);
  app_cache_add_entry(menu_layer_app_id, 10701);
  cl_assert_equal_i(MENU_LAYER_APP_ID, menu_layer_app_id);

  // simulate installing big_time_app on flash
  app_db_insert((uint8_t *)&big_time_app.uuid, sizeof(Uuid),
                (uint8_t *)&big_time_app, sizeof(AppDBEntry));
  big_time_app_id = app_db_get_install_id_for_uuid(&big_time_app.uuid);
  app_cache_add_entry(big_time_app_id, 10701);
  cl_assert_equal_i(BIG_TIME_APP_ID, big_time_app_id);

  menu_layer_init(&menu_layer, &GRect(0,0,144,76));

  rtc_set_time(100);
}

extern ListNode *s_head_callback_node_list;

void test_app_menu_data_source__cleanup(void) {
  s_head_callback_node_list = NULL;
  app_install_manager_flush_recent_communication_timestamps();
}

/*************************************

 *************************************/

static void prv_menu_layer_reload_data(void *data) {
  cl_assert_equal_p(data, &menu_layer);
  menu_layer_reload_data(&menu_layer);
}

void test_app_menu_data_source__pass_init(void) {
  app_menu_data_source_init(&data_source, &(AppMenuDataSourceCallbacks) {
    .changed = prv_menu_layer_reload_data,
    .filter = everything_filter_callback,
  }, &menu_layer);
  uint16_t num_apps = app_menu_data_source_get_count(&data_source);

  for (uint16_t i = 0; i < num_apps; i++) {
    AppMenuNode *node = app_menu_data_source_get_node_at_index(&data_source, i);
    cl_assert(node);
  }
}

void test_app_menu_data_source__check_default_order_apps(void) {
  // settings has to be at the beginning. The app_menu_data_source module enforces it
  static const AppInstallId app_default_order[] = {APP_ID_SETTINGS, APP_ID_MUSIC,
                                                   APP_ID_NOTIFICATIONS, APP_ID_ALARMS,
                                                   APP_ID_WATCHFACES, APP_ID_WORKOUT,
                                                   BG_COUNTER_APP_ID, MENU_LAYER_APP_ID};
  app_menu_data_source_init(&data_source, &(AppMenuDataSourceCallbacks) {
    .changed = prv_menu_layer_reload_data,
    .filter = app_filter_callback,
  }, &menu_layer);
  uint16_t num_apps = app_menu_data_source_get_count(&data_source);
  cl_assert_equal_i(num_apps, ARRAY_LENGTH(app_default_order));

  for (uint16_t i = 0; i < num_apps; i++) {
    AppMenuNode *node = app_menu_data_source_get_node_at_index(&data_source, i);
    cl_assert_equal_i(node->install_id, app_default_order[i]);
  }

  app_menu_data_source_deinit(&data_source);
}

static uint16_t prv_reverse_index(AppMenuDataSource *data_source, uint16_t original_index,
                                  void *context) {
  return app_menu_data_source_get_count(data_source) - 1 - original_index;
}

void test_app_menu_data_source__transform_index(void) {
  // settings has to be at the beginning. The app_menu_data_source module enforces it
  static const AppInstallId app_default_order[] = {APP_ID_SETTINGS, APP_ID_MUSIC,
                                                   APP_ID_NOTIFICATIONS, APP_ID_ALARMS,
                                                   APP_ID_WATCHFACES, APP_ID_WORKOUT,
                                                   BG_COUNTER_APP_ID, MENU_LAYER_APP_ID};
  app_menu_data_source_init(&data_source, &(AppMenuDataSourceCallbacks) {
    .changed = prv_menu_layer_reload_data,
    .filter = app_filter_callback,
    .transform_index = prv_reverse_index,
  }, &menu_layer);
  uint16_t num_apps = app_menu_data_source_get_count(&data_source);
  cl_assert_equal_i(num_apps, ARRAY_LENGTH(app_default_order));

  for (uint16_t i = 0; i < num_apps; i++) {
    AppMenuNode *node = app_menu_data_source_get_node_at_index(&data_source, i);
    cl_assert_equal_i(node->install_id, app_default_order[num_apps - 1 - i]);
  }

  app_menu_data_source_deinit(&data_source);
}

void test_app_menu_data_source__check_default_order_watchfaces(void) {
  static const AppInstallId watchface_default_order[] = {APP_ID_TICTOC,
                                                         BIG_TIME_APP_ID};
  app_menu_data_source_init(&data_source, &(AppMenuDataSourceCallbacks) {
    .changed = prv_menu_layer_reload_data,
    .filter = watchface_filter_callback,
  }, &menu_layer);
  uint16_t num_apps = app_menu_data_source_get_count(&data_source);
  cl_assert_equal_i(num_apps, ARRAY_LENGTH(watchface_default_order));

  for (uint16_t i = 0; i < num_apps; i++) {
    AppMenuNode *node = app_menu_data_source_get_node_at_index(&data_source, i);
    cl_assert_equal_i(node->install_id, watchface_default_order[i]);
  }
  app_menu_data_source_deinit(&data_source);
}

void prv_write_order_to_file(const AppInstallId order[], uint8_t num_entries) {
  uint8_t entries_to_write = num_entries + 1;
  uint16_t file_len = sizeof(uint8_t) + (entries_to_write) * sizeof(AppInstallId);

  pfs_remove("lnc_ord");
  int fd = pfs_open("lnc_ord", OP_FLAG_WRITE, FILE_TYPE_STATIC, file_len);
  pfs_write(fd, &entries_to_write, sizeof(entries_to_write));
  pfs_write(fd, order, sizeof(AppInstallId) * num_entries);
  AppInstallId zero_id = 0;
  pfs_write(fd, &zero_id, sizeof(zero_id));
  pfs_close(fd);
}

void prv_test_new_order_with_filter_callback(const AppInstallId order[], uint8_t num_entries,
                                             AppMenuFilterCallback filter_callback) {
  prv_write_order_to_file(order, num_entries);

  app_menu_data_source_init(&data_source, &(AppMenuDataSourceCallbacks) {
    .changed = prv_menu_layer_reload_data,
    .filter = filter_callback,
  }, &menu_layer);
  uint16_t num_apps = app_menu_data_source_get_count(&data_source);
  // cl_assert_equal_i(num_apps, num_entries);

  for (uint16_t i = 0; i < num_apps; i++) {
    AppMenuNode *node = app_menu_data_source_get_node_at_index(&data_source, i);
    cl_assert_equal_i(node->install_id, order[i]);
  }

  app_menu_data_source_deinit(&data_source);
}

void prv_shuffle(AppInstallId *array, uint8_t n) {
  for (uint8_t i = 0; i < n - 1; i++) {
    uint8_t j = i + rand() / (RAND_MAX / (n - i) + 1);
    int t = array[j];
    array[j] = array[i];
    array[i] = t;
  }
}

void test_app_menu_data_source__change_order_apps(void) {
  // settings has to be at the beginning. The app_menu_data_source module enforces it
  AppInstallId app_order[] = {APP_ID_SETTINGS, APP_ID_MUSIC, APP_ID_NOTIFICATIONS, APP_ID_ALARMS,
                              APP_ID_WATCHFACES, APP_ID_WORKOUT, BG_COUNTER_APP_ID,
                              MENU_LAYER_APP_ID};

  uint8_t num_entries = ARRAY_LENGTH(app_order);
  prv_test_new_order_with_filter_callback(app_order, num_entries,
                                          app_filter_callback);
}

void test_app_menu_data_source__change_order_watchfaces(void) {
  AppInstallId watchface_order[] = {BIG_TIME_APP_ID, APP_ID_TICTOC};

  for (int i = 0; i < 10; i++) {
    uint8_t num_entries = ARRAY_LENGTH(watchface_order);
    prv_shuffle(watchface_order, num_entries);
    prv_test_new_order_with_filter_callback(watchface_order, num_entries,
                                            watchface_filter_callback);
  }
}

void test_app_menu_data_source__last_app_not_in_order_file(void) {
  // settings has to be at the beginning. The app_menu_data_source module enforces it
  AppInstallId app_order[] = {APP_ID_SETTINGS, APP_ID_MUSIC, APP_ID_NOTIFICATIONS, APP_ID_ALARMS,
                              APP_ID_WATCHFACES, APP_ID_WORKOUT,
                              BG_COUNTER_APP_ID};

  uint8_t num_entries = ARRAY_LENGTH(app_order);
  prv_write_order_to_file(app_order, num_entries);

  app_menu_data_source_init(&data_source, &(AppMenuDataSourceCallbacks) {
    .changed = prv_menu_layer_reload_data,
    .filter = app_filter_callback,
  }, &menu_layer);
  uint16_t num_apps = app_menu_data_source_get_count(&data_source);
  cl_assert_equal_i(num_apps, num_entries + 1);

  for (uint16_t i = 0; i < num_apps; i++) {
    AppMenuNode *node = app_menu_data_source_get_node_at_index(&data_source, i);

    // MENU_LAYER_APP_ID isn't in file, but it should still be in the list at the end.
    if (i == (num_apps - 1)) {
      cl_assert_equal_i(node->install_id, MENU_LAYER_APP_ID);
    } else {
      cl_assert_equal_i(node->install_id, app_order[i]);
    }
  }

  app_menu_data_source_deinit(&data_source);
}

void test_app_menu_data_source__floating_music_app(void) {
  // settings has to be at the beginning. The app_menu_data_source module enforces it
  // This test will move the music app to the second position
  AppInstallId written_order[] = {APP_ID_SETTINGS, APP_ID_NOTIFICATIONS, APP_ID_ALARMS,
                                  APP_ID_WATCHFACES, APP_ID_WORKOUT, BG_COUNTER_APP_ID,
                                  MENU_LAYER_APP_ID, APP_ID_MUSIC};

  AppInstallId desired_order[] = {APP_ID_MUSIC, APP_ID_SETTINGS, APP_ID_NOTIFICATIONS,
                                  APP_ID_ALARMS, APP_ID_WATCHFACES, APP_ID_WORKOUT,
                                  BG_COUNTER_APP_ID, MENU_LAYER_APP_ID};

  uint8_t num_entries = ARRAY_LENGTH(written_order);
  prv_write_order_to_file(written_order, num_entries);

  app_install_mark_prioritized(APP_ID_MUSIC, true /* can expire */);

  app_menu_data_source_init(&data_source, &(AppMenuDataSourceCallbacks) {
    .changed = prv_menu_layer_reload_data,
    .filter = app_filter_callback,
  }, &menu_layer);
  uint16_t num_apps = app_menu_data_source_get_count(&data_source);

  for (uint16_t i = 0; i < num_apps; i++) {
    AppMenuNode *node = app_menu_data_source_get_node_at_index(&data_source, i);
    cl_assert_equal_i(node->install_id, desired_order[i]);
  }

  app_menu_data_source_deinit(&data_source);
}

void test_app_menu_data_source__all_floating_apps(void) {
  // settings has to be at the beginning. The app_menu_data_source module enforces it
  // This test will move the music app to the second position
  AppInstallId written_order[] = {APP_ID_SETTINGS, APP_ID_NOTIFICATIONS, APP_ID_ALARMS,
                                  APP_ID_WATCHFACES, APP_ID_WORKOUT, BG_COUNTER_APP_ID,
                                  MENU_LAYER_APP_ID, APP_ID_MUSIC};

  AppInstallId desired_order[] = {APP_ID_GOLF, APP_ID_WORKOUT, APP_ID_MUSIC,
                                  APP_ID_SETTINGS, APP_ID_NOTIFICATIONS, APP_ID_ALARMS,
                                  APP_ID_WATCHFACES, BG_COUNTER_APP_ID, MENU_LAYER_APP_ID};

  uint8_t num_entries = ARRAY_LENGTH(written_order);
  prv_write_order_to_file(written_order, num_entries);

  app_install_mark_prioritized(APP_ID_MUSIC, true /* can expire */);
  app_install_mark_prioritized(APP_ID_WORKOUT, false /* can expire */);
  app_install_mark_prioritized(APP_ID_GOLF, true /* can expire */);

  app_menu_data_source_init(&data_source, &(AppMenuDataSourceCallbacks) {
    .changed = prv_menu_layer_reload_data,
    .filter = app_filter_callback,
  }, &menu_layer);
  uint16_t num_apps = app_menu_data_source_get_count(&data_source);

  for (uint16_t i = 0; i < num_apps; i++) {
    AppMenuNode *node = app_menu_data_source_get_node_at_index(&data_source, i);
    cl_assert_equal_i(node->install_id, desired_order[i]);
  }

  app_menu_data_source_deinit(&data_source);
}

void test_app_menu_data_source__complete_sorted_order(void) {
  // Apps are sorted in the order of Quick Launch only, Override apps, Storage (smallest first),
  // Record (smallest first), and finally Install ID (smallest first). Verify that this is true.
  // This also tests that the Settings app (a special case) respects storage order if it exists
  // in the storage order list.
  AppInstallId storage_order[] = {APP_ID_NOTIFICATIONS, BG_COUNTER_APP_ID, APP_ID_SETTINGS};

  AppInstallId desired_order[] = {
    // Quick Launch only
    APP_ID_QUIET_TIME_TOGGLE,
    // Override apps
    APP_ID_SPORTS,
    APP_ID_GOLF,
    // Storage (smallest first) defined by `storage_order`
    APP_ID_NOTIFICATIONS,
    BG_COUNTER_APP_ID,
    APP_ID_SETTINGS,
    // Record (smallest first) defined by
    // `tests/overrides/fake_app_registry/shell/system_app_registry_list.auto.h`
    APP_ID_TICTOC,
    APP_ID_MUSIC,
    APP_ID_ALARMS,
    APP_ID_WATCHFACES,
    APP_ID_WORKOUT,
    // Install ID (smallest first)
    MENU_LAYER_APP_ID,
    BIG_TIME_APP_ID,
  };

  _Static_assert(MENU_LAYER_APP_ID < BIG_TIME_APP_ID,
                 "MENU_LAYER_APP_ID is unexpectedly >= BIG_TIME_APP_ID.");

  const uint8_t num_entries = ARRAY_LENGTH(storage_order);
  prv_write_order_to_file(storage_order, num_entries);

  app_menu_data_source_init(&data_source, &(AppMenuDataSourceCallbacks) {
    .changed = prv_menu_layer_reload_data,
    .filter = everything_filter_callback,
  }, &menu_layer);
  const uint16_t num_apps = app_menu_data_source_get_count(&data_source);

  for (uint16_t i = 0; i < num_apps; i++) {
    AppMenuNode *node = app_menu_data_source_get_node_at_index(&data_source, i);
    cl_assert_equal_i(node->install_id, desired_order[i]);
  }

  app_menu_data_source_deinit(&data_source);
}

void test_app_menu_data_source__settings_app_floats_to_top_if_absent_from_storage_order(void) {
  AppInstallId storage_order[] = {APP_ID_NOTIFICATIONS, BG_COUNTER_APP_ID, APP_ID_MUSIC};

  AppInstallId desired_order[] = {
    // Settings floats above storage entries since it's absent in the storage order
    APP_ID_SETTINGS,
    // Storage (smallest first) defined by `storage_order`
    APP_ID_NOTIFICATIONS,
    BG_COUNTER_APP_ID,
    APP_ID_MUSIC,
    // Record (smallest first) defined by
    // `tests/overrides/fake_app_registry/shell/system_app_registry_list.auto.h`
    APP_ID_ALARMS,
    APP_ID_WATCHFACES,
    APP_ID_WORKOUT,
    // Install ID (smallest first)
    MENU_LAYER_APP_ID,
    BIG_TIME_APP_ID,
  };

  _Static_assert(MENU_LAYER_APP_ID < BIG_TIME_APP_ID,
                 "MENU_LAYER_APP_ID is unexpectedly >= BIG_TIME_APP_ID.");

  const uint8_t num_entries = ARRAY_LENGTH(storage_order);
  prv_write_order_to_file(storage_order, num_entries);

  app_menu_data_source_init(&data_source, &(AppMenuDataSourceCallbacks) {
    .changed = prv_menu_layer_reload_data,
    .filter = app_filter_callback,
  }, &menu_layer);
  const uint16_t num_apps = app_menu_data_source_get_count(&data_source);

  for (uint16_t i = 0; i < num_apps; i++) {
    AppMenuNode *node = app_menu_data_source_get_node_at_index(&data_source, i);
    cl_assert_equal_i(node->install_id, desired_order[i]);
  }

  app_menu_data_source_deinit(&data_source);
}

int prv_app_node_comparator(void *app_node_ref, void *new_node_ref);

void test_app_menu_data_source__app_node_comparator_equality_cases(void) {
  // Test handling of storage and record equality cases
  AppMenuNode app_menu_nodes[] = {{
    .install_id = APP_ID_ALARMS,
    .storage_order = 0,
    .record_order = 3,
  }, {
    .install_id = APP_ID_TICTOC,
    .storage_order = 0,
    .record_order = 3,
  }, {
    .install_id = APP_ID_NOTIFICATIONS,
    .storage_order = 1,
    .record_order = 0,
  }, {
    .install_id = APP_ID_SETTINGS,
    .storage_order = 2,
    .record_order = 1,
  }, {
    .install_id = APP_ID_WATCHFACES,
    .storage_order = 0,
    .record_order = 4,
  }, {
    .install_id = APP_ID_WORKOUT,
    .storage_order = 0,
    .record_order = 5,
  }};

  AppInstallId desired_order[] = {
    APP_ID_NOTIFICATIONS,
    APP_ID_SETTINGS,
    APP_ID_TICTOC,
    APP_ID_ALARMS,
    APP_ID_WATCHFACES,
    APP_ID_WORKOUT,
  };

  AppMenuNode *app_list = NULL;
  const uint16_t num_apps = ARRAY_LENGTH(app_menu_nodes);
  for (uint16_t i = 0; i < num_apps; i++) {
    app_list = (AppMenuNode *)list_sorted_add(&app_list->node, &app_menu_nodes[i].node,
                                              prv_app_node_comparator, true /* ascending */);
  }

  for (uint16_t i = 0; i < num_apps; i++) {
    AppMenuNode *node = (AppMenuNode *)list_get_at(&app_list->node, i);
    cl_assert_equal_i(node->install_id, desired_order[i]);
  }
}