/*
 * 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 "BSCAPI.h"
#include "clar.h"
#include "bluetooth/bt_driver_comm.h"
#include "services/common/comm_session/session.h"
#include "services/common/comm_session/session_remote_version.h"
#include "services/common/comm_session/session_send_buffer.h"
#include "services/common/comm_session/session_transport.h"
#include "kernel/events.h"

extern void comm_session_set_capabilities(CommSession *session,
                                          CommSessionCapability capability_flags);
extern bool comm_session_is_valid(const CommSession *session);
extern void comm_session_init(void);
extern void comm_session_deinit(void);
extern void comm_session_send_next_immediately(CommSession *session);

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

#include "stubs_analytics.h"
#include "stubs_bt_lock.h"
#include "stubs_bt_stack.h"
#include "stubs_logging.h"
#include "stubs_passert.h"
#include "stubs_rand_ptr.h"
#include "stubs_syscall_internal.h"

void comm_session_analytics_open_session(CommSession *session) {
}

void comm_session_analytics_close_session(CommSession *session, CommSessionCloseReason reason) {
}

void comm_session_receive_router_cleanup(CommSession *session) {
}

void comm_session_send_queue_cleanup(CommSession *session) {
}

void bt_persistent_storage_set_cached_system_capabilities(
    const PebbleProtocolCapabilities *capabilities) {
}

static uint16_t s_send_queue_length;

size_t comm_session_send_queue_get_length(const CommSession *session) {
  return s_send_queue_length;
}

void fake_session_send_queue_set_length(uint32_t length) {
  s_send_queue_length = length;
}

static bool s_dls_private_handle_disconnect_called;
void dls_private_handle_disconnect(void *data) {
  s_dls_private_handle_disconnect_called = true;
}

static bool s_comm_session_event_put;
void event_put(PebbleEvent* event) {
  if (event->type == PEBBLE_COMM_SESSION_EVENT &&
      event->bluetooth.comm_session_event.is_system) {
    s_comm_session_event_put = true;
  };
}

void app_launch_trigger(void) {
}

void session_remote_version_start_requests(CommSession *session) {
}

static int s_send_next_count;

static void prv_send_next(Transport *transport) {
  ++s_send_next_count;
}

static int s_close_count;
Transport *s_last_closed_transport;

static void prv_close(Transport *transport) {
  ++s_close_count;
  s_last_closed_transport = transport;
}

static int s_reset_count;

static void prv_reset(Transport *transport) {
  ++s_reset_count;
}

typedef enum {
  TransportIDNull,
  TransportID1,
  TransportID2,
  TransportID3,
  NumTransportID,
} TransportID;

static Uuid *s_transport_uuid[NumTransportID];

static const Uuid *prv_get_uuid(Transport *transport) {
  TransportID i = (uintptr_t)transport;
  cl_assert(i >= TransportID1 && i <= TransportID3);
  return s_transport_uuid[i];
}

static CommSessionTransportType prv_get_type(struct Transport *transport) {
  return CommSessionTransportType_QEMU;
}

static const TransportImplementation s_transport_imp = {
  .send_next = prv_send_next,
  .close = prv_close,
  .reset = prv_reset,
  .get_uuid = prv_get_uuid,
  .get_type = prv_get_type,
};

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

#include "fake_kernel_malloc.h"
#include "fake_session_send_buffer.h"
#include "fake_system_task.h"
#include "fake_app_manager.h"

static void prv_system_task_cb(void *data) {
  CommSession *session = (CommSession *)data;
  bt_driver_run_send_next_job(session, true);
}

bool bt_driver_comm_schedule_send_next_job(CommSession *data) {
  // Implement this API in this test suite using the fake_system_task:
  system_task_add_callback(prv_system_task_cb, data);
  return true;
}

static bool s_bt_driver_comm_is_current_task_send_next_task;
bool bt_driver_comm_is_current_task_send_next_task(void) {
  return s_bt_driver_comm_is_current_task_send_next_task;
}

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

void test_session__initialize(void) {
  stub_app_init();
  comm_session_init();
  fake_kernel_malloc_init();
  fake_kernel_malloc_mark();
  fake_session_send_buffer_init();
  s_send_queue_length = 0;
  memset(s_transport_uuid, 0, sizeof(s_transport_uuid));
  s_send_next_count = 0;
  s_reset_count = 0;
  s_close_count = 0;
  s_last_closed_transport = NULL;
  s_dls_private_handle_disconnect_called = false;
  s_comm_session_event_put = false;
  s_bt_driver_comm_is_current_task_send_next_task = false;
}

void test_session__cleanup(void) {
  // Check for leaks:
  fake_kernel_malloc_mark_assert_equal();
  fake_kernel_malloc_deinit();

  fake_system_task_callbacks_cleanup();
}

void test_session__get_system_session_disconnected_returns_null(void) {
  cl_assert_equal_p(comm_session_get_system_session(), NULL);
}

void test_session__get_app_session_disconnected_returns_null(void) {
  cl_assert_equal_p(comm_session_get_current_app_session(), NULL);
}

void test_session__send_data_returns_false_for_null_session(void) {
  const uint16_t endpoint_id = 1234;
  uint8_t data[] = {1, 2, 3, 4};
  cl_assert_equal_b(comm_session_send_data(NULL, endpoint_id,
                                           data, sizeof(data),
                                           COMM_SESSION_DEFAULT_TIMEOUT), false);
}

void test_session__basic_open_close(void) {
  Transport *transport = (Transport *) TransportID1;
  CommSession *session = comm_session_open(transport, &s_transport_imp,
                                           TransportDestinationSystem);
  cl_assert(session);
  cl_assert_equal_b(comm_session_is_valid(session), true);
  comm_session_close(session, CommSessionCloseReason_UnderlyingDisconnection);
  cl_assert_equal_b(comm_session_is_valid(session), false);
}

void test_session__get_type_system(void) {
  Transport *transport = (Transport *) TransportID1;
  CommSession *session = comm_session_open(transport, &s_transport_imp,
                                           TransportDestinationSystem);
  cl_assert_equal_i(comm_session_get_type(session), CommSessionTypeSystem);
  cl_assert_equal_p(comm_session_get_system_session(), session);
  cl_assert_equal_p(comm_session_get_by_type(CommSessionTypeSystem), session);
  comm_session_close(session, CommSessionCloseReason_UnderlyingDisconnection);
  fake_system_task_callbacks_invoke_pending(); // get dls called
  cl_assert_equal_b(s_dls_private_handle_disconnect_called, true);
  cl_assert_equal_b(s_comm_session_event_put, true);
}

void test_session__get_type_app(void) {
  Transport *transport = (Transport *) TransportID1;
  CommSession *session = comm_session_open(transport, &s_transport_imp,
                                           TransportDestinationApp);
  cl_assert_equal_i(comm_session_get_type(session), CommSessionTypeApp);
  cl_assert_equal_p(comm_session_get_current_app_session(), session);
  cl_assert_equal_p(comm_session_get_by_type(CommSessionTypeApp), session);
  comm_session_close(session, CommSessionCloseReason_UnderlyingDisconnection);
  fake_system_task_callbacks_invoke_pending(); // get dls called
  cl_assert_equal_b(s_dls_private_handle_disconnect_called, false);
  cl_assert_equal_b(s_comm_session_event_put, false);
}

void test_session__last_system_session_wins(void) {
  Transport *system_transport = (Transport *) TransportID1;
  CommSession *system_session = comm_session_open(system_transport, &s_transport_imp,
                                                  TransportDestinationSystem);

  cl_assert_equal_i(s_close_count, 0);

  Transport *system_transport2 = (Transport *) TransportID2;
  CommSession *system_session2 = comm_session_open(system_transport2, &s_transport_imp,
                                                   TransportDestinationSystem);

  cl_assert(system_session2);
  cl_assert_equal_p(s_last_closed_transport, system_transport);
  cl_assert_equal_i(s_close_count, 1);

  comm_session_close(system_session2, CommSessionCloseReason_UnderlyingDisconnection);

  // The transport's .close is supposed to call this.
  // The stub in this test doens't, so clean up manually:
  comm_session_close(system_session, CommSessionCloseReason_UnderlyingDisconnection);
}

void test_session__get_app_session_multiple(void) {
  Transport *system_transport = (Transport *) TransportID1;
  CommSession *system_session = comm_session_open(system_transport, &s_transport_imp,
                                                  TransportDestinationSystem);
  Uuid legacy_app_uuid = {
    0xff, 0xc5, 0x24, 0x01, 0x4d, 0xbe, 0x40, 0x8b,
    0xb7, 0x3a, 0x0e, 0x80, 0xef, 0x09, 0xaf, 0x74};
  // Legacy transport (iAP) isn't aware of the app UUID, so don't set anything:
  Transport *legacy_transport = (Transport *) TransportID2;
  CommSession *legacy_app_session = comm_session_open(legacy_transport, &s_transport_imp,
                                                      TransportDestinationApp);
  Uuid modern_app_uuid = {
    0x04, 0xc5, 0x24, 0x01, 0x4d, 0xbe, 0x40, 0x8b,
    0xb7, 0x3a, 0x0e, 0x80, 0xef, 0x09, 0xaf, 0x74};
  Transport *modern_transport = (Transport *) TransportID3;
  s_transport_uuid[TransportID3] = &modern_app_uuid;
  CommSession *modern_app_session = comm_session_open(modern_transport, &s_transport_imp,
                                                      TransportDestinationApp);

  stub_app_set_uuid(legacy_app_uuid);
  cl_assert_equal_p(comm_session_get_current_app_session(), legacy_app_session);

  stub_app_set_uuid(modern_app_uuid);
  cl_assert_equal_p(comm_session_get_current_app_session(), modern_app_session);

  stub_app_set_uuid((Uuid)UUID_INVALID);
  cl_assert_equal_p(comm_session_get_current_app_session(), NULL);

  stub_app_set_uuid((Uuid)UUID_SYSTEM);
  cl_assert_equal_p(comm_session_get_current_app_session(), NULL);

  comm_session_close(system_session, CommSessionCloseReason_UnderlyingDisconnection);
  comm_session_close(legacy_app_session, CommSessionCloseReason_UnderlyingDisconnection);
  comm_session_close(modern_app_session, CommSessionCloseReason_UnderlyingDisconnection);
}

void test_session__assert_if_deinit_and_transport_did_not_clean_up_properly(void) {
  Transport *transport = (Transport *) TransportID1;
  CommSession *session = comm_session_open(transport, &s_transport_imp,
                                           TransportDestinationSystem);
  cl_assert_equal_b(comm_session_is_valid(session), true);
  // Expect assert when Transport didn't clean up after itself:
  cl_assert_passert(comm_session_deinit());
  comm_session_close(session, CommSessionCloseReason_UnderlyingDisconnection);
}

void test_session__send_next_deduping(void) {
  Transport *transport = (Transport *) TransportID1;
  CommSession *session = comm_session_open(transport, &s_transport_imp,
                                           TransportDestinationSystem);
  cl_assert_equal_b(comm_session_is_valid(session), true);
  fake_session_send_queue_set_length(1234);

  cl_assert_equal_i(fake_system_task_count_callbacks(), 0);
  comm_session_send_next(session);
  cl_assert_equal_i(fake_system_task_count_callbacks(), 1);
  comm_session_send_next(session);
  cl_assert_equal_i(fake_system_task_count_callbacks(), 1);
  comm_session_send_next(session);
  cl_assert_equal_i(fake_system_task_count_callbacks(), 1);
  comm_session_send_next(session);
  cl_assert_equal_i(fake_system_task_count_callbacks(), 1);
  comm_session_send_next(session);
  cl_assert_equal_i(fake_system_task_count_callbacks(), 1);
  cl_assert_equal_i(s_send_next_count, 0);

  fake_system_task_callbacks_invoke_pending();
  cl_assert_equal_i(fake_system_task_count_callbacks(), 0);
  cl_assert_equal_i(s_send_next_count, 1);

  comm_session_send_next(session);
  cl_assert_equal_i(fake_system_task_count_callbacks(), 1);
  comm_session_send_next(session);
  cl_assert_equal_i(fake_system_task_count_callbacks(), 1);
  cl_assert_equal_i(s_send_next_count, 1);

  fake_system_task_callbacks_invoke_pending();
  cl_assert_equal_i(s_send_next_count, 2);

  comm_session_close(session, CommSessionCloseReason_UnderlyingDisconnection);
}

void test_session__send_next_not_called_when_session_closed_in_mean_time(void) {
  Transport *transport = (Transport *) TransportID1;
  CommSession *session = comm_session_open(transport, &s_transport_imp,
                                           TransportDestinationSystem);
  cl_assert_equal_b(comm_session_is_valid(session), true);
  fake_session_send_queue_set_length(1234);

  cl_assert_equal_i(fake_system_task_count_callbacks(), 0);
  comm_session_send_next(session);

  comm_session_close(session, CommSessionCloseReason_UnderlyingDisconnection);
  fake_system_task_callbacks_invoke_pending();
  cl_assert_equal_i(fake_system_task_count_callbacks(), 0);
  cl_assert_equal_i(s_send_next_count, 0);
}

static bool prv_schedule_send_next(CommSession *session) {
  return true;
}

static bool s_is_current_task_schedule_task = false;
static bool prv_is_current_task_schedule_task(Transport *transport) {
  return s_is_current_task_schedule_task;
}

void test_session__transport_send_next_task(void) {
  TransportImplementation transport_imp = s_transport_imp;
  transport_imp.schedule = prv_schedule_send_next;
  transport_imp.is_current_task_schedule_task = prv_is_current_task_schedule_task;

  extern bool comm_session_is_current_task_send_next_task(CommSession *session);

  Transport *transport = (Transport *) TransportID1;
  CommSession *session = comm_session_open(transport, &transport_imp,
                                           TransportDestinationSystem);

  s_is_current_task_schedule_task = true;
  cl_assert_equal_b(comm_session_is_current_task_send_next_task(session), true);
  s_is_current_task_schedule_task = false;
  cl_assert_equal_b(comm_session_is_current_task_send_next_task(session), false);
  comm_session_close(session, CommSessionCloseReason_UnderlyingDisconnection);

  transport_imp.schedule = NULL;
  transport_imp.is_current_task_schedule_task = NULL;

  session = comm_session_open(transport, &transport_imp, TransportDestinationSystem);

  s_bt_driver_comm_is_current_task_send_next_task = true;
  cl_assert_equal_b(comm_session_is_current_task_send_next_task(session), true);
  s_bt_driver_comm_is_current_task_send_next_task = false;
  cl_assert_equal_b(comm_session_is_current_task_send_next_task(session), false);
  comm_session_close(session, CommSessionCloseReason_UnderlyingDisconnection);
}

void test_session__reset_valid_session(void) {
  Transport *transport = (Transport *) TransportID1;
  CommSession *session = comm_session_open(transport, &s_transport_imp,
                                           TransportDestinationSystem);
  cl_assert_equal_b(comm_session_is_valid(session), true);
  cl_assert_equal_i(s_reset_count, 0);
  comm_session_reset(session);
  cl_assert_equal_i(s_reset_count, 1);

  comm_session_close(session, CommSessionCloseReason_UnderlyingDisconnection);
}

void test_session__reset_invalid_session(void) {
  CommSession *invalid_session = (CommSession *) TransportID1;
  cl_assert_equal_b(comm_session_is_valid(invalid_session), false);
  cl_assert_equal_i(s_reset_count, 0);
  comm_session_reset(invalid_session);
  cl_assert_equal_i(s_reset_count, 0);
}

extern bool comm_session_send_next_is_scheduled(CommSession *session);

void test_session__send_next_is_schedule_flag_not_unset_after_immediate_call(void){
  Transport *transport = (Transport *) TransportID1;
  CommSession *session = comm_session_open(transport, &s_transport_imp,
                                           TransportDestinationSystem);
  cl_assert_equal_b(comm_session_is_valid(session), true);

  comm_session_send_next(session);
  cl_assert_equal_b(comm_session_send_next_is_scheduled(session), true);

  // Calling comm_session_send_next_immediately should NOT result in the flag to get unset
  comm_session_send_next_immediately(session);
  cl_assert_equal_b(comm_session_send_next_is_scheduled(session), true);

  fake_system_task_callbacks_invoke_pending();
  cl_assert_equal_b(comm_session_send_next_is_scheduled(session), false);

  comm_session_close(session, CommSessionCloseReason_UnderlyingDisconnection);
}

void test_session__capabilities(void) {
  Transport *transport = (Transport *) TransportID1;
  CommSession *session = comm_session_open(transport, &s_transport_imp,
                                           TransportDestinationSystem);

  for (int i = 0; i < (sizeof(int) * 8); ++i) {
    CommSessionCapability capability = (1 << i);
    cl_assert_equal_b(false, comm_session_has_capability(session, capability));
  }

  comm_session_set_capabilities(session, ~0);

  for (int i = 0; i < (sizeof(int) * 8); ++i) {
    CommSessionCapability capability = (1 << i);
    cl_assert_equal_b(true, comm_session_has_capability(session, capability));
  }

  comm_session_close(session, CommSessionCloseReason_UnderlyingDisconnection);
}
