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

#include "services/normal/bluetooth/ble_hrm.h"

#include "comm/ble/gap_le_connection.h"
#include "services/common/hrm/hrm_manager_private.h"

#include <bluetooth/hrm_service.h>
#include <btutil/bt_device.h>
#include <util/size.h>

#include <clar.h>


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

#include "fake_event_service.h"
#include "fake_pebble_tasks.h"
#include "fake_pbl_malloc.h"
#include "fake_regular_timer.h"

#include "stubs_analytics.h"
#include "stubs_bt_lock.h"
#include "stubs_logging.h"
#include "stubs_passert.h"

void gap_le_slave_reconnect_hrm_restart(void) {
}

void gap_le_slave_reconnect_hrm_stop(void) {
}

static bool s_activity_prefs_heart_rate_is_enabled;
bool activity_prefs_heart_rate_is_enabled(void) {
  return s_activity_prefs_heart_rate_is_enabled;
}

static bool s_bt_driver_hrm_service_is_enabled;
static int s_bt_driver_hrm_service_enable_call_count;
void bt_driver_hrm_service_enable(bool enable) {
  s_bt_driver_hrm_service_enable_call_count++;
  s_bt_driver_hrm_service_is_enabled = enable;
}

static BleHrmServiceMeasurement s_last_ble_hrm_measurement;
static int s_bt_driver_hrm_service_handle_measurement_call_count;
static BTDeviceInternal s_last_permitted_devices[10];
static size_t s_last_num_permitted_devices;
void bt_driver_hrm_service_handle_measurement(const BleHrmServiceMeasurement *measurement,
                                              const BTDeviceInternal *permitted_devices,
                                              size_t num_permitted_devices) {
  ++s_bt_driver_hrm_service_handle_measurement_call_count;
  s_last_ble_hrm_measurement = *measurement;
  s_last_num_permitted_devices = num_permitted_devices;
  memcpy(s_last_permitted_devices, permitted_devices,
         sizeof(*permitted_devices) * num_permitted_devices);
}

static BLEHRMSharingRequest *s_last_sharing_request;
static int s_ble_hrm_push_sharing_request_window_call_count;
void ble_hrm_push_sharing_request_window(BLEHRMSharingRequest *sharing_request) {
  ++s_ble_hrm_push_sharing_request_window_call_count;
  cl_assert_equal_p(s_last_sharing_request, NULL);
  s_last_sharing_request = sharing_request;
}

bool bt_driver_is_hrm_service_supported(void) {
  return true;
}

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

static void prv_assert_last_disconnected(const BTDeviceInternal *peer_address) {
  cl_assert_equal_b(bt_device_internal_equal(peer_address, &s_last_disconnected), true);
}

static int s_ble_hrm_push_reminder_popup_call_count;
void ble_hrm_push_reminder_popup(void) {
  s_ble_hrm_push_reminder_popup_call_count++;
}

static int s_hrm_manager_subscribe_with_callback_call_count;
static HRMSessionRef s_last_session_ref;
static HRMSessionRef s_next_session_ref;
HRMSessionRef hrm_manager_subscribe_with_callback(AppInstallId app_id, uint32_t update_interval_s,
                                                  uint16_t expire_s, HRMFeature features,
                                                  HRMSubscriberCallback callback, void *context) {
  cl_assert_equal_p(NULL, callback); // we're using the event service
  cl_assert_equal_i(features, HRMFeature_BPM);
  ++s_hrm_manager_subscribe_with_callback_call_count;
  s_last_session_ref = ++s_next_session_ref;
  return s_last_session_ref;
}

static GAPLEConnection *s_connections[2];

GAPLEConnection *gap_le_connection_by_device(const BTDeviceInternal *device) {
  for (int i = 0; i < ARRAY_LENGTH(s_connections); ++i) {
    if (bt_device_internal_equal(device, &s_connections[i]->device)) {
      return s_connections[i];
    }
  }
  return NULL;
}
BTDeviceInternal *device_from_le_connection(GAPLEConnection *conn) {
  return &conn->device;
}

bool gap_le_connection_is_valid(const GAPLEConnection *conn) {
  for (int i = 0; i < ARRAY_LENGTH(s_connections); ++i) {
    if (s_connections[i] == conn) {
      return true;
    }
  }
  return false;
}

void gap_le_connection_for_each(GAPLEConnectionForEachCallback cb, void *data) {
  for (int i = 0; i < ARRAY_LENGTH(s_connections); ++i) {
    cb(s_connections[i], data);
  }
}

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

bool sys_hrm_manager_is_hrm_present(void) {
  return true;
}

static int s_sys_hrm_manager_unsubscribe_call_count;
bool sys_hrm_manager_unsubscribe(HRMSessionRef session) {
  ++s_sys_hrm_manager_unsubscribe_call_count;
  cl_assert_equal_i(session, s_last_session_ref);
  return true;
}

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

static void prv_assert_event_service_subscribed(bool is_subscribed) {
  const EventServiceInfo *const info = fake_event_service_get_info(PEBBLE_HRM_EVENT);
  if (is_subscribed) {
    cl_assert(info->handler);
  } else {
    cl_assert_equal_p(NULL, info->handler);
  }
}

void test_ble_hrm__cleanup(void) {
  ble_hrm_deinit();

  prv_assert_event_service_subscribed(false);
  // hrm manager sub vs unsub calls should be the same, there should be no subscription any more
  // after de-initing:
  cl_assert_equal_i(s_sys_hrm_manager_unsubscribe_call_count,
                    s_hrm_manager_subscribe_with_callback_call_count);

  fake_pbl_malloc_check_net_allocs();

  // Assert all regular timers are deregistered:
  cl_assert_equal_p(s_seconds_callbacks.next, NULL);
  cl_assert_equal_p(s_minutes_callbacks.next, NULL);
}

#define TEST_DEVICE_NAME "iPhone Martijn"

static GAPLEConnection s_conn_a;
static GAPLEConnection s_conn_b;
static const BTDeviceInternal *s_device_a;
static const BTDeviceInternal *s_device_b;

void test_ble_hrm__initialize(void) {
  fake_pbl_malloc_clear_tracking();
  for (int i = 0; i < ARRAY_LENGTH(s_connections); ++i) {
    s_connections[i] = NULL;
  }
  s_activity_prefs_heart_rate_is_enabled = true;
  s_bt_driver_hrm_service_is_enabled = true;
  s_last_num_permitted_devices = 0;
  memset(s_last_permitted_devices, 0, sizeof(s_last_permitted_devices));
  s_bt_driver_hrm_service_enable_call_count = 0;
  s_hrm_manager_subscribe_with_callback_call_count = 0;
  s_sys_hrm_manager_unsubscribe_call_count = 0;
  s_bt_driver_hrm_service_handle_measurement_call_count = 0;
  s_ble_hrm_push_sharing_request_window_call_count = 0;
  s_ble_hrm_push_reminder_popup_call_count = 0;
  s_last_session_ref = ~0;
  s_next_session_ref = 1234;
  s_last_disconnected = (BTDeviceInternal) {};
  s_last_sharing_request = NULL;
  s_last_ble_hrm_measurement = (BleHrmServiceMeasurement) {};
  fake_event_service_init();

  // Set up fake devices/connections:
  s_conn_a = (GAPLEConnection) {
    .device_name = TEST_DEVICE_NAME,
    .device = {
      .address = {
        .octets = {1, 2, 3, 4, 5, 6},
      },
    },
  };
  s_conn_b = (GAPLEConnection) {
    .device_name = TEST_DEVICE_NAME,
    .device = {
      .address = {
        .octets = {6, 5, 4, 3, 2, 1},
      },
    },
  };
  s_connections[0] = &s_conn_a;
  s_connections[1] = &s_conn_b;
  s_device_a = device_from_le_connection(&s_conn_a);
  s_device_b = device_from_le_connection(&s_conn_b);

  ble_hrm_init();
}

void test_ble_hrm__init_deinit_no_subscriptions(void) {
  // let cleanup & initialize do the work :)
}

static void prv_assert_permissions_ui_and_respond(bool is_granted) {
  cl_assert(s_last_sharing_request);
  ble_hrm_handle_sharing_request_response(is_granted, s_last_sharing_request);
  s_last_sharing_request = NULL;
}

void test_ble_hrm__sub_unsub(void) {
  cl_assert_equal_i(s_hrm_manager_subscribe_with_callback_call_count, 0);
  cl_assert_equal_i(s_sys_hrm_manager_unsubscribe_call_count, 0);
  prv_assert_event_service_subscribed(false);

  // Device A subscribes:
  bt_driver_cb_hrm_service_update_subscription(s_device_a, true);

  // Expect HRM manager NOT to be subscribed to yet, need to grant permission first:
  cl_assert_equal_i(s_hrm_manager_subscribe_with_callback_call_count, 0);
  prv_assert_event_service_subscribed(false);
  cl_assert_equal_b(false, ble_hrm_is_sharing_to_connection(&s_conn_a));

  // Expect permissions UI to be presented:
  prv_assert_permissions_ui_and_respond(true /* is_granted */);

  // Expect HRM manager to be subscribed to:
  cl_assert_equal_i(s_hrm_manager_subscribe_with_callback_call_count, 1);
  prv_assert_event_service_subscribed(true);
  cl_assert_equal_b(true, ble_hrm_is_sharing_to_connection(&s_conn_a));

  // Device A subscribes again, should be a no-op, no new permissions prompt:
  bt_driver_cb_hrm_service_update_subscription(s_device_a, true);
  cl_assert_equal_i(s_hrm_manager_subscribe_with_callback_call_count, 1);
  cl_assert_equal_i(s_sys_hrm_manager_unsubscribe_call_count, 0);

  // Device B subscribes, shouldn't resubscribe to HRM manager, but should present a new
  // permission prompt, because it's a different device:
  bt_driver_cb_hrm_service_update_subscription(s_device_b, true);
  cl_assert_equal_b(false, ble_hrm_is_sharing_to_connection(&s_conn_b));
  prv_assert_permissions_ui_and_respond(true /* is_granted */);
  cl_assert_equal_b(true, ble_hrm_is_sharing_to_connection(&s_conn_b));
  prv_assert_event_service_subscribed(true);
  cl_assert_equal_i(s_hrm_manager_subscribe_with_callback_call_count, 1);
  cl_assert_equal_i(s_sys_hrm_manager_unsubscribe_call_count, 0);

  // Device A disconnects, shouldn't unsubscribe from HRM manager because A is still subscribed:
  ble_hrm_handle_disconnection(&s_conn_a);
  cl_assert_equal_b(false, ble_hrm_is_sharing_to_connection(&s_conn_a));
  prv_assert_event_service_subscribed(true);
  cl_assert_equal_i(s_hrm_manager_subscribe_with_callback_call_count, 1);
  cl_assert_equal_i(s_sys_hrm_manager_unsubscribe_call_count, 0);

  // Device B unsubscribes, expect to be unsubscribed from HRM manager, because there are no more
  // devices subscribed to the BLE HRM service:
  bt_driver_cb_hrm_service_update_subscription(s_device_b, false);
  cl_assert_equal_b(false, ble_hrm_is_sharing_to_connection(&s_conn_a));
  prv_assert_event_service_subscribed(false);
  cl_assert_equal_i(s_sys_hrm_manager_unsubscribe_call_count, 1);

  // Device B unsubscribes again, should be no-op
  bt_driver_cb_hrm_service_update_subscription(s_device_b, false);
  prv_assert_event_service_subscribed(false);
  cl_assert_equal_i(s_sys_hrm_manager_unsubscribe_call_count, 1);
}

void test_ble_hrm__sub_unsub_resub(void) {
  // Device A subscribes:
  bt_driver_cb_hrm_service_update_subscription(s_device_a, true);

  // Expect permissions UI to be presented:
  prv_assert_permissions_ui_and_respond(true /* is_granted */);

  // Device A unsubscribes:
  bt_driver_cb_hrm_service_update_subscription(s_device_a, false);

  prv_assert_event_service_subscribed(false);

  // Device A re-subscribes, permission should still be valid:
  bt_driver_cb_hrm_service_update_subscription(s_device_a, true);

  prv_assert_event_service_subscribed(true);
}

void test_ble_hrm__revoke(void) {
  // Device A subscribes:
  bt_driver_cb_hrm_service_update_subscription(s_device_a, true);

  // Expect permissions UI to be presented:
  prv_assert_permissions_ui_and_respond(true /* is_granted */);

  cl_assert_equal_b(true, ble_hrm_is_sharing_to_connection(&s_conn_a));
  cl_assert_equal_b(true, ble_hrm_is_sharing());
  prv_assert_event_service_subscribed(true);

  // Revoke:
  ble_hrm_revoke_sharing_permission_for_connection(&s_conn_a);

  cl_assert_equal_b(false, ble_hrm_is_sharing_to_connection(&s_conn_a));
  cl_assert_equal_b(false, ble_hrm_is_sharing());
  prv_assert_event_service_subscribed(false);
  prv_assert_last_disconnected(s_device_a);
}

void test_ble_hrm__revoke_all(void) {
  // Device A subscribes:
  bt_driver_cb_hrm_service_update_subscription(s_device_a, true);

  // Expect permissions UI to be presented:
  prv_assert_permissions_ui_and_respond(true /* is_granted */);

  // Device B subscribes:
  bt_driver_cb_hrm_service_update_subscription(s_device_b, true);

  // Expect permissions UI to be presented:
  prv_assert_permissions_ui_and_respond(true /* is_granted */);

  ble_hrm_revoke_all();

  cl_assert_equal_b(false, ble_hrm_is_sharing_to_connection(&s_conn_a));
  cl_assert_equal_b(false, ble_hrm_is_sharing_to_connection(&s_conn_b));
  cl_assert_equal_b(false, ble_hrm_is_sharing());
  prv_assert_event_service_subscribed(false);
}

void test_ble_hrm__revoke_after_disconnection(void) {
  ble_hrm_revoke_sharing_permission_for_connection(NULL);

  s_connections[0] = NULL;
  ble_hrm_revoke_sharing_permission_for_connection(&s_conn_a);

  cl_assert_equal_b(false, ble_hrm_is_sharing_to_connection(NULL));

  // Shouldn't crash or anything
}

void test_ble_hrm__grant_after_disconnection(void) {
  // Device A subscribes:
  bt_driver_cb_hrm_service_update_subscription(s_device_a, true);

  // Fake disconnection:
  s_connections[0] = NULL;

  // Grabt permission after disconnection.
  // Request object should be freed and thing shouldn't crash.
  prv_assert_permissions_ui_and_respond(true /* is_granted */);

  cl_assert_equal_b(false, ble_hrm_is_sharing_to_connection(&s_conn_a));
}

void test_ble_hrm__decline_permission_dont_ask_again_even_after_reconnecting(void) {
  bt_driver_cb_hrm_service_update_subscription(s_device_a, true);

  // Decline:
  prv_assert_permissions_ui_and_respond(false /* is_granted */);

  // Unsub, resub:
  bt_driver_cb_hrm_service_update_subscription(s_device_a, false);
  bt_driver_cb_hrm_service_update_subscription(s_device_a, true);

  // No sharing request UI:
  cl_assert_equal_p(NULL, s_last_sharing_request);

  // Fake disconnection:
  ble_hrm_handle_disconnection(&s_conn_a);

  // Fake reconn & subscribe:
  bt_driver_cb_hrm_service_update_subscription(s_device_a, true);

  // No sharing request UI:
  cl_assert_equal_p(NULL, s_last_sharing_request);

  // Still declined:
  cl_assert_equal_b(false, ble_hrm_is_sharing_to_connection(&s_conn_a));
}

void test_ble_hrm__unsub_upon_deinit(void) {
  // Device A subscribes:
  bt_driver_cb_hrm_service_update_subscription(s_device_a, true);
  prv_assert_permissions_ui_and_respond(true /* is_granted */);

  // __cleanup() will do the deinit and also assert that  there's no subscription to the HRM mgr.
}

// Test that we handle a races where a subscription/disconnection callback happens in after
// deiniting the stack:
void test_ble_hrm__sub_after_deinit(void) {
  ble_hrm_deinit();

  bt_driver_cb_hrm_service_update_subscription(s_device_a, true);
  prv_assert_event_service_subscribed(false);
  cl_assert_equal_i(s_hrm_manager_subscribe_with_callback_call_count, 0);

  ble_hrm_handle_disconnection(&s_conn_a);
  prv_assert_event_service_subscribed(false);
  cl_assert_equal_i(s_hrm_manager_subscribe_with_callback_call_count, 0);

  ble_hrm_init(); // reinit, __cleanup() will deinit again
}

static void prv_put_and_assert_hrm_event(HRMEventType subtype, uint8_t bpm, HRMQuality quality,
                                         bool expect_bt_driver_cb, bool expected_is_on_wrist) {
  int call_count_before = s_bt_driver_hrm_service_handle_measurement_call_count;

  PebbleEvent hrm_event = {
    .type = PEBBLE_HRM_EVENT,
    .hrm = {
      .event_type = subtype,
      .bpm = {
        .bpm = bpm,
        .quality = quality,
      },
    },
  };
  event_put(&hrm_event);
  fake_event_service_handle_last();

  if (expect_bt_driver_cb) {
    cl_assert_equal_i(call_count_before + 1, s_bt_driver_hrm_service_handle_measurement_call_count);
    cl_assert_equal_i(bpm, s_last_ble_hrm_measurement.bpm);
    cl_assert_equal_b(expected_is_on_wrist, s_last_ble_hrm_measurement.is_on_wrist);
  } else {
    cl_assert_equal_i(call_count_before, s_bt_driver_hrm_service_handle_measurement_call_count);
  }
}

void test_ble_hrm__handle_hrm_event(void) {
  bt_driver_cb_hrm_service_update_subscription(s_device_a, true);
  cl_assert_equal_i(0, s_bt_driver_hrm_service_handle_measurement_call_count);
  prv_assert_permissions_ui_and_respond(true /* is_granted */);

  // Don't grant permission to device B:
  bt_driver_cb_hrm_service_update_subscription(s_device_b, true);
  prv_assert_permissions_ui_and_respond(false /* is_granted */);

  prv_put_and_assert_hrm_event(HRMEvent_BPM, 80, HRMQuality_Excellent,
                               true /* expect bt driver cb */, true /* expected_is_on_wrist */);

  // Assert only device A is listed as "permitted device" and B is not:
  cl_assert_equal_i(1, s_last_num_permitted_devices);
  cl_assert_equal_m(&s_last_permitted_devices[0], s_device_a, sizeof(*s_device_a));

  prv_put_and_assert_hrm_event(HRMEvent_BPM, 80, HRMQuality_NoSignal,
                               true /* expect bt driver cb */, false /* expected_is_on_wrist */);

  prv_put_and_assert_hrm_event(HRMEvent_BPM, 80, HRMQuality_NoAccel,
                               true /* expect bt driver cb */, false /* expected_is_on_wrist */);

  prv_put_and_assert_hrm_event(HRMEvent_BPM, 80, HRMQuality_OffWrist,
                               true /* expect bt driver cb */, false /* expected_is_on_wrist */);

  // Ignore non-BPM event:
  prv_put_and_assert_hrm_event(HRMEvent_HRV, 80, HRMQuality_OffWrist,
                               false /* expect bt driver cb */, false /* expected_is_on_wrist */);
}

void test_ble_hrm__handle_activity_pref_hrm_changes(void) {
  cl_assert_equal_b(true, s_bt_driver_hrm_service_is_enabled);
  cl_assert_equal_i(0, s_bt_driver_hrm_service_enable_call_count);
  ble_hrm_handle_activity_prefs_heart_rate_is_enabled(false);
  cl_assert_equal_i(1, s_bt_driver_hrm_service_enable_call_count);
  cl_assert_equal_b(false, s_bt_driver_hrm_service_is_enabled);

  // Disabled, again -- would lead to another call to bt_driver_hrm_service_enable(),
  // the BT driver lib keeps track of whether it's enabled and is expected to ignore the call.
  ble_hrm_handle_activity_prefs_heart_rate_is_enabled(false);
  cl_assert_equal_i(2, s_bt_driver_hrm_service_enable_call_count);
  cl_assert_equal_b(false, s_bt_driver_hrm_service_is_enabled);

  // Enable
  ble_hrm_handle_activity_prefs_heart_rate_is_enabled(true);
  cl_assert_equal_i(3, s_bt_driver_hrm_service_enable_call_count);
  cl_assert_equal_b(true, s_bt_driver_hrm_service_is_enabled);
}

void test_ble_hrm__popup_after_long_continuous_use(void) {
  extern RegularTimerInfo *ble_hrm_timer(void);
  RegularTimerInfo *timer = ble_hrm_timer();

  bt_driver_cb_hrm_service_update_subscription(s_device_a, true);
  prv_assert_permissions_ui_and_respond(true /* is_granted */);

  cl_assert_equal_b(true, regular_timer_is_scheduled(timer));

  bt_driver_cb_hrm_service_update_subscription(s_device_a, false);
  cl_assert_equal_b(false, regular_timer_is_scheduled(timer));

  bt_driver_cb_hrm_service_update_subscription(s_device_a, true);
  cl_assert_equal_b(true, regular_timer_is_scheduled(timer));

  cl_assert_equal_i(0, s_ble_hrm_push_reminder_popup_call_count);
  fake_regular_timer_trigger(timer);
  cl_assert_equal_i(1, s_ble_hrm_push_reminder_popup_call_count);

  // Except timer to be rescheduled again:
  cl_assert_equal_b(true, regular_timer_is_scheduled(timer));

  bt_driver_cb_hrm_service_update_subscription(s_device_a, false);
  cl_assert_equal_b(false, regular_timer_is_scheduled(timer));
}
