/* libcoap unit tests
 *
 * Copyright (C) 2013,2015,2022-2026 Olaf Bergmann <bergmann@tzi.org>
 *
 * SPDX-License-Identifier: BSD-2-Clause
 *
 * This file is part of the CoAP library libcoap. Please see
 * README for terms of use.
 */

#include "test_common.h"
#include "test_sendqueue.h"

#include <stdio.h>

#if COAP_CLIENT_SUPPORT

#define ReturnIf_CU_ASSERT_PTR_NOT_NULL(value) \
  CU_ASSERT_PTR_NOT_NULL(value); \
  if ((void*)value == NULL) return;

#define ReturnIf_CU_ASSERT_PTR_NOT_NULL_Code(value,code) \
  CU_ASSERT_PTR_NOT_NULL(value); \
  if ((void*)value == NULL) { \
    code; \
    return; \
  }

static coap_context_t *ctx; /* Holds the coap context for most tests */
static coap_session_t *session; /* Holds a reference-counted session object */

/* timestamps for tests. The first element in this array denotes the
 * base time in ticks, the following elements are timestamps relative
 * to this basetime.
 */
static coap_tick_t timestamp[] = {
  0, 100, 200, 30, 160
};

/* nodes for testing. node[0] is left empty */
coap_queue_t *node[5];

static coap_tick_t
add_timestamps(coap_queue_t *queue, size_t num) {
  coap_tick_t t = 0;
  while (queue && num) {
    num--;
    t += queue->t;
    queue = queue->next;
  }

  return t;
}

static void
t_sendqueue1(void) {
  int result = coap_insert_node(&ctx->sendqueue, node[1]);

  CU_ASSERT(result > 0);
  ReturnIf_CU_ASSERT_PTR_NOT_NULL(ctx->sendqueue);
  CU_ASSERT_PTR_EQUAL(ctx->sendqueue, node[1]);
  CU_ASSERT(node[1]->t == timestamp[1]);
}

static void
t_sendqueue2(void) {
  int result;

  result = coap_insert_node(&ctx->sendqueue, node[2]);

  CU_ASSERT(result > 0);
  CU_ASSERT_PTR_EQUAL(ctx->sendqueue, node[1]);
  CU_ASSERT_PTR_EQUAL(ctx->sendqueue->next, node[2]);

  CU_ASSERT(ctx->sendqueue->t == timestamp[1]);
  CU_ASSERT(node[2]->t == timestamp[2] - timestamp[1]);
}

/* insert new node as first element in queue */
static void
t_sendqueue3(void) {
  int result;
  result = coap_insert_node(&ctx->sendqueue, node[3]);

  CU_ASSERT(result > 0);

  CU_ASSERT_PTR_EQUAL(ctx->sendqueue, node[3]);
  CU_ASSERT(node[3]->t == timestamp[3]);

  ReturnIf_CU_ASSERT_PTR_NOT_NULL(ctx->sendqueue->next);
  ReturnIf_CU_ASSERT_PTR_NOT_NULL(ctx->sendqueue->next->next);

  CU_ASSERT(ctx->sendqueue->next->t == timestamp[1] - timestamp[3]);
  CU_ASSERT(ctx->sendqueue->next->next->t == timestamp[2] - timestamp[1]);
}

/* insert new node as fourth element in queue */
static void
t_sendqueue4(void) {
  int result;

  result = coap_insert_node(&ctx->sendqueue, node[4]);

  CU_ASSERT(result > 0);

  CU_ASSERT_PTR_EQUAL(ctx->sendqueue, node[3]);

  ReturnIf_CU_ASSERT_PTR_NOT_NULL(ctx->sendqueue->next);
  CU_ASSERT_PTR_EQUAL(ctx->sendqueue->next, node[1]);

  ReturnIf_CU_ASSERT_PTR_NOT_NULL(ctx->sendqueue->next->next);
  CU_ASSERT_PTR_EQUAL(ctx->sendqueue->next->next, node[4]);

  ReturnIf_CU_ASSERT_PTR_NOT_NULL(ctx->sendqueue->next->next->next);
  CU_ASSERT_PTR_EQUAL(ctx->sendqueue->next->next->next, node[2]);

  CU_ASSERT(ctx->sendqueue->next->t == timestamp[1] - timestamp[3]);
  CU_ASSERT(add_timestamps(ctx->sendqueue, 1) == timestamp[3]);
  CU_ASSERT(add_timestamps(ctx->sendqueue, 2) == timestamp[1]);
  CU_ASSERT(add_timestamps(ctx->sendqueue, 3) == timestamp[4]);
  CU_ASSERT(add_timestamps(ctx->sendqueue, 4) == timestamp[2]);
}

static void
t_sendqueue5(void) {
  const coap_tick_diff_t delta1 = 20, delta2 = 130;
  unsigned int result;
  coap_tick_t now;

  /* space for saving the current node timestamps */
  static coap_tick_t times[sizeof(timestamp)/sizeof(coap_tick_t)];
  coap_queue_t *p;
  int i;

  /* save timestamps of nodes in the sendqueue in their actual order */
  memset(times, 0, sizeof(times));
  for (p = ctx->sendqueue, i = 0; p; p = p->next, i++) {
    times[i] = p->t;
  }

  coap_ticks(&now);
  ctx->sendqueue_basetime = now;

  now = (coap_tick_diff_t)now - (coap_tick_diff_t)delta1;
  result = coap_adjust_basetime(ctx, now);

  CU_ASSERT(result == 0);
  ReturnIf_CU_ASSERT_PTR_NOT_NULL(ctx->sendqueue);
  CU_ASSERT(ctx->sendqueue_basetime == now);
  CU_ASSERT(ctx->sendqueue->t == timestamp[3] + delta1);

  now = (coap_tick_diff_t)now + (coap_tick_diff_t)delta2;
  result = coap_adjust_basetime(ctx, now);
  CU_ASSERT(result == 2);
  CU_ASSERT(ctx->sendqueue_basetime == now);
  ReturnIf_CU_ASSERT_PTR_NOT_NULL(ctx->sendqueue);
  CU_ASSERT(ctx->sendqueue->t == 0);

  ReturnIf_CU_ASSERT_PTR_NOT_NULL(ctx->sendqueue->next);
  CU_ASSERT(ctx->sendqueue->next->t == 0);

  ReturnIf_CU_ASSERT_PTR_NOT_NULL(ctx->sendqueue->next->next);
  CU_ASSERT(ctx->sendqueue->next->next->t == delta2 - delta1 - timestamp[1]);

  /* restore timestamps of nodes in the sendqueue */
  for (p = ctx->sendqueue, i = 0; p; p = p->next, i++) {
    p->t = times[i];
  }
}

static void
t_sendqueue6(void) {
  unsigned int result;
  coap_tick_t now;
  const coap_tick_diff_t delta = 20;
  coap_queue_t *tmpqueue = ctx->sendqueue;

  /* space for saving the current node timestamps */
  static coap_tick_t times[sizeof(timestamp)/sizeof(coap_tick_t)];
  coap_queue_t *p;
  int i;

  /* save timestamps of nodes in the sendqueue in their actual order */
  memset(times, 0, sizeof(times));
  for (p = ctx->sendqueue, i = 0; p; p = p->next, i++) {
    times[i] = p->t;
  }

  coap_ticks(&now);
  ctx->sendqueue = NULL;
  ctx->sendqueue_basetime = now;

  result = coap_adjust_basetime(ctx, now + delta);

  CU_ASSERT(result == 0);
  CU_ASSERT(ctx->sendqueue_basetime == now + delta);

  /* restore sendqueue */
  ctx->sendqueue = tmpqueue;
}

static void
t_sendqueue7(void) {
  int result;
  coap_queue_t *tmp_node;

  ReturnIf_CU_ASSERT_PTR_NOT_NULL(ctx->sendqueue);
  CU_ASSERT_PTR_EQUAL(ctx->sendqueue, node[3]);

  ReturnIf_CU_ASSERT_PTR_NOT_NULL(ctx->sendqueue->next);
  CU_ASSERT_PTR_EQUAL(ctx->sendqueue->next, node[1]);

  result = coap_remove_from_queue(&ctx->sendqueue, session, 3, &tmp_node);

  CU_ASSERT(result == 1);
  ReturnIf_CU_ASSERT_PTR_NOT_NULL(tmp_node);
  CU_ASSERT_PTR_EQUAL(tmp_node, node[3]);

  ReturnIf_CU_ASSERT_PTR_NOT_NULL(ctx->sendqueue);
  CU_ASSERT_PTR_EQUAL(ctx->sendqueue, node[1]);

  CU_ASSERT(ctx->sendqueue->t == timestamp[1]);
}

static void
t_sendqueue8(void) {
  int result;
  coap_queue_t *tmp_node;

  result = coap_remove_from_queue(&ctx->sendqueue, session, 4, &tmp_node);

  CU_ASSERT(result == 1);
  ReturnIf_CU_ASSERT_PTR_NOT_NULL(tmp_node);
  CU_ASSERT_PTR_EQUAL(tmp_node, node[4]);

  ReturnIf_CU_ASSERT_PTR_NOT_NULL(ctx->sendqueue);
  CU_ASSERT_PTR_EQUAL(ctx->sendqueue, node[1]);
  CU_ASSERT(ctx->sendqueue->t == timestamp[1]);

  ReturnIf_CU_ASSERT_PTR_NOT_NULL(ctx->sendqueue->next);
  CU_ASSERT_PTR_EQUAL(ctx->sendqueue->next, node[2]);
  CU_ASSERT(ctx->sendqueue->next->t == timestamp[2] - timestamp[1]);

  CU_ASSERT_PTR_NULL(ctx->sendqueue->next->next);
}

static void
t_sendqueue9(void) {
  coap_queue_t *tmp_node;
  tmp_node = coap_peek_next(ctx);

  ReturnIf_CU_ASSERT_PTR_NOT_NULL(tmp_node);
  CU_ASSERT_PTR_EQUAL(tmp_node, node[1]);
  CU_ASSERT_PTR_EQUAL(tmp_node, ctx->sendqueue);

  tmp_node = coap_pop_next(ctx);

  ReturnIf_CU_ASSERT_PTR_NOT_NULL(tmp_node);
  CU_ASSERT_PTR_EQUAL(tmp_node, node[1]);

  ReturnIf_CU_ASSERT_PTR_NOT_NULL(ctx->sendqueue);
  CU_ASSERT_PTR_EQUAL(ctx->sendqueue, node[2]);

  CU_ASSERT(tmp_node->t == timestamp[1]);
  CU_ASSERT(ctx->sendqueue->t == timestamp[2]);

  CU_ASSERT_PTR_NULL(ctx->sendqueue->next);
}

static int t_sendqueue_tests_remove(void);

static void
t_sendqueue10(void) {
  coap_queue_t *tmp_node;

  tmp_node = coap_pop_next(ctx);

  ReturnIf_CU_ASSERT_PTR_NOT_NULL(tmp_node);
  CU_ASSERT_PTR_EQUAL(tmp_node, node[2]);

  CU_ASSERT_PTR_NULL(ctx->sendqueue);

  CU_ASSERT(tmp_node->t == timestamp[2]);
}

/*
 * Regression guard for ACK handling: exercise the real coap_dispatch()
 * ACK path and ensure the retransmission node is consumed exactly once.
 */
static void
t_sendqueue11(void) {
  int result;
  coap_queue_t *queued = NULL;
  coap_pdu_t *sent_pdu = NULL;
  uint8_t ack_data[] = { 0x60, 0x00, 0x12, 0x34 };

  CU_ASSERT_PTR_NULL(ctx->sendqueue);

  queued = coap_new_node();
  ReturnIf_CU_ASSERT_PTR_NOT_NULL(queued);

  sent_pdu = coap_pdu_init(COAP_MESSAGE_CON,
                           COAP_REQUEST_CODE_GET,
                           0x1234,
                           64);
  ReturnIf_CU_ASSERT_PTR_NOT_NULL_Code(sent_pdu, coap_delete_node(queued));

  queued->next = NULL;
  queued->session = coap_session_reference(session);
  queued->pdu = sent_pdu;
  queued->id = 0x1234;
  queued->t = timestamp[1];

  result = coap_insert_node(&ctx->sendqueue, queued);
  CU_ASSERT(result > 0);
  ReturnIf_CU_ASSERT_PTR_NOT_NULL(ctx->sendqueue);

  session->state = COAP_SESSION_STATE_ESTABLISHED;
  coap_lock_lock(return);
  coap_handle_dgram(ctx, session, ack_data, sizeof(ack_data));
  CU_ASSERT_PTR_NULL(ctx->sendqueue);

  /* Re-dispatch another ACK with the same mid to ensure idempotent behavior. */
  coap_handle_dgram(ctx, session, ack_data, sizeof(ack_data));
  coap_lock_unlock();
  CU_ASSERT_PTR_NULL(ctx->sendqueue);
}

/* This function creates a set of nodes for testing. These nodes
 * will exist for all tests and are modified by coap_insert_node()
 * and coap_remove_from_queue().
 */
static int
t_sendqueue_tests_create(void) {
  size_t n, error = 0;
  coap_address_t addr;
  coap_address_init(&addr);

  addr.size = sizeof(struct sockaddr_in6);
  addr.addr.sin6.sin6_family = AF_INET6;
  addr.addr.sin6.sin6_addr = in6addr_any;
  addr.addr.sin6.sin6_port = htons(COAP_DEFAULT_PORT);

  ctx = coap_new_context(&addr);

  addr.addr.sin6.sin6_addr = in6addr_loopback;
  session = coap_new_client_session(ctx, NULL, &addr, COAP_PROTO_UDP);

  coap_ticks(&timestamp[0]);

  memset(node, 0, sizeof(node));
  for (n = 1; n < sizeof(node)/sizeof(coap_queue_t *); n++) {
    node[n] = coap_new_node();
    if (!node[n]) {
      error = 1;
      break;
    }

    node[n]->id = n;
    node[n]->t = timestamp[n];
    node[n]->session = coap_session_reference(session);
  }

  if (error) {
    t_sendqueue_tests_remove();
  }

  return error;
}

static int
t_sendqueue_tests_remove(void) {
  size_t n;
  for (n = 0; n < sizeof(node)/sizeof(coap_queue_t *); n++) {
    if (node[n]) {
      coap_delete_node(node[n]);
      node[n] = NULL;
    }
  }
  coap_free_context(ctx);
  ctx = NULL;
  return 0;
}

CU_pSuite
t_init_sendqueue_tests(void) {
  CU_pSuite suite;

  suite = CU_add_suite("sendqueue",
                       t_sendqueue_tests_create, t_sendqueue_tests_remove);
  if (!suite) {                        /* signal error */
    fprintf(stderr, "W: cannot add sendqueue test suite (%s)\n",
            CU_get_error_msg());

    return NULL;
  }

#define SENDQUEUE_TEST(s,t)                                              \
  if (!CU_ADD_TEST(s,t)) {                                              \
    fprintf(stderr, "W: cannot add sendqueue test (%s)\n",              \
            CU_get_error_msg());                                      \
  }

  SENDQUEUE_TEST(suite, t_sendqueue1);
  SENDQUEUE_TEST(suite, t_sendqueue2);
  SENDQUEUE_TEST(suite, t_sendqueue3);
  SENDQUEUE_TEST(suite, t_sendqueue4);
  SENDQUEUE_TEST(suite, t_sendqueue5);
  SENDQUEUE_TEST(suite, t_sendqueue6);
  SENDQUEUE_TEST(suite, t_sendqueue7);
  SENDQUEUE_TEST(suite, t_sendqueue8);
  SENDQUEUE_TEST(suite, t_sendqueue9);
  SENDQUEUE_TEST(suite, t_sendqueue10);
  SENDQUEUE_TEST(suite, t_sendqueue11);

  return suite;
}
#endif /* COAP_CLIENT_SUPPORT */
