// SPDX-FileCopyrightText: 2024 Redict Contributors
// SPDX-FileCopyrightText: 2024 Salvatore Sanfilippo <antirez at gmail dot com>
//
// SPDX-License-Identifier: BSD-3-Clause
// SPDX-License-Identifier: LGPL-3.0-only

/* This module contains four tests :
 * 1- test.sanity    : Basic tests for argument validation mostly.
 * 2- test.sendbytes : Creates a pipe and registers its fds to the event loop,
 *                     one end of the pipe for read events and the other end for
 *                     the write events. On writable event, data is written. On
 *                     readable event data is read. Repeated until all data is
 *                     received.
 * 3- test.iteration : A test for BEFORE_SLEEP and AFTER_SLEEP callbacks.
 *                     Counters are incremented each time these events are
 *                     fired. They should be equal and increment monotonically.
 * 4- test.oneshot   : Test for oneshot API
 */

#include "redictmodule.h"
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <memory.h>
#include <errno.h>

int fds[2];
long long buf_size;
char *src;
long long src_offset;
char *dst;
long long dst_offset;

RedictModuleBlockedClient *bc;
RedictModuleCtx *reply_ctx;

void onReadable(int fd, void *user_data, int mask) {
    REDICTMODULE_NOT_USED(mask);

    RedictModule_Assert(strcmp(user_data, "userdataread") == 0);

    while (1) {
        int rd = read(fd, dst + dst_offset, buf_size - dst_offset);
        if (rd <= 0)
            return;
        dst_offset += rd;

        /* Received all bytes */
        if (dst_offset == buf_size) {
            if (memcmp(src, dst, buf_size) == 0)
                RedictModule_ReplyWithSimpleString(reply_ctx, "OK");
            else
                RedictModule_ReplyWithError(reply_ctx, "ERR bytes mismatch");

            RedictModule_EventLoopDel(fds[0], REDICTMODULE_EVENTLOOP_READABLE);
            RedictModule_EventLoopDel(fds[1], REDICTMODULE_EVENTLOOP_WRITABLE);
            RedictModule_Free(src);
            RedictModule_Free(dst);
            close(fds[0]);
            close(fds[1]);

            RedictModule_FreeThreadSafeContext(reply_ctx);
            RedictModule_UnblockClient(bc, NULL);
            return;
        }
    };
}

void onWritable(int fd, void *user_data, int mask) {
    REDICTMODULE_NOT_USED(user_data);
    REDICTMODULE_NOT_USED(mask);

    RedictModule_Assert(strcmp(user_data, "userdatawrite") == 0);

    while (1) {
        /* Check if we sent all data */
        if (src_offset >= buf_size)
            return;
        int written = write(fd, src + src_offset, buf_size - src_offset);
        if (written <= 0) {
            return;
        }

        src_offset += written;
    };
}

/* Create a pipe(), register pipe fds to the event loop and send/receive data
 * using them. */
int sendbytes(RedictModuleCtx *ctx, RedictModuleString **argv, int argc) {
    if (argc != 2) {
        RedictModule_WrongArity(ctx);
        return REDICTMODULE_OK;
    }

    if (RedictModule_StringToLongLong(argv[1], &buf_size) != REDICTMODULE_OK ||
        buf_size == 0) {
        RedictModule_ReplyWithError(ctx, "Invalid integer value");
        return REDICTMODULE_OK;
    }

    bc = RedictModule_BlockClient(ctx, NULL, NULL, NULL, 0);
    reply_ctx = RedictModule_GetThreadSafeContext(bc);

    /* Allocate source buffer and write some random data */
    src = RedictModule_Calloc(1,buf_size);
    src_offset = 0;
    memset(src, rand() % 0xFF, buf_size);
    memcpy(src, "randomtestdata", strlen("randomtestdata"));

    dst = RedictModule_Calloc(1,buf_size);
    dst_offset = 0;

    /* Create a pipe and register it to the event loop. */
    if (pipe(fds) < 0) return REDICTMODULE_ERR;
    if (fcntl(fds[0], F_SETFL, O_NONBLOCK) < 0) return REDICTMODULE_ERR;
    if (fcntl(fds[1], F_SETFL, O_NONBLOCK) < 0) return REDICTMODULE_ERR;

    if (RedictModule_EventLoopAdd(fds[0], REDICTMODULE_EVENTLOOP_READABLE,
        onReadable, "userdataread") != REDICTMODULE_OK) return REDICTMODULE_ERR;
    if (RedictModule_EventLoopAdd(fds[1], REDICTMODULE_EVENTLOOP_WRITABLE,
        onWritable, "userdatawrite") != REDICTMODULE_OK) return REDICTMODULE_ERR;
    return REDICTMODULE_OK;
}

int sanity(RedictModuleCtx *ctx, RedictModuleString **argv, int argc) {
    REDICTMODULE_NOT_USED(argv);
    REDICTMODULE_NOT_USED(argc);

    if (pipe(fds) < 0) return REDICTMODULE_ERR;

    if (RedictModule_EventLoopAdd(fds[0], 9999999, onReadable, NULL)
        == REDICTMODULE_OK || errno != EINVAL) {
        RedictModule_ReplyWithError(ctx, "ERR non-existing event type should fail");
        goto out;
    }
    if (RedictModule_EventLoopAdd(-1, REDICTMODULE_EVENTLOOP_READABLE, onReadable, NULL)
        == REDICTMODULE_OK || errno != ERANGE) {
        RedictModule_ReplyWithError(ctx, "ERR out of range fd should fail");
        goto out;
    }
    if (RedictModule_EventLoopAdd(99999999, REDICTMODULE_EVENTLOOP_READABLE, onReadable, NULL)
        == REDICTMODULE_OK || errno != ERANGE) {
        RedictModule_ReplyWithError(ctx, "ERR out of range fd should fail");
        goto out;
    }
    if (RedictModule_EventLoopAdd(fds[0], REDICTMODULE_EVENTLOOP_READABLE, NULL, NULL)
        == REDICTMODULE_OK || errno != EINVAL) {
        RedictModule_ReplyWithError(ctx, "ERR null callback should fail");
        goto out;
    }
    if (RedictModule_EventLoopAdd(fds[0], 9999999, onReadable, NULL)
        == REDICTMODULE_OK || errno != EINVAL) {
        RedictModule_ReplyWithError(ctx, "ERR non-existing event type should fail");
        goto out;
    }
    if (RedictModule_EventLoopDel(fds[0], REDICTMODULE_EVENTLOOP_READABLE)
        != REDICTMODULE_OK || errno != 0) {
        RedictModule_ReplyWithError(ctx, "ERR del on non-registered fd should not fail");
        goto out;
    }
    if (RedictModule_EventLoopDel(fds[0], 9999999) == REDICTMODULE_OK ||
        errno != EINVAL) {
        RedictModule_ReplyWithError(ctx, "ERR non-existing event type should fail");
        goto out;
    }
    if (RedictModule_EventLoopDel(-1, REDICTMODULE_EVENTLOOP_READABLE)
        == REDICTMODULE_OK || errno != ERANGE) {
        RedictModule_ReplyWithError(ctx, "ERR out of range fd should fail");
        goto out;
    }
    if (RedictModule_EventLoopDel(99999999, REDICTMODULE_EVENTLOOP_READABLE)
        == REDICTMODULE_OK || errno != ERANGE) {
        RedictModule_ReplyWithError(ctx, "ERR out of range fd should fail");
        goto out;
    }
    if (RedictModule_EventLoopAdd(fds[0], REDICTMODULE_EVENTLOOP_READABLE, onReadable, NULL)
        != REDICTMODULE_OK || errno != 0) {
        RedictModule_ReplyWithError(ctx, "ERR Add failed");
        goto out;
    }
    if (RedictModule_EventLoopAdd(fds[0], REDICTMODULE_EVENTLOOP_READABLE, onReadable, NULL)
        != REDICTMODULE_OK || errno != 0) {
        RedictModule_ReplyWithError(ctx, "ERR Adding same fd twice failed");
        goto out;
    }
    if (RedictModule_EventLoopDel(fds[0], REDICTMODULE_EVENTLOOP_READABLE)
        != REDICTMODULE_OK || errno != 0) {
        RedictModule_ReplyWithError(ctx, "ERR Del failed");
        goto out;
    }
    if (RedictModule_EventLoopAddOneShot(NULL, NULL) == REDICTMODULE_OK || errno != EINVAL) {
        RedictModule_ReplyWithError(ctx, "ERR null callback should fail");
        goto out;
    }

    RedictModule_ReplyWithSimpleString(ctx, "OK");
out:
    close(fds[0]);
    close(fds[1]);
    return REDICTMODULE_OK;
}

static long long beforeSleepCount;
static long long afterSleepCount;

int iteration(RedictModuleCtx *ctx, RedictModuleString **argv, int argc) {
    REDICTMODULE_NOT_USED(argv);
    REDICTMODULE_NOT_USED(argc);
    /* On each event loop iteration, eventloopCallback() is called. We increment
     * beforeSleepCount and afterSleepCount, so these two should be equal.
     * We reply with iteration count, caller can test if iteration count
     * increments monotonically */
    RedictModule_Assert(beforeSleepCount == afterSleepCount);
    RedictModule_ReplyWithLongLong(ctx, beforeSleepCount);
    return REDICTMODULE_OK;
}

void oneshotCallback(void* arg)
{
    RedictModule_Assert(strcmp(arg, "userdata") == 0);
    RedictModule_ReplyWithSimpleString(reply_ctx, "OK");
    RedictModule_FreeThreadSafeContext(reply_ctx);
    RedictModule_UnblockClient(bc, NULL);
}

int oneshot(RedictModuleCtx *ctx, RedictModuleString **argv, int argc) {
    REDICTMODULE_NOT_USED(argv);
    REDICTMODULE_NOT_USED(argc);

    bc = RedictModule_BlockClient(ctx, NULL, NULL, NULL, 0);
    reply_ctx = RedictModule_GetThreadSafeContext(bc);

    if (RedictModule_EventLoopAddOneShot(oneshotCallback, "userdata") != REDICTMODULE_OK) {
        RedictModule_ReplyWithError(ctx, "ERR oneshot failed");
        RedictModule_FreeThreadSafeContext(reply_ctx);
        RedictModule_UnblockClient(bc, NULL);
    }
    return REDICTMODULE_OK;
}

void eventloopCallback(struct RedictModuleCtx *ctx, RedictModuleEvent eid, uint64_t subevent, void *data) {
    REDICTMODULE_NOT_USED(ctx);
    REDICTMODULE_NOT_USED(eid);
    REDICTMODULE_NOT_USED(subevent);
    REDICTMODULE_NOT_USED(data);

    RedictModule_Assert(eid.id == REDICTMODULE_EVENT_EVENTLOOP);
    if (subevent == REDICTMODULE_SUBEVENT_EVENTLOOP_BEFORE_SLEEP)
        beforeSleepCount++;
    else if (subevent == REDICTMODULE_SUBEVENT_EVENTLOOP_AFTER_SLEEP)
        afterSleepCount++;
}

int RedictModule_OnLoad(RedictModuleCtx *ctx, RedictModuleString **argv, int argc) {
    REDICTMODULE_NOT_USED(argv);
    REDICTMODULE_NOT_USED(argc);

    if (RedictModule_Init(ctx,"eventloop",1,REDICTMODULE_APIVER_1)
        == REDICTMODULE_ERR) return REDICTMODULE_ERR;

    /* Test basics. */
    if (RedictModule_CreateCommand(ctx, "test.sanity", sanity, "", 0, 0, 0)
        == REDICTMODULE_ERR) return REDICTMODULE_ERR;

    /* Register a command to create a pipe() and send data through it by using
     * event loop API. */
    if (RedictModule_CreateCommand(ctx, "test.sendbytes", sendbytes, "", 0, 0, 0)
        == REDICTMODULE_ERR) return REDICTMODULE_ERR;

    /* Register a command to return event loop iteration count. */
    if (RedictModule_CreateCommand(ctx, "test.iteration", iteration, "", 0, 0, 0)
        == REDICTMODULE_ERR) return REDICTMODULE_ERR;

    if (RedictModule_CreateCommand(ctx, "test.oneshot", oneshot, "", 0, 0, 0)
        == REDICTMODULE_ERR) return REDICTMODULE_ERR;

    if (RedictModule_SubscribeToServerEvent(ctx, RedictModuleEvent_EventLoop,
        eventloopCallback) != REDICTMODULE_OK) return REDICTMODULE_ERR;

    return REDICTMODULE_OK;
}
