/* -*- Mode: C; tab-width: 4; c-basic-offset: 4; indent-tabs-mode: nil -*- */

/*
 *   Copyright 2013 Zynga Inc.
 *
 *   Licensed under the Apache License, Version 2.0 (the "License");
 *   you may not use this file except in compliance with the License.
 *   You may obtain a copy of the License at
 *
 *       http://www.apache.org/licenses/LICENSE-2.0
 *
 *   Unless required by applicable law or agreed to in writing, software
 *   distributed under the License is distributed on an "AS IS" BASIS,
 *   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *   See the License for the specific language governing permissions and
 *   limitations under the License.
 */
#undef NDEBUG
#include "config.h"
#include <assert.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>
#include "basic_engine_testsuite.h"

struct test_harness test_harness;

/*
 * Make sure that get_info returns something and that repeated calls to it
 * return the same something.
 */
static enum test_result get_info_test(ENGINE_HANDLE *h, ENGINE_HANDLE_V1 *h1) {
    const engine_info *info = h1->get_info(h);
    assert(info != NULL);
    assert(info == h1->get_info(h));
    return SUCCESS;
}

/*
 * Make sure that the structure returned by get_info has a non-null description.
 */
static enum test_result get_info_description_test(ENGINE_HANDLE *h, ENGINE_HANDLE_V1 *h1) {
    const engine_info *info = h1->get_info(h);
    assert(info->description != NULL);
    return SUCCESS;
}

/*
 * Make sure that the structure returned by get_info has a valid number of
 * features and that the size of the feautes array equals that value
 */
static enum test_result get_info_features_test(ENGINE_HANDLE *h, ENGINE_HANDLE_V1 *h1) {
    const engine_info *info = h1->get_info(h);
    uint32_t nfeats = info->num_features;
    assert (nfeats > 0);
    const feature_info *fi = info->features;
    while (nfeats-- > 0) {
        assert(fi++ != NULL);
    }

    return SUCCESS;
}

/*
 * Make sure we can successfully allocate an item, allocate op returns success
 * and that item struct is populated
 */
static enum test_result allocate_test(ENGINE_HANDLE *h, ENGINE_HANDLE_V1 *h1) {
    item *test_item = NULL;
    void *key = "akey";
    assert(h1->allocate(h, NULL, &test_item, key, strlen(key), 1,1,1, 0 ) == ENGINE_SUCCESS);
    assert(test_item != NULL);
    h1->release(h,NULL,test_item);
    return SUCCESS;
}

/*
 * Verify set behavior
 */
static enum test_result set_test(ENGINE_HANDLE *h, ENGINE_HANDLE_V1 *h1) {
    item *it;
    void *key = "key";
    assert(h1->allocate(h, NULL, &it, key,
                        strlen(key), 1, 1, 0,0) == ENGINE_SUCCESS);

    uint64_t prev_cas;
    uint64_t cas = 0;

    for (int ii = 0; ii < 10; ++ii) {
        prev_cas = cas;
        assert(h1->store(h, NULL, it, &cas, OPERATION_SET,0) == ENGINE_SUCCESS);
        assert(cas != prev_cas);
    }
    h1->release(h, NULL, it);
    return SUCCESS;
}

/*
 * Verify add behavior
 */
static enum test_result add_test(ENGINE_HANDLE *h, ENGINE_HANDLE_V1 *h1) {
    item *it;
    void *key = "key";
    assert(h1->allocate(h, NULL, &it, key,
                        strlen(key), 1, 1, 0,0) == ENGINE_SUCCESS);
    uint64_t cas;

    for (int ii = 0; ii < 10; ++ii) {
        ENGINE_ERROR_CODE ret = h1->store(h, NULL, it, &cas, OPERATION_ADD, 0);
        if (ii == 0) {
            assert(ret == ENGINE_SUCCESS);
            assert(cas != 0);
        } else {
            assert(ret == ENGINE_NOT_STORED);
        }
    }
    h1->release(h, NULL, it);
    return SUCCESS;
}

/*
 * Verify replace behavior
 */
static enum test_result replace_test(ENGINE_HANDLE *h, ENGINE_HANDLE_V1 *h1) {
    item *it;
    void *key = "key";
    assert(set_test(h, h1) == SUCCESS);

    assert(h1->allocate(h, NULL, &it, key,
                        strlen(key), sizeof(int), 1, 0,0) == ENGINE_SUCCESS);
    item_info item_info = { .nvalue = 1 };
    assert(h1->get_item_info(h, NULL, it, &item_info) == true);

    uint64_t prev_cas;
    uint64_t cas = 0;

    for (int ii = 0; ii < 10; ++ii) {
        prev_cas = cas;
        *(int*)(item_info.value[0].iov_base) = ii;
        assert(h1->store(h, NULL, it, &cas, OPERATION_REPLACE,0) == ENGINE_SUCCESS);
        assert(cas != prev_cas);
    }
    h1->release(h, NULL, it);

    assert(h1->get(h, NULL, &it, key, strlen(key), 0) == ENGINE_SUCCESS);
    assert(h1->get_item_info(h, NULL, it, &item_info) == true);
    assert(item_info.value[0].iov_len == sizeof(int));
    assert(*(int*)(item_info.value[0].iov_base) == 9);
    h1->release(h, NULL, it);

    return SUCCESS;
}

/*
 * Verify append behavior
 */
static enum test_result append_test(ENGINE_HANDLE *h, ENGINE_HANDLE_V1 *h1) {
    item *it;
    void *key = "key";
    uint64_t cas;

    assert(h1->allocate(h, NULL, &it, key,
                        strlen(key), 5, 1, 0, 0) == ENGINE_SUCCESS);
    item_info item_info = { .nvalue = 1 };
    assert(h1->get_item_info(h, NULL, it, &item_info) == true);
    memcpy(item_info.value[0].iov_base, "HELLO", 5);
    assert(h1->store(h, NULL, it, &cas, OPERATION_SET, 0) == ENGINE_SUCCESS);
    h1->release(h, NULL, it);
    assert(h1->allocate(h, NULL, &it, key,
                        strlen(key), 6, 1, 0,0) == ENGINE_SUCCESS);
    item_info.nvalue = 1;
    assert(h1->get_item_info(h, NULL, it, &item_info) == true);
    memcpy(item_info.value[0].iov_base, " WORLD", 6);
    assert(h1->store(h, NULL, it, &cas, OPERATION_APPEND, 0) == ENGINE_SUCCESS);
    h1->release(h, NULL, it);

    assert(h1->get(h, NULL, &it, key, strlen(key), 0) == ENGINE_SUCCESS);
    assert(h1->get_item_info(h, NULL, it, &item_info) == true);
    assert(item_info.value[0].iov_len == 11);
    assert(memcmp(item_info.value[0].iov_base, "HELLO WORLD", 11) == 0);
    h1->release(h, NULL, it);

    return SUCCESS;
}

/*
 * Verify prepend behavior
 */
static enum test_result prepend_test(ENGINE_HANDLE *h, ENGINE_HANDLE_V1 *h1) {
    item *it;
    void *key = "key";
    uint64_t cas;

    assert(h1->allocate(h, NULL, &it, key,
                        strlen(key), 5, 1, 0,0) == ENGINE_SUCCESS);
    item_info item_info = { .nvalue = 1 };
    assert(h1->get_item_info(h, NULL, it, &item_info) == true);
    memcpy(item_info.value[0].iov_base, "HELLO", 5);
    assert(h1->store(h, NULL, it, &cas, OPERATION_SET, 0) == ENGINE_SUCCESS);
    h1->release(h, NULL, it);
    assert(h1->allocate(h, NULL, &it, key,
                        strlen(key), 6, 1, 0,0) == ENGINE_SUCCESS);
    item_info.nvalue = 1;
    assert(h1->get_item_info(h, NULL, it, &item_info) == true);
    memcpy(item_info.value[0].iov_base, " WORLD", 6);
    assert(h1->store(h, NULL, it, &cas, OPERATION_PREPEND, 0) == ENGINE_SUCCESS);
    h1->release(h, NULL, it);

    assert(h1->get(h, NULL, &it, key, strlen(key), 0) == ENGINE_SUCCESS);
    assert(h1->get_item_info(h, NULL, it, &item_info) == true);
    assert(item_info.value[0].iov_len == 11);
    assert(memcmp(item_info.value[0].iov_base, " WORLDHELLO", 11) == 0);
    h1->release(h, NULL, it);

    return SUCCESS;
}

/*
 * Make sure when we can successfully store an item after it has been allocated
 * and that the cas for the stored item has been generated.
 */
static enum test_result store_test(ENGINE_HANDLE *h, ENGINE_HANDLE_V1 *h1) {
    item *test_item = NULL;
    void *key = "bkey";
    uint64_t cas = 0;
    assert(h1->allocate(h, NULL, &test_item, key, strlen(key), 1,1,1, 0 ) == ENGINE_SUCCESS);
    assert(h1->store(h, NULL, test_item, &cas, OPERATION_SET,0) == ENGINE_SUCCESS);
    assert(cas != 0);
    h1->release(h,NULL,test_item);
    return SUCCESS;
}

/*
 * Make sure when we can successfully retrieve an item that has been stored in
 * the engine
 */
static enum test_result get_test(ENGINE_HANDLE *h, ENGINE_HANDLE_V1 *h1) {
    item *test_item = NULL;
    item *test_item_get = NULL;
    void *key = "get_test_key";
    uint64_t cas = 0;
    assert(h1->allocate(h, NULL, &test_item, key, strlen(key), 1,0, 0, 0 ) == ENGINE_SUCCESS);
    assert(h1->store(h, NULL, test_item, &cas, OPERATION_SET,0) == ENGINE_SUCCESS);
    assert(h1->get(h,NULL,&test_item_get,key,strlen(key),0) == ENGINE_SUCCESS);
    h1->release(h,NULL,test_item);
    h1->release(h,NULL,test_item_get);
    return SUCCESS;
}

static enum test_result expiry_test(ENGINE_HANDLE *h, ENGINE_HANDLE_V1 *h1) {
    item *test_item = NULL;
    item *test_item_get = NULL;
    void *key = "get_test_key";
    uint64_t cas = 0;
    assert(h1->allocate(h, NULL, &test_item, key, strlen(key), 1, 0, 10, 0 ) == ENGINE_SUCCESS);
    assert(h1->store(h, NULL, test_item, &cas, OPERATION_SET, 0) == ENGINE_SUCCESS);
    test_harness.time_travel(11);
    assert(h1->get(h,NULL,&test_item_get,key,strlen(key),0) == ENGINE_KEY_ENOENT);
    h1->release(h,NULL,test_item);
    return SUCCESS;
}

/*
 * Make sure that we can release an item. For the most part all this test does
 * is ensure that thinds dont go splat when we call release. It does nothing to
 * ensure that release did much of anything.
 */
static enum test_result release_test(ENGINE_HANDLE *h, ENGINE_HANDLE_V1 *h1) {
    item *test_item = NULL;
    void *key = "release_test_key";
    uint64_t cas = 0;
    assert(h1->allocate(h, NULL, &test_item, key, strlen(key), 1, 0, 0, 0 ) == ENGINE_SUCCESS);
    assert(h1->store(h, NULL, test_item, &cas, OPERATION_SET,0) == ENGINE_SUCCESS);
    h1->release(h, NULL, test_item);
    return SUCCESS;
}

/*
 * Make sure that we can remove an item and that after the item has been
 * removed it can not be retrieved.
 */
static enum test_result remove_test(ENGINE_HANDLE *h, ENGINE_HANDLE_V1 *h1) {
    item *test_item = NULL;
    void *key = "remove_test_key";
    uint64_t cas = 0;
    assert(h1->allocate(h, NULL, &test_item, key, strlen(key), 1,0, 0, 0 ) == ENGINE_SUCCESS);
    assert(h1->store(h, NULL, test_item, &cas, OPERATION_SET,0) == ENGINE_SUCCESS);
    assert(h1->remove(h, NULL, key, strlen(key), cas, 0) == ENGINE_SUCCESS);
    item *check_item = test_item;
    assert(h1->get(h, NULL, &check_item, key, strlen(key), 0) ==  ENGINE_KEY_ENOENT);
    assert(check_item == NULL);
    h1->release(h, NULL, test_item);
    return SUCCESS;
}

/*
 * Make sure we can arithmetic operations to set the initial value of a key and
 * to then later increment that value
 */
static enum test_result incr_test(ENGINE_HANDLE *h, ENGINE_HANDLE_V1 *h1) {
    item *test_item = NULL;
    void *key = "incr_test_key";
    uint64_t cas = 0;
    uint64_t res = 0;
    assert(h1->allocate(h, NULL, &test_item, key, strlen(key), 1, 0, 0, 0 ) == ENGINE_SUCCESS);
    assert(h1->arithmetic(h, NULL, key, strlen(key), true, true, 0, 1,
           0, &cas, &res, 0 ) == ENGINE_SUCCESS);
    assert(res == 1);
    assert(h1->arithmetic(h, NULL, key, strlen(key), true, false, 1, 0,
           0, &cas, &res, 0 ) == ENGINE_SUCCESS);
    assert(res == 2);
    h1->release(h, NULL, test_item);
    return SUCCESS;
}

static void *incr_test_main(void *arg) {
    ENGINE_HANDLE *h = arg;
    ENGINE_HANDLE_V1 *h1 = arg;
    void *key = "incr_test_key";
    uint64_t cas = 0;
    uint64_t res = 0;

    for (int ii = 0; ii < 1000; ++ii) {
        assert(h1->arithmetic(h, NULL, key, strlen(key), false, false, 1, 0,
                              0, &cas, &res, 0 ) == ENGINE_SUCCESS);

    }

    return NULL;
}


/*
 * Make sure we can arithmetic operations to set the initial value of a key and
 * to then later increment that value
 */
static enum test_result mt_incr_test(ENGINE_HANDLE *h, ENGINE_HANDLE_V1 *h1) {
#ifdef __arm__
    const int max_threads = 1;
#else
    const int max_threads = 30;
#endif
    pthread_t tid[max_threads];

    if (max_threads < 2) {
        return SKIPPED;
    }

    item *test_item = NULL;
    void *key = "incr_test_key";
    uint64_t cas = 0;
    uint64_t res = 0;
    assert(h1->allocate(h, NULL, &test_item, key,
                        strlen(key), 1, 0, 0,0) == ENGINE_SUCCESS);
    assert(h1->arithmetic(h, NULL, key, strlen(key), true, true, 0, 1,
                          0, &cas, &res, 0 ) == ENGINE_SUCCESS);
    h1->release(h, NULL, test_item);

    for (int ii = 0; ii < max_threads; ++ii) {
        assert(pthread_create(&tid[ii], NULL, incr_test_main, h) == 0);
    }

    for (int ii = 0; ii < max_threads; ++ii) {
        void *ret;
        assert(pthread_join(tid[ii], &ret) == 0);
        assert(ret == NULL);
    }

    return SUCCESS;
}

/*
 * Make sure we can arithmetic operations to set the initial value of a key and
 * to then later decrement that value
 */
static enum test_result decr_test(ENGINE_HANDLE *h, ENGINE_HANDLE_V1 *h1) {
    item *test_item = NULL;
    void *key = "decr_test_key";
    uint64_t cas = 0;
    uint64_t res = 0;
    assert(h1->allocate(h, NULL, &test_item, key, strlen(key), 1,0, 0, 0 ) == ENGINE_SUCCESS);
    assert(h1->arithmetic(h, NULL, key, strlen(key), false, true, 0, 1,
           0, &cas, &res, 0 ) == ENGINE_SUCCESS);
    assert(res == 1);
    assert(h1->arithmetic(h, NULL, key, strlen(key), false, false, 1, 0,
           0, &cas, &res, 0 ) == ENGINE_SUCCESS);
    assert(res == 0);
    h1->release(h, NULL, test_item);
    return SUCCESS;
}

/*
 * Make sure we can successfully perform a flush operation and that any item
 * stored before the flush can not be retrieved
 */
static enum test_result flush_test(ENGINE_HANDLE *h, ENGINE_HANDLE_V1 *h1) {
    item *test_item = NULL;
    void *key = "flush_test_key";
    uint64_t cas = 0;
    test_harness.time_travel(3);
    assert(h1->allocate(h, NULL, &test_item, key, strlen(key), 1, 0, 0, 0 ) == ENGINE_SUCCESS);
    assert(h1->store(h, NULL, test_item, &cas, OPERATION_SET,0) == ENGINE_SUCCESS);
    assert(h1->flush(h, NULL, 0) == ENGINE_SUCCESS);
    item *check_item = test_item;
    assert(h1->get(h, NULL, &check_item, key, strlen(key), 0) ==  ENGINE_KEY_ENOENT);
    assert(check_item == NULL);
    h1->release(h, NULL, test_item);
    return SUCCESS;
}

/*
 * Make sure we can successfully retrieve the item info struct for an item and
 * that the contents of the item_info are as expected.
 */
static enum test_result get_item_info_test(ENGINE_HANDLE *h, ENGINE_HANDLE_V1 *h1) {
    item *test_item = NULL;
    char *key = "get_item_info_test_key";
    uint64_t cas = 0;
    const rel_time_t exp = 1;
    item_info ii = { .nvalue = 1 };
    assert(h1->allocate(h, NULL, &test_item, key, strlen(key), 1,0, exp, 0 ) == ENGINE_SUCCESS);
    assert(h1->store(h, NULL, test_item, &cas, OPERATION_SET,0) == ENGINE_SUCCESS);
    /* Had this been actual code, there'd be a connection here */
    assert(h1->get_item_info(h, NULL, test_item, &ii) == true);
    assert(ii.cas == cas);
    assert(ii.flags == 0);
    assert(strcmp(key,ii.key) == 0);
    assert(ii.nkey == strlen(key));
    assert(ii.nbytes == 1);
    assert(ii.exptime == exp);
    h1->release(h, NULL, test_item);
    return SUCCESS;
}

static enum test_result item_set_cas_test(ENGINE_HANDLE *h, ENGINE_HANDLE_V1 *h1) {
    item *test_item = NULL;
    char *key = "item_set_cas_test_key";
    uint64_t cas = 0;
    const rel_time_t exp = 1;
    item_info ii = { .nvalue = 1 };
    assert(h1->allocate(h, NULL, &test_item, key, strlen(key), 1,0, exp, 0 ) == ENGINE_SUCCESS);
    assert(h1->store(h, NULL, test_item, &cas, OPERATION_SET,0) == ENGINE_SUCCESS);
    uint64_t newcas = cas + 1;
    h1->item_set_cas(h, NULL, test_item, newcas);
    assert(h1->get_item_info(h, NULL, test_item, &ii) == true);
    assert(ii.cas == newcas);
    h1->release(h, NULL, test_item);
    return SUCCESS;
}

uint32_t evictions;
static void eviction_stats_handler(const char *key, const uint16_t klen,
                                   const char *val, const uint32_t vlen,
                                   const void *cookie) {
    if (memcmp(key, "evictions", klen) == 0) {
        char buffer[vlen + 1];
        memcpy(buffer, val, vlen);
        buffer[vlen] = '\0';
        evictions = atoi(buffer);
    }
}

static enum test_result lru_test(ENGINE_HANDLE *h, ENGINE_HANDLE_V1 *h1) {
    item *test_item = NULL;
    const char *hot_key = "hot_key";
    uint64_t cas = 0;
    assert(h1->allocate(h, NULL, &test_item,
                        hot_key, strlen(hot_key), 4096, 0, 0,0) == ENGINE_SUCCESS);
    assert(h1->store(h, NULL, test_item,
                     &cas, OPERATION_SET,0) == ENGINE_SUCCESS);
    h1->release(h, NULL, test_item);

    int ii;
    for (ii = 0; ii < 250; ++ii) {
        assert(h1->get(h, NULL, &test_item,
                       hot_key, strlen(hot_key), 0) ==  ENGINE_SUCCESS);
        h1->release(h, NULL, test_item);
        char key[1024];
        size_t keylen = snprintf(key, sizeof(key), "lru_test_key_%08d", ii);
        assert(h1->allocate(h, NULL, &test_item,
                            key, keylen, 4096, 0, 0,0) == ENGINE_SUCCESS);
        assert(h1->store(h, NULL, test_item,
                         &cas, OPERATION_SET,0) == ENGINE_SUCCESS);
        h1->release(h, NULL, test_item);
        assert(h1->get_stats(h, NULL, NULL, 0,
                             eviction_stats_handler) == ENGINE_SUCCESS);
        if (evictions == 2) {
            break;
        }
    }

    assert(ii < 250);
    for (int jj = 0; jj <= ii; ++jj) {
        char key[1024];
        size_t keylen = snprintf(key, sizeof(key), "lru_test_key_%08d", jj);
        if (jj == 0 || jj == 1) {
            assert(h1->get(h, NULL, &test_item,
                           key, keylen, 0) == ENGINE_KEY_ENOENT);
        } else {
            assert(h1->get(h, NULL, &test_item,
                           key, keylen, 0) == ENGINE_SUCCESS);
            assert(test_item != NULL);
            h1->release(h, NULL, test_item);
        }
    }
    return SUCCESS;
}

static enum test_result get_stats_test(ENGINE_HANDLE *h, ENGINE_HANDLE_V1 *h1) {
    return PENDING;
}

static enum test_result reset_stats_test(ENGINE_HANDLE *h, ENGINE_HANDLE_V1 *h1) {
    return PENDING;
}

static enum test_result get_stats_struct_test(ENGINE_HANDLE *h, ENGINE_HANDLE_V1 *h1) {
    return PENDING;
}

static enum test_result aggregate_stats_test(ENGINE_HANDLE *h, ENGINE_HANDLE_V1 *h1) {
    return PENDING;
}

static protocol_binary_response_header *last_response;

static void release_last_response(void) {
    free(last_response);
    last_response = NULL;
}

static bool response_handler(const void *key, uint16_t keylen,
                             const void *ext, uint8_t extlen,
                             const void *body, uint32_t bodylen,
                             uint8_t datatype, uint16_t status,
                             uint64_t cas, const void *cookie)
{
    assert(last_response == NULL);
    last_response = malloc(sizeof(*last_response) + keylen + extlen + bodylen);
    if (last_response == NULL) {
        return false;
    }
    protocol_binary_response_header *r = last_response;
    r->response.magic = PROTOCOL_BINARY_RES;
    r->response.opcode = 0xff; // we don't know this!
    r->response.keylen = htons(keylen);
    r->response.extlen = extlen;
    r->response.datatype = PROTOCOL_BINARY_RAW_BYTES;
    r->response.status = htons(status);
    r->response.bodylen = htonl(keylen + extlen + bodylen);
    r->response.opaque = 0xffffff; // we don't know this
    r->response.cas = cas;
    char *ptr = (void*)(r + 1);
    memcpy(ptr, ext, extlen);
    ptr += extlen;
    memcpy(ptr, key, keylen);
    ptr += keylen;
    memcpy(ptr, body, bodylen);

    return true;
}

static enum test_result touch_test(ENGINE_HANDLE *h, ENGINE_HANDLE_V1 *h1) {
    union request {
        protocol_binary_request_touch touch;
        char buffer[512];
    };

    void *key = "get_test_key";
    size_t keylen = strlen(key);
    union request r = {
        .touch = {
            .message = {
                .header.request = {
                    .magic = PROTOCOL_BINARY_REQ,
                    .opcode = PROTOCOL_BINARY_CMD_TOUCH,
                    .keylen = htons((uint16_t)keylen),
                    .extlen = 4,
                    .datatype = PROTOCOL_BINARY_RAW_BYTES,
                    .vbucket = 0,
                    .bodylen = htonl(keylen + 4),
                    .opaque = 0xdeadbeef,
                    .cas = 0
                },
                .body = {
                    .expiration = htonl(10)
                }
            }
        }
    };

    memcpy(r.buffer + sizeof(r.touch.bytes), key, keylen);
    ENGINE_ERROR_CODE ret;
    ret = h1->unknown_command(h, NULL, &r.touch.message.header, response_handler);
    assert(ret == ENGINE_SUCCESS);
    assert(last_response != NULL);
    assert(ntohs(last_response->response.status) == PROTOCOL_BINARY_RESPONSE_KEY_ENOENT);
    assert(last_response->response.keylen == 0);
    assert(last_response->response.extlen == 0);
    assert(last_response->response.bodylen == 0);
    release_last_response();

    // store and get a key
    assert(get_test(h, h1) == SUCCESS);

    // Set expiry time to 10 secs..
    ret = h1->unknown_command(h, NULL, &r.touch.message.header, response_handler);
    assert(ret == ENGINE_SUCCESS);
    assert(last_response != NULL);
    assert(ntohs(last_response->response.status) == PROTOCOL_BINARY_RESPONSE_SUCCESS);
    assert(last_response->response.keylen == 0);
    assert(last_response->response.extlen == 0);
    assert(last_response->response.bodylen == 0);
    release_last_response();

    // time-travel 11 secs..
    test_harness.time_travel(11);

    // The item should have expired now...
    item *item = NULL;
    assert(h1->get(h, NULL, &item, key, keylen, 0) == ENGINE_KEY_ENOENT);

    // Verify that it doesn't accept bogus packets. extlen is mandatory
    r.touch.message.header.request.extlen = 0;
    r.touch.message.header.request.bodylen = htonl(keylen);
    ret = h1->unknown_command(h, NULL, &r.touch.message.header, response_handler);
    assert(ret == ENGINE_SUCCESS);
    assert(last_response != NULL);
    assert(ntohs(last_response->response.status) == PROTOCOL_BINARY_RESPONSE_EINVAL);
    release_last_response();

    // key is mandatory!
    r.touch.message.header.request.extlen = 4;
    r.touch.message.header.request.keylen = 0;
    r.touch.message.header.request.bodylen = htonl(4);
    ret = h1->unknown_command(h, NULL, &r.touch.message.header, response_handler);
    assert(ret == ENGINE_SUCCESS);
    assert(last_response != NULL);
    assert(ntohs(last_response->response.status) == PROTOCOL_BINARY_RESPONSE_EINVAL);
    release_last_response();

    return SUCCESS;
}

static enum test_result gat_test(ENGINE_HANDLE *h, ENGINE_HANDLE_V1 *h1) {
    union request {
        protocol_binary_request_gat gat;
        char buffer[512];
    };

    void *key = "get_test_key";
    size_t keylen = strlen(key);
    union request r = {
        .gat = {
            .message = {
                .header.request = {
                    .magic = PROTOCOL_BINARY_REQ,
                    .opcode = PROTOCOL_BINARY_CMD_GAT,
                    .keylen = htons((uint16_t)keylen),
                    .extlen = 4,
                    .datatype = PROTOCOL_BINARY_RAW_BYTES,
                    .vbucket = 0,
                    .bodylen = htonl(keylen + 4),
                    .opaque = 0xdeadbeef,
                    .cas = 0
                },
                .body = {
                    .expiration = htonl(10)
                }
            }
        }
    };

    memcpy(r.buffer + sizeof(r.gat.bytes), key, keylen);
    ENGINE_ERROR_CODE ret;
    ret = h1->unknown_command(h, NULL, &r.gat.message.header, response_handler);
    assert(ret == ENGINE_SUCCESS);
    assert(last_response != NULL);
    assert(ntohs(last_response->response.status) == PROTOCOL_BINARY_RESPONSE_KEY_ENOENT);
    assert(last_response->response.keylen == 0);
    assert(last_response->response.extlen == 0);
    assert(last_response->response.bodylen == 0);
    release_last_response();

    // store and get a key
    assert(get_test(h, h1) == SUCCESS);

    // Set expiry time to 10 secs..
    ret = h1->unknown_command(h, NULL, &r.gat.message.header, response_handler);
    assert(ret == ENGINE_SUCCESS);
    assert(last_response != NULL);
    assert(ntohs(last_response->response.status) == PROTOCOL_BINARY_RESPONSE_SUCCESS);
    assert(last_response->response.keylen == 0);
    assert(last_response->response.extlen == 4);
    assert(ntohl(last_response->response.bodylen) == 5); // get_test sets 1 byte datalen
    release_last_response();

    // time-travel 11 secs..
    test_harness.time_travel(11);

    // The item should have expired now...
    item *item = NULL;
    assert(h1->get(h, NULL, &item, key, keylen, 0) == ENGINE_KEY_ENOENT);

    // Verify that it doesn't accept bogus packets. extlen is mandatory
    r.gat.message.header.request.extlen = 0;
    r.gat.message.header.request.bodylen = htonl(keylen);
    ret = h1->unknown_command(h, NULL, &r.gat.message.header, response_handler);
    assert(ret == ENGINE_SUCCESS);
    assert(last_response != NULL);
    assert(ntohs(last_response->response.status) == PROTOCOL_BINARY_RESPONSE_EINVAL);
    release_last_response();

    // key is mandatory!
    r.gat.message.header.request.extlen = 4;
    r.gat.message.header.request.keylen = 0;
    r.gat.message.header.request.bodylen = htonl(4);
    ret = h1->unknown_command(h, NULL, &r.gat.message.header, response_handler);
    assert(ret == ENGINE_SUCCESS);
    assert(last_response != NULL);
    assert(ntohs(last_response->response.status) == PROTOCOL_BINARY_RESPONSE_EINVAL);
    release_last_response();

    return SUCCESS;
}

static enum test_result gatq_test(ENGINE_HANDLE *h, ENGINE_HANDLE_V1 *h1) {
    union request {
        protocol_binary_request_gat gat;
        char buffer[512];
    };

    void *key = "get_test_key";
    size_t keylen = strlen(key);
    union request r = {
        .gat = {
            .message = {
                .header.request = {
                    .magic = PROTOCOL_BINARY_REQ,
                    .opcode = PROTOCOL_BINARY_CMD_GATQ,
                    .keylen = htons((uint16_t)keylen),
                    .extlen = 4,
                    .datatype = PROTOCOL_BINARY_RAW_BYTES,
                    .vbucket = 0,
                    .bodylen = htonl(keylen + 4),
                    .opaque = 0xdeadbeef,
                    .cas = 0
                },
                .body = {
                    .expiration = htonl(10)
                }
            }
        }
    };

    memcpy(r.buffer + sizeof(r.gat.bytes), key, keylen);
    ENGINE_ERROR_CODE ret;
    ret = h1->unknown_command(h, NULL, &r.gat.message.header, response_handler);
    assert(ret == ENGINE_SUCCESS);

    // GATQ is quiet and should not produce any result
    assert(last_response == NULL);

    // store and get a key
    assert(get_test(h, h1) == SUCCESS);

    // Set expiry time to 10 secs..
    ret = h1->unknown_command(h, NULL, &r.gat.message.header, response_handler);
    assert(ret == ENGINE_SUCCESS);
    assert(last_response != NULL);
    assert(ntohs(last_response->response.status) == PROTOCOL_BINARY_RESPONSE_SUCCESS);
    assert(last_response->response.keylen == 0);
    assert(last_response->response.extlen == 4);
    assert(ntohl(last_response->response.bodylen) == 5); // get_test sets 1 byte datalen
    release_last_response();

    // time-travel 11 secs..
    test_harness.time_travel(11);

    // The item should have expired now...
    item *item = NULL;
    assert(h1->get(h, NULL, &item, key, keylen, 0) == ENGINE_KEY_ENOENT);

    // Verify that it doesn't accept bogus packets. extlen is mandatory
    r.gat.message.header.request.extlen = 0;
    r.gat.message.header.request.bodylen = htonl(keylen);
    ret = h1->unknown_command(h, NULL, &r.gat.message.header, response_handler);
    assert(ret == ENGINE_SUCCESS);
    assert(last_response != NULL);
    assert(ntohs(last_response->response.status) == PROTOCOL_BINARY_RESPONSE_EINVAL);
    release_last_response();

    // key is mandatory!
    r.gat.message.header.request.extlen = 4;
    r.gat.message.header.request.keylen = 0;
    r.gat.message.header.request.bodylen = htonl(4);
    ret = h1->unknown_command(h, NULL, &r.gat.message.header, response_handler);
    assert(ret == ENGINE_SUCCESS);
    assert(last_response != NULL);
    assert(ntohs(last_response->response.status) == PROTOCOL_BINARY_RESPONSE_EINVAL);
    release_last_response();

    return SUCCESS;
}

MEMCACHED_PUBLIC_API
engine_test_t* get_tests(void) {
    static engine_test_t tests[]  = {
        {"get info test", get_info_test, NULL, NULL, NULL},
        {"get info description test", get_info_description_test, NULL, NULL, NULL},
        {"get info features test", get_info_features_test, NULL, NULL, NULL},
        {"allocate test", allocate_test, NULL, NULL, NULL},
        {"set test", set_test, NULL, NULL, NULL},
        {"add test", add_test, NULL, NULL, NULL},
        {"replace test", replace_test, NULL, NULL, NULL},
        {"append test", append_test, NULL, NULL, NULL},
        {"prepend test", prepend_test, NULL, NULL, NULL},
        {"store test", store_test, NULL, NULL, NULL},
        {"get test", get_test, NULL, NULL, NULL},
        {"expiry test", expiry_test, NULL, NULL, NULL},
        {"remove test", remove_test, NULL, NULL, NULL},
        {"release test", release_test, NULL, NULL, NULL},
        {"incr test", incr_test, NULL, NULL, NULL},
        {"mt incr test", mt_incr_test, NULL, NULL, NULL},
        {"decr test", decr_test, NULL, NULL, NULL},
        {"flush test", flush_test, NULL, NULL, NULL},
        {"get item info test", get_item_info_test, NULL, NULL, NULL},
        {"set cas test", item_set_cas_test, NULL, NULL, NULL},
        {"LRU test", lru_test, NULL, NULL, "cache_size=48"},
        {"get stats test", get_stats_test, NULL, NULL, NULL},
        {"reset stats test", reset_stats_test, NULL, NULL, NULL},
        {"get stats struct test", get_stats_struct_test, NULL, NULL, NULL},
        {"aggregate stats test", aggregate_stats_test, NULL, NULL, NULL},
        {"touch", touch_test, NULL, NULL, NULL},
        {"Get And Touch", gat_test, NULL, NULL, NULL},
        {"Get And Touch Quiet", gatq_test, NULL, NULL, NULL},
        {NULL, NULL, NULL, NULL, NULL}
    };
    return tests;
}

MEMCACHED_PUBLIC_API
bool setup_suite(struct test_harness *th) {
    test_harness = *th;
    return true;
}

