/*
     This file is part of GNUnet.
     Copyright (C) 2016 GNUnet e.V.

     GNUnet is free software: you can redistribute it and/or modify it
     under the terms of the GNU Affero General Public License as published
     by the Free Software Foundation, either version 3 of the License,
     or (at your option) any later version.

     GNUnet is distributed in the hope that it will be useful, but
     WITHOUT ANY WARRANTY; without even the implied warranty of
     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
     Affero General Public License for more details.

     You should have received a copy of the GNU Affero General Public License
     along with this program.  If not, see <http://www.gnu.org/licenses/>.

     SPDX-License-Identifier: AGPL3.0-or-later
 */
/**
 * @file transport-testing-main.c
 * @brief convenience main function for tests
 * @author Christian Grothoff
 */
#include "transport-testing.h"


/**
 * Closure for #connect_cb.
 */
struct GNUNET_TRANSPORT_TESTING_ConnectRequestList
{
  /**
   * Stored in a DLL.
   */
  struct GNUNET_TRANSPORT_TESTING_ConnectRequestList *next;

  /**
   * Stored in a DLL.
   */
  struct GNUNET_TRANSPORT_TESTING_ConnectRequestList *prev;

  /**
   * Overall context we are in.
   */
  struct GNUNET_TRANSPORT_TESTING_ConnectCheckContext *ccc;

  /**
   * Connect request this is about.
   */
  struct GNUNET_TRANSPORT_TESTING_ConnectRequest *cr;

  /**
   * Peer being connected.
   */
  struct GNUNET_TRANSPORT_TESTING_PeerContext *p1;

  /**
   * Peer being connected.
   */
  struct GNUNET_TRANSPORT_TESTING_PeerContext *p2;
};


/**
 * Shutdown function for the test. Stops all peers.
 *
 * @param cls our `struct GNUNET_TRANSPORT_TESTING_ConnectCheckContext *`
 */
static void
do_shutdown (void *cls)
{
  struct GNUNET_TRANSPORT_TESTING_ConnectCheckContext *ccc = cls;
  struct GNUNET_TRANSPORT_TESTING_ConnectRequestList *crl;

  GNUNET_log (GNUNET_ERROR_TYPE_DEBUG,
              "Testcase shutting down\n");
  if (NULL != ccc->shutdown_task)
    ccc->shutdown_task (ccc->shutdown_task_cls);
  if (NULL != ccc->timeout_task)
  {
    GNUNET_SCHEDULER_cancel (ccc->timeout_task);
    ccc->timeout_task = NULL;
  }
  if (NULL != ccc->connect_task)
  {
    GNUNET_SCHEDULER_cancel (ccc->connect_task);
    ccc->connect_task = NULL;
  }
  while (NULL != (crl = ccc->crl_head))
  {
    GNUNET_CONTAINER_DLL_remove (ccc->crl_head,
                                 ccc->crl_tail,
                                 crl);
    GNUNET_TRANSPORT_TESTING_connect_peers_cancel (crl->cr);
    GNUNET_free (crl);
  }
  for (unsigned int i = 0; i < ccc->num_peers; i++)
  {
    if (NULL != ccc->p[i])
    {
      GNUNET_TRANSPORT_TESTING_stop_peer (ccc->p[i]);
      ccc->p[i] = NULL;
    }
  }
}


/**
 * Testcase hit timeout, shut it down with error.
 *
 * @param cls our `struct GNUNET_TRANSPORT_TESTING_ConnectCheckContext *`
 */
static void
do_timeout (void *cls)
{
  struct GNUNET_TRANSPORT_TESTING_ConnectCheckContext *ccc = cls;

  ccc->timeout_task = NULL;
  GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
              "Testcase timed out\n");
  ccc->global_ret = GNUNET_SYSERR;
  GNUNET_SCHEDULER_shutdown ();
}


/**
 * Internal data structure.   Closure for
 * #connect_cb, #disconnect_cb, #my_nc and #start_cb.
 * Allows us to identify which peer this is about.
 */
struct GNUNET_TRANSPORT_TESTING_InternalPeerContext
{
  /**
   * Overall context of the callback.
   */
  struct GNUNET_TRANSPORT_TESTING_ConnectCheckContext *ccc;

  /**
   * Offset of the peer this is about.
   */
  unsigned int off;
};


/**
 * Information tracked per connected peer.
 */
struct ConnectPairInfo
{
  /**
   * Peer this is about.
   */
  const struct GNUNET_PeerIdentity *sender;

  /**
   * Information about the receiving peer.
   */
  struct GNUNET_TRANSPORT_TESTING_InternalPeerContext *ipi;
};


/**
 * Function called when we connected two peers.  Once we have gotten
 * to the clique, launch test-specific logic.
 *
 * @param cls our `struct GNUNET_TRANSPORT_TESTING_ConnectRequestList *`
 */
static void
connect_cb (void *cls)
{
  struct GNUNET_TRANSPORT_TESTING_ConnectRequestList *crl = cls;
  struct GNUNET_TRANSPORT_TESTING_ConnectCheckContext *ccc = crl->ccc;

  GNUNET_CONTAINER_DLL_remove (ccc->crl_head,
                               ccc->crl_tail,
                               crl);
  {
    char *p1_c = GNUNET_strdup (GNUNET_i2s (&crl->p1->id));

    GNUNET_log (GNUNET_ERROR_TYPE_INFO,
                "Peers connected: %u (%s) <-> %u (%s)\n",
                crl->p1->no,
                p1_c,
                crl->p2->no,
                GNUNET_i2s (&crl->p2->id));
    GNUNET_free (p1_c);
    GNUNET_free (crl);
  }
  if (NULL == ccc->crl_head)
  {
    GNUNET_log (GNUNET_ERROR_TYPE_DEBUG,
                "All connections UP, launching custom test logic.\n");
    GNUNET_SCHEDULER_add_now (ccc->connect_continuation,
                              ccc->connect_continuation_cls);
  }
}


/**
 * Find peer by peer ID.
 *
 * @param ccc context to search
 * @param peer peer to look for
 * @return NULL if @a peer was not found
 */
struct GNUNET_TRANSPORT_TESTING_PeerContext *
GNUNET_TRANSPORT_TESTING_find_peer (struct
                                    GNUNET_TRANSPORT_TESTING_ConnectCheckContext
                                    *ccc,
                                    const struct GNUNET_PeerIdentity *peer)
{
  for (unsigned int i = 0; i < ccc->num_peers; i++)
    if ((NULL != ccc->p[i]) &&
        (0 == memcmp (peer,
                      &ccc->p[i]->id,
                      sizeof(*peer))))
      return ccc->p[i];
  return NULL;
}


/**
 * Wrapper around peers connecting.  Calls client's nc function.
 *
 * @param cls our `struct GNUNET_TRANSPORT_TESTING_InternalPeerContext *`
 * @param peer peer we got connected to
 * @param mq message queue for transmissions to @a peer
 * @return closure for message handlers
 */
static void *
my_nc (void *cls,
       const struct GNUNET_PeerIdentity *peer,
       struct GNUNET_MQ_Handle *mq)
{
  struct GNUNET_TRANSPORT_TESTING_InternalPeerContext *ipi = cls;
  struct GNUNET_TRANSPORT_TESTING_ConnectCheckContext *ccc = ipi->ccc;
  struct ConnectPairInfo *cpi;

  if (NULL != ccc->nc)
    ccc->nc (ccc->cls,
             ccc->p[ipi->off],
             peer);
  cpi = GNUNET_new (struct ConnectPairInfo);
  cpi->ipi = ipi;
  cpi->sender = peer; /* valid until disconnect */
  return cpi;
}


/**
 * Wrapper around peers disconnecting.  Calls client's nd function.
 *
 * @param cls our `struct GNUNET_TRANSPORT_TESTING_InternalPeerContext *`
 * @param peer peer we got disconnected from
 * @param custom_cls return value from @my_nc
 */
static void
my_nd (void *cls,
       const struct GNUNET_PeerIdentity *peer,
       void *custom_cls)
{
  struct GNUNET_TRANSPORT_TESTING_InternalPeerContext *ipi = cls;
  struct GNUNET_TRANSPORT_TESTING_ConnectCheckContext *ccc = ipi->ccc;
  struct ConnectPairInfo *cpi = custom_cls;

  if (NULL != ccc->nd)
    ccc->nd (ccc->cls,
             ccc->p[ipi->off],
             peer);
  GNUNET_free (cpi);
}


/**
 * Wrapper around receiving data.  Calls client's rec function.
 *
 * @param cls our `struct ConnectPairInfo *`
 * @param message message we received
 * @return #GNUNET_OK (all messages are fine)
 */
static int
check_test (void *cls,
            const struct GNUNET_TRANSPORT_TESTING_TestMessage *message)
{
  return GNUNET_OK;
}


/**
 * Wrapper around receiving data.  Calls client's rec function.
 *
 * @param cls our `struct ConnectPairInfo *`
 * @param message message we received
 */
static void
handle_test (void *cls,
             const struct GNUNET_TRANSPORT_TESTING_TestMessage *message)
{
  struct ConnectPairInfo *cpi = cls;
  struct GNUNET_TRANSPORT_TESTING_InternalPeerContext *ipi = cpi->ipi;
  struct GNUNET_TRANSPORT_TESTING_ConnectCheckContext *ccc = ipi->ccc;

  if (NULL != ccc->rec)
    ccc->rec (ccc->cls,
              ccc->p[ipi->off],
              cpi->sender,
              message);
}


/**
 * Wrapper around receiving data.  Calls client's rec function.
 *
 * @param cls our `struct ConnectPairInfo *`
 * @param message message we received
 * @return #GNUNET_OK (all messages are fine)
 */
static int
check_test2 (void *cls,
             const struct GNUNET_TRANSPORT_TESTING_TestMessage *message)
{
  return GNUNET_OK;
}


/**
 * Wrapper around receiving data.  Calls client's rec function.
 *
 * @param cls our `struct ConnectPairInfo *`
 * @param message message we received
 */
static void
handle_test2 (void *cls,
              const struct GNUNET_TRANSPORT_TESTING_TestMessage *message)
{
  struct ConnectPairInfo *cpi = cls;
  struct GNUNET_TRANSPORT_TESTING_InternalPeerContext *ipi = cpi->ipi;
  struct GNUNET_TRANSPORT_TESTING_ConnectCheckContext *ccc = ipi->ccc;

  if (NULL != ccc->rec)
    ccc->rec (ccc->cls,
              ccc->p[ipi->off],
              cpi->sender,
              message);
}


/**
 * Connect the peers as a clique.
 *
 * @param cls our `struct GNUNET_TRANSPORT_TESTING_ConnectCheckContext`
 */
static void
do_connect (void *cls)
{
  struct GNUNET_TRANSPORT_TESTING_ConnectCheckContext *ccc = cls;

  ccc->connect_task = NULL;
  for (unsigned int i = 0; i < ccc->num_peers; i++)
    for (unsigned int j = (ccc->bi_directional ? 0 : i + 1); j < ccc->num_peers;
         j++)
    {
      struct GNUNET_TRANSPORT_TESTING_ConnectRequestList *crl;

      if (i == j)
        continue;
      crl = GNUNET_new (struct GNUNET_TRANSPORT_TESTING_ConnectRequestList);
      GNUNET_CONTAINER_DLL_insert (ccc->crl_head,
                                   ccc->crl_tail,
                                   crl);
      crl->ccc = ccc;
      crl->p1 = ccc->p[i];
      crl->p2 = ccc->p[j];
      {
        char *sender_c = GNUNET_strdup (GNUNET_i2s (&ccc->p[0]->id));

        GNUNET_log (GNUNET_ERROR_TYPE_INFO,
                    "Test tries to connect peer %u (`%s') -> peer %u (`%s')\n",
                    ccc->p[0]->no,
                    sender_c,
                    ccc->p[1]->no,
                    GNUNET_i2s (&ccc->p[1]->id));
        GNUNET_free (sender_c);
      }
      crl->cr = GNUNET_TRANSPORT_TESTING_connect_peers (ccc->p[i],
                                                        ccc->p[j],
                                                        &connect_cb,
                                                        crl);
    }
}


/**
 * Function called once we have successfully launched a peer.
 * Once all peers have been launched, we connect all of them
 * in a clique.
 *
 * @param cls our `struct GNUNET_TRANSPORT_TESTING_InternalPeerContext *`
 */
static void
start_cb (void *cls)
{
  struct GNUNET_TRANSPORT_TESTING_InternalPeerContext *ipi = cls;
  struct GNUNET_TRANSPORT_TESTING_ConnectCheckContext *ccc = ipi->ccc;
  struct GNUNET_TRANSPORT_TESTING_PeerContext *p = ccc->p[ipi->off];

  ccc->started++;
  GNUNET_log (GNUNET_ERROR_TYPE_INFO,
              "Peer %u (`%s') started\n",
              p->no,
              GNUNET_i2s (&p->id));
  if (ccc->started != ccc->num_peers)
    return;
  if (NULL != ccc->pre_connect_task)
  {
    /* Run the custom per-connect job, then give it a second to
       go into effect before we continue connecting peers. */
    ccc->pre_connect_task (ccc->pre_connect_task_cls);
    ccc->connect_task = GNUNET_SCHEDULER_add_delayed (GNUNET_TIME_UNIT_SECONDS,
                                                      &do_connect,
                                                      ccc);
  }
  else
  {
    do_connect (ccc);
  }
}


/**
 * Function run from #GNUNET_TRANSPORT_TESTING_connect_check
 * once the scheduler is up.  Should launch the peers and
 * then in the continuations try to connect them.
 *
 * @param cls our `struct GNUNET_TRANSPORT_TESTING_ConnectCheckContext *`
 * @param args ignored
 * @param cfgfile ignored
 * @param cfg configuration
 */
static void
connect_check_run (void *cls,
                   char *const *args,
                   const char *cfgfile,
                   const struct GNUNET_CONFIGURATION_Handle *cfg)
{
  struct GNUNET_TRANSPORT_TESTING_ConnectCheckContext *ccc = cls;
  int ok;

  ccc->cfg = cfg;
  ccc->timeout_task = GNUNET_SCHEDULER_add_delayed (ccc->timeout,
                                                    &do_timeout,
                                                    ccc);
  GNUNET_SCHEDULER_add_shutdown (&do_shutdown,
                                 ccc);
  ok = GNUNET_OK;
  for (unsigned int i = 0; i < ccc->num_peers; i++)
  {
    struct GNUNET_MQ_MessageHandler handlers[] = {
      GNUNET_MQ_hd_var_size (test,
                             GNUNET_TRANSPORT_TESTING_SIMPLE_MTYPE,
                             struct GNUNET_TRANSPORT_TESTING_TestMessage,
                             NULL),
      GNUNET_MQ_hd_var_size (test2,
                             GNUNET_TRANSPORT_TESTING_SIMPLE_MTYPE2,
                             struct GNUNET_TRANSPORT_TESTING_TestMessage,
                             NULL),
      GNUNET_MQ_handler_end ()
    };
    ccc->p[i] = GNUNET_TRANSPORT_TESTING_start_peer (ccc->tth,
                                                     ccc->cfg_files[i],
                                                     i + 1,
                                                     handlers,
                                                     &my_nc,
                                                     &my_nd,
                                                     &ccc->ip[i],
                                                     &start_cb,
                                                     &ccc->ip[i]);
    if (NULL == ccc->p[i])
      ok = GNUNET_SYSERR;
  }
  if (GNUNET_OK != ok)
  {
    GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
                "Fail! Could not start peers!\n");
    GNUNET_SCHEDULER_shutdown ();
  }
}


/**
 * Common implementation of the #GNUNET_TRANSPORT_TESTING_CheckCallback.
 * Starts and connects the two peers, then invokes the
 * `connect_continuation` from @a cls.  Sets up a timeout to
 * abort the test, and a shutdown handler to clean up properly
 * on exit.
 *
 * @param cls closure of type `struct GNUNET_TRANSPORT_TESTING_ConnectCheckContext`
 * @param tth_ initialized testing handle
 * @param test_plugin_ name of the plugin
 * @param test_name_ name of the test
 * @param num_peers number of entries in the @a cfg_file array
 * @param cfg_files array of names of configuration files for the peers
 * @return #GNUNET_SYSERR on error
 */
int
GNUNET_TRANSPORT_TESTING_connect_check (void *cls,
                                        struct GNUNET_TRANSPORT_TESTING_Handle *
                                        tth_,
                                        const char *test_plugin_,
                                        const char *test_name_,
                                        unsigned int num_peers,
                                        char *cfg_files[])
{
  static struct GNUNET_GETOPT_CommandLineOption options[] = {
    GNUNET_GETOPT_OPTION_END
  };
  struct GNUNET_TRANSPORT_TESTING_ConnectCheckContext *ccc = cls;
  struct GNUNET_TRANSPORT_TESTING_PeerContext *p[num_peers];
  struct GNUNET_TRANSPORT_TESTING_InternalPeerContext ip[num_peers];
  char *argv[] = {
    (char *) test_name_,
    "-c",
    (char *) ccc->config_file,
    NULL
  };

  ccc->num_peers = num_peers;
  ccc->cfg_files = cfg_files;
  ccc->test_plugin = test_plugin_;
  ccc->test_name = test_name_;
  ccc->tth = tth_;
  ccc->global_ret = GNUNET_OK;
  ccc->p = p;
  ccc->ip = ip;
  for (unsigned int i = 0; i < num_peers; i++)
  {
    ip[i].off = i;
    ip[i].ccc = ccc;
  }
  if (GNUNET_OK !=
      GNUNET_PROGRAM_run ((sizeof(argv) / sizeof(char *)) - 1,
                          argv,
                          test_name_,
                          "nohelp",
                          options,
                          &connect_check_run,
                          ccc))
    return GNUNET_SYSERR;
  return ccc->global_ret;
}


/**
 * Setup testcase.  Calls @a check with the data the test needs.
 *
 * @param argv0 binary name (argv[0])
 * @param filename source file name (__FILE__)
 * @param num_peers number of peers to start
 * @param check main function to run
 * @param check_cls closure for @a check
 * @return #GNUNET_OK on success
 */
int
GNUNET_TRANSPORT_TESTING_main_ (const char *argv0,
                                const char *filename,
                                unsigned int num_peers,
                                GNUNET_TRANSPORT_TESTING_CheckCallback check,
                                void *check_cls)
{
  struct GNUNET_TRANSPORT_TESTING_Handle *tth;
  char *test_name;
  char *test_source;
  char *test_plugin;
  char *cfg_names[num_peers];
  int ret;

  ret = GNUNET_OK;
  test_name = GNUNET_TRANSPORT_TESTING_get_test_name (argv0);
  GNUNET_log_setup (test_name,
                    "WARNING",
                    NULL);
  test_source = GNUNET_TRANSPORT_TESTING_get_test_source_name (filename);
  test_plugin = GNUNET_TRANSPORT_TESTING_get_test_plugin_name (argv0,
                                                               test_source);
  for (unsigned int i = 0; i < num_peers; i++)
    cfg_names[i] = GNUNET_TRANSPORT_TESTING_get_config_name (argv0,
                                                             i + 1);
  tth = GNUNET_TRANSPORT_TESTING_init ();
  if (NULL == tth)
  {
    ret = GNUNET_SYSERR;
  }
  else
  {
    ret = check (check_cls,
                 tth,
                 test_plugin,
                 test_name,
                 num_peers,
                 cfg_names);
    GNUNET_TRANSPORT_TESTING_done (tth);
  }
  for (unsigned int i = 0; i < num_peers; i++)
    GNUNET_free (cfg_names[i]);
  GNUNET_free (test_source);
  GNUNET_free (test_plugin);
  GNUNET_free (test_name);
  return ret;
}


/* end of transport-testing-main.c */
