/*
 * 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 "clar.h"
#include "test_jerry_port_common.h"
#include "test_rocky_common.h"

#include "applib/rockyjs/api/rocky_api_global.h"
#include "applib/rockyjs/api/rocky_api_app_message.h"
#include "applib/rockyjs/pbl_jerry_port.h"

#include "applib/app_message/app_message.h"
#include "util/dict.h"
#include "util/size.h"

#include <string.h>

// Fakes
#include "fake_app_timer.h"
#include "fake_event_service.h"
#include "fake_pbl_malloc.h"
#include "fake_time.h"

// Stubs
#include "stubs_app_state.h"
#include "stubs_comm_session.h"
#include "stubs_logging.h"
#include "stubs_passert.h"
#include "stubs_serial.h"
#include "stubs_sys_exit.h"

extern PostMessageState rocky_api_app_message_get_state(void);
extern AppTimer *rocky_api_app_message_get_app_msg_retry_timer(void);
extern AppTimer *rocky_api_app_message_get_session_closed_object_queue_timer(void);

T_STATIC jerry_value_t prv_json_stringify(jerry_value_t object);
T_STATIC jerry_value_t prv_json_parse(const char *);

T_STATIC void prv_handle_connection(void);
T_STATIC void prv_handle_disconnection(void);

// App message mocks

static AppMessageInboxReceived s_received_callback;
AppMessageInboxReceived app_message_register_inbox_received(
                                                        AppMessageInboxReceived received_callback) {
  AppMessageInboxReceived prev_cb = s_received_callback;
  s_received_callback = received_callback;
  return prev_cb;
}

static AppMessageInboxDropped s_dropped_callback;
AppMessageInboxDropped app_message_register_inbox_dropped(AppMessageInboxDropped dropped_callback) {
  AppMessageInboxDropped prev_cb = s_dropped_callback;
  s_dropped_callback = dropped_callback;
  return prev_cb;
}

static AppMessageOutboxSent s_sent_callback;
AppMessageOutboxSent app_message_register_outbox_sent(AppMessageOutboxSent sent_callback) {
  AppMessageOutboxSent prev_cb = s_sent_callback;
  s_sent_callback = sent_callback;
  return prev_cb;
}

static AppMessageOutboxFailed s_failed_callback;
AppMessageOutboxFailed app_message_register_outbox_failed(AppMessageOutboxFailed failed_callback) {
  AppMessageOutboxFailed prev_cb = s_failed_callback;
  s_failed_callback = failed_callback;
  return prev_cb;
}

void app_message_deregister_callbacks(void) {
  s_received_callback = NULL;
  s_dropped_callback = NULL;
  s_sent_callback = NULL;
  s_failed_callback = NULL;
}

static uint32_t s_inbox_size;
static uint32_t s_outbox_size;
AppMessageResult app_message_open(const uint32_t size_inbound, const uint32_t size_outbound) {
  s_inbox_size = size_inbound;
  s_outbox_size = size_outbound;
  return APP_MSG_OK;
}

static bool s_is_outbox_message_pending;
static DictionaryIterator s_outbox_iterator;
static uint8_t *s_outbox_buffer;
AppMessageResult app_message_outbox_begin(DictionaryIterator **iterator) {
  cl_assert_equal_b(s_is_outbox_message_pending, false);
  if (!s_outbox_buffer) {
    s_outbox_buffer = malloc(s_outbox_size);
  }
  dict_write_begin(&s_outbox_iterator, s_outbox_buffer, s_outbox_size);
  *iterator = &s_outbox_iterator;

  return APP_MSG_OK;
}

static int s_app_message_outbox_send_call_count;
AppMessageResult app_message_outbox_send(void) {
  ++s_app_message_outbox_send_call_count;
  s_is_outbox_message_pending = true;
  return APP_MSG_OK;
}

static bool s_comm_session_connected;
CommSession *sys_app_pp_get_comm_session(void) {
  return (CommSession *)s_comm_session_connected;
}

static void prv_rcv_app_message_ack(AppMessageResult result) {
  void *context = NULL;
  cl_assert_equal_b(s_is_outbox_message_pending, true);
  s_is_outbox_message_pending = false;
  if (result == APP_MSG_OK) {
    s_sent_callback(&s_outbox_iterator, context);
  } else {
    s_failed_callback(&s_outbox_iterator, result, context);
  }
}

static void prv_app_message_setup(void) {
  s_inbox_size = 0;
  s_outbox_size = 0;
  s_outbox_buffer = NULL;
  s_app_message_outbox_send_call_count = 0;
  s_is_outbox_message_pending = false;
  app_message_deregister_callbacks();
}

static void prv_app_message_teardown(void) {
  if (s_outbox_buffer) {
    free(s_outbox_buffer);
  }
}

// Statics and Utilities

static void prv_init_and_goto_session_open(void);

static void prv_simulate_transport_connection_event(bool is_connected) {
  // FIXME: use events here instead of poking at the internals!
  if (is_connected) {
    prv_handle_connection();
  } else {
    prv_handle_disconnection();
  }
}

static const RockyGlobalAPI *s_app_message_api[] = {
  &APP_MESSAGE_APIS,
  NULL,
};

static void prv_init_api(bool start_connected) {
  s_comm_session_connected = start_connected;
  rocky_global_init(s_app_message_api);
}

// Setup

void test_rocky_api_app_message__initialize(void) {
  fake_app_timer_init();
  fake_pbl_malloc_clear_tracking();
  prv_app_message_setup();

  s_process_manager_callback = NULL;
  s_process_manager_callback_data = NULL;

  rocky_runtime_context_init();
  jerry_init(JERRY_INIT_EMPTY);
}

void test_rocky_api_app_message__cleanup(void) {
  rocky_global_deinit();
  jerry_cleanup();
  rocky_runtime_context_deinit();
  prv_app_message_teardown();
  fake_pbl_malloc_check_net_allocs();
  fake_app_timer_deinit();
}

static const PostMessageResetCompletePayload VALID_RESET_COMPLETE = {
  .min_supported_version = POSTMESSAGE_PROTOCOL_MIN_VERSION,
  .max_supported_version = POSTMESSAGE_PROTOCOL_MAX_VERSION,
  .max_tx_chunk_size = POSTMESSAGE_PROTOCOL_MAX_TX_CHUNK_SIZE,
  .max_rx_chunk_size = POSTMESSAGE_PROTOCOL_MAX_RX_CHUNK_SIZE,
};

static const size_t TINY_CHUNK_SIZE = 4;

static const PostMessageResetCompletePayload TINY_RESET_COMPLETE = {
  .min_supported_version = POSTMESSAGE_PROTOCOL_MIN_VERSION,
  .max_supported_version = POSTMESSAGE_PROTOCOL_MAX_VERSION,
  .max_tx_chunk_size = TINY_CHUNK_SIZE,
  .max_rx_chunk_size = TINY_CHUNK_SIZE,
};

#define RCV_APP_MESSAGE(...) \
  do { \
    Tuplet tuplets[] = { __VA_ARGS__ }; \
    uint32_t buffer_size = dict_calc_buffer_size_from_tuplets(tuplets, ARRAY_LENGTH(tuplets)); \
    uint8_t buffer[buffer_size]; \
    DictionaryIterator it; \
    const DictionaryResult result = \
        dict_serialize_tuplets_to_buffer_with_iter(&it, tuplets, ARRAY_LENGTH(tuplets), \
                                                   buffer, &buffer_size); \
    cl_assert_equal_i(DICT_OK, result); \
    if (s_received_callback) { \
      s_received_callback(&it, NULL); \
    } \
  } while(0);


#define RCV_RESET_REQUEST() \
  RCV_APP_MESSAGE(TupletBytes(PostMessageKeyResetRequest, NULL, 0));

#define RCV_RESET_COMPLETE() \
  RCV_APP_MESSAGE(TupletBytes(PostMessageKeyResetComplete, \
                  (const uint8_t *)&VALID_RESET_COMPLETE, sizeof(VALID_RESET_COMPLETE)));

#define RCV_DUMMY_CHUNK() \
  do { \
    PostMessageChunkPayload chunk = {}; \
    RCV_APP_MESSAGE(TupletBytes(PostMessageKeyChunk, (const uint8_t *) &chunk, sizeof(chunk))); \
  } while(0);

//! Asserts whether the outbox has a pending message containing the tuples passed to this macro.
//! The value and type of the tuples is also asserted.
//! @note Only asserts if expected tuples are MISSING. It will not trip if there are other
//! (non-expected) tuples in the set.
#define EXPECT_OUTBOX_MESSAGE_PENDING(...) \
  do { \
    cl_assert_equal_b(true, s_is_outbox_message_pending); \
    /* The cursor must be updated! */ \
    cl_assert(s_outbox_iterator.cursor != s_outbox_iterator.dictionary->head); \
    Tuplet tuplets[] = { __VA_ARGS__ }; \
    uint32_t buffer_size = dict_calc_buffer_size_from_tuplets(tuplets, ARRAY_LENGTH(tuplets)); \
    uint8_t buffer[buffer_size]; \
    DictionaryIterator expected_it; \
    const DictionaryResult result = \
        dict_serialize_tuplets_to_buffer_with_iter(&expected_it, tuplets, ARRAY_LENGTH(tuplets), \
                                                   buffer, &buffer_size); \
    cl_assert_equal_i(DICT_OK, result); \
    for (Tuple *expected_t = dict_read_first(&expected_it); expected_t != NULL; \
         expected_t = dict_read_next(&expected_it)) { \
      Tuple *found_t = dict_find(&s_outbox_iterator, expected_t->key); \
      cl_assert(found_t); \
      cl_assert_equal_i(found_t->type, expected_t->type); \
      cl_assert_equal_i(found_t->length, expected_t->length); \
      if (expected_t->length) { \
        cl_assert_equal_i(0, memcmp(found_t->value[0].data, expected_t->value[0].data, \
                                    expected_t->length)); \
      } \
    } \
  } while (0);

#define EXPECT_OUTBOX_NO_MESSAGE_PENDING() \
  cl_assert_equal_b(false, s_is_outbox_message_pending);

#define EXPECT_OUTBOX_RESET_REQUEST() \
  EXPECT_OUTBOX_MESSAGE_PENDING(TupletBytes(PostMessageKeyResetRequest, NULL, 0));

#define EXPECT_OUTBOX_RESET_COMPLETE_PENDING() \
  EXPECT_OUTBOX_MESSAGE_PENDING(TupletBytes(PostMessageKeyResetComplete, \
                                            (const uint8_t *) &VALID_RESET_COMPLETE, \
                                            sizeof(VALID_RESET_COMPLETE)));

////////////////////////////////////////////////////////////////////////////////
// Negotiation Steps
////////////////////////////////////////////////////////////////////////////////

void test_rocky_api_app_message__disconnected__ignore_any_app_message(void) {
  prv_init_api(false /* start_connected */);

  for (PostMessageKey key = PostMessageKeyResetRequest; key < PostMessageKey_Count; ++key) {
    uint8_t dummy_data[] = {0, 1, 2};
    RCV_APP_MESSAGE(TupletBytes(key, dummy_data, sizeof(dummy_data)));
  }

  cl_assert_equal_i(0, s_app_message_outbox_send_call_count);
  cl_assert_equal_i(rocky_api_app_message_get_state(), PostMessageStateDisconnected);
}

void test_rocky_api_app_message__awaiting_reset_request__receive_reset_request(void) {
  prv_init_api(true /* start_connected */);

  RCV_RESET_REQUEST();

  // Expect responding with a ResetComplete:
  EXPECT_OUTBOX_RESET_COMPLETE_PENDING();

  cl_assert_equal_i(rocky_api_app_message_get_state(),
                    PostMessageStateAwaitingResetCompleteRemoteInitiated);
}

void test_rocky_api_app_message__awaiting_reset_request__receive_chunk(void) {
  prv_init_api(false /* start_connected */);
  prv_simulate_transport_connection_event(true /* is_connected */);

  RCV_DUMMY_CHUNK();
  // https://pebbletechnology.atlassian.net/browse/PBL-42466
  // TODO: assert that app message was NACK'd

  // Expect responding with a ResetRequest:
  EXPECT_OUTBOX_RESET_REQUEST();
  EXPECT_OUTBOX_MESSAGE_PENDING(TupletBytes(PostMessageKeyResetRequest, NULL, 0));
  // TODO: check fields

  cl_assert_equal_i(rocky_api_app_message_get_state(),
                    PostMessageStateAwaitingResetCompleteLocalInitiated);
}

void test_rocky_api_app_message__awaiting_reset_request__disconnect(void) {
  prv_init_api(false /* start_connected */);
  prv_simulate_transport_connection_event(true /* is_connected */);
  prv_simulate_transport_connection_event(false /* is_connected */);
  EXPECT_OUTBOX_NO_MESSAGE_PENDING();
  cl_assert_equal_i(rocky_api_app_message_get_state(), PostMessageStateDisconnected);
}

static void prv_init_and_goto_awaiting_reset_complete_remote_initiated(void) {
  prv_init_api(true /* start_connected */);

  RCV_RESET_REQUEST();

  EXPECT_OUTBOX_RESET_COMPLETE_PENDING();
  prv_rcv_app_message_ack(APP_MSG_OK);

  cl_assert_equal_i(rocky_api_app_message_get_state(),
                    PostMessageStateAwaitingResetCompleteRemoteInitiated);
}

static void prv_init_and_goto_awaiting_reset_complete_local_initiated(void) {
  prv_init_and_goto_awaiting_reset_complete_remote_initiated();
  RCV_DUMMY_CHUNK();
  EXPECT_OUTBOX_RESET_REQUEST();
  prv_rcv_app_message_ack(APP_MSG_OK);
  cl_assert_equal_i(rocky_api_app_message_get_state(),
                    PostMessageStateAwaitingResetCompleteLocalInitiated);
}

void test_rocky_api_app_message__awaiting_reset_complete_rem_init__receive_complete_valid_version(void) {
  prv_init_and_goto_awaiting_reset_complete_remote_initiated();

  RCV_RESET_COMPLETE();

  cl_assert_equal_i(rocky_api_app_message_get_state(), PostMessageStateSessionOpen);
  EXPECT_OUTBOX_NO_MESSAGE_PENDING();
}

void test_rocky_api_app_message__awaiting_reset_complete_rem_init__receive_complete_unsupported_ver(void) {
  prv_init_and_goto_awaiting_reset_complete_remote_initiated();

  const PostMessageResetCompletePayload unsupported = {
    .min_supported_version = POSTMESSAGE_PROTOCOL_MAX_VERSION + 1,
    .max_supported_version = POSTMESSAGE_PROTOCOL_MAX_VERSION + 1,
    .max_tx_chunk_size = POSTMESSAGE_PROTOCOL_MAX_TX_CHUNK_SIZE,
    .max_rx_chunk_size = POSTMESSAGE_PROTOCOL_MAX_RX_CHUNK_SIZE,
  };
  RCV_APP_MESSAGE(TupletBytes(PostMessageKeyResetComplete,
                              (const uint8_t *)&unsupported, sizeof(unsupported)));

  // Expect No UnsupportedError!

  // Immediately go back to AwaitingResetRequest:
  cl_assert_equal_i(rocky_api_app_message_get_state(), PostMessageStateAwaitingResetRequest);
  EXPECT_OUTBOX_NO_MESSAGE_PENDING();
}

void test_rocky_api_app_message__awaiting_reset_complete_rem_init__malformed_reset_complete(void) {
  prv_init_and_goto_awaiting_reset_complete_remote_initiated();

  // Receive malformed ResetComplete:
  uint8_t malformed_payload[sizeof(PostMessageResetCompletePayload) - 1] = {};
  RCV_APP_MESSAGE(TupletBytes(PostMessageKeyResetComplete,
                              malformed_payload, sizeof(malformed_payload)));

  // Expect Error:
  const PostMessageUnsupportedErrorPayload expected_error = {
    .error_code = PostMessageErrorMalformedResetComplete,
  };
  EXPECT_OUTBOX_MESSAGE_PENDING(TupletBytes(PostMessageKeyUnsupportedError,
                                            (const uint8_t *) &expected_error,
                                            sizeof(expected_error)));

  // Immediately go back to AwaitingResetRequest:
  cl_assert_equal_i(rocky_api_app_message_get_state(), PostMessageStateAwaitingResetRequest);
  prv_rcv_app_message_ack(APP_MSG_OK);
  EXPECT_OUTBOX_NO_MESSAGE_PENDING();
}

void test_rocky_api_app_message__awaiting_reset_complete_rem_init__receive_request(void) {
  prv_init_and_goto_awaiting_reset_complete_remote_initiated();

  RCV_RESET_REQUEST();

  cl_assert_equal_i(rocky_api_app_message_get_state(),
                    PostMessageStateAwaitingResetCompleteRemoteInitiated);

  EXPECT_OUTBOX_RESET_COMPLETE_PENDING();
}

void test_rocky_api_app_message__awaiting_reset_complete_rem_init__receive_chunk(void) {
  prv_init_and_goto_awaiting_reset_complete_remote_initiated();

  RCV_DUMMY_CHUNK();

  EXPECT_OUTBOX_RESET_REQUEST();

  cl_assert_equal_i(rocky_api_app_message_get_state(),
                    PostMessageStateAwaitingResetCompleteLocalInitiated);

  // Receive yet another chunk in "Awaiting Reset Complete Local Initiated":
  RCV_DUMMY_CHUNK();
  // https://pebbletechnology.atlassian.net/browse/PBL-42466
  // TODO: assert that chunk is NACKd

  // Receive ACK for the ResetRequest:
  prv_rcv_app_message_ack(APP_MSG_OK);

  // Chunk is ignored, no new reset request is sent out.
  EXPECT_OUTBOX_NO_MESSAGE_PENDING();

  // TODO: timeout + retry ResetRequest if no ResetComplete follows within N secs.
}

void test_rocky_api_app_message__awaiting_reset_complete_loc_init__(void) {
  prv_init_and_goto_awaiting_reset_complete_local_initiated();
}

void test_rocky_api_app_message__awaiting_reset_complete_loc_init__rcv_reset_request(void) {
  prv_init_and_goto_awaiting_reset_complete_local_initiated();

  RCV_RESET_REQUEST();

  EXPECT_OUTBOX_RESET_COMPLETE_PENDING();
  prv_rcv_app_message_ack(APP_MSG_OK);

  cl_assert_equal_i(rocky_api_app_message_get_state(),
                    PostMessageStateAwaitingResetCompleteRemoteInitiated);

  RCV_RESET_COMPLETE();

  cl_assert_equal_i(rocky_api_app_message_get_state(), PostMessageStateSessionOpen);
}

void test_rocky_api_app_message__awaiting_reset_complete_loc_init__rcv_chunk(void) {
  prv_init_and_goto_awaiting_reset_complete_local_initiated();

  RCV_DUMMY_CHUNK();

  // https://pebbletechnology.atlassian.net/browse/PBL-42466
  // TODO: assert that chunk is NACK'd

  // Chunk is ignored, state isn't changed:
  cl_assert_equal_i(rocky_api_app_message_get_state(),
                    PostMessageStateAwaitingResetCompleteLocalInitiated);
  EXPECT_OUTBOX_NO_MESSAGE_PENDING();
}

static void prv_init_and_goto_session_open(void) {
  prv_init_and_goto_awaiting_reset_complete_remote_initiated();
  RCV_RESET_COMPLETE();
  cl_assert_equal_i(rocky_api_app_message_get_state(), PostMessageStateSessionOpen);
}

void test_rocky_api_app_message__session_open__rcv_reset_request(void) {
  prv_init_and_goto_session_open();

  EXECUTE_SCRIPT("var isCalled = false;"
                 "_rocky.on('postmessagedisconnected', function() { isCalled = true; });");

  ASSERT_JS_GLOBAL_EQUALS_B("isCalled", false);

  RCV_RESET_REQUEST();

  cl_assert_equal_i(rocky_api_app_message_get_state(),
                    PostMessageStateAwaitingResetCompleteRemoteInitiated);
  EXPECT_OUTBOX_RESET_COMPLETE_PENDING();

  ASSERT_JS_GLOBAL_EQUALS_B("isCalled", true);

  // TODO: assert:
  // - flushed recv chunk reassembly buffer
}

void test_rocky_api_app_message__session_open__rcv_reset_complete(void) {
  prv_init_and_goto_session_open();

  EXECUTE_SCRIPT("var isCalled = false;"
                 "_rocky.on('postmessagedisconnected', function() { isCalled = true; });");

  ASSERT_JS_GLOBAL_EQUALS_B("isCalled", false);

  RCV_RESET_COMPLETE();

  cl_assert_equal_i(rocky_api_app_message_get_state(),
                    PostMessageStateAwaitingResetCompleteLocalInitiated);
  EXPECT_OUTBOX_RESET_REQUEST();

  ASSERT_JS_GLOBAL_EQUALS_B("isCalled", true);

  // TODO: assert:
  // - flushed recv chunk reassembly buffer
}

////////////////////////////////////////////////////////////////////////////////
// postmessageconnected / postmessagedisconnected
////////////////////////////////////////////////////////////////////////////////

static void prv_postmessageconnected_postmessagedisconnected_init(bool start_connected) {
  prv_init_api(start_connected);

  EXECUTE_SCRIPT("var c = 0; var d = 0;\n"
                 "_rocky.on('postmessageconnected', function() { c++; });\n"
                 "_rocky.on('postmessagedisconnected', function() { d++; });\n");

  // Make sure this race is handled (see comment in prv_handle_connection()):
  prv_simulate_transport_connection_event(start_connected /* is_connected */);
}

static void prv_postmessageconnected_postmessagedisconnected_negotiate_to_open_session(void) {
  // Negotiate:
  RCV_RESET_REQUEST();

  EXPECT_OUTBOX_RESET_COMPLETE_PENDING();
  prv_rcv_app_message_ack(APP_MSG_OK);

  cl_assert_equal_i(rocky_api_app_message_get_state(),
                    PostMessageStateAwaitingResetCompleteRemoteInitiated);

  RCV_RESET_COMPLETE();
  cl_assert_equal_i(rocky_api_app_message_get_state(), PostMessageStateSessionOpen);
}

void test_rocky_api_app_message__postmessageconnected_and_postmessagedisconnected_remote_rr(void) {
  prv_postmessageconnected_postmessagedisconnected_init(false /* start_connected */);
  ASSERT_JS_GLOBAL_EQUALS_I("d", 1);
  prv_simulate_transport_connection_event(true /* is_connected */);
  ASSERT_JS_GLOBAL_EQUALS_I("c", 0);
  ASSERT_JS_GLOBAL_EQUALS_I("d", 1);
  prv_postmessageconnected_postmessagedisconnected_negotiate_to_open_session();
  ASSERT_JS_GLOBAL_EQUALS_I("c", 1);
  ASSERT_JS_GLOBAL_EQUALS_I("d", 1);

  // Get a ResetRequest:
  RCV_RESET_REQUEST();
  ASSERT_JS_GLOBAL_EQUALS_I("d", 2);
}

void test_rocky_api_app_message__postmessageconnected_and_postmessagedisconnected_local_rr(void) {
  prv_postmessageconnected_postmessagedisconnected_init(false /* start_connected */);
  ASSERT_JS_GLOBAL_EQUALS_I("d", 1);
  prv_simulate_transport_connection_event(true /* is_connected */);
  ASSERT_JS_GLOBAL_EQUALS_I("c", 0);
  ASSERT_JS_GLOBAL_EQUALS_I("d", 1);
  prv_postmessageconnected_postmessagedisconnected_negotiate_to_open_session();
  ASSERT_JS_GLOBAL_EQUALS_I("c", 1);
  ASSERT_JS_GLOBAL_EQUALS_I("d", 1);

  // Get a ResetComplete (unexpected message), should trigger initiating (local) ResetRequest:
  RCV_RESET_COMPLETE();
  ASSERT_JS_GLOBAL_EQUALS_I("d", 2);
}

void test_rocky_api_app_message__postmessageconnected_and_postmessagedisconnected_start_conn(void) {
  prv_postmessageconnected_postmessagedisconnected_init(true /* start_connected */);
  ASSERT_JS_GLOBAL_EQUALS_I("c", 0);
  ASSERT_JS_GLOBAL_EQUALS_I("d", 1);

  prv_postmessageconnected_postmessagedisconnected_negotiate_to_open_session();

  ASSERT_JS_GLOBAL_EQUALS_I("c", 1);
  ASSERT_JS_GLOBAL_EQUALS_I("d", 1);
}

// TODO: test various min/max version combos
// TODO: test RX/TX buffer size combos

////////////////////////////////////////////////////////////////////////////////
// Generic Tests
////////////////////////////////////////////////////////////////////////////////

void test_rocky_api_app_message__json_stringify(void) {
  JS_VAR obj = jerry_create_object();
  JS_VAR json_str = prv_json_stringify(obj);
  char *json_c_str = rocky_string_alloc_and_copy(json_str);
  cl_assert_equal_s(json_c_str, "{}");
  task_free(json_c_str);
}

void test_rocky_api_app_message__json_parse(void) {
  JS_VAR number = prv_json_parse("1");
  cl_assert(jerry_value_is_number(number));
  cl_assert_equal_d(jerry_get_number_value(number), 1.0);

  JS_VAR object = prv_json_parse("{ \"x\" : 42 }");
  cl_assert(jerry_value_is_object(object));
  JS_VAR x = jerry_get_object_field(object, "x");
  cl_assert(jerry_value_is_number(x));
  cl_assert_equal_d(jerry_get_number_value(x), 42.0);
}

void test_rocky_api_app_message__json_parse_stress(void) {
  const int num_attempts = 0x3ff + 10; // Want this to be higher than the max refcount,
                                       // which will also be high enough for a memory stress test
  for (int i = 0; i < num_attempts; ++i) {
    JS_UNUSED_VAL = prv_json_parse(
        "var msg = { "
        "\"key\" : "
        "\"Bacon ipsum dolor amet kevin filet mignon id ut, aute sausage tri-tip "
        "frankfurter pork loin. Boudin ullamco landjaeger, kevin tongue minim tri-tip "
        "ground round dolore. Ham hock tongue swine, cillum jowl pancetta fugiat "
        "deserunt sirloin fatback tenderloin culpa andouille. Incididunt qui bacon "
        "nostrud ham hock adipisicing et ham. Ullamco esse eu capicola, ea culpa irure "
        "meatball proident laboris ut reprehenderit ex incididunt.\" };\n");
  }
}

////////////////////////////////////////////////////////////////////////////////
// .postMessage() Tests
////////////////////////////////////////////////////////////////////////////////

#define SIMPLE_TEST_OBJECT "{ \"x\" : 1 }"

static void prv_assert_simple_test_object_pending(void) {
  const char * const expected_json = "{\"x\":1}";
  const size_t  expected_json_size = strlen(expected_json) + 1;
  const size_t expected_size = sizeof(PostMessageChunkPayload) + strlen(expected_json) + 1;
  uint8_t *buffer = task_malloc(expected_size);

  PostMessageChunkPayload *chunk = (PostMessageChunkPayload *)buffer;
  *chunk = (PostMessageChunkPayload) {
    .total_size_bytes = expected_json_size,
    .is_first = true,
  };
  memcpy(chunk->chunk_data, expected_json, expected_json_size);

  EXPECT_OUTBOX_MESSAGE_PENDING(TupletBytes(PostMessageKeyChunk,
                                            (const uint8_t *) chunk, expected_size));

  // Compare with hard-coded byte array, to catch accidental changes to the ABI:
  const uint8_t raw_bytes_v1[] = {
    0x08, 0x00, 0x00, 0x80, 0x7b, 0x22, 0x78, 0x22, 0x3a, 0x31, 0x7d, 0x00,
  };
  cl_assert_equal_i(sizeof(raw_bytes_v1), expected_size);
  cl_assert_equal_m(raw_bytes_v1, buffer, expected_size);

  task_free(buffer);
}

void test_rocky_api_app_message__postmessage_just_before_connected(void) {
  prv_init_api(false /* start_connected */);

  EXECUTE_SCRIPT("var x = " SIMPLE_TEST_OBJECT ";"
                 "var hasError = false;"
                 "_rocky.on('postmessageerror', function() { hasError = true; });"
                 "_rocky.postMessage(x);");

  // First send attempt fails because not in SessionOpen
  ASSERT_JS_GLOBAL_EQUALS_B("hasError", false);

  prv_simulate_transport_connection_event(true /* is_connected */);
  prv_postmessageconnected_postmessagedisconnected_negotiate_to_open_session();

  prv_assert_simple_test_object_pending();

  prv_rcv_app_message_ack(APP_MSG_OK);

  EXPECT_OUTBOX_NO_MESSAGE_PENDING();

  ASSERT_JS_GLOBAL_EQUALS_B("hasError", false);
}

void test_rocky_api_app_message__post_message_single_chunk(void) {
  prv_init_and_goto_session_open();

  EXECUTE_SCRIPT("var x = " SIMPLE_TEST_OBJECT "; _rocky.postMessage(x);");
  prv_assert_simple_test_object_pending();

  prv_rcv_app_message_ack(APP_MSG_OK);

  EXPECT_OUTBOX_NO_MESSAGE_PENDING();
}

static void prv_init_and_goto_session_open_with_tiny_buffers(void) {
  prv_init_and_goto_awaiting_reset_complete_remote_initiated();
  RCV_APP_MESSAGE(TupletBytes(PostMessageKeyResetComplete, \
                              (const uint8_t *)&TINY_RESET_COMPLETE, sizeof(TINY_RESET_COMPLETE)));
  cl_assert_equal_i(rocky_api_app_message_get_state(), PostMessageStateSessionOpen);
}

void test_rocky_api_app_message__post_message_multi_chunk(void) {
  prv_init_and_goto_session_open_with_tiny_buffers();

  EXECUTE_SCRIPT("var x = { \"x\" : 123 }; _rocky.postMessage(x);");

  const char * const expected_json = "{\"x\":123}";
  const size_t expected_json_size = strlen(expected_json) + 1;
  size_t json_bytes_remaining = expected_json_size;

  uint8_t *buffer = task_malloc(sizeof(PostMessageChunkPayload) + TINY_CHUNK_SIZE);

  // Chunk 1:
  {
    const size_t json_bytes_size = MIN(TINY_CHUNK_SIZE, json_bytes_remaining);
    const size_t expected_size = sizeof(PostMessageChunkPayload) + json_bytes_size;

    PostMessageChunkPayload *chunk = (PostMessageChunkPayload *)buffer;
    *chunk = (PostMessageChunkPayload) {
      .total_size_bytes = expected_json_size,
      .is_first = true,
    };
    memcpy(chunk->chunk_data, expected_json, TINY_CHUNK_SIZE);

    EXPECT_OUTBOX_MESSAGE_PENDING(TupletBytes(PostMessageKeyChunk,
                                              (const uint8_t *) chunk, expected_size));

    // Compare with hard-coded byte array, to catch accidental changes to the ABI:
    const uint8_t raw_bytes_v1[] = {
      0x0a, 0x00, 0x00, 0x80, '{', '"', 'x', '"',
    };
    cl_assert_equal_i(sizeof(raw_bytes_v1), expected_size);
    cl_assert_equal_m(raw_bytes_v1, buffer, expected_size);

    prv_rcv_app_message_ack(APP_MSG_OK);
    json_bytes_remaining -= json_bytes_size;
  }

  // Chunk 2:
  {
    const size_t json_bytes_size = MIN(TINY_CHUNK_SIZE, json_bytes_remaining);
    const size_t expected_size = sizeof(PostMessageChunkPayload) + json_bytes_size;
    const int payload_offset = expected_json_size - json_bytes_remaining;

    PostMessageChunkPayload *chunk = (PostMessageChunkPayload *)buffer;
    *chunk = (PostMessageChunkPayload) {
      .offset_bytes = payload_offset,
      .continuation_is_first = false,
    };
    memcpy(chunk->chunk_data, expected_json + payload_offset, TINY_CHUNK_SIZE);

    EXPECT_OUTBOX_MESSAGE_PENDING(TupletBytes(PostMessageKeyChunk,
                                              (const uint8_t *) chunk, expected_size));

    // Compare with hard-coded byte array, to catch accidental changes to the ABI:
    const uint8_t raw_bytes_v1[] = {
      0x04, 0x00, 0x00, 0x00, ':', '1', '2', '3',
    };
    cl_assert_equal_i(sizeof(raw_bytes_v1), expected_size);
    cl_assert_equal_m(raw_bytes_v1, buffer, expected_size);

    prv_rcv_app_message_ack(APP_MSG_OK);
    json_bytes_remaining -= json_bytes_size;
  }

  // Chunk 3:
  {
    const size_t json_bytes_size = MIN(TINY_CHUNK_SIZE, json_bytes_remaining);
    const size_t expected_size = sizeof(PostMessageChunkPayload) + json_bytes_size;
    const int payload_offset = expected_json_size - json_bytes_remaining;

    PostMessageChunkPayload *chunk = (PostMessageChunkPayload *)buffer;
    *chunk = (PostMessageChunkPayload) {
      .offset_bytes = payload_offset,
      .continuation_is_first = false,
    };
    memcpy(chunk->chunk_data, expected_json + payload_offset, TINY_CHUNK_SIZE);

    EXPECT_OUTBOX_MESSAGE_PENDING(TupletBytes(PostMessageKeyChunk,
                                              (const uint8_t *) chunk, expected_size));

    // Compare with hard-coded byte array, to catch accidental changes to the ABI:
    const uint8_t raw_bytes_v1[] = {
      0x08, 0x00, 0x00, 0x00, '}', '\0',
    };
    cl_assert_equal_i(sizeof(raw_bytes_v1), expected_size);
    cl_assert_equal_m(raw_bytes_v1, buffer, expected_size);

    prv_rcv_app_message_ack(APP_MSG_OK);
    json_bytes_remaining -= json_bytes_size;
  }

  EXPECT_OUTBOX_NO_MESSAGE_PENDING();

  task_free(buffer);
}

void test_rocky_api_app_message__postmessage_not_jsonable(void) {
  prv_init_and_goto_session_open();

  const char *not_jsonable_error =
  "TypeError: Argument at index 0 is not a JSON.stringify()-able object";

  EXECUTE_SCRIPT_EXPECT_ERROR("_rocky.postMessage(undefined);", not_jsonable_error);
  EXECUTE_SCRIPT_EXPECT_ERROR("_rocky.postMessage(function() {});", not_jsonable_error);
  EXECUTE_SCRIPT_EXPECT_ERROR("_rocky.postMessage({toJSON: function() {throw 'toJSONError';}});",
                              "toJSONError");
}

void test_rocky_api_app_message__postmessage_no_args(void) {
  prv_init_api(false /* start_connected */);
  EXECUTE_SCRIPT_EXPECT_ERROR("_rocky.postMessage();", "TypeError: Not enough arguments");
}

void test_rocky_api_app_message__postmessage_oom(void) {
  prv_init_api(false /* start_connected */);

  fake_malloc_set_largest_free_block(0);

  EXECUTE_SCRIPT_EXPECT_ERROR("_rocky.postMessage('x');",
                              "RangeError: Out of memory: can't postMessage() -- object too large");
}

////////////////////////////////////////////////////////////////////////////////
// Receive Tests
////////////////////////////////////////////////////////////////////////////////
void test_rocky_api_app_message__receive_message_multi_chunk(void) {
  prv_init_and_goto_session_open_with_tiny_buffers();

  EXECUTE_SCRIPT("var event = null;\n"
                 "var json_str = null;\n"
                 "_rocky.on('message', function(e) {\n"
                 "  json_str = JSON.stringify(e.data);\n" // stringify again to make assert simple
                 "  event = e;\n"
                 "});");
  JS_VAR event_null = prv_js_global_get_value("event");
  cl_assert_equal_b(true, jerry_value_is_null(event_null));

  // Get 3x the same message in a row:
  for (int j = 0; j < 3; ++j) {

    // Chunks for: {"x":123}
    const struct {
      uint8_t byte_array[8];
      size_t length;
    } chunk_msg_defs[] = {
      {
        .byte_array = {0x0a, 0x00, 0x00, 0x80, '{', '"', 'x', '"'},
        .length = 8,
      },
      {
        .byte_array = {0x04, 0x00, 0x00, 0x00, ':', '1', '2', '3'},
        .length = 8,
      },
      {
        .byte_array = {0x08, 0x00, 0x00, 0x00, '}', '\0'},
        .length = 6,
      }
    };

    for (int i = 0; i < ARRAY_LENGTH(chunk_msg_defs); ++i) {
      RCV_APP_MESSAGE(TupletBytes(PostMessageKeyChunk,
                                  (const uint8_t *) chunk_msg_defs[i].byte_array,
                                  chunk_msg_defs[i].length));
    }

    JS_VAR event_valid = prv_js_global_get_value("event");
    cl_assert_equal_b(true, jerry_value_is_object(event_valid));

    // Make sure that there is a "data" property
    JS_VAR data_prop = jerry_get_object_field(event_valid, "data");
    cl_assert_equal_b(true, jerry_value_is_object(data_prop));

    // Make sure the re-serialized object matches:
    ASSERT_JS_GLOBAL_EQUALS_S("json_str", "{\"x\":123}");

    EXECUTE_SCRIPT("json_str = null;\n"
                   "event = null");
  }
}

////////////////////////////////////////////////////////////////////////////////
// "postmessageerror" event
////////////////////////////////////////////////////////////////////////////////

void test_rocky_api_app_message__postmessageerror(void) {
  prv_init_and_goto_session_open();

  EXECUTE_SCRIPT("var didError = false;"
                 "var x = { \"x\" : 1 };"
                 "var dataJSON = undefined;"
                 "_rocky.on('postmessageerror', "
                 "          function(e) { didError = true; dataJSON = JSON.stringify(e.data); });"
                 "_rocky.postMessage(x);"
                 "x.x = 2;");

  ASSERT_JS_GLOBAL_EQUALS_B("didError", false);

  for (int i = 0; i < 3; ++i) {
    cl_assert_equal_b(s_is_outbox_message_pending, true);

    // NACK
    prv_rcv_app_message_ack(APP_MSG_BUSY);

    AppTimer *t = rocky_api_app_message_get_app_msg_retry_timer();
    cl_assert(t != EVENTED_TIMER_INVALID_ID);
    cl_assert_equal_b(fake_app_timer_is_scheduled(t), true);

    // Enqueuing more objects shouldn't affect the pace at which things are retried:
    EXECUTE_SCRIPT("_rocky.postMessage('')");

    EXPECT_OUTBOX_NO_MESSAGE_PENDING();

    cl_assert_equal_b(app_timer_trigger(t), true);
  }

  ASSERT_JS_GLOBAL_EQUALS_B("didError", true);
  ASSERT_JS_GLOBAL_EQUALS_S("dataJSON", "{\"x\":1}");
}

void test_rocky_api_app_message__postmessageerror_while_disconnected(void) {
  prv_init_api(false /* start_connected */);

  EXECUTE_SCRIPT("var didError = false;"
                 "var x = " SIMPLE_TEST_OBJECT ";"
                 "_rocky.on('postmessageerror', "
                 "          function(e) { didError = true; dataJSON = JSON.stringify(e.data); });"
                 /* 3x postMessage(): */
                 "_rocky.postMessage(x);"
                 "_rocky.postMessage(x);"
                 "_rocky.postMessage(x);");

  // Let the first 2 timeout:
  for (int i = 0; i < 2; ++i) {
    ASSERT_JS_GLOBAL_EQUALS_B("didError", false);

    AppTimer *t = rocky_api_app_message_get_session_closed_object_queue_timer();
    cl_assert(t != EVENTED_TIMER_INVALID_ID);
    cl_assert_equal_b(fake_app_timer_is_scheduled(t), true);

    EXPECT_OUTBOX_NO_MESSAGE_PENDING();

    cl_assert_equal_b(app_timer_trigger(t), true);

    ASSERT_JS_GLOBAL_EQUALS_B("didError", true);

    EXECUTE_SCRIPT("didError = false;");
  }

  // Timer for the 3rd should be set:
  AppTimer *t = rocky_api_app_message_get_session_closed_object_queue_timer();
  cl_assert(t != EVENTED_TIMER_INVALID_ID);
  cl_assert_equal_b(fake_app_timer_is_scheduled(t), true);

  // Connect:
  prv_simulate_transport_connection_event(true /* is_connected */);
  prv_postmessageconnected_postmessagedisconnected_negotiate_to_open_session();

  // Timer for the 3rd should be cancelled now:
  cl_assert(EVENTED_TIMER_INVALID_ID == rocky_api_app_message_get_session_closed_object_queue_timer());

  prv_assert_simple_test_object_pending();
}