#include <mongoc/mongoc-config.h>

#include <bson/bson.h>

#include <errno.h>
#ifdef MONGOC_ENABLE_SSL_OPENSSL
#include <openssl/err.h>
#include <openssl/ssl.h>
#endif

#include <mongoc/mongoc-stream-tls.h>

#include <TestSuite.h>
#include <ssl-test.h>

#define TIMEOUT 10 * 1000

#define NUM_IOVECS 2000

/** this function is meant to be run from ssl_test as a child thread
 *
 * It:
 *    1. spins up
 *    2. binds and listens to a random port
 *    3. notifies the client of its port through a condvar
 *    4. accepts a request
 *    5. reads a 32 bit length
 *    6. reads a string of that length
 *    7. echoes it back to the client
 *    8. shuts down
 */
static BSON_THREAD_FUN(ssl_test_server, ptr)
{
   ssl_test_data_t *data = (ssl_test_data_t *)ptr;

   mongoc_stream_t *sock_stream;
   mongoc_stream_t *ssl_stream;
   mongoc_socket_t *listen_sock;
   mongoc_socket_t *conn_sock;
   mongoc_socklen_t sock_len;
   char buf[4 * NUM_IOVECS];
   ssize_t r;
   bson_error_t error;
   mongoc_iovec_t iov;
   struct sockaddr_in server_addr = {0};
   int len;

   iov.iov_base = buf;
   iov.iov_len = sizeof buf;

   listen_sock = mongoc_socket_new(AF_INET, SOCK_STREAM, 0);
   BSON_ASSERT(listen_sock);

   server_addr.sin_family = AF_INET;
   server_addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
   server_addr.sin_port = htons(0);

   r = mongoc_socket_bind(listen_sock, (struct sockaddr *)&server_addr, sizeof server_addr);
   BSON_ASSERT(r == 0);

   sock_len = sizeof(server_addr);
   r = mongoc_socket_getsockname(listen_sock, (struct sockaddr *)&server_addr, &sock_len);
   BSON_ASSERT(r == 0);

   r = mongoc_socket_listen(listen_sock, 10);
   BSON_ASSERT(r == 0);

   bson_mutex_lock(&data->cond_mutex);
   data->server_port = ntohs(server_addr.sin_port);
   mongoc_cond_signal(&data->cond);
   bson_mutex_unlock(&data->cond_mutex);

   conn_sock = mongoc_socket_accept(listen_sock, -1);
   BSON_ASSERT(conn_sock);

   sock_stream = mongoc_stream_socket_new(conn_sock);
   BSON_ASSERT(sock_stream);
   ssl_stream = mongoc_stream_tls_new_with_hostname(sock_stream, NULL, data->server, 0);
   if (!ssl_stream) {
#ifdef MONGOC_ENABLE_SSL_OPENSSL
      unsigned long err = ERR_get_error();
#else
      unsigned long err = 42;
#endif
      BSON_ASSERT(err);

      data->server_result->ssl_err = err;
      data->server_result->result = SSL_TEST_SSL_INIT;
#ifdef MONGOC_ENABLE_SSL_OPENSSL
      MONGOC_ERROR("ERRORED (line: %d): %s\n", (int)(__LINE__), ERR_error_string(ERR_get_error(), NULL));
#endif
      mongoc_stream_destroy(sock_stream);
      mongoc_socket_destroy(listen_sock);

      BSON_THREAD_RETURN;
   }
   BSON_ASSERT(ssl_stream);

   r = mongoc_stream_tls_handshake_block(ssl_stream, data->host, TIMEOUT, &error);

   if (!r) {
      unsigned long err = 43;

      MONGOC_ERROR("ERRORED (line: %d): %s\n", (int)(__LINE__), error.message);
#ifdef MONGOC_ENABLE_SSL_OPENSSL
      MONGOC_ERROR("msg: %s\n", ERR_error_string(ERR_get_error(), NULL));
#endif
      data->server_result->ssl_err = err;
      data->server_result->result = SSL_TEST_SSL_HANDSHAKE;

      mongoc_socket_destroy(listen_sock);
      mongoc_stream_destroy(ssl_stream);

      BSON_THREAD_RETURN;
   }

   r = mongoc_stream_readv(ssl_stream, &iov, 1, 4, TIMEOUT);
   if (r < 0) {
      data->server_result->err = errno;
      data->server_result->result = SSL_TEST_TIMEOUT;
      MONGOC_ERROR("ERRORED (line: %d): %s\n", (int)(__LINE__), "mongoc_stream_readv failed.");

      mongoc_stream_destroy(ssl_stream);
      mongoc_socket_destroy(listen_sock);

      BSON_THREAD_RETURN;
   }

   BSON_ASSERT(r == 4);
   memcpy(&len, iov.iov_base, r);

   r = mongoc_stream_readv(ssl_stream, &iov, 1, len, TIMEOUT);
   BSON_ASSERT(r == len);

   iov.iov_len = r;
   mongoc_stream_writev(ssl_stream, &iov, 1, TIMEOUT);

   mongoc_stream_destroy(ssl_stream);

   mongoc_socket_destroy(listen_sock);

   data->server_result->result = SSL_TEST_SUCCESS;

   BSON_THREAD_RETURN;
}

/** this function is meant to be run from ssl_test as a child thread
 *
 * It:
 *    1. spins up
 *    2. waits on a condvar until the server is up
 *    3. connects to the server's port
 *    4. writes a 4 bytes length
 *    5. writes a string of length size
 *    6. reads a response back of the given length
 *    7. confirms that its the same as what was written
 *    8. shuts down
 */
static BSON_THREAD_FUN(ssl_test_client, ptr)
{
   ssl_test_data_t *data = (ssl_test_data_t *)ptr;
   mongoc_stream_t *sock_stream;
   mongoc_stream_t *ssl_stream;
   mongoc_socket_t *conn_sock;
   int i;
   char buf[1024];
   ssize_t r;
   mongoc_iovec_t riov;
   mongoc_iovec_t wiov;
   mongoc_iovec_t wiov_many[NUM_IOVECS];
   struct sockaddr_in server_addr = {0};
   int len;
   bson_error_t error;

   riov.iov_base = buf;
   riov.iov_len = sizeof buf;

   conn_sock = mongoc_socket_new(AF_INET, SOCK_STREAM, 0);
   BSON_ASSERT(conn_sock);

   bson_mutex_lock(&data->cond_mutex);
   while (!data->server_port) {
      mongoc_cond_wait(&data->cond, &data->cond_mutex);
   }
   bson_mutex_unlock(&data->cond_mutex);

   server_addr.sin_family = AF_INET;
   server_addr.sin_port = htons(data->server_port);
   server_addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK);

   r = mongoc_socket_connect(conn_sock, (struct sockaddr *)&server_addr, sizeof(server_addr), -1);
   if (r != 0) {
      test_error("mongoc_socket_connect returned %zd: \"%s\"", r, strerror(errno));
   }

   sock_stream = mongoc_stream_socket_new(conn_sock);
   BSON_ASSERT(sock_stream);
   if (data->client_stream_wrapper) {
      sock_stream = data->client_stream_wrapper(sock_stream);
   }
   ssl_stream = mongoc_stream_tls_new_with_hostname(sock_stream, data->host, data->client, 1);
   if (!ssl_stream) {
#ifdef MONGOC_ENABLE_SSL_OPENSSL
      unsigned long err = ERR_get_error();
#else
      unsigned long err = 44;
#endif
      BSON_ASSERT(err);

      data->client_result->ssl_err = err;
      data->client_result->result = SSL_TEST_SSL_INIT;
      MONGOC_ERROR("ERRORED (line: %d): %s\n", (int)(__LINE__), "mongoc_stream_tls_new_with_hostname failed.");

      mongoc_stream_destroy(sock_stream);

      BSON_THREAD_RETURN;
   }
   BSON_ASSERT(ssl_stream);

   r = mongoc_stream_tls_handshake_block(ssl_stream, data->host, TIMEOUT, &error);

   if (!r) {
      unsigned long err = 45;

      data->client_result->ssl_err = err;
      data->client_result->result = SSL_TEST_SSL_HANDSHAKE;
      MONGOC_ERROR("ERRORED (line: %d): %s\n", (int)(__LINE__), error.message);

      mongoc_stream_destroy(ssl_stream);
      BSON_THREAD_RETURN;
   }

   len = 4 * NUM_IOVECS;

   wiov.iov_base = (void *)&len;
   wiov.iov_len = 4;
   r = mongoc_stream_writev(ssl_stream, &wiov, 1, TIMEOUT);

   BSON_ASSERT(mlib_cmp(r, ==, wiov.iov_len));

   for (i = 0; i < NUM_IOVECS; i++) {
      wiov_many[i].iov_base = (void *)"foo";
      wiov_many[i].iov_len = 4;
   }

   r = mongoc_stream_writev(ssl_stream, wiov_many, NUM_IOVECS, TIMEOUT);
   BSON_ASSERT(mlib_cmp(r, ==, wiov_many[0].iov_len * NUM_IOVECS));

   riov.iov_len = 1;

   r = mongoc_stream_readv(ssl_stream, &riov, 1, 1, TIMEOUT);
   BSON_ASSERT(r == 1);
   BSON_ASSERT(memcmp(riov.iov_base, "f", 1) == 0);

   riov.iov_len = 3;

   r = mongoc_stream_readv(ssl_stream, &riov, 1, 3, TIMEOUT);
   BSON_ASSERT(r == 3);
   BSON_ASSERT(memcmp(riov.iov_base, "oo", 3) == 0);

   mongoc_stream_destroy(ssl_stream);

   data->client_result->result = SSL_TEST_SUCCESS;

   BSON_THREAD_RETURN;
}


static void
_ssl_test_run(ssl_test_data_t *data)
{
   bson_thread_t server;
   bson_thread_t client;

   bson_mutex_init(&data->cond_mutex);
   mongoc_cond_init(&data->cond);

   ASSERT_CMPINT(mcommon_thread_create(&server, &ssl_test_server, data), ==, 0);
   ASSERT_CMPINT(mcommon_thread_create(&client, &ssl_test_client, data), ==, 0);

   mcommon_thread_join(server);
   mcommon_thread_join(client);

   bson_mutex_destroy(&data->cond_mutex);
   mongoc_cond_destroy(&data->cond);
}

void
ssl_test(mongoc_ssl_opt_t *client,
         mongoc_ssl_opt_t *server,
         const char *host,
         ssl_test_result_t *client_result,
         ssl_test_result_t *server_result)
{
   _ssl_test_run(&(ssl_test_data_t){
      .client = client,
      .server = server,
      .client_result = client_result,
      .server_result = server_result,
      .host = host,
   });
}

void
ssl_test_with_client_stream_wrapper(mongoc_ssl_opt_t *client,
                                    mongoc_ssl_opt_t *server,
                                    const char *host,
                                    ssl_test_result_t *client_result,
                                    ssl_test_result_t *server_result,
                                    mongoc_stream_t *(*wrapper)(mongoc_stream_t *stream))
{
   _ssl_test_run(&(ssl_test_data_t){
      .client = client,
      .server = server,
      .client_result = client_result,
      .server_result = server_result,
      .host = host,
      .client_stream_wrapper = wrapper,
   });
}
