/* -*- Mode: C; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ 
/* vim: set ts=2 et sw=2 tw=80: */
/*
** Copyright (c) 2013 D. Richard Hipp
**
** This program is free software; you can redistribute it and/or
** modify it under the terms of the Simplified BSD License (also
** known as the "2-Clause License" or "FreeBSD License".)
**
** This program 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.
**
** Author contact information:
**   drh@hwaci.com
**   http://www.hwaci.com/drh/
**
*******************************************************************************
** This file holds some basic sanity test code.
*/

#include "fossil-scm/fossil-cli.h" /* Fossil App mini-framework */
#include "fossil-scm/fossil-internal.h"
#include <string.h>
#include <time.h>

static void fcli_local_help(){
  printf("Usage:\n\t%s \n\n", fcli.appName);

  puts("Runs some basic sanity tests. Requires a checkout.");

}

static int test_sanity_0(){
  int rc = 0;
  /*
    Testing a bug/fix:

    Build on a 32-bit environment (untested on 64-bit)
    Enable 'long long'
    typedef fsl_int32_t fsl_id_t
    #define FSL_ID_T_PFMT FSL_INT32_T_PFMT

    And then...
  */
  char buf[80];
  int i = 0;
  f_out("Checking for (not asserting) sizeof()/va_list "
        "weirdness... (use -V for more info)\n");
  f_out("On 64-bit platforms these examples probably all look"
        "right (output=='1 2 3'), "
        "but mixing in a 64-bit type on a 32-bit platform can lead "
        "to weirdness (bugs) in conjunction with va_list arguments.\n");
  f_out("#%d: %"FSL_ID_T_PFMT" %"FSL_ID_T_PFMT" %"FSL_ID_T_PFMT"\n",
        ++i, 1, 2, 3);
  f_out("#%d: %"FSL_SIZE_T_PFMT" %"FSL_ID_T_PFMT" %"FSL_SIZE_T_PFMT"\n",
        ++i, (fsl_size_t)1, (fsl_id_t)2, (fsl_size_t)3);
  VERBOSE(("This next one (might) fail badly because the numeric arguments "
           "are passed on as integers and the va_arg() handling extracts "
           "a larger type, effectively skipping over arguments and potentially "
           "overrunning memory (i.e. corruption):\n"));
  f_out("#%d: %"FSL_SIZE_T_PFMT" %"FSL_ID_T_PFMT" %"FSL_SIZE_T_PFMT"\n",
        ++i, 1, 2, 3);
#if 0
  /* Interesting... we get warnings for printf()...

     Unfortunately, we can't recycle gcc's printf warnings here
     (via __attribute__), because it does not like our custom
     string formatting options.
   */
  puts("printf(3) says:");
  printf("#%d: %"FSL_SIZE_T_PFMT" %"FSL_ID_T_PFMT" %"FSL_SIZE_T_PFMT"\n",
         ++i, 1, 2, 3);
#endif
  VERBOSE(("The trigger is the 'll' format specifier in conjunction with "
           "integer arguments which are not strongly typed and don't match "
           "their format specifier's expected sizeof() exactly. They are "
           "passed on as sizeof==4 and extracted as sizeof==8.\n"));
  VERBOSE(("It can be worked around by explicity casting the arguments "
           "to fsl_size_t resp. fsl_id_t, and this is sometimes needed "
           "EVEN IF the arguments are strongly typed (not sure why!).\n"));
  {
    fsl_size_t sz1 = 1, sz3 = 3;
    f_out("#%d: %"FSL_SIZE_T_PFMT" %"FSL_ID_T_PFMT" %"FSL_SIZE_T_PFMT"\n",
          ++i, sz1, 2, sz3);
  }

  /*
    Problem has something to do with the sizeof(int) resp. sizeof(fsl_id_t)
    and the va_arg() handling of the etRADIX bits in fsl_appendf().

    The fix? Find the right combination of formatting string for integers
    in that combination. %d is apparently not correct.
    
    And it only appears to happen when we cross multiple levels of va_list
    handling? Namely, it happens in fsl_snprintf() but not fsl_output():
  */
  fsl_snprintf(buf, sizeof(buf),
               "#%d: ?? %"FSL_SIZE_T_PFMT" %"FSL_ID_T_PFMT" %"FSL_SIZE_T_PFMT" ??",
               (int)++i, (fsl_size_t)1, (fsl_id_t)2, (fsl_size_t)3);
  f_out("#%d fsl_snprintf(cast) says: %s\n", i, buf);
  fsl_snprintf(buf, sizeof(buf),
               "#%d: ?? %"FSL_SIZE_T_PFMT" %"FSL_ID_T_PFMT" %"FSL_SIZE_T_PFMT" ??",
               ++i, 1, 2, 3);
  f_out("#%d fsl_snprintf() says: %s\n", i, buf);
  VERBOSE(("That one is (or was, at one point) failing differently in tcc/gcc "
           "in some build combinations.\n"));
  ++i;
  f_out("#%d: f_out(cast)   says: %"FSL_SIZE_T_PFMT" %"FSL_ID_T_PFMT" %"FSL_SIZE_T_PFMT" ??\n",
        (int)++i, (fsl_size_t)1, (fsl_id_t)2, (fsl_size_t)3);
  f_out("#%d: f_out()       says: ?? %"FSL_SIZE_T_PFMT" %"FSL_ID_T_PFMT" %"FSL_SIZE_T_PFMT" ??\n",
        ++i, 1, 2, 3);
  /** Those fsl_snprintf() and f_out() calls SHOULD fail in the same
      way but they're not on my 32-bit box (in gcc - they do in
      tcc!). They go through the same code, differing only by how many
      times the va_list is passed on, thus my suspicion regarding the
      number of va_list indirections having something to do with this.
  */
  return rc;
}

static int test_sanity_repo(){
  fsl_cx * f = fcli.f;
  int rc = 0;
  fsl_buffer buf = fsl_buffer_empty;
  fsl_deck d = fsl_deck_empty;
  fsl_id_t rid;
  fsl_uuid_str uuid = NULL;
  char * str;
  fsl_db * db = fsl_cx_db_repo(f);
  fsl_size_t slen, slen2;
  f_out("Running basic uuid/rid tests against this repo...\n");
  assert( 1==fsl_uuid_to_rid(f, "99237c3636730f20ed07b227c5092c087aea8b0c") );
  assert( 1==fsl_uuid_to_rid(f, "99237c363673"));
  assert( 0>fsl_uuid_to_rid(f, "992") );
  assert(f->error.code == FSL_RC_AMBIGUOUS);
  assert( strstr(fsl_buffer_cstr(&f->error.msg), "ambig") );
  fcli_err_reset();

  rc = fsl_sym_to_rid(f, "prev", FSL_ATYPE_CHECKIN, &rid);
  assert(!rc);
  assert(rid>0);
  rc = fsl_content_get(f, rid, &buf);
  assert(!rc);
  /* assert('B' == *fsl_buffer_cstr(&buf)); */
  d.f = f;
  rc = fsl_deck_parse(&d, &buf);
  assert(0==rc);
  fsl_buffer_clear(&buf);
  assert(FSL_CATYPE_MANIFEST==d.type);
  assert(rid==d.rid);
  uuid = fsl_rid_to_uuid(f, d.rid);
  assert(uuid);
  assert(0==fsl_uuidcmp(d.uuid, uuid));
  fsl_free(uuid);
  assert(d.D>0 && d.D<fsl_julian_now());
  fsl_deck_finalize(&d);
  assert(0==rc);

  slen = 0;
  str = fsl_db_g_text(db, &slen, "SELECT FSL_USER()");
  assert(str && "fsl_user() SQL func is broken");
  /* f_out("SELECT fsl_user()=%s\n", str); */
  assert(0==fsl_strcmp(str, fsl_cx_user_get(f)));
  fsl_free(str);

  slen = 0;
  str = fsl_db_g_text(db, &slen, "select fsl_content('rid:1')");
  assert(str && slen && "fsl_content() SQL func is broken");
  fsl_free(str);
  assert(168 == slen) /* side of rid 1 in libfossil */;
  /* f_out("SELECT fsl_content() got %"FSL_SIZE_T_PFMT" bytes\n", (fsl_size_t)slen); */

  slen2 = 0;
  str = fsl_db_g_text(db, &slen2, "select fsl_content(1)");
  assert(str && slen2 && "fsl_content(sym) SQL func is broken");
  assert(slen2==slen);
  fsl_free(str);
  /* f_out("SELECT fsl_content(rid) got %"FSL_SIZE_T_PFMT" bytes\n", (fsl_size_t)slen2); */

  rid = fsl_db_g_id(db, -1, "select fsl_sym2rid('root:7bfbc3dba6c65')");
  assert(1==rid);
  /* f_out("SELECT fsl_sym2rid(...) got %"FSL_ID_T_PFMT"\n", (fsl_id_t)rid); */

  rid = fsl_db_g_id(db, -666, "select fsl_sym2rid('abcdefabcdef')");
  assert(-666==rid);
  assert(db->error.code);
  assert(db->error.msg.used);
  f_out("Caught expected SQL-triggered error: %b\n", &db->error.msg);
#if 0
  fcli_err_reset() /* b/c fsl_sym2rid() (indirectly) sets error at the fsl_cx context level */;
#endif
  return 0;
}

static int test_sanity_delta(){
  fsl_buffer f1 = fsl_buffer_empty,
    f2 = fsl_buffer_empty,
    d12 = fsl_buffer_empty,
    d21 = fsl_buffer_empty,
    a1 = fsl_buffer_empty,
    a2 = fsl_buffer_empty;
  int rc;
  char const * F1 = __FILE__;
  char const * F2 = "f-sizeof.c";
  fsl_size_t len1 = 0, len2 = 0;
  f_out("Running core delta tests...\n");
  rc = fsl_buffer_fill_from_filename(&f1, F1);
  assert(!rc);
  rc = fsl_buffer_fill_from_filename(&f2, F2);
  assert(!rc);
  f_out("Input file sizes: f1: %"FSL_SIZE_T_PFMT", f2: %"FSL_SIZE_T_PFMT"\n",
        f1.used, f2.used);

  rc = fsl_buffer_delta_create(&f1, &f2, &d12);
  assert(!rc);
  rc = fsl_buffer_delta_create(&f2, &f1, &d21);
  assert(!rc);

  f_out("Delta sizes: f1=>f2: %"FSL_SIZE_T_PFMT", f2=>f1: %"FSL_SIZE_T_PFMT"\n",
        d12.used, d21.used);
  /* f_out("%b\n", &d12); */

  rc = fsl_delta_applied_size(d12.mem, d12.used, &len1);
  assert(!rc);
  assert(len1==f2.used);
  rc = fsl_delta_applied_size(d21.mem, d21.used, &len2);
  assert(!rc);
  assert(len2==f1.used);

  rc = fsl_buffer_delta_apply(&f1, &d12, &a2);
  assert(!rc);
  rc = fsl_buffer_delta_apply(&f2, &d21, &a1);
  assert(!rc);
  if( fsl_buffer_compare(&f1,&a1) || fsl_buffer_compare(&f2, &a2) ){
    fsl_fatal(FSL_RC_CONSISTENCY, "delta test failed");
  }
  fsl_buffer_clear(&f1);
  fsl_buffer_clear(&f2);
  fsl_buffer_clear(&a1);
  fsl_buffer_clear(&a2);
  fsl_buffer_clear(&d12);
  fsl_buffer_clear(&d21);
  return rc;
}

static void test_sanity_tkt_01(){
  fsl_buffer sql = fsl_buffer_empty;
  fsl_cx * f = fcli.f;
  char const * orig = fsl_schema_ticket();
  int rc = fsl_cx_schema_ticket(f, &sql);
  assert(!rc);
  assert(sql.used>200);
  if(0) f_out("Ticket schema size=%"FSL_SIZE_T_PFMT
              ", default schema size=%"FSL_SIZE_T_PFMT"\n",
              sql.used, fsl_strlen(orig));
  fsl_buffer_clear(&sql);
}

static void test_sanity_tkt_fields(){
  fsl_cx * f = fcli.f;
  int rc;
  f_out("Loading custom ticket fields...\n");
  rc = fsl_cx_ticket_load_fields(f, 0);
  assert(!rc);
  f_out("Ticket field count=%"FSL_SIZE_T_PFMT": ",
        f->ticket.customFields.used);
  for(rc = 0; rc < (int)f->ticket.customFields.used; ++rc){
    fsl_card_J const * jc = f->ticket.customFields.list[rc];
    f_out( "%s%s", rc ? ", " : "", jc->field);
  }
  f_out("\n");
}


static void test_fs_mkdir(){
  int rc;
  char const * path = "_sanity/foo/bar/baz/";
  fsl_buffer b = fsl_buffer_empty;
  fsl_fstat fst = fsl_fstat_empty;
  f_out("fsl_mkdir2(%s,...)...\n", path);
  rc = fsl_mkdir2(path, 1);
  assert(0==rc);
  rc = fsl_stat( path, &fst, 1);
  assert(0==rc);
  assert(FSL_FSTAT_TYPE_DIR==fst.type);
  fsl_buffer_appendf(&b, "%//", __FILE__);
  rc = fsl_mkdir2(fsl_buffer_cstr(&b), 0);
  assert(FSL_RC_TYPE==rc);
  path = "f-sanity.c";
  assert(fsl_is_file(path));
  rc = fsl_mkdir2(path, 0);
  assert(0==rc) /* b/c no path component, nothing to do */;
  b.used = 0;
  fsl_buffer_appendf(&b, "%s/", path);
  rc = fsl_mkdir2(fsl_buffer_cstr(&b), 0);
  assert(FSL_RC_TYPE==rc);
  fsl_buffer_clear(&b);
}

static void test_fs_cx_stat(){
  fsl_cx * f = fcli.f;
  int rc;
  fsl_fstat fst = fsl_fstat_empty;
  fsl_int64_t time1, time2;
  char const * fname = "src/fsl_cli.c";
  fsl_time_t const now = time(0);
  f_out("fsl_cx_stat()...\n");
  rc = fsl_cx_stat( f, "no-such-file", &fst );
  assert(FSL_RC_NOT_FOUND==rc);
  rc = fsl_cx_stat( f, fname, &fst );
  assert(0==rc);
  assert(FSL_FSTAT_TYPE_FILE==fst.type);
  assert(fst.ctime>0 && fst.ctime<now);
  assert(fst.mtime>0 && fst.mtime<now);
  assert(fst.size>0);


  fname = 0 ? "test" : fcli.appName;
  rc = fsl_cx_stat(f, fname, &fst);
  assert(0==rc);
  time1 = fst.mtime;
  rc = fsl_file_set_mtime(fname, now);
  assert(0==rc);
  f_out("old mtime=%"FSL_INT64_T_PFMT"\n", time1);
  rc = fsl_cx_stat(f, fname, &fst);
  assert(0==rc);
  time2 = fst.mtime;
  f_out("new mtime=%"FSL_INT64_T_PFMT"\n", time2);
  assert(time2 > time1);

}

static void test_sanity_fs(){
  char * s = NULL;
  fsl_buffer b = fsl_buffer_empty;
  int rc;
  fsl_fstat fst = fsl_fstat_empty;
  rc = fsl_find_home_dir(&b, 1);
  f_out("Home dir: %b\n", &b);
  assert(0==rc);
  b.used = 0;
  rc = fsl_stat(__FILE__, &fst, 1);
  assert(0==rc);
  assert(FSL_FSTAT_TYPE_FILE == fst.type);
  assert(fst.mtime>0);
  assert(fst.mtime<time(0));

  fsl_free(s);
  fsl_buffer_clear(&b);

  test_fs_mkdir();
  test_fs_cx_stat();
}

static void test_sanity_localtime(){
  fsl_int64_t now = time(0);
  fsl_cx * f = fcli.f;
  char * tsNow;
  char * ts2;
  fsl_db * db = fsl_cx_db(f);
  tsNow = fsl_db_unix_to_iso8601(db, now, 0);
#if 1
  ts2 = fsl_db_unix_to_iso8601(db, now, 1);
  f_out("now utc: %s\nlocal:   %s\n",
        tsNow, ts2);
#else
  {
    char * ts1;
    fsl_int64_t t1, t2;
    /* still very unsure about this. And it's broken. */
    fsl_cx_flag_set(f, FSL_CX_F_LOCALTIME_GMT, 0 );
    assert(!(f->flags & FSL_CX_F_LOCALTIME_GMT)); 
    t1 = fsl_cx_time_adj(f, now);
    ts1 = fsl_db_unix_to_iso8601(db, t1, 0);

    fsl_cx_flag_set(f, FSL_CX_F_LOCALTIME_GMT, 1 );
    assert(f->flags & FSL_CX_F_LOCALTIME_GMT); 
    t2 = fsl_cx_time_adj(f, now);
    ts2 = fsl_db_unix_to_iso8601(db, t2, 0);

    fsl_cx_flag_set(f, FSL_CX_F_LOCALTIME_GMT, 0 );
    f_out("now=%s\nts1=%s\nts2=%s\n",
          tsNow, ts1, ts2);
    fsl_free(ts1);
  }
#endif
  fsl_free(tsNow);
  fsl_free(ts2);
}

static void test_julian(){
  char const * ts;
  double tolerance = 0
    /* Tolerance, in Julian Days, for round-trip rounding errors. In
       my tests most time conversions are either exact or +/-1ms, but
       the exact range of error is probably at least partially
       platform-dependent. The differences between the reference JD
       values (taken from sqlite3) and the results are consistently
       (0, 0.000000001, and 0.000000011), which is quite close enough
       for what we do with these.
    */
    ? 1
    : 0.000000012;
  double eB;
  double eE;
  char buf[24];
  double j = 0;
  char rc;
  char vbose = fcli.verbose>1;
  struct tester {
    /*
      ISO8601 time string.
    */
    char const * ts;
    /* Expected JD result. This value is normally taken
       from sqlite3's strftime('%J', this->ts).
    */
    double expect;
  } list[] = {
  {"2013-12-13T01:02:03.000", 2456639.543090278},
  {"2013-09-10T23:35:53.471", 2456546.483257755},
  {"2013-09-10T22:35:53.471", 2456546.441591088},
  {"2013-09-10T21:35:53.471", 2456546.399924421},
  {"2013-09-10T20:35:53.471", 2456546.358257755},
  {"2013-09-10T19:35:53.471", 2456546.316591088},
  {"2013-09-10T18:35:53.471", 2456546.274924421},
  {"2013-09-10T17:35:53.471", 2456546.233257755},
  {"2013-09-10T16:35:53.471", 2456546.191591088},
  {"2013-09-10T15:35:53.471", 2456546.149924421},
  {"2013-09-10T14:35:53.471", 2456546.108257755},
  {"2013-09-10T13:14:15.167", 2456546.051564422},
  {"2013-09-10T01:02:03.004", 2456545.543090324},
  {"2013-09-09T12:11:10.987", 2456545.007766053},
  {"2013-09-09T11:10:09.876", 2456544.965392072},
  {"2013-02-02T12:13:14.000", 2456326.009189815},
  {"2013-01-02T12:13:14.000", 2456295.009189815},
  {"2011-12-31T12:51:46.686", 2455927.035957014},
  {"2011-01-01T00:15:54.000", 2455562.511041666},
  {"2008-12-20T02:23:18.000", 2454820.599513888},
  {"2008-01-02T14:53:47.000", 2454468.12068287},
  {"2007-07-21T14:09:59.000", 2454303.090266198},
  {"2013-12-31T23:59:59.997", 2456658.499999954},
  {0,0}
  };
  struct tester * t;
  f_out("iso8601 <==> julian tests...\n");
  for( t = list; t->ts; ++t ){
    ts = t->ts;
    j = 0;
    if(vbose) f_out("\ninput : %s\n", ts);
    rc = fsl_iso8601_to_julian(ts, &j);
    assert(rc);
    if(vbose){
      
      f_out("julian: %"FSL_JULIAN_T_PFMT"\n", j);
      f_out("expect: %"FSL_JULIAN_T_PFMT"\n", t->expect);
    }
    buf[0] = 0;
    rc = fsl_julian_to_iso8601(j, buf, 1);
    assert(rc);
    if(vbose) f_out("j=>iso: %s\n", buf);
    if(0 != t->expect){
      eB = t->expect - tolerance;
      eE = t->expect + tolerance;
      assert( (j>=eB && j<=eE) );
    }
    if(0!=fsl_strncmp(ts, buf, 22/*not the final digit*/)){
      f_out("WARNING: round-trip mismatch at >ms level "
            "for:"
            "\n\t%"FSL_JULIAN_T_PFMT"\t%s"
            "\n\t%"FSL_JULIAN_T_PFMT"\t%s\n",
            t->expect, ts, j, buf);
    }else{
      rc = fsl_julian_to_iso8601(j, buf, 0);
      if(vbose) f_out("j=>iso: %s\n", buf);
      assert(rc);
      if(0!=fsl_strncmp(ts, buf, 19)){
        /* This is caused by:
           SS.000 ==> (SS-1).999
           in julian-to-iso8601
        */
        f_out("WARNING: mismatch:\n\t%s\n\t%s\n", ts, buf);
      }
    }
  }
}

static void test_julian2(){
  fsl_cx * f = fcli.f;
  fsl_db * db = fsl_cx_db_repo(f);
  double tolerance = 0
    /* Tolerance, in Julian Days, for round-trip rounding errors. In
       my tests most time conversions are either exact or +/-1ms, but
       the exact range of error is probably at least partially
       platform-dependent. The differences between the reference JD
       values (taken from sqlite3) and the results are consistently
       (0, 0.000000001, and 0.000000011), which is quite close enough
       for what we do with these.
    */
    ? 1
    : 0.000000012;
  double eB;
  double eE;
  char buf[24];
  int rc;
  char vbose = fcli.verbose>1;
  fsl_stmt q = fsl_stmt_empty;
  int counter = 0, warnings = 0, diffBy1Ms = 0;
  f_out("Running all event.mtimes through the julian<==>iso converters...\n");
  rc = fsl_db_prepare(db, &q,
                      "SELECT mtime, strftime('%%Y-%%m-%%dT%%H:%%M:%%f',mtime) "
                      "FROM event ORDER BY mtime DESC "
                      /*"LIMIT 100"*/
                      );
  while( FSL_RC_STEP_ROW == fsl_stmt_step(&q) ){
    char const * ts = fsl_stmt_g_text(&q, 1, NULL);
    double const jexp = fsl_stmt_g_double(&q, 0);
    double j = 0;
    int const oldWarn = warnings;
    ++counter;
    if(vbose) f_out("\ninput : %s\n", ts);
    rc = fsl_iso8601_to_julian(ts, &j);
    assert(rc);
    if(vbose){
      f_out("julian: %"FSL_JULIAN_T_PFMT"\n", j);
      f_out("expect: %"FSL_JULIAN_T_PFMT"\n", jexp);
    }
    assert(j>0.0);
    buf[0] = 0;
    rc = fsl_julian_to_iso8601(j, buf, 1);
    assert(rc);
    if(vbose) f_out("j=>iso: %s\n", buf);
    eB = jexp - tolerance;
    eE = jexp + tolerance;
    assert( (j>=eB && j<=eE) );
    if(0!=fsl_strncmp(ts, buf, 22/*not the final digit!*/)){
      /* See if only the last three digits differ by 1 integer
         point (1ms), and don't warn if that's the case. There's a
         corner case there when N.x99 rolls over, and another for
         N.999, etc., but we'll punt on that problem.
      */
      int const f1 = atoi(ts+20);
      int const f2 = atoi(buf+20);
      int const d = f1 - f2;
      /* assert(f1 || f2); */
      if(d<-1 || d>1){
        f_out("WARNING: possible round-trip fidelity mismatch: "
              "\n\twant: %"FSL_JULIAN_T_PFMT"\t%s"
              "\n\tgot : %"FSL_JULIAN_T_PFMT"\t%s\n",
              jexp, ts, j, buf);
        if(1==++warnings){
          f_out("These are normally caused by, e.g., N.789 ==> N.790 "
                "or vice versa, which is still well within tolerance "
                "but is more difficult to test against in string form.\n"
                );
        }
      }else{
        if(0!=fsl_strncmp(ts, buf, 19)){
          f_out("Mismatch at YMDHmS level:\n\t%s\n\t%s\n", ts, buf);
        }
        ++diffBy1Ms;
      }
    }
    /* Try without fractional part. These should never mismatch. */
    rc = fsl_julian_to_iso8601(j, buf, 0);
    if(vbose) f_out("j=>iso: %s\n", buf);
    assert(rc);
    if(0!=fsl_strncmp(ts, buf, 19)){
      /* f_out("VERY UNPEXECTED MISMATCH:\n\t%.*s\n\t%.*s\n", 19, ts, 19, buf); */
      /* assert(!"These should never mismatch. "
         "Unless, of course, the SS.999 problem hits."); */
      if(oldWarn == warnings) ++warnings /* avoid double counting */;
      /* This is caused by:
         SS.000 ==> (SS-1).999
         in julian-to-iso8601. But that's "fixed" now - we shouldn't
         see this anymore. Still seeing it in the main fossil repo
         for about 0.27% of the records :/.
      */
    }
  }
  fsl_stmt_finalize(&q);
  f_out("%d Julian timestamps tested from event.mtime.\n", counter);
  if(diffBy1Ms>0){
    f_out("%d record(s) (%3.2f%%) differed round-trip by 1ms "
          "(this is \"normal\"/not unexpected).\n", diffBy1Ms,
          ((diffBy1Ms+0.0)/(counter+0.0)*100));
    assert((1.0 > ((diffBy1Ms+0.0)/(counter+0.0)*100))
          /*^^^ 1% was arbitrarily chosen! Increase if needed! */
           && "Suspiciously many Julian/ISO8601 off-by-1ms conversions.");
  }
  if(warnings>0){
    f_out("ACHTUNG: conversion warning count: %d (%3.2f%% of total)\n",
          warnings, ((warnings+0.0)/(counter+0.0)*100));
    assert((1.0 > ((warnings+0.0)/(counter+0.0)*100))
          /*^^^ 1% based on current rate of 0.51% of 779 records. */
           && "Suspiciously many Julian/ISO8601 conversion warnings.");
  }
}


static void test_pathfinder(){
  fsl_pathfinder PF = fsl_pathfinder_empty;
  fsl_pathfinder * pf = &PF;
  char const * found = NULL;
  int rc;
  f_out("fsl_pathfinder sanity checks...\n");
  /* add() cannot fail here b/c of custom allocator, resp.
     it will abort() if it fails. */
  fsl_pathfinder_dir_add(pf, "src");
  fsl_pathfinder_ext_add(pf, ".c");

  rc = fsl_pathfinder_search(pf, "fsl_fs", &found, NULL);
  assert(0==rc);
  assert(strstr(found, "fsl_fs.c"));
  f_out("pathfinder found %s\n", found);
  rc = fsl_pathfinder_search(pf, "nono", &found, NULL);
  assert(FSL_RC_NOT_FOUND==rc);

  fsl_pathfinder_clear(pf);
}

static void test_fs_dirpart(){
  fsl_buffer B = fsl_buffer_empty;
  fsl_buffer * b = &B;
  char const * cwd = "/foo/bar/baz";
  fsl_size_t n = fsl_strlen(cwd);
  int rc;
  f_out("fsl_file_dirpart() tests...\n");
  rc = fsl_file_dirpart(cwd, (fsl_int_t)n, b, 0);
  assert(0==rc);
  assert(0==fsl_strcmp("/foo/bar", fsl_buffer_cstr(b)));
  assert('/'!=b->mem[b->used-1]);

  b->used = 0;
  rc = fsl_file_dirpart(cwd, (fsl_int_t)n, b, 1);
  assert(0==rc);
  assert(0==fsl_strcmp("/foo/bar/", fsl_buffer_cstr(b)));
  assert('/'==b->mem[b->used-1]);

  b->used = 0;
  rc = fsl_file_dirpart("/", 1, b, 0);
  assert(0==rc);
  assert(0==b->used);

  rc = fsl_file_dirpart("/", 1, b, 1);
  assert(1==b->used);
  assert('/'==b->mem[0]);

  fsl_buffer_clear(b);
}

static int fsl_list_visitor_f_dump_str(void * p, void * visitorState ){
  VERBOSE(("dir entry: %s\n", (char const *)p));
  ++*((int*)visitorState);
  return 0;
}

static void test_dir_names(){
  fsl_list li = fsl_list_empty;
  int rc;
  fsl_id_t rid;
  int count = 0;
  rid =
#if 0
    2973 /* some commit rid with dirs in it. Not rid 1. */
#elif 0
    -1 /* all versions */
#else
    0 /* current checkout */
#endif
    ;
  f_out("fsl_repo_dir_name()/fsl_dirpart() UDF tests "
        "for rid=%"FSL_ID_T_PFMT"...\n",(fsl_id_t)rid);

  rc = fsl_repo_dir_names( fcli.f, rid, &li, 1 );
  fcli_err_report(0);
  assert(!rc);
  assert(li.used>0);
  fsl_list_visit( &li, 0, fsl_list_visitor_f_dump_str, &count );
  assert(count==li.used);
  f_out("%d dir entry(ies).\n", count);
  fsl_list_visit_free(&li, 1);
  assert(NULL == li.list);
  assert(0 == li.used);
}

static void test_tree_name(){
  fsl_cx * f = fcli.f;
  fsl_buffer buf = fsl_buffer_empty;
  int rc;
  char const * zName = "src/fsl.c";
  char const * zOrig = zName;
  f_out("Starting fsl_checkout_filename_check() test.\n");
  rc = fsl_checkout_filename_check(f, zName, &buf);
  /* f_out("fsl_checkout_filename_check(%s) ==> %b\n", zName, &buf); */
  assert(0==rc);
  assert(0==fsl_strcmp(zName, fsl_buffer_cstr(&buf)));
  buf.used = 0;
  zName = "/etc/hosts";
  rc = fsl_checkout_filename_check(f, zName, &buf);
  assert(FSL_RC_RANGE == fcli_error()->code);
  /*fcli_err_report(1); assert(fcli_error()->code == 0); */
  fcli_err_reset(); assert(fcli_error()->code == 0);
  assert(FSL_RC_RANGE==rc);

  rc = fsl_checkout_filename_check(f, zOrig, NULL);
  assert(0==rc);

  buf.used = 0;
  rc = fsl_checkout_filename_check(f, "...../....", &buf);
  assert(FSL_RC_RANGE==rc);
  /* fcli_err_report(1); */
  zName = NULL;
  fsl_error_get( fcli_error(), &zName, NULL);
  assert(NULL != zName);
  assert(NULL != strstr(zName, "resolve"));
  fcli_err_reset();
  fsl_buffer_clear(&buf);
}

static void test_strftime(){
  enum { BufSize = 256 };
  char buf[BufSize];
  fsl_size_t len;
  char const * str = buf;
  struct tm * tm;
  time_t timt;
  struct FmtInfo {
    char const * fmt;
    char const * label;
  } fmtList[] = {
  {"%Y-%m-%d %H:%M:%S", "YYYY-mm-dd HH:MM:SS"},
  {"%%", "Literal percent"},
  {"%a", "Abbr. weekday name"},
  {"%A", "Full weekday name"},
  {"%b", "Abbr. month name"},
  {"%e", "day of month, blank-padded"},
  {"%h", "Same as %b"},
  {"%B", "Full month name"},
  {"%c", "??? \"appropriate date/time representation\""},
  {"%C", "Century as two digits"},
  {"%d", "day of month, 01-31"},
  {"%D", "%m/%d/%y"},
  {"%E", "ignored"},
  {"%H", "hour, 00-23"},
  {"%I", "hour, 01-12"},
  {"%j", "day of year, 001-366"},
  {"%k", "hour, 0-24, blank-padded"},
  {"%l", "hour, 1-12, blank-padded"},
  {"%m", "month, 01-12"},
  {"%M", "minute, 00-59"},
  {"%n", "\\n"},
  {"%O", "ignored"},
  {"%p", "\"am\" or \"pm\""},
  {"%r", "%I:%M:%S %p"},
  {"%R", "%H:%M"},
  {"%S", "seconds, 00-61"},
  {"%t", "\\t"},
  {"%T", "%H:%M:%S"},
  {"%u", "ISO-8601 weeday as number 1-7, 1=Monday"},
  {"%U", "week of year, Sunday as first day"},
  {"%v", "dd-bbb-YYYY"},
  {"%V", "ISO-8601 week number"},
  {"%w", "weekday, 0-6, Sunday=0"},
  {"%W", "week of year, Monday as first day"},
  {"%x", "??? \"appropriate date representation\""},
  {"%X", "??? \"appropriate time representation\""},
  {"%y", "year, 00-99"},
  {"%Y", "year with century"},
  {NULL,NULL}
  };
  struct FmtInfo const * fi = fmtList;
  f_out("fsl_strftime() tests...\n");
  time(&timt);
  tm = localtime(&timt);
  if(fcli_is_verbose()){
    for( ; fi->fmt; ++fi ){
      len = fsl_strftime(buf, BufSize, fi->fmt, tm);
      f_out("strftime: %s %s ==> %s\n", fi->fmt, fi->label, buf);
      assert(len>0);
    }
  }else{
    len = fsl_strftime(buf, BufSize, "%Y-%m-%d %H:%M:%S", tm);
    f_out("strftime: %s\n", str);
    assert(19==len);
  }

  {
    int rc;
    fsl_buffer bf = fsl_buffer_empty;
    rc = fsl_buffer_strftime(&bf, "%H:%M:%S", tm);
    assert(0==rc);
    assert(8==bf.used);
    fsl_buffer_clear(&bf);
  }
}

static void test_config_db(){
  fsl_confdb_t mode = FSL_CONFDB_CKOUT;
  fsl_cx * f = fcli_cx();
  char * str;
  fsl_size_t slen;
  int rc;
  f_out("fsl_config_xxx() tests...\n");

  rc = fsl_config_transaction_begin(f, mode);
  assert(!rc);
  rc = fsl_config_set_text(f, mode, "sanity-text", "hi");
  assert(!rc);
  rc = fsl_config_set_blob(f, mode, "sanity-blob", "hi!!!", 3/*yes, 3!*/);
  assert(!rc);
  rc = fsl_config_set_int32(f, mode, "sanity-int32", 32);
  assert(!rc);
  rc = fsl_config_set_int32(f, mode, "sanity-int64", 64);
  assert(!rc);
  rc = fsl_config_set_id(f, mode, "sanity-id", 2345);
  assert(!rc);
  rc = fsl_config_set_double(f, mode, "sanity-double", -42.24);
  assert(!rc);
  rc = fsl_config_set_bool(f, mode, "sanity-bool", 1);
  assert(!rc);
  
  str = fsl_config_get_text(f, mode, "sanity-text", &slen);
  assert(0==fsl_strcmp("hi", str));
  assert(2==slen);
  fsl_free(str);

  str = fsl_config_get_text(f, mode, "sanity-blob", &slen);
  assert(0==fsl_strcmp("hi!", str));
  assert(3==slen);
  fsl_free(str);

  assert(32==fsl_config_get_int32(f, mode, -1, "sanity-int32"));
  assert(64==fsl_config_get_int64(f, mode, -1, "sanity-int64"));
  assert(2345==fsl_config_get_id(f, mode, -1, "sanity-id"));
  assert(1==fsl_config_get_bool(f, mode, -1, "sanity-bool"));
  { /* portability problem: on my x64 box sanity-double==-42.24. On my i32
       box it prints out as -42.24 but does not compare == to -42.24. So
       we'll do a slightly different check...
    */
    fsl_double_t dbl = fsl_config_get_double(f, mode, -1, "sanity-double");
    /* f_out("dbl=%"FSL_DOUBLE_T_PFMT"\n", dbl); */
    /* assert(-42.24==dbl); */
    assert(-42 == (int)dbl);
    assert(-24 == (int)(dbl*100) % -100);
  }

  rc = fsl_config_transaction_end(f, mode, 0);
  assert(!rc);

}

static void test_mtime_of_manifest(){
  fsl_cx * f = fcli_cx();
  char const * tag = "current";
  fsl_id_t mrid = 0;
  fsl_id_t fnid = 0;
  int rc;
  fsl_deck d = fsl_deck_empty;
  fsl_size_t i;
  fsl_time_t timey;
  fsl_db * dbR = fsl_cx_db_repo(f);
  struct tm * tm;
  enum { BufSize = 30 };
  char strBuf[BufSize];
  fsl_card_F const * fc;
  f_out("fsl_mtime_of_manifest() checks...\n");
  rc = fsl_sym_to_rid(f, tag, FSL_ATYPE_CHECKIN, &mrid);
  assert(!rc);
  rc = fsl_deck_load_rid(f, mrid, &d, FSL_CATYPE_MANIFEST);
  assert(!rc);
  /* Pedantic note: we are knowingly bypassing any lookup
     of files in the parent manifest here. Don't try this
     at home. */
  rc = fsl_deck_F_rewind(&d);
  for( i = 0; i < d.F.list.used; ++i){
    fsl_int64_t stm;
    fc = (fsl_card_F const * )d.F.list.list[i];
  /* if(!rc) while( !(rc=fsl_deck_F_next(&d, &fc)) && fc) { */
    if(!fc->uuid) continue;
    fnid = fsl_uuid_to_rid(f, fc->uuid);
    assert(fnid>0);
    rc = fsl_mtime_of_manifest_file(f, mrid, fnid, &timey);
    assert(0==rc);
    {
      time_t tt = (time_t)timey;
      tm = localtime(&tt);
    }
    strBuf[0] = 0;
    fsl_strftime(strBuf, BufSize, "%Y-%m-%d %H:%M:%S", tm);
    if(fcli_is_verbose()){
      f_out("%8"FSL_ID_T_PFMT" %.8s %"FSL_TIME_T_PFMT" => %s %s\n",
            (fsl_id_t)fnid, fc->uuid, (fsl_time_t)timey,
            strBuf, fc->name);
    }
    assert(19==fsl_strlen(strBuf));
    stm = fsl_db_g_int64(dbR, -1, "SELECT FSL_CI_MTIME("
                         "%"FSL_ID_T_PFMT",%"FSL_ID_T_PFMT")",
                         (fsl_id_t)mrid, (fsl_id_t)fnid);
    assert(stm == timey);
  }

  fsl_deck_finalize(&d);  

}

static void test_buffer_count_lines(){
  fsl_buffer to = fsl_buffer_empty;
  fsl_buffer from = fsl_buffer_empty;
  int rc;
  f_out("fsl_buffer_copy_lines() tests...\n");
  fsl_buffer_append(&from, "a\nb\nc\nd\n", -1);
  rc = fsl_buffer_copy_lines(&from, &to, 2);
  assert(0==rc);
  rc = fsl_strcmp("a\nb\n", fsl_buffer_cstr(&to));
  assert(0==rc);
  to.used = 0;
  rc = fsl_buffer_copy_lines(&from, &to, 3);
  assert(0==rc);
  rc = fsl_strcmp("c\nd\n", fsl_buffer_cstr(&to));
  /* f_out("<<<%b>>>\n", &to); */
  assert(from.cursor==from.used);
  assert(0==rc);

  fsl_buffer_clear(&to);
  fsl_buffer_clear(&from);
}

static void test_buffer_streams(){
  fsl_buffer bin = fsl_buffer_empty;
  fsl_buffer bout = fsl_buffer_empty;
  f_out("fsl_buffer_(input|output)_f() tests...\n");
  fsl_buffer_append(&bin, "Buffer stream.", -1);
  fsl_stream(fsl_input_f_buffer, &bin,
             fsl_output_f_buffer, &bout);
  assert(bin.used==bin.cursor);
  assert(0==fsl_buffer_compare(&bin, &bout));
  /* f_out("bout=<<<%b>>>\n", &bout); */
  fsl_buffer_clear(&bin);
  fsl_buffer_clear(&bout);
}

/* static */ void test_repo_is_readonly(){
  /* TODO */
}

int main(int argc, char * const * argv ){
  int rc = 0;
  fsl_cx * f;
  fcli.appHelp = fcli_local_help;
  rc = fcli_setup(argc, argv);
  if(FSL_RC_BREAK==rc) /* --help */return 0;
  else if(rc) goto end;
  f = fcli.f;
  f_out("Checkout dir=%s\n", fsl_cx_dir_name_checkout(f, NULL));
  f_out("Checkout db=%s\n", fsl_cx_db_file_checkout(f, NULL));
  f_out("Repo db=%s\n", fsl_cx_db_file_repo(f, NULL));
  if(1){
    rc = test_sanity_0();
    if(!rc) rc = test_sanity_repo();
    if(!rc) rc = test_sanity_delta();
    if(!rc){
      test_pathfinder();
      test_fs_dirpart();
      test_sanity_tkt_01();
      test_sanity_tkt_fields();
      test_sanity_fs();
      test_sanity_localtime();
      test_julian();
      test_julian2();
      test_dir_names();
      test_tree_name();
      test_strftime();
      test_config_db();
      test_mtime_of_manifest();
      test_buffer_count_lines();
      test_buffer_streams();
      assert(fsl_rid_is_a_checkin(f, 1));
      assert(!fsl_rid_is_a_checkin(f, 2));
    }
  }else{
    /* placeholder for use while developing tests. */
  }
  {
    char const * ckoutUuid = NULL;
    fsl_id_t ckoutRid = 0;
    fsl_checkout_version_info(f, &ckoutUuid, &ckoutRid);
    f_out("checkout UUID=%s (RID %"FSL_ID_T_PFMT")\n",
          ckoutUuid, (fsl_id_t)ckoutRid);
  }
  end:
  f_out("If you made it this far, no assertions were triggered. "
        "Now try again with valgrind.\n");
  return (fcli_err_report(0)||rc)
    ? EXIT_FAILURE : EXIT_SUCCESS;
}
