/*
 * Validate -readconfig
 *
 * Copyright (c) 2022 Red Hat, Inc.
 *
 * This work is licensed under the terms of the GNU GPL, version 2 or later.
 * See the COPYING file in the top-level directory.
 */

#include "qemu/osdep.h"
#include "libqtest.h"
#include "qapi/error.h"
#include "qapi/qapi-visit-machine.h"
#include "qapi/qapi-visit-qom.h"
#include "qapi/qapi-visit-ui.h"
#include "qapi/qmp/qdict.h"
#include "qapi/qmp/qlist.h"
#include "qapi/qobject-input-visitor.h"
#include "qapi/qmp/qstring.h"
#include "qemu/units.h"

static QTestState *qtest_init_with_config(const char *cfgdata)
{
    GError *error = NULL;
    g_autofree char *args = NULL;
    int cfgfd = -1;
    g_autofree char *cfgpath = NULL;
    QTestState *qts;
    ssize_t ret;

    cfgfd = g_file_open_tmp("readconfig-test-XXXXXX", &cfgpath, &error);
    g_assert_no_error(error);
    g_assert_cmpint(cfgfd, >=, 0);

    ret = qemu_write_full(cfgfd, cfgdata, strlen(cfgdata));
    close(cfgfd);
    if (ret < 0) {
        unlink(cfgpath);
    }
    g_assert_cmpint(ret, ==, strlen(cfgdata));

    args = g_strdup_printf("-nodefaults -machine none -readconfig %s", cfgpath);

    qts = qtest_init(args);

    unlink(cfgpath);

    return qts;
}

static void test_x86_memdev_resp(QObject *res, const char *mem_id, int size)
{
    Visitor *v;
    g_autoptr(MemdevList) memdevs = NULL;
    Memdev *memdev;

    g_assert(res);
    v = qobject_input_visitor_new(res);
    visit_type_MemdevList(v, NULL, &memdevs, &error_abort);

    g_assert(memdevs);
    g_assert(memdevs->value);
    g_assert(!memdevs->next);

    memdev = memdevs->value;
    g_assert_cmpstr(memdev->id, ==, mem_id);
    g_assert_cmpint(memdev->size, ==, size * MiB);

    visit_free(v);
}

static void test_x86_memdev(void)
{
    QDict *resp;
    QTestState *qts;
    const char *cfgdata =
        "[memory]\n"
        "size = \"200\"";

    qts = qtest_init_with_config(cfgdata);
    /* Test valid command */
    resp = qtest_qmp(qts, "{ 'execute': 'query-memdev' }");
    test_x86_memdev_resp(qdict_get(resp, "return"), "ram", 200);
    qobject_unref(resp);

    qtest_quit(qts);
}

/* FIXME: The test is currently broken on FreeBSD */
#if defined(CONFIG_SPICE) && !defined(__FreeBSD__)
static void test_spice_resp(QObject *res)
{
    Visitor *v;
    g_autoptr(SpiceInfo) spice = NULL;

    g_assert(res);
    v = qobject_input_visitor_new(res);
    visit_type_SpiceInfo(v, "spice", &spice, &error_abort);

    g_assert(spice);
    g_assert(spice->enabled);

    visit_free(v);
}

static void test_spice(void)
{
    QDict *resp;
    QTestState *qts;
    const char *cfgdata =
        "[spice]\n"
#ifndef WIN32
        "unix = \"on\"\n"
#endif
        "disable-ticketing = \"on\"\n";

    qts = qtest_init_with_config(cfgdata);
    /* Test valid command */
    resp = qtest_qmp(qts, "{ 'execute': 'query-spice' }");
    test_spice_resp(qdict_get(resp, "return"));
    qobject_unref(resp);

    qtest_quit(qts);
}
#endif

static void test_object_available(QObject *res, const char *name,
                                  const char *type)
{
    Visitor *v;
    g_autoptr(ObjectPropertyInfoList) objs = NULL;
    ObjectPropertyInfoList *tmp;
    ObjectPropertyInfo *obj;
    bool object_available = false;
    g_autofree char *childtype = g_strdup_printf("child<%s>", type);

    g_assert(res);
    v = qobject_input_visitor_new(res);
    visit_type_ObjectPropertyInfoList(v, NULL, &objs, &error_abort);

    g_assert(objs);
    tmp = objs;
    while (tmp) {
        g_assert(tmp->value);

        obj = tmp->value;
        if (g_str_equal(obj->name, name) && g_str_equal(obj->type, childtype)) {
            object_available = true;
            break;
        }

        tmp = tmp->next;
    }

    g_assert(object_available);

    visit_free(v);
}

static void test_object_rng(void)
{
    QDict *resp;
    QTestState *qts;
    const char *cfgdata =
        "[object]\n"
        "qom-type = \"rng-builtin\"\n"
        "id = \"rng0\"\n";

    qts = qtest_init_with_config(cfgdata);
    /* Test valid command */
    resp = qtest_qmp(qts,
                     "{ 'execute': 'qom-list',"
                     "  'arguments': {'path': '/objects' }}");
    test_object_available(qdict_get(resp, "return"), "rng0", "rng-builtin");
    qobject_unref(resp);

    qtest_quit(qts);
}

static void test_docs_config_ich9(void)
{
    QTestState *qts;
    QDict *resp;
    QObject *qobj;

    qts = qtest_initf("-nodefaults -readconfig docs/config/ich9-ehci-uhci.cfg");

    resp = qtest_qmp(qts, "{ 'execute': 'qom-list',"
                          "  'arguments': {'path': '/machine/peripheral' }}");
    qobj = qdict_get(resp, "return");
    test_object_available(qobj, "ehci", "ich9-usb-ehci1");
    test_object_available(qobj, "uhci-1", "ich9-usb-uhci1");
    test_object_available(qobj, "uhci-2", "ich9-usb-uhci2");
    test_object_available(qobj, "uhci-3", "ich9-usb-uhci3");
    qobject_unref(resp);

    qtest_quit(qts);
}

#if defined(CONFIG_POSIX) && defined(CONFIG_SLIRP)

static char *make_temp_img(const char *template, const char *format, int size)
{
    GError *error = NULL;
    char *temp_name;
    int fd;

    /* Create a temporary image names */
    fd = g_file_open_tmp(template, &temp_name, &error);
    if (fd == -1) {
        fprintf(stderr, "unable to create file: %s\n", error->message);
        g_error_free(error);
        return NULL;
    }
    close(fd);

    if (!mkimg(temp_name, format, size)) {
        fprintf(stderr, "qemu-img failed to create %s\n", temp_name);
        g_free(temp_name);
        return NULL;
    }

    return temp_name;
}

struct device {
    const char *name;
    const char *type;
};

static void test_docs_q35(const char *input_file, struct device *devices)
{
    QTestState *qts;
    QDict *resp;
    QObject *qobj;
    int ret, i;
    g_autofree char *cfg_file = NULL, *sedcmd = NULL;
    g_autofree char *hd_file = NULL, *cd_file = NULL;

    /* Check that all the devices are available in the QEMU binary */
    for (i = 0; devices[i].name; i++) {
        if (!qtest_has_device(devices[i].type)) {
            g_test_skip("one of the required devices is not available");
            return;
        }
    }

    hd_file = make_temp_img("qtest_disk_XXXXXX.qcow2", "qcow2", 1);
    cd_file = make_temp_img("qtest_cdrom_XXXXXX.iso", "raw", 1);
    if (!hd_file || !cd_file) {
        g_test_skip("could not create disk images");
        goto cleanup;
    }

    /* Create a temporary config file where we replace the disk image names */
    ret = g_file_open_tmp("q35-emulated-XXXXXX.cfg", &cfg_file, NULL);
    if (ret == -1) {
        g_test_skip("could not create temporary config file");
        goto cleanup;
    }
    close(ret);

    sedcmd = g_strdup_printf("sed -e 's,guest.qcow2,%s,' -e 's,install.iso,%s,'"
                             " %s %s > '%s'",
                             hd_file, cd_file,
                             !qtest_has_accel("kvm") ? "-e '/accel/d'" : "",
                             input_file, cfg_file);
    ret = system(sedcmd);
    if (ret) {
        g_test_skip("could not modify temporary config file");
        goto cleanup;
    }

    qts = qtest_initf("-machine none -nodefaults -readconfig %s", cfg_file);

    /* Check memory size */
    resp = qtest_qmp(qts, "{ 'execute': 'query-memdev' }");
    test_x86_memdev_resp(qdict_get(resp, "return"), "pc.ram", 1024);
    qobject_unref(resp);

    resp = qtest_qmp(qts, "{ 'execute': 'qom-list',"
                          "  'arguments': {'path': '/machine/peripheral' }}");
    qobj = qdict_get(resp, "return");

    /* Check that all the devices have been created */
    for (i = 0; devices[i].name; i++) {
        test_object_available(qobj, devices[i].name, devices[i].type);
    }

    qobject_unref(resp);

    qtest_quit(qts);

cleanup:
    if (hd_file) {
        unlink(hd_file);
    }
    if (cd_file) {
        unlink(cd_file);
    }
    if (cfg_file) {
        unlink(cfg_file);
    }
}

static void test_docs_q35_emulated(void)
{
    struct device devices[] = {
        { "ich9-pcie-port-1", "ioh3420" },
        { "ich9-pcie-port-2", "ioh3420" },
        { "ich9-pcie-port-3", "ioh3420" },
        { "ich9-pcie-port-4", "ioh3420" },
        { "ich9-pci-bridge", "i82801b11-bridge" },
        { "ich9-ehci-1", "ich9-usb-ehci1" },
        { "ich9-ehci-2", "ich9-usb-ehci2" },
        { "ich9-uhci-1", "ich9-usb-uhci1" },
        { "ich9-uhci-2", "ich9-usb-uhci2" },
        { "ich9-uhci-3", "ich9-usb-uhci3" },
        { "ich9-uhci-4", "ich9-usb-uhci4" },
        { "ich9-uhci-5", "ich9-usb-uhci5" },
        { "ich9-uhci-6", "ich9-usb-uhci6" },
        { "sata-disk", "ide-hd" },
        { "sata-optical-disk", "ide-cd" },
        { "net", "e1000" },
        { "video", "VGA" },
        { "ich9-hda-audio", "ich9-intel-hda" },
        { "ich9-hda-duplex", "hda-duplex" },
        { NULL, NULL }
    };

    test_docs_q35("docs/config/q35-emulated.cfg", devices);
}

static void test_docs_q35_virtio_graphical(void)
{
    struct device devices[] = {
        { "pcie.1", "pcie-root-port" },
        { "pcie.2", "pcie-root-port" },
        { "pcie.3", "pcie-root-port" },
        { "pcie.4", "pcie-root-port" },
        { "pcie.5", "pcie-root-port" },
        { "pcie.6", "pcie-root-port" },
        { "pcie.7", "pcie-root-port" },
        { "pcie.8", "pcie-root-port" },
        { "scsi", "virtio-scsi-pci" },
        { "scsi-disk", "scsi-hd" },
        { "scsi-optical-disk", "scsi-cd" },
        { "net", "virtio-net-pci" },
        { "usb", "nec-usb-xhci" },
        { "tablet", "usb-tablet" },
        { "video", "qxl-vga" },
        { "sound", "ich9-intel-hda" },
        { "duplex", "hda-duplex" },
        { NULL, NULL }
    };

    test_docs_q35("docs/config/q35-virtio-graphical.cfg", devices);
}

static void test_docs_q35_virtio_serial(void)
{
    struct device devices[] = {
        { "pcie.1", "pcie-root-port" },
        { "pcie.2", "pcie-root-port" },
        { "pcie.3", "pcie-root-port" },
        { "pcie.4", "pcie-root-port" },
        { "pcie.5", "pcie-root-port" },
        { "pcie.6", "pcie-root-port" },
        { "pcie.7", "pcie-root-port" },
        { "pcie.8", "pcie-root-port" },
        { "scsi", "virtio-scsi-pci" },
        { "scsi-disk", "scsi-hd" },
        { "scsi-optical-disk", "scsi-cd" },
        { "net", "virtio-net-pci" },
        { NULL, NULL }
    };

    test_docs_q35("docs/config/q35-virtio-serial.cfg", devices);
}

#endif /* CONFIG_LINUX */

int main(int argc, char *argv[])
{
    const char *arch;
    g_test_init(&argc, &argv, NULL);

    arch = qtest_get_arch();

    if (g_str_equal(arch, "i386") ||
        g_str_equal(arch, "x86_64")) {
        qtest_add_func("readconfig/x86/memdev", test_x86_memdev);
        if (qtest_has_device("ich9-usb-ehci1") &&
            qtest_has_device("ich9-usb-uhci1")) {
            qtest_add_func("readconfig/x86/ich9-ehci-uhci", test_docs_config_ich9);
        }
#if defined(CONFIG_POSIX) && defined(CONFIG_SLIRP)
        qtest_add_func("readconfig/x86/q35-emulated", test_docs_q35_emulated);
        qtest_add_func("readconfig/x86/q35-virtio-graphical",
                       test_docs_q35_virtio_graphical);
        if (g_test_slow()) {
            /*
             * q35-virtio-serial.cfg is a subset of q35-virtio-graphical.cfg,
             * so we can skip the test in quick mode
             */
            qtest_add_func("readconfig/x86/q35-virtio-serial",
                           test_docs_q35_virtio_serial);
        }
#endif
    }
#if defined(CONFIG_SPICE) && !defined(__FreeBSD__)
    qtest_add_func("readconfig/spice", test_spice);
#endif

    qtest_add_func("readconfig/object-rng", test_object_rng);

    return g_test_run();
}
