/*-
 * Public Domain 2014-present MongoDB, Inc.
 * Public Domain 2008-2014 WiredTiger, Inc.
 *
 * This is free and unencumbered software released into the public domain.
 *
 * Anyone is free to copy, modify, publish, use, compile, sell, or
 * distribute this software, either in source code form or as a compiled
 * binary, for any purpose, commercial or non-commercial, and by any
 * means.
 *
 * In jurisdictions that recognize copyright laws, the author or authors
 * of this software dedicate any and all copyright interest in the
 * software to the public domain. We make this dedication for the benefit
 * of the public at large and to the detriment of our heirs and
 * successors. We intend this dedication to be an overt act of
 * relinquishment in perpetuity of all present and future rights to this
 * software under copyright law.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
 * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
 * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
 * IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
 * OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
 * ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
 * OTHER DEALINGS IN THE SOFTWARE.
 */
#include "test_util.h"

#define KEY "key"
#define VALUE "value,value,value"

static int ignore_errors;

static int
handle_error(WT_EVENT_HANDLER *handler, WT_SESSION *session, int error, const char *message)
{
    (void)(handler);

    /* Skip the error messages we're expecting to see. */
    if (ignore_errors > 0 &&
      (strstr(message, "requires key be set") != NULL ||
        strstr(message, "requires value be set") != NULL)) {
        --ignore_errors;
        return (0);
    }

    (void)fprintf(stderr, "%s: %s\n", message, session->strerror(session, error));
    return (0);
}

static WT_EVENT_HANDLER event_handler = {handle_error, NULL, NULL, NULL};

#define SET_KEY                                   \
    do {                                          \
        if (recno)                                \
            cursor->set_key(cursor, (uint64_t)1); \
        else {                                    \
            strcpy(keybuf, KEY);                  \
            cursor->set_key(cursor, keybuf);      \
        }                                         \
    } while (0)
#define SET_VALUE                                 \
    do {                                          \
        strcpy(valuebuf, VALUE);                  \
        if (vstring)                              \
            cursor->set_value(cursor, valuebuf);  \
        else {                                    \
            vu.size = strlen(vu.data = valuebuf); \
            cursor->set_value(cursor, &vu);       \
        }                                         \
    } while (0)

static void
cursor_scope_ops(WT_SESSION *session, const char *uri)
{
    struct {
        const char *op;
        enum {
            INSERT_GET_KEY,
            INSERT_GET_VALUE,
            MODIFY,
            SEARCH,
            SEARCH_NEAR,
            REMOVE_GET_KEY,
            REMOVE_GET_VALUE,
            REMOVE_POS,
            RESERVE,
            UPDATE
        } func;
    } * op,
      ops[] = {/*
                * The ops order is specific: insert has to happen first so
                * other operations are possible, and remove has to be last.
                */
        {
          "insert",
          INSERT_GET_KEY,
        },
        {
          "insert",
          INSERT_GET_VALUE,
        },
        {
          "search",
          SEARCH,
        },
        {
          "search",
          SEARCH_NEAR,
        },
        {
          "reserve",
          RESERVE,
        },
        {
          "insert",
          MODIFY,
        },
        {
          "update",
          UPDATE,
        },
        {
          "remove",
          REMOVE_GET_KEY,
        },
        {
          "remove",
          REMOVE_GET_VALUE,
        },
        {
          "remove",
          REMOVE_POS,
        },
        {NULL, INSERT_GET_KEY}};
    WT_CURSOR *cursor;
#define MODIFY_ENTRIES 2
    WT_MODIFY entries[MODIFY_ENTRIES];
    WT_ITEM vu;
    uint64_t keyr;
    const char *key, *vs;
    char keybuf[100], valuebuf[100];
    int exact;
    bool recno, rollback, vstring;

    cursor = NULL;
    for (op = ops; op->op != NULL; op++) {
        /* Open a cursor, track key/value formats. */
        testutil_check(session->open_cursor(session, uri, NULL, NULL, &cursor));
        recno = strcmp(cursor->key_format, "r") == 0;
        vstring = strcmp(cursor->value_format, "S") == 0;

        /* Remove any leftover key/value pair, start fresh. */
        SET_KEY;
        testutil_check(cursor->remove(cursor));

        /* If not an insert operation, make sure there's a key/value pair to operate on. */
        if (op->func != INSERT_GET_KEY && op->func != INSERT_GET_VALUE) {
            SET_KEY;
            SET_VALUE;
            testutil_check(cursor->insert(cursor));
        }
        /* Discard that cursor, we'll open one inside the transaction. */
        testutil_check(cursor->close(cursor));

        /* Modify and reserve require a transaction, modify requires snapshot isolation. */
        testutil_check(session->begin_transaction(session, "isolation=snapshot"));
        rollback = false;

        /* Open a cursor, track key/value formats. */
        testutil_check(session->open_cursor(session, uri, NULL, NULL, &cursor));
        recno = strcmp(cursor->key_format, "r") == 0;
        vstring = strcmp(cursor->value_format, "S") == 0;

        /*
         * Set up application buffers so we can detect overwrites or failure to copy application
         * information into library memory.
         */
        SET_KEY;
        SET_VALUE;

        /*
         * The application must keep key and value memory valid until the next operation that
         * positions the cursor, modifies the data, or resets or closes the cursor.
         *
         * Modifying either the key or value buffers is not permitted.
         */
        switch (op->func) {
        case INSERT_GET_KEY:
        case INSERT_GET_VALUE:
            testutil_check(cursor->insert(cursor));
            break;
        case MODIFY:
            /* Modify, but don't really change anything. */
            entries[0].data.data = &VALUE[0];
            entries[0].data.size = 2;
            entries[0].offset = 0;
            entries[0].size = 2;
            entries[1].data.data = &VALUE[3];
            entries[1].data.size = 5;
            entries[1].offset = 3;
            entries[1].size = 5;

            testutil_check(cursor->modify(cursor, entries, MODIFY_ENTRIES));
            break;
        case SEARCH:
            testutil_check(cursor->search(cursor));
            break;
        case SEARCH_NEAR:
            testutil_check(cursor->search_near(cursor, &exact));
            break;
        case REMOVE_POS:
            /*
             * Remove has two modes, one where the remove is based on a cursor position, the other
             * where it's based on a set key. The results are different, so test them separately.
             */
            testutil_check(cursor->search(cursor));
        /* FALLTHROUGH */
        case REMOVE_GET_KEY:
        case REMOVE_GET_VALUE:
            testutil_check(cursor->remove(cursor));
            break;
        case RESERVE:
            testutil_check(cursor->reserve(cursor));
            break;
        case UPDATE:
            testutil_check(cursor->update(cursor));
            break;
        }

        /*
         * The cursor should no longer reference application memory, and application buffers can be
         * safely overwritten.
         */
        memset(keybuf, 'K', sizeof(keybuf));
        memset(valuebuf, 'V', sizeof(valuebuf));

        /*
         * Check that get_key/get_value behave as expected after the operation.
         */
        key = vs = NULL;
        keyr = 37;
        memset(&vu, 0, sizeof(vu));
        switch (op->func) {
        case INSERT_GET_KEY:
        case REMOVE_GET_KEY:
            /*
             * Insert and remove configured with a search key do not position the cursor and have no
             * key or value.
             *
             * There should be two error messages, ignore them, and errors require rollback.
             */
            ignore_errors = 1;
            rollback = true;
            if (recno)
                testutil_assert(cursor->get_key(cursor, &keyr) != 0);
            else
                testutil_assert(cursor->get_key(cursor, &key) != 0);
            testutil_assert(ignore_errors == 0);
            break;
        case INSERT_GET_VALUE:
        case REMOVE_GET_VALUE:
            /*
             * Insert and remove configured with a search key do not position the cursor and have no
             * key or value.
             *
             * There should be two error messages, ignore them, and errors require rollback.
             */
            ignore_errors = 1;
            rollback = true;
            if (vstring)
                testutil_assert(cursor->get_value(cursor, &vs) != 0);
            else
                testutil_assert(cursor->get_value(cursor, &vu) != 0);
            testutil_assert(ignore_errors == 0);
            break;
        case REMOVE_POS:
            /*
             * Remove configured with a cursor position has a key, but no value.
             *
             * There should be one error message, ignore it, and errors require rollback.
             */
            if (recno) {
                testutil_assert(cursor->get_key(cursor, &keyr) == 0);
                testutil_assert(keyr == 1);
            } else {
                testutil_assert(cursor->get_key(cursor, &key) == 0);
                testutil_assert(key != keybuf);
                testutil_assert(strcmp(key, KEY) == 0);
            }
            ignore_errors = 1;
            rollback = true;
            if (vstring)
                testutil_assert(cursor->get_value(cursor, &vs) != 0);
            else
                testutil_assert(cursor->get_value(cursor, &vu) != 0);
            testutil_assert(ignore_errors == 0);
            break;
        case MODIFY:
        case RESERVE:
        case SEARCH:
        case SEARCH_NEAR:
        case UPDATE:
            /*
             * Modify, reserve, search, search-near and update all position the cursor and have both
             * a key and value.
             *
             * Any key/value should not reference application memory.
             */
            if (recno) {
                testutil_assert(cursor->get_key(cursor, &keyr) == 0);
                testutil_assert(keyr == 1);
            } else {
                testutil_assert(cursor->get_key(cursor, &key) == 0);
                testutil_assert(key != keybuf);
                testutil_assert(strcmp(key, KEY) == 0);
            }
            if (vstring) {
                testutil_assert(cursor->get_value(cursor, &vs) == 0);
                testutil_assert(vs != valuebuf);
                testutil_assert(strcmp(vs, VALUE) == 0);
            } else {
                testutil_assert(cursor->get_value(cursor, &vu) == 0);
                testutil_assert(vu.data != valuebuf);
                testutil_assert(vu.size == strlen(VALUE));
                testutil_assert(memcmp(vu.data, VALUE, strlen(VALUE)) == 0);
            }
            break;
        }

        if (rollback)
            testutil_check(session->rollback_transaction(session, NULL));
        else
            testutil_check(session->commit_transaction(session, NULL));
        testutil_check(cursor->close(cursor));
    }
}

static void
run(WT_CONNECTION *conn, const char *uri, const char *config)
{
    WT_SESSION *session;

    testutil_check(conn->open_session(conn, NULL, NULL, &session));
    testutil_check(session->create(session, uri, config));
    cursor_scope_ops(session, uri);
    testutil_check(session->close(session, NULL));
}

int
main(int argc, char *argv[])
{
    TEST_OPTS *opts, _opts;

    opts = &_opts;
    memset(opts, 0, sizeof(*opts));
    testutil_check(testutil_parse_opts(argc, argv, opts));
    testutil_make_work_dir(opts->home);

    testutil_check(wiredtiger_open(opts->home, &event_handler, "create", &opts->conn));

    run(opts->conn, "file:file.SS", "key_format=S,value_format=S");
    run(opts->conn, "file:file.Su", "key_format=S,value_format=u");
    run(opts->conn, "file:file.rS", "key_format=r,value_format=S");
    run(opts->conn, "file:file.ru", "key_format=r,value_format=u");

    run(opts->conn, "lsm:lsm.SS", "key_format=S,value_format=S");
    run(opts->conn, "lsm:lsm.Su", "key_format=S,value_format=u");

    run(opts->conn, "table:table.SS", "key_format=S,value_format=S");
    run(opts->conn, "table:table.Su", "key_format=S,value_format=u");
    run(opts->conn, "table:table.rS", "key_format=r,value_format=S");
    run(opts->conn, "table:table.ru", "key_format=r,value_format=u");

    testutil_cleanup(opts);

    return (EXIT_SUCCESS);
}
