/*
**  OSSP var -- Variable Expansion
**  Copyright (c) 2001-2004 Ralf S. Engelschall <rse@engelschall.com>
**  Copyright (c) 2001-2004 The OSSP Project (http://www.ossp.org/)
**  Copyright (c) 2001-2004 Cable & Wireless (http://www.cw.com/)
**
**  This file is part of OSSP var, a variable expansion
**  library which can be found at http://www.ossp.org/pkg/lib/var/.
**
**  Permission to use, copy, modify, and distribute this software for
**  any purpose with or without fee is hereby granted, provided that
**  the above copyright notice and this permission notice appear in all
**  copies.
**
**  THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED
**  WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
**  MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
**  IN NO EVENT SHALL THE AUTHORS AND COPYRIGHT HOLDERS AND THEIR
**  CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
**  SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
**  LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
**  USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
**  ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
**  OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
**  OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
**  SUCH DAMAGE.
**
**  var_test.c: library regression test
*/

#include <stdio.h>
#include <string.h>
#include <errno.h>
#include <ctype.h>

#include "config.h"
#if defined(HAVE_DMALLOC_H) && defined(WITH_DMALLOC)
#include "dmalloc.h"
#endif

#include "var.h"
#include "ts.h"

/*
**
**  ==== VARIABLE LOOKUP CALLBACK ====
**
*/

struct variable {
    const char *name;
    const unsigned int idx;
    const char *data;
};

static const struct variable lookup_vars[] = {
    { "ARRAY",     0, "entry0"                 },
    { "ARRAY",     1, "entry1"                 },
    { "ARRAY",     2, "entry2"                 },
    { "ARRAY",     3, "entry3"                 },
    { "BAR",       0, "type"                   },
    { "EMPTY",     0, ""                       },
    { "FOO",       0, "os"                     },
    { "HEINZ",     0, "heinz0"                 },
    { "HEINZ",     1, "heinz1"                 },
    { "HOME",      0, "/home/regression-tests" },
    { "MULTILINE", 0, "line1\nline2\n"         },
    { "NUMBER",    0, "+2"                     },
    { "NUMEXP",    0, "((16)%5)"               },
    { "OSTYPE",    0, "regression-os"          },
    { "TERM",      0, "regression-term"        },
    { NULL,        0, NULL                     }
};

static var_rc_t lookup_cb(
    var_t *var, void *context,
    const char *varname, size_t name_len, int idx,
    const char **data, size_t *data_len,
    size_t *buffer_size)
{
    const struct variable *vars = context;
    size_t i, counter, length;
    static char buf[((sizeof(int)*8)/3)+10]; /* sufficient size: <#bits> x log_10(2) + safety */

    if (idx >= 0) {
        for (i = 0; vars[i].name; ++i) {
            if (strncmp(varname, vars[i].name, name_len) == 0 && vars[i].idx == idx) {
                *data = vars[i].data;
                *data_len = strlen(*data);
                *buffer_size = 0;
                return VAR_OK;
            }
        }
    }
    else {
        for (i = 0; vars[i].name; ++i) {
            if (strncmp(varname, vars[i].name, name_len) == 0) {
                counter = 1;
                length = strlen(vars[i].data);
                while (   vars[i + counter].data
                       && strncmp(varname, vars[i + counter].name, name_len) == 0)
                    counter++;
                if (counter == 1)
                    sprintf(buf, "%d", length);
                else
                    sprintf(buf, "%d", counter);
                *data = buf;
                *data_len = strlen(buf);
                *buffer_size = 0;
                return VAR_OK;
            }
        }
    }
    return VAR_ERR_UNDEFINED_VARIABLE;
}

/*
**
**  ==== OPERATION CALLBACK ====
**
*/

static var_rc_t
operate_cb(
    var_t *var, void *ctx,
    const char  *op_ptr, size_t op_len,
    const char  *arg_ptr, size_t arg_len,
    const char  *val_ptr, size_t val_len,
    char **out_ptr, size_t *out_len, size_t *out_size)
{
    int i;

    if (val_ptr == NULL) {
        *out_ptr = "";
        *out_len = 0;
        *out_size = 0;
        return VAR_OK;
    }
    if (op_len == 6 && strncmp(op_ptr, "return", 6) == 0) {
        *out_ptr = malloc(arg_len);
        *out_len = arg_len;
        *out_size = arg_len;
        memcpy(*out_ptr, arg_ptr, arg_len);
        return VAR_OK;
    }
    else if (op_len == 5 && strncmp(op_ptr, "upper", 5) == 0) {
        *out_ptr = malloc(val_len);
        *out_len = val_len;
        *out_size = val_len;
        for (i = 0; i < val_len; i++)
            (*out_ptr)[i] = (char)toupper((int)(val_ptr[i]));
        return VAR_OK;
    }
    else if (op_len == 5 && strncmp(op_ptr, "lower", 5) == 0) {
        *out_ptr = malloc(val_len);
        *out_len = val_len;
        *out_size = val_len;
        for (i = 0; i < val_len; i++)
            (*out_ptr)[i] = (char)tolower((int)(val_ptr[i]));
        return VAR_OK;
    }
    else
        return VAR_ERR_UNDEFINED_OPERATION;
}

/*
**
**  ==== TEST CASES ====
**
*/

TS_TEST(test_expand)
{
    struct test_case {
        const char *input;
        const char *expected;
    };
    const struct test_case tests[] = {
        { "${HOME}${!!}",                 "/home/regression-tests${!!}"                    },
        { "$HOME",                        "/home/regression-tests"                         },
        { "${FOO}",                       "os"                                             },
        { "${BAR}",                       "type"                                           },
        { "${${FOO:u}${BAR:u}:l:u}",      "REGRESSION-OS"                                  },
        { "${UNDEFINED}",                 "${UNDEFINED}"                                   },
        { "${OSTYPE:#}",                  "13"                                             },
        { "${EMPTY:-test${FOO}test}",     "testostest"                                     },
        { "${EMPTY:-test${FOO:u}test}",   "testOStest"                                     },
        { "${TERM:-test${FOO}test}",      "regression-term"                                },
        { "${EMPTY:+FOO}",                ""                                               },
        { "${HOME:+test${FOO}test}",      "testostest"                                     },
        { "${HOME:+${OS${BAR:u}}}",       "regression-os"                                  },
        { "${HOME:+OS${UNDEFINED:u}}",    "OS${UNDEFINED:u}"                               },
        { "${UNDEFINED:+OS${BAR:u}}",     "${UNDEFINED:+OS${BAR:u}}"                       },
        { "${HOME:*heinz}",               ""                                               },
        { "${EMPTY:*claus}",              "claus"                                          },
        { "${TERM}",                      "regression-term"                                },
        { "${HOME:s/reg/bla/}",           "/home/blaression-tests"                         },
        { "${HOME:s/e/bla/}",             "/hombla/regression-tests"                       },
        { "${HOME:s/e/bla/g}",            "/hombla/rblagrblassion-tblasts"                 },
        { "${HOME:s/\\//_/g}",            "_home_regression-tests"                         },
        { "${HOME:s/[eso]/_/g}",          "/h_m_/r_gr___i_n-t__t_"                         },
        { "${HOME:s/[esO]/_/g}",          "/hom_/r_gr___ion-t__t_"                         },
        { "${HOME:s/[esO]/_/gi}",         "/h_m_/r_gr___i_n-t__t_"                         },
        { "${OSTYPE:s/^[re]/_/g}",        "_egression-os"                                  },
        { "${EMPTY:s/^[re]/_/g}",         ""                                               },
        { "${HOME:s/.*\\x{}/heinz/}",     "heinz"                                          },
        { "${HOME:s/e/bla/t}",            "/hombla/regression-tests"                       },
        { "${HOME:s/E/bla/t}",            "/home/regression-tests"                         },
        { "${HOME:s/E/bla/ti}",           "/hombla/regression-tests"                       },
        { "${HOME:s/E/bla/tig}",          "/hombla/rblagrblassion-tblasts"                 },
        { "${HOME:s/^(.*)$/<\\1>/}",      "</home/regression-tests>"                       },
        { "${HOME:o1-5}",                 "home/"                                          },
        { "${HOME:o1,5}",                 "home/"                                          },
        { "${HOME:o5,}",                  "/regression-tests"                              },
        { "${HOME:o5-}",                  "/regression-tests"                              },
        { "${HOME:o7,13}",                "egressi"                                        },
        { "${HOME:y/a-z/A-YZ/}",          "/HOME/REGRESSION-TESTS"                         },
        { "${HOME:y/e-g/a-c/}",           "/homa/racrassion-tasts"                         },
        { "${FOO:p/15/../l}",             "os............."                                },
        { "${FOO:p/15/12345/l}",          "os1234512345123"                                },
        { "${FOO:p/15/../r}",             ".............os"                                },
        { "${FOO:p/15/12345/r}",          "1234512345123os"                                },
        { "${FOO:p/15/../c}",             "......os......."                                },
        { "${FOO:p/15/12345/c}",          "123451os1234512"                                },
        { "${FOO:s/os/\\x{4F}\\123/g}",   "OS"                                             },
        { "${FOO:s/os/\\1\\x4F\\123/t}",  "\\1OS"                                          },
        { "${HOME:s/g(res)s/x\\1x/g}",    "/home/rexresxion-tests"                         },
        { "${HOME:s/(s+)/_\\1_/g}",       "/home/regre_ss_ion-te_s_t_s_"                   },
        { "${HOME:s/\\x65/\\x45/g}",      "/homE/rEgrEssion-tEsts"                         },
        { "${HOME:s/(s*)/x\\1X/g}",       "xX/xXhxXoxXmxXexX/xXrxXexXgxXrxXexssXxXixXoxXnxX-xXtxXexsXxXtxsX" },
        { "${HOME:s/./\\\\/g}",           "\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\"   },
        { "${ARRAY[1]}",                  "entry1",                                        },
        { "${ARRAY[5+4*2-1]}",            "${ARRAY[5+4*2-1]}",                             },
        { "${ARRAY[(5+(3+4)*2)%16]}",     "entry3",                                        },
        { "${ARRAY[(5+(3+4)*2)/9]}",      "entry2",                                        },
        { "${ARRAY[+1--2]}",              "entry3"                                         },
        { "${ARRAY[-1]}",                 "4"                                              },
        { "${HOME[-1]}",                  "22"                                             },
        { "${ARRAY[$NUMBER]}",            "entry2"                                         },
        { "${ARRAY[$NUMEXP]}",            "entry1"                                         },
        { "${ARRAY[$NUMEXP-1]}",          "entry0"                                         },
        { "${ARRAY[${UNDEFINED}-1]}",     "${ARRAY[${UNDEFINED}-1]}"                       },
        { "${ARRAY[5/(${UNDEFINED})]}",   "${ARRAY[5/(${UNDEFINED})]}"                     },
        { "[${ARRAY[#]}-]",               "entry0-entry1-entry2-entry3-"                   },
        { "[${ARRAY[#+1]}-]",             "entry1-entry2-entry3-"                          },
        { "-[${ARRAY[#]}:]{1,$NUMBER}-",  "-entry1:entry2:-"                               },
        { "-[${ARRAY[#]}:]{1,3,5}-",      "-entry1::-"                                     },
        { "${MULTILINE:s/^/ | /gm}",      " | line1\n | line2\n"                           },
        { "${HOME:%upper}",               "/HOME/REGRESSION-TESTS"                         },
        { "${HOME:%upper:%lower}",        "/home/regression-tests"                         },
        { "${EMPTY:%return($HOME)}",      "/home/regression-tests"                         },
        { "[${ARRAY}:${ARRAY[#]}-]",      "entry0:entry0-entry0:entry1-entry0:entry2-entry0:entry3-" },
        { "[${HEINZ[#]}:${ARRAY[#]}-]",   "heinz0:entry0-heinz1:entry1-:entry2-:entry3-" },
        { "[${HEINZ[#]}:[${ARRAY[#]}] ]", "heinz0:entry0entry1entry2entry3 heinz1:entry0entry1entry2entry3 " },
        { "[${HEINZ[#]}: [${ARRAY[#]}${ARRAY[#+1]:+, }]${HEINZ[#+1]:+; }]", "heinz0: entry0, entry1, entry2, entry3; heinz1: entry0, entry1, entry2, entry3" },
        { "[${ARRAY[#]}:[${ARRAY[#]},]{1,2,} ]{0,2,}", "entry0:entry1,entry3, entry2:entry1,entry3, " },
    };
    char *tmp;
    size_t tmp_len;
    var_rc_t rc;
    size_t i;
    char buffer[1024];
    var_t *var;
    char *err;

    ts_test_check(TS_CTX, "create environment");
    if ((rc = var_create(&var)) != VAR_OK) {
        var_strerror(NULL, rc, &err);
        ts_test_fail(TS_CTX, "var_create: %s (%d)", err, rc);
        return;
    }
    if ((rc = var_config(var, VAR_CONFIG_CB_VALUE, lookup_cb, lookup_vars)) != VAR_OK) {
        var_strerror(NULL, rc, &err);
        ts_test_fail(TS_CTX, "var_config: %s (%d)", err, rc);
        return;
    }
    if ((rc = var_config(var, VAR_CONFIG_CB_OPERATION, operate_cb, NULL)) != VAR_OK) {
        var_strerror(NULL, rc, &err);
        ts_test_fail(TS_CTX, "var_config: %s (%d)", err, rc);
        return;
    }

    for (i = 0; i < sizeof(tests)/sizeof(struct test_case); i++) {
        ts_test_check(TS_CTX, "expansion (#%d): raw input \"%s\"", i, tests[i].input);
        rc = var_unescape(var, tests[i].input, strlen(tests[i].input),
                          buffer, sizeof(buffer), 0);
        if (rc != VAR_OK) {
            var_strerror(var, rc, &err);
            ts_test_fail(TS_CTX, "var_unescape: %s (%d)", err, rc);
            continue;
        }
        ts_test_check(TS_CTX, "expansion (#%d): unescaped input \"%s\"", i, buffer);
        rc = var_expand(var, buffer, strlen(buffer), &tmp, &tmp_len, 0);
        if (rc != VAR_OK) {
            var_strerror(var, rc, &err);
            ts_test_fail(TS_CTX, "var_expand: %s (%d)", err, rc);
            continue;
        }
        ts_test_check(TS_CTX, "expansion (#%d): expanded output \"%s\"", i, tmp);
        if (   tmp_len != strlen(tests[i].expected)
            || memcmp(tests[i].expected, tmp, tmp_len) != 0) {
            ts_test_fail(TS_CTX, "expected \"%s\", got \"%s\"", tests[i].expected, tmp);
            free(tmp);
            continue;
        }
        free(tmp);
    }

    var_destroy(var);

    return;
}

TS_TEST(test_format)
{
    var_rc_t rc;
    var_t *var;
    char *err;
    char *fmt;
    char *expect;
    char *got;

    ts_test_check(TS_CTX, "create environment");
    if ((rc = var_create(&var)) != VAR_OK) {
        var_strerror(NULL, rc, &err);
        ts_test_fail(TS_CTX, "var_create: %s (%d)", err, rc);
        return;
    }
    if ((rc = var_config(var, VAR_CONFIG_CB_VALUE, lookup_cb, lookup_vars)) != VAR_OK) {
        var_strerror(NULL, rc, &err);
        ts_test_fail(TS_CTX, "var_config: %s (%d)", err, rc);
        return;
    }

    /* check trivial formatting */
    fmt = "foo";
    expect = "foo";
    ts_test_check(TS_CTX, "formatting \"%s\"", fmt);
    if ((rc = var_format(var, &got, 1, fmt)) != VAR_OK) {
        var_strerror(var, rc, &err);
        ts_test_fail(TS_CTX, "var_format: %s (%d)", err, rc);
        return;
    }
    if (strcmp(got, expect) != 0) {
        ts_test_fail(TS_CTX, "var_format: expected \"%s\", got \"%s\"\n", expect, got);
        free(got);
        return;
    }
    free(got);

    /* check real formatting */
    fmt = "foo<%s><%d><%c>quux";
    expect = "foo<bar><123><x>quux";
    ts_test_check(TS_CTX, "formatting \"%s\"", fmt);
    if ((rc = var_format(var, &got, 1, fmt, "bar", 123, 'x')) != VAR_OK) {
        var_strerror(var, rc, &err);
        ts_test_fail(TS_CTX, "var_format: %s (%d)", err, rc);
        return;
    }
    if (strcmp(got, expect) != 0) {
        ts_test_fail(TS_CTX, "var_format: expected \"%s\", got \"%s\"\n", expect, got);
        free(got);
        return;
    }
    free(got);

    /* check combined formatting and expansion */
    fmt = "foo<${ARRAY[%d]}>bar";
    expect = "foo<entry1>bar";
    ts_test_check(TS_CTX, "formatting \"%s\"", fmt);
    if ((rc = var_format(var, &got, 1, fmt, 1)) != VAR_OK) {
        var_strerror(var, rc, &err);
        ts_test_fail(TS_CTX, "var_format: %s (%d)", err, rc);
        return;
    }
    if (strcmp(got, expect) != 0) {
        ts_test_fail(TS_CTX, "var_format: expected \"%s\", got \"%s\"\n", expect, got);
        free(got);
        return;
    }
    free(got);

    var_destroy(var);
}

int main(int argc, char *argv[])
{
    ts_suite_t *ts;
    int n;

    ts = ts_suite_new("OSSP var (Variable Expansion)");
    ts_suite_test(ts, test_expand,  "expansion");
    ts_suite_test(ts, test_format,  "formatting");
    n = ts_suite_run(ts);
    ts_suite_free(ts);
    return n;
}

