/*
 * QTest QMP helpers for migration
 *
 * Copyright (c) 2016-2018 Red Hat, Inc. and/or its affiliates
 *   based on the vhost-user-test.c that is:
 *      Copyright (c) 2014 Virtual Open Systems Sarl.
 *
 * 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 "migration-qmp.h"
#include "migration-util.h"
#include "qapi/error.h"
#include "qapi/qapi-types-migration.h"
#include "qapi/qapi-visit-migration.h"
#include "qobject/qdict.h"
#include "qobject/qjson.h"
#include "qobject/qlist.h"
#include "qapi/qobject-input-visitor.h"
#include "qapi/qobject-output-visitor.h"

/*
 * Number of seconds we wait when looking for migration
 * status changes, to avoid test suite hanging forever
 * when things go wrong. Needs to be higher enough to
 * avoid false positives on loaded hosts.
 */
#define MIGRATION_STATUS_WAIT_TIMEOUT 120

/*
 * Wait for a "MIGRATION" event.  This is what Libvirt uses to track
 * migration status changes.
 */
void migration_event_wait(QTestState *s, const char *target)
{
    QDict *response, *data;
    const char *status;
    bool found;

    do {
        response = qtest_qmp_eventwait_ref(s, "MIGRATION");
        data = qdict_get_qdict(response, "data");
        g_assert(data);
        status = qdict_get_str(data, "status");
        found = (strcmp(status, target) == 0);
        qobject_unref(response);
    } while (!found);
}

/*
 * Convert a string representing a single channel to an object.
 * @str may be in JSON or dotted keys format.
 */
QObject *migrate_str_to_channel(const char *str)
{
    Visitor *v;
    MigrationChannel *channel;
    QObject *obj;

    /* Create the channel */
    v = qobject_input_visitor_new_str(str, "channel-type", &error_abort);
    visit_type_MigrationChannel(v, NULL, &channel, &error_abort);
    visit_free(v);

    /* Create the object */
    v = qobject_output_visitor_new(&obj);
    visit_type_MigrationChannel(v, NULL, &channel, &error_abort);
    visit_complete(v, &obj);
    visit_free(v);

    qapi_free_MigrationChannel(channel);
    return obj;
}

void migrate_qmp_fail(QTestState *who, const char *uri,
                      QObject *channels, const char *fmt, ...)
{
    va_list ap;
    QDict *args, *err;

    va_start(ap, fmt);
    args = qdict_from_vjsonf_nofail(fmt, ap);
    va_end(ap);

    g_assert(!qdict_haskey(args, "uri"));
    if (uri) {
        qdict_put_str(args, "uri", uri);
    }

    g_assert(!qdict_haskey(args, "channels"));
    if (channels) {
        qdict_put_obj(args, "channels", channels);
    }

    err = qtest_qmp_assert_failure_ref(
        who, "{ 'execute': 'migrate', 'arguments': %p}", args);

    g_assert(qdict_haskey(err, "desc"));

    qobject_unref(err);
}

/*
 * Send QMP command "migrate".
 * Arguments are built from @fmt... (formatted like
 * qobject_from_jsonf_nofail()) with "uri": @uri spliced in.
 */
void migrate_qmp(QTestState *who, QTestState *to, const char *uri,
                 QObject *channels, const char *fmt, ...)
{
    va_list ap;
    QDict *args;
    g_autofree char *connect_uri = NULL;

    va_start(ap, fmt);
    args = qdict_from_vjsonf_nofail(fmt, ap);
    va_end(ap);

    g_assert(!qdict_haskey(args, "uri"));
    if (uri) {
        qdict_put_str(args, "uri", uri);
    } else if (!channels) {
        connect_uri = migrate_get_connect_uri(to);
        qdict_put_str(args, "uri", connect_uri);
    }

    g_assert(!qdict_haskey(args, "channels"));
    if (channels) {
        QList *channel_list = qobject_to(QList, channels);
        migrate_set_ports(to, channel_list);
        qdict_put_obj(args, "channels", channels);
    }

    qtest_qmp_assert_success(who,
                             "{ 'execute': 'migrate', 'arguments': %p}", args);
}

void migrate_set_capability(QTestState *who, const char *capability,
                            bool value)
{
    qtest_qmp_assert_success(who,
                             "{ 'execute': 'migrate-set-capabilities',"
                             "'arguments': { "
                             "'capabilities': [ { "
                             "'capability': %s, 'state': %i } ] } }",
                             capability, value);
}

void migrate_incoming_qmp(QTestState *to, const char *uri, QObject *channels,
                          const char *fmt, ...)
{
    va_list ap;
    QDict *args, *rsp;

    va_start(ap, fmt);
    args = qdict_from_vjsonf_nofail(fmt, ap);
    va_end(ap);

    g_assert(!qdict_haskey(args, "uri"));
    if (uri) {
        qdict_put_str(args, "uri", uri);
    }

    g_assert(!qdict_haskey(args, "channels"));
    if (channels) {
        qdict_put_obj(args, "channels", channels);
    }

    /* This function relies on the event to work, make sure it's enabled */
    migrate_set_capability(to, "events", true);

    /*
     * Set the incoming migration to never exit QEMU abruptly during
     * the tests. It causes issues when running sanitizers and
     * expecting a failure exit code can mask other issues.
     */
    g_assert(!qdict_haskey(args, "exit-on-error"));
    qdict_put_bool(args, "exit-on-error", false);
    rsp = qtest_qmp(to, "{ 'execute': 'migrate-incoming', 'arguments': %p}",
                    args);

    if (!qdict_haskey(rsp, "return")) {
        g_autoptr(GString) s = qobject_to_json_pretty(QOBJECT(rsp), true);
        g_test_message("%s", s->str);
    }

    g_assert(qdict_haskey(rsp, "return"));
    qobject_unref(rsp);

    migration_event_wait(to, "setup");
}

static bool check_migration_status(QTestState *who, const char *goal,
                                   const char **ungoals)
{
    bool ready;
    char *current_status;
    const char **ungoal;

    current_status = migrate_query_status(who);
    ready = strcmp(current_status, goal) == 0;
    if (!ungoals) {
        g_assert_cmpstr(current_status, !=, "failed");
        /*
         * If looking for a state other than completed,
         * completion of migration would cause the test to
         * hang.
         */
        if (strcmp(goal, "completed") != 0) {
            g_assert_cmpstr(current_status, !=, "completed");
        }
    } else {
        for (ungoal = ungoals; *ungoal; ungoal++) {
            g_assert_cmpstr(current_status, !=,  *ungoal);
        }
    }
    g_free(current_status);
    return ready;
}

void wait_for_migration_status(QTestState *who,
                               const char *goal, const char **ungoals)
{
    g_test_timer_start();
    while (!check_migration_status(who, goal, ungoals)) {
        usleep(1000);

        g_assert(g_test_timer_elapsed() < MIGRATION_STATUS_WAIT_TIMEOUT);
    }
}

void wait_for_migration_complete(QTestState *who)
{
    wait_for_migration_status(who, "completed", NULL);
}

void wait_for_migration_fail(QTestState *from, bool allow_active)
{
    g_test_timer_start();
    QDict *rsp_return;
    char *status;
    bool failed;

    do {
        status = migrate_query_status(from);
        bool result = !strcmp(status, "setup") || !strcmp(status, "failed") ||
            (allow_active && !strcmp(status, "active")) ||
            !strcmp(status, "failing");
        if (!result) {
            fprintf(stderr, "%s: unexpected status status=%s allow_active=%d\n",
                    __func__, status, allow_active);
        }
        g_assert(result);
        failed = !strcmp(status, "failed");
        g_free(status);

        g_assert(g_test_timer_elapsed() < MIGRATION_STATUS_WAIT_TIMEOUT);
    } while (!failed);

    /* Is the machine currently running? */
    rsp_return = qtest_qmp_assert_success_ref(from,
                                              "{ 'execute': 'query-status' }");
    g_assert(qdict_haskey(rsp_return, "running"));
    g_assert(qdict_get_bool(rsp_return, "running"));
    qobject_unref(rsp_return);
}

void wait_for_stop(QTestState *who, QTestMigrationState *state)
{
    if (!state->stop_seen) {
        qtest_qmp_eventwait(who, "STOP");
    }
}

void wait_for_resume(QTestState *who, QTestMigrationState *state)
{
    if (!state->resume_seen) {
        qtest_qmp_eventwait(who, "RESUME");
    }
}

void wait_for_suspend(QTestState *who, QTestMigrationState *state)
{
    if (state->suspend_me && !state->suspend_seen) {
        qtest_qmp_eventwait(who, "SUSPEND");
    }
}

/*
 * Note: caller is responsible to free the returned object via
 * qobject_unref() after use
 */
QDict *migrate_query(QTestState *who)
{
    return qtest_qmp_assert_success_ref(who, "{ 'execute': 'query-migrate' }");
}

QDict *migrate_query_not_failed(QTestState *who)
{
    const char *status;
    QDict *rsp = migrate_query(who);
    status = qdict_get_str(rsp, "status");
    if (g_str_equal(status, "failed")) {
        g_printerr("query-migrate shows failed migration: %s\n",
                   qdict_get_str(rsp, "error-desc"));
    }
    g_assert(!g_str_equal(status, "failed"));
    return rsp;
}

/*
 * Note: caller is responsible to free the returned object via
 * g_free() after use
 */
gchar *migrate_query_status(QTestState *who)
{
    QDict *rsp_return = migrate_query(who);
    gchar *status = g_strdup(qdict_get_str(rsp_return, "status"));

    g_assert(status);
    qobject_unref(rsp_return);

    return status;
}

int64_t read_ram_property_int(QTestState *who, const char *property)
{
    QDict *rsp_return, *rsp_ram;
    int64_t result;

    rsp_return = migrate_query_not_failed(who);
    if (!qdict_haskey(rsp_return, "ram")) {
        /* Still in setup */
        result = 0;
    } else {
        rsp_ram = qdict_get_qdict(rsp_return, "ram");
        result = qdict_get_try_int(rsp_ram, property, 0);
    }
    qobject_unref(rsp_return);
    return result;
}

int64_t read_migrate_property_int(QTestState *who, const char *property)
{
    QDict *rsp_return;
    int64_t result;

    rsp_return = migrate_query_not_failed(who);
    result = qdict_get_try_int(rsp_return, property, 0);
    qobject_unref(rsp_return);
    return result;
}

uint64_t get_migration_pass(QTestState *who)
{
    return read_ram_property_int(who, "dirty-sync-count");
}

void read_blocktime(QTestState *who)
{
    QDict *rsp_return;

    rsp_return = migrate_query_not_failed(who);
    g_assert(qdict_haskey(rsp_return, "postcopy-blocktime"));
    g_assert(qdict_haskey(rsp_return, "postcopy-vcpu-blocktime"));
    g_assert(qdict_haskey(rsp_return, "postcopy-latency"));
    g_assert(qdict_haskey(rsp_return, "postcopy-latency-dist"));
    g_assert(qdict_haskey(rsp_return, "postcopy-vcpu-latency"));
    g_assert(qdict_haskey(rsp_return, "postcopy-non-vcpu-latency"));
    qobject_unref(rsp_return);
}

/*
 * Wait for two changes in the migration pass count, but bail if we stop.
 */
void wait_for_migration_pass(QTestState *who, QTestMigrationState *src_state)
{
    uint64_t pass, prev_pass = 0, changes = 0;

    while (changes < 2 && !src_state->stop_seen && !src_state->suspend_seen) {
        usleep(1000);
        pass = get_migration_pass(who);
        changes += (pass != prev_pass);
        prev_pass = pass;
    }
}

static long long migrate_get_parameter_int(QTestState *who,
                                           const char *parameter)
{
    QDict *rsp;
    long long result;

    rsp = qtest_qmp_assert_success_ref(
        who, "{ 'execute': 'query-migrate-parameters' }");
    result = qdict_get_int(rsp, parameter);
    qobject_unref(rsp);
    return result;
}

static void migrate_check_parameter_int(QTestState *who, const char *parameter,
                                        long long value)
{
    long long result;

    result = migrate_get_parameter_int(who, parameter);
    g_assert_cmpint(result, ==, value);
}

void migrate_set_parameter_int(QTestState *who, const char *parameter,
                               long long value)
{
    qtest_qmp_assert_success(who,
                             "{ 'execute': 'migrate-set-parameters',"
                             "'arguments': { %s: %lld } }",
                             parameter, value);
    migrate_check_parameter_int(who, parameter, value);
}

static char *migrate_get_parameter_str(QTestState *who, const char *parameter)
{
    QDict *rsp;
    char *result;

    rsp = qtest_qmp_assert_success_ref(
        who, "{ 'execute': 'query-migrate-parameters' }");
    result = g_strdup(qdict_get_str(rsp, parameter));
    qobject_unref(rsp);
    return result;
}

static void migrate_check_parameter_str(QTestState *who, const char *parameter,
                                        const char *value)
{
    g_autofree char *result = migrate_get_parameter_str(who, parameter);
    g_assert_cmpstr(result, ==, value);
}

void migrate_set_parameter_str(QTestState *who, const char *parameter,
                               const char *value)
{
    qtest_qmp_assert_success(who,
                             "{ 'execute': 'migrate-set-parameters',"
                             "'arguments': { %s: %s } }",
                             parameter, value);
    migrate_check_parameter_str(who, parameter, value);
}

void migrate_set_parameter_strv(QTestState *who, const char *parameter,
                                char **strv)
{
    g_autofree char *args = g_strjoinv("\",\"", strv);
    g_autoptr(GString) value = g_string_new("");
    g_autofree char *command = NULL;

    g_string_printf(value, "\"%s\"", args);

    command = g_strdup_printf("{ 'execute': 'migrate-set-parameters',"
                              "'arguments': { %%s: [ %s ]}}",
                              value->str);

    qtest_qmp_assert_success(who, command, parameter);
}

void migrate_set_parameter_null(QTestState *who, const char *parameter)
{
    qtest_qmp_assert_success(who,
                             "{ 'execute': 'migrate-set-parameters',"
                             "'arguments': { %s: null } }",
                             parameter);
    migrate_check_parameter_str(who, parameter, "");
}

static long long migrate_get_parameter_bool(QTestState *who,
                                            const char *parameter)
{
    QDict *rsp;
    int result;

    rsp = qtest_qmp_assert_success_ref(
        who, "{ 'execute': 'query-migrate-parameters' }");
    result = qdict_get_bool(rsp, parameter);
    qobject_unref(rsp);
    return !!result;
}

static void migrate_check_parameter_bool(QTestState *who, const char *parameter,
                                         int value)
{
    int result;

    result = migrate_get_parameter_bool(who, parameter);
    g_assert_cmpint(result, ==, value);
}

void migrate_set_parameter_bool(QTestState *who, const char *parameter,
                                int value)
{
    qtest_qmp_assert_success(who,
                             "{ 'execute': 'migrate-set-parameters',"
                             "'arguments': { %s: %i } }",
                             parameter, value);
    migrate_check_parameter_bool(who, parameter, value);
}

void migrate_ensure_non_converge(QTestState *who)
{
    /* Can't converge with 1ms downtime + 3 mbs bandwidth limit */
    migrate_set_parameter_int(who, "max-bandwidth", 3 * 1000 * 1000);
    migrate_set_parameter_int(who, "downtime-limit", 1);
}

void migrate_ensure_converge(QTestState *who)
{
    /* Should converge with 30s downtime + 1 gbs bandwidth limit */
    migrate_set_parameter_int(who, "max-bandwidth", 1 * 1000 * 1000 * 1000);
    migrate_set_parameter_int(who, "downtime-limit", 30 * 1000);
}

void migrate_pause(QTestState *who)
{
    qtest_qmp_assert_success(who, "{ 'execute': 'migrate-pause' }");
}

void migrate_continue(QTestState *who, const char *state)
{
    qtest_qmp_assert_success(who,
                             "{ 'execute': 'migrate-continue',"
                             "  'arguments': { 'state': %s } }",
                             state);
}

void migrate_recover(QTestState *who, const char *uri)
{
    qtest_qmp_assert_success(who,
                             "{ 'exec-oob': 'migrate-recover', "
                             "  'id': 'recover-cmd', "
                             "  'arguments': { 'uri': %s } }",
                             uri);
}

void migrate_cancel(QTestState *who)
{
    qtest_qmp_assert_success(who, "{ 'execute': 'migrate_cancel' }");
}

void migrate_postcopy_start(QTestState *from, QTestState *to,
                            QTestMigrationState *src_state)
{
    qtest_qmp_assert_success(from, "{ 'execute': 'migrate-start-postcopy' }");

    wait_for_stop(from, src_state);
    qtest_qmp_eventwait(to, "RESUME");
}
