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

#include "comm/ble/kernel_le_client/ams/ams.h"

#include "comm/bt_conn_mgr.h"

#include "services/normal/music.h"
#include "services/normal/music_internal.h"

#include "clar.h"

// Stubs & Fakes
///////////////////////////////////////////////////////////

#include "fake_events.h"
#include "fake_gatt_client_operations.h"
#include "fake_gatt_client_subscriptions.h"
#include "fake_pebble_tasks.h"
#include "fake_rtc.h"

#include "stubs_app_install_manager.h"
#include "stubs_app_manager.h"
#include "stubs_bt_lock.h"
#include "stubs_hexdump.h"
#include "stubs_logging.h"
#include "stubs_mutex.h"
#include "stubs_passert.h"
#include "stubs_pbl_malloc.h"
#include "stubs_serial.h"
#include "stubs_tick.h"

void analytics_event_ams(uint8_t type, int32_t aux_info) {
}

struct {
  ResponseTimeState state;
  uint16_t max_period_secs;
} s_conn_mgr_states[NumBtConsumer];

void conn_mgr_set_ble_conn_response_time(GAPLEConnection *hdl, BtConsumer consumer,
                                         ResponseTimeState state, uint16_t max_period_secs) {
  s_conn_mgr_states[consumer].state = state;
  s_conn_mgr_states[consumer].max_period_secs = max_period_secs;
}

GAPLEConnection *gap_le_connection_by_device(const BTDeviceInternal *device) {
  return NULL;
}

BTDeviceInternal gatt_client_characteristic_get_device(BLECharacteristic characteristic_ref) {
  return (BTDeviceInternal) {
    .address.octets = {
      0x11, 0x22, 0x33, 0x44, 0x55, 0x66,
    },
  };
}

static void (*s_launcher_task_callback)(void *data);
static void *s_launcher_task_callback_data;

void launcher_task_add_callback(void (*callback)(void *data), void *data) {
  // Simple fake, can only handle one call
  cl_assert_equal_p(s_launcher_task_callback, NULL);

  s_launcher_task_callback = callback;
  s_launcher_task_callback_data = data;
}

// Tests: Disover AMS
///////////////////////////////////////////////////////////
#define NUM_AMS_INSTANCES 2
static BLECharacteristic s_characteristics[NUM_AMS_INSTANCES][NumAMSCharacteristic] = {
  // AMS instance one:
  [0] = {
    [AMSCharacteristicRemoteCommand] = 1,
    [AMSCharacteristicEntityUpdate] = 2,
    [AMSCharacteristicEntityAttribute] = 3,
  },
  // AMS instance two:
  [1] = {
    [AMSCharacteristicRemoteCommand] = 4,
    [AMSCharacteristicEntityUpdate] = 5,
    [AMSCharacteristicEntityAttribute] = 6,
  },
};

static const BLECharacteristic s_unknown_characteristic = 999;

static void prv_assert_can_handle_characteristics(uint32_t instance_idx, bool expect_can_handle) {
  for (AMSCharacteristic c = 0; c < NumAMSCharacteristic; ++c) {
    cl_assert_equal_b(ams_can_handle_characteristic(s_characteristics[instance_idx][c]),
                      expect_can_handle);
  }
}

static void prv_discover_ams(uint8_t num_instances) {
  cl_assert(num_instances <= 2);
  if (num_instances) {
    for (int i = 0; i < num_instances && i < NUM_AMS_INSTANCES; i++) {
      ams_handle_service_discovered(&s_characteristics[i][0]);
    }
  } else {
    ams_invalidate_all_references();
  }
}

void test_ams__cannot_handle_any_characteristic_after_destroy(void) {
  prv_discover_ams(1);
  ams_destroy();
  prv_assert_can_handle_characteristics(0, false /* expect_can_handle */);
}

void test_ams__cannot_handle_unknown_characteristic(void) {
  prv_discover_ams(1);
  cl_assert_equal_b(ams_can_handle_characteristic(s_unknown_characteristic), false);
}

void test_ams__discover_of_ams_should_subscribe_to_entity_update_characteristic(void) {
  // Pass in 2 instances, it should be able to cope with this
  prv_discover_ams(2 /* num_instances */);

  // Assert ams.c can now handle the characteristic reference for the first instance:
  prv_assert_can_handle_characteristics(0, true /* expect_can_handle */);

  // Assert ams.c can not handle the characteristic reference for the second instance:
  prv_assert_can_handle_characteristics(1, false /* expect_can_handle */);

  // The first instance is expected to be used.
  BLECharacteristic entity_update = s_characteristics[0][AMSCharacteristicEntityUpdate];
  fake_gatt_client_subscriptions_assert_subscribe(entity_update,
                                                  BLESubscriptionNotifications, GAPLEClientKernel);
}

void test_ams__connect_to_music_service_upon_subscribing_entity_update_characteristic(void) {
  prv_discover_ams(1 /* num_instances */);
  // Not connected yet (still need to subscribe):
  cl_assert_equal_s(music_get_connected_server_debug_name(), NULL);

  // Simulate successful subscription:
  BLECharacteristic entity_update = s_characteristics[0][AMSCharacteristicEntityUpdate];
  ams_handle_subscribe(entity_update, BLESubscriptionNotifications, BLEGATTErrorSuccess);

  // Music service should be connected now:
  cl_assert_equal_s(music_get_connected_server_debug_name(), ams_music_server_debug_name());

  // Rediscovery will disconnect music service (until resubscribed):
  ams_invalidate_all_references();
  prv_discover_ams(1 /* num_instances */);
  cl_assert_equal_s(music_get_connected_server_debug_name(), NULL);
}

void test_ams__dont_connect_music_service_if_subscribe_entity_update_characteristic_fails(void) {
  prv_discover_ams(1 /* num_instances */);

  // Simulate failed subscription:
  BLECharacteristic entity_update = s_characteristics[0][AMSCharacteristicEntityUpdate];
  ams_handle_subscribe(entity_update, BLESubscriptionNotifications, BLEGATTErrorUnlikelyError);

  // Not connected because subscription failed:
  cl_assert_equal_s(music_get_connected_server_debug_name(), NULL);
}

void test_ams__update_characteristics_ams_not_found(void) {
  // AMS Found:
  prv_discover_ams(1 /* num_instances */);
  // AMS Disappeared:
  prv_discover_ams(0 /* num_instances */);
  // Verify ams.c cannot handle the previous characteristics any more:
  prv_assert_can_handle_characteristics(0, false /* expect_can_handle */);
}

// Tests: Register for Entity Updates
///////////////////////////////////////////////////////////

static const uint8_t s_register_player_entity[] = {0x00,
                                                  // Apple bug #21283910
                                                  // See ams.c, prv_get_registration_cmd_for_entity
                                                  // 0x00,
                                                  0x01, 0x02};

static const uint8_t s_register_queue_entity[] = {0x01, 0x00, 0x01, 0x02, 0x03};

static const uint8_t s_register_track_entity[] = {0x02, 0x00, 0x01, 0x02, 0x03};

void test_ams__register_for_entity_updates(void) {
  prv_discover_ams(1 /* num_instances */);

  cl_assert_equal_b(ams_is_registered_for_all_entity_updates(), false);

  // Simulate successful subscription:
  BLECharacteristic entity_update = s_characteristics[0][AMSCharacteristicEntityUpdate];
  ams_handle_subscribe(entity_update, BLESubscriptionNotifications, BLEGATTErrorSuccess);

  // Expect to have written the command to register for the Player entity:
  fake_gatt_client_op_assert_write(entity_update, s_register_player_entity,
                                   sizeof(s_register_player_entity), GAPLEClientKernel,
                                   true /* is_response_required */);
  ams_handle_write_response(entity_update, BLEGATTErrorSuccess);

  cl_assert_equal_b(ams_is_registered_for_all_entity_updates(), false);

  // Expect to have written the command to register for the Queue entity:
  fake_gatt_client_op_assert_write(entity_update, s_register_queue_entity,
                                   sizeof(s_register_queue_entity), GAPLEClientKernel,
                                   true /* is_response_required */);
  ams_handle_write_response(entity_update, BLEGATTErrorSuccess);

  cl_assert_equal_b(ams_is_registered_for_all_entity_updates(), false);

  // Expect to have written the command to register for the Track entity:
  fake_gatt_client_op_assert_write(entity_update, s_register_track_entity,
                                   sizeof(s_register_track_entity), GAPLEClientKernel,
                                   true /* is_response_required */);
  ams_handle_write_response(entity_update, BLEGATTErrorSuccess);

  cl_assert_equal_b(ams_is_registered_for_all_entity_updates(), true);

  // After GATT re-discovery, the registration needs to happen again
  ams_invalidate_all_references();
  prv_discover_ams(1 /* num_instances */);
  cl_assert_equal_b(ams_is_registered_for_all_entity_updates(), false);
}

void test_ams__register_for_entity_updates_retry_if_out_of_resources(void) {
  prv_discover_ams(1 /* num_instances */);

  // Simulate not having enough resources to process the request:
  fake_gatt_client_op_set_write_return_value(BTErrnoNotEnoughResources);

  // Simulate successful subscription:
  BLECharacteristic entity_update = s_characteristics[0][AMSCharacteristicEntityUpdate];
  ams_handle_subscribe(entity_update, BLESubscriptionNotifications, BLEGATTErrorSuccess);

  // Nothing should have been written because out of resources:
  fake_gatt_client_op_assert_no_write();

  // Resources come available again:
  fake_gatt_client_op_set_write_return_value(BTErrnoOK);

  // Simulate processing the callback to retry:
  cl_assert(s_launcher_task_callback != NULL);
  s_launcher_task_callback(s_launcher_task_callback_data);

  // Expect to have written the command to register for the Player entity:
  fake_gatt_client_op_assert_write(entity_update, s_register_player_entity,
                                   sizeof(s_register_player_entity), GAPLEClientKernel,
                                   true /* is_response_required */);
  ams_handle_write_response(entity_update, BLEGATTErrorSuccess);
}

static const MusicServerImplementation s_dummy_server_implementation = {};
static void prv_set_dummy_server_connected(bool connected) {
  music_set_connected_server(&s_dummy_server_implementation,
                             connected /* connected */);
}

void test_ams__dont_register_if_another_music_server_is_already_connected(void) {
  prv_discover_ams(1 /* num_instances */);
  // Not connected yet (still need to subscribe):
  cl_assert_equal_s(music_get_connected_server_debug_name(), NULL);

  prv_set_dummy_server_connected(true /* connected */);

  // Simulate successful subscription:
  BLECharacteristic entity_update = s_characteristics[0][AMSCharacteristicEntityUpdate];
  ams_handle_subscribe(entity_update, BLESubscriptionNotifications, BLEGATTErrorSuccess);

  // Nothing should have been written, because there's already a music server connected.
  fake_gatt_client_op_assert_no_write();

  // Clean up after ourselves:
  prv_set_dummy_server_connected(false /* connected */);
}

// Tests: Sending Remote Commands
///////////////////////////////////////////////////////////

static void prv_connect_ams(void) {
  prv_discover_ams(1 /* num_instances */);

  // Simulate successful subscription:
  BLECharacteristic entity_update = s_characteristics[0][AMSCharacteristicEntityUpdate];
  ams_handle_subscribe(entity_update, BLESubscriptionNotifications, BLEGATTErrorSuccess);

  // Simulate successful write responses (for the Entity Update registration write requests):
  ams_handle_write_response(entity_update, BLEGATTErrorSuccess);
  ams_handle_write_response(entity_update, BLEGATTErrorSuccess);
  ams_handle_write_response(entity_update, BLEGATTErrorSuccess);

  cl_assert_equal_b(ams_is_registered_for_all_entity_updates(), true);

  fake_gatt_client_op_clear_write_list();
}

static uint8_t prv_ams_command_for_music_command(MusicCommand command) {
  switch (command) {
    case MusicCommandPlay:
      return 0x00;
    case MusicCommandPause:
      return 0x01;
    case MusicCommandTogglePlayPause:
      return 0x02;
    case MusicCommandNextTrack:
      return 0x03;
    case MusicCommandPreviousTrack:
      return 0x04;
    case MusicCommandVolumeUp:
      return 0x05;
    case MusicCommandVolumeDown:
      return 0x06;
    case MusicCommandAdvanceRepeatMode:
      return 0x07;
    case MusicCommandAdvanceShuffleMode:
      return 0x08;
    case MusicCommandSkipForward:
      return 0x09;
    case MusicCommandSkipBackward:
      return 0x0A;
    case MusicCommandLike:
      return 0x0B;
    case MusicCommandDislike:
      return 0x0C;
    case MusicCommandBookmark:
      return 0x0D;
    default:
      cl_assert(false);
      return 0xFF;
  }
}

void test_ams__send_remote_command(void) {
  prv_connect_ams();

  // Exercise all MusicCommand types (currently they are all supported):
  for (MusicCommand music_cmd = 0; music_cmd < NumMusicCommand; ++music_cmd) {
    music_command_send(music_cmd);

    BLECharacteristic remote_command = s_characteristics[0][AMSCharacteristicRemoteCommand];
    const uint8_t ams_cmd = prv_ams_command_for_music_command(music_cmd);
    fake_gatt_client_op_assert_write(remote_command, &ams_cmd, sizeof(ams_cmd),
                                     GAPLEClientKernel, true /* is_response_required */);

    // Simulate receiving the response:
    ams_handle_write_response(remote_command, BLEGATTErrorSuccess);
  }

  // Invalid/Unsupported command:
  music_command_send(NumMusicCommand);
  fake_gatt_client_op_assert_no_write();
}

void test_ams__send_remote_command_non_kernel_main_task(void) {
  prv_connect_ams();

  // Simulate Music app calling music_command_send():
  stub_pebble_tasks_set_current(PebbleTask_App);
  music_command_send(MusicCommandPlay);

  // Process the KernelMain callback:
  cl_assert(s_launcher_task_callback != NULL);
  stub_pebble_tasks_set_current(PebbleTask_KernelMain);
  s_launcher_task_callback(s_launcher_task_callback_data);

  BLECharacteristic remote_command = s_characteristics[0][AMSCharacteristicRemoteCommand];
  const uint8_t ams_cmd = prv_ams_command_for_music_command(MusicCommandPlay);
  fake_gatt_client_op_assert_write(remote_command, &ams_cmd, sizeof(ams_cmd),
                                   GAPLEClientKernel, true /* is_response_required */);
}

void test_ams__send_remote_command_non_kernel_main_task_then_disconnect(void) {
  prv_connect_ams();

  // Simulate Music app calling music_command_send():
  stub_pebble_tasks_set_current(PebbleTask_App);
  music_command_send(MusicCommandPlay);

  // Simulate disconnecting:
  ams_destroy();

  // Process the KernelMain callback:
  cl_assert(s_launcher_task_callback != NULL);
  stub_pebble_tasks_set_current(PebbleTask_KernelMain);
  s_launcher_task_callback(s_launcher_task_callback_data);

  // No crashes, no writes
  fake_gatt_client_op_assert_no_write();
}

// Tests: music_needs_user_to_start_playback_on_phone
///////////////////////////////////////////////////////////

void test_ams__music_needs_user_to_start_playback_on_phone(void) {
  prv_connect_ams();

  // No metadata received yet, user needs to start playback on phone
  cl_assert_equal_b(music_needs_user_to_start_playback_on_phone(), true);

  // Received metadata
//  cl_assert_equal_b(music_needs_user_to_start_playback_on_phone(), false);

  // Metadata cleared
  cl_assert_equal_b(music_needs_user_to_start_playback_on_phone(), true);
}

// Tests: music_request_..._latency
///////////////////////////////////////////////////////////

void test_ams__music_request_reduced_latency(void) {
  prv_connect_ams();

  music_request_reduced_latency(true /* reduced_latency */);
  cl_assert_equal_i(s_conn_mgr_states[BtConsumerMusicServiceIndefinite].state,
                    ResponseTimeMiddle);
  cl_assert_equal_i(s_conn_mgr_states[BtConsumerMusicServiceIndefinite].max_period_secs,
                    MAX_PERIOD_RUN_FOREVER);

  music_request_reduced_latency(false /* reduced_latency */);
  cl_assert_equal_i(s_conn_mgr_states[BtConsumerMusicServiceIndefinite].state,
                    ResponseTimeMax);
}

void test_ams__music_request_low_latency_for_period(void) {
  prv_connect_ams();

  const uint32_t period_s = 1234;
  music_request_low_latency_for_period(period_s * 1000);
  cl_assert_equal_i(s_conn_mgr_states[BtConsumerMusicServiceMomentary].state,
                    ResponseTimeMin);
  cl_assert_equal_i(s_conn_mgr_states[BtConsumerMusicServiceMomentary].max_period_secs,
                    period_s);
}

// Tests: Receiving Player updates (the happy paths)
///////////////////////////////////////////////////////////

static void prv_receive_entity_update(const uint8_t *update, uint16_t update_length) {
  BLECharacteristic entity_update = s_characteristics[0][AMSCharacteristicEntityUpdate];
  ams_handle_read_or_notification(entity_update, update, update_length, BLEGATTErrorSuccess);
}

void test_ams__receive_player_name_update(void) {
  prv_connect_ams();

  // 0000  00 00 00 4d 75 73 69 63                            ...Music
  uint8_t player_name_update[] = {
    0x00, 0x00, 0x00, 0x4d, 0x75, 0x73, 0x69, 0x63,
  };
  prv_receive_entity_update(player_name_update, sizeof(player_name_update));

  char player_name[MUSIC_BUFFER_LENGTH];
  cl_assert_equal_b(music_get_player_name(player_name), true);
  cl_assert_equal_s(player_name, "Music");
}

void test_ams__receive_player_playback_info_update(void) {
  prv_connect_ams();

  // Receive: playing, 200% playback rate, elapsed time 184.755s
  // 0000  00 01 00 31 2c 32 2e 30  2c 31 38 34 2e 37 35 35   ...1,2.0 ,184.755
  uint8_t playback_info_update[] = {
    0x00, 0x01, 0x00, 0x31, 0x2c, 0x32, 0x2e, 0x30,
    0x2c, 0x31, 0x38, 0x34, 0x2e, 0x37, 0x35, 0x35,
  };
  prv_receive_entity_update(playback_info_update, sizeof(playback_info_update));

  cl_assert_equal_i(music_get_playback_state(), MusicPlayStatePlaying);

  // music_get_pos relies on having a sensible track duration, so simulate receiving this too:
  // 02 03 00 33 31 39 2e 35  30 37                     ...319.5 07
  uint8_t track_duration_update[] = {
    0x02, 0x03, 0x00, 0x33, 0x31, 0x39, 0x2e, 0x35,
    0x30, 0x37,
  };
  prv_receive_entity_update(track_duration_update, sizeof(track_duration_update));

  uint32_t track_pos_ms;
  uint32_t track_duration_ms;
  music_get_pos(&track_pos_ms, &track_duration_ms);
  cl_assert_equal_i(track_pos_ms, 184755);
  cl_assert_equal_i(track_duration_ms, 319507);

  cl_assert_equal_i(music_get_playback_rate_percent(), 200);
}

void test_ams__receive_player_playback_info_update_nulls(void) {
  prv_connect_ams();

  // Receive: paused, empty string, empty string
  // 0000  00 01 00 30 2c 2c                                  ...0,,
  uint8_t playback_info_update[] = {
    0x00, 0x01, 0x00, 0x30, 0x2c, 0x2c,
  };
  prv_receive_entity_update(playback_info_update, sizeof(playback_info_update));

  cl_assert_equal_i(music_get_playback_state(), MusicPlayStatePaused);

  uint32_t track_pos_ms;
  uint32_t track_duration_ms;
  music_get_pos(&track_pos_ms, &track_duration_ms);
  cl_assert_equal_i(track_pos_ms, 0);
  cl_assert_equal_i(track_duration_ms, 0);

  cl_assert_equal_i(music_get_playback_rate_percent(), 0);
}

void test_ams__receive_player_volume_update(void) {
  prv_connect_ams();

  // Receive volume of 0.604925
  // 00 02 00 30 2e 36 30 34  39 32 35                  ...0.604 925
  uint8_t volume_update[] = {
    0x00, 0x02, 0x00, 0x30, 0x2e, 0x36, 0x30, 0x34,
    0x39, 0x32, 0x35,
  };
  prv_receive_entity_update(volume_update, sizeof(volume_update));

  cl_assert_equal_i(music_get_volume_percent(), 60);
}

// Tests: Receiving Player updates (the unhappy paths)
///////////////////////////////////////////////////////////

static void prv_assert_initial_playback_state(void) {
  cl_assert_equal_i(music_get_playback_state(), MusicPlayStateUnknown);

  uint32_t track_pos_ms;
  uint32_t track_duration_ms;
  music_get_pos(&track_pos_ms, &track_duration_ms);
  cl_assert_equal_i(track_pos_ms, 0);
  cl_assert_equal_i(track_duration_ms, 0);

  cl_assert_equal_i(music_get_playback_rate_percent(), 0);
}

void test_ams__receive_non_numeric_player_playback_info_update(void) {
  prv_connect_ams();

  // Receive: 'A', 'B.0' playback rate, elapsed time 184.755s
  // 0000  00 01 00 41 2c 42 2e 30  2c 31 38 34 2e 37 35 35   ...A,B.0 ,184.755
  uint8_t nan_playback_info_update[] = {
    0x00, 0x01, 0x00, 0x41, 0x2c, 0x42, 0x2e, 0x30,
    0x2c, 0x31, 0x38, 0x34, 0x2e, 0x37, 0x35, 0x35,
  };
  prv_receive_entity_update(nan_playback_info_update, sizeof(nan_playback_info_update));

  prv_assert_initial_playback_state();
}

void test_ams__receive_incomplete_csv_list_player_playback_info_update(void) {
  prv_connect_ams();

  // Receive: playing, 200% playback rate
  // 0000  00 01 00 31 2c 32 2e 30    ...1,2.0
  uint8_t incomplete_playback_info_update[] = {
    0x00, 0x01, 0x00, 0x31, 0x2c, 0x32, 0x2e, 0x30,
  };
  prv_receive_entity_update(incomplete_playback_info_update,
                            sizeof(incomplete_playback_info_update));

  prv_assert_initial_playback_state();
}

void test_ams__receive_malformed_player_volume_update(void) {
  prv_connect_ams();

  cl_assert_equal_i(music_get_volume_percent(), 0);

  // Receive volume of 0.604925
  // 00 02 00 30 2e 41 30 34  39 32 35                  ...0.A04 925
  uint8_t volume_update[] = {
    0x00, 0x02, 0x00, 0x30, 0x2e, 0x41, 0x30, 0x34,
    0x39, 0x32, 0x35,
  };
  prv_receive_entity_update(volume_update, sizeof(volume_update));

  cl_assert_equal_i(music_get_volume_percent(), 0);
}

// Tests: Receiving Track updates
///////////////////////////////////////////////////////////

void test_ams__receive_track_artist_update(void) {
  prv_connect_ams();

  // 0000  02 00 00 4d 69 6c 65 73  20 44 61 76 69 73         ...Miles  Davis
  uint8_t track_artist_update[] = {
    0x02, 0x00, 0x00, 0x4d, 0x69, 0x6c, 0x65, 0x73,
    0x20, 0x44, 0x61, 0x76, 0x69, 0x73,
  };
  prv_receive_entity_update(track_artist_update, sizeof(track_artist_update));

  char artist[MUSIC_BUFFER_LENGTH];
  music_get_now_playing(NULL, artist, NULL);

  cl_assert_equal_s(artist, "Miles Davis");
}

void test_ams__receive_track_title_update(void) {
  prv_connect_ams();

  // 0000  02 02 00 53 6f 20 57 68  61 74                     ...So Wh at
  uint8_t track_title_update[] = {
    0x02, 0x02, 0x00, 0x53, 0x6f, 0x20, 0x57, 0x68,
    0x61, 0x74,
  };
  prv_receive_entity_update(track_title_update, sizeof(track_title_update));

  char title[MUSIC_BUFFER_LENGTH];
  music_get_now_playing(title, NULL, NULL);

  cl_assert_equal_s(title, "So What");
}

void test_ams__receive_track_album_update(void) {
  prv_connect_ams();

  // 0000  02 01 00 4b 69 6e 64 20  4f 66 20 42 6c 75 65 20   ...Kind  Of Blue
  // 0010  28 4c 65 67 61 63 79 20  45 64 69 74 69 6f 6e 29   (Legacy  Edition)
  uint8_t track_album_update[] = {
    0x02, 0x01, 0x00, 0x4b, 0x69, 0x6e, 0x64, 0x20,
    0x4f, 0x66, 0x20, 0x42, 0x6c, 0x75, 0x65, 0x20,
    0x28, 0x4c, 0x65, 0x67, 0x61, 0x63, 0x79, 0x20,
    0x45, 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x29,
  };
  prv_receive_entity_update(track_album_update, sizeof(track_album_update));

  char album[MUSIC_BUFFER_LENGTH];
  music_get_now_playing(NULL, NULL, album);

  cl_assert_equal_s(album, "Kind Of Blue (Legacy Edition)");
}

// Tests: Music service capabilities
///////////////////////////////////////////////////////////

void test_ams__supported_capabilities(void) {
  prv_connect_ams();

  // music_is_progress_reporting_supported() relies on a valid track duration
  uint8_t track_duration_update[] = {
    0x02, 0x03, 0x00, 0x33, 0x31, 0x39, 0x2e, 0x35,
    0x30, 0x37,
  };
  prv_receive_entity_update(track_duration_update, sizeof(track_duration_update));

  cl_assert_equal_b(music_is_playback_state_reporting_supported(), true);
  cl_assert_equal_b(music_is_progress_reporting_supported(), true);
  cl_assert_equal_b(music_is_volume_reporting_supported(), true);
  cl_assert_equal_b(music_needs_user_to_start_playback_on_phone(), true);
  for (MusicCommand cmd = 0; cmd < NumMusicCommand; ++cmd) {
    cl_assert_equal_b(music_is_command_supported(cmd), true);
  }

  cl_assert_equal_b(music_is_command_supported(NumMusicCommand), false);
}

// Tests: Create & Destroy
///////////////////////////////////////////////////////////

void test_ams__create_again_trips_assert(void) {
  cl_assert_passert(ams_create());
}

void test_ams__create_works_again_after_destroy(void) {
  ams_destroy();
  ams_create();
  // No assert hit.
}

void test_ams__destroy_after_destroy_is_fine(void) {
  ams_destroy();
  ams_destroy();
  // No assert hit.
}

void test_ams__destroy_disconnects_from_music_service(void) {
  prv_discover_ams(1 /* num_instances */);

  // Simulate successful subscription:
  BLECharacteristic entity_update = s_characteristics[0][AMSCharacteristicEntityUpdate];
  ams_handle_subscribe(entity_update, BLESubscriptionNotifications, BLEGATTErrorSuccess);

  ams_destroy();
  cl_assert_equal_s(music_get_connected_server_debug_name(), NULL);
}

// Test setup
///////////////////////////////////////////////////////////

void test_ams__initialize(void) {
  s_launcher_task_callback = NULL;
  memset(s_conn_mgr_states, 0, sizeof(s_conn_mgr_states));
  fake_rtc_init(1234, 5678);
  fake_event_init();
  fake_gatt_client_subscriptions_init();
  fake_gatt_client_op_init();
  stub_pebble_tasks_set_current(PebbleTask_KernelMain);
  ams_create();
  cl_assert_equal_s(music_get_connected_server_debug_name(), NULL);
}

void test_ams__cleanup(void) {
  ams_destroy();
  fake_gatt_client_op_deinit();
  fake_gatt_client_subscriptions_deinit();
}
