/*
     This file is part of GNUnet.
     Copyright (C) 2009, 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 arm/test_exponential_backoff.c
 * @brief testcase for gnunet-service-arm.c
 * @author Christian Grothoff
 */
#include "platform.h"
#include "gnunet_arm_service.h"
#include "gnunet_util_lib.h"
#include "gnunet_protocols.h"

#define LOG(...) GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, __VA_ARGS__)

#define LOG_BACKOFF GNUNET_NO

#define TIMEOUT GNUNET_TIME_relative_multiply (GNUNET_TIME_UNIT_SECONDS, 10)

#define SERVICE_TEST_TIMEOUT GNUNET_TIME_UNIT_FOREVER_REL

#define FIVE_MILLISECONDS GNUNET_TIME_relative_multiply ( \
    GNUNET_TIME_UNIT_MILLISECONDS, 5)

#define SERVICE "do-nothing"

#define BINARY "mockup-service"

#define CFGFILENAME "test_arm_api_data2.conf"


static const struct GNUNET_CONFIGURATION_Handle *cfg;

static struct GNUNET_ARM_Handle *arm;

static struct GNUNET_ARM_MonitorHandle *mon;

static struct GNUNET_SCHEDULER_Task *kt;

static int ok = 1;

static int phase = 0;

static int trialCount;

static struct GNUNET_TIME_Absolute startedWaitingAt;

struct GNUNET_TIME_Relative waitedFor;

struct GNUNET_TIME_Relative waitedFor_prev;

#if LOG_BACKOFF
static FILE *killLogFilePtr;

static char *killLogFileName;
#endif


/**
 * Context for handling the shutdown of a service.
 */
struct ShutdownContext
{
  /**
   * Connection to the service that is being shutdown.
   */
  struct GNUNET_MQ_Handle *mq;

  /**
   * Task set up to cancel the shutdown request on timeout.
   */
  struct GNUNET_SCHEDULER_Task *cancel_task;
};


static void
kill_task (void *cbData);


/**
 * Shutting down took too long, cancel receive and return error.
 *
 * @param cls closure
 */
static void
service_shutdown_timeout (void *cls)
{
  GNUNET_assert (0);
}


/**
 * Generic error handler, called with the appropriate error code and
 * the same closure specified at the creation of the message queue.
 * Not every message queue implementation supports an error handler.
 *
 * @param cls closure with the `struct ShutdownContext *`
 * @param error error code
 */
static void
mq_error_handler (void *cls,
                  enum GNUNET_MQ_Error error)
{
  struct ShutdownContext *shutdown_ctx = cls;

  GNUNET_log (GNUNET_ERROR_TYPE_DEBUG,
              "Service shutdown complete (MQ error).\n");
  GNUNET_SCHEDULER_cancel (shutdown_ctx->cancel_task);
  GNUNET_MQ_destroy (shutdown_ctx->mq);
  GNUNET_free (shutdown_ctx);
}


static void
kill_task (void *cbData)
{
  struct ShutdownContext *shutdown_ctx
    = GNUNET_new (struct ShutdownContext);
  struct GNUNET_MQ_Envelope *env;
  struct GNUNET_MessageHeader *msg;
  struct GNUNET_MQ_MessageHandler handlers[] = {
    GNUNET_MQ_handler_end ()
  };

  kt = NULL;
  if (trialCount == 13)
  {
    LOG ("Saw enough kills, asking ARM to stop mock service for good\n");
    GNUNET_ARM_request_service_stop (arm,
                                     SERVICE,
                                     NULL,
                                     NULL);
    ok = 0;
    trialCount++;
    GNUNET_free (shutdown_ctx);
    return;
  }
  shutdown_ctx->mq = GNUNET_CLIENT_connect (cfg,
                                            SERVICE,
                                            handlers,
                                            &mq_error_handler,
                                            shutdown_ctx);
  GNUNET_assert (NULL != shutdown_ctx->mq);
  trialCount++;
  LOG ("Sending a shutdown request to the mock service\n");
  env = GNUNET_MQ_msg (msg,
                       GNUNET_MESSAGE_TYPE_ARM_STOP); /* FIXME: abuse of message type */
  GNUNET_MQ_send (shutdown_ctx->mq,
                  env);
  shutdown_ctx->cancel_task
    = GNUNET_SCHEDULER_add_delayed (TIMEOUT,
                                    &service_shutdown_timeout,
                                    shutdown_ctx);
}


static void
trigger_disconnect (void *cls)
{
  GNUNET_ARM_disconnect (arm);
  GNUNET_ARM_monitor_stop (mon);
  if (NULL != kt)
  {
    GNUNET_SCHEDULER_cancel (kt);
    kt = NULL;
  }
}


static void
arm_stop_cb (void *cls,
             enum GNUNET_ARM_RequestStatus status,
             enum GNUNET_ARM_Result result)
{
  GNUNET_break (status == GNUNET_ARM_REQUEST_SENT_OK);
  GNUNET_break (result == GNUNET_ARM_RESULT_STOPPED);
  LOG ("ARM service stopped\n");
  GNUNET_SCHEDULER_shutdown ();
}


static void
srv_status (void *cls,
            const char *service,
            enum GNUNET_ARM_ServiceMonitorStatus status)
{
  if (status == GNUNET_ARM_SERVICE_MONITORING_STARTED)
  {
    LOG ("ARM monitor started, starting mock service\n");
    phase++;
    GNUNET_ARM_request_service_start (arm,
                                      SERVICE,
                                      GNUNET_OS_INHERIT_STD_OUT_AND_ERR,
                                      NULL,
                                      NULL);
    return;
  }
  if (0 != strcasecmp (service, SERVICE))
    return; /* not what we care about */
  if (phase == 1)
  {
    GNUNET_break (status == GNUNET_ARM_SERVICE_STARTING);
    GNUNET_break (phase == 1);
    LOG ("do-nothing is starting\n");
    phase++;
    ok = 1;
    GNUNET_assert (NULL == kt);
    startedWaitingAt = GNUNET_TIME_absolute_get ();
    kt = GNUNET_SCHEDULER_add_delayed (GNUNET_TIME_UNIT_SECONDS,
                                       &kill_task,
                                       NULL);
  }
  else if (phase == 2)
  {
    /* We passively monitor ARM for status updates. ARM should tell us
     * when do-nothing dies (no need to run a service upness test ourselves).
     */
    if (status == GNUNET_ARM_SERVICE_STARTING)
    {
      waitedFor = GNUNET_TIME_absolute_get_duration (startedWaitingAt);
      LOG ("Waited for: %s\n",
           GNUNET_STRINGS_relative_time_to_string (waitedFor,
                                                   GNUNET_YES));

      LOG ("do-nothing is starting, killing it...\n");
      GNUNET_assert (NULL == kt);
      kt = GNUNET_SCHEDULER_add_now (&kill_task, &ok);
    }
    else if ((status == GNUNET_ARM_SERVICE_STOPPED) && (trialCount == 14))
    {
      phase++;
      LOG ("do-nothing stopped working %u times, we are done here\n",
           (unsigned int) trialCount);
      GNUNET_ARM_request_service_stop (arm,
                                       "arm",
                                       &arm_stop_cb,
                                       NULL);
    }
  }
}


static void
arm_start_cb (void *cls,
              enum GNUNET_ARM_RequestStatus status,
              enum GNUNET_ARM_Result result)
{
  GNUNET_break (status == GNUNET_ARM_REQUEST_SENT_OK);
  GNUNET_break (result == GNUNET_ARM_RESULT_STARTING);
  GNUNET_break (phase == 0);
  LOG ("Sent 'START' request for arm to ARM %s\n",
       (status == GNUNET_ARM_REQUEST_SENT_OK) ? "successfully" :
       "unsuccessfully");
}


static void
task (void *cls,
      char *const *args,
      const char *cfgfile,
      const struct GNUNET_CONFIGURATION_Handle *c)
{
  cfg = c;
  arm = GNUNET_ARM_connect (cfg, NULL, NULL);
  if (NULL == arm)
  {
    GNUNET_break (0);
    return;
  }
  mon = GNUNET_ARM_monitor_start (cfg,
                                  &srv_status,
                                  NULL);
  if (NULL == mon)
  {
    GNUNET_break (0);
    GNUNET_ARM_disconnect (arm);
    arm = NULL;
    return;
  }
  GNUNET_ARM_request_service_start (arm,
                                    "arm",
                                    GNUNET_OS_INHERIT_STD_OUT_AND_ERR,
                                    &arm_start_cb,
                                    NULL);
  GNUNET_SCHEDULER_add_shutdown (&trigger_disconnect,
                                 NULL);
}


static int
check ()
{
  char *const argv[] = {
    "test-exponential-backoff",
    "-c", CFGFILENAME,
    NULL
  };
  struct GNUNET_GETOPT_CommandLineOption options[] = {
    GNUNET_GETOPT_OPTION_END
  };

  /* Running ARM  and running the do_nothing task */
  GNUNET_assert (GNUNET_OK ==
                 GNUNET_PROGRAM_run ((sizeof(argv) / sizeof(char *)) - 1,
                                     argv,
                                     "test-exponential-backoff",
                                     "nohelp",
                                     options,
                                     &task,
                                     NULL));
  return ok;
}


#ifndef PATH_MAX
/**
 * Assumed maximum path length (for the log file name).
 */
#define PATH_MAX 4096
#endif


static int
init ()
{
  struct GNUNET_CONFIGURATION_Handle *cfg;
  char pwd[PATH_MAX];
  char *binary;

  cfg = GNUNET_CONFIGURATION_create ();
  if (GNUNET_OK != GNUNET_CONFIGURATION_parse (cfg,
                                               "test_arm_api_data.conf"))
  {
    GNUNET_CONFIGURATION_destroy (cfg);
    return GNUNET_SYSERR;
  }
  if (NULL == getcwd (pwd, PATH_MAX))
    return GNUNET_SYSERR;
  GNUNET_assert (0 < GNUNET_asprintf (&binary,
                                      "%s/%s",
                                      pwd,
                                      BINARY));
  GNUNET_CONFIGURATION_set_value_string (cfg,
                                         SERVICE,
                                         "BINARY",
                                         binary);
  GNUNET_free (binary);
  if (GNUNET_OK != GNUNET_CONFIGURATION_write (cfg,
                                               CFGFILENAME))
  {
    GNUNET_CONFIGURATION_destroy (cfg);
    return GNUNET_SYSERR;
  }
  GNUNET_CONFIGURATION_destroy (cfg);

#if LOG_BACKOFF
  killLogFileName = GNUNET_DISK_mktemp ("exponential-backoff-waiting.log");
  if (NULL == (killLogFilePtr = fopen (killLogFileName,
                                       "w")))
  {
    GNUNET_log_strerror_file (GNUNET_ERROR_TYPE_WARNING,
                              "fopen",
                              killLogFileName);
    GNUNET_free (killLogFileName);
    return GNUNET_SYSERR;
  }
#endif
  return GNUNET_OK;
}


static void
houseKeep ()
{
#if LOG_BACKOFF
  GNUNET_assert (0 == fclose (killLogFilePtr));
  GNUNET_free (killLogFileName);
#endif
  (void) unlink (CFGFILENAME);
}


int
main (int argc, char *argv[])
{
  int ret;

  GNUNET_log_setup ("test-exponential-backoff",
                    "WARNING",
                    NULL);

  if (GNUNET_OK != init ())
    return 1;
  ret = check ();
  houseKeep ();
  return ret;
}


/* end of test_exponential_backoff.c */
