/*
 * 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 "applib/app_message/app_message_internal.h"
#include "clar.h"
#include "services/normal/app_message/app_message_sender.h"
#include "services/common/comm_session/session_internal.h"
#include "services/common/comm_session/protocol.h"
#include "services/common/comm_session/session.h"
#include "process_management/app_install_manager.h"
#include "services/normal/app_outbox_service.h"
#include "util/math.h"
#include "util/net.h"

extern const SessionSendJobImpl s_app_message_send_job_impl;
extern void comm_session_send_queue_cleanup(CommSession *session);

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

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

static int s_app_install_timestamp_update_count;
void app_install_mark_prioritized(AppInstallId install_id, bool can_expire) {
  ++s_app_install_timestamp_update_count;
}

AppInstallId app_manager_get_current_app_id(void) {
  return INSTALL_ID_INVALID;
}

static PebbleProcessMd s_process_md;
const PebbleProcessMd* app_manager_get_current_app_md(void) {
  return &s_process_md;
}

static int s_consumed_count;
static AppOutboxStatus s_last_status_code;
void app_outbox_service_consume_message(AppOutboxMessage *message, AppOutboxStatus status) {
  s_last_status_code = status;
  ++s_consumed_count;
  kernel_free(message);
}

static AppOutboxMessageHandler s_outbox_message_handler;
static size_t s_service_data_size;
void app_outbox_service_register(AppOutboxServiceTag service_tag,
                                 AppOutboxMessageHandler message_handler,
                                 PebbleTask consumer_task,
                                 size_t service_data_size) {
  s_outbox_message_handler = message_handler;
  s_service_data_size = service_data_size;
}

static bool s_is_message_cancelled;
bool app_outbox_service_is_message_cancelled(AppOutboxMessage *message) {
  return s_is_message_cancelled;
}

void app_outbox_service_cleanup_all_pending_messages(void) {
  s_is_message_cancelled = true;
}

void comm_session_analytics_inc_bytes_sent(CommSession *session, uint16_t length) {
}

static CommSession s_system_session;
static CommSession *s_system_session_ptr;
CommSession *comm_session_get_system_session(void) {
  return s_system_session_ptr;
}

static CommSession s_app_session;
static CommSession *s_app_session_ptr;
CommSession *comm_session_get_current_app_session(void) {
  if (s_process_md.allow_js) {
    return comm_session_get_system_session();
  }
  return s_app_session_ptr;
}

bool comm_session_is_valid(const CommSession *session) {
  if (!session) {
    return false;
  }
  return (session == comm_session_get_current_app_session() ||
          session == comm_session_get_system_session());
}

static int s_send_next_count = 0;
void comm_session_send_next(CommSession *session) {
  ++s_send_next_count;
}

void comm_session_set_responsiveness(CommSession *session, BtConsumer consumer,
                                     ResponseTimeState state, uint16_t max_period_secs) {
}

void comm_session_sanitize_app_session(CommSession **session_in_out) {
  CommSession *permitted_session = comm_session_get_current_app_session();
  *session_in_out = ((!*session_in_out) ||
                     (*session_in_out == permitted_session)) ? permitted_session : NULL;
}

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

static void prv_send_outbox_raw_data(const uint8_t *data, size_t length) {
  AppOutboxMessage *outbox_message = kernel_zalloc(sizeof(AppOutboxMessage) + s_service_data_size);
  cl_assert(outbox_message);
  outbox_message->data = data;
  outbox_message->length = length;
  s_outbox_message_handler(outbox_message);
}

static AppMessageAppOutboxData *prv_create_and_send_outbox_message(CommSession *session,
                                                                   uint16_t endpoint_id,
                                                                   const uint8_t *payload,
                                                                   size_t payload_length) {
  const size_t outbox_data_size = sizeof(AppMessageAppOutboxData) + payload_length;
  AppMessageAppOutboxData *outbox_data = app_malloc(outbox_data_size);
  cl_assert(outbox_data);
  outbox_data->session = session;
  outbox_data->endpoint_id = endpoint_id;
  memcpy(outbox_data->payload, payload, payload_length);
  prv_send_outbox_raw_data((const uint8_t *)outbox_data, outbox_data_size);
  return outbox_data;
}

static void prv_process_send_queue(CommSession *session) {
  cl_assert(session);
  size_t length = comm_session_send_queue_get_length(session);
  if (length) {
    comm_session_send_queue_consume(session, length);
  }
}

#define assert_consumed(expected_last_status, expected_consumed_count) \
{ \
  cl_assert_equal_i(expected_last_status, s_last_status_code); \
  cl_assert_equal_i(expected_consumed_count, s_consumed_count); \
}

#define assert_not_consumed() \
  cl_assert_equal_i(0, s_consumed_count);

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

#define DISALLOWED_ENDPOINT_ID (9000)  // GetBytes
#define ALLOWED_ENDPOINT_ID (APP_MESSAGE_ENDPOINT_ID)

static const uint8_t TEST_PAYLOAD[] = {0xaa, 0xbb, 0xcc, 0xdd};
static uint8_t TEST_EXPECTED_PP_MSG[sizeof(PebbleProtocolHeader) + sizeof(TEST_PAYLOAD)];

void test_app_message_sender__initialize(void) {
  s_system_session_ptr = &s_system_session;
  s_app_session_ptr = &s_app_session;
  s_send_next_count = 0;

  s_outbox_message_handler = NULL;
  s_service_data_size = 0;

  s_is_message_cancelled = false;

  s_last_status_code = AppOutboxStatusUserRangeEnd;
  s_consumed_count = 0;

  s_app_install_timestamp_update_count = 0;

  s_process_md.allow_js = false;

  PebbleProtocolHeader *header = (PebbleProtocolHeader *)TEST_EXPECTED_PP_MSG;
  header->length = htons(sizeof(TEST_PAYLOAD));
  header->endpoint_id = htons(ALLOWED_ENDPOINT_ID);
  memcpy(TEST_EXPECTED_PP_MSG + sizeof(*header), TEST_PAYLOAD, sizeof(TEST_PAYLOAD));

  app_message_sender_init();
  cl_assert(s_outbox_message_handler);
}

void test_app_message_sender__cleanup(void) {
  // Flush out to avoid other tests failing:
  if (s_system_session_ptr) {
    prv_process_send_queue(s_system_session_ptr);
  }
  if (s_app_session_ptr) {
    prv_process_send_queue(s_app_session_ptr);
  }
}

// Tests that exercise the sanity checking of the input from the app
////////////////////////////////////////////////////////////////////////////////////////////////////

void test_app_message_sender__outbox_data_too_short(void) {
  // This is one byte too small, because the PP payload has to be at least one in length:
  AppMessageAppOutboxData data = {};
  prv_send_outbox_raw_data((const uint8_t *)&data, sizeof(data));
  assert_consumed(AppMessageSenderErrorDataTooShort, 1);
}

void test_app_message_sender__disallowed_endpoint(void) {
  AppMessageAppOutboxData *outbox_data =
      prv_create_and_send_outbox_message(s_system_session_ptr, DISALLOWED_ENDPOINT_ID,
                                         TEST_PAYLOAD, sizeof(TEST_PAYLOAD));
  assert_consumed(AppMessageSenderErrorEndpointDisallowed, 1);
  app_free(outbox_data);
}

void test_app_message_sender__system_session_but_not_js_app(void) {
  AppMessageAppOutboxData *outbox_data =
      prv_create_and_send_outbox_message(s_system_session_ptr, ALLOWED_ENDPOINT_ID,
                                         TEST_PAYLOAD, sizeof(TEST_PAYLOAD));
  assert_consumed(AppMessageSenderErrorDisconnected, 1);
  app_free(outbox_data);
}

void test_app_message_sender__app_session_but_js_app(void) {
  s_process_md.allow_js = true;
  AppMessageAppOutboxData *outbox_data =
      prv_create_and_send_outbox_message(s_app_session_ptr, ALLOWED_ENDPOINT_ID,
                                         TEST_PAYLOAD, sizeof(TEST_PAYLOAD));
  assert_consumed(AppMessageSenderErrorDisconnected, 1);
  app_free(outbox_data);
}

void test_app_message_sender__no_sessions_connected(void) {
  s_system_session_ptr = NULL;
  s_app_session_ptr = NULL;
  AppMessageAppOutboxData *outbox_data =
      prv_create_and_send_outbox_message(NULL /* auto-select */, ALLOWED_ENDPOINT_ID,
                                         TEST_PAYLOAD, sizeof(TEST_PAYLOAD));
  assert_consumed(AppMessageSenderErrorDisconnected, 1);
  app_free(outbox_data);
}

void test_app_message_sender__auto_select_not_js_app(void) {
  AppMessageAppOutboxData *outbox_data =
      prv_create_and_send_outbox_message(NULL /* auto-select */, ALLOWED_ENDPOINT_ID,
                                         TEST_PAYLOAD, sizeof(TEST_PAYLOAD));
  prv_process_send_queue(s_system_session_ptr);
  assert_not_consumed();

  prv_process_send_queue(s_app_session_ptr);
  assert_consumed(AppMessageSenderErrorSuccess, 1);

  app_free(outbox_data);
}

void test_app_message_sender__auto_select_js_app(void) {
  s_process_md.allow_js = true;
  AppMessageAppOutboxData *outbox_data =
      prv_create_and_send_outbox_message(NULL /* auto-select */, ALLOWED_ENDPOINT_ID,
                                         TEST_PAYLOAD, sizeof(TEST_PAYLOAD));
  prv_process_send_queue(s_app_session_ptr);
  assert_not_consumed();

  prv_process_send_queue(s_system_session_ptr);
  assert_consumed(AppMessageSenderErrorSuccess, 1);

  app_free(outbox_data);
}

void test_app_message_sender__system_session_and_js_app(void) {
  s_process_md.allow_js = true;
  AppMessageAppOutboxData *outbox_data =
      prv_create_and_send_outbox_message(s_system_session_ptr, ALLOWED_ENDPOINT_ID,
                                         TEST_PAYLOAD, sizeof(TEST_PAYLOAD));
  assert_not_consumed();
  prv_process_send_queue(s_system_session_ptr);
  assert_consumed(AppMessageSenderErrorSuccess, 1);
  cl_assert_equal_i(s_app_install_timestamp_update_count, 1);
  app_free(outbox_data);
}

// Tests that exercise interface towards the Send Queue
////////////////////////////////////////////////////////////////////////////////////////////////////

void test_app_message_sender__freed_but_not_sent_entirely(void) {
  AppMessageAppOutboxData *outbox_data =
      prv_create_and_send_outbox_message(NULL /* auto-select */, ALLOWED_ENDPOINT_ID,
                                         TEST_PAYLOAD, sizeof(TEST_PAYLOAD));
  size_t length = comm_session_send_queue_get_length(s_app_session_ptr);
  comm_session_send_queue_consume(s_app_session_ptr, length - 1);
  comm_session_send_queue_cleanup(s_app_session_ptr);
  assert_consumed(AppMessageSenderErrorDisconnected, 1);
  cl_assert_equal_i(s_app_install_timestamp_update_count, 0);
  app_free(outbox_data);
}

void test_app_message_sender__byte_by_byte_consume(void) {
  AppMessageAppOutboxData *outbox_data =
      prv_create_and_send_outbox_message(NULL /* auto-select */, ALLOWED_ENDPOINT_ID,
                                         TEST_PAYLOAD, sizeof(TEST_PAYLOAD));
  size_t length = comm_session_send_queue_get_length(s_app_session_ptr);
  cl_assert_equal_i(length, sizeof(PebbleProtocolHeader) + sizeof(TEST_PAYLOAD));

  for (int i = 0; i < length; ++i) {
    // Test the `length` implementation:
    cl_assert_equal_i(length - i, comm_session_send_queue_get_length(s_app_session_ptr));

    // Test the `read_pointer` implementation:
    const uint8_t *read_pointer = NULL;
    size_t length_available = comm_session_send_queue_get_read_pointer(s_app_session_ptr,
                                                                       &read_pointer);
    cl_assert(read_pointer);
    cl_assert_equal_i(TEST_EXPECTED_PP_MSG[i], *read_pointer);
    // Expect that the header and payload will be non-contiguous:
    if (i < sizeof(PebbleProtocolHeader)) {
      cl_assert_equal_i(sizeof(PebbleProtocolHeader) - i, length_available);
    } else {
      cl_assert_equal_i(length - i, length_available);
    }

    // Test the `copy` implementation:
    uint8_t byte_out = 0xff;
    cl_assert_equal_i(1, comm_session_send_queue_copy(s_app_session_ptr, 0 /* offset */,
                                                      1 /* length */, &byte_out));
    cl_assert_equal_i(TEST_EXPECTED_PP_MSG[i], byte_out);

    comm_session_send_queue_consume(s_app_session_ptr, 1 /* length */);
  }

  assert_consumed(AppMessageSenderErrorSuccess, 1);
  cl_assert_equal_i(s_app_install_timestamp_update_count, 1);
  app_free(outbox_data);
}

void test_app_message_sender__byte_by_byte_copy_with_offset(void) {
  AppMessageAppOutboxData *outbox_data =
      prv_create_and_send_outbox_message(NULL /* auto-select */, ALLOWED_ENDPOINT_ID,
                                         TEST_PAYLOAD, sizeof(TEST_PAYLOAD));
  size_t length = comm_session_send_queue_get_length(s_app_session_ptr);
  cl_assert_equal_i(length, sizeof(PebbleProtocolHeader) + sizeof(TEST_PAYLOAD));

  uint8_t bytes_out[length];
  memset(bytes_out, 0xff, length);

  // Consume byte by byte:
  for (int c = 0; c < length; ++c) {
    // Shift offset byte by byte:
    for (int o = 0; o < (length - c); ++o) {
      size_t length_to_copy = (length - c - o);
      cl_assert_equal_i(length_to_copy, comm_session_send_queue_copy(s_app_session_ptr, o,
                                                                     length_to_copy, bytes_out));
      cl_assert_equal_i(0, memcmp(bytes_out, TEST_EXPECTED_PP_MSG + o + c, length_to_copy));
    }

    comm_session_send_queue_consume(s_app_session_ptr, 1 /* length */);
  }

  assert_consumed(AppMessageSenderErrorSuccess, 1);
  cl_assert_equal_i(s_app_install_timestamp_update_count, 1);
  app_free(outbox_data);
}

// Tests that deal with the edge case of app outbox messages getting cancelled,
// because the app that provides the buffer for the payload is quit while they are in the
// process of being sent out.
////////////////////////////////////////////////////////////////////////////////////////////////////

static void prv_quit_app_after_pp_msg_byte(uint32_t num_bytes) {
  AppMessageAppOutboxData *outbox_data =
  prv_create_and_send_outbox_message(NULL /* auto-select */, ALLOWED_ENDPOINT_ID,
                                     TEST_PAYLOAD, sizeof(TEST_PAYLOAD));

  size_t length = comm_session_send_queue_get_length(s_app_session_ptr);
  uint8_t bytes_out[length];
  memset(bytes_out, 0xff, length);

  // Copy & consume one byte of the header -- note the header is 4 bytes total:
  size_t first_length = num_bytes;
  cl_assert_equal_i(first_length, comm_session_send_queue_copy(s_app_session_ptr, 0,
                                                               first_length, bytes_out));
  comm_session_send_queue_consume(s_app_session_ptr, first_length);

  // App quits with only one header byte consumed:
  app_outbox_service_cleanup_all_pending_messages();

  // Copy & consume the rest:
  size_t second_length = (length - first_length);
  cl_assert_equal_i(second_length, comm_session_send_queue_copy(s_app_session_ptr, 0,
                                                                second_length,
                                                                bytes_out + first_length));
  comm_session_send_queue_consume(s_app_session_ptr, second_length);

  // The message should be consumed now (to free the resources associated with it):
  assert_consumed(AppMessageSenderErrorSuccess, 1);

  // Expect at least the PebbleProtocol header or more to be intact:
  size_t intact_size = MAX(num_bytes, sizeof(PebbleProtocolHeader));
  cl_assert_equal_m(bytes_out, TEST_EXPECTED_PP_MSG, intact_size);

  // Expect the remainder to be filled with zeroes:
  for (int i = 0; i < (length - intact_size); ++i) {
    cl_assert_equal_i(bytes_out[intact_size + i], 0x00);
  }

  app_free(outbox_data);
}

void test_app_message_sender__cancelled_message_in_flight_header_and_payload_not_finished(void) {
  // Expect header to get sent out normally, then a payload with all zeroes
  prv_quit_app_after_pp_msg_byte(sizeof(PebbleProtocolHeader) - 1);
}

void test_app_message_sender__cancelled_message_in_flight_payload_not_finished(void) {
  // Expect remainder payload to be all zeroes
  prv_quit_app_after_pp_msg_byte(sizeof(PebbleProtocolHeader) + 1);
}
