#include <config.h>

#include <unistd.h>

#include <sys/types.h>
#include <fcntl.h>

#include "internal.h"
#include "testutils.h"
#include "network_conf.h"
#include "bridge_driver.h"
#define LIBVIRT_BRIDGE_DRIVER_PRIV_H_ALLOW
#include "bridge_driver_priv.h"
#define LIBVIRT_VIRCOMMANDPRIV_H_ALLOW
#include "vircommandpriv.h"

#define VIR_FROM_THIS VIR_FROM_NONE

typedef enum {
    TEST_COMPARE_NET_XML2XML_RESULT_SUCCESS,
    TEST_COMPARE_NET_XML2XML_RESULT_FAIL_PARSE,
    TEST_COMPARE_NET_XML2XML_RESULT_FAIL_VALIDATE,
    TEST_COMPARE_NET_XML2XML_RESULT_FAIL_FORMAT,
    TEST_COMPARE_NET_XML2XML_RESULT_FAIL_COMPARE,
} testCompareNetXML2XMLResult;

struct _testInfo {
    const char *name;
    unsigned int flags;
    testCompareNetXML2XMLResult expectResult;
    virNetworkXMLOption *xmlopt; /* borrowed, immutable */
    dnsmasqCaps *caps;
    virNetworkDef *def;
    char *inxml;
    char *outxml;
    char *outconf;
    char *outhostsfile;
    GHashTable *existingTestCases;
};

typedef struct _testInfo testInfo;
void testInfoFree(testInfo *info);
G_DEFINE_AUTOPTR_CLEANUP_FUNC(testInfo, testInfoFree);

void testInfoFree(testInfo *info)
{
    if (!info)
        return;

    virNetworkDefFree(info->def);
    VIR_FREE(info->inxml);
    VIR_FREE(info->outxml);
    VIR_FREE(info->outconf);
    VIR_FREE(info->outhostsfile);
    VIR_FREE(info);
}

static int
testCompareXMLToXMLFiles(const void *data)
{
    testInfo *info = (void *) data;
    g_autofree char *actual = NULL;
    int ret;
    testCompareNetXML2XMLResult result = TEST_COMPARE_NET_XML2XML_RESULT_SUCCESS;
    g_autoptr(virNetworkDef) def = NULL;

    virTestCaseMarkUsed(info->existingTestCases, info->inxml);
    if (!(def = virNetworkDefParse(NULL, info->inxml, info->xmlopt, false))) {
        result = TEST_COMPARE_NET_XML2XML_RESULT_FAIL_PARSE;
        goto cleanup;
    }
    if (info->expectResult == TEST_COMPARE_NET_XML2XML_RESULT_FAIL_PARSE)
        goto cleanup;

    if (networkValidateTests(def) < 0) {
        result = TEST_COMPARE_NET_XML2XML_RESULT_FAIL_VALIDATE;
        goto cleanup;
    }
    if (info->expectResult == TEST_COMPARE_NET_XML2XML_RESULT_FAIL_VALIDATE)
        goto cleanup;

    if (!(actual = virNetworkDefFormat(def, info->xmlopt, info->flags))) {
        result = TEST_COMPARE_NET_XML2XML_RESULT_FAIL_FORMAT;
        goto cleanup;
    }
    if (info->expectResult == TEST_COMPARE_NET_XML2XML_RESULT_FAIL_FORMAT)
        goto cleanup;

    virTestCaseMarkUsed(info->existingTestCases, info->outxml);
    if (virTestCompareToFile(actual, info->outxml) < 0) {
        result = TEST_COMPARE_NET_XML2XML_RESULT_FAIL_COMPARE;
        goto cleanup;
    }
    if (info->expectResult == TEST_COMPARE_NET_XML2XML_RESULT_FAIL_COMPARE)
        goto cleanup;

 cleanup:
    if (result == info->expectResult) {
        ret = 0;
        if (info->expectResult != TEST_COMPARE_NET_XML2XML_RESULT_SUCCESS) {
            VIR_TEST_DEBUG("Got expected failure code=%d msg=%s",
                           result, virGetLastErrorMessage());
        } else {
            info->def = g_steal_pointer(&def);
        }
    } else {
        ret = -1;
        VIR_TEST_DEBUG("Expected result code=%d but received code=%d",
                       info->expectResult, result);
    }
    virResetLastError();

    return ret;
}


static int
testCompareXMLToConfFiles(const void *data)
{
    testInfo *info = (void *) data;
    char *confactual = NULL;
    g_autofree char *hostsfileactual = NULL;
    int ret = -1;
    virNetworkDef *def = NULL;
    virNetworkObj *obj = NULL;
    g_autofree char *pidfile = NULL;
    g_autoptr(dnsmasqContext) dctx = NULL;
    bool compareFailed = false;

    if (!(obj = virNetworkObjNew()))
        goto cleanup;

    if (!(def = g_steal_pointer(&info->def))) {
        /* Previous test wasn't executed. */
        virTestCaseMarkUsed(info->existingTestCases, info->inxml);
        if (!(def = virNetworkDefParse(NULL, info->inxml, info->xmlopt, false)))
            goto cleanup;

        if (networkValidateTests(def) < 0) {
            virNetworkDefFree(def);
            goto cleanup;
        }
    }

    virNetworkObjSetDef(obj, def);

    if (!networkNeedsDnsmasq(def)) {
        ret = EXIT_AM_SKIP;
        goto cleanup;
    }

    dctx = dnsmasqContextNew(def->name, "/var/lib/libvirt/dnsmasq");

    if (dctx == NULL)
        goto cleanup;

    if (networkDnsmasqConfContents(obj, pidfile, &confactual,
                                   &hostsfileactual, dctx, info->caps) < 0)
        goto cleanup;

    /* Any changes to this function ^^ should be reflected here too. */
#ifndef __linux__
    {
        char * tmp;

        if (!(tmp = virStringReplace(confactual,
                                     "except-interface=lo0\n",
                                     "except-interface=lo\n")))
            goto cleanup;
        VIR_FREE(confactual);
        confactual = g_steal_pointer(&tmp);
    }
#endif

    virTestCaseMarkUsed(info->existingTestCases, info->outconf);
    if (virTestCompareToFile(confactual, info->outconf) < 0)
        compareFailed = true;

    if (hostsfileactual) {
        virTestCaseMarkUsed(info->existingTestCases, info->outhostsfile);
        if (virTestCompareToFile(hostsfileactual, info->outhostsfile) < 0) {
            compareFailed = true;
        }
    } else {
        if (virFileExists(info->outhostsfile)) {
            VIR_TEST_DEBUG("%s: hostsfile exists but the configuration did not specify any host",
                           info->outhostsfile);
            compareFailed = true;
        }
    }

    if (compareFailed)
        goto cleanup;

    ret = 0;

 cleanup:
    VIR_FREE(confactual);
    virNetworkObjEndAPI(&obj);
    return ret;
}

static void
buildCapsCallback(const char *const*args,
                  const char *const*env G_GNUC_UNUSED,
                  const char *input G_GNUC_UNUSED,
                  char **output,
                  char **error G_GNUC_UNUSED,
                  int *status,
                  void *opaque G_GNUC_UNUSED)
{
    if (STREQ(args[0], "/usr/sbin/dnsmasq") && STREQ(args[1], "--version")) {
        *output = g_strdup("Dnsmasq version 2.67\n");
        *status = EXIT_SUCCESS;
    } else {
        *status = EXIT_FAILURE;
    }
}


static dnsmasqCaps *
buildCaps(void)
{
    g_autoptr(dnsmasqCaps) caps = NULL;
    g_autoptr(virCommandDryRunToken) dryRunToken = virCommandDryRunTokenNew();

    virCommandSetDryRun(dryRunToken, NULL, true, true, buildCapsCallback, NULL);

    caps = dnsmasqCapsNewFromBinary();

    return g_steal_pointer(&caps);
}


static void
testRun(const char *name,
        int *ret,
        virNetworkXMLOption *xmlopt,
        dnsmasqCaps *caps,
        testCompareNetXML2XMLResult expectResult,
        GHashTable *existingTestCases,
        unsigned int flags)
{
    g_autofree char *name_xml2xml = g_strdup_printf("Network XML-2-XML %s", name);
    g_autofree char *name_xml2conf = g_strdup_printf("Network XML-2-Conf %s", name);
    g_autoptr(testInfo) info = g_new0(testInfo, 1);

    info->name = name;
    info->flags = flags;
    info->expectResult = expectResult;
    info->xmlopt = xmlopt;
    info->caps = caps;
    info->inxml = g_strdup_printf("%s/networkxmlconfdata/%s.xml", abs_srcdir, name);
    info->outxml = g_strdup_printf("%s/networkxmlconfdata/%s.expect.xml", abs_srcdir, name);
    info->outconf = g_strdup_printf("%s/networkxmlconfdata/%s.conf", abs_srcdir, name);
    info->outhostsfile = g_strdup_printf("%s/networkxmlconfdata/%s.hostsfile", abs_srcdir, name);
    info->existingTestCases = existingTestCases;

    virTestRunLog(ret, name_xml2xml, testCompareXMLToXMLFiles, info);

    if (expectResult == TEST_COMPARE_NET_XML2XML_RESULT_SUCCESS)
        virTestRunLog(ret, name_xml2conf, testCompareXMLToConfFiles, info);
}


static bool
testCaseEnumerate(struct dirent *ent)
{
#ifndef __linux__
    /* This test case is ran only on Linux. See comment in mymain(). */
    if (STREQ(ent->d_name, "hostdev.expect.xml"))
        return false;
#endif
    return virStringHasSuffix(ent->d_name, ".xml") ||
        virStringHasSuffix(ent->d_name, ".conf") ||
        virStringHasSuffix(ent->d_name, ".hostsfile");
}


static int
mymain(void)
{
    g_autoptr(virNetworkXMLOption) xmlopt = NULL;
    g_autoptr(dnsmasqCaps) caps = NULL;
    g_autoptr(GHashTable) existingTestCases = NULL;
    int ret = 0;

    if (virTestEnumerateTestCases(abs_srcdir "/networkxmlconfdata",
                                  testCaseEnumerate,
                                  &existingTestCases) < 0) {
        return -1;
    }

    if (!(xmlopt = networkDnsmasqCreateXMLConf()))
        return -1;

    if (!(caps = buildCaps()))
        return -1;

#define DO_TEST_FULL(name, flags, expectResult) \
    testRun(name, &ret, xmlopt, caps, expectResult, existingTestCases, flags)
#define DO_TEST(name) \
    DO_TEST_FULL(name, 0, TEST_COMPARE_NET_XML2XML_RESULT_SUCCESS)
#define DO_TEST_FLAGS(name, flags) \
    DO_TEST_FULL(name, flags, TEST_COMPARE_NET_XML2XML_RESULT_SUCCESS)
#define DO_TEST_PARSE_ERROR(name) \
    DO_TEST_FULL(name, 0, TEST_COMPARE_NET_XML2XML_RESULT_FAIL_PARSE)
#define DO_TEST_VALIDATE_ERROR(name) \
    DO_TEST_FULL(name, 0, TEST_COMPARE_NET_XML2XML_RESULT_FAIL_VALIDATE)

    DO_TEST("dhcp6-network");
    DO_TEST("dhcp6-nat-network");
    DO_TEST("dhcp6host-routed-network");
    DO_TEST("empty-allow-ipv6");
    DO_TEST("isolated-network");
    DO_TEST("routed-network");
    DO_TEST("routed-network-no-dns");
    DO_TEST_PARSE_ERROR("routed-network-no-dns-extra-elements");
    DO_TEST("open-network");
    DO_TEST_PARSE_ERROR("open-network-with-forward-dev");
    DO_TEST("nat-network");
    DO_TEST("netboot-network");
    DO_TEST("netboot-proxy-network");
    DO_TEST("netboot-tftp");
    DO_TEST("nat-network-dns-txt-record");
    DO_TEST("nat-network-dns-srv-record");
    DO_TEST("nat-network-dns-srv-records");
    DO_TEST("nat-network-dns-srv-record-minimal");
    DO_TEST("nat-network-dns-hosts");
    DO_TEST("nat-network-dns-forward-plain");
    DO_TEST("nat-network-dns-forwarders");
    DO_TEST("nat-network-dns-forwarder-no-resolv");
    DO_TEST("nat-network-dns-local-domain");
    DO_TEST("nat-network-forward-nat-ipv6");
    DO_TEST("nat-network-forward-nat-address");
    DO_TEST("nat-network-forward-nat-no-address");
    DO_TEST("nat-network-name-with-quotes");
    DO_TEST("nat-network-mtu");
    DO_TEST("8021Qbh-net");
    DO_TEST("direct-net");
    DO_TEST("host-bridge-net");
    DO_TEST("vepa-net");
    DO_TEST("bandwidth-network");
    DO_TEST("openvswitch-net");
    DO_TEST_VALIDATE_ERROR("passthrough-pf");
#ifdef __linux__
    DO_TEST("hostdev");
#else
    /* Our test runners call networkValidateTests() which for
     * <forward mode='hostdev'/> means validating that PCI
     * devices are VFs. It's done so by querying sysfs which
     * obviously works on Linux only. Thus, expect a validation
     * error elsewhere. */
    DO_TEST_VALIDATE_ERROR("hostdev");
#endif
    DO_TEST_FLAGS("hostdev-pf", VIR_NETWORK_XML_INACTIVE);
    DO_TEST_FLAGS("hostdev-pf-driver-model", VIR_NETWORK_XML_INACTIVE);
    DO_TEST("ptr-domains-auto");
    DO_TEST_VALIDATE_ERROR("passthrough-address-crash");
    DO_TEST("nat-network-explicit-flood");
    DO_TEST("host-bridge-no-flood");
    DO_TEST_PARSE_ERROR("hostdev-duplicate");
    DO_TEST_PARSE_ERROR("passthrough-duplicate");
    DO_TEST("metadata");
    DO_TEST("set-mtu");
    DO_TEST("dnsmasq-options");
    DO_TEST("leasetime-seconds");
    DO_TEST("leasetime-minutes");
    DO_TEST("leasetime-hours");
    DO_TEST("leasetime-infinite");
    DO_TEST("isolated-ports");

    if (virTestCheckUnusedTestCases(existingTestCases) < 0)
        ret = -1;

    return ret == 0 ? EXIT_SUCCESS : EXIT_FAILURE;
}

VIR_TEST_MAIN_PRELOAD(mymain,
                      VIR_TEST_MOCK("virpci"),
                      VIR_TEST_MOCK("virdnsmasq"),
                      VIR_TEST_MOCK("virrandom"))
