/*
 * Copyright (C) Red Hat, Inc. 2014
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library.  If not, see
 * <http://www.gnu.org/licenses/>.
 */

#include <config.h>

#include "testutils.h"
#include "domain_capabilities.h"
#include "virfilewrapper.h"
#include "configmake.h"


#define VIR_FROM_THIS VIR_FROM_NONE

#if WITH_QEMU || WITH_BHYVE
static int G_GNUC_NULL_TERMINATED
fillStringValues(virDomainCapsStringValues *values, ...)
{
    va_list list;
    const char *str;

    va_start(list, values);
    while ((str = va_arg(list, const char *))) {
        VIR_REALLOC_N(values->values, values->nvalues + 1);
        values->values[values->nvalues] = g_strdup(str);
        values->nvalues++;
    }
    va_end(list);

    return 0;
}
#endif /* WITH_QEMU || WITH_BHYVE */

#if WITH_QEMU
# include "testutilsqemu.h"
# include "testutilshostcpus.h"

static int
fakeHostCPU(virArch arch)
{
    g_autoptr(virCPUDef) cpu = NULL;

    if (!(cpu = testUtilsHostCpusGetDefForArch(arch))) {
        virReportError(VIR_ERR_INTERNAL_ERROR,
                       "cannot fake host CPU for arch %s",
                       virArchToString(arch));
        return -1;
    }

    qemuTestSetHostCPU(NULL, arch, cpu);

    return 0;
}

static int
fillQemuCaps(virDomainCaps *domCaps,
             const char *name,
             const char *arch,
             const char *variant,
             const char *machine,
             virQEMUDriverConfig *cfg)
{
    g_autofree char *path = NULL;
    g_autoptr(virQEMUCaps) qemuCaps = NULL;
    virDomainCapsLoader *loader = &domCaps->os.loader;
    virDomainVirtType virtType;

    if (fakeHostCPU(domCaps->arch) < 0)
        return -1;

    path = g_strdup_printf("%s/%s_%s%s.xml", TEST_QEMU_CAPS_PATH, name, arch, variant);
    if (!(qemuCaps = qemuTestParseCapabilitiesArch(domCaps->arch, path)))
        return -1;

    if (virQEMUCapsGet(qemuCaps, QEMU_CAPS_KVM))
        virtType = VIR_DOMAIN_VIRT_KVM;
    else
        virtType = VIR_DOMAIN_VIRT_QEMU;

    if (machine) {
        VIR_FREE(domCaps->machine);
        domCaps->machine = g_strdup(virQEMUCapsGetCanonicalMachine(qemuCaps, virtType, machine));
    }

    if (!domCaps->machine)
        domCaps->machine = g_strdup(virQEMUCapsGetPreferredMachine(qemuCaps, virtType));

    if (virQEMUCapsFillDomainCaps(cfg,
                                  qemuCaps, domCaps->arch, domCaps,
                                  false) < 0)
        return -1;

    /* The function above tries to query host's VFIO capabilities by calling
     * virHostdevHostSupportsPassthroughVFIO() which, however, can't be
     * successfully mocked as they are not exposed as internal APIs. Therefore,
     * instead of mocking set the expected values here by hand. */
    VIR_DOMAIN_CAPS_ENUM_SET(domCaps->hostdev.pciBackend,
                             VIR_DEVICE_HOSTDEV_PCI_DRIVER_NAME_DEFAULT,
                             VIR_DEVICE_HOSTDEV_PCI_DRIVER_NAME_VFIO);

    /* As of f05b6a918e28 we are expecting to see OVMF_CODE.fd file which
     * may not exists everywhere. */
    while (loader->values.nvalues)
        VIR_FREE(loader->values.values[--loader->values.nvalues]);

    if (fillStringValues(&loader->values,
                         "/obviously/fake/firmware1.fd",
                         "/obviously/fake/firmware2.fd",
                         NULL) < 0)
        return -1;

    return 0;
}


#endif /* WITH_QEMU */


#ifdef WITH_LIBXL
# include "testutilsxen.h"

static int
fillXenCaps(virDomainCaps *domCaps)
{
    g_autoptr(virFirmware) fw_hvmloader = g_new0(virFirmware, 1);
    g_autoptr(virFirmware) fw_ovmf = g_new0(virFirmware, 1);
    virFirmware *firmwares[] = { fw_hvmloader, fw_ovmf };

    firmwares[0]->name = g_strdup("/usr/lib/xen/boot/hvmloader");
    firmwares[1]->name = g_strdup("/usr/lib/xen/boot/ovmf.bin");

    if (libxlMakeDomainCapabilities(domCaps, firmwares, 2) < 0)
        return -1;

    return 0;
}
#endif /* WITH_LIBXL */

#ifdef WITH_BHYVE
# include "bhyve/bhyve_capabilities.h"

static int
fillBhyveCaps(virDomainCaps *domCaps, unsigned int *bhyve_caps)
{
    g_autofree virDomainCapsStringValues *firmwares = NULL;

    firmwares = g_new0(virDomainCapsStringValues, 1);

    if (fillStringValues(firmwares, "/foo/bar", "/foo/baz", NULL) < 0)
        return -1;

    if (virBhyveDomainCapsFill(domCaps, *bhyve_caps, firmwares) < 0)
        return -1;

    return 0;
}
#endif /* WITH_BHYVE */

enum testCapsType {
    CAPS_NONE,
    CAPS_QEMU,
    CAPS_LIBXL,
    CAPS_BHYVE,
};

struct testData {
    const char *name;
    const char *emulator;
    const char *machine;
    const char *arch;
    const char *variant;
    virDomainVirtType type;
    enum testCapsType capsType;
    const char *capsName;
    void *capsOpaque;
};

static int
test_virDomainCapsFormat(const void *opaque)
{
    const struct testData *data = opaque;
    g_autoptr(virDomainCaps) domCaps = NULL;
    g_autofree char *path = NULL;
    g_autofree char *domCapsXML = NULL;

    path = g_strdup_printf("%s/domaincapsdata/%s.xml", abs_srcdir, data->name);

    if (!(domCaps = virDomainCapsNew(data->emulator, data->machine,
                                     virArchFromString(data->arch),
                                     data->type)))
        return -1;

    switch (data->capsType) {
    case CAPS_NONE:
        break;

    case CAPS_QEMU:
#if WITH_QEMU
        if (fillQemuCaps(domCaps, data->capsName, data->arch, data->variant,
                         data->machine, data->capsOpaque) < 0)
            return -1;
#endif
        break;

    case CAPS_LIBXL:
#if WITH_LIBXL
        if (fillXenCaps(domCaps) < 0)
            return -1;
#endif
        break;
    case CAPS_BHYVE:
#if WITH_BHYVE
        if (fillBhyveCaps(domCaps, data->capsOpaque) < 0)
            return -1;
#endif
        break;
    }

    if (!(domCapsXML = virDomainCapsFormat(domCaps)))
        return -1;

    if (virTestCompareToFile(domCapsXML, path) < 0)
        return -1;

    return 0;
}


#if WITH_QEMU

static int
doTestQemuInternal(const char *version,
                   const char *machine,
                   const char *arch,
                   const char *variant,
                   virDomainVirtType type,
                   void *opaque)
{
    g_autofree char *name = NULL;
    g_autofree char *capsName = g_strdup_printf("caps_%s", version);
    g_autofree char *emulator = g_strdup_printf("/usr/bin/qemu-system-%s", arch);
    const char *typestr = NULL;
    g_autofree char *mach = NULL;
    int rc;
    struct testData data = {
        .emulator = emulator,
        .machine = machine,
        .arch = arch,
        .variant = variant,
        .type = type,
        .capsType = CAPS_QEMU,
        .capsName = capsName,
        .capsOpaque = opaque,
    };

    switch ((unsigned int) type) {
    case VIR_DOMAIN_VIRT_QEMU:
        typestr = "-tcg";
        break;

    case VIR_DOMAIN_VIRT_KVM:
        typestr = "";
        break;

    case VIR_DOMAIN_VIRT_HVF:
        typestr = "-hvf";
        break;

    default:
        abort();
        break;
    }

    if (machine)
        mach = g_strdup_printf("-%s", machine);
    else
        mach = g_strdup("");

    data.name = name = g_strdup_printf("qemu_%s%s%s.%s%s",
                                       version, typestr, mach, arch, variant);

    if (STRPREFIX(version, "3.") ||
        STRPREFIX(version, "4.") ||
        STRPREFIX(version, "5.")) {
        g_setenv(TEST_TPM_ENV_VAR, TPM_VER_1_2, true);
    } else if (STRPREFIX(version, "6.")) {
        g_setenv(TEST_TPM_ENV_VAR, TPM_VER_1_2 TPM_VER_2_0, true);
    } else {
        g_setenv(TEST_TPM_ENV_VAR, TPM_VER_2_0, true);
    }

    rc = virTestRun(name, test_virDomainCapsFormat, &data);

    g_unsetenv(TEST_TPM_ENV_VAR);

    if (rc < 0)
        return -1;

    return 0;
}

static int
doTestQemu(const char *inputDir G_GNUC_UNUSED,
           const char *prefix G_GNUC_UNUSED,
           const char *version,
           const char *arch,
           const char *variant,
           const char *suffix G_GNUC_UNUSED,
           void *opaque)
{
    bool hvf = false;
    int ret = 0;

    if (STREQ(variant, "+hvf"))
        hvf = true;

    if (STREQ(arch, "x86_64")) {
        /* For x86_64 based on the test variant we test:
         *
         *   '' (default) variant or any other non-listed variant:
         *      - KVM with default machine
         *      - KVM with Q35 machine
         *  '+hvf' variant:
         *      - hvf with default machine
         *
         *   - TCG with default machine
         */
        if (hvf) {
            if (doTestQemuInternal(version, NULL, arch, variant,
                                   VIR_DOMAIN_VIRT_HVF, opaque) < 0)
                ret = -1;
        } else {
            if (doTestQemuInternal(version, NULL, arch, variant,
                                   VIR_DOMAIN_VIRT_KVM, opaque) < 0)
                ret = -1;

            if (doTestQemuInternal(version, "q35", arch, variant,
                                   VIR_DOMAIN_VIRT_KVM, opaque) < 0)
                ret = -1;
        }

        if (doTestQemuInternal(version, NULL, arch, variant,
                               VIR_DOMAIN_VIRT_QEMU, opaque) < 0)
            ret = -1;
    } else if (STREQ(arch, "aarch64")) {
        /* For aarch64 based on the test variant we test:
         *
         *   '' (default) variant or any other non-listed variant:
         *      - KVM with default machine
         *      - KVM with virt machine
         *
         *  '+hvf' variant:
         *    - hvf with default machine
         */
        if (hvf) {
            if (doTestQemuInternal(version, NULL, arch, variant,
                                   VIR_DOMAIN_VIRT_HVF, opaque) < 0)
                ret = -1;
        } else {
            if (doTestQemuInternal(version, NULL, arch, variant,
                                   VIR_DOMAIN_VIRT_KVM, opaque) < 0)
                ret = -1;

            if (doTestQemuInternal(version, "virt", arch, variant,
                                   VIR_DOMAIN_VIRT_KVM, opaque) < 0)
                ret = -1;
        }
    } else if (STRPREFIX(arch, "riscv") || STRPREFIX(arch, "loongarch64")) {
        /* For riscv64 or loongarch64 we test two combinations for any variant:
         * ('+hvf' is not applicable for this case)
         *
         *   - KVM with virt machine
         *   - TCG with virt machine
         */
        if (doTestQemuInternal(version, "virt", arch, variant,
                               VIR_DOMAIN_VIRT_KVM, opaque) < 0)
            ret = -1;

        if (doTestQemuInternal(version, "virt", arch, variant,
                               VIR_DOMAIN_VIRT_QEMU, opaque) < 0)
            ret = -1;
    } else {
        if (doTestQemuInternal(version, NULL, arch, variant,
                               VIR_DOMAIN_VIRT_KVM, opaque) < 0)
            ret = -1;
    }

    return ret;
}

#endif

static int
mymain(void)
{
    int ret = 0;

#if WITH_BHYVE
    unsigned int bhyve_caps = 0;
#endif

#if WITH_QEMU
    g_autoptr(virQEMUDriverConfig) cfg = virQEMUDriverConfigNew(false, NULL);

    if (!cfg)
        return EXIT_FAILURE;
#endif

#define DO_TEST(Name, Emulator, Machine, Arch, Type, CapsType) \
    do { \
        struct testData data = { \
            .name = Name, \
            .emulator = Emulator, \
            .machine = Machine, \
            .arch = Arch, \
            .variant = "", \
            .type = Type, \
            .capsType = CapsType, \
        }; \
        if (virTestRun(Name, test_virDomainCapsFormat, &data) < 0) \
            ret = -1; \
    } while (0)

#define DO_TEST_LIBXL(Name, Emulator, Machine, Arch, Type) \
    do { \
        struct testData data = { \
            .name = Name, \
            .emulator = Emulator, \
            .machine = Machine, \
            .arch = Arch, \
            .variant = "", \
            .type = Type, \
            .capsType = CAPS_LIBXL, \
        }; \
        if (virTestRun(Name, test_virDomainCapsFormat, &data) < 0) \
            ret = -1; \
    } while (0)

#define DO_TEST_BHYVE(Name, Emulator, BhyveCaps, Type) \
    do { \
        g_autofree char *name = NULL; \
        struct testData data; \
        name = g_strdup_printf("bhyve_%s.x86_64", Name); \
        data = (struct testData) { \
            .name = name, \
            .emulator = Emulator, \
            .arch = "x86_64", \
            .variant = "", \
            .type = Type, \
            .capsType = CAPS_BHYVE, \
            .capsOpaque = BhyveCaps, \
        }; \
        if (virTestRun(name, test_virDomainCapsFormat, &data) < 0) \
            ret = -1; \
    } while (0)

    DO_TEST("empty", "/bin/emulatorbin", "my-machine-type",
            "x86_64", VIR_DOMAIN_VIRT_KVM, CAPS_NONE);

#if WITH_QEMU

    virFileWrapperAddPrefix(SYSCONFDIR "/qemu/firmware",
                            abs_srcdir "/qemufirmwaredata/etc/qemu/firmware");
    virFileWrapperAddPrefix(PREFIX "/share/qemu/firmware",
                            abs_srcdir "/qemufirmwaredata/usr/share/qemu/firmware");
    virFileWrapperAddPrefix("/home/user/.config/qemu/firmware",
                            abs_srcdir "/qemufirmwaredata/home/user/.config/qemu/firmware");

    if (testQemuCapsIterate(".xml", doTestQemu, cfg) < 0)
        ret = -1;

    /*
     * Run "tests/qemucapsprobe /path/to/qemu/binary >foo.replies"
     * to generate updated or new *.replies data files.
     *
     * If you manually edit replies files or want to edit them programmaticaly
     * use scripts/qemu-replies-tool --regenerate
     *
     * Once a replies file has been generated and tweaked if necessary,
     * you can drop it into tests/qemucapabilitiesdata/ (with a sensible
     * name - look at what's already there for inspiration) and test
     * programs will automatically pick it up.
     *
     * To generate the corresponding output files after a new replies
     * file has been added, run "VIR_TEST_REGENERATE_OUTPUT=1 ninja test".
     */

    virFileWrapperClearPrefixes();

#endif /* WITH_QEMU */

#if WITH_LIBXL

    DO_TEST_LIBXL("libxl-xenpv", "/usr/bin/qemu-system-x86_64",
                  "xenpv", "x86_64", VIR_DOMAIN_VIRT_XEN);
    DO_TEST_LIBXL("libxl-xenfv", "/usr/bin/qemu-system-x86_64",
                  "xenfv", "x86_64", VIR_DOMAIN_VIRT_XEN);

#endif /* WITH_LIBXL */

#if WITH_BHYVE
    DO_TEST_BHYVE("basic", "/usr/sbin/bhyve", &bhyve_caps, VIR_DOMAIN_VIRT_BHYVE);

    bhyve_caps |= BHYVE_CAP_LPC_BOOTROM;
    DO_TEST_BHYVE("uefi", "/usr/sbin/bhyve", &bhyve_caps, VIR_DOMAIN_VIRT_BHYVE);

    bhyve_caps |= BHYVE_CAP_FBUF;
    DO_TEST_BHYVE("fbuf", "/usr/sbin/bhyve", &bhyve_caps, VIR_DOMAIN_VIRT_BHYVE);
#endif /* WITH_BHYVE */

    return ret == 0 ? EXIT_SUCCESS : EXIT_FAILURE;
}

#if WITH_QEMU
VIR_TEST_MAIN_PRELOAD(mymain,
                      VIR_TEST_MOCK("domaincaps"),
                      VIR_TEST_MOCK("qemucpu"))
#else
VIR_TEST_MAIN_PRELOAD(mymain, VIR_TEST_MOCK("domaincaps"))
#endif
