#include <config.h>

#include <fcntl.h>
#include "internal.h"
#include "testutils.h"
#include "testutilsqemu.h"
#include "qemu/qemu_domain.h"
#include "qemu/qemu_nbdkit.h"
#define LIBVIRT_QEMU_NBDKITPRIV_H_ALLOW
#include "qemu/qemu_nbdkitpriv.h"
#include "vircommand.h"
#define LIBVIRT_VIRCOMMANDPRIV_H_ALLOW
#include "vircommandpriv.h"
#include "virutil.h"
#include "virsecret.h"
#include "datatypes.h"
#include "virmock.h"

#define VIR_FROM_THIS VIR_FROM_QEMU

static virQEMUDriver driver;


/* Some mock implementations for testing */
#define PIPE_FD_START 777
static int mockpipefd = PIPE_FD_START;

static int (*real_virPipeQuiet)(int fds[2]);
static void
init_syms(void)
{
    VIR_MOCK_REAL_INIT(virPipeQuiet);
}

static int
moveToStableFd(int fd)
{
    int newfd;

    /* don't overwrite an existing fd */
    if (fcntl(mockpipefd, F_GETFD) != -1)
        abort();

    newfd = dup2(fd, mockpipefd++);

    VIR_FORCE_CLOSE(fd);

    return newfd;
}


int
virPipeQuiet(int fds[2])
{
    int tempfds[2];

    init_syms();

    if (real_virPipeQuiet(tempfds) < 0)
        return -1;

    if ((fds[0] = moveToStableFd(tempfds[0])) < 0 ||
        (fds[1] = moveToStableFd(tempfds[1])) < 0)
        return -1;

    return 0;
}


int
virSecretGetSecretString(virConnectPtr conn G_GNUC_UNUSED,
                         virSecretLookupTypeDef *seclookupdef,
                         virSecretUsageType secretUsageType,
                         uint8_t **secret,
                         size_t *secret_size)
{
    char uuidstr[VIR_UUID_BUFLEN];
    const char *secretname = NULL;
    char *tmp = NULL;

    switch (seclookupdef->type) {
        case VIR_SECRET_LOOKUP_TYPE_UUID:
            virUUIDFormat(seclookupdef->u.uuid, uuidstr);
            secretname = uuidstr;
            break;
        case VIR_SECRET_LOOKUP_TYPE_USAGE:
            secretname = seclookupdef->u.usage;
            break;
        case VIR_SECRET_LOOKUP_TYPE_NONE:
        case VIR_SECRET_LOOKUP_TYPE_LAST:
        default:
            virReportEnumRangeError(virSecretLookupType, seclookupdef->type);
            return -1;
    };

    /* For testing, just generate a value for the secret that includes the type
     * and the id of the secret */
    tmp = g_strdup_printf("%s-%s-secret", virSecretUsageTypeToString(secretUsageType), secretname);
    *secret = (uint8_t*)tmp;
    *secret_size = strlen(tmp) + 1;

    return 0;
}

virConnectPtr virGetConnectSecret(void)
{
    return virGetConnect();
}

/* end of mock implementations */


typedef struct {
    const char *name;
    char* infile;
    char* outtemplate;
    qemuNbdkitCaps *nbdkitcaps;
    bool expectFail;
} TestInfo;


typedef enum {
    NBDKIT_ARG_CAPS,
    NBDKIT_ARG_EXPECT_FAIL,
    NBDKIT_ARG_END
} NbdkitArgName;


static void
testInfoSetPaths(TestInfo *info)
{
    info->infile = g_strdup_printf("%s/qemuxmlconfdata/%s.xml",
                                   abs_srcdir, info->name);
    info->outtemplate = g_strdup_printf("%s/qemunbdkitdata/%s",
                                        abs_srcdir, info->name);
}

static void
testInfoClear(TestInfo *info)
{
    g_free(info->infile);
    g_free(info->outtemplate);
    g_clear_object(&info->nbdkitcaps);
}

static void
testInfoSetArgs(TestInfo *info, ...)
{
    va_list argptr;
    NbdkitArgName argname;
    unsigned int cap;

    va_start(argptr, info);
    while ((argname = va_arg(argptr, NbdkitArgName)) != NBDKIT_ARG_END) {
        switch (argname) {
            case NBDKIT_ARG_CAPS:
                while ((cap = va_arg(argptr, unsigned int)) < QEMU_NBDKIT_CAPS_LAST)
                    qemuNbdkitCapsSet(info->nbdkitcaps, cap);
                break;
            case NBDKIT_ARG_EXPECT_FAIL:
                info->expectFail = va_arg(argptr, unsigned int);
                break;
            case NBDKIT_ARG_END:
            default:
                break;
        }
    }
}


static int
testNbdkit(const void *data)
{
    const TestInfo *info = data;
    g_autoptr(virDomainDef) def = NULL;
    size_t i;
    size_t n;
    int ret = 0;
    virStorageSource *backing = NULL;
    g_autofree char *statedir = NULL;

    /* restart mock pipe fds so tests are consistent */
    mockpipefd = PIPE_FD_START;

    if (!virFileExists(info->infile)) {
        virReportError(VIR_ERR_INTERNAL_ERROR,
                       "Test input file '%s' is missing", info->infile);
        return -1;
    }

    if (!(def = virDomainDefParseFile(info->infile, driver.xmlopt, NULL,
                                      VIR_DOMAIN_DEF_PARSE_SKIP_VALIDATE)))
        return -1;

    statedir = g_strdup_printf("/tmp/domain-%s", def->name);
    for (i = 0; i < def->ndisks; i++) {
        virDomainDiskDef *disk = def->disks[i];
        for (n = 0, backing = disk->src; backing != NULL; n++, backing = backing->backingStore) {
            g_autofree char *alias = g_strdup_printf("disk%zi-src%zi", i, n);
            g_autofree char *cmdfile = g_strdup_printf("%s.args.%s",
                                                       info->outtemplate, alias);

            if (qemuNbdkitInitStorageSource(info->nbdkitcaps, backing, statedir,
                                            alias, 101, 101)) {
                qemuDomainStorageSourcePrivate *srcPriv =
                    qemuDomainStorageSourcePrivateFetch(backing);
                g_autoptr(virCommand) cmd = NULL;
                g_autoptr(virCommandDryRunToken) dryRunToken = virCommandDryRunTokenNew();
                g_auto(virBuffer) buf = VIR_BUFFER_INITIALIZER;
                g_autofree char *actualCmdline = NULL;
                virCommandSendBuffer *sendbuffers;
                int nsendbuffers;
                size_t j;

                if (srcPriv->nbdkitProcess == NULL)
                    continue;

                virCommandSetDryRun(dryRunToken, &buf, true, true, NULL, NULL);
                cmd = qemuNbdkitProcessBuildCommand(srcPriv->nbdkitProcess);

                if (virCommandRun(cmd, NULL) < 0) {
                    ret = -1;
                    continue;
                }
                virCommandPeekSendBuffers(cmd, &sendbuffers, &nsendbuffers);

                if (!(actualCmdline = virBufferContentAndReset(&buf))) {
                    ret = -1;
                    continue;
                }

                if (virTestCompareToFileFull(actualCmdline, cmdfile, false) < 0)
                    ret = -1;

                for (j = 0; j < nsendbuffers; j++) {
                    virCommandSendBuffer *buffer = &sendbuffers[j];
                    g_autofree char *pipefile = g_strdup_printf("%s.pipe.%i",
                                                                cmdfile,
                                                                buffer->fd);

                    if (virTestCompareToFile((const char*)buffer->buffer, pipefile) < 0)
                        ret = -1;
                }
            } else {
                if (virFileExists(cmdfile)) {
                    virReportError(VIR_ERR_INTERNAL_ERROR, "%s",
                                   "qemuNbdkitInitStorageSource() was not expected to fail");
                    ret = -1;
                }
            }
        }
    }

    if (info->expectFail) {
        if (ret == 0) {
            ret = -1;
            VIR_TEST_DEBUG("Error expected but there wasn't any.");
        } else {
            ret = 0;
        }
    }
    return ret;
}

static int
mymain(void)
{
    g_autoptr(GHashTable) capslatest = testQemuGetLatestCaps();
    g_autoptr(GHashTable) capscache = virHashNew(virObjectUnref);
    int ret = 0;

    if (qemuTestDriverInit(&driver) < 0)
        return EXIT_FAILURE;

    if (testQemuInsertRealCaps(driver.qemuCapsCache, "x86_64", "latest", "",
                               capslatest, capscache, NULL, NULL) < 0) {
        ret = -1;
        goto cleanup;
    }

#define DO_TEST_FULL(_name, ...) \
    do { \
        TestInfo info = { \
            .name = _name, \
            .nbdkitcaps = qemuNbdkitCapsNew(TEST_NBDKIT_PATH), \
        }; \
        testInfoSetPaths(&info); \
        testInfoSetArgs(&info, __VA_ARGS__); \
        virTestRunLog(&ret, "nbdkit " _name, testNbdkit, &info); \
        testInfoClear(&info); \
    } while (0)

#define DO_TEST(_name, ...) \
    DO_TEST_FULL(_name, NBDKIT_ARG_CAPS, __VA_ARGS__, QEMU_NBDKIT_CAPS_LAST, NBDKIT_ARG_END)

#define DO_TEST_FAILURE(_name, ...) \
    DO_TEST_FULL(_name, \
                 NBDKIT_ARG_EXPECT_FAIL, 1, \
                 NBDKIT_ARG_CAPS, __VA_ARGS__, QEMU_NBDKIT_CAPS_LAST, NBDKIT_ARG_END)

#define DO_TEST_NOCAPS(_name) \
    DO_TEST_FULL(_name, NBDKIT_ARG_END)

    DO_TEST("disk-cdrom-network", QEMU_NBDKIT_CAPS_PLUGIN_CURL);
    DO_TEST("disk-network-http", QEMU_NBDKIT_CAPS_PLUGIN_CURL);
    DO_TEST("disk-network-source-curl-nbdkit-backing", QEMU_NBDKIT_CAPS_PLUGIN_CURL);
    DO_TEST("disk-network-source-curl", QEMU_NBDKIT_CAPS_PLUGIN_CURL);
    DO_TEST("disk-network-ssh", QEMU_NBDKIT_CAPS_PLUGIN_SSH);
    DO_TEST("disk-network-ssh-password", QEMU_NBDKIT_CAPS_PLUGIN_SSH);
    DO_TEST("disk-network-ssh-key", QEMU_NBDKIT_CAPS_PLUGIN_SSH);

 cleanup:
    qemuTestDriverFree(&driver);

    return ret == 0 ? EXIT_SUCCESS : EXIT_FAILURE;
}

VIR_TEST_MAIN(mymain)