/* whoopsie
 * 
 * Copyright © 2011-2013 Canonical Ltd.
 * Author: Evan Dandrea <evan.dandrea@canonical.com>
 * 
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; version 3 of the License.
 *
 * This program 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 General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
#define _XOPEN_SOURCE
#define _GNU_SOURCE

#include <gio/gio.h>
#include <glib.h>
#include <glib-object.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>

#include "../src/identifier.h"

static gboolean
is_root (void)
{
    return !(getuid () || g_getenv ("FAKEROOTKEY"));
}

static void
brofono_start (void)
{
    whoopsie_identifier_set_ofono_name ("org.brofono");
    char *mod = realpath("../../tools/brofono.py", NULL);
    if (!mod) {
        perror ("realpath");
        return;
    }

    GError* error = NULL;
    gchar* argv[] = {"python3", "-u", mod, NULL };

    static gint stdin = 0;
    gint stdout = 0;

    if (!g_spawn_async_with_pipes (NULL, argv, NULL, G_SPAWN_SEARCH_PATH, NULL,
                                   NULL, NULL, &stdin, &stdout, NULL,
                                   &error)) {
        g_error("spawn: %s", error->message);
    }

    /* helper prints to its stdout when ready to go */
    char buf[2] = {0};
    read(stdout, buf, 1);

    return;
}

static void
brofono_stop (void)
{
    GError* err = NULL;
    GDBusConnection* con = g_bus_get_sync (G_BUS_TYPE_SYSTEM, NULL, &err);

    g_assert_no_error (err);

    g_dbus_connection_call_sync (con, "org.brofono", "/",
                                 "org.ofono.Manager", "Quit",
                                 NULL, NULL,
                                 G_DBUS_CALL_FLAGS_NONE,
                                 -1, NULL, &err);

    g_assert_no_error (err);
    g_object_unref (con);
    whoopsie_identifier_set_ofono_name ("org.ofono");
}

static void
brofono_poke (gboolean expose, const char *path)
{
    GError* err = NULL;
    GDBusConnection* con = g_bus_get_sync (G_BUS_TYPE_SYSTEM, NULL, &err);

    g_assert_no_error (err);

    GVariant* params = g_variant_new ("(bo)", expose, path);

    g_dbus_connection_call_sync (con, "org.brofono", "/",
                                 "org.ofono.Manager", "Poke",
                                 params, NULL,
                                 G_DBUS_CALL_FLAGS_NONE,
                                 -1, NULL, &err);

    g_assert_no_error (err);
    g_object_unref (con);
}

static void
test_hex_to_char (void)
{
    char buf[9];
    whoopsie_hex_to_char (buf, "\xFF\xFF\xFF\xFF", 4);
    g_assert_cmpstr (buf, ==, "ffffffff");

}

static void
test_get_mac_address (void)
{
    char* res = NULL;
    GError* error = NULL;
    int fp = 0;
    char buf[18];
    GDir *net_dir;
    const gchar *device;
    gchar *path;
    gboolean found = FALSE;

    whoopsie_identifier_get_mac_address (&res, &error);
    g_assert (res != NULL);
    g_assert (error == NULL);

    /* ensure the MAC address matches any of the system's interfaces */
    net_dir = g_dir_open ("/sys/class/net", 0, NULL);
    if (net_dir == NULL) {
        g_print ("Could not open /sys/class/net\n");
        g_test_fail ();
        goto out;
    }

    while ((device = g_dir_read_name (net_dir)) != NULL) {
        /* ignore loopback device */
        if (strcmp (device, "lo") == 0)
            continue;

        path = g_build_filename ("/sys/class/net", device, "address", NULL);
        fp = open (path, O_RDONLY);
        g_free (path);

        if (fp < 0) {
            g_print ("Could not open /sys/class/net/%s/address\n", device);
            g_test_fail ();
            goto out;
        }
        if (read (fp, buf, 17) < 17) {
            g_print ("Could not read /sys/class/net/%s/address\n", device);
            g_test_fail ();
            goto out;
        }
        buf[17] = '\0';

        if (g_strcmp0 (buf, res) == 0) {
            found = TRUE;
            break;
        }
    }

    if (!found) {
        g_print ("MAC address does not match any value in /sys/class/net/*/address\n");
        g_test_fail ();
    }
out:
    if (net_dir != NULL)
        g_dir_close (net_dir);
    if (fp >= 0)
        close (fp);
    
    g_free (res);
}

static void
test_get_mac_address_fail_when_told_to (void)
{
    char* res = NULL;
    GError* error = NULL;

    /* check it works before */
    whoopsie_identifier_get_mac_address (&res, &error);
    g_assert (res != NULL);
    g_assert (error == NULL);
    g_free (res);
    res = NULL;

    /* set the flag */
    whoopsie_identifier_fail_next_get_mac (WHOOPSIE_FAIL_GENERIC);
    whoopsie_identifier_get_mac_address (&res, &error);
    g_assert (res == NULL);
    g_assert (error != NULL);
    g_assert_error (error, g_quark_from_static_string ("whoopsie-quark"), WHOOPSIE_FAILED_BY_REQUEST);
    error = NULL;

    /* flag is cleared */
    whoopsie_identifier_get_mac_address (&res, &error);
    g_assert (res != NULL);
    g_assert (error == NULL);
    g_free (res);
}

static void
test_get_mac_address_fail_when_no_siocgifflags (void)
{
    char* res = NULL;
    GError* error = NULL;
    whoopsie_identifier_fail_next_get_mac (WHOOPSIE_FAIL_NO_IFACES);
    whoopsie_identifier_get_mac_address (&res, &error);
    g_assert (error != NULL);
    g_assert (res == NULL);
}

static void
test_get_system_uuid (void)
{
    /* DEADBEEF-1234-1234-1234-DEADBEEF1234 */
    char* res = NULL;
    whoopsie_identifier_get_system_uuid (&res, NULL);
    if (!is_root ()) {
        g_print ("Need root to run this complete test: ");
        goto out;
    }

    if (res == NULL) {
        g_test_fail ();
        goto out;
    }

    if (strlen (res) != 36) {
        g_test_fail ();
        goto out;
    }

    if ((res[8] != '-' || res[13] != '-') ||
        (res[18] != '-' || res[23] != '-')) {
        g_test_fail ();
        goto out;
    }

out:
    if (res)
        g_free (res);
    return;
}

static void
test_get_system_uuid_fail_when_told_to (void)
{
    char* res = NULL;
    GError* error = NULL;

    if (!is_root ()) {
        g_print ("Need root to run this complete test: ");
    } else {
        /* check it works before */
        whoopsie_identifier_get_system_uuid (&res, &error);
        g_assert (res != NULL);
        g_assert (error == NULL);
        g_free (res);
        res = NULL;
    }

    /* set the flag */
    whoopsie_identifier_fail_next_get_uuid ();
    whoopsie_identifier_get_system_uuid (&res, &error);
    g_assert (res == NULL);
    g_assert (error != NULL);
    g_assert_error (error, g_quark_from_static_string ("whoopsie-quark"), WHOOPSIE_FAILED_BY_REQUEST);
    error = NULL;

    if (is_root ()) {
        /* flag is cleared */
        whoopsie_identifier_get_system_uuid (&res, &error);
        g_assert (res != NULL);
        g_assert (error == NULL);
        g_free (res);
    }
}

static void
test_sha512 (void)
{
    char res[HASHLEN + 1] = {0};
    // "foo" -> sha512
    const char* expected = "f7fbba6e0636f890e56fbbf3283e524c6fa3204ae298382"
                           "d624741d0dc6638326e282c41be5e4254d8820772c5518a"
                           "2c5a8c0c7f7eda19594a7eb539453e1ed7";
    whoopsie_identifier_sha512 ("foo", res, NULL);
    if (strcmp (res, expected) != 0)
        g_test_fail ();
}

static void
test_identifier_generate (void)
{
    char* result = NULL;
    char* expected = NULL;
    char hashed[HASHLEN + 1];

    brofono_start ();
    brofono_poke (TRUE, "/okish");

    whoopsie_identifier_generate (&result, NULL);
    if (!is_root ()) {
        whoopsie_identifier_get_mac_address (&expected, NULL);
        whoopsie_identifier_append_imei (&expected, NULL);
    } else {
        whoopsie_identifier_get_system_uuid (&expected, NULL);
    }
    whoopsie_identifier_sha512 (expected, hashed, NULL);
    g_assert_cmpstr (result, ==, hashed);

    g_free (result);
    g_free (expected);
    brofono_stop ();
}

static void
test_identifier_generate_fail_imei (void)
{
    char* result = NULL;
    char* expected = NULL;
    char hashed[HASHLEN + 1];

    if (is_root ())
        g_test_skip ("Not running this one as root");

    /* note we haven't set up brofono */

    whoopsie_identifier_generate (&result, NULL);
    whoopsie_identifier_get_mac_address (&expected, NULL);
    whoopsie_identifier_sha512 (expected, hashed, NULL);
    g_assert_cmpstr (result, ==, hashed);

    g_free (result);
    g_free (expected);
}

static void
test_generate_fails_on_complete_failure (void)
{
    char* res = NULL;
    GError* error = NULL;

    /* check we succeed on uuid failure */
    whoopsie_identifier_fail_next_get_uuid ();
    whoopsie_identifier_generate (&res, &error);
    g_assert (res != NULL);
    g_assert (error == NULL);

    g_free (res);
    res = NULL;

    if (g_file_test ("/sys/class/dmi/id/product_uuid", G_FILE_TEST_EXISTS)) {
        /* the system has a UUID */ 
        if (!is_root ()) {
            g_print ("Need to be root to run this complete test: ");
        } else {
            /* test we succeed if we can't get a mac */
            whoopsie_identifier_fail_next_get_mac (WHOOPSIE_FAIL_GENERIC);
            whoopsie_identifier_generate (&res, &error);
            g_assert (res != NULL);
            g_assert (error == NULL);
            g_free (res);
            res = NULL;
        }
    } else {
        g_print ("(skipping UUID test on this system) ");
    }

    whoopsie_identifier_fail_next_get_mac (WHOOPSIE_FAIL_GENERIC);
    whoopsie_identifier_fail_next_get_uuid ();
    whoopsie_identifier_generate (&res, &error);
    g_assert (res == NULL);
    g_assert (error != NULL);
}

static void
test_get_imei (void)
{
    char* res = NULL;
    GError* error = NULL;

    brofono_start ();
    brofono_poke (TRUE, "/okish");

    whoopsie_identifier_append_imei (&res, &error);
    g_assert_no_error (error);
    g_assert (res != NULL);
    brofono_stop ();
}

static void
test_get_imei_no_ofono (void)
{
    char* res = "bananas";
    GError* error = NULL;

    whoopsie_identifier_append_imei (&res, &error);
    g_assert (error != NULL);
    g_assert_cmpstr (res, ==, "bananas");
}

static void
test_get_imei_no_getmodems (void)
{
    char* res = "bananas";
    GError* error = NULL;

    brofono_start ();
    brofono_poke (FALSE, "/empty");

    whoopsie_identifier_append_imei (&res, &error);
    g_assert (error != NULL);
    g_assert_cmpstr (res, ==, "bananas");

    brofono_stop ();
}

static void
test_get_imei_empty_reply (void)
{
    char* res = "bananas";
    GError* error = NULL;

    brofono_start ();
    brofono_poke (TRUE, "/empty");

    whoopsie_identifier_append_imei (&res, &error);
    g_assert (error != NULL);
    g_assert_cmpstr (res, ==, "bananas");
    brofono_stop ();
}

static void
test_get_imei_bad_imei (void)
{
    char* res = "bananas";
    GError* error = NULL;

    brofono_start ();
    brofono_poke (TRUE, "/bad/imei");

    whoopsie_identifier_append_imei (&res, &error);
    g_assert (error != NULL);
    g_assert_cmpstr (res, ==, "bananas");
    brofono_stop ();
}

static void
test_get_imei_no_imei (void)
{
    char* res = "bananas";
    GError* error = NULL;

    brofono_start ();
    brofono_poke (TRUE, "/no/imei");

    whoopsie_identifier_append_imei (&res, &error);
    g_assert (error != NULL);
    g_assert_cmpstr (res, ==, "bananas");
    brofono_stop ();
}

static void
test_identifier_cache (void)
{
    const char* expected = "f7fbba6e0636f890e56fbbf3283e524c6fa3204ae298382"
                           "d624741d0dc6638326e282c41be5e4254d8820772c5518a"
                           "2c5a8c0c7f7eda19594a7eb539453e1ed7";
    char* res = NULL;
    GError* error = NULL;

    gchar *test_path = NULL;
    gchar *test_dir = NULL;
    gchar test_tmpl[PATH_MAX] = "/tmp/whoopsie-test-XXXXXX";
    test_dir = g_mkdtemp (test_tmpl);
    g_assert (test_dir != NULL);

    /* Check non-existent id file fails lookup */
    test_path = g_strdup_printf ("%s/non-existant", test_dir);
    whoopsie_identifier_set_cache_file (test_path);
    whoopsie_identifier_get_cached (&res, &error);
    g_assert_null (res);
    g_assert_error (error, g_quark_from_static_string ("whoopsie-quark"), 0);
    g_free (test_path);
    g_error_free (error);
    error = NULL;

    /* Check good cache is loaded */
    test_path = g_strdup_printf ("%s/good", test_dir);
    g_assert (g_file_set_contents (test_path, expected, -1, NULL));
    whoopsie_identifier_set_cache_file (test_path);    
    whoopsie_identifier_get_cached (&res, &error);
    g_assert_nonnull (res);
    g_assert_no_error (error);
    g_assert_cmpstr (res, ==, expected);
    g_unlink (test_path);
    g_free (test_path);

    /* Check bad cache is not loaded */
    test_path = g_strdup_printf ("%s/good", test_dir);
    g_assert (g_file_set_contents (test_path, expected, HASHLEN-3, NULL));
    whoopsie_identifier_set_cache_file (test_path);
    whoopsie_identifier_get_cached (&res, &error);
    g_assert_null (res);
    g_assert_error (error, g_quark_from_static_string ("whoopsie-quark"), 0);
    g_unlink (test_path);
    g_free (test_path);

    g_assert (g_rmdir (test_dir) == 0);
}

int
main (int argc, char** argv)
{
#if GLIB_MAJOR_VERSION <= 2 && GLIB_MINOR_VERSION < 35
    /* Deprecated in glib 2.35/2.36. */
    g_type_init ();
#endif
    g_test_init (&argc, &argv, NULL);

    /* Prevent reading currently running system id */
    whoopsie_identifier_set_cache_file ("/tmp");

    g_test_add_func ("/whoopsie/get-system-uuid", test_get_system_uuid);
    g_test_add_func ("/whoopsie/get-uuid-fails-when-told", test_get_system_uuid_fail_when_told_to);
    g_test_add_func ("/whoopsie/hex-to-char", test_hex_to_char);
    g_test_add_func ("/whoopsie/get-mac-address", test_get_mac_address);
    g_test_add_func ("/whoopsie/get-mac-fails-when-told", test_get_mac_address_fail_when_told_to);
    g_test_add_func ("/whoopsie/get-mac-fails-when-no-siocgifflags", test_get_mac_address_fail_when_no_siocgifflags);
    g_test_add_func ("/whoopsie/sha512", test_sha512);
    g_test_add_func ("/whoopsie/identifier-generate", test_identifier_generate);
    g_test_add_func ("/whoopsie/identifier-generate-fails-on-failure", test_generate_fails_on_complete_failure);
    g_test_add_func ("/whoopsie/imei", test_get_imei);
    g_test_add_func ("/whoopsie/imei-no-ofono", test_get_imei_no_ofono);
    g_test_add_func ("/whoopsie/imei-no-getmodems", test_get_imei_no_getmodems);
    g_test_add_func ("/whoopsie/imei-empty-reply", test_get_imei_empty_reply);
    g_test_add_func ("/whoopsie/imei-bad", test_get_imei_bad_imei);
    g_test_add_func ("/whoopsie/imei-missing", test_get_imei_no_imei);
    g_test_add_func ("/whoopsie/get-cached", test_identifier_cache);

    /* Do not consider warnings to be fatal. */
    g_log_set_always_fatal (G_LOG_FATAL_MASK);

    return g_test_run ();
}
