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

#include "comm/ble/kernel_le_client/ppogatt/ppogatt.h"
#include "comm/ble/kernel_le_client/ppogatt/ppogatt_internal.h"
#include "services/common/comm_session/session_transport.h"
#include "services/common/regular_timer.h"

#include <util/size.h>

#include "clar.h"

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

#include "stubs_analytics.h"
#include "stubs_bt_conn_mgr.h"
#include "stubs_bt_lock.h"
#include "stubs_logging.h"
#include "stubs_mfg_info.h"
#include "stubs_mutex.h"
#include "stubs_passert.h"
#include "stubs_print.h"
#include "stubs_prompt.h"
#include "stubs_rand_ptr.h"
#include "stubs_serial.h"

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

#include "fake_gatt_client_operations.h"
#include "fake_gatt_client_subscriptions.h"
#include "fake_new_timer.h"
#include "fake_pbl_malloc.h"
#include "fake_session.h"
#include "fake_system_task.h"

#define MTU_SIZE (158)
#define MAX_PAYLOAD_SIZE (MTU_SIZE - 3 /* ATT Header size */ - 1 /* PPoGATT Packet Header */)

uint16_t s_mtu_size;

int bt_driver_gap_le_disconnect(const BTDeviceInternal *peer_address) {
  return 0;
}

uint16_t gap_le_connection_get_gatt_mtu(const BTDeviceInternal *device) {
  return s_mtu_size;
}

GAPLEConnection *gap_le_connection_get_gateway(void) {
  return NULL;
}

GAPLEConnection *gatt_client_characteristic_get_connection(BLECharacteristic characteristic_ref) {
  return NULL;
}

static BTDeviceInternal s_device = {};

BTDeviceInternal gatt_client_characteristic_get_device(BLECharacteristic characteristic_ref) {
  return s_device;
}

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

// Helpers
///////////////////////////////////////////////////////////

extern Transport *ppogatt_client_for_uuid(const Uuid *uuid);
extern bool ppogatt_has_client_for_uuid(const Uuid *uuid);
extern uint32_t ppogatt_client_count(void);
extern void ppogatt_trigger_rx_ack_send_timeout(void);
extern TransportDestination ppogatt_get_destination(Transport *transport);

static const uint8_t s_num_service_instances = 2;
static BLECharacteristic s_characteristics[s_num_service_instances][PPoGATTCharacteristicNum] = {
  [0] = {
    [PPoGATTCharacteristicData] = 01,
    [PPoGATTCharacteristicMeta] = 02,
  },
  [1] = {
    [PPoGATTCharacteristicData] = 11,
    [PPoGATTCharacteristicMeta] = 12,
  },
};

static const BLECharacteristic s_unknown_characteristics = 0x55;

static const PPoGATTMetaV0 s_meta_v0_app = {
  .ppogatt_min_version = PPOGATT_MIN_VERSION,
  .ppogatt_max_version = USE_PPOGATT_VERSION,
  .app_uuid = UuidMake(0xA4, 0x83, 0x2A, 0x0E, 0x74, 0x54, 0x45, 0x32,
                       0xB2, 0xA2, 0x4E, 0x6F, 0x8F, 0x7B, 0x68, 0x6F)
};

static const PPoGATTMetaV0 s_meta_v0_system = {
  .ppogatt_min_version = PPOGATT_MIN_VERSION,
  .ppogatt_max_version = USE_PPOGATT_VERSION,
  .app_uuid = (const Uuid) UUID_SYSTEM,
};

static const PPoGATTMetaV1 s_meta_v1_hybrid = {
  .ppogatt_min_version = 0,
  .ppogatt_max_version = 0,
  .app_uuid = (const Uuid) UUID_SYSTEM,
  .pp_session_type = PPoGATTSessionType_Hybrid,
};

static const PPoGATTMetaV1 s_meta_v1_system_inferred = {
  .ppogatt_min_version = 0,
  .ppogatt_max_version = 0,
  .app_uuid = (const Uuid) UUID_SYSTEM,
  .pp_session_type = PPoGATTSessionType_InferredFromUuid,
};

static const PPoGATTMetaV1 s_meta_v1_app_inferred = {
  .ppogatt_min_version = 0,
  .ppogatt_max_version = 0,
  .app_uuid = UuidMake(0xA4, 0x83, 0x2A, 0x0E, 0x74, 0x54, 0x45, 0x32,
                       0xB2, 0xA2, 0x4E, 0x6F, 0x8F, 0x7B, 0x68, 0x6F),
  .pp_session_type = PPoGATTSessionType_InferredFromUuid,
};

static PPoGATTPacket s_reset_complete = (const PPoGATTPacket) {
  .sn = 0,
  .type = PPoGATTPacketTypeResetComplete,
};

static PPoGATTPacket s_server_reset_request = (const PPoGATTPacket) {
  .sn = 0,
  .type = PPoGATTPacketTypeResetRequest,
};

static PPoGATTPacket * s_client_reset_request;
static uint16_t s_client_reset_request_size;

static PPoGATTPacket * s_client_reset_complete;
static uint16_t s_client_reset_complete_size;

static int s_ppogatt_version;
static int s_tx_window_size;
static int s_rx_window_size;

static void prv_create_expected_reset_request(void) {
  s_client_reset_request_size = sizeof(PPoGATTPacket) + sizeof(PPoGATTResetRequestClientIDPayload);
  s_client_reset_request = (PPoGATTPacket *) malloc(s_client_reset_request_size);
  *s_client_reset_request = (const PPoGATTPacket) {
    .sn = 0,
    .type = PPoGATTPacketTypeResetRequest,
  };
  PPoGATTResetRequestClientIDPayload *client_id_payload =
  (PPoGATTResetRequestClientIDPayload *)s_client_reset_request->payload;
  *client_id_payload = (const PPoGATTResetRequestClientIDPayload) {
    .ppogatt_version = s_ppogatt_version,
  };
  memcpy(client_id_payload->serial_number, mfg_get_serial_number(), MFG_SERIAL_NUMBER_SIZE);
}

static void prv_create_expected_reset_complete(void) {
  s_client_reset_complete_size = sizeof(PPoGATTPacket);
  if (s_ppogatt_version  > 0) {
    s_client_reset_complete_size += sizeof(PPoGATTResetCompleteClientIDPayloadV1);
  }

  s_client_reset_complete = (PPoGATTPacket *) malloc(s_client_reset_complete_size);
  *s_client_reset_complete = (const PPoGATTPacket) {
    .sn = 0,
    .type = PPoGATTPacketTypeResetComplete,
  };

  if (s_ppogatt_version > 0) {
    *((PPoGATTResetCompleteClientIDPayloadV1 *)s_client_reset_complete->payload) =
        (const PPoGATTResetCompleteClientIDPayloadV1) {
      .ppogatt_max_rx_window = s_rx_window_size,
      .ppogatt_max_tx_window = s_tx_window_size,
    };
  }
}

static void prv_receive_reset_request(BLECharacteristic characteristic) {
  ppogatt_handle_read_or_notification(characteristic, (const uint8_t *) &s_server_reset_request,
                                      sizeof(s_server_reset_request), BLEGATTErrorSuccess);
}
static void prv_receive_reset_complete(BLECharacteristic characteristic) {
  ppogatt_handle_read_or_notification(characteristic, (const uint8_t *)s_client_reset_complete,
                                      s_client_reset_complete_size, BLEGATTErrorSuccess);
}

static const uint8_t s_short_data_fragment[] = {
  0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77,
};

static void prv_receive_short_data_fragment(BLECharacteristic characteristic, uint8_t sn) {
  PPoGATTPacket *packet = malloc(sizeof(PPoGATTPacket) + sizeof(s_short_data_fragment));
  packet->sn = sn;
  packet->type = PPoGATTPacketTypeData;
  memcpy(packet->payload, s_short_data_fragment, sizeof(s_short_data_fragment));
  ppogatt_handle_read_or_notification(characteristic, (const uint8_t *) packet,
                                      sizeof(s_short_data_fragment), BLEGATTErrorSuccess);
  free(packet);
}

static void prv_receive_ack(BLECharacteristic characteristic, uint8_t sn) {
  const PPoGATTPacket ack = (const PPoGATTPacket) {
    .sn = sn,
    .type = PPoGATTPacketTypeAck,
  };
  ppogatt_handle_read_or_notification(characteristic, (const uint8_t *) &ack,
                                      sizeof(ack), BLEGATTErrorSuccess);
}

static void prv_assert_sent_reset_request(BLECharacteristic characteristic) {
  fake_gatt_client_op_assert_write(characteristic,
                                   (const uint8_t *) s_client_reset_request,
                                   s_client_reset_request_size,
                                   GAPLEClientKernel, false /* is_response_required */);
}

static void prv_assert_sent_reset_complete(BLECharacteristic characteristic) {
  struct PACKED {
    PPoGATTPacketType type:3;
    uint8_t sn:PPOGATT_SN_BITS;
    PPoGATTResetCompleteClientIDPayloadV1 payload;
  } expected_response = {
    .sn = 0,
    .type = PPoGATTPacketTypeResetComplete
  };

  if (s_ppogatt_version > 0) {
    expected_response.payload = (const PPoGATTResetCompleteClientIDPayloadV1) {
      .ppogatt_max_rx_window = PPOGATT_V1_DESIRED_RX_WINDOW_SIZE,
      .ppogatt_max_tx_window = PPOGATT_V0_WINDOW_SIZE,
    };
  }

  fake_gatt_client_op_assert_write(characteristic,
                                   (const uint8_t *) &expected_response, s_client_reset_complete_size,
                                   GAPLEClientKernel, false /* is_response_required */);

  if (s_ppogatt_version > 0) {
    s_tx_window_size = MIN(s_tx_window_size, expected_response.payload.ppogatt_max_tx_window);
    s_rx_window_size = MIN(s_rx_window_size, expected_response.payload.ppogatt_max_rx_window);
  } else {
    s_tx_window_size = s_rx_window_size =  PPOGATT_V0_WINDOW_SIZE;
  }
}

static void prv_assert_sent_ack(BLECharacteristic characteristic, uint8_t sn) {
  const PPoGATTPacket ack = (const PPoGATTPacket) {
    .sn = sn,
    .type = PPoGATTPacketTypeAck,
  };
  fake_gatt_client_op_assert_write(characteristic, (const uint8_t *) &ack, sizeof(ack),
                                   GAPLEClientKernel, false /* is_response_required */);
}

static void prv_assert_sent_data(BLECharacteristic characteristic, uint8_t sn,
                                 const uint8_t *data, size_t length) {
  cl_assert(length <= MAX_PAYLOAD_SIZE);
  PPoGATTPacket *packet = malloc(sizeof(PPoGATTPacket) + length);
  packet->sn = sn;
  packet->type = PPoGATTPacketTypeData;
  memcpy(packet->payload, data, length);
  fake_gatt_client_op_assert_write(characteristic, (const uint8_t *) packet,
                                   sizeof(PPoGATTPacket) + length,
                                   GAPLEClientKernel, false /* is_response_required */);
  free(packet);
}

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

void test_ppogatt__initialize(void) {
  s_ppogatt_version = USE_PPOGATT_VERSION;
  s_tx_window_size = 25;
  s_rx_window_size = 25;
  prv_create_expected_reset_request();
  prv_create_expected_reset_complete();
  s_mtu_size = MTU_SIZE;
  fake_pbl_malloc_clear_tracking();
  fake_gatt_client_op_init();
  fake_gatt_client_subscriptions_init();
  regular_timer_init();
  fake_comm_session_init();
  ppogatt_create();
}

void test_ppogatt__cleanup(void) {
  ppogatt_destroy();
  cl_assert_equal_i(ppogatt_client_count(), 0);
  cl_assert_equal_i(regular_timer_seconds_count(), 0);
  regular_timer_deinit();
  fake_gatt_client_op_deinit();
  fake_gatt_client_subscriptions_deinit();

  // Check for leaks:
  fake_pbl_malloc_check_net_allocs();
  fake_pbl_malloc_clear_tracking();

  fake_comm_session_cleanup();
  free(s_client_reset_request);
  free(s_client_reset_complete);
}

void prv_notify_services_discovered(int num_services_to_register) {
  for (int i = 0; i < s_num_service_instances && i < num_services_to_register; i++) {
    ppogatt_handle_service_discovered(s_characteristics[i]);
  }
}

void test_ppogatt__find_pebble_app_and_3rd_party_app(void) {
  prv_notify_services_discovered(s_num_service_instances);

  // Assert GATT reads requests to Meta characteristics happened:
  fake_gatt_client_op_assert_read(s_characteristics[0][PPoGATTCharacteristicMeta],
                                  GAPLEClientKernel);
  fake_gatt_client_op_assert_read(s_characteristics[1][PPoGATTCharacteristicMeta],
                                  GAPLEClientKernel);

  // Simulate read responses:
  ppogatt_handle_read_or_notification(s_characteristics[0][PPoGATTCharacteristicMeta],
                                      (const uint8_t *) &s_meta_v0_system, sizeof(s_meta_v0_system),
                                      BLEGATTErrorSuccess);
  cl_assert_equal_b(ppogatt_has_client_for_uuid(&s_meta_v0_system.app_uuid), true);

  ppogatt_handle_read_or_notification(s_characteristics[1][PPoGATTCharacteristicMeta],
                                      (const uint8_t *) &s_meta_v0_app, sizeof(s_meta_v0_app),
                                      BLEGATTErrorSuccess);
  cl_assert_equal_b(ppogatt_has_client_for_uuid(&s_meta_v0_app.app_uuid), true);
}

void test_ppogatt__handles_unknown_read_response(void) {
  uint8_t data;
  ppogatt_handle_read_or_notification(s_unknown_characteristics,
                                      &data, sizeof(data), BLEGATTErrorSuccess);
  // No crashes / asserts etc.
}

void test_ppogatt__handles_too_short_meta_length(void) {
  prv_notify_services_discovered(1);

  ppogatt_handle_read_or_notification(s_characteristics[0][PPoGATTCharacteristicMeta],
                                      (const uint8_t *) &s_meta_v0_system,
                                      sizeof(s_meta_v0_system) - 1 /* missing last byte */,
                                      BLEGATTErrorSuccess);
  // No client created:
  cl_assert_equal_i(ppogatt_client_count(), 0);
  cl_assert_equal_b(ppogatt_has_client_for_uuid(&s_meta_v0_system.app_uuid), false);
}

void test_ppogatt__handles_meta_v1(void) {
  struct {
    const PPoGATTMetaV1 *meta;
    TransportDestination expected_destination;
  } metas[] = {
    {
      .meta = &s_meta_v1_hybrid,
      .expected_destination = TransportDestinationHybrid,
    },
    {
      .meta = &s_meta_v1_system_inferred,
      .expected_destination = TransportDestinationSystem,
    },
    {
      .meta = &s_meta_v1_app_inferred,
      .expected_destination = TransportDestinationApp,
    },
  };

  for (int i = 0; i < ARRAY_LENGTH(metas); ++i) {
    prv_notify_services_discovered(1);
    ppogatt_handle_read_or_notification(s_characteristics[0][PPoGATTCharacteristicMeta],
                                        (const uint8_t *) metas[i].meta,
                                        sizeof(PPoGATTMetaV1),
                                        BLEGATTErrorSuccess);
    // Client created:
    cl_assert_equal_i(ppogatt_client_count(), 1);

    Transport *client = ppogatt_client_for_uuid(&metas[i].meta->app_uuid);
    cl_assert_equal_i(ppogatt_get_destination(client), metas[i].expected_destination);
    ppogatt_close(client);
  }
}

void test_ppogatt__handles_unsupported_meta_ppogatt_version(void) {
  PPoGATTMetaV0 future_meta_non_compatible = s_meta_v0_system;
  future_meta_non_compatible.ppogatt_min_version = 0xaa;
  future_meta_non_compatible.ppogatt_max_version = 0xff;

  prv_notify_services_discovered(1);

  ppogatt_handle_read_or_notification(s_characteristics[0][PPoGATTCharacteristicMeta],
                                      (const uint8_t *) &future_meta_non_compatible,
                                      sizeof(future_meta_non_compatible),
                                      BLEGATTErrorSuccess);
  // No client created:
  cl_assert_equal_i(ppogatt_client_count(), 0);
  cl_assert_equal_b(ppogatt_has_client_for_uuid(&future_meta_non_compatible.app_uuid), false);
}

void test_ppogatt__handles_invalid_uuid_meta(void) {
  PPoGATTMetaV0 meta_invalid_uuid = s_meta_v0_system;
  meta_invalid_uuid.app_uuid = UUID_INVALID;

  prv_notify_services_discovered(1);

  ppogatt_handle_read_or_notification(s_characteristics[0][PPoGATTCharacteristicMeta],
                                      (const uint8_t *) &meta_invalid_uuid,
                                      sizeof(meta_invalid_uuid),
                                      BLEGATTErrorSuccess);
  // No client created:
  cl_assert_equal_i(ppogatt_client_count(), 0);
  cl_assert_equal_b(ppogatt_has_client_for_uuid(&meta_invalid_uuid.app_uuid), false);
}

void test_ppogatt__deletes_existing_client_after_rediscovery(void) {
  prv_notify_services_discovered(1);

  ppogatt_handle_read_or_notification(s_characteristics[0][PPoGATTCharacteristicMeta],
                                      (const uint8_t *) &s_meta_v0_system,
                                      sizeof(s_meta_v0_system),
                                      BLEGATTErrorSuccess);
  // Client created:
  cl_assert_equal_i(ppogatt_client_count(), 1);
  cl_assert_equal_b(ppogatt_has_client_for_uuid(&s_meta_v0_system.app_uuid), true);
  Transport *client = ppogatt_client_for_uuid(&s_meta_v0_system.app_uuid);

  // Rediscovery:
  ppogatt_invalidate_all_references();
  prv_notify_services_discovered(1);

  // Still one client:
  cl_assert_equal_i(ppogatt_client_count(), 1);

  ppogatt_handle_read_or_notification(s_characteristics[0][PPoGATTCharacteristicMeta],
                                      (const uint8_t *) &s_meta_v0_system,
                                      sizeof(s_meta_v0_system),
                                      BLEGATTErrorSuccess);
  // Still one client:
  cl_assert_equal_i(ppogatt_client_count(), 1);
  cl_assert_equal_b(ppogatt_has_client_for_uuid(&s_meta_v0_system.app_uuid), true);
  Transport *client2 = ppogatt_client_for_uuid(&s_meta_v0_system.app_uuid);
}

void test_ppogatt__invalidate_characteristic_refs_immediately_after_update(void) {
  prv_notify_services_discovered(1);
  ppogatt_handle_service_removed(&s_characteristics[0][0], PPoGATTCharacteristicNum);
  const bool can_handle =
                 ppogatt_can_handle_characteristic(s_characteristics[0][PPoGATTCharacteristicData]);
  cl_assert_equal_b(can_handle, false);
}

void test_ppogatt__handle_subscribe_to_unknown_characteristic(void) {
  ppogatt_handle_subscribe(s_unknown_characteristics, BLESubscriptionNotifications,
                           BLEGATTErrorSuccess);

  // Expect to unsubscribe from the unknown characteristic:
  fake_gatt_client_subscriptions_assert_subscribe(s_unknown_characteristics, BLESubscriptionNone,
                                                  GAPLEClientKernel);
}

void test_ppogatt__cleanup_client_when_meta_read_fails(void) {
  fake_gatt_client_op_set_read_return_value(BTErrnoInvalidParameter);
  prv_notify_services_discovered(1);
  cl_assert_equal_i(ppogatt_client_count(), 0);
}

void test_ppogatt__cleanup_client_when_meta_read_gets_error_response(void) {
  fake_gatt_client_op_set_read_return_value(BTErrnoOK);
  prv_notify_services_discovered(1);
  ppogatt_handle_read_or_notification(s_characteristics[0][PPoGATTCharacteristicMeta],
                                      NULL, 0,
                                      BLEGATTErrorInvalidHandle);
  cl_assert_equal_i(ppogatt_client_count(), 0);
}

void test_ppogatt__cleanup_client_when_data_subscription_cccd_write_failed(void) {
  fake_gatt_client_subscriptions_set_subscribe_return_value(BTErrnoInvalidParameter);

  prv_notify_services_discovered(1);

  ppogatt_handle_read_or_notification(s_characteristics[0][PPoGATTCharacteristicMeta],
                                      (const uint8_t *) &s_meta_v0_system,
                                      sizeof(s_meta_v0_system),
                                      BLEGATTErrorSuccess);
  cl_assert_equal_i(ppogatt_client_count(), 0);
  cl_assert_equal_b(ppogatt_has_client_for_uuid(&s_meta_v0_system.app_uuid), false);
}

void test_ppogatt__cleanup_client_when_data_subscription_error_response(void) {
  prv_notify_services_discovered(1);

  ppogatt_handle_read_or_notification(s_characteristics[0][PPoGATTCharacteristicMeta],
                                      (const uint8_t *) &s_meta_v0_system,
                                      sizeof(s_meta_v0_system),
                                      BLEGATTErrorSuccess);
  // Expect subscribe request was made:
  fake_gatt_client_subscriptions_assert_subscribe(s_characteristics[0][PPoGATTCharacteristicData],
                                                  BLESubscriptionNotifications,
                                                  GAPLEClientKernel);
  // Simulate getting the subscription failure:
  ppogatt_handle_subscribe(s_characteristics[0][PPoGATTCharacteristicData],
                           BLESubscriptionNotifications, BLEGATTErrorReadNotPermitted);
  cl_assert_equal_i(ppogatt_client_count(), 0);
  cl_assert_equal_b(ppogatt_has_client_for_uuid(&s_meta_v0_system.app_uuid), false);
}

static void prv_discover_and_read_meta_and_reset(void) {
  prv_notify_services_discovered(1);

  ppogatt_handle_read_or_notification(s_characteristics[0][PPoGATTCharacteristicMeta],
                                      (const uint8_t *) &s_meta_v0_system,
                                      sizeof(s_meta_v0_system),
                                      BLEGATTErrorSuccess);
  // Expect subscribe request was made:
  fake_gatt_client_subscriptions_assert_subscribe(s_characteristics[0][PPoGATTCharacteristicData],
                                                  BLESubscriptionNotifications,
                                                  GAPLEClientKernel);
  // Simulate getting the subscription confirmation:
  ppogatt_handle_subscribe(s_characteristics[0][PPoGATTCharacteristicData],
                           BLESubscriptionNotifications, BLEGATTErrorSuccess);

  // Expect Reset to be initiated ("Reset Request" sent by FW):
  prv_assert_sent_reset_request(s_characteristics[0][PPoGATTCharacteristicData]);

  // Session should still no have opened yet:
  cl_assert_equal_i(fake_comm_session_open_call_count(), 0);
}

void test_ppogatt__open_session_when_found_pebble_app(void) {
  prv_discover_and_read_meta_and_reset();

  // Simulate getting "Reset Complete" from remote:
  prv_receive_reset_complete(s_characteristics[0][PPoGATTCharacteristicData]);

  // Expect "Reset Complete" to be sent by FW:
  prv_assert_sent_reset_complete(s_characteristics[0][PPoGATTCharacteristicData]);

  // Expect Session to be opened now:
  cl_assert_equal_i(fake_comm_session_open_call_count(), 1);
}

void test_ppogatt__start_reset_upon_out_of_range_ack(void) {
  test_ppogatt__open_session_when_found_pebble_app();
  // Simulate getting an Ack that's outside of the window of outstanding SNs:
  prv_receive_ack(s_characteristics[0][PPoGATTCharacteristicData], PPOGATT_SN_MOD_DIV / 2);
  // Expect Reset to be initiated ("Reset Request" sent by FW):
  prv_assert_sent_reset_request(s_characteristics[0][PPoGATTCharacteristicData]);
}

void test_ppogatt__ignore_retransmitted_ack(void) {
  test_ppogatt__open_session_when_found_pebble_app();

  Transport *transport = ppogatt_client_for_uuid(&s_meta_v0_system.app_uuid);
  for (uint8_t sn = 0; sn < 3; ++sn) {
    const bool success =
        fake_comm_session_send_buffer_write_raw_by_transport(transport,
                                                             s_short_data_fragment,
                                                             sizeof(s_short_data_fragment));
    cl_assert_equal_b(success, true);
    ppogatt_send_next(transport);
    prv_assert_sent_data(s_characteristics[0][PPoGATTCharacteristicData], sn,
                         s_short_data_fragment, sizeof(s_short_data_fragment));
  }

  // Receive ACK for first data packet with sn=0:
  prv_receive_ack(s_characteristics[0][PPoGATTCharacteristicData], 0);

  // Pretend data packets with sn=1 got lost in the ether, but data sn=2 was received...

  // Receive a retransmit for the ACK sn=0, to indicate data was missing.
  prv_receive_ack(s_characteristics[0][PPoGATTCharacteristicData], 0);

  // The retransmitted ACK should be ignored.
  fake_gatt_client_op_assert_no_write();

  // Session shouldn't get closed:
  cl_assert_equal_i(fake_comm_session_close_call_count(), 0);
}

void test_ppogatt__ignore_server_reset_request_while_resetting_due_to_server_reset_request(void) {
  test_ppogatt__open_session_when_found_pebble_app();

  prv_receive_reset_request(s_characteristics[0][PPoGATTCharacteristicData]);
  prv_assert_sent_reset_complete(s_characteristics[0][PPoGATTCharacteristicData]);

  prv_receive_reset_request(s_characteristics[0][PPoGATTCharacteristicData]);
  fake_gatt_client_op_assert_no_write();
}

void test_ppogatt__ignore_server_reset_request_while_resetting_due_to_own_reset_request(void) {
  prv_notify_services_discovered(1);

  ppogatt_handle_read_or_notification(s_characteristics[0][PPoGATTCharacteristicMeta],
                                      (const uint8_t *) &s_meta_v0_system,
                                      sizeof(s_meta_v0_system),
                                      BLEGATTErrorSuccess);
  ppogatt_handle_subscribe(s_characteristics[0][PPoGATTCharacteristicData],
                           BLESubscriptionNotifications, BLEGATTErrorSuccess);

  // Expect Reset to be initiated ("Reset Request" sent by FW):
  prv_assert_sent_reset_request(s_characteristics[0][PPoGATTCharacteristicData]);

  prv_receive_reset_request(s_characteristics[0][PPoGATTCharacteristicData]);
  fake_gatt_client_op_assert_no_write();
}

void test_ppogatt__timeout_waiting_for_reset_complete_remote_initiated(void) {
  test_ppogatt__open_session_when_found_pebble_app();

  prv_receive_reset_request(s_characteristics[0][PPoGATTCharacteristicData]);
  prv_assert_sent_reset_complete(s_characteristics[0][PPoGATTCharacteristicData]);

  // Timeout waiting for "Reset Complete":
  for (int i = 0; i < PPOGATT_TIMEOUT_TICKS; ++i) {
    regular_timer_fire_seconds(PPOGATT_TIMEOUT_TICK_INTERVAL_SECS);
  }

  // Expect "Reset Request" sent by FW:
  prv_assert_sent_reset_request(s_characteristics[0][PPoGATTCharacteristicData]);
}

void test_ppogatt__timeout_waiting_for_reset_complete_self_initiated(void) {
  prv_discover_and_read_meta_and_reset();

  // Timeout waiting for "Reset Complete":
  for (int i = 0; i < PPOGATT_TIMEOUT_TICKS; ++i) {
    regular_timer_fire_seconds(PPOGATT_TIMEOUT_TICK_INTERVAL_SECS);
  }

  // Expect "Reset Request" sent by FW:
  prv_assert_sent_reset_request(s_characteristics[0][PPoGATTCharacteristicData]);
}

void test_ppogatt__server_reset_request_while_pending_ack(void) {
  test_ppogatt__open_session_when_found_pebble_app();

  // Simulate outbound queue full, so ack will have to wait until there's buffer space:
  fake_gatt_client_op_set_write_return_value(BTErrnoNotEnoughResources);
  // Receive data (that needs to be ack'd):
  uint8_t sn = 0;
  prv_receive_short_data_fragment(s_characteristics[0][PPoGATTCharacteristicData], sn);
  fake_gatt_client_op_assert_no_write();

  // Receive Reset Request:
  prv_receive_reset_request(s_characteristics[0][PPoGATTCharacteristicData]);
  fake_gatt_client_op_assert_no_write();

  // Simulate outbound queue having space again:
  fake_gatt_client_op_set_write_return_value(BTErrnoOK);
  ppogatt_handle_buffer_empty();

  // Expect Reset Complete to be sent out, but nothing more than that:
  prv_assert_sent_reset_complete(s_characteristics[0][PPoGATTCharacteristicData]);
  fake_gatt_client_op_assert_no_write();

  // In the past we had a bug here where the pending ACK would get sent out.
  // See https://pebbletechnology.atlassian.net/browse/PBL-24651
}

void test_ppogatt__ignore_invalid_packet_type(void) {
  test_ppogatt__open_session_when_found_pebble_app();
  PPoGATTPacket packet = {
    .sn = 0,
    .type = PPoGATTPacketTypeInvalidRangeStart,
  };
  ppogatt_handle_read_or_notification(s_characteristics[0][PPoGATTCharacteristicData],
                                      (const uint8_t *) &packet, sizeof(packet),
                                      BLEGATTErrorSuccess);
  // No crash etc, client still alive:
  cl_assert_equal_i(ppogatt_client_count(), 1);
  cl_assert_equal_b(ppogatt_has_client_for_uuid(&s_meta_v0_system.app_uuid), true);
  cl_assert_equal_i(fake_comm_session_close_call_count(), 0);
}

void test_ppogatt__ignore_reset_complete_while_open(void) {
  test_ppogatt__open_session_when_found_pebble_app();
  // Simulate getting "Reset Complete" from remote:
  prv_receive_reset_complete(s_characteristics[0][PPoGATTCharacteristicData]);
  // No crash etc, client still alive:
  cl_assert_equal_i(ppogatt_client_count(), 1);
  cl_assert_equal_b(ppogatt_has_client_for_uuid(&s_meta_v0_system.app_uuid), true);
  cl_assert_equal_i(fake_comm_session_close_call_count(), 0);
}

void test_ppogatt__ignore_data_during_reset(void) {
  prv_notify_services_discovered(1);

  ppogatt_handle_read_or_notification(s_characteristics[0][PPoGATTCharacteristicMeta],
                                      (const uint8_t *) &s_meta_v0_system,
                                      sizeof(s_meta_v0_system),
                                      BLEGATTErrorSuccess);
  ppogatt_handle_subscribe(s_characteristics[0][PPoGATTCharacteristicData],
                           BLESubscriptionNotifications, BLEGATTErrorSuccess);

  // Expect Reset to be initiated ("Reset Request" sent by FW):
  prv_assert_sent_reset_request(s_characteristics[0][PPoGATTCharacteristicData]);

  // Receive data:
  prv_receive_short_data_fragment(s_characteristics[0][PPoGATTCharacteristicData], 3 /* sn */);

  // Simulate getting "Reset Complete" from remote:
  prv_receive_reset_complete(s_characteristics[0][PPoGATTCharacteristicData]);

  // Expect "Reset Complete" to be sent by FW:
  prv_assert_sent_reset_complete(s_characteristics[0][PPoGATTCharacteristicData]);

  // Expect Session to be opened now:
  cl_assert_equal_i(fake_comm_session_open_call_count(), 1);
}

void test_ppogatt__ignore_zero_length_notification(void) {
  test_ppogatt__open_session_when_found_pebble_app();
  ppogatt_handle_read_or_notification(s_characteristics[0][PPoGATTCharacteristicData], NULL, 0,
                                      BLEGATTErrorSuccess);
  // No crash etc, client still alive:
  cl_assert_equal_i(ppogatt_client_count(), 1);
  cl_assert_equal_b(ppogatt_has_client_for_uuid(&s_meta_v0_system.app_uuid), true);
  cl_assert_equal_i(fake_comm_session_close_call_count(), 0);
}

void test_ppogatt__ack_received_data(void) {
  test_ppogatt__open_session_when_found_pebble_app();

  // Receive data:
  for (uint8_t i = 0; i < PPOGATT_SN_MOD_DIV + 1; ++i) {
    const uint8_t sn = i % PPOGATT_SN_MOD_DIV;
    ppogatt_trigger_rx_ack_send_timeout();
    prv_receive_short_data_fragment(s_characteristics[0][PPoGATTCharacteristicData], sn);
    prv_assert_sent_ack(s_characteristics[0][PPoGATTCharacteristicData], sn);
  }
}

void test_ppogatt__close(void) {
  test_ppogatt__open_session_when_found_pebble_app();

  Transport *client = ppogatt_client_for_uuid(&s_meta_v0_system.app_uuid);
  cl_assert(client);

  ppogatt_close(client);

  cl_assert_equal_p(NULL, ppogatt_client_for_uuid(&s_meta_v0_system.app_uuid));
}

void test_ppogatt__missing_inbound_packet(void) {
  test_ppogatt__open_session_when_found_pebble_app();

  // Receive data:
  prv_receive_short_data_fragment(s_characteristics[0][PPoGATTCharacteristicData],
                                  1 /* sn (expecting sn=0) */);
  // Expect nothing to be sent, rely on other end to hit time-out and retransmit
  fake_gatt_client_op_assert_no_write();
}

void test_ppogatt__send_data_max_payload_size(void) {
  test_ppogatt__open_session_when_found_pebble_app();
  uint8_t *data = malloc(MAX_PAYLOAD_SIZE);
  memset(data, MAX_PAYLOAD_SIZE, 0x55);

  Transport *transport = ppogatt_client_for_uuid(&s_meta_v0_system.app_uuid);
  for (uint8_t sn = 0; sn < s_tx_window_size; ++sn) {
    cl_assert_equal_b(fake_comm_session_send_buffer_write_raw_by_transport(transport, data,
                                                                           MAX_PAYLOAD_SIZE), true);
    ppogatt_send_next(transport);
    prv_assert_sent_data(s_characteristics[0][PPoGATTCharacteristicData], sn,
                         data, MAX_PAYLOAD_SIZE);
  }

 free(data);
}

void test_ppogatt__cap_number_of_data_packets_in_flight(void) {
  test_ppogatt__open_session_when_found_pebble_app();

  uint8_t sn = 0;
  Transport *transport = ppogatt_client_for_uuid(&s_meta_v0_system.app_uuid);

  // Get s_rx_window_size packets in flight:
  for (sn = 0; sn < s_tx_window_size; ++sn) {
    const bool success = fake_comm_session_send_buffer_write_raw_by_transport(transport,
                                                                    s_short_data_fragment,
                                                                    sizeof(s_short_data_fragment));
    printf("SEND %d %d\n", sn, success);
    cl_assert_equal_b(success, true);
    ppogatt_send_next(transport);
    prv_assert_sent_data(s_characteristics[0][PPoGATTCharacteristicData], sn,
                         s_short_data_fragment, sizeof(s_short_data_fragment));
  }

  printf("done\n");

  // Enqueue another:
  cl_assert_equal_b(fake_comm_session_send_buffer_write_raw_by_transport(transport,
                                                             s_short_data_fragment,
                                                             sizeof(s_short_data_fragment)), true);
  ppogatt_send_next(transport);
  fake_gatt_client_op_assert_no_write();

  // Ack the first one (sn=0):
  prv_receive_ack(s_characteristics[0][PPoGATTCharacteristicData], 0 /* sn */);

  // The last enqueued one should now be sent out:
  prv_assert_sent_data(s_characteristics[0][PPoGATTCharacteristicData], sn,
                       s_short_data_fragment, sizeof(s_short_data_fragment));
}

void test_ppogatt__receive_ack_for_all_packets_in_flight(void) {
  test_ppogatt__open_session_when_found_pebble_app();
  uint8_t sn = 0;
  Transport *transport = ppogatt_client_for_uuid(&s_meta_v0_system.app_uuid);

  // Get s_tx_window_size packets in flight:
  for (sn = 0; sn < s_tx_window_size; ++sn) {
    cl_assert_equal_b(fake_comm_session_send_buffer_write_raw_by_transport(transport,
                                                             s_short_data_fragment,
                                                             sizeof(s_short_data_fragment)), true);
    ppogatt_send_next(transport);
    prv_assert_sent_data(s_characteristics[0][PPoGATTCharacteristicData], sn,
                         s_short_data_fragment, sizeof(s_short_data_fragment));
  }

  // Ack the last one (sn == s_tx_window_size - 1), which will be interpreted as Ack'ing all
  // the packets before it too:
  prv_receive_ack(s_characteristics[0][PPoGATTCharacteristicData],
                  s_tx_window_size - 1 /* sn */);

  // We should now be able to submit s_tx_window_size packets again:
  for (sn = s_tx_window_size; sn < 2 * s_tx_window_size; ++sn) {
    cl_assert_equal_b(fake_comm_session_send_buffer_write_raw_by_transport(transport,
                                                             s_short_data_fragment,
                                                             sizeof(s_short_data_fragment)), true);
    ppogatt_send_next(transport);
    prv_assert_sent_data(s_characteristics[0][PPoGATTCharacteristicData], sn,
                         s_short_data_fragment, sizeof(s_short_data_fragment));
  }
}

void test_ppogatt__handle_client_disappearing_for_send_callback(void) {
  // ppogatt_send_next() is called from the KernelBG task sometimes.
  // It's possible that the pointer is dangling by the time the callback executes.
  // Therefore ppogatt_send_next() should be able to handle this dangling pointer gracefully.
  uint8_t fake_client = 0;
  ppogatt_send_next((struct Transport *) &fake_client);
  // No crashes, no writes, etc.
  fake_gatt_client_op_assert_no_write();
}

void test_ppogatt__handle_bluetooth_stack_queue_full_and_empty_events(void) {
  test_ppogatt__open_session_when_found_pebble_app();
  fake_gatt_client_op_set_write_return_value(BTErrnoNotEnoughResources);

  Transport *transport = ppogatt_client_for_uuid(&s_meta_v0_system.app_uuid);
  cl_assert_equal_b(fake_comm_session_send_buffer_write_raw_by_transport(transport,
                                                             s_short_data_fragment,
                                                             sizeof(s_short_data_fragment)), true);
  ppogatt_send_next(transport);
  fake_gatt_client_op_assert_no_write();

  fake_gatt_client_op_set_write_return_value(BTErrnoOK);
  ppogatt_handle_buffer_empty();
  prv_assert_sent_data(s_characteristics[0][PPoGATTCharacteristicData], 0 /* sn */,
                       s_short_data_fragment, sizeof(s_short_data_fragment));
}

void test_ppogatt__retransmit_timed_out_data_packets_all_at_once(void) {
  test_ppogatt__open_session_when_found_pebble_app();
  uint8_t sn = 0;
  Transport *transport = ppogatt_client_for_uuid(&s_meta_v0_system.app_uuid);

  // Get s_tx_window_size packets in flight:
  for (sn = 0; sn < s_tx_window_size; ++sn) {
    cl_assert_equal_b(fake_comm_session_send_buffer_write_raw_by_transport(transport,
                                                         s_short_data_fragment,
                                                         sizeof(s_short_data_fragment) - sn), true);
    ppogatt_send_next(transport);
    prv_assert_sent_data(s_characteristics[0][PPoGATTCharacteristicData], sn,
                         s_short_data_fragment, sizeof(s_short_data_fragment) - sn);
  }

  // Simulate the regular timer firing a bunch of times to expire the timeout for all the packets:
  for (int i = 0; i < PPOGATT_TIMEOUT_TICKS; ++i) {
    regular_timer_fire_seconds(PPOGATT_TIMEOUT_TICK_INTERVAL_SECS);
  }

  fake_comm_session_process_send_next();

  // The data should *NOT* get concatenated in a single packet, even though it might fit. The
  // fragmentation should be the same as the previous transmission pass, because there is a race
  // condition where there are Ack(s) in flight for the "original" data packets. Because we're
  // using the same SNs, we cannot change the fragmentation, because we cannot know whether they
  // would refer to the old or new fragmentation.

  for (sn = 0; sn < s_tx_window_size; ++sn) {
    prv_assert_sent_data(s_characteristics[0][PPoGATTCharacteristicData], sn /* sn */,
                         s_short_data_fragment, sizeof(s_short_data_fragment) - sn);
  }
}

void test_ppogatt__retransmit_timed_out_data_packets_first_but_not_later_ones(void) {
  test_ppogatt__open_session_when_found_pebble_app();
  uint8_t sn = 0;
  Transport *transport = ppogatt_client_for_uuid(&s_meta_v0_system.app_uuid);

  // Get s_tx_window_size packets in flight:
  uint8_t secs_passed = 0;
  for (sn = 0; sn < s_tx_window_size; ++sn) {
    cl_assert_equal_b(fake_comm_session_send_buffer_write_raw_by_transport(transport,
                                                         s_short_data_fragment,
                                                         sizeof(s_short_data_fragment) - sn), true);
    ppogatt_send_next(transport);
    prv_assert_sent_data(s_characteristics[0][PPoGATTCharacteristicData], sn,
                         s_short_data_fragment, sizeof(s_short_data_fragment) - sn);
    if (sn == 0 || sn == 1) {
      // Make the first and second packet time out each, one second earlier
      // than the 3rd and 4rd packets:
      regular_timer_fire_seconds(PPOGATT_TIMEOUT_TICK_INTERVAL_SECS);
      ++secs_passed;
    }
    cl_assert(secs_passed < PPOGATT_TIMEOUT_TICKS);
  }

  // Simulate the regular timer firing a bunch of times to expire the timeout for the in-flight packets
  // This will trigger a retransmit of the un-acked packets
  for (int i = 0; i < PPOGATT_TIMEOUT_TICKS - 1; ++i) {
    regular_timer_fire_seconds(PPOGATT_TIMEOUT_TICK_INTERVAL_SECS);
  }

  fake_comm_session_process_send_next();

  for (sn = 0; sn < s_tx_window_size; ++sn) {
    prv_assert_sent_data(s_characteristics[0][PPoGATTCharacteristicData], sn /* sn */,
                         s_short_data_fragment, sizeof(s_short_data_fragment) - sn);
  }
}

void test_ppogatt__retransmit_timed_out_data_packets_race_everything_acked_at_once(void) {
  test_ppogatt__open_session_when_found_pebble_app();
  Transport *transport = ppogatt_client_for_uuid(&s_meta_v0_system.app_uuid);

  // Get s_tx_window_size packets in flight:
  uint8_t sn = 0;
  for (; sn < s_tx_window_size; ++sn) {
    cl_assert_equal_b(fake_comm_session_send_buffer_write_raw_by_transport(transport,
                                                         s_short_data_fragment,
                                                         sizeof(s_short_data_fragment) - sn), true);
    ppogatt_send_next(transport);
    prv_assert_sent_data(s_characteristics[0][PPoGATTCharacteristicData], sn,
                         s_short_data_fragment, sizeof(s_short_data_fragment) - sn);
  }

  // Time-out all packets in flight, rolling back for retransmission:
  for (int i = 0; i < PPOGATT_TIMEOUT_TICKS; ++i) {
    regular_timer_fire_seconds(PPOGATT_TIMEOUT_TICK_INTERVAL_SECS);
  }

  // Simulate receiving an ack for the last, after the roll-back, but before the packets are
  // retransmitted (the last part shouldn't matter much, but simplifies the test a bit)
  prv_receive_ack(s_characteristics[0][PPoGATTCharacteristicData],
                  (sn - 1) % PPOGATT_SN_MOD_DIV);

  // Some new data has been queued up in the mean time:
  cl_assert_equal_b(fake_comm_session_send_buffer_write_raw_by_transport(transport,
                                                         s_short_data_fragment,
                                                         sizeof(s_short_data_fragment) - sn), true);
  // Only now the system task callback is fired (prv_send_next_packets_async):
  fake_comm_session_process_send_next();

  // Expect the new data to come through, no retransmissions at all.
  // (They all got considered Ack'd by the one Ack)
  prv_assert_sent_data(s_characteristics[0][PPoGATTCharacteristicData], sn /* sn */,
                       s_short_data_fragment, sizeof(s_short_data_fragment) - sn);
}

void test_ppogatt__retransmit_max_number_of_times(void) {
  test_ppogatt__open_session_when_found_pebble_app();
  Transport *transport = ppogatt_client_for_uuid(&s_meta_v0_system.app_uuid);

  // Get a packet in flight:
  uint8_t sn = 0;
  cl_assert_equal_b(fake_comm_session_send_buffer_write_raw_by_transport(transport,
                                                         s_short_data_fragment,
                                                         sizeof(s_short_data_fragment) - sn), true);
  ppogatt_send_next(transport);
  prv_assert_sent_data(s_characteristics[0][PPoGATTCharacteristicData], sn,
                       s_short_data_fragment, sizeof(s_short_data_fragment) - sn);

  for (int j = 0; j < PPOGATT_TIMEOUT_COUNT_MAX - 1; ++j) {
    // Time-out the packet over and over until (max - 1) is reached:
    for (int i = 0; i < PPOGATT_TIMEOUT_TICKS; ++i) {
      regular_timer_fire_seconds(PPOGATT_TIMEOUT_TICK_INTERVAL_SECS);
    }
    fake_comm_session_process_send_next();
    prv_assert_sent_data(s_characteristics[0][PPoGATTCharacteristicData], sn,
                         s_short_data_fragment, sizeof(s_short_data_fragment) - sn);
  }

  // The last straw:
  for (int i = 0; i < PPOGATT_TIMEOUT_TICKS; ++i) {
    regular_timer_fire_seconds(PPOGATT_TIMEOUT_TICK_INTERVAL_SECS);
  }
  prv_assert_sent_reset_request(s_characteristics[0][PPoGATTCharacteristicData]);
}

void test_ppogatt__make_sure_timeout_reset_after_data_ack(void) {
  test_ppogatt__open_session_when_found_pebble_app();
  Transport *transport = ppogatt_client_for_uuid(&s_meta_v0_system.app_uuid);

  uint8_t num_packets = s_tx_window_size;

  // Get a packet in flight:
  for (int sn = 0; sn < num_packets; sn++) {
    cl_assert_equal_b(
        fake_comm_session_send_buffer_write_raw_by_transport(
        transport, s_short_data_fragment, sizeof(s_short_data_fragment) - sn), true);
    ppogatt_send_next(transport);
  }

  for (int sn = 0; sn < num_packets; sn++) {
    for (int i = 0; i < (PPOGATT_TIMEOUT_TICKS - 1); ++i) {
      regular_timer_fire_seconds(PPOGATT_TIMEOUT_TICK_INTERVAL_SECS);
    }

    prv_receive_ack(s_characteristics[0][PPoGATTCharacteristicData], sn /* sn */);
  }

  fake_comm_session_process_send_next();

  for (int sn = 0; sn < num_packets; sn++) {
    prv_assert_sent_data(s_characteristics[0][PPoGATTCharacteristicData], sn,
                         s_short_data_fragment, sizeof(s_short_data_fragment) - sn);
  }

  // There should be no writes we haven't already checked for. That would only happen if we timed
  // out!
  fake_gatt_client_op_assert_no_write();
}

void test_ppogatt__mtu_zero_due_to_disconnection(void) {
  test_ppogatt__open_session_when_found_pebble_app();
  Transport *transport = ppogatt_client_for_uuid(&s_meta_v0_system.app_uuid);

  // Get a packet in flight:
  uint8_t sn = 0;
  cl_assert_equal_b(fake_comm_session_send_buffer_write_raw_by_transport(transport,
                                                         s_short_data_fragment,
                                                         sizeof(s_short_data_fragment) - sn), true);
  fake_malloc_set_largest_free_block(1000);
  s_mtu_size = 0;
  ppogatt_send_next(transport);
  // No crash
}

//! When client ID info got added to the Reset Packet (PBL-14099), a potential buffer overrun
//! situation got introduced accidentally. This test is a white-box test to catch this issue.
//! For the Reset Packet, a buffer needs to be allocated. The size of this buffer is based upon
//! the MTU of the connection. It's possible the lookup fails and returns 0. In this case, the
//! packet shouldn't be attempted to be written at all, because it will not fit and overrun the
//! buffer.
void test_ppogatt__mtu_zero_due_to_service_rediscovery_while_resetting(void) {
  ppogatt_handle_service_discovered(s_characteristics[0]);

  ppogatt_handle_read_or_notification(s_characteristics[0][PPoGATTCharacteristicMeta],
                                      (const uint8_t *) &s_meta_v0_system,
                                      sizeof(s_meta_v0_system),
                                      BLEGATTErrorSuccess);
  // Expect subscribe request was made:
  fake_gatt_client_subscriptions_assert_subscribe(s_characteristics[0][PPoGATTCharacteristicData],
                                                  BLESubscriptionNotifications,
                                                  GAPLEClientKernel);

  // During service re-discovery the cached characteristic handles will be stale for a brief period.
  // This will cause the gatt_client_characteristic_get_device to return BT_DEVICE_INTERNAL_INVALID
  // and eventually gap_le_connection_get_gatt_mtu call to return 0. See PBL-22038.
  s_mtu_size = 0;

  // Simulate getting the subscription confirmation, this will normally trigger PPoGATT to try to
  // write out the Reset packet, but because the MTU is couldn't be looked up, no packet should get
  // sent out:
  ppogatt_handle_subscribe(s_characteristics[0][PPoGATTCharacteristicData],
                           BLESubscriptionNotifications, BLEGATTErrorSuccess);

  // Expect nothing to be sent out by FW:
  fake_gatt_client_op_assert_no_write();

  // No crash nor DUMA failures
}

void test_ppogatt__unsubcribe_when_no_memory_for_comm_session(void) {
  // TODO
}
