/*
     This file is part of GNUnet.
     Copyright (C) 2003, 2004, 2005, 2006, 2007 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 util/test_configuration.c
 * @brief Test that the configuration module works.
 * @author Christian Grothoff
 */


#include "platform.h"
#include "gnunet_util_lib.h"


/* Test Configuration Diffs Options */
enum
{
  EDIT_NOTHING,
  EDIT_SECTION,
  EDIT_ALL,
  ADD_NEW_SECTION,
  ADD_NEW_ENTRY,
  REMOVE_SECTION,
  REMOVE_ENTRY,
  COMPARE,
  PRINT
};

static struct GNUNET_CONFIGURATION_Handle *cfg;
static struct GNUNET_CONFIGURATION_Handle *cfg_default;

struct DiffsCBData
{
  struct GNUNET_CONFIGURATION_Handle *cfg;
  struct GNUNET_CONFIGURATION_Handle *cfgDiffs;
  const char *section;
  int callBackOption;
  int status;
};


static void
initDiffsCBData (struct DiffsCBData *cbData)
{
  cbData->section = NULL;
  cbData->cfg = NULL;
  cbData->cfgDiffs = NULL;
  cbData->callBackOption = -1;
  cbData->status = 0;
}


/**
 * callback function for modifying
 * and comparing configuration
 */
static void
diffsCallBack (void *cls,
               const char *section,
               const char *option,
               const char *value)
{
  struct DiffsCBData *cbData = cls;
  int cbOption = cbData->callBackOption;

  if (0 == strcasecmp ("PATHS",
                       section))
    return;
  switch (cbOption)
  {
  case EDIT_SECTION:
    if (NULL == cbData->section)
      cbData->section = section;
    if (strcmp (cbData->section, section) == 0)
    {
      GNUNET_CONFIGURATION_set_value_string (cbData->cfg, section, option,
                                             "new-value");
      GNUNET_CONFIGURATION_set_value_string (cbData->cfgDiffs, section, option,
                                             "new-value");
    }
    break;
  case EDIT_ALL:
    GNUNET_CONFIGURATION_set_value_string (cbData->cfg, section, option,
                                           "new-value");
    GNUNET_CONFIGURATION_set_value_string (cbData->cfgDiffs, section, option,
                                           "new-value");
    break;
  case ADD_NEW_ENTRY:
    {
      static int hit = 0;

      if (hit == 0)
      {
        hit = 1;
        GNUNET_CONFIGURATION_set_value_string (cbData->cfg, section, "new-key",
                                               "new-value");
        GNUNET_CONFIGURATION_set_value_string (cbData->cfgDiffs, section,
                                               "new-key", "new-value");
      }
      break;
    }
  case COMPARE:
    {
      int ret;
      char *diffValue;

      diffValue = NULL;
      ret =
        GNUNET_CONFIGURATION_get_value_string (cbData->cfgDiffs,
                                               section,
                                               option,
                                               &diffValue);
      if (NULL != diffValue)
      {
        if ( (ret == GNUNET_SYSERR) ||
             (strcmp (diffValue, value) != 0) )
        {
          GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
                      "Unexpected value %s in %s/%s\n",
                      diffValue,
                      section,
                      option);
          cbData->status = 1;
        }
      }
      else
      {
        GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
                    "Unexpected value %s in %s/%s\n",
                    diffValue,
                    section,
                    option);
        cbData->status = 1;
      }
      GNUNET_free (diffValue);
      break;
    }

#if 0
  case PRINT:
    if (NULL == cbData->section)
    {
      cbData->section = section;
      printf ("\nSection: %s\n", section);
    }
    else if (strcmp (cbData->section, section) != 0)
    {
      cbData->section = section;
      printf ("\nSection: %s\n", section);
    }
    printf ("%s = %s\n", option, value);
#endif
  default:
    break;
  }
}


static struct GNUNET_CONFIGURATION_Handle *
editConfiguration (struct GNUNET_CONFIGURATION_Handle *cfg_,
                   int option)
{
  struct DiffsCBData diffsCB;

  initDiffsCBData (&diffsCB);
  diffsCB.cfgDiffs = GNUNET_CONFIGURATION_create (GNUNET_OS_project_data_gnunet
                                                    ());

  switch (option)
  {
  case EDIT_SECTION:
  case EDIT_ALL:
  case ADD_NEW_ENTRY:
    diffsCB.callBackOption = option;
    diffsCB.cfg = cfg_;
    GNUNET_CONFIGURATION_iterate (cfg_, diffsCallBack, &diffsCB);
    break;

  case EDIT_NOTHING:
    /* Do nothing */
    break;

  case ADD_NEW_SECTION:
    {
      int i;
      char *key;

      for (i = 0; i < 5; i++)
      {
        GNUNET_asprintf (&key, "key%d", i);
        GNUNET_CONFIGURATION_set_value_string (cfg_, "new-section", key,
                                               "new-value");
        GNUNET_CONFIGURATION_set_value_string (diffsCB.cfgDiffs, "new-section",
                                               key, "new-value");
        GNUNET_free (key);
      }
      break;
    }

  case REMOVE_SECTION:
    break;

  case REMOVE_ENTRY:
    break;

  default:
    break;
  }

  return diffsCB.cfgDiffs;
}


/**
 * Checking configuration diffs
 */
static int
checkDiffs (struct GNUNET_CONFIGURATION_Handle *cfg_, int option)
{
  struct GNUNET_CONFIGURATION_Handle *cfg_new;
  struct GNUNET_CONFIGURATION_Handle *cfgDiffs;
  struct DiffsCBData cbData;
  int ret;
  char *diffsFileName;

  initDiffsCBData (&cbData);

  cfg_new = GNUNET_CONFIGURATION_create (GNUNET_OS_project_data_gnunet ());
  /* load defaults */
  GNUNET_assert (GNUNET_OK == GNUNET_CONFIGURATION_load (cfg_new, NULL));

  /* Modify configuration and save it */
  cfgDiffs = editConfiguration (cfg_new, option);
  diffsFileName = GNUNET_DISK_mktemp ("gnunet-test-configurations-diffs.conf");
  if (diffsFileName == NULL)
  {
    GNUNET_break (0);
    GNUNET_CONFIGURATION_destroy (cfg_new);
    GNUNET_CONFIGURATION_destroy (cfgDiffs);
    return 1;
  }
  GNUNET_CONFIGURATION_write_diffs (cfg_, cfg_new, diffsFileName);
  GNUNET_CONFIGURATION_destroy (cfg_new);

  /* Compare the dumped configuration with modifications done */
  cfg_new = GNUNET_CONFIGURATION_create (GNUNET_OS_project_data_gnunet ());
  GNUNET_assert (GNUNET_OK ==
                 GNUNET_CONFIGURATION_parse (cfg_new,
                                             diffsFileName));
  if (0 != remove (diffsFileName))
    GNUNET_log_strerror_file (GNUNET_ERROR_TYPE_WARNING, "remove",
                              diffsFileName);
  cbData.callBackOption = COMPARE;
  cbData.cfgDiffs = cfgDiffs;
  GNUNET_CONFIGURATION_iterate (cfg_new,
                                &diffsCallBack,
                                &cbData);
  if (1 == (ret = cbData.status))
  {
    fprintf (stderr, "%s",
             "Incorrect Configuration Diffs: Diffs may contain data not actually edited\n");
    goto housekeeping;
  }
  cbData.cfgDiffs = cfg_new;
  GNUNET_CONFIGURATION_iterate (cfgDiffs, diffsCallBack, &cbData);
  if ((ret = cbData.status) == 1)
    fprintf (stderr, "%s",
             "Incorrect Configuration Diffs: Data may be missing in diffs\n");

housekeeping:
#if 0
  cbData.section = NULL;
  cbData.callBackOption = PRINT;
  printf ("\nExpected Diffs:\n");
  GNUNET_CONFIGURATION_iterate (cfgDiffs, diffsCallBack, &cbData);
  cbData.section = NULL;
  printf ("\nActual Diffs:\n");
  GNUNET_CONFIGURATION_iterate (cfg, diffsCallBack, &cbData);
#endif
  GNUNET_CONFIGURATION_destroy (cfg_new);
  GNUNET_CONFIGURATION_destroy (cfgDiffs);
  GNUNET_free (diffsFileName);
  return ret;
}


static int
testConfig ()
{
  char *c;
  unsigned long long l;
  float f;

  if (GNUNET_OK != GNUNET_CONFIGURATION_get_value_string (cfg, "test", "b", &c))
    return 1;
  if (0 != strcmp ("b", c))
  {
    fprintf (stderr, "Got `%s'\n", c);
    GNUNET_free (c);
    return 2;
  }
  GNUNET_free (c);
  if (GNUNET_OK !=
      GNUNET_CONFIGURATION_get_value_number (cfg, "test", "five", &l))
  {
    GNUNET_break (0);
    return 3;
  }
  if (5 != l)
  {
    GNUNET_break (0);
    return 4;
  }
  GNUNET_CONFIGURATION_set_value_string (cfg, "more", "c", "YES");
  if (GNUNET_NO == GNUNET_CONFIGURATION_get_value_yesno (cfg, "more", "c"))
  {
    GNUNET_break (0);
    return 5;
  }
  GNUNET_CONFIGURATION_set_value_number (cfg, "NUMBERS", "TEN", 10);
  if (GNUNET_OK !=
      GNUNET_CONFIGURATION_get_value_string (cfg, "NUMBERS", "TEN", &c))
  {
    GNUNET_break (0);
    return 6;
  }
  if (0 != strcmp (c, "10"))
  {
    GNUNET_free (c);
    GNUNET_break (0);
    return 7;
  }
  GNUNET_free (c);

  // This only works if we have this locale.
  if (NULL != setlocale (LC_NUMERIC, "de_DE"))
  {
    GNUNET_CONFIGURATION_set_value_string (cfg, "NUMBERS", "FLOAT_TEST_INVALID",
                                           "1,23");
    if (GNUNET_SYSERR !=
        GNUNET_CONFIGURATION_get_value_float (cfg, "NUMBERS",
                                              "FLOAT_TEST_INVALID", &f))
    {
      printf ("Parsed: %f\n", f);
      GNUNET_break (0);
      return 6;
    }
    GNUNET_CONFIGURATION_set_value_float (cfg, "NUMBERS", "FLOAT_TEST", 1.23);
    if (GNUNET_OK !=
        GNUNET_CONFIGURATION_get_value_string (cfg, "NUMBERS", "FLOAT_TEST", &c)
        )
    {
      GNUNET_break (0);
      return 6;
    }
    if (0 != strncmp (c, "1.23", 4))
    {
      printf ("%s\n", c);
      GNUNET_free (c);
      GNUNET_break (0);
      return 7;
    }
    GNUNET_free (c);
  }
  if (GNUNET_OK !=
      GNUNET_CONFIGURATION_get_value_filename (cfg, "last", "test", &c))
  {
    GNUNET_break (0);
    return 8;
  }

  if (0 != strcmp (c, "/hello/world"))
  {
    GNUNET_break (0);
    GNUNET_free (c);
    return 9;
  }
  GNUNET_free (c);

  if (GNUNET_OK !=
      GNUNET_CONFIGURATION_get_value_size (cfg, "last", "size", &l))
  {
    GNUNET_break (0);
    return 10;
  }
  if (l != 512 * 1024)
  {
    GNUNET_break (0);
    return 11;
  }
  return 0;
}


static const char *want[] = {
  "/Hello",
  "/File Name",
  "/World",
  NULL,
  NULL,
};

static int
check (void *data, const char *fn)
{
  int *idx = data;

  if (0 == strcmp (want[*idx], fn))
  {
    (*idx)++;
    return GNUNET_OK;
  }
  GNUNET_break (0);
  return GNUNET_SYSERR;
}


static int
testConfigFilenames ()
{
  int idx;

  idx = 0;
  if (3 !=
      GNUNET_CONFIGURATION_iterate_value_filenames (cfg, "FILENAMES", "test",
                                                    &check, &idx))
  {
    GNUNET_break (0);
    return 8;
  }
  if (idx != 3)
    return 16;
  if (GNUNET_OK !=
      GNUNET_CONFIGURATION_remove_value_filename (cfg, "FILENAMES", "test",
                                                  "/File Name"))
  {
    GNUNET_break (0);
    return 24;
  }

  if (GNUNET_NO !=
      GNUNET_CONFIGURATION_remove_value_filename (cfg, "FILENAMES", "test",
                                                  "/File Name"))
  {
    GNUNET_break (0);
    return 32;
  }
  if (GNUNET_NO !=
      GNUNET_CONFIGURATION_remove_value_filename (cfg, "FILENAMES", "test",
                                                  "Stuff"))
  {
    GNUNET_break (0);
    return 40;
  }

  if (GNUNET_NO !=
      GNUNET_CONFIGURATION_append_value_filename (cfg, "FILENAMES", "test",
                                                  "/Hello"))
  {
    GNUNET_break (0);
    return 48;
  }
  if (GNUNET_NO !=
      GNUNET_CONFIGURATION_append_value_filename (cfg, "FILENAMES", "test",
                                                  "/World"))
  {
    GNUNET_break (0);
    return 56;
  }

  if (GNUNET_YES !=
      GNUNET_CONFIGURATION_append_value_filename (cfg, "FILENAMES", "test",
                                                  "/File 1"))
  {
    GNUNET_break (0);
    return 64;
  }

  if (GNUNET_YES !=
      GNUNET_CONFIGURATION_append_value_filename (cfg, "FILENAMES", "test",
                                                  "/File 2"))
  {
    GNUNET_break (0);
    return 72;
  }

  idx = 0;
  want[1] = "/World";
  want[2] = "/File 1";
  want[3] = "/File 2";
  if (4 !=
      GNUNET_CONFIGURATION_iterate_value_filenames (cfg, "FILENAMES", "test",
                                                    &check, &idx))
  {
    GNUNET_break (0);
    return 80;
  }
  if (idx != 4)
  {
    GNUNET_break (0);
    return 88;
  }
  return 0;
}


int
main (int argc, char *argv[])
{
  int failureCount = 0;
  char *c;

  GNUNET_log_setup ("test_configuration", "WARNING", NULL);
  cfg = GNUNET_CONFIGURATION_create (GNUNET_OS_project_data_gnunet ());
  GNUNET_assert (cfg != NULL);
  if (GNUNET_OK !=
      GNUNET_CONFIGURATION_parse (cfg, "test_configuration_data.conf"))
  {
    fprintf (stderr, "%s", "Failed to parse configuration file\n");
    GNUNET_CONFIGURATION_destroy (cfg);
    return 1;
  }
  failureCount += testConfig ();
  if (failureCount > 0)
    goto error;

  failureCount = testConfigFilenames ();
  if (failureCount > 0)
    goto error;

  if (GNUNET_OK != GNUNET_CONFIGURATION_write (cfg, "/tmp/gnunet-test.conf"))
  {
    fprintf (stderr, "%s", "Failed to write configuration file\n");
    GNUNET_CONFIGURATION_destroy (cfg);
    return 1;
  }
  GNUNET_CONFIGURATION_destroy (cfg);
  GNUNET_assert (0 == unlink ("/tmp/gnunet-test.conf"));

  cfg = GNUNET_CONFIGURATION_create (GNUNET_OS_project_data_gnunet ());
  if (GNUNET_OK !=
      GNUNET_CONFIGURATION_load (cfg,
                                 "test_configuration_data.conf"))
  {
    GNUNET_break (0);
    GNUNET_CONFIGURATION_destroy (cfg);
    return 1;
  }
  if (GNUNET_OK !=
      GNUNET_CONFIGURATION_get_value_string (cfg, "TESTING", "WEAKRANDOM", &c))
  {
    GNUNET_break (0);
    GNUNET_CONFIGURATION_destroy (cfg);
    return 1;
  }
  if (0 != strcmp (c, "YES"))
  {
    GNUNET_break (0);
    GNUNET_free (c);
    GNUNET_CONFIGURATION_destroy (cfg);
    return 1;
  }

  GNUNET_free (c);
  GNUNET_CONFIGURATION_destroy (cfg);

  /* Testing configuration diffs */
  cfg_default = GNUNET_CONFIGURATION_create (GNUNET_OS_project_data_gnunet ());
  if (GNUNET_OK !=
      GNUNET_CONFIGURATION_load (cfg_default,
                                 NULL))
  {
    GNUNET_break (0);
    GNUNET_CONFIGURATION_destroy (cfg_default);
    return 1;
  }

  /* Nothing changed in the new configuration */
  failureCount += checkDiffs (cfg_default, EDIT_NOTHING);

  /* Modify all entries of the last section */
  failureCount += checkDiffs (cfg_default, EDIT_SECTION);

  /* Add a new section */
  failureCount += checkDiffs (cfg_default, ADD_NEW_SECTION);

  /* Add a new entry to the last section */
  failureCount += checkDiffs (cfg_default, ADD_NEW_ENTRY);

  /* Modify all entries in the configuration */
  failureCount += checkDiffs (cfg_default, EDIT_ALL);

  GNUNET_CONFIGURATION_destroy (cfg_default);

error:
  if (failureCount != 0)
  {
    fprintf (stderr, "Test failed: %u\n", failureCount);
    return 1;
  }
  return 0;
}
