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

#include "orconfig.h"

#include <math.h>

#define TOR_CHANNEL_INTERNAL_
#include "or.h"
#include "address.h"
#include "buffers.h"
#include "channel.h"
#include "channeltls.h"
#include "connection_or.h"
#include "config.h"
/* For init/free stuff */
#include "scheduler.h"
#include "tortls.h"

/* Test suite stuff */
#include "test.h"
#include "fakechans.h"

/* The channeltls unit tests */
static void test_channeltls_create(void *arg);
static void test_channeltls_num_bytes_queued(void *arg);
static void test_channeltls_overhead_estimate(void *arg);

/* Mocks used by channeltls unit tests */
static size_t tlschan_buf_datalen_mock(const buf_t *buf);
static or_connection_t * tlschan_connection_or_connect_mock(
    const tor_addr_t *addr,
    uint16_t port,
    const char *digest,
    const ed25519_public_key_t *ed_id,
    channel_tls_t *tlschan);
static int tlschan_is_local_addr_mock(const tor_addr_t *addr);

/* Fake close method */
static void tlschan_fake_close_method(channel_t *chan);

/* Flags controlling behavior of channeltls unit test mocks */
static int tlschan_local = 0;
static const buf_t * tlschan_buf_datalen_mock_target = NULL;
static size_t tlschan_buf_datalen_mock_size = 0;

/* Thing to cast to fake tor_tls_t * to appease assert_connection_ok() */
static int fake_tortls = 0; /* Bleh... */

static void
test_channeltls_create(void *arg)
{
  tor_addr_t test_addr;
  channel_t *ch = NULL;
  const char test_digest[DIGEST_LEN] = {
    0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a,
    0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10, 0x11, 0x12, 0x13, 0x14 };

  (void)arg;

  /* Set up a fake address to fake-connect to */
  test_addr.family = AF_INET;
  test_addr.addr.in_addr.s_addr = htonl(0x01020304);

  /* For this test we always want the address to be treated as non-local */
  tlschan_local = 0;
  /* Install is_local_addr() mock */
  MOCK(is_local_addr, tlschan_is_local_addr_mock);

  /* Install mock for connection_or_connect() */
  MOCK(connection_or_connect, tlschan_connection_or_connect_mock);

  /* Try connecting */
  ch = channel_tls_connect(&test_addr, 567, test_digest, NULL);
  tt_ptr_op(ch, OP_NE, NULL);

 done:
  if (ch) {
    MOCK(scheduler_release_channel, scheduler_release_channel_mock);
    /*
     * Use fake close method that doesn't try to do too much to fake
     * orconn
     */
    ch->close = tlschan_fake_close_method;
    channel_mark_for_close(ch);
    free_fake_channel(ch);
    UNMOCK(scheduler_release_channel);
  }

  UNMOCK(connection_or_connect);
  UNMOCK(is_local_addr);

  return;
}

static void
test_channeltls_num_bytes_queued(void *arg)
{
  tor_addr_t test_addr;
  channel_t *ch = NULL;
  const char test_digest[DIGEST_LEN] = {
    0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a,
    0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10, 0x11, 0x12, 0x13, 0x14 };
  channel_tls_t *tlschan = NULL;
  size_t len;
  int fake_outbuf = 0, n;

  (void)arg;

  /* Set up a fake address to fake-connect to */
  test_addr.family = AF_INET;
  test_addr.addr.in_addr.s_addr = htonl(0x01020304);

  /* For this test we always want the address to be treated as non-local */
  tlschan_local = 0;
  /* Install is_local_addr() mock */
  MOCK(is_local_addr, tlschan_is_local_addr_mock);

  /* Install mock for connection_or_connect() */
  MOCK(connection_or_connect, tlschan_connection_or_connect_mock);

  /* Try connecting */
  ch = channel_tls_connect(&test_addr, 567, test_digest, NULL);
  tt_ptr_op(ch, OP_NE, NULL);

  /*
   * Next, we have to test ch->num_bytes_queued, which is
   * channel_tls_num_bytes_queued_method.  We can't mock
   * connection_get_outbuf_len() directly because it's static inline
   * in connection.h, but we can mock buf_datalen().
   */

  tt_assert(ch->num_bytes_queued != NULL);
  tlschan = BASE_CHAN_TO_TLS(ch);
  tt_ptr_op(tlschan, OP_NE, NULL);
  if (TO_CONN(tlschan->conn)->outbuf == NULL) {
    /* We need an outbuf to make sure buf_datalen() gets called */
    fake_outbuf = 1;
    TO_CONN(tlschan->conn)->outbuf = buf_new();
  }
  tlschan_buf_datalen_mock_target = TO_CONN(tlschan->conn)->outbuf;
  tlschan_buf_datalen_mock_size = 1024;
  MOCK(buf_datalen, tlschan_buf_datalen_mock);
  len = ch->num_bytes_queued(ch);
  tt_int_op(len, OP_EQ, tlschan_buf_datalen_mock_size);
  /*
   * We also cover num_cells_writeable here; since wide_circ_ids = 0 on
   * the fake tlschans, cell_network_size returns 512, and so with
   * tlschan_buf_datalen_mock_size == 1024, we should be able to write
   * ceil((OR_CONN_HIGHWATER - 1024) / 512) = ceil(OR_CONN_HIGHWATER / 512)
   * - 2 cells.
   */
  n = ch->num_cells_writeable(ch);
  tt_int_op(n, OP_EQ, CEIL_DIV(OR_CONN_HIGHWATER, 512) - 2);
  UNMOCK(buf_datalen);
  tlschan_buf_datalen_mock_target = NULL;
  tlschan_buf_datalen_mock_size = 0;
  if (fake_outbuf) {
    buf_free(TO_CONN(tlschan->conn)->outbuf);
    TO_CONN(tlschan->conn)->outbuf = NULL;
  }

 done:
  if (ch) {
    MOCK(scheduler_release_channel, scheduler_release_channel_mock);
    /*
     * Use fake close method that doesn't try to do too much to fake
     * orconn
     */
    ch->close = tlschan_fake_close_method;
    channel_mark_for_close(ch);
    free_fake_channel(ch);
    UNMOCK(scheduler_release_channel);
  }

  UNMOCK(connection_or_connect);
  UNMOCK(is_local_addr);

  return;
}

static void
test_channeltls_overhead_estimate(void *arg)
{
  tor_addr_t test_addr;
  channel_t *ch = NULL;
  const char test_digest[DIGEST_LEN] = {
    0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a,
    0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10, 0x11, 0x12, 0x13, 0x14 };
  double r;
  channel_tls_t *tlschan = NULL;

  (void)arg;

  /* Set up a fake address to fake-connect to */
  test_addr.family = AF_INET;
  test_addr.addr.in_addr.s_addr = htonl(0x01020304);

  /* For this test we always want the address to be treated as non-local */
  tlschan_local = 0;
  /* Install is_local_addr() mock */
  MOCK(is_local_addr, tlschan_is_local_addr_mock);

  /* Install mock for connection_or_connect() */
  MOCK(connection_or_connect, tlschan_connection_or_connect_mock);

  /* Try connecting */
  ch = channel_tls_connect(&test_addr, 567, test_digest, NULL);
  tt_ptr_op(ch, OP_NE, NULL);

  /* First case: silly low ratios should get clamped to 1.0 */
  tlschan = BASE_CHAN_TO_TLS(ch);
  tt_ptr_op(tlschan, OP_NE, NULL);
  tlschan->conn->bytes_xmitted = 128;
  tlschan->conn->bytes_xmitted_by_tls = 64;
  r = ch->get_overhead_estimate(ch);
  tt_assert(fabs(r - 1.0) < 1E-12);

  tlschan->conn->bytes_xmitted_by_tls = 127;
  r = ch->get_overhead_estimate(ch);
  tt_assert(fabs(r - 1.0) < 1E-12);

  /* Now middle of the range */
  tlschan->conn->bytes_xmitted_by_tls = 192;
  r = ch->get_overhead_estimate(ch);
  tt_assert(fabs(r - 1.5) < 1E-12);

  /* Now above the 2.0 clamp */
  tlschan->conn->bytes_xmitted_by_tls = 257;
  r = ch->get_overhead_estimate(ch);
  tt_assert(fabs(r - 2.0) < 1E-12);

  tlschan->conn->bytes_xmitted_by_tls = 512;
  r = ch->get_overhead_estimate(ch);
  tt_assert(fabs(r - 2.0) < 1E-12);

 done:
  if (ch) {
    MOCK(scheduler_release_channel, scheduler_release_channel_mock);
    /*
     * Use fake close method that doesn't try to do too much to fake
     * orconn
     */
    ch->close = tlschan_fake_close_method;
    channel_mark_for_close(ch);
    free_fake_channel(ch);
    UNMOCK(scheduler_release_channel);
  }

  UNMOCK(connection_or_connect);
  UNMOCK(is_local_addr);

  return;
}

static size_t
tlschan_buf_datalen_mock(const buf_t *buf)
{
  if (buf != NULL && buf == tlschan_buf_datalen_mock_target) {
    return tlschan_buf_datalen_mock_size;
  } else {
    return buf_datalen__real(buf);
  }
}

static or_connection_t *
tlschan_connection_or_connect_mock(const tor_addr_t *addr,
                                   uint16_t port,
                                   const char *digest,
                                   const ed25519_public_key_t *ed_id,
                                   channel_tls_t *tlschan)
{
  or_connection_t *result = NULL;
  (void) ed_id; // XXXX Not yet used.

  tt_ptr_op(addr, OP_NE, NULL);
  tt_uint_op(port, OP_NE, 0);
  tt_ptr_op(digest, OP_NE, NULL);
  tt_ptr_op(tlschan, OP_NE, NULL);

  /* Make a fake orconn */
  result = tor_malloc_zero(sizeof(*result));
  result->base_.magic = OR_CONNECTION_MAGIC;
  result->base_.state = OR_CONN_STATE_OPEN;
  result->base_.type = CONN_TYPE_OR;
  result->base_.socket_family = addr->family;
  result->base_.address = tor_strdup("<fake>");
  memcpy(&(result->base_.addr), addr, sizeof(tor_addr_t));
  result->base_.port = port;
  memcpy(result->identity_digest, digest, DIGEST_LEN);
  result->chan = tlschan;
  memcpy(&(result->real_addr), addr, sizeof(tor_addr_t));
  result->tls = (tor_tls_t *)((void *)(&fake_tortls));

 done:
  return result;
}

static void
tlschan_fake_close_method(channel_t *chan)
{
  channel_tls_t *tlschan = NULL;

  tt_ptr_op(chan, OP_NE, NULL);
  tt_int_op(chan->magic, OP_EQ, TLS_CHAN_MAGIC);

  tlschan = BASE_CHAN_TO_TLS(chan);
  tt_ptr_op(tlschan, OP_NE, NULL);

  /* Just free the fake orconn */
  tor_free(tlschan->conn->base_.address);
  tor_free(tlschan->conn);

  channel_closed(chan);

 done:
  return;
}

static int
tlschan_is_local_addr_mock(const tor_addr_t *addr)
{
  tt_ptr_op(addr, OP_NE, NULL);

 done:
  return tlschan_local;
}

struct testcase_t channeltls_tests[] = {
  { "create", test_channeltls_create, TT_FORK, NULL, NULL },
  { "num_bytes_queued", test_channeltls_num_bytes_queued,
    TT_FORK, NULL, NULL },
  { "overhead_estimate", test_channeltls_overhead_estimate,
    TT_FORK, NULL, NULL },
  END_OF_TESTCASES
};

