/* Copyright (c) 2013-2019, The Tor Project, Inc. */
/* See LICENSE for licensing information */

#define CONNECTION_PRIVATE
#define TOR_CHANNEL_INTERNAL_
#define CONTROL_PRIVATE
#include "core/or/or.h"
#include "core/or/channel.h"
#include "core/or/channeltls.h"
#include "core/or/circuitlist.h"
#include "core/mainloop/connection.h"
#include "feature/control/control.h"
#include "test/test.h"

#include "core/or/or_circuit_st.h"
#include "core/or/origin_circuit_st.h"

static void
add_testing_cell_stats_entry(circuit_t *circ, uint8_t command,
                             unsigned int waiting_time,
                             unsigned int removed, unsigned int exitward)
{
  testing_cell_stats_entry_t *ent = tor_malloc_zero(
                                    sizeof(testing_cell_stats_entry_t));
  ent->command = command;
  ent->waiting_time = waiting_time;
  ent->removed = removed;
  ent->exitward = exitward;
  if (!circ->testing_cell_stats)
    circ->testing_cell_stats = smartlist_new();
  smartlist_add(circ->testing_cell_stats, ent);
}

static void
test_cntev_sum_up_cell_stats(void *arg)
{
  or_circuit_t *or_circ;
  circuit_t *circ;
  cell_stats_t *cell_stats = NULL;
  (void)arg;

  /* This circuit is fake. */
  or_circ = tor_malloc_zero(sizeof(or_circuit_t));
  or_circ->base_.magic = OR_CIRCUIT_MAGIC;
  or_circ->base_.purpose = CIRCUIT_PURPOSE_OR;
  circ = TO_CIRCUIT(or_circ);

  /* A single RELAY cell was added to the appward queue. */
  cell_stats = tor_malloc_zero(sizeof(cell_stats_t));
  add_testing_cell_stats_entry(circ, CELL_RELAY, 0, 0, 0);
  sum_up_cell_stats_by_command(circ, cell_stats);
  tt_u64_op(1, OP_EQ, cell_stats->added_cells_appward[CELL_RELAY]);

  /* A single RELAY cell was added to the exitward queue. */
  add_testing_cell_stats_entry(circ, CELL_RELAY, 0, 0, 1);
  sum_up_cell_stats_by_command(circ, cell_stats);
  tt_u64_op(1, OP_EQ, cell_stats->added_cells_exitward[CELL_RELAY]);

  /* A single RELAY cell was removed from the appward queue where it spent
   * 20 msec. */
  add_testing_cell_stats_entry(circ, CELL_RELAY, 2, 1, 0);
  sum_up_cell_stats_by_command(circ, cell_stats);
  tt_u64_op(20, OP_EQ, cell_stats->total_time_appward[CELL_RELAY]);
  tt_u64_op(1, OP_EQ, cell_stats->removed_cells_appward[CELL_RELAY]);

  /* A single RELAY cell was removed from the exitward queue where it
   * spent 30 msec. */
  add_testing_cell_stats_entry(circ, CELL_RELAY, 3, 1, 1);
  sum_up_cell_stats_by_command(circ, cell_stats);
  tt_u64_op(30, OP_EQ, cell_stats->total_time_exitward[CELL_RELAY]);
  tt_u64_op(1, OP_EQ, cell_stats->removed_cells_exitward[CELL_RELAY]);

 done:
  tor_free(cell_stats);
  tor_free(or_circ);
}

static void
test_cntev_append_cell_stats(void *arg)
{
  smartlist_t *event_parts;
  char *cp = NULL;
  const char *key = "Z";
  uint64_t include_if_non_zero[CELL_COMMAND_MAX_ + 1],
           number_to_include[CELL_COMMAND_MAX_ + 1];
  (void)arg;

  event_parts = smartlist_new();
  memset(include_if_non_zero, 0,
         (CELL_COMMAND_MAX_ + 1) * sizeof(uint64_t));
  memset(number_to_include, 0,
         (CELL_COMMAND_MAX_ + 1) * sizeof(uint64_t));

  /* All array entries empty. */
  append_cell_stats_by_command(event_parts, key,
                               include_if_non_zero,
                               number_to_include);
  tt_int_op(0, OP_EQ, smartlist_len(event_parts));

  /* There's a RELAY cell to include, but the corresponding field in
   * include_if_non_zero is still zero. */
  number_to_include[CELL_RELAY] = 1;
  append_cell_stats_by_command(event_parts, key,
                               include_if_non_zero,
                               number_to_include);
  tt_int_op(0, OP_EQ, smartlist_len(event_parts));

  /* Now include single RELAY cell. */
  include_if_non_zero[CELL_RELAY] = 2;
  append_cell_stats_by_command(event_parts, key,
                               include_if_non_zero,
                               number_to_include);
  cp = smartlist_pop_last(event_parts);
  tt_str_op("Z=relay:1", OP_EQ, cp);
  tor_free(cp);

  /* Add four CREATE cells. */
  include_if_non_zero[CELL_CREATE] = 3;
  number_to_include[CELL_CREATE] = 4;
  append_cell_stats_by_command(event_parts, key,
                               include_if_non_zero,
                               number_to_include);
  cp = smartlist_pop_last(event_parts);
  tt_str_op("Z=create:4,relay:1", OP_EQ, cp);

 done:
  tor_free(cp);
  smartlist_free(event_parts);
}

static void
test_cntev_format_cell_stats(void *arg)
{
  char *event_string = NULL;
  origin_circuit_t *ocirc = NULL;
  or_circuit_t *or_circ = NULL;
  cell_stats_t *cell_stats = NULL;
  channel_tls_t *n_chan=NULL, *p_chan=NULL;
  (void)arg;

  n_chan = tor_malloc_zero(sizeof(channel_tls_t));
  n_chan->base_.global_identifier = 1;

  ocirc = tor_malloc_zero(sizeof(origin_circuit_t));
  ocirc->base_.magic = ORIGIN_CIRCUIT_MAGIC;
  ocirc->base_.purpose = CIRCUIT_PURPOSE_C_GENERAL;
  ocirc->global_identifier = 2;
  ocirc->base_.n_circ_id = 3;
  ocirc->base_.n_chan = &(n_chan->base_);

  /* Origin circuit was completely idle. */
  cell_stats = tor_malloc_zero(sizeof(cell_stats_t));
  format_cell_stats(&event_string, TO_CIRCUIT(ocirc), cell_stats);
  tt_str_op("ID=2 OutboundQueue=3 OutboundConn=1", OP_EQ, event_string);
  tor_free(event_string);

  /* Origin circuit had 4 RELAY cells added to its exitward queue. */
  cell_stats->added_cells_exitward[CELL_RELAY] = 4;
  format_cell_stats(&event_string, TO_CIRCUIT(ocirc), cell_stats);
  tt_str_op("ID=2 OutboundQueue=3 OutboundConn=1 OutboundAdded=relay:4",
            OP_EQ, event_string);
  tor_free(event_string);

  /* Origin circuit also had 5 CREATE2 cells added to its exitward
   * queue. */
  cell_stats->added_cells_exitward[CELL_CREATE2] = 5;
  format_cell_stats(&event_string, TO_CIRCUIT(ocirc), cell_stats);
  tt_str_op("ID=2 OutboundQueue=3 OutboundConn=1 OutboundAdded=relay:4,"
            "create2:5", OP_EQ, event_string);
  tor_free(event_string);

  /* Origin circuit also had 7 RELAY cells removed from its exitward queue
   * which together spent 6 msec in the queue. */
  cell_stats->total_time_exitward[CELL_RELAY] = 6;
  cell_stats->removed_cells_exitward[CELL_RELAY] = 7;
  format_cell_stats(&event_string, TO_CIRCUIT(ocirc), cell_stats);
  tt_str_op("ID=2 OutboundQueue=3 OutboundConn=1 OutboundAdded=relay:4,"
            "create2:5 OutboundRemoved=relay:7 OutboundTime=relay:6",
            OP_EQ, event_string);
  tor_free(event_string);

  p_chan = tor_malloc_zero(sizeof(channel_tls_t));
  p_chan->base_.global_identifier = 2;

  or_circ = tor_malloc_zero(sizeof(or_circuit_t));
  or_circ->base_.magic = OR_CIRCUIT_MAGIC;
  or_circ->base_.purpose = CIRCUIT_PURPOSE_OR;
  or_circ->p_circ_id = 8;
  or_circ->p_chan = &(p_chan->base_);
  or_circ->base_.n_circ_id = 9;
  or_circ->base_.n_chan = &(n_chan->base_);

  tor_free(cell_stats);

  /* OR circuit was idle. */
  cell_stats = tor_malloc_zero(sizeof(cell_stats_t));
  format_cell_stats(&event_string, TO_CIRCUIT(or_circ), cell_stats);
  tt_str_op("InboundQueue=8 InboundConn=2 OutboundQueue=9 OutboundConn=1",
            OP_EQ, event_string);
  tor_free(event_string);

  /* OR circuit had 3 RELAY cells added to its appward queue. */
  cell_stats->added_cells_appward[CELL_RELAY] = 3;
  format_cell_stats(&event_string, TO_CIRCUIT(or_circ), cell_stats);
  tt_str_op("InboundQueue=8 InboundConn=2 InboundAdded=relay:3 "
            "OutboundQueue=9 OutboundConn=1", OP_EQ, event_string);
  tor_free(event_string);

  /* OR circuit had 7 RELAY cells removed from its appward queue which
   * together spent 6 msec in the queue. */
  cell_stats->total_time_appward[CELL_RELAY] = 6;
  cell_stats->removed_cells_appward[CELL_RELAY] = 7;
  format_cell_stats(&event_string, TO_CIRCUIT(or_circ), cell_stats);
  tt_str_op("InboundQueue=8 InboundConn=2 InboundAdded=relay:3 "
            "InboundRemoved=relay:7 InboundTime=relay:6 "
            "OutboundQueue=9 OutboundConn=1", OP_EQ, event_string);

 done:
  tor_free(cell_stats);
  tor_free(event_string);
  tor_free(or_circ);
  tor_free(ocirc);
  tor_free(p_chan);
  tor_free(n_chan);
}

static void
test_cntev_event_mask(void *arg)
{
  unsigned int test_event, selected_event;
  (void)arg;

  /* Check that nothing is interesting when no events are set */
  control_testing_set_global_event_mask(EVENT_MASK_NONE_);

  /* Check that nothing is interesting between EVENT_MIN_ and EVENT_MAX_ */
  for (test_event = EVENT_MIN_; test_event <= EVENT_MAX_; test_event++)
    tt_assert(!control_event_is_interesting(test_event));

  /* Check that nothing is interesting outside EVENT_MIN_ to EVENT_MAX_
   * This will break if control_event_is_interesting() checks its arguments */
  for (test_event = 0; test_event < EVENT_MIN_; test_event++)
    tt_assert(!control_event_is_interesting(test_event));
  for (test_event = EVENT_MAX_ + 1;
       test_event < EVENT_CAPACITY_;
       test_event++)
    tt_assert(!control_event_is_interesting(test_event));

  /* Check that all valid events are interesting when all events are set */
  control_testing_set_global_event_mask(EVENT_MASK_ALL_);

  /* Check that everything is interesting between EVENT_MIN_ and EVENT_MAX_ */
  for (test_event = EVENT_MIN_; test_event <= EVENT_MAX_; test_event++)
    tt_assert(control_event_is_interesting(test_event));

  /* Check that nothing is interesting outside EVENT_MIN_ to EVENT_MAX_
   * This will break if control_event_is_interesting() checks its arguments */
  for (test_event = 0; test_event < EVENT_MIN_; test_event++)
    tt_assert(!control_event_is_interesting(test_event));
  for (test_event = EVENT_MAX_ + 1;
       test_event < EVENT_CAPACITY_;
       test_event++)
    tt_assert(!control_event_is_interesting(test_event));

  /* Check that only that event is interesting when a single event is set */
  for (selected_event = EVENT_MIN_;
       selected_event <= EVENT_MAX_;
       selected_event++) {
    control_testing_set_global_event_mask(EVENT_MASK_(selected_event));

    /* Check that only this event is interesting
     * between EVENT_MIN_ and EVENT_MAX_ */
    for (test_event = EVENT_MIN_; test_event <= EVENT_MAX_; test_event++) {
      if (test_event == selected_event) {
        tt_assert(control_event_is_interesting(test_event));
      } else {
        tt_assert(!control_event_is_interesting(test_event));
      }
    }

    /* Check that nothing is interesting outside EVENT_MIN_ to EVENT_MAX_
     * This will break if control_event_is_interesting checks its arguments */
    for (test_event = 0; test_event < EVENT_MIN_; test_event++)
      tt_assert(!control_event_is_interesting(test_event));
    for (test_event = EVENT_MAX_ + 1;
         test_event < EVENT_CAPACITY_;
         test_event++)
      tt_assert(!control_event_is_interesting(test_event));
  }

  /* Check that only that event is not-interesting
   * when a single event is un-set */
  for (selected_event = EVENT_MIN_;
       selected_event <= EVENT_MAX_;
       selected_event++) {
    control_testing_set_global_event_mask(
                                          EVENT_MASK_ALL_
                                          & ~(EVENT_MASK_(selected_event))
                                          );

    /* Check that only this event is not-interesting
     * between EVENT_MIN_ and EVENT_MAX_ */
    for (test_event = EVENT_MIN_; test_event <= EVENT_MAX_; test_event++) {
      if (test_event == selected_event) {
        tt_assert(!control_event_is_interesting(test_event));
      } else {
        tt_assert(control_event_is_interesting(test_event));
      }
    }

    /* Check that nothing is interesting outside EVENT_MIN_ to EVENT_MAX_
     * This will break if control_event_is_interesting checks its arguments */
    for (test_event = 0; test_event < EVENT_MIN_; test_event++)
      tt_assert(!control_event_is_interesting(test_event));
    for (test_event = EVENT_MAX_ + 1;
         test_event < EVENT_CAPACITY_;
         test_event++)
      tt_assert(!control_event_is_interesting(test_event));
  }

 done:
  ;
}

static char *saved_event_str = NULL;

static void
mock_queue_control_event_string(uint16_t event, char *msg)
{
  (void)event;

  tor_free(saved_event_str);
  saved_event_str = msg;
}

/* Helper macro for checking bootstrap control event strings */
#define assert_bootmsg(s)                                               \
  tt_ptr_op(strstr(saved_event_str, "650 STATUS_CLIENT NOTICE "         \
                   "BOOTSTRAP PROGRESS=" s), OP_EQ, saved_event_str)

/* Test deferral of directory bootstrap messages (requesting_descriptors) */
static void
test_cntev_dirboot_defer_desc(void *arg)
{
  (void)arg;

  MOCK(queue_control_event_string, mock_queue_control_event_string);
  control_testing_set_global_event_mask(EVENT_MASK_(EVENT_STATUS_CLIENT));
  control_event_bootstrap(BOOTSTRAP_STATUS_STARTING, 0);
  assert_bootmsg("0 TAG=starting");
  /* This event should get deferred */
  control_event_boot_dir(BOOTSTRAP_STATUS_REQUESTING_DESCRIPTORS, 0);
  assert_bootmsg("0 TAG=starting");
  control_event_bootstrap(BOOTSTRAP_STATUS_CONN_DIR, 0);
  assert_bootmsg("5 TAG=conn_dir");
  control_event_bootstrap(BOOTSTRAP_STATUS_HANDSHAKE, 0);
  assert_bootmsg("10 TAG=handshake_dir");
  /* The deferred event should appear */
  control_event_boot_first_orconn();
  assert_bootmsg("45 TAG=requesting_descriptors");
 done:
  tor_free(saved_event_str);
  UNMOCK(queue_control_event_string);
}

/* Test deferral of directory bootstrap messages (conn_or) */
static void
test_cntev_dirboot_defer_orconn(void *arg)
{
  (void)arg;

  MOCK(queue_control_event_string, mock_queue_control_event_string);
  control_testing_set_global_event_mask(EVENT_MASK_(EVENT_STATUS_CLIENT));
  control_event_bootstrap(BOOTSTRAP_STATUS_STARTING, 0);
  assert_bootmsg("0 TAG=starting");
  /* This event should get deferred */
  control_event_boot_dir(BOOTSTRAP_STATUS_CONN_OR, 0);
  assert_bootmsg("0 TAG=starting");
  control_event_bootstrap(BOOTSTRAP_STATUS_CONN_DIR, 0);
  assert_bootmsg("5 TAG=conn_dir");
  control_event_bootstrap(BOOTSTRAP_STATUS_HANDSHAKE, 0);
  assert_bootmsg("10 TAG=handshake_dir");
  /* The deferred event should appear */
  control_event_boot_first_orconn();
  assert_bootmsg("80 TAG=conn_or");
 done:
  tor_free(saved_event_str);
  UNMOCK(queue_control_event_string);
}

#define TEST(name, flags)                                               \
  { #name, test_cntev_ ## name, flags, 0, NULL }

struct testcase_t controller_event_tests[] = {
  TEST(sum_up_cell_stats, TT_FORK),
  TEST(append_cell_stats, TT_FORK),
  TEST(format_cell_stats, TT_FORK),
  TEST(event_mask, TT_FORK),
  TEST(dirboot_defer_desc, TT_FORK),
  TEST(dirboot_defer_orconn, TT_FORK),
  END_OF_TESTCASES
};
