#include "config.h"

#include <sys/types.h>
#include <dirent.h>
#include <fcntl.h>
#include <glib.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <unistd.h>

#include "date.h"
#include "Group.h"
#include "gnc-engine.h"
#include "gnc-engine-util.h"
#include "Transaction.h"

#include "test-engine-stuff.h"
#include "test-stuff.h"

static gboolean glist_strings_only = FALSE;

static GHashTable *exclude_kvp_types = NULL;
static gint kvp_max_depth = 5;
static gint kvp_frame_max_elements = 10;

static gint max_group_depth = 4;
static gint max_group_accounts = 10;

static kvp_value* get_random_kvp_value_depth (int type, gint depth);
static gpointer get_random_list_element (GList *list);
static void add_random_splits(GNCBook *book, Transaction *trn);


/***********************************************************************/

void
set_max_group_depth (gint max_group_depth_in)
{
  max_group_depth = MAX (max_group_depth_in, 1);
}

void
set_max_group_accounts (gint max_group_accounts_in)
{
  max_group_accounts = MAX (max_group_accounts_in, 1);
}

void
set_max_kvp_depth (gint max_kvp_depth)
{
  kvp_max_depth = MAX (max_kvp_depth, 1);
}

void
set_max_kvp_frame_elements (gint max_kvp_frame_elements)
{
  kvp_frame_max_elements = MAX (max_kvp_frame_elements, 1);
}

void
kvp_exclude_type (kvp_value_t kvp_type)
{
  gint *key;

  if (!exclude_kvp_types)
    exclude_kvp_types = g_hash_table_new (g_int_hash, g_int_equal);

  key = g_new (gint, 1);
  *key = kvp_type;

  g_hash_table_insert (exclude_kvp_types, key, exclude_kvp_types);
}

static gboolean
kvp_type_excluded (kvp_value_t kvp_type)
{
  gint key = kvp_type;

  if (!exclude_kvp_types)
    return FALSE;

  if (g_hash_table_lookup (exclude_kvp_types, &key))
    return TRUE;

  return FALSE;
}

void
random_glist_strings_only (gboolean strings_only)
{
  glist_strings_only = strings_only;
}

static gboolean zero_nsec = FALSE;

void
random_timespec_zero_nsec (gboolean zero_nsec_in)
{
  zero_nsec = zero_nsec_in;
}

static gboolean usec_resolution = FALSE;

void
random_timespec_usec_resolution (gboolean usec_resolution_in)
{
  usec_resolution = usec_resolution_in;
}

Timespec*
get_random_timespec(void)
{
  Timespec *ret;

  ret = g_new0(Timespec, 1);

  while (ret->tv_sec <= 0)
    ret->tv_sec = rand();

  if (zero_nsec)
    ret->tv_nsec = 0;
  else
  {
    ret->tv_nsec = rand();

    if (usec_resolution)
    {
      ret->tv_nsec = MIN (ret->tv_nsec, 999999999);
      ret->tv_nsec /= 1000;
      ret->tv_nsec *= 1000;
    }
  }

  return ret;
}

gnc_numeric
get_random_gnc_numeric(void)
{
    return gnc_numeric_create(get_random_gint64(), rand());
}

const char *types[] =
{
    "NASDAQ",
    "NYSE",
    "EUREX",
    "FUND",
    "AMEX",
    NULL
};

const char*
get_random_commodity_namespace(void)
{
    return get_random_string_in_array(types);
}

void
make_random_changes_to_price (GNCBook *book, GNCPrice *p)
{
  Timespec *ts;
  char *string;
  gnc_commodity *c;

  g_return_if_fail (book && p);

  gnc_price_begin_edit (p);

  c = get_random_commodity (book);
  gnc_price_set_commodity (p, c);

  c = get_random_commodity (book);
  gnc_price_set_currency (p, c);

  ts = get_random_timespec ();
  gnc_price_set_time (p, *ts);
  g_free (ts);

  string = get_random_string ();
  gnc_price_set_source (p, string);
  g_free (string);

  string = get_random_string ();
  gnc_price_set_type (p, string);
  g_free (string);

  gnc_price_set_value (p, get_random_gnc_numeric ());

  gnc_price_commit_edit (p);
}

GNCPrice *
get_random_price(GNCBook *book)
{
  GNCPrice *p;

  p = gnc_price_create (book);

  make_random_changes_to_price (book, p);

  return p;
}

void
make_random_pricedb (GNCBook *book, GNCPriceDB *db)
{
  int num_prices;

  num_prices = get_random_int_in_range (0, 40);

  while (num_prices-- > 0)
  {
    GNCPrice *p;

    p = get_random_price (book);

    gnc_pricedb_add_price (db, p);

    gnc_price_unref (p);
  }
}

GNCPriceDB *
get_random_pricedb(GNCBook *book)
{
  GNCPriceDB *db;

  db = gnc_pricedb_create (book);
  make_random_pricedb (book, db);

  return db;
}

static gboolean
price_accumulator (GNCPrice *p, gpointer data)
{
  GList **list = data;

  *list = g_list_prepend (*list, p);

  return TRUE;
}

void
make_random_changes_to_pricedb (GNCBook *book, GNCPriceDB *pdb)
{
  GList *list = NULL;
  GList *node;

  g_return_if_fail (pdb);

  gnc_pricedb_foreach_price (pdb, price_accumulator, &list, FALSE);

  for (node = list; node; node = node->next)
  {
    GNCPrice *p = node->data;

    switch (get_random_int_in_range (0, 5))
    {
      case 0: /* Delete */
        gnc_pricedb_remove_price (pdb, p);
        break;

      case 1:
      case 2: /* Change */
        make_random_changes_to_price (book, p);
        break;

      default: /* nothing */
        break;
    }
  }

  g_list_free (list);

  /* Add a few new ones */
  {
    int i = get_random_int_in_range (1, 5);

    while (i--)
    {
      GNCPrice *p = get_random_price (book);

      gnc_pricedb_add_price (pdb, p);

      gnc_price_unref (p);
    }
  }
}

GUID*
get_random_guid(void)
{
    GUID *ret;

    ret = g_new(GUID, 1);
    guid_new(ret);

    return ret;
}

static GList*
get_random_glist_depth (gint depth)
{
    GList *ret = NULL;
    int count = get_random_int_in_range(1, 5);
    int i;

    if (depth >= kvp_max_depth)
      return NULL;

    for (i = 0; i < count; i++)
    {
        kvp_value_t kvpt;
        kvp_value *value;

        kvpt = glist_strings_only ? KVP_TYPE_STRING : -2;

        do
        {
          value = get_random_kvp_value_depth (kvpt, depth + 1);
        }
        while (!value);

        ret = g_list_prepend(ret, value);
    }

    return ret;
}

GList*
get_random_glist(void)
{
  return get_random_glist_depth (0);
}

bin_data*
get_random_binary_data(void)
{
    int len;
    bin_data *ret;

    len = get_random_int_in_range(20,100);
    ret = g_new(bin_data, 1);
    ret->data = g_new(char, len);
    ret->len = len;

    for(len--; len >= 0; len--)
    {
        ret->data[len] = (char)get_random_int_in_range(0,255);
    }

    return ret;
}

static kvp_frame*
get_random_kvp_frame_depth (gint depth)
{
    kvp_frame *ret;
    int vals_to_add;
    gboolean val_added;

    if (depth >= kvp_max_depth)
      return NULL;

    ret = kvp_frame_new();

    vals_to_add = get_random_int_in_range(1,kvp_frame_max_elements);
    val_added = FALSE;

    for (;vals_to_add > 0; vals_to_add--)
    {
        gchar *key;
        kvp_value *val;

        do
        {
          key = get_random_string_without("/");
        } while (!key || *key == '\0');

        val = get_random_kvp_value_depth (-1, depth + 1);
        if (!val)
        {
          if (!val_added)
            vals_to_add++;
          continue;
        }

        val_added = TRUE;

        kvp_frame_set_slot_nc(ret, key, val);

        g_free(key);
    }

    return ret;
}

kvp_frame*
get_random_kvp_frame (void)
{
  return get_random_kvp_frame_depth (0);
}

static kvp_value*
get_random_kvp_value_depth (int type, gint depth)
{
    int datype = type;

    if (datype == -1)
    {
        datype = get_random_int_in_range(KVP_TYPE_GINT64, KVP_TYPE_FRAME);
    }

    if (datype == -2)
    {
        datype = get_random_int_in_range(KVP_TYPE_GINT64, KVP_TYPE_FRAME - 1);
    }

    if (datype == KVP_TYPE_FRAME && depth >= kvp_max_depth)
      return NULL;

    if (datype == KVP_TYPE_GLIST && depth >= kvp_max_depth)
      return NULL;

    if (kvp_type_excluded (datype))
      return NULL;

    switch(datype)
    {
    case KVP_TYPE_GINT64:
        return kvp_value_new_gint64(get_random_gint64());
        break;

    case KVP_TYPE_DOUBLE:
	return NULL;
        break;

    case KVP_TYPE_NUMERIC:
        return kvp_value_new_gnc_numeric(get_random_gnc_numeric());
        break;

    case KVP_TYPE_STRING:
    {
        gchar *tmp_str;
        kvp_value *ret;
        tmp_str = get_random_string();
        if(!tmp_str)
        {
            return NULL;
        }
        
        ret = kvp_value_new_string(tmp_str);
        g_free(tmp_str);
        return ret;
    }
        break;

    case KVP_TYPE_GUID:
    {
        GUID *tmp_guid;
        kvp_value *ret;
        tmp_guid = get_random_guid();
        ret = kvp_value_new_guid(tmp_guid);
        g_free(tmp_guid);
        return ret;
    }
        break;

    case KVP_TYPE_TIMESPEC:
    {
        Timespec *ts = get_random_timespec();
        return kvp_value_new_timespec (*ts);
    }
	break;
    
    case KVP_TYPE_BINARY:
    {
        bin_data *tmp_data;
        kvp_value *ret;
        tmp_data = get_random_binary_data();
        ret = kvp_value_new_binary(tmp_data->data, tmp_data->len);
        g_free(tmp_data->data);
        g_free(tmp_data);
        return ret;
    }
        break;
 
    case KVP_TYPE_GLIST:
        return kvp_value_new_glist_nc(get_random_glist_depth (depth + 1));
        break;

    case KVP_TYPE_FRAME:
    {
        kvp_frame *tmp_frame;
        kvp_value *ret;
        tmp_frame = get_random_kvp_frame_depth(depth + 1);
        ret = kvp_value_new_frame(tmp_frame);
        kvp_frame_delete(tmp_frame);
        return ret;
    }
        break;

    default:
        return NULL;
        break;
    }
}

kvp_value *
get_random_kvp_value(int type)
{
  return get_random_kvp_value_depth (type, 0);
}

static void
set_account_random_string(Account* act,
                          void(*func)(Account *act, const gchar*str))
{
    gchar *tmp_str = get_random_string();
    if(tmp_str)
    {
        (func)(act, tmp_str);
        g_free(tmp_str);
    }
}

static void
account_add_subaccounts (GNCBook *book, Account *account, int depth)
{
  int num_accounts;

  if (depth == 0)
    return;

  num_accounts = get_random_int_in_range (1, 10);

  while (num_accounts-- > 0)
  {
    Account *sub = get_random_account (book);

    xaccAccountInsertSubAccount (account, sub);

    account_add_subaccounts (book, sub, depth - 1);
  }
}

static void
make_random_group_depth (GNCBook *book, AccountGroup *group, int depth)
{
  int num_accounts;

  g_return_if_fail (book);
  g_return_if_fail (group);

  if (depth <= 0)
    return;

  num_accounts = get_random_int_in_range (1, max_group_accounts);

  while (num_accounts-- > 0)
  {
    Account *account = get_random_account (book);

    xaccGroupInsertAccount (group, account);

    account_add_subaccounts (book, account, depth - 1);
  }
}

static void
make_random_group (GNCBook *book, AccountGroup * group)
{
  int depth;

  g_return_if_fail (book);
  g_return_if_fail (group);

  depth = get_random_int_in_range (1, max_group_depth);

  make_random_group_depth (book, group, depth);
}

AccountGroup *
get_random_group (GNCBook *book)
{
  AccountGroup * group;

  g_return_val_if_fail (book, NULL);

  group = xaccMallocAccountGroup (book);

  make_random_group (book, group);

  return group;
}

typedef struct
{
  GUID guid;
} TransInfo;

void
make_random_changes_to_transaction_and_splits (GNCBook *book,
                                               Transaction *trans,
                                               GList *accounts)
{
  GList *splits;
  GList *node;
  Split *split;

  g_return_if_fail (book);
  g_return_if_fail (trans);
  g_return_if_fail (accounts);

  xaccTransBeginEdit (trans);

  make_random_changes_to_transaction (book, trans);

  switch (get_random_int_in_range (0, 3))
  {
    case 0: /* delete some splits, add some more */
      if (xaccTransGetVoidStatus (trans))
        break;

      do
      {
        split = xaccTransGetSplit (trans, 0);

        xaccSplitDestroy (split);
      } while (split);

      add_random_splits (book, trans);

      /* fall through */

    case 1: /* move the splits around */
      if (xaccTransGetVoidStatus (trans))
        break;

      splits = xaccTransGetSplitList (trans);
      for (node = splits; node; node = node->next)
      {
        Split *split = node->data;
        Account *account;

        account = get_random_list_element (accounts);

        xaccAccountInsertSplit (account, split);
      }
      break;

    case 2: /* destroy the transaction */
      xaccTransDestroy (trans);
      xaccTransCommitEdit (trans);
      return;

    default: /* do nothing */
      break;
  }

  if (xaccTransGetVoidStatus (trans))
  {
    xaccTransCommitEdit (trans);
    return;
  }

  /* mess with the splits */
  splits = xaccTransGetSplitList (trans);
  for (node = splits; node; node = node->next)
  {
    Split *split = node->data;

    if (get_random_boolean ())
      make_random_changes_to_split (split);
  }

  if (get_random_boolean ())
    xaccTransCommitEdit (trans);
  else
    xaccTransRollbackEdit (trans);
}

static gboolean
add_trans_helper (Transaction *trans, gpointer data)
{
  TransInfo *ti;
  GList **list = data;

  ti = g_new (TransInfo, 1);

  ti->guid = *xaccTransGetGUID (trans);

  *list = g_list_prepend (*list, ti);

  return TRUE;
}

void
make_random_changes_to_group (GNCBook *book, AccountGroup *group)
{
  Account *new_account;
  Account *account;
  GList *accounts;
  GList *transes;
  GList *splits;
  GList *node;

  g_return_if_fail (group && book);

  accounts = xaccGroupGetSubAccounts (group);

  /* Add a new account */
  new_account = get_random_account (book);

  if (get_random_boolean () || !accounts)
    xaccGroupInsertAccount (group, new_account);
  else
  {
    account = get_random_list_element (accounts);

    xaccAccountInsertSubAccount (account, new_account);
  }

  g_list_free (accounts);
  accounts = xaccGroupGetSubAccounts (group);

  /* Add some new transactions */
  add_random_transactions_to_book (book, get_random_int_in_range (1, 6));

  /* Mess with the accounts */
  for (node = accounts; node; node = node->next)
  {
    Account *account = node->data;

    if (get_random_boolean ())
      make_random_changes_to_account (book, account);
  }

  /* Mess with the transactions & splits */
  transes = NULL;
  xaccGroupForEachTransaction (group, add_trans_helper, &transes);

  for (node = transes; node; node = node->next)
  {
    TransInfo *ti = node->data;
    Transaction *trans = xaccTransLookup (&ti->guid, book);

    if (!trans)
      continue;

    make_random_changes_to_transaction_and_splits (book, trans, accounts);
  }

  for (node = transes; node; node = node->next)
  {
    TransInfo *ti = node->data;

    g_free (ti);
  }
  g_list_free (transes);
  transes = NULL;

  /* delete an account */
  account = get_random_list_element (accounts);

  splits = xaccAccountGetSplitList (account);
  splits = g_list_copy (splits);

  for (node = splits; node; node = node->next)
  {
    Split *split = node->data;

    do
    {
      new_account = get_random_list_element (accounts);
    } while (new_account == account);

    xaccAccountInsertSplit (new_account, split);
  }

  xaccAccountBeginEdit (account);
  xaccAccountDestroy (account);

  g_list_free (splits);
  g_list_free (accounts);

  accounts = xaccGroupGetSubAccounts (group);

  /* move some accounts around */
  if (accounts && (g_list_length (accounts) > 1))
  {
    int i = get_random_int_in_range (1, 4);

    while (i--)
    {
      Account *a1, *a2;

      a1 = get_random_list_element (accounts);

      if (get_random_boolean ())
        a2 = get_random_list_element (accounts);
      else
        a2 = NULL;

      if (!a2)
      {
        xaccGroupInsertAccount (group, a1);
        continue;
      }

      if (a1 == a2 ||
          xaccAccountHasAncestor (a1, a2) ||
          xaccAccountHasAncestor (a2, a1))
      {
        i++;
        continue;
      }

      xaccAccountInsertSubAccount (a2, a1);
    }
  }

  g_list_free (accounts);
}

Account*
get_random_account(GNCBook *book)
{
    Account *ret;
    int tmp_int;

    ret = xaccMallocAccount(book);

    xaccAccountBeginEdit(ret);

    set_account_random_string(ret, xaccAccountSetName);

    tmp_int = get_random_int_in_range(BANK, NUM_ACCOUNT_TYPES - 1);
    xaccAccountSetType(ret, tmp_int);

    set_account_random_string(ret, xaccAccountSetCode);
    set_account_random_string(ret, xaccAccountSetDescription);

    xaccAccountSetCommodity(ret, get_random_commodity(book));

    xaccAccountSetSlots_nc(ret, get_random_kvp_frame());

    xaccAccountCommitEdit(ret);

    return ret;
}

void
make_random_changes_to_account (GNCBook *book, Account *account)
{
    int tmp_int;

    g_return_if_fail (account);

    xaccAccountBeginEdit (account);

    set_account_random_string (account, xaccAccountSetName);

    tmp_int = get_random_int_in_range (BANK, CREDITLINE);
    xaccAccountSetType (account, tmp_int);

    set_account_random_string (account, xaccAccountSetCode);
    set_account_random_string (account, xaccAccountSetDescription);

    xaccAccountSetCommodity (account, get_random_commodity(book));

    xaccAccountSetSlots_nc (account, get_random_kvp_frame());

    xaccAccountCommitEdit (account);
}

static void
set_split_random_string(Split *spl,
                       void(*func)(Split *act, const gchar*str))
{
    gchar *tmp_str = get_random_string();
    if(tmp_str)
    {
        (func)(spl, tmp_str);
        g_free(tmp_str);
    }
}

/* Don't do voiding here, it should be done by xaccTransVoid */
static char possible_chars[] = { NREC, CREC, YREC, FREC };

Split*
get_random_split(GNCBook *book, gnc_numeric num)
{
    Split *ret;
    gnc_numeric oneVal;

    ret = xaccMallocSplit(book);

    set_split_random_string(ret, xaccSplitSetMemo);
    set_split_random_string(ret, xaccSplitSetAction);

    xaccSplitSetReconcile(ret, possible_chars[get_random_int_in_range(0, 3)]);

    xaccSplitSetDateReconciledTS(ret, get_random_timespec());

    xaccSplitSetValue(ret, num);
    xaccSplitSetAmount(ret, num);

    oneVal = gnc_numeric_create(1,1);
    xaccSplitSetSharePrice(ret, oneVal);

    xaccSplitSetSlots_nc(ret, get_random_kvp_frame());

    return ret;
}

void
make_random_changes_to_split (Split *split)
{
  Transaction *trans;

  g_return_if_fail (split);

  trans = xaccSplitGetParent (split);

  xaccTransBeginEdit (trans);

  set_split_random_string (split, xaccSplitSetMemo);
  set_split_random_string (split, xaccSplitSetAction);

  xaccSplitSetReconcile (split, possible_chars[get_random_int_in_range(0, 3)]);

  xaccSplitSetDateReconciledTS (split, get_random_timespec());

  xaccSplitSetSlots_nc (split, get_random_kvp_frame());

  /* Don't change share values/prices here, since that would
   * throw transactions out of balance. Do that in the corresponding
   * change transaction function. */

  xaccTransCommitEdit (trans);
}

static void
set_tran_random_string(Transaction* trn,
                       void(*func)(Transaction *act, const gchar*str))
{
    gchar *tmp_str = get_random_string();
    if(tmp_str)
    {
        (func)(trn, tmp_str);
        g_free(tmp_str);
    }
}

static void
add_random_splits(GNCBook *book, Transaction *trn)
{
    gnc_numeric num = get_random_gnc_numeric();

    xaccTransAppendSplit(trn, get_random_split(book, num));
    xaccTransAppendSplit(trn, get_random_split(book, gnc_numeric_neg(num)));
}

static void
trn_add_ran_timespec(Transaction *trn, void (*func)(Transaction*,
                                                    const Timespec*))
{
    Timespec *to_set;

    to_set = get_random_timespec();
    func(trn, to_set);
    g_free(to_set);
}

    
Transaction *
get_random_transaction_with_currency(GNCBook *book,
                                     gnc_commodity *currency)
{
    Transaction* ret;

    ret = xaccMallocTransaction(book);

    xaccTransBeginEdit(ret);

    xaccTransSetCurrency (ret,
                          currency ? currency :
                          get_random_commodity (book));

    set_tran_random_string(ret, xaccTransSetNum);

    trn_add_ran_timespec(ret, xaccTransSetDatePostedTS);
    trn_add_ran_timespec(ret, xaccTransSetDateEnteredTS);

    set_tran_random_string(ret, xaccTransSetDescription);

    xaccTransSetSlots_nc(ret, get_random_kvp_frame());

    add_random_splits(book, ret);

    if (get_random_int_in_range (1, 10) == 1)
    {
      char *reason = get_random_string ();
      xaccTransVoid (ret, reason);
      g_free (reason);
    }

    xaccTransCommitEdit(ret);

    return ret;
}

Transaction*
get_random_transaction (GNCBook *book)
{
  return get_random_transaction_with_currency (book, NULL);
}

void
make_random_changes_to_transaction (GNCBook *book, Transaction *trans)
{
  g_return_if_fail (trans && book);

  if (xaccTransGetVoidStatus (trans))
    return;

  xaccTransBeginEdit (trans);

  xaccTransSetCurrency (trans, get_random_commodity (book));

  set_tran_random_string (trans, xaccTransSetNum);

  trn_add_ran_timespec (trans, xaccTransSetDatePostedTS);
  trn_add_ran_timespec (trans, xaccTransSetDateEnteredTS);

  set_tran_random_string (trans, xaccTransSetDescription);

  xaccTransSetSlots_nc (trans, get_random_kvp_frame());

  /* Do split manipulations in higher-level functions */

  xaccTransCommitEdit (trans);
}

static gpointer
get_random_list_element (GList *list)
{
  g_return_val_if_fail (list, NULL);

  return g_list_nth_data (list,
                          get_random_int_in_range (0,
                                                   g_list_length (list) - 1));
}

static gnc_commodity *
get_random_commodity_from_table (gnc_commodity_table *table)
{
  GList *namespaces;
  gnc_commodity *com = NULL;

  g_return_val_if_fail (table, NULL);

  namespaces = gnc_commodity_table_get_namespaces (table);

  do
  {
    GList *commodities;
    char *namespace;

    namespace = get_random_list_element (namespaces);

    commodities = gnc_commodity_table_get_commodities (table, namespace);
    if (!commodities)
      continue;

    com = get_random_list_element (commodities);

    g_list_free (commodities);

  } while (!com);


  g_list_free (namespaces);

  return com;
}

gnc_commodity*
get_random_commodity (GNCBook *book)
{
    gnc_commodity *ret;
    gchar *name;
    const gchar *space;
    gchar *mn;
    gchar *xcode;
    int ran_int;
    gnc_commodity_table *table;

    table = gnc_book_get_commodity_table (book);

#if 0
    if (table &&
        (gnc_commodity_table_get_size (table) > 0) &&
        get_random_int_in_range (1, 5) < 5)
      return get_random_commodity_from_table (table);
#endif

    mn = get_random_string();
    space = get_random_commodity_namespace();

    if (table)
    {
      ret = gnc_commodity_table_lookup (table, space, mn);

      if (ret)
      {
        g_free (mn);
        return ret;
      }
    }

    name = get_random_string();
    xcode = get_random_string();
    ran_int = get_random_int_in_range(1, 100000);

    ret = gnc_commodity_new (name, space, mn, xcode, ran_int);

    g_free(mn);
    g_free(name);
    g_free(xcode);

    if (table)
      ret = gnc_commodity_table_insert (table, ret);

    return ret;
}

void
make_random_changes_to_commodity (gnc_commodity *com)
{
  char *str;

  g_return_if_fail (com);

  str = get_random_string ();
  gnc_commodity_set_namespace (com, str);
  g_free (str);

  str = get_random_string ();
  gnc_commodity_set_mnemonic (com, str);
  g_free (str);

  str = get_random_string ();
  gnc_commodity_set_fullname (com, str);
  g_free (str);

  str = get_random_string ();
  gnc_commodity_set_exchange_code (com, str);
  g_free (str);

  gnc_commodity_set_fraction (com, get_random_int_in_range (1, 100000));
}

void
make_random_changes_to_commodity_table (gnc_commodity_table *table)
{
  GList *namespaces;
  GList *node;

  g_return_if_fail (table);

  namespaces = gnc_commodity_table_get_namespaces (table);

  for (node = namespaces; node; node = node->next)
  {
    const char *ns = node->data;
    GList *commodities;
    GList *com_node;

    if (strcmp (ns, GNC_COMMODITY_NS_ISO) == 0)
      continue;

    commodities = gnc_commodity_table_get_commodities (table, ns);

    for (com_node = commodities; com_node; com_node = com_node->next)
    {
      gnc_commodity *com = com_node->data;

      gnc_commodity_table_remove (table, com);
      make_random_changes_to_commodity (com);
      gnc_commodity_table_insert (table, com);
    }

    g_list_free (commodities);
  }

  g_list_free (namespaces);
}

static GList *
get_random_guids(int max)
{
  GList *guids = NULL;
  int num_guids;

  if (max < 1) return NULL;

  num_guids = get_random_int_in_range (1, max);

  while (num_guids-- > 0)
    g_list_prepend (guids, get_random_guid ());

  return guids;
}

static void
free_random_guids(GList *guids)
{
  GList *node;

  for (node = guids; node; node = node->next)
    g_free (node->data);

  g_list_free (guids);
}

static QueryOp
get_random_queryop(void)
{
  return get_random_int_in_range (1, QUERY_XOR);
}

static GSList *
get_random_kvp_path (void)
{
  GSList *path;
  gint len;

  path = NULL;
  len = get_random_int_in_range (1, kvp_max_depth);

  while (len--)
    path = g_slist_prepend (path, get_random_string ());

  return g_slist_reverse (path);
}

static void
free_random_kvp_path (GSList *path)
{
  GSList *node;

  for (node = path; node; node = node->next)
    g_free (node->data);

  g_slist_free (path);
}

static GNCIdType
get_random_id_type (void)
{
  switch (get_random_int_in_range (1,3)) {
  case 1:
    return GNC_ID_SPLIT;
  case 2:
    return GNC_ID_TRANS;
  case 3:
    return GNC_ID_ACCOUNT;
  default:
    return get_random_string ();
  }
}

typedef enum {
  BY_STANDARD = 1,
  BY_DATE,
  BY_DATE_ENTERED,
  BY_DATE_RECONCILED,
  BY_NUM,
  BY_AMOUNT,
  BY_MEMO,
  BY_DESC,
  BY_NONE
} sort_type_t;

static void
set_query_sort (Query *q, sort_type_t sort_code)
{
  GSList *p1 = NULL, *p2 = NULL, *p3 = NULL, *standard;

  standard = g_slist_prepend (NULL, QUERY_DEFAULT_SORT);

  switch (sort_code)
  {
    case BY_STANDARD:
      p1 = standard;
      break;
    case BY_DATE:
      p1 = g_slist_prepend (p1, TRANS_DATE_POSTED);
      p1 = g_slist_prepend (p1, SPLIT_TRANS);
      p2 = standard;
      break;
    case BY_DATE_ENTERED:
      p1 = g_slist_prepend (p1, TRANS_DATE_ENTERED);
      p1 = g_slist_prepend (p1, SPLIT_TRANS);
      p2 = standard;
      break;
    case BY_DATE_RECONCILED:
      p1 = g_slist_prepend (p1, SPLIT_RECONCILE);
      p2 = g_slist_prepend (p2, SPLIT_DATE_RECONCILED);
      p3 = standard;
      break;
    case BY_NUM:
      p1 = g_slist_prepend (p1, TRANS_NUM);
      p1 = g_slist_prepend (p1, SPLIT_TRANS);
      p2 = standard;
      break;
    case BY_AMOUNT:
      p1 = g_slist_prepend (p1, SPLIT_VALUE);
      p2 = standard;
      break;
    case BY_MEMO:
      p1 = g_slist_prepend (p1, SPLIT_MEMO);
      p2 = standard;
      break;
    case BY_DESC:
      p1 = g_slist_prepend (p1, TRANS_DESCRIPTION);
      p1 = g_slist_prepend (p1, SPLIT_TRANS);
      p2 = standard;
      break;
    case BY_NONE:
      g_slist_free (standard);
      break;
    default:
      g_slist_free (standard);
      g_return_if_fail (FALSE);
  }

  gncQuerySetSortOrder (q, p1, p2, p3);
}

Query *
get_random_query(void)
{
  Query *q;
  int num_terms;

  num_terms = get_random_int_in_range (1, 3);

  q = xaccMallocQuery ();

  while (num_terms-- > 0)
  {
    gint pr_type;
    kvp_value *value;
    Timespec *start;
    Timespec *end;
    GList *guids;
    GSList *path;
    char *string;
    GUID *guid;

    pr_type = get_random_int_in_range (1, 20);

    switch (pr_type)
    {
      case 1: /*PR_ACCOUNT */
        guids = get_random_guids (10);
        xaccQueryAddAccountGUIDMatch
          (q,
           guids,
           get_random_int_in_range (1, GUID_MATCH_NONE),
           get_random_queryop ());
        free_random_guids (guids);
        break;

      case 2: /*PR_ACTION */
        string = get_random_string ();
        xaccQueryAddActionMatch (q,
                                 string,
                                 get_random_boolean (),
                                 get_random_boolean (),
                                 get_random_queryop ());
        g_free (string);
        break;

      case 3: /* PR_BALANCE */
        xaccQueryAddBalanceMatch
          (q,
           get_random_boolean (),
           get_random_queryop ());
        break;

      case 4: /* PR_CLEARED */
        xaccQueryAddClearedMatch
          (q,
           get_random_int_in_range (1,
                                    CLEARED_NO |
                                    CLEARED_CLEARED |
                                    CLEARED_RECONCILED |
                                    CLEARED_FROZEN |
                                    CLEARED_VOIDED),
           get_random_queryop ());
        break;

      case 5: /* PR_DATE */
        start = get_random_timespec ();
        end = get_random_timespec ();
        xaccQueryAddDateMatchTS (q, 
                                 get_random_boolean (),
                                 *start,
                                 get_random_boolean (),
                                 *end,
                                 get_random_queryop ());
        g_free (start);
        g_free (end);
        break;

      case 6: /* PR_DESC */
        string = get_random_string ();
        xaccQueryAddDescriptionMatch (q,
                                      string,
                                      get_random_boolean (),
                                      get_random_boolean (),
                                      get_random_queryop ());
        g_free (string);
        break;

      case 7: /* PR_GUID */
        guid = get_random_guid ();
        xaccQueryAddGUIDMatch (q,
                               guid,
                               get_random_id_type (),
                               get_random_queryop ());
        g_free (guid);
        break;

      case 8: /* PR_KVP */
        path = get_random_kvp_path ();
        do
        {
          value = get_random_kvp_value_depth (-2, kvp_max_depth);
        } while (!value);
        xaccQueryAddKVPMatch (q,
                              path,
                              value,
                              get_random_int_in_range (1, COMPARE_NEQ),
                              get_random_id_type (),
                              get_random_queryop ());
        kvp_value_delete (value);
        free_random_kvp_path (path);
        break;

      case 9: /* PR_MEMO */
        string = get_random_string ();
        xaccQueryAddMemoMatch (q,
                               string,
                               get_random_boolean (),
                               get_random_boolean (),
                               get_random_queryop ());
        g_free (string);
        break;

      case 10: /* PR_NUM */
        string = get_random_string ();
        xaccQueryAddNumberMatch (q,
                               string,
                               get_random_boolean (),
                               get_random_boolean (),
                               get_random_queryop ());
        g_free (string);
        break;

      case 11: /*  PR_PRICE */
        xaccQueryAddSharePriceMatch
          (q,
           get_random_gnc_numeric (), 
           get_random_int_in_range (1, COMPARE_NEQ),
           get_random_queryop ());
        break;

      case 12: /* PR_SHRS */
        xaccQueryAddSharesMatch
          (q,
           get_random_gnc_numeric (), 
           get_random_int_in_range (1, COMPARE_NEQ),
           get_random_queryop ());
        break;

      case 13: /* PR_VALUE */
        xaccQueryAddValueMatch
          (q,
           get_random_gnc_numeric (),
           get_random_int_in_range (1, NUMERIC_MATCH_ANY),
           get_random_int_in_range (1, COMPARE_NEQ),
           get_random_queryop ());
        break;

      default:
        num_terms++;
        break;
    }
  }

  set_query_sort (q, get_random_int_in_range (1, BY_NONE));

  xaccQuerySetSortIncreasing (q,
                              get_random_boolean (),
                              get_random_boolean (),
                              get_random_boolean ());

  xaccQuerySetMaxSplits (q, get_random_int_in_range (-50000, 50000));

  return q;
}

GNCBook *
get_random_book (void)
{
  GNCBook *book;

  book = gnc_book_new ();

  make_random_group (book, gnc_book_get_group (book));
  make_random_pricedb (book, gnc_book_get_pricedb (book));

  return book;
}

GNCSession *
get_random_session (void)
{
  GNCSession *session;
  GNCBook *book;

  session = gnc_session_new ();

  book = gnc_session_get_book (session);

  make_random_group (book, gnc_book_get_group (book));
  make_random_pricedb (book, gnc_book_get_pricedb (book));

  return session;
}

void
add_random_transactions_to_book (GNCBook *book, gint num_transactions)
{
  gnc_commodity_table *table;
  GList *accounts;
  gint num_accounts;

  if (num_transactions <= 0) return;

  g_return_if_fail (book);

  accounts = xaccGroupGetSubAccounts (gnc_book_get_group (book));

  g_return_if_fail (accounts);

  num_accounts = g_list_length (accounts);

  table = gnc_book_get_commodity_table (book);

  while (num_transactions--)
  {
    gnc_commodity *com;
    Transaction *trans;
    Account *account;
    Split *split;

    com = get_random_commodity_from_table (table);

    trans = get_random_transaction_with_currency (book, com);

    xaccTransBeginEdit (trans);

    split = xaccTransGetSplit (trans, 0);
    account = get_random_list_element (accounts);
    xaccAccountInsertSplit (account, split);

    split = xaccTransGetSplit (trans, 1);
    account = get_random_list_element (accounts);
    xaccAccountInsertSplit (account, split);

    xaccTransCommitEdit (trans);
  }

  g_list_free (accounts);
}

void
make_random_changes_to_book (GNCBook *book)
{
  g_return_if_fail (book);

  make_random_changes_to_group (book, gnc_book_get_group (book));
  make_random_changes_to_pricedb (book, gnc_book_get_pricedb (book));

#if 0
  make_random_changes_to_commodity_table (gnc_book_get_commodity_table (book));
#endif
}

void
make_random_changes_to_session (GNCSession *session)
{
  g_return_if_fail (session);

  make_random_changes_to_book (gnc_session_get_book (session));
}

typedef struct
{
  GNCIdType where;
  GSList *path;
  Query *q;
} KVPQueryData;

static void
add_kvp_value_query (const char *key, kvp_value *value, gpointer data)
{
  KVPQueryData *kqd = data;
  GSList *node;

  kqd->path = g_slist_append (kqd->path, (gpointer) key);

  if (kvp_value_get_type (value) == KVP_TYPE_FRAME)
    kvp_frame_for_each_slot (kvp_value_get_frame (value),
                             add_kvp_value_query, data);
  else
    xaccQueryAddKVPMatch (kqd->q, kqd->path, value,
                          COMPARE_EQUAL, kqd->where,
                          QUERY_AND);

  node = g_slist_last (kqd->path);
  kqd->path = g_slist_remove_link (kqd->path, node);
  g_slist_free_1 (node);
}

static void
add_kvp_query (Query *q, kvp_frame *frame, GNCIdType where)
{
  KVPQueryData kqd;

  kqd.where = where;
  kqd.path = NULL;
  kqd.q = q;

  kvp_frame_for_each_slot (frame, add_kvp_value_query, &kqd);
}

static gboolean include_price = TRUE;

void
trans_query_include_price (gboolean include_price_in)
{
  include_price = include_price_in;
}

TestQueryTypes
get_random_query_type (void)
{
  switch (get_random_int_in_range (0, 4))
  {
    case 0: return SIMPLE_QT;
    case 1: return SPLIT_KVP_QT;
    case 2: return TRANS_KVP_QT;
    case 3: return ACCOUNT_KVP_QT;
    case 4: return GUID_QT;
    default: return SIMPLE_QT;
  }
}

Query *
make_trans_query (Transaction *trans, TestQueryTypes query_types)
{
  Account *a;
  gnc_numeric n;
  Query *q;
  Split *s;

  if (query_types == RANDOM_QT)
    query_types = get_random_query_type ();

  q = xaccMallocQuery ();

  s = xaccTransGetSplit (trans, 0);
  a = xaccSplitGetAccount (s);

  if (query_types & SIMPLE_QT)
  {
    xaccQueryAddSingleAccountMatch (q, xaccSplitGetAccount (s), QUERY_AND);

    xaccQueryAddDescriptionMatch (q, xaccTransGetDescription (trans),
                                  TRUE, FALSE, QUERY_AND);

    xaccQueryAddNumberMatch (q, xaccTransGetNum (trans),
                             TRUE, FALSE, QUERY_AND);

    xaccQueryAddActionMatch (q, xaccSplitGetAction (s),
                             TRUE, FALSE, QUERY_AND);

    n = xaccSplitGetValue (s);
    xaccQueryAddValueMatch (q, n, NUMERIC_MATCH_ANY,
                              COMPARE_EQUAL, QUERY_AND);

    n = xaccSplitGetAmount (s);
    xaccQueryAddSharesMatch (q, n, COMPARE_EQUAL, QUERY_AND);

    if (include_price)
    {
      n = xaccSplitGetSharePrice (s);
      xaccQueryAddSharePriceMatch (q, n, COMPARE_EQUAL, QUERY_AND);
    }

    {
      Timespec ts;

      xaccTransGetDatePostedTS (trans, &ts);
      xaccQueryAddDateMatchTS (q, TRUE, ts, TRUE, ts, QUERY_AND);
    }

    xaccQueryAddMemoMatch (q, xaccSplitGetMemo (s), TRUE, FALSE, QUERY_AND);

    {
      cleared_match_t how;

      switch (xaccSplitGetReconcile (s))
      {
        case NREC:
          how = CLEARED_NO;
          break;
        case CREC:
          how = CLEARED_CLEARED;
          break;
        case YREC:
          how = CLEARED_RECONCILED;
          break;
        case FREC:
          how = CLEARED_FROZEN;
          break;
        case VREC:
          how = CLEARED_VOIDED;
          break;
        default:
          failure ("bad reconcile flag");
          xaccFreeQuery (q);
          return NULL;
      }

      xaccQueryAddClearedMatch (q, how, QUERY_AND);
    }
  }

  if (query_types & ACCOUNT_QT)
  {
    GList * list;
    GList * node;

    /* GUID_MATCH_ALL */
    list = NULL;
    for (node = xaccTransGetSplitList (trans); node; node = node->next)
    {
      Split * split = node->data;
      list = g_list_prepend (list, xaccSplitGetAccount (split));
    }
    xaccQueryAddAccountMatch (q, list, GUID_MATCH_ALL, QUERY_AND);
    g_list_free (list);

    /* GUID_MATCH_NONE */
    list = NULL;
    list = g_list_prepend (list, get_random_guid ());
    list = g_list_prepend (list, get_random_guid ());
    list = g_list_prepend (list, get_random_guid ());
    xaccQueryAddAccountGUIDMatch (q, list, GUID_MATCH_NONE, QUERY_AND);

    /* GUID_MATCH_ANY */
    {
      GUID * guid = get_random_guid ();
      *guid = *xaccAccountGetGUID (a);
      list = g_list_prepend (list, guid);
    }
    xaccQueryAddAccountGUIDMatch (q, list, GUID_MATCH_ANY, QUERY_AND);

    for (node = list; node; node = node->next)
      g_free (node->data);
    g_list_free (list);
  }

  if (query_types & GUID_QT)
  {
    xaccQueryAddGUIDMatch (q, xaccSplitGetGUID (s),
                           GNC_ID_SPLIT, QUERY_AND);

    xaccQueryAddGUIDMatch (q, xaccTransGetGUID (trans),
                           GNC_ID_TRANS, QUERY_AND);

    xaccQueryAddGUIDMatch (q, xaccAccountGetGUID (a),
                           GNC_ID_ACCOUNT, QUERY_AND);
  }

  if (query_types & SPLIT_KVP_QT)
    add_kvp_query (q, xaccSplitGetSlots (s), GNC_ID_SPLIT);

  if (query_types & TRANS_KVP_QT)
    add_kvp_query (q, xaccTransGetSlots (trans), GNC_ID_TRANS);

  if (query_types & ACCOUNT_KVP_QT)
    add_kvp_query (q, xaccAccountGetSlots (a), GNC_ID_ACCOUNT);

  return q;
}
