/*
 * 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 "services/common/put_bytes/put_bytes.h"

#include "services/common/comm_session/session_receive_router.h"
#include "os/tick.h"
#include "system/bootbits.h"
#include "system/firmware_storage.h"
#include "system/logging.h"
#include "util/attributes.h"
#include "util/net.h"

#include <bluetooth/conn_event_stats.h>

#include "FreeRTOS.h"
#include "semphr.h"

#include "clar.h"

#include <limits.h>

#include "fake_events.h"
#include "fake_pbl_malloc.h"
#include "fake_new_timer.h"
#include "fake_put_bytes_storage_mem.h"
#include "fake_queue.h"
#include "fake_rtc.h"
#include "fake_session.h"
#include "fake_spi_flash.h"
#include "fake_system_task.h"

#include "stubs_bt_lock.h"
#include "stubs_freertos.h"
#include "stubs_hexdump.h"
#include "stubs_logging.h"
#include "stubs_mutex.h"
#include "stubs_passert.h"
#include "stubs_pfs.h"
#include "stubs_prompt.h"
#include "stubs_serial.h"
#include "stubs_task_watchdog.h"
#include "stubs_tick.h"

extern SemaphoreHandle_t put_bytes_get_semaphore(void);
extern TimerID put_bytes_get_timer_id(void);
extern uint32_t put_bytes_get_index(void);
extern uint8_t prv_put_bytes_get_max_batched_pb_ops(void);

extern const ReceiverImplementation g_put_bytes_receiver_impl;

static const PebbleProtocolEndpoint s_put_bytes_endpoint = (const PebbleProtocolEndpoint) {
  .endpoint_id = 0xBEEF,
  .handler = NULL,
  .access_mask = PebbleProtocolAccessPrivate,
  .receiver_imp = &g_put_bytes_receiver_impl,
  .receiver_opt = NULL,
};

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

uint32_t s_boot_bits_orred;
void boot_bit_set(BootBitValue bit) {
  s_boot_bits_orred |= bit;
}

static bool s_firmware_update_is_in_progress;
bool firmware_update_is_in_progress(void) {
  return s_firmware_update_is_in_progress;
}

void psleep(int millis) {
}

void app_storage_get_file_name(char *name, size_t buf_length,
                               AppInstallId app_id, PebbleTask task) {
  strcpy(name, "t");
}

void bluetooth_analytics_handle_put_bytes_stats(bool successful, uint8_t type, uint32_t total_size,
                                                uint32_t elapsed_time_ms,
                                                const SlaveConnEventStats *orig_stats) {
}

bool bt_driver_analytics_get_conn_event_stats(SlaveConnEventStats *stats) {
  return false;
}

typedef enum {
  CmdInit = 0x01,
  CmdPut = 0x02,
  CmdCommit = 0x03,
  CmdAbort = 0x04,
  CmdInstall = 0x05,
  CmdInvalid = 0xff,
} Cmd;

typedef enum {
  ResponseAck = 0x01,
  ResponseNack = 0x02,
} Response;

// Send an INIT message
typedef struct PACKED {
  Cmd cmd:8;
  uint32_t total_size;
  PutBytesObjectType type:8;
  union {
    struct {
      uint8_t index;
      char filename[];
    };
    uint32_t cookie;
  };
} InitRequest;

typedef struct PACKED {
  Cmd cmd:8;
  uint32_t cookie;
  uint32_t payload_size;
  uint8_t payload[];
} PutRequest;

typedef struct PACKED {
  Cmd cmd:8;
  uint32_t cookie;
} InstallRequest;

typedef struct PACKED {
  Cmd cmd:8;
  uint32_t cookie;
} AbortRequest;

typedef struct PACKED {
  Cmd cmd:8;
  uint32_t cookie;
  uint32_t crc;
} CommitRequest;

typedef struct PACKED {
  Response response:8;
  uint32_t cookie;
} ResponseMsg;

static int s_acks_received;
static int s_nacks_received;
static uint32_t s_last_response_cookie;

static CommSession *s_session;

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

#define VALID_OBJECT_SIZE (4)
#define PUT_BYTES_TIMEOUT_MS (30000)
#define EXPECTED_CRC (0x12345678)
#define EXPECTED_COOKIE (0xabcd1234)
#define EXPECT_INIT_TIMEOUT_MS (1000)


static void(*s_do_before_write)(void);

static void prv_receive_data(CommSession *session, const uint8_t* data, size_t length) {
  Receiver *r = g_put_bytes_receiver_impl.prepare(session, &s_put_bytes_endpoint, length);
  if (r) {
    if (s_do_before_write) {
      s_do_before_write();
    }
    g_put_bytes_receiver_impl.write(r, data, length);
    g_put_bytes_receiver_impl.finish(r);
  } else {
    PBL_LOG(LOG_LEVEL_ERROR, "No receiver returned!");
  }
}

static void prv_receive_init(uint32_t total_size, PutBytesObjectType object_type) {
  InitRequest init_msg = (InitRequest) {
    .cmd = CmdInit,
    .total_size = htonl(total_size),
    .type = object_type,
    .cookie = htonl(1),
  };
  prv_receive_data(s_session, (const uint8_t *) &init_msg, sizeof(init_msg));
}

static void prv_receive_init_cookie(uint32_t total_size, PutBytesObjectType object_type,
                                    uint32_t cookie) {
  InitRequest init_msg = (InitRequest) {
    .cmd = CmdInit,
    .total_size = htonl(total_size),
    .type = object_type | (1 << 7),
    .cookie = htonl(cookie),
  };
  prv_receive_data(s_session, (const uint8_t *) &init_msg, sizeof(init_msg));
}

static void prv_receive_init_file(uint32_t total_size, const char *fn, size_t fn_len) {
  uint8_t buffer[sizeof(InitRequest) + fn_len];

  InitRequest *init_msg = (InitRequest *)buffer;
  *init_msg = (InitRequest) {
    .cmd = CmdInit,
    .total_size = htonl(total_size),
    .type = ObjectFile,
  };
  memcpy(&init_msg->filename[0], fn, fn_len);
  prv_receive_data(s_session, buffer, sizeof(buffer));
}

static void prv_receive_put(uint32_t cookie, const uint8_t *payload, uint32_t payload_size) {
  uint8_t buffer[sizeof(PutRequest) + payload_size];

  PutRequest *put_msg = (PutRequest *)buffer;
  *put_msg = (PutRequest) {
    .cmd = CmdPut,
    .cookie = htonl(cookie),
    .payload_size = htonl(payload_size),
  };
  memcpy(&put_msg->payload[0], payload, payload_size);
  prv_receive_data(s_session, buffer, sizeof(buffer));
}

static void prv_receive_commit(uint32_t cookie, uint32_t crc) {
  CommitRequest commit_msg = (CommitRequest) {
    .cmd = CmdCommit,
    .cookie = htonl(cookie),
    .crc = htonl(crc),
  };
  prv_receive_data(s_session, (const uint8_t *)&commit_msg, sizeof(commit_msg));
}

static void prv_receive_abort(uint32_t cookie) {
  AbortRequest abort_msg = (AbortRequest) {
    .cmd = CmdAbort,
    .cookie = htonl(cookie),
  };
  prv_receive_data(s_session, (const uint8_t *) &abort_msg, sizeof(abort_msg));
}

static void prv_receive_install(uint32_t cookie) {
  InstallRequest install_msg = (InstallRequest) {
    .cmd = CmdInstall,
    .cookie = htonl(cookie),
  };
  prv_receive_data(s_session, (const uint8_t *) &install_msg, sizeof(install_msg));
}

#define assert_ack_count(c) \
  { \
    fake_comm_session_process_send_next(); \
    cl_assert_equal_i(s_acks_received, c); \
  }

#define assert_nack_count(c) \
  { \
    fake_comm_session_process_send_next(); \
    cl_assert_equal_i(s_nacks_received, c); \
  }

#define assert_cleanup_event(object_type_, object_size_) \
  PebbleEvent event = fake_event_get_last(); \
  cl_assert_equal_i(event.type, PEBBLE_PUT_BYTES_EVENT); \
  cl_assert_equal_i(event.put_bytes.type, PebblePutBytesEventTypeCleanup); \
  cl_assert_equal_i(event.put_bytes.object_type, object_type_); \
  cl_assert_equal_i(event.put_bytes.total_size, object_size_); \
  cl_assert_equal_i(event.put_bytes.progress_percent, 0); \
  cl_assert_equal_b(event.put_bytes.failed, true); \

static void prv_receive_init_fw_object(void) {
  prv_receive_init(VALID_OBJECT_SIZE, ObjectFirmware);
  fake_comm_session_process_send_next();
  fake_system_task_callbacks_invoke_pending();
}

static void prv_process_and_reset_test_counters(void) {
  fake_comm_session_process_send_next();
  fake_system_task_callbacks_invoke_pending();
  s_acks_received = 0;
  s_nacks_received = 0;
}

static void prv_receive_init_and_put_fw_object(void) {
  prv_receive_init(VALID_OBJECT_SIZE, ObjectFirmware);
  fake_comm_session_process_send_next();
  fake_system_task_callbacks_invoke_pending();

  const uint8_t chunk[] = { 0xaa, 0xbb, 0xcc, 0xdd };
  prv_receive_put(s_last_response_cookie, chunk, sizeof(chunk));
  prv_process_and_reset_test_counters();
}

static void prv_receive_init_put_and_commit_fw_object(void) {
  prv_receive_init_and_put_fw_object();
  prv_receive_commit(s_last_response_cookie, EXPECTED_CRC);
  prv_process_and_reset_test_counters();
}

static void prv_receive_init_put_commit_and_install(PutBytesObjectType object_type) {
  prv_receive_init(VALID_OBJECT_SIZE, object_type);
  prv_process_and_reset_test_counters();

  const uint8_t chunk[] = { 0xaa, 0xbb, 0xcc, 0xdd };
  prv_receive_put(s_last_response_cookie, chunk, sizeof(chunk));
  prv_process_and_reset_test_counters();

  prv_receive_commit(s_last_response_cookie, EXPECTED_CRC);
  prv_process_and_reset_test_counters();

  prv_receive_install(s_last_response_cookie);
}

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

static void prv_system_msg_sent_callback(uint16_t endpoint_id,
                                         const uint8_t* data, unsigned int data_length) {
  if (endpoint_id != 0xBEEF) {
    // Not the put bytes endpoint, ignore
    return;
  }

  // We should only be getting ACKs and NACKs back on this endpoint, which are both 5 bytes long
  cl_assert_equal_i(data_length, 5);

  ResponseMsg *response_msg = (ResponseMsg *)data;
  s_last_response_cookie = ntohl(response_msg->cookie);
  if (response_msg->response == ResponseAck) {
    ++s_acks_received;
  } else if (response_msg->response == ResponseNack) {
    ++s_nacks_received;
  }
}

void test_put_bytes__initialize(void) {
  fake_pb_storage_mem_reset();
  fake_pb_storage_mem_set_crc(EXPECTED_CRC);
  fake_comm_session_init();
  fake_event_reset_count();

  Transport *transport = fake_transport_create(TransportDestinationSystem, NULL,
                                               prv_system_msg_sent_callback);
  s_session = fake_transport_set_connected(transport, true /* connected */);
  cl_assert_equal_p(comm_session_get_system_session(), s_session);

  prv_process_and_reset_test_counters();
  s_last_response_cookie = 0;
  s_boot_bits_orred = 0;
  s_do_before_write = NULL;

  // Common for most tests:
  s_firmware_update_is_in_progress = true;

  fake_spi_flash_init(0, 0x1000000);

  put_bytes_init();
}

void test_put_bytes__cleanup(void) {
  put_bytes_deinit();

  fake_comm_session_cleanup();
  fake_system_task_callbacks_cleanup();
  fake_event_clear_last();

  fake_pbl_malloc_check_net_allocs();
  fake_pbl_malloc_clear_tracking();
}

////////////////////////////////////////////////////////////////////////////////////////////////////
// Misc


static TickType_t prv_taking_too_long_yield_cb(QueueHandle_t queue) {
  return milliseconds_to_ticks(1000);
}

void test_put_bytes__lock_contention_upon_prepare_message(void) {
  // When the PutBytes lock is taken for a long time when a PutBytes message is prepared,
  // expect to receive a Nack:

  // Take and hold for a long time:
  xSemaphoreTake(put_bytes_get_semaphore(), portMAX_DELAY);
  fake_queue_set_yield_callback(put_bytes_get_semaphore(), prv_taking_too_long_yield_cb);

  prv_receive_init(4, ObjectFirmware);

  // Release it:
  xSemaphoreGive(put_bytes_get_semaphore());
  fake_queue_set_yield_callback(put_bytes_get_semaphore(), NULL);

  assert_nack_count(1);
}

static void prv_hold_lock_before_write(void) {
  // Take and hold for a long time:
  xSemaphoreTake(put_bytes_get_semaphore(), portMAX_DELAY);
  fake_queue_set_yield_callback(put_bytes_get_semaphore(), prv_taking_too_long_yield_cb);
}

void test_put_bytes__lock_contention_upon_write_message(void) {
  // When the PutBytes lock is taken for a long time when a PutBytes message is written,
  // expect to receive a Nack:

  s_do_before_write = prv_hold_lock_before_write;

  prv_receive_init(4, ObjectFirmware);

  // Release it:
  xSemaphoreGive(put_bytes_get_semaphore());
  fake_queue_set_yield_callback(put_bytes_get_semaphore(), NULL);

  assert_nack_count(1);
}

static void prv_cancel_before_write_second_message(void) {
  put_bytes_cancel();
}

void test_put_bytes__cancel_between_prepare_and_finish(void) {
  // When the put_bytes_cancel() is called while the PutBytes message is written (between "prepare"
  // and "finish"), expect to receive a Nack:

  prv_receive_init(4, ObjectWatchApp);
  assert_ack_count(1);
  assert_nack_count(0);

  s_do_before_write = prv_cancel_before_write_second_message;

  const uint8_t payload[] = { 0xaa, 0xbb, 0xcc };
  prv_receive_put(s_last_response_cookie, payload, sizeof(payload));

  assert_nack_count(1);
}

void test_put_bytes__invalid_command_opcode(void) {
  uint8_t invalid_cmd[] = { CmdInvalid };
  prv_receive_data(s_session, (const uint8_t *) invalid_cmd, sizeof(invalid_cmd));

  // Messages with invalid command opcodes are NACK'd:
  assert_ack_count(0);
  assert_nack_count(1);
}

////////////////////////////////////////////////////////////////////////////////////////////////////
// Init Message

void test_put_bytes__init_firmware(void) {
  prv_receive_init(VALID_OBJECT_SIZE, ObjectFirmware);

  // All good!
  assert_ack_count(1);
  assert_nack_count(0);

  // Expect "Start" event:
  PebbleEvent event = fake_event_get_last();
  cl_assert_equal_i(event.type, PEBBLE_PUT_BYTES_EVENT);
  cl_assert_equal_i(event.put_bytes.type, PebblePutBytesEventTypeStart);
  cl_assert_equal_i(event.put_bytes.object_type, ObjectFirmware);
  cl_assert_equal_i(event.put_bytes.total_size, VALID_OBJECT_SIZE);
  cl_assert_equal_i(event.put_bytes.progress_percent, 0);
  cl_assert_equal_b(event.put_bytes.failed, false);
}

void test_put_bytes__init_while_already_busy(void) {
  prv_receive_init(VALID_OBJECT_SIZE, ObjectFirmware);
  prv_receive_init(VALID_OBJECT_SIZE, ObjectFirmware);
  assert_nack_count(1);
}

void test_put_bytes__init_too_large(void) {
  prv_receive_init(UINT_MAX, ObjectFirmware);

  // Fail due to massive total_size in our init message
  assert_ack_count(0);
  assert_nack_count(1);
}

void test_put_bytes__init_msg_incomplete(void) {
  const uint8_t incomplete_init_msg = CmdInit;
  prv_receive_data(s_session, (const uint8_t *) &incomplete_init_msg,
                                  sizeof(incomplete_init_msg));
  assert_ack_count(0);
  assert_nack_count(1);
}

void test_put_bytes__init_invalid_object_type(void) {
  PutBytesObjectType invalid_object_type = 0xff;
  prv_receive_init(VALID_OBJECT_SIZE, invalid_object_type);

  // Fail due to massive total_size in our init message
  assert_ack_count(0);
  assert_nack_count(1);
}

void test_put_bytes__init_firmware_object_while_not_in_fw_update_mode(void) {
  s_firmware_update_is_in_progress = false;
  prv_receive_init(VALID_OBJECT_SIZE, ObjectFirmware);
  assert_ack_count(0);
  assert_nack_count(1);
}

void test_put_bytes__init_recovery_object_while_not_in_fw_update_mode(void) {
  s_firmware_update_is_in_progress = false;
  prv_receive_init(VALID_OBJECT_SIZE, ObjectRecovery);
  assert_ack_count(0);
  assert_nack_count(1);
}

void test_put_bytes__init_sys_resources_object_while_not_in_fw_update_mode(void) {
  s_firmware_update_is_in_progress = false;
  prv_receive_init(VALID_OBJECT_SIZE, ObjectSysResources);
  assert_ack_count(0);
  assert_nack_count(1);
}

void test_put_bytes__init_app_resources_okay_while_not_in_fw_update_mode(void) {
  s_firmware_update_is_in_progress = false;
  prv_receive_init_cookie(VALID_OBJECT_SIZE, ObjectAppResources, EXPECTED_COOKIE);
  assert_ack_count(1);
  assert_nack_count(0);

  cl_assert_equal_i(EXPECTED_COOKIE, put_bytes_get_index());
}

void test_put_bytes__init_watch_app_okay_while_not_in_fw_update_mode(void) {
  s_firmware_update_is_in_progress = false;
  prv_receive_init_cookie(VALID_OBJECT_SIZE, ObjectWatchApp, EXPECTED_COOKIE);
  assert_ack_count(1);
  assert_nack_count(0);

  cl_assert_equal_i(EXPECTED_COOKIE, put_bytes_get_index());
}

void test_put_bytes__init_file_okay_while_not_in_fw_update_mode(void) {
  s_firmware_update_is_in_progress = false;
  const char fn[] = "test.txt";
  prv_receive_init_file(VALID_OBJECT_SIZE, fn, strlen(fn) + 1);
  assert_ack_count(1);
  assert_nack_count(0);
}

void test_put_bytes__init_worker_okay_while_not_in_fw_update_mode(void) {
  s_firmware_update_is_in_progress = false;
  prv_receive_init_cookie(VALID_OBJECT_SIZE, ObjectWatchWorker, EXPECTED_COOKIE);
  assert_ack_count(1);
  assert_nack_count(0);

  cl_assert_equal_i(EXPECTED_COOKIE, put_bytes_get_index());
}

void test_put_bytes__init_nack_upon_oom(void) {
  fake_malloc_set_largest_free_block(1024);  // PutBytes allocates ~2K
  prv_receive_init(1024 * 1024, ObjectFirmware);

  fake_malloc_set_largest_free_block(~0);
  assert_ack_count(0);
  assert_nack_count(1);
}

////////////////////////////////////////////////////////////////////////////////////////////////////
// Put Message

void test_put_bytes__put_message_too_short(void) {
  prv_receive_init_fw_object();
  prv_process_and_reset_test_counters();

  const uint8_t incomplete_put_msg = CmdPut;
  prv_receive_data(s_session, (const uint8_t *) &incomplete_put_msg,
                                  sizeof(incomplete_put_msg));
  assert_ack_count(0);
  assert_nack_count(1);
}

void test_put_bytes__put_message_length_field_too_long(void) {
  prv_receive_init_fw_object();
  prv_process_and_reset_test_counters();

  const size_t payload_size = 2;
  const uint8_t chunk[] = { 0xaa, 0xbb };
  uint8_t buffer[sizeof(PutRequest) + payload_size];

  PutRequest *put_msg = (PutRequest *)buffer;
  *put_msg = (PutRequest) {
    .cmd = CmdPut,
    .cookie = htonl(s_last_response_cookie),
    .payload_size = htonl(payload_size) + 1 /* one off! */,
  };
  memcpy(&put_msg->payload[0], chunk, payload_size);
  prv_receive_data(s_session, buffer, sizeof(buffer));

  assert_ack_count(0);
  assert_nack_count(1);
}

void test_put_bytes__invalid_session_cookie(void) {
  prv_receive_init_fw_object();
  prv_process_and_reset_test_counters();

  const uint8_t chunk[] = { 0xaa, 0xbb, 0xcc };
  prv_receive_put(~s_last_response_cookie, chunk, sizeof(chunk));

  assert_ack_count(0);
  assert_nack_count(1);
}

void test_put_bytes__not_in_fw_update_mode(void) {
  prv_receive_init_fw_object();
  prv_process_and_reset_test_counters();

  s_firmware_update_is_in_progress = false;

  const uint8_t chunk[] = { 0xaa, 0xbb, 0xcc };
  prv_receive_put(s_last_response_cookie, chunk, sizeof(chunk));

  assert_ack_count(0);
  assert_nack_count(1);
}

void test_put_bytes__previous_chunk_not_acked_yet(void) {
  uint8_t max_put_ops = prv_put_bytes_get_max_batched_pb_ops();
  prv_receive_init(VALID_OBJECT_SIZE * max_put_ops, ObjectFirmware);
  prv_process_and_reset_test_counters();

  const uint8_t chunk[] = { 0xaa, 0xbb, 0xcc };
  uint8_t max_pb_ops = prv_put_bytes_get_max_batched_pb_ops();
  for (int i = 0; i <= max_pb_ops; i++) {
    prv_receive_put(s_last_response_cookie, chunk, sizeof(chunk));
  }

  assert_ack_count(max_pb_ops);
  assert_nack_count(1);
}

void test_put_bytes__chunk_too_large(void) {
  prv_receive_init_fw_object();
  prv_process_and_reset_test_counters();

  size_t chunk_size = 1024 * 1024;
  uint8_t *chunk = kernel_malloc(chunk_size);
  prv_receive_put(s_last_response_cookie, chunk, chunk_size);
  kernel_free(chunk);

  assert_ack_count(0);
  assert_nack_count(1);
}

void test_put_bytes__app_cancelled_before_chunk_got_processed(void) {
  prv_receive_init_cookie(VALID_OBJECT_SIZE, ObjectWatchApp, EXPECTED_COOKIE);
  prv_process_and_reset_test_counters();

  const uint8_t chunk[] = { 0xaa, 0xbb, 0xcc };
  prv_receive_put(s_last_response_cookie, chunk, sizeof(chunk));

  put_bytes_cancel();

  assert_cleanup_event(ObjectWatchApp, VALID_OBJECT_SIZE);

  if (prv_put_bytes_get_max_batched_pb_ops() > 1) {
    // With pre-acking, the put will have already been ack'ed and then a Nack will follow
    assert_ack_count(1);
  } else {
    assert_ack_count(0);
  }
  assert_nack_count(1);
}

void test_put_bytes__chunk_written_to_storage_and_progress_event_put(void) {
  prv_receive_init_fw_object();
  prv_process_and_reset_test_counters();

  const uint8_t chunk[] = { 0xaa, 0xbb, 0xcc };
  prv_receive_put(s_last_response_cookie, chunk, sizeof(chunk));

  assert_ack_count(1);
  assert_nack_count(0);

  fake_pb_storage_mem_assert_contents_written(chunk, sizeof(chunk));

  PebbleEvent event = fake_event_get_last();
  cl_assert_equal_i(event.type, PEBBLE_PUT_BYTES_EVENT);
  cl_assert_equal_i(event.put_bytes.type, PebblePutBytesEventTypeProgress);
  cl_assert_equal_i(event.put_bytes.object_type, ObjectFirmware);
  cl_assert_equal_i(event.put_bytes.bytes_transferred, sizeof(chunk));
  cl_assert_equal_i(event.put_bytes.progress_percent, 100 * sizeof(chunk) / VALID_OBJECT_SIZE);
  cl_assert_equal_b(event.put_bytes.failed, false);
}

static uint32_t s_next_value_to_write;
static void prv_cb_before_write(void) {
  prv_receive_put(s_last_response_cookie, (uint8_t *)&s_next_value_to_write, VALID_OBJECT_SIZE);
}

void test_put_bytes__receive_batched_messages(void) {
  uint8_t max_batched_ops = prv_put_bytes_get_max_batched_pb_ops();
  int num_ops = 500;

  if (max_batched_ops < 2) { // This race condition is not possible if we aren't pre-Acking
    return;
  }

  prv_receive_init(VALID_OBJECT_SIZE * num_ops, ObjectFirmware);
  fake_comm_session_process_send_next();
  fake_system_task_callbacks_invoke_pending();

  uint8_t buffer[num_ops * VALID_OBJECT_SIZE];
  for (size_t i = 0; i < sizeof(buffer); i += VALID_OBJECT_SIZE) {
    uint32_t towrite = i;
    memcpy(&buffer[i], &towrite, sizeof(towrite));
  }

  // Make sure we can receive new data in the middle of a pb_storage_append operation
  for (int i = 0; i < num_ops; i += 2) {
    int idx = i * VALID_OBJECT_SIZE;
    prv_receive_put(s_last_response_cookie, &buffer[idx], VALID_OBJECT_SIZE);

    idx += VALID_OBJECT_SIZE;
    memcpy(&s_next_value_to_write, &buffer[idx], VALID_OBJECT_SIZE);
    fake_pb_storage_register_cb_before_write(prv_cb_before_write);

    fake_comm_session_process_send_next();
    fake_system_task_callbacks_invoke_pending();
  }

  fake_pb_storage_mem_assert_contents_written(buffer, sizeof(buffer));
}

////////////////////////////////////////////////////////////////////////////////////////////////////
// Commit Message

void test_put_bytes__commit_message_too_short(void) {
  prv_receive_init_and_put_fw_object();

  const uint8_t incomplete_put_msg = CmdCommit;
  prv_receive_data(s_session, (const uint8_t *) &incomplete_put_msg,
                                  sizeof(incomplete_put_msg));
  assert_ack_count(0);
  assert_nack_count(1);
}

void test_put_bytes__commit_message_sent_while_previous_put_was_not_acked_yet(void) {
  uint8_t max_put_ops = prv_put_bytes_get_max_batched_pb_ops();
  prv_receive_init(VALID_OBJECT_SIZE * max_put_ops, ObjectFirmware);
  prv_process_and_reset_test_counters();

  const uint8_t chunk[] = { 0xaa, 0xbb, 0xcc, 0xdd };
  for (int i = 0; i < max_put_ops; i++) {
    prv_receive_put(s_last_response_cookie, chunk, sizeof(chunk));
  }

  prv_receive_commit(s_last_response_cookie, EXPECTED_CRC);
  assert_ack_count(max_put_ops);  // For the Put(s)
  assert_nack_count(1); // For the Commit
}

void test_put_bytes__commit_message_cookie_mismatch(void) {
  prv_receive_init_and_put_fw_object();

  prv_receive_commit(~s_last_response_cookie, EXPECTED_CRC);
  assert_ack_count(0);
  assert_nack_count(1);
}

void test_put_bytes__commit_message_while_not_in_fw_update_mode(void) {
  prv_receive_init_and_put_fw_object();

  s_firmware_update_is_in_progress = false;
  prv_receive_commit(s_last_response_cookie, EXPECTED_CRC);
  assert_ack_count(0);
  assert_nack_count(1);
}

void test_put_bytes__commit_message_crc_mismatch(void) {
  prv_receive_init_and_put_fw_object();

  prv_receive_commit(s_last_response_cookie, ~EXPECTED_CRC);
  assert_ack_count(0);
  assert_nack_count(1);
}

void test_put_bytes__commit_message_fw_description_is_written(void) {
  prv_receive_init_and_put_fw_object();
  prv_receive_commit(s_last_response_cookie, EXPECTED_CRC);
  fake_comm_session_process_send_next();
  fake_system_task_callbacks_invoke_pending();

  // Assert the FW description got written at the beginning of the storage:
  const FirmwareDescription fw_descr = {
    .description_length = sizeof(FirmwareDescription),
    .firmware_length = VALID_OBJECT_SIZE,
    .checksum = EXPECTED_CRC,
  };
  fake_pb_storage_mem_assert_fw_description_written(&fw_descr);
}

////////////////////////////////////////////////////////////////////////////////////////////////////
// Abort Message

void test_put_bytes__abort_message_too_short(void) {
  prv_receive_init_and_put_fw_object();
  prv_process_and_reset_test_counters();

  const uint8_t incomplete_abort_msg = CmdAbort;
  prv_receive_data(s_session, (const uint8_t *) &incomplete_abort_msg,
                                  sizeof(incomplete_abort_msg));
  assert_ack_count(0);
  assert_nack_count(1);
}

void test_put_bytes__abort_message_cookie_mismatch(void) {
  prv_receive_init_and_put_fw_object();
  prv_process_and_reset_test_counters();

  prv_receive_abort(~s_last_response_cookie);

  assert_ack_count(0);
  assert_nack_count(1);
}

void test_put_bytes__abort_message_ok(void) {
  prv_receive_init_and_put_fw_object();
  prv_process_and_reset_test_counters();

  prv_receive_abort(s_last_response_cookie);

  assert_ack_count(1);
  assert_nack_count(0);
  assert_cleanup_event(ObjectFirmware, VALID_OBJECT_SIZE);
}

////////////////////////////////////////////////////////////////////////////////////////////////////
// Install Message

void test_put_bytes__install_message_while_not_idle(void) {
  prv_receive_init(VALID_OBJECT_SIZE, ObjectFirmware);
  prv_process_and_reset_test_counters();

  prv_receive_install(s_last_response_cookie);
  assert_ack_count(0);
  assert_nack_count(1);
}

void test_put_bytes__install_message_too_short(void) {
  prv_receive_init_put_and_commit_fw_object();

  const uint8_t incomplete_install_msg = CmdInstall;
  prv_receive_data(s_session, (const uint8_t *) &incomplete_install_msg,
                                  sizeof(incomplete_install_msg));
  assert_ack_count(0);
  assert_nack_count(1);
}

void test_put_bytes__install_message_while_not_in_fw_update_mode(void) {
  prv_receive_init_put_and_commit_fw_object();

  s_firmware_update_is_in_progress = false;
  prv_receive_install(s_last_response_cookie);
  assert_ack_count(0);
  assert_nack_count(1);
}

void test_put_bytes__install_message_cookie_mismatch(void) {
  prv_receive_init_put_and_commit_fw_object();
  prv_receive_install(~s_last_response_cookie);
  assert_ack_count(0);
  assert_nack_count(1);
}

void test_put_bytes__install_message_prf_boot_bit_set(void) {
  prv_receive_init_put_commit_and_install(ObjectRecovery);
  assert_ack_count(1);
  assert_nack_count(0);
  cl_assert_equal_i((s_boot_bits_orred & BOOT_BIT_NEW_PRF_AVAILABLE), BOOT_BIT_NEW_PRF_AVAILABLE);
}

void test_put_bytes__install_message_fw_and_sys_resources_boot_bits_set(void) {
  // Firmware object:
  prv_receive_init_put_commit_and_install(ObjectFirmware);
  assert_ack_count(1);
  assert_nack_count(0);

  // Expect boot bit not to be set yet:
  cl_assert_equal_i((s_boot_bits_orred & BOOT_BIT_NEW_FW_AVAILABLE), 0);

  // System Resources object:
  prv_receive_init_put_commit_and_install(ObjectSysResources);
  assert_ack_count(1);
  assert_nack_count(0);

  // Finally, expect both boot bits to be set at once:
  cl_assert_equal_i((s_boot_bits_orred & BOOT_BIT_NEW_FW_AVAILABLE), BOOT_BIT_NEW_FW_AVAILABLE);
  cl_assert_equal_i((s_boot_bits_orred & BOOT_BIT_NEW_SYSTEM_RESOURCES_AVAILABLE),
                    BOOT_BIT_NEW_SYSTEM_RESOURCES_AVAILABLE);
}

////////////////////////////////////////////////////////////////////////////////////////////////////
// Timeouts

void test_put_bytes__init_starts_timeout_timer(void) {
  prv_receive_init_fw_object();

  TimerID timer_id = put_bytes_get_timer_id();
  cl_assert_equal_b(true, stub_new_timer_is_scheduled(timer_id));
  cl_assert_equal_i(PUT_BYTES_TIMEOUT_MS, stub_new_timer_timeout(timer_id));
}

void test_put_bytes__put_chunk_restarts_timeout_timer(void) {
  prv_receive_init_fw_object();

  // Stop the timer, so we can easily detect it gets restarted again:
  TimerID timer_id = put_bytes_get_timer_id();
  new_timer_stop(timer_id);

  const uint8_t chunk[] = { 0xaa, 0xbb, 0xcc };
  prv_receive_put(s_last_response_cookie, chunk, sizeof(chunk));
  fake_system_task_callbacks_invoke_pending();

  cl_assert_equal_b(true, stub_new_timer_is_scheduled(timer_id));
  cl_assert_equal_i(PUT_BYTES_TIMEOUT_MS, stub_new_timer_timeout(timer_id));
}

void test_put_bytes__after_timeout_cleanup_and_allow_init_again(void) {
  prv_receive_init_fw_object();
  assert_ack_count(1);
  assert_nack_count(0);

  stub_new_timer_fire(put_bytes_get_timer_id());
  fake_system_task_callbacks_invoke_pending();

  cl_assert_equal_b(fake_pb_storage_mem_get_last_success(), false);

  assert_cleanup_event(ObjectFirmware, VALID_OBJECT_SIZE);

  // Send "Init" again:
  prv_receive_init_fw_object();
  assert_ack_count(2);
  assert_nack_count(0);
}

////////////////////////////////////////////////////////////////////////////////////////////////////
// put_bytes_expect_init

void test_put_bytes__expect_init_noop_while_not_idle(void) {
  cl_assert(EXPECT_INIT_TIMEOUT_MS != PUT_BYTES_TIMEOUT_MS);

  prv_receive_init_fw_object();

  put_bytes_expect_init(EXPECT_INIT_TIMEOUT_MS);

  // The timer is still not overridden by the "expect_init" timer:
  cl_assert_equal_i(stub_new_timer_timeout(put_bytes_get_timer_id()), PUT_BYTES_TIMEOUT_MS);
}

void test_put_bytes__expect_init_no_event_when_init_received(void) {
  put_bytes_expect_init(EXPECT_INIT_TIMEOUT_MS);

  prv_receive_init_fw_object();

  // The timer is overridden by the 30s Put Bytes timeout:
  cl_assert_equal_i(stub_new_timer_timeout(put_bytes_get_timer_id()), PUT_BYTES_TIMEOUT_MS);

  fake_event_reset_count();
  stub_new_timer_fire(put_bytes_get_timer_id());
  fake_system_task_callbacks_invoke_pending();

  // Expect only "Cleanup" event:
  cl_assert_equal_i(fake_event_get_count(), 1);
  assert_cleanup_event(ObjectFirmware, VALID_OBJECT_SIZE);
}

void test_put_bytes__expect_init_event_upon_timeout(void) {
  put_bytes_expect_init(EXPECT_INIT_TIMEOUT_MS);

  stub_new_timer_fire(put_bytes_get_timer_id());

  PebbleEvent event = fake_event_get_last();
  cl_assert_equal_i(event.type, PEBBLE_PUT_BYTES_EVENT);
  cl_assert_equal_i(event.put_bytes.type, PebblePutBytesEventTypeInitTimeout);
  cl_assert_equal_i(event.put_bytes.object_type, ObjectUnknown);
  cl_assert_equal_i(event.put_bytes.total_size, 0);
  cl_assert_equal_i(event.put_bytes.progress_percent, 0);
  cl_assert_equal_b(event.put_bytes.failed, true);
}

////////////////////////////////////////////////////////////////////////////////////////////////////
// put_bytes_handle_remote_app_event

void test_put_bytes__session_closed_after_fw_init(void) {
  prv_receive_init_fw_object();

  PebbleCommSessionEvent app_event = {
    .is_open = false,
    .is_system = true
  };

  // Close the BT session, have put_bytes react
  put_bytes_handle_comm_session_event(&app_event);
  fake_system_task_callbacks_invoke_pending();

  assert_cleanup_event(ObjectFirmware, VALID_OBJECT_SIZE);
}


void test_put_bytes__session_closed_after_expect_init(void) {
  put_bytes_expect_init(EXPECT_INIT_TIMEOUT_MS);

  PebbleCommSessionEvent app_event = {
    .is_open = false,
    .is_system = true
  };

  // Close the BT session, have put_bytes react
  put_bytes_handle_comm_session_event(&app_event);
  fake_system_task_callbacks_invoke_pending();

  assert_cleanup_event(ObjectUnknown, 0);
}
