/*
 * Copyright (C) 2017 Christian J. Kellner <christian@kellner.me>
 *
 * SPDX-License-Identifier: LGPL-2.1+
 */

#include "config.h"

#include <fwupd.h>
#include <glib.h>
#include <glib/gprintf.h>
#include <glib/gstdio.h>
#include <gudev/gudev.h>

#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

#include <umockdev.h>

#include <locale.h>

#include "fu-plugin-private.h"
#include "fu-thunderbolt-firmware.h"
#include "fu-thunderbolt-firmware-update.h"
#include "fu-udev-device-private.h"

static gchar *
udev_mock_add_domain (UMockdevTestbed *bed, int id)
{
	gchar *path;
	g_autofree gchar *name = NULL;

	name = g_strdup_printf ("domain%d", id);
	path = umockdev_testbed_add_device (bed, "thunderbolt", name,
					    NULL,
					    "security", "secure",
					    NULL,
					    "DEVTYPE",
					    "thunderbolt_domain",
					    NULL);

	g_assert_nonnull (path);
	return path;
}


static gchar *
udev_mock_add_nvmem (UMockdevTestbed *bed,
		     gboolean         active,
		     const char      *parent,
		     int              id)
{
	g_autofree gchar *name = NULL;
	gchar *path;

	name = g_strdup_printf ("%s%d", active ? "nvm_active" : "nvm_non_active", id);
	path = umockdev_testbed_add_device (bed, "nvmem", name,
					    parent,
					    "nvmem", "",
					    NULL,
					    NULL);

	g_assert_nonnull (path);
	return path;
}

typedef struct MockDevice MockDevice;

struct MockDevice {

	const char *name; /* sysfs: device_name */
	const char *id;   /* sysfs: device */
	const char *nvm_version;
	const char *nvm_parsed_version;

	int delay_ms;

	int domain_id;

	struct MockDevice *children;

	/* optionally filled out */
	const char *uuid;
};

typedef struct MockTree MockTree;

struct MockTree {
	const MockDevice *device;

	MockTree  *parent;
	GPtrArray *children;

	gchar *sysfs_parent;
	int    sysfs_id;
	int    sysfs_nvm_id;

	gchar *uuid;

	UMockdevTestbed *bed;
	gchar  *path;
	gchar  *nvm_non_active;
	gchar  *nvm_active;
	guint   nvm_authenticate;
	gchar  *nvm_version;

	FuDevice *fu_device;
};

static MockTree *
mock_tree_new (MockTree *parent, MockDevice *device, int *id)
{
	MockTree *node = g_slice_new0 (MockTree);
	int current_id = (*id)++;

	node->device = device;
	node->sysfs_id = current_id;
	node->sysfs_nvm_id = current_id;
	node->parent = parent;

	if (device->uuid)
		node->uuid = g_strdup (device->uuid);
	else
		node->uuid = g_uuid_string_random ();

	node->nvm_version = g_strdup (device->nvm_version);
	return node;
}

static void
mock_tree_free (MockTree *tree)
{
	for (guint i = 0; i < tree->children->len; i++) {
		MockTree *child = g_ptr_array_index (tree->children, i);
		mock_tree_free (child);
	}

	g_ptr_array_free (tree->children, TRUE);

	if (tree->fu_device)
		g_object_unref (tree->fu_device);

	g_free (tree->uuid);
	if (tree->bed != NULL) {
		if (tree->nvm_active) {
			umockdev_testbed_uevent (tree->bed, tree->nvm_active, "remove");
			umockdev_testbed_remove_device (tree->bed, tree->nvm_active);
		}

		if (tree->nvm_non_active) {
			umockdev_testbed_uevent (tree->bed, tree->nvm_non_active, "remove");
			umockdev_testbed_remove_device (tree->bed, tree->nvm_non_active);
		}

		if (tree->path) {
			umockdev_testbed_uevent (tree->bed, tree->path, "remove");
			umockdev_testbed_remove_device (tree->bed, tree->path);
		}


		g_object_unref (tree->bed);
	}

	g_free (tree->nvm_version);
	g_free (tree->nvm_active);
	g_free (tree->nvm_non_active);
	g_free (tree->path);
	g_free (tree->sysfs_parent);
	g_slice_free (MockTree, tree);
}

#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wunused-function"
G_DEFINE_AUTOPTR_CLEANUP_FUNC (MockTree, mock_tree_free);
#pragma clang diagnostic pop

static GPtrArray *
mock_tree_init_children (MockTree *node, int *id)
{
	GPtrArray *children = g_ptr_array_new ();
	MockDevice *iter;

	for (iter = node->device->children; iter && iter->name; iter++) {
		MockTree *child = mock_tree_new (node, iter, id);
		g_ptr_array_add (children, child);
		child->children = mock_tree_init_children (child, id);
	}

	return children;
}

static MockTree *
mock_tree_init (MockDevice *device)
{
	MockTree *tree;
	int devices = 0;

	tree = mock_tree_new (NULL, device, &devices);
	tree->children = mock_tree_init_children (tree, &devices);

	return tree;
}

static void
mock_tree_dump (const MockTree *node, int level)
{
	if (node->path) {
		g_debug ("%*s * %s [%s] at %s", level, " ",
			 node->device->name, node->uuid, node->path);
		g_debug ("%*s   non-active nvmem at %s", level, " ",
			 node->nvm_non_active);
		g_debug ("%*s   active nvmem at %s", level, " ",
			 node->nvm_active);
	} else {
		g_debug ("%*s * %s [%s] %d", level, " ",
			 node->device->name, node->uuid, node->sysfs_id);
	}

	for (guint i = 0; i < node->children->len; i++) {
		const MockTree *child = g_ptr_array_index (node->children, i);
		mock_tree_dump (child, level + 2);
	}
}

static void
mock_tree_firmware_verify (const MockTree *node, GBytes *data)
{
	g_autoptr(GFile) nvm_device = NULL;
	g_autoptr(GFile) nvm = NULL;
	g_autoptr(GInputStream) is = NULL;
	g_autoptr(GChecksum) chk = NULL;
	g_autoptr(GError) error = NULL;
	g_autofree gchar *sum_data = NULL;
	const gchar *sum_disk = NULL;
	gsize s;

	sum_data = g_compute_checksum_for_bytes (G_CHECKSUM_SHA1, data);
	chk = g_checksum_new (G_CHECKSUM_SHA1);

	g_assert_nonnull (node);
	g_assert_nonnull (node->nvm_non_active);

	nvm_device = g_file_new_for_path (node->nvm_non_active);
	nvm = g_file_get_child (nvm_device, "nvmem");

	is = (GInputStream *) g_file_read (nvm, NULL, &error);
	g_assert_no_error (error);
	g_assert_nonnull (is);

	do {
		g_autoptr(GBytes) b = NULL;
		const guchar *d;

		b = g_input_stream_read_bytes (is, 4096, NULL, &error);
		g_assert_no_error (error);
		g_assert_nonnull (is);

		d = g_bytes_get_data (b, &s);
		if (s > 0)
			g_checksum_update (chk, d, (gssize) s);

	} while (s > 0);

	sum_disk = g_checksum_get_string (chk);

	g_assert_cmpstr (sum_data, ==, sum_disk);
}

typedef gboolean (* MockTreePredicate) (const MockTree *node, gpointer data);

static const MockTree *
mock_tree_contains (const MockTree    *node,
		    MockTreePredicate  predicate,
		    gpointer           data)
{
	if (predicate (node, data))
		return node;

	for (guint i = 0; i < node->children->len; i++) {
		const MockTree *child;
		const MockTree *match;

		child = g_ptr_array_index (node->children, i);
		match = mock_tree_contains (child, predicate, data);
		if (match != NULL)
			return match;
	}

	return NULL;
}

static gboolean
mock_tree_all (const MockTree    *node,
	       MockTreePredicate  predicate,
	       gpointer           data)
{
	if (!predicate (node, data))
		return FALSE;

	for (guint i = 0; i < node->children->len; i++) {
		const MockTree *child;

		child = g_ptr_array_index (node->children, i);
		if (!mock_tree_all (child, predicate, data))
			return FALSE;
	}

	return TRUE;
}

static gboolean
mock_tree_compare_uuid (const MockTree *node, gpointer data)
{
	const gchar *uuid = (const gchar *) data;
	return g_str_equal (node->uuid, uuid);
}

static const MockTree *
mock_tree_find_uuid (const MockTree *root, const char *uuid)
{
	return mock_tree_contains (root,
				   mock_tree_compare_uuid,
				   (gpointer) uuid);
}

static gboolean
mock_tree_node_have_fu_device (const MockTree *node, gpointer data)
{
	return node->fu_device != NULL;
}

static void
write_controller_fw (const gchar *nvm)
{
	g_autoptr(GFile) nvm_device = NULL;
	g_autoptr(GFile) nvmem = NULL;
	g_autofree gchar *fw_path = NULL;
	g_autoptr(GFile) fw_file = NULL;
	g_autoptr(GInputStream) is = NULL;
	g_autoptr(GOutputStream) os = NULL;
	g_autoptr(GError) error = NULL;
	gssize n;

	fw_path = g_build_filename (TESTDATADIR, "thunderbolt/minimal-fw-controller.bin", NULL);
	fw_file = g_file_new_for_path (fw_path);
	g_assert_nonnull (fw_file);

	nvm_device = g_file_new_for_path (nvm);
	g_assert_nonnull (nvm_device);

	nvmem = g_file_get_child (nvm_device, "nvmem");
	g_assert_nonnull (nvmem);

	os = (GOutputStream *) g_file_append_to (nvmem,
						 G_FILE_CREATE_NONE,
						 NULL,
						 &error);
	g_assert_no_error (error);
	g_assert_nonnull (os);

	is = (GInputStream *) g_file_read (fw_file, NULL, &error);
	g_assert_no_error (error);
	g_assert_nonnull (is);

	n = g_output_stream_splice (os,
				    is,
				    G_OUTPUT_STREAM_SPLICE_NONE,
				    NULL,
				    &error);
	g_assert_no_error (error);
	g_assert_cmpuint (n, >, 0);
}

static gboolean
mock_tree_attach_device (gpointer user_data)
{
	MockTree *tree = (MockTree *) user_data;
	const MockDevice *dev = tree->device;
	g_autofree gchar *idstr = NULL;
	g_autofree gchar *authenticate = NULL;

	g_assert_nonnull (tree);
	g_assert_nonnull (tree->sysfs_parent);
	g_assert_nonnull (dev);

	idstr = g_strdup_printf ("%d-%d", dev->domain_id, tree->sysfs_id);
	authenticate = g_strdup_printf ("0x%x", tree->nvm_authenticate);

	tree->path = umockdev_testbed_add_device (tree->bed, "thunderbolt", idstr,
						  tree->sysfs_parent,
						  "device_name", dev->name,
						  "device", dev->id,
						  "vendor", "042",
						  "vendor_name", "GNOME.org",
						  "authorized", "1",
						  "nvm_authenticate", authenticate,
						  "nvm_version", tree->nvm_version,
						  "unique_id", tree->uuid,
						  NULL,
						  "DEVTYPE",
						  "thunderbolt_device",
						  NULL);

	tree->nvm_non_active = udev_mock_add_nvmem (tree->bed,
						    FALSE,
						    tree->path,
						    tree->sysfs_id);

	tree->nvm_active = udev_mock_add_nvmem (tree->bed,
						TRUE,
						tree->path,
						tree->sysfs_id);

	g_assert_nonnull (tree->path);
	g_assert_nonnull (tree->nvm_non_active);
	g_assert_nonnull (tree->nvm_active);

	write_controller_fw (tree->nvm_active);

	for (guint i = 0; i < tree->children->len; i++) {
		MockTree *child;

		child = g_ptr_array_index (tree->children, i);

		child->bed = g_object_ref (tree->bed);
		child->sysfs_parent = g_strdup (tree->path);

		g_timeout_add (child->device->delay_ms,
			       mock_tree_attach_device,
			       child);
	}

	return FALSE;
}

typedef struct SyncContext {
	MockTree  *tree;
	GMainLoop *loop;
} SyncContext;

static gboolean
on_sync_timeout (gpointer user_data)
{
	SyncContext *ctx = (SyncContext *) user_data;
	g_main_loop_quit (ctx->loop);
	return FALSE;
}

static void
sync_device_added (FuPlugin *plugin, FuDevice *device, gpointer user_data)
{
	SyncContext *ctx = (SyncContext *) user_data;
	MockTree *tree = ctx->tree;
	const gchar *uuid = fu_device_get_physical_id (device);
	MockTree *target;

	target = (MockTree *) mock_tree_find_uuid (tree, uuid);

	if (target == NULL) {
		g_critical ("Got device that could not be matched: %s", uuid);
		return;
	}

	if (target->fu_device != NULL)
		g_object_unref (target->fu_device);

	target->fu_device = g_object_ref (device);
}

static void
sync_device_removed (FuPlugin *plugin, FuDevice *device, gpointer user_data)
{
	SyncContext *ctx = (SyncContext *) user_data;
	MockTree *tree = ctx->tree;
	const gchar *uuid = fu_device_get_physical_id (device);
	MockTree *target;

	target = (MockTree *) mock_tree_find_uuid (tree, uuid);

	if (target == NULL) {
		g_warning ("Got device that could not be matched: %s", uuid);
		return;
	} else if (target->fu_device == NULL) {
		g_warning ("Got remove event for out-of-tree device %s", uuid);
		return;
	}

	g_object_unref (target->fu_device);
	target->fu_device = NULL;
}

static void
mock_tree_sync (MockTree *root, FuPlugin *plugin, int timeout_ms)
{
	g_autoptr(GMainLoop) mainloop = g_main_loop_new (NULL, FALSE);
	gulong id_add;
	gulong id_del;
	SyncContext ctx = {
		.tree = root,
		.loop = mainloop,
	};

	id_add = g_signal_connect (plugin, "device-added",
				   G_CALLBACK (sync_device_added),
				   &ctx);

	id_del = g_signal_connect (plugin, "device-removed",
				   G_CALLBACK (sync_device_removed),
				   &ctx);

	if (timeout_ms > 0)
		g_timeout_add (timeout_ms, on_sync_timeout, &ctx);

	g_main_loop_run (mainloop);

	g_signal_handler_disconnect (plugin, id_add);
	g_signal_handler_disconnect (plugin, id_del);
}


typedef struct AttachContext {
	/* in */
	MockTree  *tree;
	GMainLoop *loop;
	/* out */
	gboolean   complete;

} AttachContext;

static void
mock_tree_plugin_device_added (FuPlugin *plugin, FuDevice *device, gpointer user_data)
{
	AttachContext *ctx = (AttachContext *) user_data;
	MockTree *tree = ctx->tree;
	const gchar *uuid = fu_device_get_physical_id (device);
	MockTree *target;

	target = (MockTree *) mock_tree_find_uuid (tree, uuid);

	if (target == NULL) {
		g_warning ("Got device that could not be matched: %s", uuid);
		return;
	}

	g_set_object (&target->fu_device, device);

	if (mock_tree_all (tree, mock_tree_node_have_fu_device, NULL)) {
		ctx->complete = TRUE;
		g_main_loop_quit (ctx->loop);
	}
}

static gboolean
mock_tree_settle (MockTree *root, FuPlugin *plugin)
{
	g_autoptr(GMainLoop) mainloop = g_main_loop_new (NULL, FALSE);
	gulong id;
	AttachContext ctx = {
		.tree = root,
		.loop = mainloop,
	};

	id = g_signal_connect (plugin, "device-added",
			       G_CALLBACK (mock_tree_plugin_device_added),
			       &ctx);

	g_main_loop_run (mainloop);
	g_signal_handler_disconnect (plugin, id);

	return ctx.complete;
}

static gboolean
mock_tree_attach (MockTree *root, UMockdevTestbed *bed, FuPlugin *plugin)
{
	root->bed = g_object_ref (bed);
	root->sysfs_parent = udev_mock_add_domain (bed, root->device->domain_id);
	g_assert_nonnull (root->sysfs_parent);

	g_timeout_add (root->device->delay_ms, mock_tree_attach_device, root);

	return mock_tree_settle (root, plugin);
}

/* the unused parameter makes the function signature compatible
 * with 'MockTreePredicate' */
static gboolean
mock_tree_node_is_detached (const MockTree *node, gpointer unused)
{
	gboolean ret = node->path == NULL;

	/* consistency checks: if ret, make sure we are
	 * fully detached */
	if (ret) {
		g_assert_null (node->nvm_active);
		g_assert_null (node->nvm_non_active);
		g_assert_null (node->bed);
	} else {
		g_assert_nonnull (node->nvm_active);
		g_assert_nonnull (node->nvm_non_active);
		g_assert_nonnull (node->bed);
	}

	return ret;
}

static void
mock_tree_detach (MockTree *node)
{
	UMockdevTestbed *bed;

	if (mock_tree_node_is_detached (node, NULL))
		return;

	for (guint i = 0; i < node->children->len; i++) {
		MockTree *child = g_ptr_array_index (node->children, i);
		mock_tree_detach (child);
		g_free (child->sysfs_parent);
		child->sysfs_parent = NULL;
	}

	bed  = node->bed;
	umockdev_testbed_uevent (bed, node->nvm_active, "remove");
	umockdev_testbed_remove_device (bed, node->nvm_active);

	umockdev_testbed_uevent (bed, node->nvm_non_active, "remove");
	umockdev_testbed_remove_device (bed, node->nvm_non_active);

	umockdev_testbed_uevent (bed, node->path, "remove");
	umockdev_testbed_remove_device (bed, node->path);

	g_free (node->path);
	g_free (node->nvm_non_active);
	g_free (node->nvm_active);

	node->path = NULL;
	node->nvm_non_active = NULL;
	node->nvm_active = NULL;

	g_object_unref (bed);
	node->bed = NULL;
}

typedef enum UpdateResult {
	UPDATE_SUCCESS     = 0,
	/* nvm_authenticate will report error condition */
	UPDATE_FAIL_DEVICE_INTERNAL = 1,
	/* device to be updated will NOT re-appear */
	UPDATE_FAIL_DEVICE_NOSHOW   = 2
} UpdateResult;

typedef struct UpdateContext {
	GFileMonitor *monitor;

	UpdateResult result;
	guint timeout;
	GBytes *data;
	UMockdevTestbed *bed;
	FuPlugin *plugin;


	MockTree *node;
	gchar *version;
} UpdateContext;

static void
update_context_free (UpdateContext *ctx)
{
	if (ctx == NULL)
		return;

	g_file_monitor_cancel (ctx->monitor);

	g_object_unref (ctx->bed);
	g_object_unref (ctx->plugin);
	g_object_unref (ctx->monitor);
	g_bytes_unref (ctx->data);
	g_free (ctx->version);
	g_free (ctx);
}

#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wunused-function"
G_DEFINE_AUTOPTR_CLEANUP_FUNC (UpdateContext, update_context_free);
#pragma clang diagnostic pop

static gboolean
reattach_tree (gpointer user_data)
{
	UpdateContext *ctx = (UpdateContext *) user_data;
	MockTree *node = ctx->node;

	g_debug ("Mock update done, reattaching tree...");

	node->bed = g_object_ref (ctx->bed);
	g_timeout_add (node->device->delay_ms, mock_tree_attach_device, node);

	return FALSE;
}

static void
udev_file_changed_cb (GFileMonitor     *monitor,
		      GFile            *file,
		      GFile            *other_file,
		      GFileMonitorEvent event_type,
		      gpointer          user_data)
{
	UpdateContext *ctx = (UpdateContext *) user_data;
	gboolean ok;
	gsize len;
	g_autofree gchar *data = NULL;
	g_autoptr(GError) error = NULL;

	g_debug ("Got update trigger");
	ok = g_file_monitor_cancel (monitor);
	g_assert_true (ok);

	ok = g_file_load_contents (file, NULL, &data, &len, NULL, &error);
	g_assert_no_error (error);
	g_assert_true (ok);

	if (!g_str_has_prefix (data, "1"))
		return;

	/* verify the firmware is correct */
	mock_tree_firmware_verify (ctx->node, ctx->data);

	g_debug ("Removing tree below and including: %s", ctx->node->path);
	mock_tree_detach (ctx->node);

	ctx->node->nvm_authenticate = (guint) ctx->result;

	/* update the version only on "success" simulations */
	if (ctx->result == UPDATE_SUCCESS) {
		g_free (ctx->node->nvm_version);
		ctx->node->nvm_version = g_strdup (ctx->version);
	}

	g_debug ("Simulating update to '%s' with result: 0x%x",
		 ctx->version, ctx->node->nvm_authenticate);

	if (ctx->result == UPDATE_FAIL_DEVICE_NOSHOW) {
		g_debug ("Simulating no-show fail:"
			 " device tree will not reappear");
		return;
	}

	g_debug ("Device tree reattachment in %3.2f seconds",
		 ctx->timeout / 1000.0);
	g_timeout_add (ctx->timeout, reattach_tree, ctx);
}

static UpdateContext *
mock_tree_prepare_for_update (MockTree    *node,
			      FuPlugin    *plugin,
			      const char  *version,
			      GBytes      *fw_data,
			      guint        timeout_ms)
{
	UpdateContext *ctx;
	g_autoptr(GFile) dir = NULL;
	g_autoptr(GFile) f = NULL;
	g_autoptr(GError) error = NULL;
	GFileMonitor *monitor;

	ctx = g_new0 (UpdateContext, 1);
	dir = g_file_new_for_path (node->path);
	f = g_file_get_child (dir, "nvm_authenticate");

	monitor = g_file_monitor_file (f, G_FILE_MONITOR_NONE, NULL, &error);
	g_assert_no_error (error);
	g_assert_nonnull (monitor);

	ctx->node = node;
	ctx->plugin = g_object_ref (plugin);
	ctx->bed = g_object_ref (node->bed);
	ctx->timeout = timeout_ms;
	ctx->monitor = monitor;
	ctx->version = g_strdup (version);
	ctx->data = g_bytes_ref (fw_data);

	g_signal_connect (monitor, "changed",
			  G_CALLBACK (udev_file_changed_cb), ctx);

	return ctx;
}

static MockDevice root_one = {

	.name = "Laptop",
	.id = "0x23",
	.nvm_version = "20.2",
	.nvm_parsed_version = "20.02",

	.children = (MockDevice[]) {
		{
			.name = "Thunderbolt Cable",
			.id = "0x24",
			.nvm_version = "20.0",
			.nvm_parsed_version = "20.00",

			.children = (MockDevice[]) {
				{
					.name = "Thunderbolt Dock",
					.id = "0x25",
					.nvm_version = "10.0",
					.nvm_parsed_version = "10.00",
				},
				{ NULL, }

			},
		}, {
			.name = "Thunderbolt Cable",
			.id = "0x24",
			.nvm_version = "23.0",
			.nvm_parsed_version = "23.00",

			.children = (MockDevice[]) {
				{
					.name = "Thunderbolt SSD",
					.id = "0x26",

					.nvm_version = "5.0",
					.nvm_parsed_version = "05.00",
				},
				{ NULL, }
			},
		},
		{ NULL, },
	},

};


typedef struct TestParam {
	gboolean initialize_tree;
	gboolean attach_and_coldplug;

	const char *firmware_file;
} TestParam;

typedef enum TestFlags {
	TEST_INITIALIZE_TREE     = 1 << 0,
	TEST_ATTACH = 1 << 1,
	TEST_PREPARE_FIRMWARE    = 1 << 2,

	TEST_PREPARE_ALL = TEST_INITIALIZE_TREE |
	                   TEST_ATTACH |
	                   TEST_PREPARE_FIRMWARE
} TestFlags;

#define TEST_INIT_FULL (GUINT_TO_POINTER (TEST_PREPARE_ALL))
#define TEST_INIT_NONE (GUINT_TO_POINTER (0))

typedef struct ThunderboltTest {
	UMockdevTestbed *bed;
	FuPlugin *plugin;
	GUdevClient *udev_client;

	/* if TestParam::initialize_tree */
	MockTree *tree;

	/* if TestParam::firmware_file is nonnull */
	GMappedFile *fw_file;
	GBytes      *fw_data;

} ThunderboltTest;

static void
fu_thunderbolt_gudev_uevent_cb (GUdevClient *gudev_client,
				const gchar *action,
				GUdevDevice *udev_device,
				ThunderboltTest *tt)
{
	if (g_strcmp0 (action, "add") == 0) {
		g_autoptr(FuUdevDevice) device = fu_udev_device_new (udev_device);
		g_autoptr(GError) error_local = NULL;
		if (!fu_plugin_runner_backend_device_added (tt->plugin, FU_DEVICE (device), &error_local))
			g_debug ("failed to add: %s", error_local->message);
		return;
	}
	if (g_strcmp0 (action, "remove") == 0) {
		if (tt->tree->fu_device != NULL)
			fu_plugin_device_remove (tt->plugin, tt->tree->fu_device);
		return;
	}
	if (g_strcmp0 (action, "change") == 0) {
		const gchar *uuid = g_udev_device_get_sysfs_attr (udev_device, "unique_id");
		MockTree *target = (MockTree *) mock_tree_find_uuid (tt->tree, uuid);
		g_assert_nonnull (target);
		fu_udev_device_emit_changed (FU_UDEV_DEVICE (target->fu_device));
		return;
	}
}
static void
test_set_up (ThunderboltTest *tt, gconstpointer params)
{
	TestFlags flags = GPOINTER_TO_UINT(params);
	gboolean ret;
	g_autofree gchar *pluginfn = NULL;
	g_autofree gchar *sysfs = NULL;
	g_autoptr(GError) error = NULL;
	const gchar *udev_subsystems[] = { "thunderbolt", NULL };

	tt->bed = umockdev_testbed_new ();
	g_assert_nonnull (tt->bed);

	sysfs = umockdev_testbed_get_sys_dir (tt->bed);
	g_debug ("mock sysfs at %s", sysfs);

	tt->plugin = fu_plugin_new ();
	g_assert_nonnull (tt->plugin);

	pluginfn = g_build_filename (PLUGINBUILDDIR,
				     "libfu_plugin_thunderbolt." G_MODULE_SUFFIX,
				     NULL);
	ret = fu_plugin_open (tt->plugin, pluginfn, &error);

	g_assert_no_error (error);
	g_assert_true (ret);

	ret = fu_plugin_runner_startup (tt->plugin, &error);
	g_assert_no_error (error);
	g_assert_true (ret);

	if (flags & TEST_INITIALIZE_TREE) {
		tt->tree = mock_tree_init (&root_one);
		g_assert_nonnull (tt->tree);
	}

	if (!umockdev_in_mock_environment ()) {
		g_warning ("Need to run with umockdev-wrapper");
		return;
	}

	tt->udev_client = g_udev_client_new (udev_subsystems);
	g_assert_nonnull (tt->udev_client);
	g_signal_connect (tt->udev_client, "uevent",
			  G_CALLBACK (fu_thunderbolt_gudev_uevent_cb), tt);

	if (flags & TEST_ATTACH) {
		g_assert_true (flags & TEST_INITIALIZE_TREE);

		ret = mock_tree_attach (tt->tree, tt->bed, tt->plugin);
		g_assert_true (ret);
	}

	if (flags & TEST_PREPARE_FIRMWARE) {
		g_autofree gchar *fw_path = NULL;

		fw_path = g_build_filename (TESTDATADIR, "thunderbolt/minimal-fw.bin", NULL);
		tt->fw_file = g_mapped_file_new (fw_path, FALSE, &error);
		g_assert_no_error (error);
		g_assert_nonnull (tt->fw_file);

		tt->fw_data = g_mapped_file_get_bytes (tt->fw_file);
		g_assert_nonnull (tt->fw_data);
	}
}

static void
test_tear_down (ThunderboltTest *tt, gconstpointer user_data)
{
	g_object_unref (tt->plugin);
	g_object_unref (tt->bed);
	g_object_unref (tt->udev_client);

	if (tt->tree)
		mock_tree_free (tt->tree);

	if (tt->fw_data)
		g_bytes_unref (tt->fw_data);

	if (tt->fw_file)
		g_mapped_file_unref (tt->fw_file);
}



static gboolean
test_tree_uuids (const MockTree *node, gpointer data)
{
	const MockTree *root = (MockTree *) data;
	const gchar *uuid = node->uuid;
	const MockTree *found;

	g_assert_nonnull (uuid);

	g_debug ("Looking for %s", uuid);

	found = mock_tree_find_uuid (root, uuid);
	g_assert_nonnull (node);
	g_assert_nonnull (found);
	g_assert_cmpstr (node->uuid, ==, found->uuid);

	/* return false so we traverse the whole tree */
	return FALSE;
}

static void
test_tree (ThunderboltTest *tt, gconstpointer user_data)
{
	const MockTree *found;
	gboolean ret;
	g_autoptr(MockTree) tree = NULL;

	tree = mock_tree_init (&root_one);
	g_assert_nonnull (tree);

	mock_tree_dump (tree, 0);

	(void) mock_tree_contains (tree, test_tree_uuids, tree);

	found = mock_tree_find_uuid (tree, "nonexistentuuid");
	g_assert_null (found);

	ret = mock_tree_attach (tree, tt->bed, tt->plugin);
	g_assert_true (ret);

	mock_tree_detach (tree);
	ret = mock_tree_all (tree, mock_tree_node_is_detached, NULL);
	g_assert_true (ret);
}

static gboolean
_compare_images (FuThunderboltFirmware *firmware_old, FuThunderboltFirmware *firmware, GError **error)
{
	if (fu_thunderbolt_firmware_is_host (firmware) !=
	    fu_thunderbolt_firmware_is_host (firmware_old)) {
		g_set_error (error,
			     FWUPD_ERROR,
			     FWUPD_ERROR_INVALID_FILE,
			     "incorrect firmware mode, got %s, expected %s",
			     fu_thunderbolt_firmware_is_host (firmware) ? "host" : "device",
			     fu_thunderbolt_firmware_is_host (firmware_old) ? "host" : "device");
		return FALSE;
	}
	if (fu_thunderbolt_firmware_get_vendor_id (firmware) !=
	    fu_thunderbolt_firmware_get_vendor_id (firmware_old)) {
		g_set_error (error,
			     FWUPD_ERROR,
			     FWUPD_ERROR_INVALID_FILE,
			     "incorrect device vendor, got 0x%04x, expected 0x%04x",
			     fu_thunderbolt_firmware_get_vendor_id (firmware),
			     fu_thunderbolt_firmware_get_vendor_id (firmware_old));
		return FALSE;
	}
	if (fu_thunderbolt_firmware_get_device_id (firmware) !=
	    fu_thunderbolt_firmware_get_device_id (firmware_old)) {
		g_set_error (error,
			     FWUPD_ERROR,
			     FWUPD_ERROR_INVALID_FILE,
			     "incorrect device type, got 0x%04x, expected 0x%04x",
			     fu_thunderbolt_firmware_get_device_id (firmware),
			     fu_thunderbolt_firmware_get_device_id (firmware_old));
		return FALSE;
	}
	if (fu_thunderbolt_firmware_get_model_id (firmware) !=
		fu_thunderbolt_firmware_get_model_id (firmware_old)) {
		g_set_error (error,
				FWUPD_ERROR,
				FWUPD_ERROR_INVALID_FILE,
				"incorrect device model, got 0x%04x, expected 0x%04x",
				fu_thunderbolt_firmware_get_model_id (firmware),
				fu_thunderbolt_firmware_get_model_id (firmware_old));
		return FALSE;
	}
	if (fu_thunderbolt_firmware_get_has_pd (firmware_old) &&
	    !fu_thunderbolt_firmware_get_has_pd (firmware)) {
		g_set_error_literal (error,
				     FWUPD_ERROR,
				     FWUPD_ERROR_INVALID_FILE,
				     "new firmware is missing PD");
		return FALSE;
	}
	if (fu_thunderbolt_firmware_get_flash_size (firmware) !=
		fu_thunderbolt_firmware_get_flash_size (firmware_old)) {
		g_set_error_literal (error,
				     FWUPD_ERROR,
				     FWUPD_ERROR_INVALID_FILE,
				     "incorrect flash size");
		return FALSE;
	}
	return TRUE;
}

static void
test_image_validation (ThunderboltTest *tt, gconstpointer user_data)
{
	gboolean ret;
	g_autofree gchar *ctl_path = NULL;
	g_autofree gchar *fwi_path = NULL;
	g_autofree gchar *bad_path = NULL;
	g_autoptr(GMappedFile) fwi_file = NULL;
	g_autoptr(GMappedFile) ctl_file = NULL;
	g_autoptr(GMappedFile) bad_file = NULL;
	g_autoptr(GBytes)      fwi_data = NULL;
	g_autoptr(GBytes)      ctl_data = NULL;
	g_autoptr(GBytes)      bad_data = NULL;
	g_autoptr(GError)      error = NULL;
	g_autoptr(FuThunderboltFirmwareUpdate) firmware_fwi = fu_thunderbolt_firmware_update_new ();
	g_autoptr(FuThunderboltFirmware) firmware_ctl = fu_thunderbolt_firmware_new ();
	g_autoptr(FuThunderboltFirmware) firmware_bad = fu_thunderbolt_firmware_new ();

	/* image as if read from the controller (i.e. no headers) */
	ctl_path = g_build_filename (TESTDATADIR,
					 "thunderbolt/minimal-fw-controller.bin", NULL);
	ctl_file = g_mapped_file_new (ctl_path, FALSE, &error);
	g_assert_no_error (error);
	g_assert_nonnull (ctl_file);

	ctl_data = g_mapped_file_get_bytes (ctl_file);
	g_assert_nonnull (ctl_data);

	/* parse controller image */
	ret = fu_firmware_parse (FU_FIRMWARE (firmware_ctl), ctl_data, FWUPD_INSTALL_FLAG_NONE, &error);
	g_assert_true (ret);
	g_assert_no_error (error);

	/* valid firmware update image */
	fwi_path = g_build_filename (TESTDATADIR, "thunderbolt/minimal-fw.bin", NULL);
	fwi_file = g_mapped_file_new (fwi_path, FALSE, &error);
 	g_assert_no_error (error);
	g_assert_nonnull (fwi_file);

	fwi_data = g_mapped_file_get_bytes (fwi_file);
	g_assert_nonnull (fwi_data);

	/* parse */
	ret = fu_firmware_parse (FU_FIRMWARE (firmware_fwi), fwi_data, FWUPD_INSTALL_FLAG_NONE, &error);
	g_assert_true (ret);
	g_assert_no_error (error);

	/* a wrong/bad firmware update image */
	bad_path = g_build_filename (TESTDATADIR, "colorhug/firmware.bin", NULL);
	bad_file = g_mapped_file_new (bad_path, FALSE, &error);
	g_assert_no_error (error);
	g_assert_nonnull (bad_file);

	bad_data = g_mapped_file_get_bytes (bad_file);
	g_assert_nonnull (bad_data);

	/* parse; should fail, bad image */
	ret = fu_firmware_parse (FU_FIRMWARE (firmware_bad), bad_data, FWUPD_INSTALL_FLAG_NONE, &error);
	g_assert_false (ret);
	g_assert_error (error, FWUPD_ERROR, FWUPD_ERROR_READ);
	g_debug ("expected image validation error [ctl]: %s", error->message);
	g_clear_error (&error);

	/* now for some testing ... this should work */
	ret = _compare_images (firmware_ctl, FU_THUNDERBOLT_FIRMWARE (firmware_fwi), &error);
	g_assert_no_error (error);
	g_assert_true (ret);
}

static void
test_change_uevent (ThunderboltTest *tt, gconstpointer user_data)
{
	FuPlugin *plugin = tt->plugin;
	MockTree *tree = tt->tree;
	gboolean ret;
	const gchar *version_after;

	/* test sanity check */
	g_assert_nonnull (tree);

	/* simulate change of version via a change even, i.e.
	 * without add, remove. */
	umockdev_testbed_set_attribute (tt->bed, tree->path, "nvm_version", "42.23");
	umockdev_testbed_uevent (tt->bed, tree->path, "change");

	/* we just "wait" for 500ms, should be enough */
	mock_tree_sync (tree, plugin, 500);

	/* the tree should not have changed */
	ret = mock_tree_all (tree, mock_tree_node_have_fu_device, NULL);
	g_assert_true (ret);

	/* we should have the version change in the FuDevice */
	version_after = fu_device_get_version (tree->fu_device);
	g_assert_cmpstr (version_after, ==, "42.23");
}

static void
test_update_working (ThunderboltTest *tt, gconstpointer user_data)
{
	FuPlugin *plugin = tt->plugin;
	MockTree *tree = tt->tree;
	GBytes *fw_data = tt->fw_data;
	gboolean ret;
	const gchar *version_after;
	g_autoptr(GError) error = NULL;
	g_autoptr(UpdateContext) up_ctx = NULL;

	/* test sanity check */
	g_assert_nonnull (tree);
	g_assert_nonnull (fw_data);

	/* simulate an update, where the device goes away and comes back
	 * after the time in the last parameter (given in ms) */
	up_ctx = mock_tree_prepare_for_update (tree, plugin, "42.23", fw_data, 1000);
	ret = fu_plugin_runner_update (plugin, tree->fu_device, fw_data, 0, &error);
	g_assert_no_error (error);
	g_assert_true (ret);

	/* we wait until the plugin has picked up  all the
	 * subtree changes */
	ret = mock_tree_settle (tree, plugin);
	g_assert_true (ret);

	ret = fu_plugin_runner_update_attach (plugin, tree->fu_device, &error);
	g_assert_no_error (error);
	g_assert_true (ret);

	version_after = fu_device_get_version (tree->fu_device);
	g_debug ("version after update: %s", version_after);
	g_assert_cmpstr (version_after, ==, "42.23");

	/* make sure all pending events have happened */
	ret = mock_tree_settle (tree, plugin);
	g_assert_true (ret);

	/* now we check if the every tree node has a corresponding FuDevice,
	 * this implicitly checks that we are handling uevents correctly
	 * after the event, and that we are in sync with the udev tree */
	ret = mock_tree_all (tree, mock_tree_node_have_fu_device, NULL);
	g_assert_true (ret);
}

static void
test_update_wd19 (ThunderboltTest *tt, gconstpointer user_data)
{
	FuPlugin *plugin = tt->plugin;
	MockTree *tree = tt->tree;
	GBytes *fw_data = tt->fw_data;
	gboolean ret;
	const gchar *version_before;
	const gchar *version_after;
	g_autoptr(GError) error = NULL;

	/* test sanity check */
	g_assert_nonnull (tree);
	g_assert_nonnull (fw_data);

	/* simulate a wd19 update which will not disappear / re-appear */
	fu_device_add_flag (tree->fu_device, FWUPD_DEVICE_FLAG_SKIPS_RESTART);
	fu_device_add_flag (tree->fu_device, FWUPD_DEVICE_FLAG_USABLE_DURING_UPDATE);
	version_before = fu_device_get_version (tree->fu_device);

	ret = fu_plugin_runner_update (plugin, tree->fu_device, fw_data, 0, &error);
	g_assert_no_error (error);
	g_assert_true (ret);

	ret = fu_device_has_flag (tree->fu_device, FWUPD_DEVICE_FLAG_NEEDS_ACTIVATION);
	g_assert_true (ret);

	version_after = fu_device_get_version (tree->fu_device);
	g_assert_cmpstr (version_after, ==, version_before);
}

static void
test_update_fail (ThunderboltTest *tt, gconstpointer user_data)
{
	FuPlugin *plugin = tt->plugin;
	MockTree *tree = tt->tree;
	GBytes *fw_data = tt->fw_data;
	gboolean ret;
	const gchar *version_after;
	g_autoptr(GError) error = NULL;
	g_autoptr(UpdateContext) up_ctx = NULL;

	/* test sanity check */
	g_assert_nonnull (tree);
	g_assert_nonnull (fw_data);

	/* simulate an update, as in test_update_working,
	 * but simulate an error indicated by the device
	 */
	up_ctx = mock_tree_prepare_for_update (tree, plugin, "42.23", fw_data, 1000);
	up_ctx->result = UPDATE_FAIL_DEVICE_INTERNAL;

	ret = fu_plugin_runner_update (plugin, tree->fu_device, fw_data, 0, &error);
	g_assert_no_error (error);
	g_assert_true (ret);

	/* we wait until the plugin has picked up all the
	 * subtree changes, and make sure we still receive
	 * udev updates correctly and are in sync */
	ret = mock_tree_settle (tree, plugin);
	g_assert_true (ret);

	ret = fu_plugin_runner_update_attach (plugin, tree->fu_device, &error);
	g_assert_error (error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL);
	g_assert_false (ret);

	/* make sure all pending events have happened */
	ret = mock_tree_settle (tree, plugin);
	g_assert_true (ret);

	/* version should *not* have changed (but we get parsed version) */
	version_after = fu_device_get_version (tree->fu_device);
	g_debug ("version after update: %s", version_after);
	g_assert_cmpstr (version_after, ==, tree->device->nvm_parsed_version);

	ret = mock_tree_all (tree, mock_tree_node_have_fu_device, NULL);
	g_assert_true (ret);
}


static void
test_update_fail_nowshow (ThunderboltTest *tt, gconstpointer user_data)
{
	FuPlugin *plugin = tt->plugin;
	MockTree *tree = tt->tree;
	GBytes *fw_data = tt->fw_data;
	gboolean ret;
	g_autoptr(GError) error = NULL;
	g_autoptr(UpdateContext) up_ctx = NULL;

	/* test sanity check */
	g_assert_nonnull (tree);
	g_assert_nonnull (fw_data);

	/* simulate an update, as in test_update_working,
	 * but simulate an error indicated by the device
	 */
	up_ctx = mock_tree_prepare_for_update (tree, plugin, "42.23", fw_data, 1000);
	up_ctx->result = UPDATE_FAIL_DEVICE_NOSHOW;

	ret = fu_plugin_runner_update (plugin, tree->fu_device, fw_data, 0, &error);
	g_assert_no_error (error);
	g_assert_true (ret);

	mock_tree_sync (tree, plugin, 500);

	ret = mock_tree_all (tree, mock_tree_node_have_fu_device, NULL);
	g_assert_false (ret);
}

int
main (int argc, char **argv)
{
	g_test_init (&argc, &argv, NULL);
	g_log_set_fatal_mask (NULL, G_LOG_LEVEL_ERROR | G_LOG_LEVEL_CRITICAL);


	g_test_add ("/thunderbolt/basic",
		    ThunderboltTest,
		    NULL,
		    test_set_up,
		    test_tree,
		    test_tear_down);

	g_test_add ("/thunderbolt/image-validation",
		    ThunderboltTest,
		    TEST_INIT_NONE,
		    test_set_up,
		    test_image_validation,
		    test_tear_down);

	g_test_add ("/thunderbolt/change-uevent",
		    ThunderboltTest,
		    GUINT_TO_POINTER (TEST_INITIALIZE_TREE |
				      TEST_ATTACH),
		    test_set_up,
		    test_change_uevent,
		    test_tear_down);

	g_test_add ("/thunderbolt/update{working}",
		    ThunderboltTest,
		    TEST_INIT_FULL,
		    test_set_up,
		    test_update_working,
		    test_tear_down);

	g_test_add ("/thunderbolt/update{failing}",
		    ThunderboltTest,
		    TEST_INIT_FULL,
		    test_set_up,
		    test_update_fail,
		    test_tear_down);

	g_test_add ("/thunderbolt/update{failing-noshow}",
		    ThunderboltTest,
		    TEST_INIT_FULL,
		    test_set_up,
		    test_update_fail_nowshow,
		    test_tear_down);
	g_test_add ("/thunderbolt/update{delayed_activation}",
		    ThunderboltTest,
		    TEST_INIT_FULL,
		    test_set_up,
		    test_update_wd19,
		    test_tear_down);

	return g_test_run ();
}
