/*
 * Copyright (C) 2017 Canonical Ltd.
 *
 * This library is free software; you can redistribute it and/or modify it under
 * the terms of the GNU Lesser General Public License as published by the Free
 * Software Foundation; either version 2 or version 3 of the License.
 * See http://www.gnu.org/copyleft/lgpl.html the full text of the license.
 */

#include <sys/types.h>
#include <sys/socket.h>
#include <string.h>
#include <ctype.h>
#include <errno.h>

#include <glib/gstdio.h>
#include <gio/gunixsocketaddress.h>
#include <libsoup/soup.h>
#include <json-glib/json-glib.h>

#include "mock-snapd.h"

struct _MockSnapd
{
    GObject parent_instance;

    GMutex mutex;
    GCond condition;

    GThread *thread;
    GError **thread_init_error;
    GMainLoop *loop;
    GMainContext *context;

    gchar *dir_path;
    gchar *socket_path;
    gboolean close_on_request;
    gboolean decline_auth;
    GList *accounts;
    GList *users;
    GList *interfaces;
    GList *snaps;
    GList *snapshots;
    gchar *build_id;
    gchar *confinement;
    GHashTable *sandbox_features;
    gchar *store;
    gchar *maintenance_kind;
    gchar *maintenance_message;
    gboolean managed;
    gboolean on_classic;
    gchar *refresh_hold;
    gchar *refresh_last;
    gchar *refresh_next;
    gchar *refresh_schedule;
    gchar *refresh_timer;
    GList *store_sections;
    GList *store_snaps;
    GList *established_connections;
    GList *undesired_connections;
    GList *assertions;
    int change_index;
    GList *changes;
    gchar *suggested_currency;
    gchar *spawn_time;
    gchar *ready_time;
    SoupMessageHeaders *last_request_headers;
    GHashTable *gtk_theme_status;
    GHashTable *icon_theme_status;
    GHashTable *sound_theme_status;
};

G_DEFINE_TYPE (MockSnapd, mock_snapd, G_TYPE_OBJECT)

struct _MockAccount
{
    gint64 id;
    gchar *email;
    gchar *username;
    gchar *password;
    gchar *otp;
    GStrv ssh_keys;
    gchar *macaroon;
    GStrv discharges;
    gboolean sudoer;
    gboolean known;
    gboolean terms_accepted;
    gboolean has_payment_methods;
    GList *private_snaps;
};

struct _MockAlias
{
    gchar *name;
    gboolean automatic;
    gboolean enabled;
};

struct _MockApp
{
    gchar *name;
    gchar *common_id;
    gchar *daemon;
    gchar *desktop_file;
    gboolean enabled;
    gboolean active;
    GList *aliases;
};

struct _MockChange
{
    gchar *id;
    gchar *kind;
    gchar *summary;
    gchar *spawn_time;
    gchar *ready_time;
    int task_index;
    GList *tasks;
    JsonNode *data;
};

struct _MockChannel
{
    gchar *risk;
    gchar *branch;
    gchar *confinement;
    gchar *epoch;
    gchar *released_at;
    gchar *revision;
    int size;
    gchar *version;
};

struct _MockPrice
{
    gdouble amount;
    gchar *currency;
};

struct _MockMedia
{
    gchar *type;
    gchar *url;
    int width;
    int height;
};

struct _MockInterface
{
    gchar *name;
    gchar *summary;
    gchar *doc_url;
};

struct _MockPlug
{
    MockSnap *snap;
    gchar *name;
    MockInterface *interface;
    GHashTable *attributes;
    gchar *label;
};

struct _MockSlot
{
    MockSnap *snap;
    gchar *name;
    MockInterface *interface;
    GHashTable *attributes;
    gchar *label;
};

struct _MockConnection
{
    MockPlug *plug;
    MockSlot *slot;
    gboolean manual;
    gboolean gadget;
};

struct _MockSnap
{
    GList *apps;
    gchar *base;
    gchar *broken;
    gchar *channel;
    GHashTable *configuration;
    gchar *confinement;
    gchar *contact;
    gchar *description;
    gboolean devmode;
    int download_size;
    gchar *icon;
    gchar *icon_mime_type;
    GBytes *icon_data;
    gchar *id;
    gchar *install_date;
    int installed_size;
    gboolean jailmode;
    gchar *license;
    GList *media;
    gchar *mounted_from;
    gchar *name;
    GList *prices;
    gboolean is_private;
    gchar *publisher_display_name;
    gchar *publisher_id;
    gchar *publisher_username;
    gchar *publisher_validation;
    gchar *revision;
    gchar *status;
    gchar *store_url;
    gchar *summary;
    gchar *title;
    gchar *tracking_channel;
    GList *tracks;
    gboolean trymode;
    gchar *type;
    gchar *version;
    gchar *website;
    GList *store_sections;
    GList *plugs;
    GList *slots_;
    gboolean disabled;
    gboolean dangerous;
    gchar *snap_data;
    gchar *snap_path;
    gchar *error;
    gboolean restart_required;
    gboolean preferred;
    gboolean scope_is_wide;
};

struct _MockSnapshot
{
    gchar *name;
};

struct _MockTask
{
    gchar *id;
    gchar *kind;
    gchar *summary;
    gchar *status;
    gchar *progress_label;
    int progress_done;
    int progress_total;
    gchar *spawn_time;
    gchar *ready_time;
    MockSnap *snap;
    gchar *snap_name;
    gchar *error;
    gboolean purge;
};

struct _MockTrack
{
    gchar *name;
    GList *channels;
};

static void
mock_alias_free (MockAlias *alias)
{
    g_free (alias->name);
    g_slice_free (MockAlias, alias);
}

static void
mock_app_free (MockApp *app)
{
    g_free (app->name);
    g_free (app->common_id);
    g_free (app->daemon);
    g_free (app->desktop_file);
    g_list_free_full (app->aliases, (GDestroyNotify) mock_alias_free);
    g_slice_free (MockApp, app);
}

static void
mock_channel_free (MockChannel *channel)
{
    g_free (channel->risk);
    g_free (channel->branch);
    g_free (channel->confinement);
    g_free (channel->epoch);
    g_free (channel->released_at);
    g_free (channel->revision);
    g_free (channel->version);
    g_slice_free (MockChannel, channel);
}

static void
mock_track_free (MockTrack *track)
{
    g_free (track->name);
    g_list_free_full (track->channels, (GDestroyNotify) mock_channel_free);
    g_slice_free (MockTrack, track);
}

static void
mock_price_free (MockPrice *price)
{
    g_free (price->currency);
    g_slice_free (MockPrice, price);
}

static void
mock_media_free (MockMedia *media)
{
    g_free (media->type);
    g_free (media->url);
    g_slice_free (MockMedia, media);
}

static void
mock_interface_free (MockInterface *interface)
{
    g_free (interface->name);
    g_free (interface->summary);
    g_free (interface->doc_url);
    g_slice_free (MockInterface, interface);
}

static void
mock_plug_free (MockPlug *plug)
{
    g_free (plug->name);
    g_hash_table_unref (plug->attributes);
    g_free (plug->label);
    g_slice_free (MockPlug, plug);
}

static void
mock_slot_free (MockSlot *slot)
{
    g_free (slot->name);
    g_hash_table_unref (slot->attributes);
    g_free (slot->label);
    g_slice_free (MockSlot, slot);
}

static void
mock_connection_free (MockConnection *connection)
{
    g_slice_free (MockConnection, connection);
}

static void
mock_snap_free (MockSnap *snap)
{
    g_list_free_full (snap->apps, (GDestroyNotify) mock_app_free);
    g_free (snap->base);
    g_free (snap->broken);
    g_free (snap->channel);
    g_hash_table_unref (snap->configuration);
    g_free (snap->confinement);
    g_free (snap->contact);
    g_free (snap->description);
    g_free (snap->icon);
    g_free (snap->icon_mime_type);
    g_bytes_unref (snap->icon_data);
    g_free (snap->id);
    g_free (snap->install_date);
    g_free (snap->license);
    g_list_free_full (snap->media, (GDestroyNotify) mock_media_free);
    g_free (snap->mounted_from);
    g_free (snap->name);
    g_list_free_full (snap->prices, (GDestroyNotify) mock_price_free);
    g_free (snap->publisher_display_name);
    g_free (snap->publisher_id);
    g_free (snap->publisher_username);
    g_free (snap->publisher_validation);
    g_free (snap->revision);
    g_free (snap->status);
    g_free (snap->store_url);
    g_free (snap->summary);
    g_free (snap->title);
    g_free (snap->tracking_channel);
    g_list_free_full (snap->tracks, (GDestroyNotify) mock_track_free);
    g_free (snap->type);
    g_free (snap->version);
    g_free (snap->website);
    g_list_free_full (snap->store_sections, g_free);
    g_list_free_full (snap->plugs, (GDestroyNotify) mock_plug_free);
    g_list_free_full (snap->slots_, (GDestroyNotify) mock_slot_free);
    g_free (snap->snap_data);
    g_free (snap->snap_path);
    g_free (snap->error);
    g_slice_free (MockSnap, snap);
}

MockSnapd *
mock_snapd_new (void)
{
    return g_object_new (MOCK_TYPE_SNAPD, NULL);
}

const gchar *
mock_snapd_get_socket_path (MockSnapd *self)
{
    g_return_val_if_fail (MOCK_IS_SNAPD (self), NULL);
    return self->socket_path;
}

void
mock_snapd_set_close_on_request (MockSnapd *self, gboolean close_on_request)
{
    g_return_if_fail (MOCK_IS_SNAPD (self));
    self->close_on_request = close_on_request;
}

void
mock_snapd_set_decline_auth (MockSnapd *self, gboolean decline_auth)
{
    g_return_if_fail (MOCK_IS_SNAPD (self));
    self->decline_auth = decline_auth;
}

void
mock_snapd_set_maintenance (MockSnapd *self, const gchar *kind, const gchar *message)
{
    g_return_if_fail (MOCK_IS_SNAPD (self));
    g_free (self->maintenance_kind);
    self->maintenance_kind = g_strdup (kind);
    g_free (self->maintenance_message);
    self->maintenance_message = g_strdup (message);
}

void
mock_snapd_set_build_id (MockSnapd *self, const gchar *build_id)
{
    g_return_if_fail (MOCK_IS_SNAPD (self));

    g_autoptr(GMutexLocker) locker = g_mutex_locker_new (&self->mutex);
    g_free (self->build_id);
    self->build_id = g_strdup (build_id);
}

void
mock_snapd_set_confinement (MockSnapd *self, const gchar *confinement)
{
    g_return_if_fail (MOCK_IS_SNAPD (self));

    g_autoptr(GMutexLocker) locker = g_mutex_locker_new (&self->mutex);
    g_free (self->confinement);
    self->confinement = g_strdup (confinement);
}

void
mock_snapd_add_sandbox_feature (MockSnapd *self, const gchar *backend, const gchar *feature)
{
    g_return_if_fail (MOCK_IS_SNAPD (self));

    g_autoptr(GMutexLocker) locker = g_mutex_locker_new (&self->mutex);
    GPtrArray *backend_features = g_hash_table_lookup (self->sandbox_features, backend);
    if (backend_features == NULL) {
        backend_features = g_ptr_array_new_with_free_func (g_free);
        g_hash_table_insert (self->sandbox_features, g_strdup (backend), backend_features);
    }
    g_ptr_array_add (backend_features, g_strdup (feature));
}

void
mock_snapd_set_store (MockSnapd *self, const gchar *name)
{
    g_return_if_fail (MOCK_IS_SNAPD (self));

    g_autoptr(GMutexLocker) locker = g_mutex_locker_new (&self->mutex);
    g_free (self->store);
    self->store = g_strdup (name);
}

void
mock_snapd_set_managed (MockSnapd *self, gboolean managed)
{
    g_return_if_fail (MOCK_IS_SNAPD (self));

    g_autoptr(GMutexLocker) locker = g_mutex_locker_new (&self->mutex);
    self->managed = managed;
}

void
mock_snapd_set_on_classic (MockSnapd *self, gboolean on_classic)
{
    g_return_if_fail (MOCK_IS_SNAPD (self));

    g_autoptr(GMutexLocker) locker = g_mutex_locker_new (&self->mutex);
    self->on_classic = on_classic;
}

void
mock_snapd_set_refresh_hold (MockSnapd *self, const gchar *refresh_hold)
{
    g_return_if_fail (MOCK_IS_SNAPD (self));

    g_autoptr(GMutexLocker) locker = g_mutex_locker_new (&self->mutex);
    g_free (self->refresh_hold);
    self->refresh_hold = g_strdup (refresh_hold);
}

void
mock_snapd_set_refresh_last (MockSnapd *self, const gchar *refresh_last)
{
    g_return_if_fail (MOCK_IS_SNAPD (self));

    g_autoptr(GMutexLocker) locker = g_mutex_locker_new (&self->mutex);
    g_free (self->refresh_last);
    self->refresh_last = g_strdup (refresh_last);
}

void
mock_snapd_set_refresh_next (MockSnapd *self, const gchar *refresh_next)
{
    g_return_if_fail (MOCK_IS_SNAPD (self));

    g_autoptr(GMutexLocker) locker = g_mutex_locker_new (&self->mutex);
    g_free (self->refresh_next);
    self->refresh_next = g_strdup (refresh_next);
}

void
mock_snapd_set_refresh_schedule (MockSnapd *self, const gchar *schedule)
{
    g_return_if_fail (MOCK_IS_SNAPD (self));

    g_autoptr(GMutexLocker) locker = g_mutex_locker_new (&self->mutex);
    g_free (self->refresh_schedule);
    self->refresh_schedule = g_strdup (schedule);
}

void
mock_snapd_set_refresh_timer (MockSnapd *self, const gchar *timer)
{
    g_return_if_fail (MOCK_IS_SNAPD (self));

    g_autoptr(GMutexLocker) locker = g_mutex_locker_new (&self->mutex);
    g_free (self->refresh_timer);
    self->refresh_timer = g_strdup (timer);
}

void
mock_snapd_set_suggested_currency (MockSnapd *self, const gchar *currency)
{
    g_return_if_fail (MOCK_IS_SNAPD (self));

    g_autoptr(GMutexLocker) locker = g_mutex_locker_new (&self->mutex);
    g_free (self->suggested_currency);
    self->suggested_currency = g_strdup (currency);
}

void
mock_snapd_set_spawn_time (MockSnapd *self, const gchar *spawn_time)
{
    g_return_if_fail (MOCK_IS_SNAPD (self));

    g_autoptr(GMutexLocker) locker = g_mutex_locker_new (&self->mutex);
    g_free (self->spawn_time);
    self->spawn_time = g_strdup (spawn_time);
}

void
mock_snapd_set_ready_time (MockSnapd *self, const gchar *ready_time)
{
    g_return_if_fail (MOCK_IS_SNAPD (self));

    g_autoptr(GMutexLocker) locker = g_mutex_locker_new (&self->mutex);
    g_free (self->ready_time);
    self->ready_time = g_strdup (ready_time);
}

static MockAccount *
add_account (MockSnapd *self, const gchar *email, const gchar *username, const gchar *password)
{
    g_return_val_if_fail (MOCK_IS_SNAPD (self), NULL);

    MockAccount *account = g_slice_new0 (MockAccount);
    account->email = g_strdup (email);
    account->username = g_strdup (username);
    account->password = g_strdup (password);
    account->macaroon = g_strdup_printf ("MACAROON-%s", username);
    account->discharges = g_malloc (sizeof (gchar *) * 2);
    account->discharges[0] = g_strdup_printf ("DISCHARGE-%s", username);
    account->discharges[1] = NULL;
    self->accounts = g_list_append (self->accounts, account);
    account->id = g_list_length (self->accounts);

    return account;
}

MockAccount *
mock_snapd_add_account (MockSnapd *self, const gchar *email, const gchar *username, const gchar *password)
{
    g_return_val_if_fail (MOCK_IS_SNAPD (self), NULL);

    g_autoptr(GMutexLocker) locker = g_mutex_locker_new (&self->mutex);

    return add_account (self, email, username, password);
}

void
mock_account_set_terms_accepted (MockAccount *account, gboolean terms_accepted)
{
    account->terms_accepted = terms_accepted;
}

void
mock_account_set_has_payment_methods (MockAccount *account, gboolean has_payment_methods)
{
    account->has_payment_methods = has_payment_methods;
}

gint64
mock_account_get_id (MockAccount *account)
{
    return account->id;
}

const gchar *
mock_account_get_macaroon (MockAccount *account)
{
    return account->macaroon;
}

GStrv
mock_account_get_discharges (MockAccount *account)
{
    return account->discharges;
}

gboolean
mock_account_get_sudoer (MockAccount *account)
{
    return account->sudoer;
}

gboolean
mock_account_get_known (MockAccount *account)
{
    return account->known;
}

void
mock_account_set_otp (MockAccount *account, const gchar *otp)
{
    g_clear_pointer (&account->otp, g_free);
    account->otp = g_strdup (otp);
}

void
mock_account_set_ssh_keys (MockAccount *account, GStrv ssh_keys)
{
    g_clear_pointer (&account->ssh_keys, g_strfreev);
    account->ssh_keys = g_strdupv (ssh_keys);
}

static MockAccount *
find_account_by_id (MockSnapd *self, int id)
{
    for (GList *link = self->accounts; link; link = link->next) {
        MockAccount *account = link->data;
        if (account->id == id)
            return account;
    }

    return NULL;
}

MockAccount *
mock_snapd_find_account_by_id (MockSnapd *self, gint64 id)
{
    g_return_val_if_fail (MOCK_IS_SNAPD (self), NULL);

    g_autoptr(GMutexLocker) locker = g_mutex_locker_new (&self->mutex);

    return find_account_by_id (self, id);
}

static MockAccount *
find_account_by_username (MockSnapd *self, const gchar *username)
{
    for (GList *link = self->accounts; link; link = link->next) {
        MockAccount *user = link->data;
        if (strcmp (user->username, username) == 0)
            return user;
    }

    return NULL;
}

MockAccount *
mock_snapd_find_account_by_username (MockSnapd *self, const gchar *username)
{
    g_return_val_if_fail (MOCK_IS_SNAPD (self), NULL);

    g_autoptr(GMutexLocker) locker = g_mutex_locker_new (&self->mutex);

    return find_account_by_username (self, username);
}

static MockAccount *
find_account_by_email (MockSnapd *self, const gchar *email)
{
    for (GList *link = self->accounts; link; link = link->next) {
        MockAccount *user = link->data;
        if (strcmp (user->email, email) == 0)
            return user;
    }

    return NULL;
}

MockAccount *
mock_snapd_find_account_by_email (MockSnapd *self, const gchar *email)
{
    g_return_val_if_fail (MOCK_IS_SNAPD (self), NULL);

    g_autoptr(GMutexLocker) locker = g_mutex_locker_new (&self->mutex);

    return find_account_by_email (self, email);
}

static MockChange *
add_change (MockSnapd *self)
{
    MockChange *change = g_slice_new0 (MockChange);
    self->change_index++;
    change->id = g_strdup_printf ("%d", self->change_index);
    change->kind = g_strdup ("KIND");
    change->summary = g_strdup ("SUMMARY");
    change->task_index = self->change_index * 100;
    self->changes = g_list_append (self->changes, change);

    return change;
}

MockChange *
mock_snapd_add_change (MockSnapd *self)
{
    g_return_val_if_fail (MOCK_IS_SNAPD (self), NULL);

    g_autoptr(GMutexLocker) locker = g_mutex_locker_new (&self->mutex);

    return add_change (self);
}

MockTask *
mock_change_add_task (MockChange *change, const gchar *kind)
{
    MockTask *task = g_slice_new0 (MockTask);
    task->id = g_strdup_printf ("%d", change->task_index);
    change->task_index++;
    task->kind = g_strdup (kind);
    task->summary = g_strdup ("SUMMARY");
    task->status = g_strdup ("Do");
    task->progress_label = g_strdup ("LABEL");
    task->progress_done = 0;
    task->progress_total = 1;
    change->tasks = g_list_append (change->tasks, task);

    return task;
}

void
mock_task_set_snap_name (MockTask *task, const gchar *snap_name)
{
    task->snap_name = g_strdup (snap_name);
}

void
mock_task_set_status (MockTask *task, const gchar *status)
{
    g_free (task->status);
    task->status = g_strdup (status);
}

void
mock_task_set_progress (MockTask *task, int done, int total)
{
    task->progress_done = done;
    task->progress_total = total;
}

void
mock_task_set_spawn_time (MockTask *task, const gchar *spawn_time)
{
    g_free (task->spawn_time);
    task->spawn_time = g_strdup (spawn_time);
}

void
mock_task_set_ready_time (MockTask *task, const gchar *ready_time)
{
    g_free (task->ready_time);
    task->ready_time = g_strdup (ready_time);
}

void
mock_change_set_spawn_time (MockChange *change, const gchar *spawn_time)
{
    g_free (change->spawn_time);
    change->spawn_time = g_strdup (spawn_time);
}

void
mock_change_set_ready_time (MockChange *change, const gchar *ready_time)
{
    g_free (change->ready_time);
    change->ready_time = g_strdup (ready_time);
}

static MockSnap *
mock_snap_new (const gchar *name)
{
    MockSnap *snap = g_slice_new0 (MockSnap);
    snap->configuration = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, g_free);
    snap->confinement = g_strdup ("strict");
    snap->publisher_display_name = g_strdup ("PUBLISHER-DISPLAY-NAME");
    snap->publisher_id = g_strdup ("PUBLISHER-ID");
    snap->publisher_username = g_strdup ("PUBLISHER-USERNAME");
    snap->icon = g_strdup ("ICON");
    snap->id = g_strdup ("ID");
    snap->name = g_strdup (name);
    snap->revision = g_strdup ("REVISION");
    snap->status = g_strdup ("active");
    snap->type = g_strdup ("app");
    snap->version = g_strdup ("VERSION");

    return snap;
}

static void
mock_snapshot_free (MockSnapshot *snapshot)
{
    g_free (snapshot->name);
    g_slice_free (MockSnapshot, snapshot);
}

static MockSnapshot *
mock_snapshot_new (const gchar *name)
{
    MockSnapshot *snapshot = g_slice_new0 (MockSnapshot);
    snapshot->name = g_strdup (name);

    return snapshot;
}

MockSnap *
mock_account_add_private_snap (MockAccount *account, const gchar *name)
{
    MockSnap *snap = mock_snap_new (name);
    snap->download_size = 65535;
    snap->is_private = TRUE;
    account->private_snaps = g_list_append (account->private_snaps, snap);

    return snap;
}

static gboolean
discharges_match (GStrv discharges0, GStrv discharges1)
{
    int i;
    for (i = 0; discharges0[i] && discharges1[i]; i++)
        if (strcmp (discharges0[i], discharges1[i]) != 0)
            return FALSE;

    if (discharges0[i] != discharges1[i])
        return FALSE;

    return discharges0[i] == NULL;
}

static MockAccount *
find_account_by_macaroon (MockSnapd *self, const gchar *macaroon, GStrv discharges)
{
    for (GList *link = self->accounts; link; link = link->next) {
        MockAccount *account = link->data;
        if (strcmp (account->macaroon, macaroon) == 0 &&
            discharges_match (account->discharges, discharges))
            return account;
    }

    return NULL;
}

static void
mock_account_free (MockAccount *account)
{
    g_free (account->email);
    g_free (account->username);
    g_free (account->password);
    g_free (account->otp);
    g_strfreev (account->ssh_keys);
    g_free (account->macaroon);
    g_strfreev (account->discharges);
    g_list_free_full (account->private_snaps, (GDestroyNotify) mock_snap_free);
    g_slice_free (MockAccount, account);
}

static MockInterface *
mock_interface_new (const gchar *name)
{
    MockInterface *interface = g_slice_new0 (MockInterface);
    interface->name = g_strdup (name);

    return interface;
}

MockInterface *
mock_snapd_add_interface (MockSnapd *self, const gchar *name)
{
    g_return_val_if_fail (MOCK_IS_SNAPD (self), NULL);

    g_autoptr(GMutexLocker) locker = g_mutex_locker_new (&self->mutex);

    MockInterface *interface = mock_interface_new (name);
    self->interfaces = g_list_append (self->interfaces, interface);

    return interface;
}

void
mock_interface_set_summary (MockInterface *interface, const gchar *summary)
{
    g_free (interface->summary);
    interface->summary = g_strdup (summary);
}

void
mock_interface_set_doc_url (MockInterface *interface, const gchar *url)
{
    g_free (interface->doc_url);
    interface->doc_url = g_strdup (url);
}

MockSnap *
mock_snapd_add_snap (MockSnapd *self, const gchar *name)
{
    g_return_val_if_fail (MOCK_IS_SNAPD (self), NULL);

    g_autoptr(GMutexLocker) locker = g_mutex_locker_new (&self->mutex);

    MockSnap *snap = mock_snap_new (name);
    self->snaps = g_list_append (self->snaps, snap);

    return snap;
}

static MockSnap *
find_snap (MockSnapd *self, const gchar *name)
{
    for (GList *link = self->snaps; link; link = link->next) {
        MockSnap *snap = link->data;
        if (strcmp (snap->name, name) == 0)
            return snap;
    }

    return NULL;
}

MockSnap *
mock_snapd_find_snap (MockSnapd *self, const gchar *name)
{
    g_return_val_if_fail (MOCK_IS_SNAPD (self), NULL);

    g_autoptr(GMutexLocker) locker = g_mutex_locker_new (&self->mutex);
    return find_snap (self, name);
}

static MockSnapshot *
find_snapshot (MockSnapd *self, const gchar *name)
{
    for (GList *link = self->snapshots; link; link = link->next) {
        MockSnapshot *snapshot = link->data;
        if (strcmp (snapshot->name, name) == 0)
            return snapshot;
    }

    return NULL;
}

MockSnapshot *
mock_snapd_find_snapshot (MockSnapd *self, const gchar *name)
{
    g_return_val_if_fail (MOCK_IS_SNAPD (self), NULL);

    g_autoptr(GMutexLocker) locker = g_mutex_locker_new (&self->mutex);
    return find_snapshot (self, name);
}

void
mock_snapd_add_store_section (MockSnapd *self, const gchar *name)
{
    g_return_if_fail (MOCK_IS_SNAPD (self));

    g_autoptr(GMutexLocker) locker = g_mutex_locker_new (&self->mutex);
    self->store_sections = g_list_append (self->store_sections, g_strdup (name));
}

MockSnap *
mock_snapd_add_store_snap (MockSnapd *self, const gchar *name)
{
    g_return_val_if_fail (MOCK_IS_SNAPD (self), NULL);

    g_autoptr(GMutexLocker) locker = g_mutex_locker_new (&self->mutex);

    MockSnap *snap = mock_snap_new (name);
    snap->download_size = 65535;
    self->store_snaps = g_list_append (self->store_snaps, snap);

    mock_snap_add_track (snap, "latest");

    return snap;
}

static MockSnap *
find_store_snap_by_name (MockSnapd *self, const gchar *name, const gchar *channel,  const gchar *revision)
{
    for (GList *link = self->store_snaps; link; link = link->next) {
        MockSnap *snap = link->data;
        if (strcmp (snap->name, name) == 0 &&
            (channel == NULL || g_strcmp0 (snap->channel, channel) == 0) &&
            (revision == NULL || g_strcmp0 (snap->revision, revision) == 0))
            return snap;
    }

    return NULL;
}

static MockSnap *
find_store_snap_by_id (MockSnapd *self, const gchar *id)
{
    for (GList *link = self->store_snaps; link; link = link->next) {
        MockSnap *snap = link->data;
        if (strcmp (snap->id, id) == 0)
            return snap;
    }

    return NULL;
}

MockApp *
mock_snap_add_app (MockSnap *snap, const gchar *name)
{
    MockApp *app = g_slice_new0 (MockApp);
    app->name = g_strdup (name);
    snap->apps = g_list_append (snap->apps, app);

    return app;
}

MockApp *
mock_snap_find_app (MockSnap *snap, const gchar *name)
{
    for (GList *link = snap->apps; link; link = link->next) {
        MockApp *app = link->data;
        if (strcmp (app->name, name) == 0)
            return app;
    }

    return NULL;
}

void
mock_app_set_active (MockApp *app, gboolean active)
{
    app->active = active;
}

void
mock_app_set_enabled (MockApp *app, gboolean enabled)
{
    app->enabled = enabled;
}

void
mock_app_set_common_id (MockApp *app, const gchar *id)
{
    g_free (app->common_id);
    app->common_id = g_strdup (id);
}

void
mock_app_set_daemon (MockApp *app, const gchar *daemon)
{
    g_free (app->daemon);
    app->daemon = g_strdup (daemon);
}

void
mock_app_set_desktop_file (MockApp *app, const gchar *desktop_file)
{
    g_free (app->desktop_file);
    app->desktop_file = g_strdup (desktop_file);
}

static void
add_alias (MockApp *app, const gchar *name, gboolean automatic, gboolean enabled)
{
    MockAlias *a = g_slice_new (MockAlias);
    a->name = g_strdup (name);
    a->automatic = automatic;
    a->enabled = enabled;
    app->aliases = g_list_append (app->aliases, a);
}

void
mock_app_add_auto_alias (MockApp *app, const gchar *name)
{
    add_alias (app, name, TRUE, TRUE);
}

void
mock_app_add_manual_alias (MockApp *app, const gchar *name, gboolean enabled)
{
    MockAlias *alias = mock_app_find_alias (app, name);
    if (alias != NULL) {
        alias->enabled = enabled;
        return;
    }

    add_alias (app, name, FALSE, enabled);
}

MockAlias *
mock_app_find_alias (MockApp *app, const gchar *name)
{
    for (GList *link = app->aliases; link; link = link->next) {
        MockAlias *alias = link->data;
        if (strcmp (alias->name, name) == 0)
            return alias;
    }

    return NULL;
}

void
mock_snap_set_base (MockSnap *snap, const gchar *base)
{
    g_free (snap->base);
    snap->base = g_strdup (base);
}

void
mock_snap_set_broken (MockSnap *snap, const gchar *broken)
{
    g_free (snap->broken);
    snap->broken = g_strdup (broken);
}

void
mock_snap_set_channel (MockSnap *snap, const gchar *channel)
{
    g_free (snap->channel);
    snap->channel = g_strdup (channel);
}

const gchar *
mock_snap_get_channel (MockSnap *snap)
{
    return snap->channel;
}

void
mock_snap_set_conf (MockSnap *snap, const gchar *name, const gchar *value)
{
    g_hash_table_insert (snap->configuration, g_strdup (name), g_strdup (value));
}

gsize
mock_snap_get_conf_count (MockSnap *snap)
{
    return g_hash_table_size (snap->configuration);
}

const gchar *
mock_snap_get_conf (MockSnap *snap, const gchar *name)
{
    return g_hash_table_lookup (snap->configuration, name);
}

MockTrack *
mock_snap_add_track (MockSnap *snap, const gchar *name)
{
    for (GList *link = snap->tracks; link; link = link->next) {
        MockTrack *track = link->data;
        if (g_strcmp0 (track->name, name) == 0)
            return track;
    }

    MockTrack *track = g_slice_new0 (MockTrack);
    track->name = g_strdup (name);
    snap->tracks = g_list_append (snap->tracks, track);

    return track;
}

MockChannel *
mock_track_add_channel (MockTrack *track, const gchar *risk, const gchar *branch)
{
    MockChannel *channel = g_slice_new0 (MockChannel);
    channel->risk = g_strdup (risk);
    channel->branch = g_strdup (branch);
    channel->confinement = g_strdup ("strict");
    channel->epoch = g_strdup ("0");
    channel->revision = g_strdup ("REVISION");
    channel->size = 65535;
    channel->version = g_strdup ("VERSION");
    track->channels = g_list_append (track->channels, channel);

    return channel;
}

void
mock_channel_set_branch (MockChannel *channel, const gchar *branch)
{
    g_free (channel->branch);
    channel->branch = g_strdup (branch);
}

void
mock_channel_set_confinement (MockChannel *channel, const gchar *confinement)
{
    g_free (channel->confinement);
    channel->confinement = g_strdup (confinement);
}

void
mock_channel_set_epoch (MockChannel *channel, const gchar *epoch)
{
    g_free (channel->epoch);
    channel->epoch = g_strdup (epoch);
}

void
mock_channel_set_released_at (MockChannel *channel, const gchar *released_at)
{
    g_free (channel->released_at);
    channel->released_at = g_strdup (released_at);
}

void
mock_channel_set_revision (MockChannel *channel, const gchar *revision)
{
    g_free (channel->revision);
    channel->revision = g_strdup (revision);
}

const gchar *
mock_snap_get_revision (MockSnap *snap)
{
    return snap->revision;
}

void
mock_channel_set_size (MockChannel *channel, int size)
{
    channel->size = size;
}

void
mock_channel_set_version (MockChannel *channel, const gchar *version)
{
    g_free (channel->version);
    channel->version = g_strdup (version);
}

void
mock_snap_set_confinement (MockSnap *snap, const gchar *confinement)
{
    g_free (snap->confinement);
    snap->confinement = g_strdup (confinement);
}

const gchar *
mock_snap_get_confinement (MockSnap *snap)
{
    return snap->confinement;
}

void
mock_snap_set_contact (MockSnap *snap, const gchar *contact)
{
    g_free (snap->contact);
    snap->contact = g_strdup (contact);
}

gboolean
mock_snap_get_dangerous (MockSnap *snap)
{
    return snap->dangerous;
}

const gchar *
mock_snap_get_data (MockSnap *snap)
{
    return snap->snap_data;
}

void
mock_snap_set_description (MockSnap *snap, const gchar *description)
{
    g_free (snap->description);
    snap->description = g_strdup (description);
}

void
mock_snap_set_devmode (MockSnap *snap, gboolean devmode)
{
    snap->devmode = devmode;
}

gboolean
mock_snap_get_devmode (MockSnap *snap)
{
    return snap->devmode;
}

void
mock_snap_set_disabled (MockSnap *snap, gboolean disabled)
{
    snap->disabled = disabled;
}

gboolean
mock_snap_get_disabled (MockSnap *snap)
{
    return snap->disabled;
}

void
mock_snap_set_download_size (MockSnap *snap, int download_size)
{
    snap->download_size = download_size;
}

void
mock_snap_set_error (MockSnap *snap, const gchar *error)
{
    g_free (snap->error);
    snap->error = g_strdup (error);
}

void
mock_snap_set_icon (MockSnap *snap, const gchar *icon)
{
    g_free (snap->icon);
    snap->icon = g_strdup (icon);
}

void
mock_snap_set_icon_data (MockSnap *snap, const gchar *mime_type, GBytes *data)
{
    g_free (snap->icon_mime_type);
    snap->icon_mime_type = g_strdup (mime_type);
    g_bytes_unref (snap->icon_data);
    snap->icon_data = g_bytes_ref (data);
}

void
mock_snap_set_id (MockSnap *snap, const gchar *id)
{
    g_free (snap->id);
    snap->id = g_strdup (id);
}

void
mock_snap_set_install_date (MockSnap *snap, const gchar *install_date)
{
    g_free (snap->install_date);
    snap->install_date = g_strdup (install_date);
}

void
mock_snap_set_installed_size (MockSnap *snap, int installed_size)
{
    snap->installed_size = installed_size;
}

void
mock_snap_set_jailmode (MockSnap *snap, gboolean jailmode)
{
    snap->jailmode = jailmode;
}

gboolean
mock_snap_get_jailmode (MockSnap *snap)
{
    return snap->jailmode;
}

void
mock_snap_set_license (MockSnap *snap, const gchar *license)
{
    g_free (snap->license);
    snap->license = g_strdup (license);
}

void
mock_snap_set_mounted_from (MockSnap *snap, const gchar *mounted_from)
{
    g_free (snap->mounted_from);
    snap->mounted_from = g_strdup (mounted_from);
}

const gchar *
mock_snap_get_path (MockSnap *snap)
{
    return snap->snap_path;
}

gboolean
mock_snap_get_preferred (MockSnap *snap)
{
    return snap->preferred;
}

MockPrice *
mock_snap_add_price (MockSnap *snap, gdouble amount, const gchar *currency)
{
    MockPrice *price = g_slice_new0 (MockPrice);
    price->amount = amount;
    price->currency = g_strdup (currency);
    snap->prices = g_list_append (snap->prices, price);

    return price;
}

static gdouble
mock_snap_find_price (MockSnap *snap, const gchar *currency)
{
    for (GList *link = snap->prices; link; link = link->next) {
        MockPrice *price = link->data;
        if (strcmp (price->currency, currency) == 0)
            return price->amount;
    }

    return 0.0;
}

void
mock_snap_set_publisher_display_name (MockSnap *snap, const gchar *display_name)
{
    g_free (snap->publisher_display_name);
    snap->publisher_display_name = g_strdup (display_name);
}

void
mock_snap_set_publisher_id (MockSnap *snap, const gchar *id)
{
    g_free (snap->publisher_id);
    snap->publisher_id = g_strdup (id);
}

void
mock_snap_set_publisher_username (MockSnap *snap, const gchar *username)
{
    g_free (snap->publisher_username);
    snap->publisher_username = g_strdup (username);
}

void
mock_snap_set_publisher_validation (MockSnap *snap, const gchar *validation)
{
    g_free (snap->publisher_validation);
    snap->publisher_validation = g_strdup (validation);
}

void
mock_snap_set_restart_required (MockSnap *snap, gboolean restart_required)
{
    snap->restart_required = restart_required;
}

void
mock_snap_set_revision (MockSnap *snap, const gchar *revision)
{
    g_free (snap->revision);
    snap->revision = g_strdup (revision);
}

void
mock_snap_set_scope_is_wide (MockSnap *snap, gboolean scope_is_wide)
{
    snap->scope_is_wide = scope_is_wide;
}

MockMedia *
mock_snap_add_media (MockSnap *snap, const gchar *type, const gchar *url, int width, int height)
{
    MockMedia *media = g_slice_new0 (MockMedia);
    media->type = g_strdup (type);
    media->url = g_strdup (url);
    media->width = width;
    media->height = height;
    snap->media = g_list_append (snap->media, media);

    return media;
}

void
mock_snap_set_status (MockSnap *snap, const gchar *status)
{
    g_free (snap->status);
    snap->status = g_strdup (status);
}

void
mock_snap_set_store_url (MockSnap *snap, const gchar *store_url)
{
    g_free (snap->store_url);
    snap->store_url = g_strdup (store_url);
}

void
mock_snap_set_summary (MockSnap *snap, const gchar *summary)
{
    g_free (snap->summary);
    snap->summary = g_strdup (summary);
}

void
mock_snap_set_title (MockSnap *snap, const gchar *title)
{
    g_free (snap->title);
    snap->title = g_strdup (title);
}

void
mock_snap_set_tracking_channel (MockSnap *snap, const gchar *channel)
{
    g_free (snap->tracking_channel);
    snap->tracking_channel = g_strdup (channel);
}

const gchar *
mock_snap_get_tracking_channel (MockSnap *snap)
{
    return snap->tracking_channel;
}

void
mock_snap_set_trymode (MockSnap *snap, gboolean trymode)
{
    snap->trymode = trymode;
}

void
mock_snap_set_type (MockSnap *snap, const gchar *type)
{
    g_free (snap->type);
    snap->type = g_strdup (type);
}

void
mock_snap_set_version (MockSnap *snap, const gchar *version)
{
    g_free (snap->version);
    snap->version = g_strdup (version);
}

void
mock_snap_set_website (MockSnap *snap, const gchar *website)
{
    g_free (snap->website);
    snap->website = g_strdup (website);
}

void
mock_snap_add_store_section (MockSnap *snap, const gchar *name)
{
    snap->store_sections = g_list_append (snap->store_sections, g_strdup (name));
}

MockPlug *
mock_snap_add_plug (MockSnap *snap, MockInterface *interface, const gchar *name)
{
    MockPlug *plug = g_slice_new0 (MockPlug);
    plug->snap = snap;
    plug->name = g_strdup (name);
    plug->interface = interface;
    plug->attributes = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, g_free);
    plug->label = g_strdup ("LABEL");
    snap->plugs = g_list_append (snap->plugs, plug);

    return plug;
}

MockPlug *
mock_snap_find_plug (MockSnap *snap, const gchar *name)
{
    for (GList *link = snap->plugs; link; link = link->next) {
        MockPlug *plug = link->data;
        if (strcmp (plug->name, name) == 0)
            return plug;
    }

    return NULL;
}

void
mock_plug_add_attribute (MockPlug *plug, const gchar *name, const gchar *value)
{
    g_hash_table_insert (plug->attributes, g_strdup (name), g_strdup (value));
}

MockSlot *
mock_snap_add_slot (MockSnap *snap, MockInterface *interface, const gchar *name)
{
    MockSlot *slot = g_slice_new0 (MockSlot);
    slot->snap = snap;
    slot->name = g_strdup (name);
    slot->interface = interface;
    slot->attributes = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, g_free);
    slot->label = g_strdup ("LABEL");
    snap->slots_ = g_list_append (snap->slots_, slot);

    return slot;
}

MockSlot *
mock_snap_find_slot (MockSnap *snap, const gchar *name)
{
    for (GList *link = snap->slots_; link; link = link->next) {
        MockSlot *slot = link->data;
        if (strcmp (slot->name, name) == 0)
            return slot;
    }

    return NULL;
}

void
mock_slot_add_attribute (MockSlot *slot, const gchar *name, const gchar *value)
{
    g_hash_table_insert (slot->attributes, g_strdup (name), g_strdup (value));
}

void
mock_snapd_connect (MockSnapd *self, MockPlug *plug, MockSlot *slot, gboolean manual, gboolean gadget)
{
    g_return_if_fail (plug != NULL);

    if (slot != NULL) {
        /* Remove existing connections */
        g_autoptr(GList) old_established_connections = g_steal_pointer (&self->established_connections);
        for (GList *link = old_established_connections; link; link = link->next) {
            MockConnection *connection = link->data;
            if (connection->plug == plug)
                continue;
            self->established_connections = g_list_append (self->established_connections, connection);
        }
        g_autoptr(GList) old_undesired_connections = g_steal_pointer (&self->undesired_connections);
        for (GList *link = old_undesired_connections; link; link = link->next) {
            MockConnection *connection = link->data;
            if (connection->plug == plug)
                continue;
            self->undesired_connections = g_list_append (self->undesired_connections, connection);
        }

        MockConnection *connection = g_slice_new0 (MockConnection);
        connection->plug = plug;
        connection->slot = slot;
        connection->manual = manual;
        connection->gadget = gadget;
        self->established_connections = g_list_append (self->established_connections, connection);
    }
    else {
        /* Remove existing connections */
        MockSlot *autoconnected_slot = NULL;
        g_autoptr(GList) old_established_connections = g_steal_pointer (&self->established_connections);
        for (GList *link = old_established_connections; link; link = link->next) {
            MockConnection *connection = link->data;
            if (connection->plug == plug) {
                if (!connection->manual)
                    autoconnected_slot = connection->slot;
                mock_connection_free (connection);
                continue;
            }
            self->established_connections = g_list_append (self->established_connections, connection);
        }
        g_autoptr(GList) old_undesired_connections = g_steal_pointer (&self->undesired_connections);
        for (GList *link = old_undesired_connections; link; link = link->next) {
            MockConnection *connection = link->data;
            if (connection->plug == plug)
                continue;
            self->undesired_connections = g_list_append (self->undesired_connections, connection);
        }

        /* Mark as undesired if overriding auto connection */
        if (autoconnected_slot != NULL) {
            MockConnection *connection = g_slice_new0 (MockConnection);
            connection->plug = plug;
            connection->slot = autoconnected_slot;
            connection->manual = manual;
            connection->gadget = gadget;
            self->undesired_connections = g_list_append (self->undesired_connections, connection);
        }
    }
}

MockSlot *
mock_snapd_find_plug_connection (MockSnapd *self, MockPlug *plug)
{
    for (GList *link = self->established_connections; link; link = link->next) {
        MockConnection *connection = link->data;
        if (connection->plug == plug)
            return connection->slot;
    }

    return NULL;
}

GList *
mock_snapd_find_slot_connections (MockSnapd *self, MockSlot *slot)
{
    GList *plugs = NULL;
    for (GList *link = self->established_connections; link; link = link->next) {
        MockConnection *connection = link->data;
        if (connection->slot == slot)
            plugs = g_list_append (plugs, connection->plug);
    }

    return plugs;
}

static void
add_assertion (MockSnapd *self, const gchar *assertion)
{
    self->assertions = g_list_append (self->assertions, g_strdup (assertion));
}

void
mock_snapd_add_assertion (MockSnapd *self, const gchar *assertion)
{
    g_autoptr(GMutexLocker) locker = g_mutex_locker_new (&self->mutex);
    return add_assertion (self, assertion);
}

GList *
mock_snapd_get_assertions (MockSnapd *self)
{
    g_return_val_if_fail (MOCK_IS_SNAPD (self), NULL);

    g_autoptr(GMutexLocker) locker = g_mutex_locker_new (&self->mutex);
    return self->assertions;
}

const gchar *
mock_snapd_get_last_user_agent (MockSnapd *self)
{
    g_return_val_if_fail (MOCK_IS_SNAPD (self), NULL);

    g_autoptr(GMutexLocker) locker = g_mutex_locker_new (&self->mutex);

    if (self->last_request_headers == NULL)
        return NULL;

    return soup_message_headers_get_one (self->last_request_headers, "User-Agent");
}

const gchar *
mock_snapd_get_last_accept_language (MockSnapd *self)
{
    g_return_val_if_fail (MOCK_IS_SNAPD (self), NULL);

    g_autoptr(GMutexLocker) locker = g_mutex_locker_new (&self->mutex);

    if (self->last_request_headers == NULL)
        return NULL;

    return soup_message_headers_get_one (self->last_request_headers, "Accept-Language");
}

const gchar *
mock_snapd_get_last_allow_interaction (MockSnapd *self)
{
    g_return_val_if_fail (MOCK_IS_SNAPD (self), NULL);

    g_autoptr(GMutexLocker) locker = g_mutex_locker_new (&self->mutex);

    if (self->last_request_headers == NULL)
        return NULL;

    return soup_message_headers_get_one (self->last_request_headers, "X-Allow-Interaction");
}

void
mock_snapd_set_gtk_theme_status (MockSnapd *self, const gchar *name, const gchar *status)
{
    g_hash_table_insert (self->gtk_theme_status, g_strdup (name), g_strdup (status));
}

void
mock_snapd_set_icon_theme_status (MockSnapd *self, const gchar *name, const gchar *status)
{
    g_hash_table_insert (self->icon_theme_status, g_strdup (name), g_strdup (status));
}

void
mock_snapd_set_sound_theme_status (MockSnapd *self, const gchar *name, const gchar *status)
{
    g_hash_table_insert (self->sound_theme_status, g_strdup (name), g_strdup (status));
}

static MockChange *
get_change (MockSnapd *self, const gchar *id)
{
    for (GList *link = self->changes; link; link = link->next) {
        MockChange *change = link->data;
        if (strcmp (change->id, id) == 0)
            return change;
    }

    return NULL;
}

static void
mock_task_free (MockTask *task)
{
    g_free (task->id);
    g_free (task->kind);
    g_free (task->summary);
    g_free (task->status);
    g_free (task->progress_label);
    g_free (task->spawn_time);
    g_free (task->ready_time);
    if (task->snap != NULL)
        mock_snap_free (task->snap);
    g_free (task->snap_name);
    g_free (task->error);
    g_slice_free (MockTask, task);
}

static void
mock_change_free (MockChange *change)
{
    g_free (change->id);
    g_free (change->kind);
    g_free (change->summary);
    g_free (change->spawn_time);
    g_free (change->ready_time);
    g_list_free_full (change->tasks, (GDestroyNotify) mock_task_free);
    if (change->data != NULL)
        json_node_unref (change->data);
    g_slice_free (MockChange, change);
}

static void
send_response (SoupMessage *message, guint status_code, const gchar *content_type, const guint8 *content, gsize content_length)
{
    soup_message_set_status (message, status_code);
    soup_message_headers_set_content_type (message->response_headers,
                                           content_type, NULL);
    soup_message_headers_set_content_length (message->response_headers,
                                             content_length);

    soup_message_body_append (message->response_body, SOUP_MEMORY_COPY,
                              content, content_length);
}

static void
send_json_response (SoupMessage *message, guint status_code, JsonNode *node)
{
    g_autoptr(JsonGenerator) generator = json_generator_new ();
    json_generator_set_root (generator, node);
    gsize data_length;
    g_autofree gchar *data = json_generator_to_data (generator, &data_length);

    send_response (message, status_code, "application/json", (guint8*) data, data_length);
}

static JsonNode *
make_response (MockSnapd *self, const gchar *type, guint status_code, JsonNode *result, const gchar *change_id, const gchar *suggested_currency) // FIXME: sources
{
    g_autoptr(JsonBuilder) builder = json_builder_new ();
    json_builder_begin_object (builder);
    json_builder_set_member_name (builder, "type");
    json_builder_add_string_value (builder, type);
    json_builder_set_member_name (builder, "status-code");
    json_builder_add_int_value (builder, status_code);
    json_builder_set_member_name (builder, "status");
    json_builder_add_string_value (builder, soup_status_get_phrase (status_code));
    json_builder_set_member_name (builder, "result");
    if (result != NULL)
        json_builder_add_value (builder, result);
     else
        json_builder_add_null_value (builder); // FIXME: Snapd sets it to null, but it might be a bug?
    if (change_id != NULL) {
        json_builder_set_member_name (builder, "change");
        json_builder_add_string_value (builder, change_id);
    }
    if (suggested_currency != NULL) {
        json_builder_set_member_name (builder, "suggested-currency");
        json_builder_add_string_value (builder, suggested_currency);
    }
    if (self->maintenance_kind != NULL) {
        json_builder_set_member_name (builder, "maintenance");
        json_builder_begin_object (builder);
        json_builder_set_member_name (builder, "kind");
        json_builder_add_string_value (builder, self->maintenance_kind);
        json_builder_set_member_name (builder, "message");
        json_builder_add_string_value (builder, self->maintenance_message);
        json_builder_end_object (builder);
    }
    json_builder_end_object (builder);

    return json_builder_get_root (builder);
}

static void
send_sync_response (MockSnapd *self, SoupMessage *message, guint status_code, JsonNode *result, const gchar *suggested_currency)
{
    g_autoptr(JsonNode) response = make_response (self, "sync", status_code, result, NULL, suggested_currency);
    send_json_response (message, status_code, response);
}

static void
send_async_response (MockSnapd *self, SoupMessage *message, guint status_code, const gchar *change_id)
{
    g_autoptr(JsonNode) response = make_response (self, "async", status_code, NULL, change_id, NULL);
    send_json_response (message, status_code, response);
}

static void
send_error_response (MockSnapd *self, SoupMessage *message, guint status_code, const gchar *error_message, const gchar *kind, JsonNode *error_value)
{
    g_autoptr(JsonBuilder) builder = json_builder_new ();
    json_builder_begin_object (builder);
    json_builder_set_member_name (builder, "message");
    json_builder_add_string_value (builder, error_message);
    if (kind != NULL) {
        json_builder_set_member_name (builder, "kind");
        json_builder_add_string_value (builder, kind);
    }
    if (error_value != NULL) {
        json_builder_set_member_name (builder, "value");
        json_builder_add_value (builder, error_value);
    }
    json_builder_end_object (builder);

    g_autoptr(JsonNode) response = make_response (self, "error", status_code, json_builder_get_root (builder), NULL, NULL);
    send_json_response (message, status_code, response);
}

static void
send_error_bad_request (MockSnapd *self, SoupMessage *message, const gchar *error_message, const gchar *kind)
{
    send_error_response (self, message, 400, error_message, kind, NULL);
}

static void
send_error_unauthorized (MockSnapd *self, SoupMessage *message, const gchar *error_message, const gchar *kind)
{
    send_error_response (self, message, 401, error_message, kind, NULL);
}

static void
send_error_forbidden (MockSnapd *self, SoupMessage *message, const gchar *error_message, const gchar *kind)
{
    send_error_response (self, message, 403, error_message, kind, NULL);
}

static void
send_error_not_found (MockSnapd *self, SoupMessage *message, const gchar *error_message, const gchar *kind)
{
    send_error_response (self, message, 404, error_message, kind, NULL);
}

static void
send_error_method_not_allowed (MockSnapd *self, SoupMessage *message, const gchar *error_message)
{
    send_error_response (self, message, 405, error_message, NULL, NULL);
}

static void
handle_system_info (MockSnapd *self, SoupMessage *message)
{
    if (strcmp (message->method, "GET") != 0) {
        send_error_method_not_allowed (self, message, "method not allowed");
        return;
    }

    g_autoptr(JsonBuilder) builder = json_builder_new ();
    json_builder_begin_object (builder);
    if (self->build_id) {
        json_builder_set_member_name (builder, "build-id");
        json_builder_add_string_value (builder, self->build_id);
    }
    if (self->confinement) {
        json_builder_set_member_name (builder, "confinement");
        json_builder_add_string_value (builder, self->confinement);
    }
    json_builder_set_member_name (builder, "os-release");
    json_builder_begin_object (builder);
    json_builder_set_member_name (builder, "id");
    json_builder_add_string_value (builder, "OS-ID");
    json_builder_set_member_name (builder, "version-id");
    json_builder_add_string_value (builder, "OS-VERSION");
    json_builder_end_object (builder);
    json_builder_set_member_name (builder, "series");
    json_builder_add_string_value (builder, "SERIES");
    json_builder_set_member_name (builder, "version");
    json_builder_add_string_value (builder, "VERSION");
    json_builder_set_member_name (builder, "managed");
    json_builder_add_boolean_value (builder, self->managed);
    json_builder_set_member_name (builder, "on-classic");
    json_builder_add_boolean_value (builder, self->on_classic);
    json_builder_set_member_name (builder, "kernel-version");
    json_builder_add_string_value (builder, "KERNEL-VERSION");
    json_builder_set_member_name (builder, "locations");
    json_builder_begin_object (builder);
    json_builder_set_member_name (builder, "snap-mount-dir");
    json_builder_add_string_value (builder, "/snap");
    json_builder_set_member_name (builder, "snap-bin-dir");
    json_builder_add_string_value (builder, "/snap/bin");
    json_builder_end_object (builder);
    if (g_hash_table_size (self->sandbox_features) > 0) {
        json_builder_set_member_name (builder, "sandbox-features");
        json_builder_begin_object (builder);
        GHashTableIter iter;
        gpointer key, value;
        g_hash_table_iter_init (&iter, self->sandbox_features);
        while (g_hash_table_iter_next (&iter, &key, &value)) {
            const gchar *backend = key;
            GPtrArray *backend_features = value;

            json_builder_set_member_name (builder, backend);
            json_builder_begin_array (builder);
            for (guint i = 0; i < backend_features->len; i++)
                json_builder_add_string_value (builder, g_ptr_array_index (backend_features, i));
            json_builder_end_array (builder);
        }
        json_builder_end_object (builder);
    }
    if (self->store) {
        json_builder_set_member_name (builder, "store");
        json_builder_add_string_value (builder, self->store);
    }
    json_builder_set_member_name (builder, "refresh");
    json_builder_begin_object (builder);
    if (self->refresh_timer) {
        json_builder_set_member_name (builder, "timer");
        json_builder_add_string_value (builder, self->refresh_timer);
    }
    else if (self->refresh_schedule) {
        json_builder_set_member_name (builder, "schedule");
        json_builder_add_string_value (builder, self->refresh_schedule);
    }
    if (self->refresh_last) {
        json_builder_set_member_name (builder, "last");
        json_builder_add_string_value (builder, self->refresh_last);
    }
    if (self->refresh_next) {
        json_builder_set_member_name (builder, "next");
        json_builder_add_string_value (builder, self->refresh_next);
    }
    if (self->refresh_hold) {
        json_builder_set_member_name (builder, "hold");
        json_builder_add_string_value (builder, self->refresh_hold);
    }
    json_builder_end_object (builder);
    json_builder_end_object (builder);

    send_sync_response (self, message, 200, json_builder_get_root (builder), NULL);
}

static gboolean
parse_macaroon (const gchar *authorization, gchar **macaroon, GStrv *discharges)
{
    if (authorization == NULL)
        return FALSE;

    const gchar *i = authorization;
    while (isspace (*i)) i++;
    const gchar *start = i;
    while (*i && !isspace (*i)) i++;
    g_autofree gchar *scheme = g_strndup (start, i - start);
    if (strcmp (scheme, "Macaroon") != 0)
        return FALSE;

    g_autoptr(GPtrArray) d = g_ptr_array_new_with_free_func (g_free);
    g_autofree gchar *m = NULL;
    while (TRUE) {
        while (isspace (*i)) i++;
        if (*i == '\0')
            break;

        start = i;
        while (*i && !(isspace (*i) || *i == '=')) i++;
        g_autofree gchar *param_name = g_strndup (start, i - start);
        if (*i == '\0')
            break;
        i++;

        while (isspace (*i)) i++;
        if (*i == '\0')
            break;
        g_autofree gchar *param_value = NULL;
        if (*i == '"') {
            i++;
            start = i;
            while (*i && *i != '"') i++;
            param_value = g_strndup (start, i - start);
            i++;
            while (isspace (*i)) i++;
            while (*i && *i != ',') i++;
        }
        else {
            start = i;
            while (*i && *i != ',') i++;
            param_value = g_strndup (start, i - start);
        }

        if (strcmp (param_name, "root") == 0) {
             g_free (m);
             m = g_steal_pointer (&param_value);
        }
        else if (strcmp (param_name, "discharge") == 0) {
             g_ptr_array_add (d, g_steal_pointer (&param_value));
        }

        if (*i != ',')
            break;
        i++;
    }
    g_ptr_array_add (d, NULL);

    if (m == NULL)
        return FALSE;

    *macaroon = g_steal_pointer (&m);
    *discharges = (GStrv) d->pdata;
    g_ptr_array_free (d, FALSE);
    d = NULL;
    return TRUE;
}

static MockAccount *
get_account (MockSnapd *self, SoupMessage *message)
{
    const gchar *authorization = soup_message_headers_get_one (message->request_headers, "Authorization");
    if (authorization == NULL)
        return NULL;

    g_autofree gchar *macaroon = NULL;
    g_auto(GStrv) discharges = NULL;
    if (!parse_macaroon (authorization, &macaroon, &discharges))
        return NULL;

    return find_account_by_macaroon (self, macaroon, discharges);
}

static JsonNode *
get_json (SoupMessage *message)
{
    const gchar *content_type = soup_message_headers_get_content_type (message->request_headers, NULL);
    if (content_type == NULL)
        return NULL;

    g_autoptr(JsonParser) parser = json_parser_new ();
    g_autoptr(GError) error = NULL;
    if (!json_parser_load_from_data (parser, message->request_body->data, message->request_body->length, &error)) {
        g_warning ("Failed to parse request: %s", error->message);
        return NULL;
    }

    return json_node_ref (json_parser_get_root (parser));
}

static void
add_user_response (JsonBuilder *builder, MockAccount *account, gboolean send_macaroon, gboolean send_ssh_keys, gboolean send_email)
{
    json_builder_begin_object (builder);
    json_builder_set_member_name (builder, "id");
    json_builder_add_int_value (builder, account->id);
    if (send_email && account->email != NULL) {
        json_builder_set_member_name (builder, "email");
        json_builder_add_string_value (builder, account->email);
    }
    json_builder_set_member_name (builder, "username");
    json_builder_add_string_value (builder, account->username);
    if (send_ssh_keys && account->ssh_keys != NULL) {
        json_builder_set_member_name (builder, "ssh-keys");
        json_builder_begin_array (builder);
        for (int i = 0; account->ssh_keys[i] != NULL; i++)
            json_builder_add_string_value (builder, account->ssh_keys[i]);
        json_builder_end_array (builder);
    }
    if (send_macaroon && account->macaroon != NULL) {
        json_builder_set_member_name (builder, "macaroon");
        json_builder_add_string_value (builder, account->macaroon);
        json_builder_set_member_name (builder, "discharges");
        json_builder_begin_array (builder);
        for (int i = 0; account->discharges[i] != NULL; i++)
            json_builder_add_string_value (builder, account->discharges[i]);
        json_builder_end_array (builder);
    }
    json_builder_end_object (builder);
}

static void
handle_login (MockSnapd *self, SoupMessage *message)
{
    if (strcmp (message->method, "POST") != 0) {
        send_error_method_not_allowed (self, message, "method not allowed");
        return;
    }

    g_autoptr(JsonNode) request = get_json (message);
    if (request == NULL) {
        send_error_bad_request (self, message, "unknown content type", NULL);
        return;
    }

    JsonObject *o = json_node_get_object (request);
    const gchar *email = json_object_get_string_member (o, "email");
    if (email == NULL)
        email = json_object_get_string_member (o, "username");
    const gchar *password = json_object_get_string_member (o, "password");
    const gchar *otp = NULL;
    if (json_object_has_member (o, "otp"))
        otp = json_object_get_string_member (o, "otp");

    if (strstr (email, "@") == NULL) {
        send_error_bad_request (self, message, "please use a valid email address.", "invalid-auth-data");
        return;
    }

    MockAccount *account = find_account_by_email (self, email);
    if (account == NULL) {
        send_error_unauthorized (self, message, "cannot authenticate to snap store: Provided email/password is not correct.", "login-required");
        return;
    }

    if (strcmp (account->password, password) != 0) {
        send_error_unauthorized (self, message, "cannot authenticate to snap store: Provided email/password is not correct.", "login-required");
        return;
    }

    if (account->otp != NULL) {
        if (otp == NULL) {
            send_error_unauthorized (self, message, "two factor authentication required", "two-factor-required");
            return;
        }
        if (strcmp (account->otp, otp) != 0) {
            send_error_unauthorized (self, message, "two factor authentication failed", "two-factor-failed");
            return;
        }
    }

    g_autoptr(JsonBuilder) builder = json_builder_new ();
    add_user_response (builder, account, TRUE, FALSE, TRUE);
    send_sync_response (self, message, 200, json_builder_get_root (builder), NULL);
}

static void
handle_logout (MockSnapd *self, SoupMessage *message)
{
    if (strcmp (message->method, "POST") != 0) {
        send_error_method_not_allowed (self, message, "method not allowed");
        return;
    }

    g_autoptr(JsonNode) request = get_json (message);
    if (request == NULL) {
        send_error_bad_request (self, message, "unknown content type", NULL);
        return;
    }

    JsonObject *o = json_node_get_object (request);
    int id = json_object_get_int_member (o, "id");

    MockAccount *account = find_account_by_id (self, id);
    if (account == NULL || get_account (self, message) != account) {
        send_error_bad_request (self, message, "not logged in", NULL);
        return;
    }

    self->accounts = g_list_remove (self->accounts, account);
    mock_account_free (account);

    send_sync_response (self, message, 200, NULL, NULL);
}

static JsonNode *
make_app_node (MockApp *app, const gchar *snap_name)
{
    g_autoptr(JsonBuilder) builder = json_builder_new ();
    json_builder_begin_object (builder);
    if (snap_name != NULL) {
        json_builder_set_member_name (builder, "snap");
        json_builder_add_string_value (builder, snap_name);
    }
    if (app->name != NULL) {
        json_builder_set_member_name (builder, "name");
        json_builder_add_string_value (builder, app->name);
    }
    if (app->common_id != NULL) {
        json_builder_set_member_name (builder, "common-id");
        json_builder_add_string_value (builder, app->common_id);
    }
    if (app->daemon != NULL) {
        json_builder_set_member_name (builder, "daemon");
        json_builder_add_string_value (builder, app->daemon);
    }
    if (app->desktop_file != NULL) {
        json_builder_set_member_name (builder, "desktop-file");
        json_builder_add_string_value (builder, app->desktop_file);
    }
    if (app->active) {
        json_builder_set_member_name (builder, "active");
        json_builder_add_boolean_value (builder, TRUE);
    }
    if (app->enabled) {
        json_builder_set_member_name (builder, "enabled");
        json_builder_add_boolean_value (builder, TRUE);
    }
    json_builder_end_object (builder);

    return json_builder_get_root (builder);
}

static JsonNode *
make_snap_node (MockSnap *snap)
{
    g_autoptr(JsonBuilder) builder = json_builder_new ();
    json_builder_begin_object (builder);
    if (snap->apps != NULL) {
        json_builder_set_member_name (builder, "apps");
        json_builder_begin_array (builder);
        for (GList *link = snap->apps; link; link = link->next) {
            MockApp *app = link->data;
            json_builder_add_value (builder, make_app_node (app, NULL));
        }
        json_builder_end_array (builder);
    }
    if (snap->broken) {
        json_builder_set_member_name (builder, "broken");
        json_builder_add_string_value (builder, snap->broken);
    }
    if (snap->base) {
        json_builder_set_member_name (builder, "base");
        json_builder_add_string_value (builder, snap->base);
    }
    if (snap->channel) {
        json_builder_set_member_name (builder, "channel");
        json_builder_add_string_value (builder, snap->channel);
    }
    if (snap->tracks != NULL) {
        json_builder_set_member_name (builder, "channels");
        json_builder_begin_object (builder);
        for (GList *link = snap->tracks; link; link = link->next) {
            MockTrack *track = link->data;

            for (GList *channel_link = track->channels; channel_link; channel_link = channel_link->next) {
                MockChannel *channel = channel_link->data;

                g_autofree gchar *key = NULL;
                if (channel->branch != NULL)
                    key = g_strdup_printf ("%s/%s/%s", track->name, channel->risk, channel->branch);
                else
                    key = g_strdup_printf ("%s/%s", track->name, channel->risk);

                g_autofree gchar *name = NULL;
                if (g_strcmp0 (track->name, "latest") == 0) {
                    if (channel->branch != NULL)
                        name = g_strdup_printf ("%s/%s", channel->risk, channel->branch);
                    else
                        name = g_strdup (channel->risk);
                }
                else
                    name = g_strdup (key);

                json_builder_set_member_name (builder, key);
                json_builder_begin_object (builder);
                json_builder_set_member_name (builder, "revision");
                json_builder_add_string_value (builder, channel->revision);
                json_builder_set_member_name (builder, "confinement");
                json_builder_add_string_value (builder, channel->confinement);
                json_builder_set_member_name (builder, "version");
                json_builder_add_string_value (builder, channel->version);
                json_builder_set_member_name (builder, "channel");
                json_builder_add_string_value (builder, name);
                json_builder_set_member_name (builder, "epoch");
                json_builder_add_string_value (builder, channel->epoch);
                json_builder_set_member_name (builder, "size");
                json_builder_add_int_value (builder, channel->size);
                if (channel->released_at != NULL) {
                    json_builder_set_member_name (builder, "released-at");
                    json_builder_add_string_value (builder, channel->released_at);
                }
                json_builder_end_object (builder);
            }
        }
        json_builder_end_object (builder);
    }
    if (snap->apps != NULL) {
        int id_count = 0;
        for (GList *link = snap->apps; link; link = link->next) {
            MockApp *app = link->data;
            if (app->common_id == NULL)
                continue;
            if (id_count == 0) {
                json_builder_set_member_name (builder, "common-ids");
                json_builder_begin_array (builder);
            }
            id_count++;
            json_builder_add_string_value (builder, app->common_id);
        }
        if (id_count > 0)
            json_builder_end_array (builder);
    }
    json_builder_set_member_name (builder, "confinement");
    json_builder_add_string_value (builder, snap->confinement);
    if (snap->contact) {
        json_builder_set_member_name (builder, "contact");
        json_builder_add_string_value (builder, snap->contact);
    }
    if (snap->description) {
        json_builder_set_member_name (builder, "description");
        json_builder_add_string_value (builder, snap->description);
    }
    json_builder_set_member_name (builder, "developer");
    json_builder_add_string_value (builder, snap->publisher_username);
    json_builder_set_member_name (builder, "devmode");
    json_builder_add_boolean_value (builder, snap->devmode);
    if (snap->download_size > 0) {
        json_builder_set_member_name (builder, "download-size");
        json_builder_add_int_value (builder, snap->download_size);
    }
    json_builder_set_member_name (builder, "icon");
    json_builder_add_string_value (builder, snap->icon);
    json_builder_set_member_name (builder, "id");
    json_builder_add_string_value (builder, snap->id);
    if (snap->install_date != NULL) {
        json_builder_set_member_name (builder, "install-date");
        json_builder_add_string_value (builder, snap->install_date);
    }
    if (snap->installed_size > 0) {
        json_builder_set_member_name (builder, "installed-size");
        json_builder_add_int_value (builder, snap->installed_size);
    }
    json_builder_set_member_name (builder, "jailmode");
    json_builder_add_boolean_value (builder, snap->jailmode);
    if (snap->license) {
        json_builder_set_member_name (builder, "license");
        json_builder_add_string_value (builder, snap->license);
    }
    if (snap->mounted_from) {
        json_builder_set_member_name (builder, "mounted-from");
        json_builder_add_string_value (builder, snap->mounted_from);
    }
    int screenshot_count = 0;
    if (snap->media != NULL) {
        json_builder_set_member_name (builder, "media");
        json_builder_begin_array (builder);
        for (GList *link = snap->media; link; link = link->next) {
            MockMedia *media = link->data;

            if (strcmp (media->type, "screenshot") == 0)
                screenshot_count++;

            json_builder_begin_object (builder);
            json_builder_set_member_name (builder, "type");
            json_builder_add_string_value (builder, media->type);
            json_builder_set_member_name (builder, "url");
            json_builder_add_string_value (builder, media->url);
            if (media->width > 0 && media->height > 0) {
                json_builder_set_member_name (builder, "width");
                json_builder_add_int_value (builder, media->width);
                json_builder_set_member_name (builder, "height");
                json_builder_add_int_value (builder, media->height);
            }
            json_builder_end_object (builder);
        }
        json_builder_end_array (builder);
    }
    json_builder_set_member_name (builder, "name");
    json_builder_add_string_value (builder, snap->name);
    if (snap->prices != NULL) {
        json_builder_set_member_name (builder, "prices");
        json_builder_begin_object (builder);
        for (GList *link = snap->prices; link; link = link->next) {
            MockPrice *price = link->data;

            json_builder_set_member_name (builder, price->currency);
            json_builder_add_double_value (builder, price->amount);
        }
        json_builder_end_object (builder);
    }
    if (snap->is_private) {
        json_builder_set_member_name (builder, "private");
        json_builder_add_boolean_value (builder, TRUE);
    }
    json_builder_set_member_name (builder, "publisher");
    json_builder_begin_object (builder);
    json_builder_set_member_name (builder, "id");
    json_builder_add_string_value (builder, snap->publisher_id);
    json_builder_set_member_name (builder, "username");
    json_builder_add_string_value (builder, snap->publisher_username);
    json_builder_set_member_name (builder, "display-name");
    json_builder_add_string_value (builder, snap->publisher_display_name);
    if (snap->publisher_validation != NULL) {
        json_builder_set_member_name (builder, "validation");
        json_builder_add_string_value (builder, snap->publisher_validation);
    }
    json_builder_end_object (builder);
    json_builder_set_member_name (builder, "resource");
    g_autofree gchar *resource = g_strdup_printf ("/v2/snaps/%s", snap->name);
    json_builder_add_string_value (builder, resource);
    json_builder_set_member_name (builder, "revision");
    json_builder_add_string_value (builder, snap->revision);
    json_builder_set_member_name (builder, "screenshots");
    json_builder_begin_array (builder);
    json_builder_begin_object (builder);
    json_builder_set_member_name (builder, "note");
    json_builder_add_string_value (builder, "'screenshots' is deprecated; use 'media' instead. More info at https://forum.snapcraft.io/t/8086");
    json_builder_end_object (builder);
    json_builder_end_array (builder);
    json_builder_set_member_name (builder, "status");
    json_builder_add_string_value (builder, snap->status);
    if (snap->store_url) {
        json_builder_set_member_name (builder, "store-url");
        json_builder_add_string_value (builder, snap->store_url);
    }
    if (snap->summary) {
        json_builder_set_member_name (builder, "summary");
        json_builder_add_string_value (builder, snap->summary);
    }
    if (snap->title) {
        json_builder_set_member_name (builder, "title");
        json_builder_add_string_value (builder, snap->title);
    }
    if (snap->tracking_channel) {
        json_builder_set_member_name (builder, "tracking-channel");
        json_builder_add_string_value (builder, snap->tracking_channel);
    }
    if (snap->tracks) {
        json_builder_set_member_name (builder, "tracks");
        json_builder_begin_array (builder);
        for (GList *link = snap->tracks; link; link = link->next) {
            MockTrack *track = link->data;
            json_builder_add_string_value (builder, track->name);
        }
        json_builder_end_array (builder);
    }
    json_builder_set_member_name (builder, "trymode");
    json_builder_add_boolean_value (builder, snap->trymode);
    json_builder_set_member_name (builder, "type");
    json_builder_add_string_value (builder, snap->type);
    json_builder_set_member_name (builder, "version");
    json_builder_add_string_value (builder, snap->version);
    if (snap->website) {
        json_builder_set_member_name (builder, "website");
        json_builder_add_string_value (builder, snap->website);
    }
    json_builder_end_object (builder);

    return json_builder_get_root (builder);
}

static GList *
get_refreshable_snaps (MockSnapd *self)
{
    g_autoptr(GList) refreshable_snaps = NULL;
    for (GList *link = self->store_snaps; link; link = link->next) {
        MockSnap *store_snap = link->data;
        MockSnap *snap;

        snap = find_snap (self, store_snap->name);
        if (snap != NULL && strcmp (store_snap->revision, snap->revision) > 0)
            refreshable_snaps = g_list_append (refreshable_snaps, store_snap);
    }

    return g_steal_pointer (&refreshable_snaps);
}

static gboolean
filter_snaps (GStrv selected_snaps, MockSnap *snap)
{
    /* If no filter selected, then return all snaps */
    if (selected_snaps == NULL || selected_snaps[0] == NULL)
        return TRUE;

    for (int i = 0; selected_snaps[i] != NULL; i++) {
        if (strcmp (selected_snaps[i], snap->name) == 0)
            return TRUE;
    }

    return FALSE;
}

static void
handle_snaps (MockSnapd *self, SoupMessage *message, GHashTable *query)
{
    const gchar *content_type = soup_message_headers_get_content_type (message->request_headers, NULL);

    if (strcmp (message->method, "GET") == 0) {
        const gchar *select_param = NULL;
        g_auto(GStrv) selected_snaps = NULL;
        if (query != NULL) {
            const gchar *snaps_param = NULL;

            select_param = g_hash_table_lookup (query, "select");
            snaps_param = g_hash_table_lookup (query, "snaps");
            if (snaps_param != NULL)
                selected_snaps = g_strsplit (snaps_param, ",", -1);
        }

        g_autoptr(JsonBuilder) builder = json_builder_new ();
        json_builder_begin_array (builder);
        for (GList *link = self->snaps; link; link = link->next) {
            MockSnap *snap = link->data;

            if (!filter_snaps (selected_snaps, snap))
                continue;

            if ((select_param == NULL || strcmp (select_param, "enabled") == 0) && strcmp (snap->status, "active") != 0)
                continue;

            json_builder_add_value (builder, make_snap_node (snap));
        }
        json_builder_end_array (builder);

        send_sync_response (self, message, 200, json_builder_get_root (builder), NULL);
    }
    else if (strcmp (message->method, "POST") == 0 && g_strcmp0 (content_type, "application/json") == 0) {
        g_autoptr(JsonNode) request = get_json (message);
        if (request == NULL) {
            send_error_bad_request (self, message, "unknown content type", NULL);
            return;
        }

        JsonObject *o = json_node_get_object (request);
        const gchar *action = json_object_get_string_member (o, "action");
        if (strcmp (action, "refresh") == 0) {
            g_autoptr(GList) refreshable_snaps = get_refreshable_snaps (self);

            g_autoptr(JsonBuilder) builder = json_builder_new ();
            json_builder_begin_object (builder);
            json_builder_set_member_name (builder, "snap-names");
            json_builder_begin_array (builder);
            for (GList *link = refreshable_snaps; link; link = link->next) {
                MockSnap *snap = link->data;
                json_builder_add_string_value (builder, snap->name);
            }
            json_builder_end_array (builder);
            json_builder_end_object (builder);

            MockChange *change = add_change (self);
            change->data = json_builder_get_root (builder);
            mock_change_add_task (change, "refresh");
            send_async_response (self, message, 202, change->id);
        }
        else {
            send_error_bad_request (self, message, "unsupported multi-snap operation", NULL);
            return;
        }
    }
    else if (strcmp (message->method, "POST") == 0 && g_str_has_prefix (content_type, "multipart/")) {
        g_autoptr(SoupMultipart) multipart = soup_multipart_new_from_message (message->request_headers, message->request_body);
        if (multipart == NULL) {
            send_error_bad_request (self, message, "cannot read POST form", NULL);
            return;
        }

        gboolean classic = FALSE, dangerous = FALSE, devmode = FALSE, jailmode = FALSE;
        g_autofree gchar *action = NULL;
        g_autofree gchar *snap = NULL;
        g_autofree gchar *snap_path = NULL;
        for (int i = 0; i < soup_multipart_get_length (multipart); i++) {
            SoupMessageHeaders *part_headers;
            SoupBuffer *part_body;
            g_autofree gchar *disposition = NULL;
            g_autoptr(GHashTable) params = NULL;

            if (!soup_multipart_get_part (multipart, i, &part_headers, &part_body))
                continue;
            if (!soup_message_headers_get_content_disposition (part_headers, &disposition, &params))
                continue;

            if (strcmp (disposition, "form-data") == 0) {
                const gchar *name = g_hash_table_lookup (params, "name");

                if (g_strcmp0 (name, "action") == 0)
                    action = g_strndup (part_body->data, part_body->length);
                else if (g_strcmp0 (name, "classic") == 0)
                    classic = strncmp (part_body->data, "true", part_body->length) == 0;
                else if (g_strcmp0 (name, "dangerous") == 0)
                    dangerous = strncmp (part_body->data, "true", part_body->length) == 0;
                else if (g_strcmp0 (name, "devmode") == 0)
                    devmode = strncmp (part_body->data, "true", part_body->length) == 0;
                else if (g_strcmp0 (name, "jailmode") == 0)
                    jailmode = strncmp (part_body->data, "true", part_body->length) == 0;
                else if (g_strcmp0 (name, "snap") == 0)
                    snap = g_strndup (part_body->data, part_body->length);
                else if (g_strcmp0 (name, "snap-path") == 0)
                    snap_path = g_strndup (part_body->data, part_body->length);
            }
        }

        if (g_strcmp0 (action, "try") == 0) {
            if (snap_path == NULL) {
                send_error_bad_request (self, message, "need 'snap-path' value in form", NULL);
                return;
            }

            if (strcmp (snap_path, "*") == 0) {
                send_error_bad_request (self, message, "directory does not contain an unpacked snap", "snap-not-a-snap");
                return;
            }

            MockChange *change = add_change (self);
            mock_change_set_spawn_time (change, self->spawn_time);
            mock_change_set_ready_time (change, self->ready_time);
            MockTask *task = mock_change_add_task (change, "try");
            task->snap = mock_snap_new ("try");
            task->snap->trymode = TRUE;
            task->snap->snap_path = g_steal_pointer (&snap_path);

            send_async_response (self, message, 202, change->id);
        }
        else {
            if (snap == NULL) {
                send_error_bad_request (self, message, "cannot find \"snap\" file field in provided multipart/form-data payload", NULL);
                return;
            }

            MockChange *change = add_change (self);
            mock_change_set_spawn_time (change, self->spawn_time);
            mock_change_set_ready_time (change, self->ready_time);
            MockTask *task = mock_change_add_task (change, "install");
            task->snap = mock_snap_new ("sideload");
            if (classic)
                mock_snap_set_confinement (task->snap, "classic");
            task->snap->dangerous = dangerous;
            task->snap->devmode = devmode; // FIXME: Should set confinement to devmode?
            task->snap->jailmode = jailmode;
            g_free (task->snap->snap_data);
            task->snap->snap_data = g_steal_pointer (&snap);

            send_async_response (self, message, 202, change->id);
        }
    }
    else {
        send_error_method_not_allowed (self, message, "method not allowed");
        return;
    }
}

static void
handle_snap (MockSnapd *self, SoupMessage *message, const gchar *name)
{
    if (strcmp (message->method, "GET") == 0) {
        MockSnap *snap = find_snap (self, name);
        if (snap != NULL)
            send_sync_response (self, message, 200, make_snap_node (snap), NULL);
        else
            send_error_not_found (self, message, "cannot find snap", "snap-not-found");
    }
    else if (strcmp (message->method, "POST") == 0) {
        g_autoptr(JsonNode) request = get_json (message);
        if (request == NULL) {
            send_error_bad_request (self, message, "unknown content type", NULL);
            return;
        }

        JsonObject *o = json_node_get_object (request);
        const gchar *action = json_object_get_string_member (o, "action");
        const gchar *channel = NULL, *revision = NULL;
        gboolean classic = FALSE, dangerous = FALSE, devmode = FALSE, jailmode = FALSE, purge = FALSE;
        if (json_object_has_member (o, "channel"))
            channel = json_object_get_string_member (o, "channel");
        if (json_object_has_member (o, "revision"))
            revision = json_object_get_string_member (o, "revision");
        if (json_object_has_member (o, "classic"))
            classic = json_object_get_boolean_member (o, "classic");
        if (json_object_has_member (o, "dangerous"))
            dangerous = json_object_get_boolean_member (o, "dangerous");
        if (json_object_has_member (o, "devmode"))
            devmode = json_object_get_boolean_member (o, "devmode");
        if (json_object_has_member (o, "jailmode"))
            jailmode = json_object_get_boolean_member (o, "jailmode");
        if (json_object_has_member (o, "purge"))
            purge = json_object_get_boolean_member (o, "purge");

        if (strcmp (action, "install") == 0) {
            if (self->decline_auth) {
                send_error_forbidden (self, message, "cancelled", "auth-cancelled");
                return;
            }

            MockSnap *snap = find_snap (self, name);
            if (snap != NULL) {
                send_error_bad_request (self, message, "snap is already installed", "snap-already-installed");
                return;
            }

            snap = find_store_snap_by_name (self, name, NULL, NULL);
            if (snap == NULL) {
                send_error_not_found (self, message, "cannot install, snap not found", "snap-not-found");
                return;
            }

            snap = find_store_snap_by_name (self, name, channel, NULL);
            if (snap == NULL) {
                send_error_not_found (self, message, "no snap revision on specified channel", "snap-channel-not-available");
                return;
            }

            snap = find_store_snap_by_name (self, name, channel, revision);
            if (snap == NULL) {
                send_error_not_found (self, message, "no snap revision available as specified", "snap-revision-not-available");
                return;
            }

            if (strcmp (snap->confinement, "classic") == 0 && !classic) {
                send_error_bad_request (self, message, "requires classic confinement", "snap-needs-classic");
                return;
            }
            if (strcmp (snap->confinement, "classic") == 0 && !self->on_classic) {
                send_error_bad_request (self, message, "requires classic confinement which is only available on classic systems", "snap-needs-classic-system");
                return;
            }
            if (classic && strcmp (snap->confinement, "classic") != 0) {
                send_error_bad_request (self, message, "snap not compatible with --classic", "snap-not-classic");
                return;
            }
            if (strcmp (snap->confinement, "devmode") == 0 && !devmode) {
                send_error_bad_request (self, message, "requires devmode or confinement override", "snap-needs-devmode");
                return;
            }

            MockChange *change = add_change (self);
            mock_change_set_spawn_time (change, self->spawn_time);
            mock_change_set_ready_time (change, self->ready_time);
            MockTask *task = mock_change_add_task (change, "install");
            task->snap = mock_snap_new (name);
            mock_snap_set_confinement (task->snap, snap->confinement);
            mock_snap_set_channel (task->snap, snap->channel);
            mock_snap_set_revision (task->snap, snap->revision);
            task->snap->devmode = devmode;
            task->snap->jailmode = jailmode;
            task->snap->dangerous = dangerous;
            if (snap->error != NULL)
                task->error = g_strdup (snap->error);

            send_async_response (self, message, 202, change->id);
        }
        else if (strcmp (action, "refresh") == 0) {
            MockSnap *snap = find_snap (self, name);
            if (snap != NULL) {
                /* Find if we have a store snap with a newer revision */
                MockSnap *store_snap = find_store_snap_by_name (self, name, channel, NULL);
                if (store_snap == NULL) {
                    send_error_bad_request (self, message, "cannot perform operation on local snap", "snap-local");
                }
                else if (strcmp (store_snap->revision, snap->revision) > 0) {
                    mock_snap_set_channel (snap, channel);

                    MockChange *change = add_change (self);
                    mock_change_set_spawn_time (change, self->spawn_time);
                    mock_change_set_ready_time (change, self->ready_time);
                    mock_change_add_task (change, "refresh");
                    send_async_response (self, message, 202, change->id);
                }
                else
                    send_error_bad_request (self, message, "snap has no updates available", "snap-no-update-available");
            }
            else
                send_error_bad_request (self, message, "cannot refresh: cannot find snap", "snap-not-installed");
        }
        else if (strcmp (action, "remove") == 0) {
            if (self->decline_auth) {
                send_error_forbidden (self, message, "cancelled", "auth-cancelled");
                return;
            }

            MockSnap *snap = find_snap (self, name);
            if (snap != NULL) {
                MockChange *change = add_change (self);
                mock_change_set_spawn_time (change, self->spawn_time);
                mock_change_set_ready_time (change, self->ready_time);
                MockTask *task = mock_change_add_task (change, "remove");
                mock_task_set_snap_name (task, name);
                task->purge = purge;
                if (snap->error != NULL)
                    task->error = g_strdup (snap->error);

                send_async_response (self, message, 202, change->id);
            }
            else
                send_error_bad_request (self, message, "snap is not installed", "snap-not-installed");
        }
        else if (strcmp (action, "enable") == 0) {
            MockSnap *snap = find_snap (self, name);
            if (snap != NULL) {
                if (!snap->disabled) {
                    send_error_bad_request (self, message, "cannot enable: snap is already enabled", NULL);
                    return;
                }
                snap->disabled = FALSE;

                MockChange *change = add_change (self);
                mock_change_add_task (change, "enable");
                send_async_response (self, message, 202, change->id);
            }
            else
                send_error_bad_request (self, message, "cannot enable: cannot find snap", "snap-not-installed");
        }
        else if (strcmp (action, "disable") == 0) {
            MockSnap *snap = find_snap (self, name);
            if (snap != NULL) {
                if (snap->disabled) {
                    send_error_bad_request (self, message, "cannot disable: snap is already disabled", NULL);
                    return;
                }
                snap->disabled = TRUE;

                MockChange *change = add_change (self);
                mock_change_set_spawn_time (change, self->spawn_time);
                mock_change_set_ready_time (change, self->ready_time);
                mock_change_add_task (change, "disable");
                send_async_response (self, message, 202, change->id);
            }
            else
                send_error_bad_request (self, message, "cannot disable: cannot find snap", "snap-not-installed");
        }
        else if (strcmp (action, "switch") == 0) {
            MockSnap *snap = find_snap (self, name);
            if (snap != NULL) {
                mock_snap_set_tracking_channel (snap, channel);

                MockChange *change = add_change (self);
                mock_change_set_spawn_time (change, self->spawn_time);
                mock_change_set_ready_time (change, self->ready_time);
                mock_change_add_task (change, "switch");
                send_async_response (self, message, 202, change->id);
            }
            else
                send_error_bad_request (self, message, "cannot switch: cannot find snap", "snap-not-installed");
        }
        else
            send_error_bad_request (self, message, "unknown action", NULL);
    }
    else
        send_error_method_not_allowed (self, message, "method not allowed");
}

static gint
compare_keys (gconstpointer a, gconstpointer b)
{
    gchar *name_a = *((gchar **) a);
    gchar *name_b = *((gchar **) b);
    return strcmp (name_a, name_b);
}

static void
handle_snap_conf (MockSnapd *self, SoupMessage *message, const gchar *name, GHashTable *query)
{
    if (strcmp (message->method, "GET") == 0) {
        MockSnap *snap;
        if (strcmp (name, "system") == 0)
            snap = find_snap (self, "core");
        else
            snap = find_snap (self, name);

        g_autoptr(GPtrArray) keys = g_ptr_array_new_with_free_func (g_free);
        const gchar *keys_param = NULL;
        if (query != NULL)
            keys_param = g_hash_table_lookup (query, "keys");
        if (keys_param != NULL) {
            g_auto(GStrv) key_names = g_strsplit (keys_param, ",", -1);
            for (int i = 0; key_names[i] != NULL; i++)
                g_ptr_array_add (keys, g_strdup (g_strstrip (key_names[i])));
        }
        if (keys->len == 0 && snap != NULL) {
            g_autofree GStrv key_names = (GStrv) g_hash_table_get_keys_as_array (snap->configuration, NULL);
            for (int i = 0; key_names[i] != NULL; i++)
                g_ptr_array_add (keys, g_strdup (g_strstrip (key_names[i])));
        }
        g_ptr_array_sort (keys, compare_keys);

        g_autoptr(JsonBuilder) builder = json_builder_new ();
        json_builder_begin_object (builder);
        for (guint i = 0; i < keys->len; i++) {
            const gchar *key = g_ptr_array_index (keys, i);

            const gchar *value = NULL;
            if (snap != NULL)
                value = g_hash_table_lookup (snap->configuration, key);

            if (value == NULL) {
                g_autofree gchar *error_message = g_strdup_printf ("snap \"%s\" has no \"%s\" configuration option", name, key);
                send_error_bad_request (self, message, error_message, "option-not-found");
                return;
            }

            g_autoptr(JsonParser) parser = json_parser_new ();
            gboolean result = json_parser_load_from_data (parser, value, -1, NULL);
            g_assert_true (result);

            json_builder_set_member_name (builder, key);
            json_builder_add_value (builder, json_node_ref (json_parser_get_root (parser)));
        }
        json_builder_end_object (builder);

        send_sync_response (self, message, 200, json_builder_get_root (builder), NULL);
    }
    else if (strcmp (message->method, "PUT") == 0) {
        g_autoptr(JsonNode) request = get_json (message);
        if (request == NULL) {
            send_error_bad_request (self, message, "cannot decode request body into patch values", NULL);
            return;
        }

        MockSnap *snap;
        if (strcmp (name, "system") == 0)
            snap = find_snap (self, "core");
        else
            snap = find_snap (self, name);
        if (snap == NULL) {
            send_error_not_found (self, message, "snap is not installed", "snap-not-found");
            return;
        }

        JsonObject *o = json_node_get_object (request);

        JsonObjectIter iter;
        const gchar *key;
        JsonNode *value_node;
        json_object_iter_init (&iter, o);
        while (json_object_iter_next (&iter, &key, &value_node)) {
            g_autoptr(JsonGenerator) generator = json_generator_new ();
            json_generator_set_root (generator, value_node);
            g_autofree gchar *value = json_generator_to_data (generator, NULL);
            mock_snap_set_conf (snap, key, value);
        }

        MockChange *change = add_change (self);
        send_async_response (self, message, 202, change->id);
    }
    else
        send_error_method_not_allowed (self, message, "method not allowed");
}

static void
handle_apps (MockSnapd *self, SoupMessage *message, GHashTable *query)
{
    if (strcmp (message->method, "GET") != 0) {
        send_error_method_not_allowed (self, message, "method not allowed");
        return;
    }

    const gchar *select_param = NULL;
    g_auto(GStrv) selected_snaps = NULL;
    if (query != NULL) {
        const gchar *snaps_param = NULL;

        select_param = g_hash_table_lookup (query, "select");
        snaps_param = g_hash_table_lookup (query, "names");
        if (snaps_param != NULL)
            selected_snaps = g_strsplit (snaps_param, ",", -1);
    }

    g_autoptr(JsonBuilder) builder = json_builder_new ();
    json_builder_begin_array (builder);
    for (GList *link = self->snaps; link; link = link->next) {
        MockSnap *snap = link->data;
        GList *app_link;

        if (!filter_snaps (selected_snaps, snap))
            continue;

        for (app_link = snap->apps; app_link; app_link = app_link->next) {
            MockApp *app = app_link->data;

            if (g_strcmp0 (select_param, "service") == 0 && app->daemon == NULL)
                continue;

            json_builder_add_value (builder, make_app_node (app, snap->name));
        }
    }
    json_builder_end_array (builder);

    send_sync_response (self, message, 200, json_builder_get_root (builder), NULL);
}

static void
handle_icon (MockSnapd *self, SoupMessage *message, const gchar *path)
{
    if (strcmp (message->method, "GET") != 0) {
        send_error_method_not_allowed (self, message, "method not allowed");
        return;
    }

    if (!g_str_has_suffix (path, "/icon")) {
        send_error_not_found (self, message, "not found", NULL);
        return;
    }
    g_autofree gchar *name = g_strndup (path, strlen (path) - strlen ("/icon"));

    MockSnap *snap = find_snap (self, name);
    if (snap == NULL)
        send_error_not_found (self, message, "cannot find snap", NULL);
    else if (snap->icon_data == NULL)
        send_error_not_found (self, message, "not found", NULL);
    else
        send_response (message, 200, snap->icon_mime_type,
                       (const guint8 *) g_bytes_get_data (snap->icon_data, NULL),
                       g_bytes_get_size (snap->icon_data));
}

static void
handle_assertions (MockSnapd *self, SoupMessage *message, const gchar *type)
{
    if (strcmp (message->method, "GET") == 0) {
        g_autoptr(GString) response_content = g_string_new (NULL);
        g_autofree gchar *type_header = g_strdup_printf ("type: %s\n", type);
        int count = 0;
        for (GList *link = self->assertions; link; link = link->next) {
            const gchar *assertion = link->data;

            if (!g_str_has_prefix (assertion, type_header))
                continue;

            count++;
            if (count != 1)
                g_string_append (response_content, "\n\n");
            g_string_append (response_content, assertion);
        }

        if (count == 0) {
            send_error_bad_request (self, message, "invalid assert type", NULL);
            return;
        }

        // FIXME: X-Ubuntu-Assertions-Count header
        send_response (message, 200, "application/x.ubuntu.assertion; bundle=y", (guint8*) response_content->str, response_content->len);
    }
    else if (strcmp (message->method, "POST") == 0) {
        g_autofree gchar *assertion = g_strndup (message->request_body->data, message->request_body->length);
        add_assertion (self, assertion);
        send_sync_response (self, message, 200, NULL, NULL);
    }
    else {
        send_error_method_not_allowed (self, message, "method not allowed");
        return;
    }
}

static void
make_attributes (GHashTable *attributes, JsonBuilder *builder)
{
    g_autoptr(GPtrArray) keys = g_ptr_array_new_with_free_func (g_free);
    g_autofree GStrv key_names = (GStrv) g_hash_table_get_keys_as_array (attributes, NULL);
    for (int i = 0; key_names[i] != NULL; i++)
        g_ptr_array_add (keys, g_strdup (g_strstrip (key_names[i])));
    g_ptr_array_sort (keys, compare_keys);

    json_builder_begin_object (builder);
    for (guint i = 0; i < keys->len; i++) {
        const gchar *key = g_ptr_array_index (keys, i);
        const gchar *value = g_hash_table_lookup (attributes, key);

        g_autoptr(JsonParser) parser = json_parser_new ();
        gboolean result = json_parser_load_from_data (parser, value, -1, NULL);
        g_assert_true (result);

        json_builder_set_member_name (builder, key);
        json_builder_add_value (builder, json_node_ref (json_parser_get_root (parser)));
    }
    json_builder_end_object (builder);
}

static void
make_connections (MockSnapd *self, JsonBuilder *builder)
{
    json_builder_begin_object (builder);
    json_builder_set_member_name (builder, "plugs");
    json_builder_begin_array (builder);
    for (GList *link = self->snaps; link; link = link->next) {
        MockSnap *snap = link->data;

        for (GList *l = snap->plugs; l; l = l->next) {
            MockPlug *plug = l->data;

            g_autoptr(GList) slots = NULL;
            for (GList *l2 = self->established_connections; l2; l2 = l2->next) {
                MockConnection *connection = l2->data;
                if (connection->plug == plug)
                    slots = g_list_append (slots, connection->slot);
            }

            json_builder_begin_object (builder);
            json_builder_set_member_name (builder, "snap");
            json_builder_add_string_value (builder, snap->name);
            json_builder_set_member_name (builder, "plug");
            json_builder_add_string_value (builder, plug->name);
            json_builder_set_member_name (builder, "interface");
            json_builder_add_string_value (builder, plug->interface->name);
            if (g_hash_table_size (plug->attributes) > 0) {
                json_builder_set_member_name (builder, "attrs");
                make_attributes (plug->attributes, builder);
            }
            json_builder_set_member_name (builder, "label");
            json_builder_add_string_value (builder, plug->label);
            if (slots != NULL) {
                json_builder_set_member_name (builder, "connections");
                json_builder_begin_array (builder);
                for (GList *l2 = slots; l2; l2 = l2->next) {
                    MockSlot *slot = l2->data;
                    json_builder_begin_object (builder);
                    json_builder_set_member_name (builder, "snap");
                    json_builder_add_string_value (builder, slot->snap->name);
                    json_builder_set_member_name (builder, "slot");
                    json_builder_add_string_value (builder, slot->name);
                    json_builder_end_object (builder);
                }
                json_builder_end_array (builder);
            }
            json_builder_end_object (builder);
        }
    }
    json_builder_end_array (builder);
    json_builder_set_member_name (builder, "slots");
    json_builder_begin_array (builder);
    for (GList *link = self->snaps; link; link = link->next) {
        MockSnap *snap = link->data;

        for (GList *l = snap->slots_; l; l = l->next) {
            MockSlot *slot = l->data;

            g_autoptr(GList) plugs = NULL;
            for (GList *l2 = self->established_connections; l2; l2 = l2->next) {
                MockConnection *connection = l2->data;
                if (connection->slot == slot)
                    plugs = g_list_append (plugs, connection->plug);
            }

            json_builder_begin_object (builder);
            json_builder_set_member_name (builder, "snap");
            json_builder_add_string_value (builder, snap->name);
            json_builder_set_member_name (builder, "slot");
            json_builder_add_string_value (builder, slot->name);
            json_builder_set_member_name (builder, "interface");
            json_builder_add_string_value (builder, slot->interface->name);
            if (g_hash_table_size (slot->attributes) > 0) {
                json_builder_set_member_name (builder, "attrs");
                make_attributes (slot->attributes, builder);
            }
            json_builder_set_member_name (builder, "label");
            json_builder_add_string_value (builder, slot->label);
            if (plugs != NULL) {
                json_builder_set_member_name (builder, "connections");
                json_builder_begin_array (builder);
                for (GList *l2 = plugs; l2; l2 = l2->next) {
                    MockPlug *plug = l2->data;
                    json_builder_begin_object (builder);
                    json_builder_set_member_name (builder, "snap");
                    json_builder_add_string_value (builder, plug->snap->name);
                    json_builder_set_member_name (builder, "plug");
                    json_builder_add_string_value (builder, plug->name);
                    json_builder_end_object (builder);
                }
                json_builder_end_array (builder);
            }
            json_builder_end_object (builder);
        }
    }
    json_builder_end_array (builder);
    json_builder_end_object (builder);
}

static gboolean
filter_interfaces (GStrv selected_interfaces, MockInterface *interface)
{
    /* If no filter selected, then return all interfaces */
    if (selected_interfaces == NULL || selected_interfaces[0] == NULL)
        return TRUE;

    for (int i = 0; selected_interfaces[i] != NULL; i++) {
        if (strcmp (selected_interfaces[i], interface->name) == 0)
            return TRUE;
    }

    return FALSE;
}

static gboolean
interface_connected (MockSnapd *self, MockInterface *interface)
{
    for (GList *l = self->snaps; l != NULL; l = l->next) {
        MockSnap *snap = l->data;

        for (GList *l2 = snap->plugs; l2 != NULL; l2 = l2->next) {
            MockPlug *plug = l2->data;
            if (plug->interface == interface)
                return TRUE;
        }
        for (GList *l2 = snap->slots_; l2 != NULL; l2 = l2->next) {
            MockSlot *slot = l2->data;
            if (slot->interface == interface)
                return TRUE;
        }
    }

    return FALSE;
}

static void
make_interfaces (MockSnapd *self, GHashTable *query, JsonBuilder *builder)
{
    const gchar *names_param = g_hash_table_lookup (query, "names");
    g_auto(GStrv) selected_interfaces = NULL;
    if (names_param != NULL)
        selected_interfaces = g_strsplit (names_param, ",", -1);
    gboolean only_connected = g_strcmp0 (g_hash_table_lookup (query, "select"), "connected") == 0;
    gboolean include_plugs = g_strcmp0 (g_hash_table_lookup (query, "plugs"), "true") == 0;
    gboolean include_slots = g_strcmp0 (g_hash_table_lookup (query, "slots"), "true") == 0;

    json_builder_begin_array (builder);

    for (GList *l = self->interfaces; l != NULL; l = l->next) {
        MockInterface *interface = l->data;

        if (!filter_interfaces (selected_interfaces, interface))
            continue;

        if (only_connected && !interface_connected (self, interface))
            continue;

        json_builder_begin_object (builder);
        json_builder_set_member_name (builder, "name");
        json_builder_add_string_value (builder, interface->name);
        json_builder_set_member_name (builder, "summary");
        json_builder_add_string_value (builder, interface->summary);
        json_builder_set_member_name (builder, "doc-url");
        json_builder_add_string_value (builder, interface->doc_url);

        if (include_plugs) {
            json_builder_set_member_name (builder, "plugs");
            json_builder_begin_array (builder);

            for (GList *l2 = self->snaps; l2 != NULL; l2 = l2->next) {
                MockSnap *snap = l2->data;

                for (GList *l3 = snap->plugs; l3 != NULL; l3 = l3->next) {
                    MockPlug *plug = l3->data;

                    if (plug->interface != interface)
                        continue;

                    json_builder_begin_object (builder);
                    json_builder_set_member_name (builder, "snap");
                    json_builder_add_string_value (builder, plug->snap->name);
                    json_builder_set_member_name (builder, "plug");
                    json_builder_add_string_value (builder, plug->name);
                    json_builder_end_object (builder);
                }
            }

            json_builder_end_array (builder);
        }

        if (include_slots) {
            json_builder_set_member_name (builder, "slots");
            json_builder_begin_array (builder);

            for (GList *l2 = self->snaps; l2 != NULL; l2 = l2->next) {
                MockSnap *snap = l2->data;

                for (GList *l3 = snap->slots_; l3 != NULL; l3 = l3->next) {
                    MockSlot *slot = l3->data;

                    if (slot->interface != interface)
                        continue;

                    json_builder_begin_object (builder);
                    json_builder_set_member_name (builder, "snap");
                    json_builder_add_string_value (builder, slot->snap->name);
                    json_builder_set_member_name (builder, "slot");
                    json_builder_add_string_value (builder, slot->name);
                    json_builder_end_object (builder);
                }
            }

            json_builder_end_array (builder);
        }

        json_builder_end_object (builder);
    }

    json_builder_end_array (builder);
}

static void
handle_interfaces (MockSnapd *self, SoupMessage *message, GHashTable *query)
{
    if (strcmp (message->method, "GET") == 0) {
        g_autoptr(JsonBuilder) builder = json_builder_new ();

        if (query != NULL && g_hash_table_lookup (query, "select") != NULL) {
            make_interfaces (self, query, builder);
        }
        else {
            make_connections (self, builder);
        }
        send_sync_response (self, message, 200, json_builder_get_root (builder), NULL);
    }
    else if (strcmp (message->method, "POST") == 0) {
        g_autoptr(JsonNode) request = get_json (message);
        if (request == NULL) {
            send_error_bad_request (self, message, "unknown content type", NULL);
            return;
        }

        JsonObject *o = json_node_get_object (request);
        const gchar *action = json_object_get_string_member (o, "action");

        JsonArray *a = json_object_get_array_member (o, "plugs");
        g_autoptr(GList) plugs = NULL;
        g_autoptr(GList) slots = NULL;
        for (guint i = 0; i < json_array_get_length (a); i++) {
            JsonObject *po = json_array_get_object_element (a, i);

            MockSnap *snap = find_snap (self, json_object_get_string_member (po, "snap"));
            if (snap == NULL) {
                send_error_bad_request (self, message, "invalid snap", NULL);
                return;
            }
            MockPlug *plug = mock_snap_find_plug (snap, json_object_get_string_member (po, "plug"));
            if (plug == NULL) {
                send_error_bad_request (self, message, "invalid plug", NULL);
                return;
            }
            plugs = g_list_append (plugs, plug);
        }

        a = json_object_get_array_member (o, "slots");
        for (guint i = 0; i < json_array_get_length (a); i++) {
            JsonObject *so = json_array_get_object_element (a, i);

            MockSnap *snap = find_snap (self, json_object_get_string_member (so, "snap"));
            if (snap == NULL) {
                send_error_bad_request (self, message, "invalid snap", NULL);
                return;
            }
            MockSlot *slot = mock_snap_find_slot (snap, json_object_get_string_member (so, "slot"));
            if (slot == NULL) {
                send_error_bad_request (self, message, "invalid slot", NULL);
                return;
            }
            slots = g_list_append (slots, slot);
        }

        if (strcmp (action, "connect") == 0) {
            if (g_list_length (plugs) < 1 || g_list_length (slots) < 1) {
                send_error_bad_request (self, message, "at least one plug and slot is required", NULL);
                return;
            }

            for (GList *link = plugs; link; link = link->next) {
                MockPlug *plug = link->data;
                mock_snapd_connect (self, plug, slots->data, TRUE, FALSE);
            }

            MockChange *change = add_change (self);
            mock_change_set_spawn_time (change, self->spawn_time);
            mock_change_set_ready_time (change, self->ready_time);
            mock_change_add_task (change, "connect-snap");
            send_async_response (self, message, 202, change->id);
        }
        else if (strcmp (action, "disconnect") == 0) {
            if (g_list_length (plugs) < 1 || g_list_length (slots) < 1) {
                send_error_bad_request (self, message, "at least one plug and slot is required", NULL);
                return;
            }

            for (GList *link = plugs; link; link = link->next) {
                MockPlug *plug = link->data;
                mock_snapd_connect (self, plug, NULL, TRUE, FALSE);
            }

            MockChange *change = add_change (self);
            mock_change_set_spawn_time (change, self->spawn_time);
            mock_change_set_ready_time (change, self->ready_time);
            mock_change_add_task (change, "disconnect");
            send_async_response (self, message, 202, change->id);
        }
        else
            send_error_bad_request (self, message, "unsupported interface action", NULL);
    }
    else
        send_error_method_not_allowed (self, message, "method not allowed");
}

static void
add_connection (JsonBuilder *builder, MockConnection *connection)
{
    json_builder_begin_object (builder);

    MockSlot *slot = connection->slot;
    json_builder_set_member_name (builder, "slot");
    json_builder_begin_object (builder);
    json_builder_set_member_name (builder, "snap");
    json_builder_add_string_value (builder, slot->snap->name);
    json_builder_set_member_name (builder, "slot");
    json_builder_add_string_value (builder, slot->name);
    json_builder_end_object (builder);

    if (g_hash_table_size (slot->attributes) > 0) {
        json_builder_set_member_name (builder, "slot-attrs");
        make_attributes (slot->attributes, builder);
    }

    MockPlug *plug = connection->plug;
    json_builder_set_member_name (builder, "plug");
    json_builder_begin_object (builder);
    json_builder_set_member_name (builder, "snap");
    json_builder_add_string_value (builder, plug->snap->name);
    json_builder_set_member_name (builder, "plug");
    json_builder_add_string_value (builder, plug->name);
    json_builder_end_object (builder);

    if (g_hash_table_size (plug->attributes) > 0) {
        json_builder_set_member_name (builder, "plug-attrs");
        make_attributes (plug->attributes, builder);
    }

    json_builder_set_member_name (builder, "interface");
    json_builder_add_string_value (builder, plug->interface->name);

    if (connection->manual) {
        json_builder_set_member_name (builder, "manual");
        json_builder_add_boolean_value (builder, TRUE);
    }

    if (connection->gadget) {
        json_builder_set_member_name (builder, "gadget");
        json_builder_add_boolean_value (builder, TRUE);
    }

    json_builder_end_object (builder);
}

static gboolean
matches_name (MockSnap *snap, const gchar *name)
{
    return name == NULL || strcmp (snap->name, name) == 0;
}

static gboolean
matches_interface_name (MockInterface *interface, const gchar *name)
{
    return name == NULL || strcmp (interface->name, name) == 0;
}

static void
handle_connections (MockSnapd *self, SoupMessage *message, GHashTable *query)
{
    if (strcmp (message->method, "GET") != 0) {
        send_error_method_not_allowed (self, message, "method not allowed");
        return;
    }

    const gchar *snap_name = query != NULL ? g_hash_table_lookup (query, "snap") : NULL;
    const gchar *interface_name = query != NULL ? g_hash_table_lookup (query, "interface") : NULL;
    const gchar *select = query != NULL ? g_hash_table_lookup (query, "select") : NULL;

    if (select != NULL && strcmp (select, "all") != 0) {
        send_error_bad_request (self, message, "unsupported select qualifier", NULL);
        return;
    }
    gboolean only_connected = TRUE;
    if (g_strcmp0 (select, "all") == 0)
        only_connected = FALSE;

    g_autoptr(JsonBuilder) builder = json_builder_new ();
    json_builder_begin_object (builder);

    json_builder_set_member_name (builder, "established");
    json_builder_begin_array (builder);
    for (GList *link = self->established_connections; link; link = link->next) {
        MockConnection *connection = link->data;

        if (matches_name (connection->plug->snap, snap_name) || matches_name (connection->slot->snap, snap_name))
            add_connection (builder, connection);
    }
    json_builder_end_array (builder);

    g_autoptr(GList) undesired_connections = NULL;
    for (GList *link = self->undesired_connections; link; link = link->next) {
         MockConnection *connection = link->data;

         if (only_connected && snap_name == NULL)
             continue;
         if (!matches_name (connection->plug->snap, snap_name) && !matches_name (connection->slot->snap, snap_name))
             continue;

         undesired_connections = g_list_append (undesired_connections, connection);
    }
    if (undesired_connections != NULL) {
        json_builder_set_member_name (builder, "undesired");
        json_builder_begin_array (builder);
        for (GList *link = undesired_connections; link; link = link->next) {
            MockConnection *connection = link->data;
            add_connection (builder, connection);
        }
        json_builder_end_array (builder);
    }

    json_builder_set_member_name (builder, "plugs");
    json_builder_begin_array (builder);
    for (GList *link = self->snaps; link; link = link->next) {
        MockSnap *snap = link->data;

        for (GList *l = snap->plugs; l; l = l->next) {
            MockPlug *plug = l->data;

            if (!matches_interface_name (plug->interface, interface_name))
                 continue;

            if (!matches_name (snap, snap_name) && !matches_name (plug->snap, snap_name))
                continue;

            g_autoptr(GList) slots = NULL;
            for (GList *l2 = self->established_connections; l2; l2 = l2->next) {
                MockConnection *connection = l2->data;

                if (!matches_name (snap, snap_name) && !matches_name (connection->slot->snap, snap_name))
                    continue;

                if (connection->plug == plug)
                    slots = g_list_append (slots, connection->slot);
            }

            if (!matches_name (snap, snap_name) && slots == NULL)
                 continue;

            if (only_connected && slots == NULL)
                 continue;

            json_builder_begin_object (builder);
            json_builder_set_member_name (builder, "snap");
            json_builder_add_string_value (builder, snap->name);
            json_builder_set_member_name (builder, "plug");
            json_builder_add_string_value (builder, plug->name);
            json_builder_set_member_name (builder, "interface");
            json_builder_add_string_value (builder, plug->interface->name);
            if (g_hash_table_size (plug->attributes) > 0) {
                json_builder_set_member_name (builder, "attrs");
                make_attributes (plug->attributes, builder);
            }
            json_builder_set_member_name (builder, "label");
            json_builder_add_string_value (builder, plug->label);
            if (slots != NULL) {
                json_builder_set_member_name (builder, "connections");
                json_builder_begin_array (builder);
                for (GList *l2 = slots; l2; l2 = l2->next) {
                    MockSlot *slot = l2->data;
                    json_builder_begin_object (builder);
                    json_builder_set_member_name (builder, "snap");
                    json_builder_add_string_value (builder, slot->snap->name);
                    json_builder_set_member_name (builder, "slot");
                    json_builder_add_string_value (builder, slot->name);
                    json_builder_end_object (builder);
                }
                json_builder_end_array (builder);
            }
            json_builder_end_object (builder);
        }
    }
    json_builder_end_array (builder);
    json_builder_set_member_name (builder, "slots");
    json_builder_begin_array (builder);
    for (GList *link = self->snaps; link; link = link->next) {
        MockSnap *snap = link->data;

        for (GList *l = snap->slots_; l; l = l->next) {
            MockSlot *slot = l->data;

            if (!matches_interface_name (slot->interface, interface_name))
                 continue;

            g_autoptr(GList) plugs = NULL;
            for (GList *l2 = self->established_connections; l2; l2 = l2->next) {
                MockConnection *connection = l2->data;

                if (!matches_name (snap, snap_name) && !matches_name (connection->plug->snap, snap_name))
                    continue;

                if (connection->slot == slot)
                    plugs = g_list_append (plugs, connection->plug);
            }

            if (!matches_name (snap, snap_name) && plugs == NULL)
                 continue;

            if (only_connected && plugs == NULL)
                 continue;

            json_builder_begin_object (builder);
            json_builder_set_member_name (builder, "snap");
            json_builder_add_string_value (builder, snap->name);
            json_builder_set_member_name (builder, "slot");
            json_builder_add_string_value (builder, slot->name);
            json_builder_set_member_name (builder, "interface");
            json_builder_add_string_value (builder, slot->interface->name);
            if (g_hash_table_size (slot->attributes) > 0) {
                json_builder_set_member_name (builder, "attrs");
                make_attributes (slot->attributes, builder);
            }
            json_builder_set_member_name (builder, "label");
            json_builder_add_string_value (builder, slot->label);
            if (plugs != NULL) {
                json_builder_set_member_name (builder, "connections");
                json_builder_begin_array (builder);
                for (GList *l2 = plugs; l2; l2 = l2->next) {
                    MockPlug *plug = l2->data;
                    json_builder_begin_object (builder);
                    json_builder_set_member_name (builder, "snap");
                    json_builder_add_string_value (builder, plug->snap->name);
                    json_builder_set_member_name (builder, "plug");
                    json_builder_add_string_value (builder, plug->name);
                    json_builder_end_object (builder);
                }
                json_builder_end_array (builder);
            }
            json_builder_end_object (builder);
        }
    }
    json_builder_end_array (builder);
    json_builder_end_object (builder);

    send_sync_response (self, message, 200, json_builder_get_root (builder), NULL);
}

static MockTask *
get_current_task (MockChange *change)
{
    for (GList *link = change->tasks; link; link = link->next) {
        MockTask *task = link->data;
        if (strcmp (task->status, "Done") != 0)
            return task;
    }

    return NULL;
}

static gboolean
change_get_ready (MockChange *change)
{
    MockTask *task = get_current_task (change);
    return task == NULL || strcmp (task->status, "Error") == 0;
}

static JsonNode *
make_change_node (MockChange *change)
{
    int progress_total = 0, progress_done = 0;
    for (GList *link = change->tasks; link; link = link->next) {
        MockTask *task = link->data;
        progress_done += task->progress_done;
        progress_total += task->progress_total;
    }

    MockTask *task = get_current_task (change);
    const gchar *status = task != NULL ? task->status : "Done";
    const gchar *error = task != NULL && strcmp (task->status, "Error") == 0 ? task->error : NULL;

    g_autoptr(JsonBuilder) builder = json_builder_new ();
    json_builder_begin_object (builder);
    json_builder_set_member_name (builder, "id");
    json_builder_add_string_value (builder, change->id);
    json_builder_set_member_name (builder, "kind");
    json_builder_add_string_value (builder, change->kind);
    json_builder_set_member_name (builder, "summary");
    json_builder_add_string_value (builder, change->summary);
    json_builder_set_member_name (builder, "status");
    json_builder_add_string_value (builder, status);
    json_builder_set_member_name (builder, "tasks");
    json_builder_begin_array (builder);
    for (GList *link = change->tasks; link; link = link->next) {
        MockTask *task = link->data;

        json_builder_begin_object (builder);
        json_builder_set_member_name (builder, "id");
        json_builder_add_string_value (builder, task->id);
        json_builder_set_member_name (builder, "kind");
        json_builder_add_string_value (builder, task->kind);
        json_builder_set_member_name (builder, "summary");
        json_builder_add_string_value (builder, task->summary);
        json_builder_set_member_name (builder, "status");
        json_builder_add_string_value (builder, task->status);
        json_builder_set_member_name (builder, "progress");
        json_builder_begin_object (builder);
        json_builder_set_member_name (builder, "label");
        json_builder_add_string_value (builder, task->progress_label);
        json_builder_set_member_name (builder, "done");
        json_builder_add_int_value (builder, task->progress_done);
        json_builder_set_member_name (builder, "total");
        json_builder_add_int_value (builder, task->progress_total);
        json_builder_end_object (builder);
        if (task->spawn_time != NULL) {
            json_builder_set_member_name (builder, "spawn-time");
            json_builder_add_string_value (builder, task->spawn_time);
        }
        if (task->progress_done >= task->progress_total && task->ready_time != NULL) {
            json_builder_set_member_name (builder, "ready-time");
            json_builder_add_string_value (builder, task->ready_time);
        }
        json_builder_end_object (builder);
    }
    json_builder_end_array (builder);
    json_builder_set_member_name (builder, "ready");
    json_builder_add_boolean_value (builder, change_get_ready (change));
    json_builder_set_member_name (builder, "spawn-time");
    json_builder_add_string_value (builder, change->spawn_time);
    if (change_get_ready (change) && change->ready_time != NULL) {
        json_builder_set_member_name (builder, "ready-time");
        json_builder_add_string_value (builder, change->ready_time);
    }
    if (change_get_ready (change) && change->data != NULL) {
        json_builder_set_member_name (builder, "data");
        json_builder_add_value (builder, json_node_ref (change->data));
    }
    if (error != NULL) {
        json_builder_set_member_name (builder, "err");
        json_builder_add_string_value (builder, error);
    }
    json_builder_end_object (builder);

    return json_builder_get_root (builder);
}

static gboolean
change_relates_to_snap (MockChange *change, const gchar *snap_name)
{
    for (GList *link = change->tasks; link; link = link->next) {
        MockTask *task = link->data;

        if (g_strcmp0 (snap_name, task->snap_name) == 0)
            return TRUE;
        if (task->snap != NULL && g_strcmp0 (snap_name, task->snap->name) == 0)
            return TRUE;
    }

    return FALSE;
}

static void
handle_changes (MockSnapd *self, SoupMessage *message, GHashTable *query)
{
    if (strcmp (message->method, "GET") != 0) {
        send_error_method_not_allowed (self, message, "method not allowed");
        return;
    }

    const gchar *select_param = g_hash_table_lookup (query, "select");
    if (select_param == NULL)
        select_param = "in-progress";
    const gchar *for_param = g_hash_table_lookup (query, "for");

    g_autoptr(JsonBuilder) builder = json_builder_new ();
    json_builder_begin_array (builder);
    for (GList *link = self->changes; link; link = link->next) {
        MockChange *change = link->data;

        if (g_strcmp0 (select_param, "in-progress") == 0 && change_get_ready (change))
            continue;
        if (g_strcmp0 (select_param, "ready") == 0 && !change_get_ready (change))
            continue;
        if (for_param != NULL && !change_relates_to_snap (change, for_param))
            continue;

        json_builder_add_value (builder, make_change_node (change));
    }
    json_builder_end_array (builder);

    send_sync_response (self, message, 200, json_builder_get_root (builder), NULL);
}

static void
mock_task_complete (MockSnapd *self, MockTask *task)
{
    if (strcmp (task->kind, "install") == 0 || strcmp (task->kind, "try") == 0)
        self->snaps = g_list_append (self->snaps, g_steal_pointer (&task->snap));
    else if (strcmp (task->kind, "remove") == 0) {
        MockSnap *snap = find_snap (self, task->snap_name);
        self->snaps = g_list_remove (self->snaps, snap);
        mock_snap_free (snap);

        /* Add a snapshot */
        if (!task->purge && find_snapshot (self, task->snap_name) == NULL)
            self->snapshots = g_list_append (self->snapshots, mock_snapshot_new (task->snap_name));
    }
    mock_task_set_status (task, "Done");
}

static void
mock_change_progress (MockSnapd *self, MockChange *change)
{
    for (GList *link = change->tasks; link; link = link->next) {
        MockTask *task = link->data;

        if (task->error != NULL) {
            mock_task_set_status (task, "Error");
            return;
        }

        if (task->progress_done < task->progress_total) {
            mock_task_set_status (task, "Doing");
            task->progress_done++;
            if (task->progress_done == task->progress_total)
                mock_task_complete (self, task);
            return;
        }
    }
}

static void
handle_change (MockSnapd *self, SoupMessage *message, const gchar *change_id)
{
    if (strcmp (message->method, "GET") == 0) {
        MockChange *change = get_change (self, change_id);
        if (change == NULL) {
            send_error_not_found (self, message, "cannot find change", NULL);
            return;
        }
        mock_change_progress (self, change);

        send_sync_response (self, message, 200, make_change_node (change), NULL);
    }
    else if (strcmp (message->method, "POST") == 0) {
        MockChange *change = get_change (self, change_id);
        if (change == NULL) {
            send_error_not_found (self, message, "cannot find change", NULL);
            return;
        }

        g_autoptr(JsonNode) request = get_json (message);
        if (request == NULL) {
            send_error_bad_request (self, message, "unknown content type", NULL);
            return;
        }

        JsonObject *o = json_node_get_object (request);
        const gchar *action = json_object_get_string_member (o, "action");
        if (strcmp (action, "abort") == 0) {
            MockTask *task = get_current_task (change);
            if (task == NULL) {
                send_error_bad_request (self, message, "task not in progress", NULL);
                return;
            }
            mock_task_set_status (task, "Error");
            task->error = g_strdup ("cancelled");
            send_sync_response (self, message, 200, make_change_node (change), NULL);
        }
        else {
            send_error_bad_request (self, message, "change action is unsupported", NULL);
            return;
        }
    }
    else {
        send_error_method_not_allowed (self, message, "method not allowed");
        return;
    }
}

static gboolean
matches_query (MockSnap *snap, const gchar *query)
{
    return query == NULL || strstr (snap->name, query) != NULL;
}

static gboolean
matches_scope (MockSnap *snap, const gchar *scope)
{
    if (g_strcmp0 (scope, "wide") == 0)
        return TRUE;

    return !snap->scope_is_wide;
}

static gboolean
has_common_id (MockSnap *snap, const gchar *common_id)
{
    if (common_id == NULL)
        return TRUE;

    for (GList *link = snap->apps; link; link = link->next) {
        MockApp *app = link->data;
        if (g_strcmp0 (app->common_id, common_id) == 0)
            return TRUE;
    }

    return FALSE;
}

static gboolean
in_section (MockSnap *snap, const gchar *section)
{
    if (section == NULL)
        return TRUE;

    for (GList *link = snap->store_sections; link; link = link->next)
        if (strcmp (link->data, section) == 0)
            return TRUE;

    return FALSE;
}

static void
handle_find (MockSnapd *self, SoupMessage *message, GHashTable *query)
{
    if (strcmp (message->method, "GET") != 0) {
        send_error_method_not_allowed (self, message, "method not allowed");
        return;
    }

    const gchar *common_id_param = g_hash_table_lookup (query, "common-id");
    const gchar *query_param = g_hash_table_lookup (query, "q");
    const gchar *name_param = g_hash_table_lookup (query, "name");
    const gchar *select_param = g_hash_table_lookup (query, "select");
    const gchar *section_param = g_hash_table_lookup (query, "section");
    const gchar *scope_param = g_hash_table_lookup (query, "scope");

    if (common_id_param && strcmp (common_id_param, "") == 0)
        common_id_param = NULL;
    if (query_param && strcmp (query_param, "") == 0)
        query_param = NULL;
    if (name_param && strcmp (name_param, "") == 0)
        name_param = NULL;
    if (select_param && strcmp (select_param, "") == 0)
        select_param = NULL;
    if (section_param && strcmp (section_param, "") == 0)
        section_param = NULL;
    if (scope_param && strcmp (scope_param, "") == 0)
        scope_param = NULL;

    GList *snaps;
    if (g_strcmp0 (select_param, "refresh") == 0) {
        g_autoptr(GList) refreshable_snaps = NULL;

        if (query_param != NULL) {
            send_error_bad_request (self, message, "cannot use 'q' with 'select=refresh'", NULL);
            return;
        }
        if (name_param != NULL) {
            send_error_bad_request (self, message, "cannot use 'name' with 'select=refresh'", NULL);
            return;
        }

        g_autoptr(JsonBuilder) builder = json_builder_new ();
        json_builder_begin_array (builder);
        refreshable_snaps = get_refreshable_snaps (self);
        for (GList *link = refreshable_snaps; link; link = link->next)
            json_builder_add_value (builder, make_snap_node (link->data));
        json_builder_end_array (builder);

        send_sync_response (self, message, 200, json_builder_get_root (builder), self->suggested_currency);
        return;
    }
    else if (g_strcmp0 (select_param, "private") == 0) {
        MockAccount *account = get_account (self, message);
        if (account == NULL) {
            send_error_bad_request (self, message, "you need to log in first", "login-required");
            return;
        }
        snaps = account->private_snaps;
    }
    else
        snaps = self->store_snaps;

    /* Make a special query that never responds */
    if (g_strcmp0 (query_param, "do-not-respond") == 0)
        return;

    /* Make a special query that simulates a network timeout */
    if (g_strcmp0 (query_param, "network-timeout") == 0) {
        send_error_bad_request (self, message, "unable to contact snap store", "network-timeout");
        return;
    }

    /* Make a special query that simulates a DNS Failure */
    if (g_strcmp0 (query_param, "dns-failure") == 0) {
        send_error_bad_request (self, message, "failed to resolve address", "dns-failure");
        return;
    }

    /* Certain characters not allowed in queries */
    if (query_param != NULL) {
        for (int i = 0; query_param[i] != '\0'; i++) {
            const gchar *invalid_chars = "+=&|><!(){}[]^\"~*?:\\/";
            if (strchr (invalid_chars, query_param[i]) != NULL) {
                send_error_bad_request (self, message, "bad query", "bad-query");
                return;
            }
        }
    }

    g_autoptr(JsonBuilder) builder = json_builder_new ();
    json_builder_begin_array (builder);
    for (GList *link = snaps; link; link = link->next) {
        MockSnap *snap = link->data;

        if (!has_common_id (snap, common_id_param))
            continue;

        if (!in_section (snap, section_param))
            continue;

        if (!matches_query (snap, query_param))
            continue;

        if (!matches_name (snap, name_param))
            continue;

        if (!matches_scope (snap, scope_param))
            continue;

        json_builder_add_value (builder, make_snap_node (snap));
    }
    json_builder_end_array (builder);

    send_sync_response (self, message, 200, json_builder_get_root (builder), self->suggested_currency);
}

static void
handle_buy_ready (MockSnapd *self, SoupMessage *message)
{
    if (strcmp (message->method, "GET") != 0) {
        send_error_method_not_allowed (self, message, "method not allowed");
        return;
    }

    MockAccount *account = get_account (self, message);
    if (account == NULL) {
        send_error_bad_request (self, message, "you need to log in first", "login-required");
        return;
    }

    if (!account->terms_accepted) {
        send_error_bad_request (self, message, "terms of service not accepted", "terms-not-accepted");
        return;
    }

    if (!account->has_payment_methods) {
        send_error_bad_request (self, message, "no payment methods", "no-payment-methods");
        return;
    }

    JsonNode *result = json_node_new (JSON_NODE_VALUE);
    json_node_set_boolean (result, TRUE);

    send_sync_response (self, message, 200, result, NULL);
}

static void
handle_buy (MockSnapd *self, SoupMessage *message)
{
    if (strcmp (message->method, "POST") != 0) {
        send_error_method_not_allowed (self, message, "method not allowed");
        return;
    }

    MockAccount *account = get_account (self, message);
    if (account == NULL) {
        send_error_bad_request (self, message, "you need to log in first", "login-required");
        return;
    }

    if (!account->terms_accepted) {
        send_error_bad_request (self, message, "terms of service not accepted", "terms-not-accepted");
        return;
    }

    if (!account->has_payment_methods) {
        send_error_bad_request (self, message, "no payment methods", "no-payment-methods");
        return;
    }

    g_autoptr(JsonNode) request = get_json (message);
    if (request == NULL) {
        send_error_bad_request (self, message, "unknown content type", NULL);
        return;
    }

    JsonObject *o = json_node_get_object (request);
    const gchar *snap_id = json_object_get_string_member (o, "snap-id");
    gdouble price = json_object_get_double_member (o, "price");
    const gchar *currency = json_object_get_string_member (o, "currency");

    MockSnap *snap = find_store_snap_by_id (self, snap_id);
    if (snap == NULL) {
        send_error_not_found (self, message, "not found", NULL); // FIXME: Check is error snapd returns
        return;
    }

    gdouble amount = mock_snap_find_price (snap, currency);
    if (amount == 0) {
        send_error_bad_request (self, message, "no price found", "payment-declined"); // FIXME: Check is error snapd returns
        return;
    }
    if (amount != price) {
        send_error_bad_request (self, message, "invalid price", "payment-declined"); // FIXME: Check is error snapd returns
        return;
    }

    send_sync_response (self, message, 200, NULL, NULL);
}

static void
handle_sections (MockSnapd *self, SoupMessage *message)
{
    if (strcmp (message->method, "GET") != 0) {
        send_error_method_not_allowed (self, message, "method not allowed");
        return;
    }

    g_autoptr(JsonBuilder) builder = json_builder_new ();
    json_builder_begin_array (builder);
    for (GList *link = self->store_sections; link; link = link->next)
        json_builder_add_string_value (builder, link->data);
    json_builder_end_array (builder);

    send_sync_response (self, message, 200, json_builder_get_root (builder), NULL);
}

static MockSnap *
find_snap_by_alias (MockSnapd *self, const gchar *name)
{
    GList *link;

    for (link = self->snaps; link; link = link->next) {
        MockSnap *snap = link->data;
        GList *app_link;

        for (app_link = snap->apps; app_link; app_link = app_link->next) {
            MockApp *app = app_link->data;
            MockAlias *alias;

            alias = mock_app_find_alias (app, name);
            if (alias != NULL)
                return snap;
        }
    }

    return NULL;
}

static void
unalias (MockSnapd *self, const gchar *name)
{
    for (GList *link = self->snaps; link; link = link->next) {
        MockSnap *snap = link->data;

        for (GList *app_link = snap->apps; app_link; app_link = app_link->next) {
            MockApp *app = app_link->data;

            MockAlias *alias = mock_app_find_alias (app, name);
            if (alias != NULL) {
                if (alias->automatic)
                    alias->enabled = FALSE;
                else {
                    app->aliases = g_list_remove (app->aliases, alias);
                    mock_alias_free (alias);
                }
                return;
            }
        }
    }
}

static void
handle_aliases (MockSnapd *self, SoupMessage *message)
{
    if (strcmp (message->method, "GET") == 0) {
        g_autoptr(JsonBuilder) builder = json_builder_new ();
        json_builder_begin_object (builder);
        for (GList *link = self->snaps; link; link = link->next) {
            MockSnap *snap = link->data;

            int alias_count = 0;
            for (GList *link2 = snap->apps; link2; link2 = link2->next) {
                MockApp *app = link2->data;

                for (GList *link3 = app->aliases; link3; link3 = link3->next) {
                    MockAlias *alias = link3->data;

                    if (alias_count == 0) {
                        json_builder_set_member_name (builder, snap->name);
                        json_builder_begin_object (builder);
                    }
                    alias_count++;

                    json_builder_set_member_name (builder, alias->name);
                    json_builder_begin_object (builder);
                    g_autofree gchar *command = g_strdup_printf ("%s.%s", snap->name, app->name);
                    json_builder_set_member_name (builder, "command");
                    json_builder_add_string_value (builder, command);
                    if (alias->automatic) {
                        json_builder_set_member_name (builder, "status");
                        if (alias->enabled)
                            json_builder_add_string_value (builder, "auto");
                        else
                            json_builder_add_string_value (builder, "disabled");
                        json_builder_set_member_name (builder, "auto");
                        json_builder_add_string_value (builder, app->name);
                    }
                    else {
                        json_builder_set_member_name (builder, "status");
                        json_builder_add_string_value (builder, "manual");
                        json_builder_set_member_name (builder, "manual");
                        json_builder_add_string_value (builder, app->name);
                    }
                    json_builder_end_object (builder);
                }
            }

            if (alias_count > 0)
                json_builder_end_object (builder);
        }
        json_builder_end_object (builder);
        send_sync_response (self, message, 200, json_builder_get_root (builder), NULL);
    }
    else if (strcmp (message->method, "POST") == 0) {
        g_autoptr(JsonNode) request = get_json (message);
        if (request == NULL) {
            send_error_bad_request (self, message, "unknown content type", NULL);
            return;
        }

        JsonObject *o = json_node_get_object (request);
        const gchar *alias = NULL;
        if (json_object_has_member (o, "alias"))
            alias = json_object_get_string_member (o, "alias");
        MockSnap *snap;
        if (json_object_has_member (o, "snap")) {
            snap = find_snap (self, json_object_get_string_member (o, "snap"));
            if (snap == NULL) {
                send_error_not_found (self, message, "cannot find snap", NULL);
                return;
            }
        }
        else if (alias != NULL) {
            snap = find_snap_by_alias (self, alias);
            if (snap == NULL) {
                send_error_not_found (self, message, "cannot find snap", NULL);
                return;
            }
        }
        else {
            send_error_not_found (self, message, "cannot find snap", NULL);
            return;
        }

        const gchar *action = json_object_get_string_member (o, "action");
        if (g_strcmp0 (action, "alias") == 0) {
            const gchar *app_name;
            MockApp *app;

            app_name = json_object_get_string_member (o, "app");
            if (app_name == NULL) {
                send_error_not_found (self, message, "No app specified", NULL);
                return;
            }
            app = mock_snap_find_app (snap, app_name);
            if (app == NULL) {
                send_error_not_found (self, message, "App not found", NULL);
                return;
            }
            mock_app_add_manual_alias (app, alias, TRUE);
        }
        else if (g_strcmp0 (action, "unalias") == 0) {
            unalias (self, alias);
        }
        else if (g_strcmp0 (action, "prefer") == 0) {
            snap->preferred = TRUE;
        }
        else {
            send_error_bad_request (self, message, "unsupported alias action", NULL);
            return;
        }

        MockChange *change = add_change (self);
        mock_change_set_spawn_time (change, self->spawn_time);
        mock_change_set_ready_time (change, self->ready_time);
        mock_change_add_task (change, action);
        send_async_response (self, message, 202, change->id);
    }
    else {
        send_error_method_not_allowed (self, message, "method not allowed");
        return;
    }
}

static void
handle_snapctl (MockSnapd *self, SoupMessage *message)
{
    if (strcmp (message->method, "POST") != 0) {
        send_error_method_not_allowed (self, message, "method not allowed");
        return;
    }

    g_autoptr(JsonNode) request = get_json (message);
    if (request == NULL) {
        send_error_bad_request (self, message, "unknown content type", NULL);
        return;
    }

    JsonObject *o = json_node_get_object (request);
    const gchar *context_id = json_object_get_string_member (o, "context-id");
    JsonArray *args = json_object_get_array_member (o, "args");

    g_autoptr(GString) stdout_text = g_string_new ("STDOUT");
    g_string_append_printf (stdout_text, ":%s", context_id);
    for (int i = 0; i < json_array_get_length (args); i++) {
        const gchar *arg = json_array_get_string_element (args, i);
        g_string_append_printf (stdout_text, ":%s", arg);
    }

    if (!g_strcmp0 (context_id, "return-error")) {
        g_autoptr(JsonBuilder) builder = json_builder_new ();
        json_builder_begin_object (builder);
        json_builder_set_member_name (builder, "stdout");
        json_builder_add_string_value (builder, stdout_text->str);
        json_builder_set_member_name (builder, "stderr");
        json_builder_add_string_value (builder, "STDERR");
        json_builder_set_member_name (builder, "exit-code");
        json_builder_add_int_value (builder, 1);
        json_builder_end_object (builder);

        send_error_response (self, message, 200, "unsuccessful with exit code: 1", "unsuccessful", json_builder_get_root (builder));
        return;
    }

    g_autoptr(JsonBuilder) builder = json_builder_new ();
    json_builder_begin_object (builder);
    json_builder_set_member_name (builder, "stdout");
    json_builder_add_string_value (builder, stdout_text->str);
    json_builder_set_member_name (builder, "stderr");
    json_builder_add_string_value (builder, "STDERR");
    json_builder_end_object (builder);

    send_sync_response (self, message, 200, json_builder_get_root (builder), NULL);
}

static void
handle_create_user (MockSnapd *self, SoupMessage *message)
{
    if (strcmp (message->method, "POST") != 0) {
        send_error_method_not_allowed (self, message, "method not allowed");
        return;
    }

    g_autoptr(JsonNode) request = get_json (message);
    if (request == NULL) {
        send_error_bad_request (self, message, "unknown content type", NULL);
        return;
    }

    JsonObject *o = json_node_get_object (request);
    if (json_object_has_member (o, "email")) {
        g_autoptr(JsonBuilder) builder = json_builder_new ();

        const gchar *email = json_object_get_string_member (o, "email");
        gboolean sudoer = FALSE;
        if (json_object_has_member (o, "sudoer"))
            sudoer = json_object_get_boolean_member (o, "sudoer");
        gboolean known = FALSE;
        if (json_object_has_member (o, "known"))
            known = json_object_get_boolean_member (o, "known");
        g_auto(GStrv) email_keys = g_strsplit (email, "@", 2);
        const gchar *username = email_keys[0];
        g_auto(GStrv) ssh_keys = g_strsplit ("KEY1;KEY2", ";", -1);
        MockAccount *a = add_account (self, email, username, "password");
        a->sudoer = sudoer;
        a->known = known;
        mock_account_set_ssh_keys (a, ssh_keys);

        add_user_response (builder, a, FALSE, TRUE, FALSE);
        send_sync_response (self, message, 200, json_builder_get_root (builder), NULL);
        return;
    }
    else {
        if (json_object_has_member (o, "known") && json_object_get_boolean_member (o, "known")) {
            g_autoptr(JsonBuilder) builder = json_builder_new ();
            json_builder_begin_array (builder);

            g_auto(GStrv) ssh_keys = g_strsplit ("KEY1;KEY2", ";", -1);
            MockAccount *a = add_account (self, "admin@example.com", "admin", "password");
            a->sudoer = TRUE;
            a->known = TRUE;
            mock_account_set_ssh_keys (a, ssh_keys);
            add_user_response (builder, a, FALSE, TRUE, FALSE);
            a = add_account (self, "alice@example.com", "alice", "password");
            a->known = TRUE;
            mock_account_set_ssh_keys (a, ssh_keys);
            add_user_response (builder, a, FALSE, TRUE, FALSE);
            a = add_account (self, "bob@example.com", "bob", "password");
            a->known = TRUE;
            mock_account_set_ssh_keys (a, ssh_keys);
            add_user_response (builder, a, FALSE, TRUE, FALSE);

            json_builder_end_array (builder);

            send_sync_response (self, message, 200, json_builder_get_root (builder), NULL);
            return;
        }
        else {
            send_error_bad_request (self, message, "cannot create user", NULL);
            return;
        }
    }
}

static void
handle_users (MockSnapd *self, SoupMessage *message)
{
    if (strcmp (message->method, "GET") != 0) {
        send_error_method_not_allowed (self, message, "method not allowed");
        return;
    }

    g_autoptr(JsonBuilder) builder = json_builder_new ();
    json_builder_begin_array (builder);
    for (GList *link = self->accounts; link; link = link->next) {
        MockAccount *a = link->data;
        add_user_response (builder, a, FALSE, FALSE, TRUE);
    }
    json_builder_end_array (builder);
    send_sync_response (self, message, 200, json_builder_get_root (builder), NULL);
}

static void
handle_download (MockSnapd *self, SoupMessage *message)
{
    if (strcmp (message->method, "POST") != 0) {
        send_error_method_not_allowed (self, message, "method not allowed");
        return;
    }

    g_autoptr(JsonNode) request = get_json (message);
    if (request == NULL) {
        send_error_bad_request (self, message, "unknown content type", NULL);
        return;
    }

    JsonObject *o = json_node_get_object (request);
    const gchar *snap_name = json_object_get_string_member (o, "snap-name");
    const gchar *channel = NULL;
    if (json_object_has_member (o, "channel"))
        channel = json_object_get_string_member (o, "channel");
    const gchar *revision = NULL;
    if (json_object_has_member (o, "revision"))
        revision = json_object_get_string_member (o, "revision");

    g_autoptr(GString) contents = g_string_new ("SNAP");
    g_string_append_printf (contents, ":name=%s", snap_name);
    if (channel != NULL)
        g_string_append_printf (contents, ":channel=%s", channel);
    if (revision != NULL)
        g_string_append_printf (contents, ":revision=%s", revision);

    send_response (message, 200, "application/octet-stream", (const guint8 *) contents->str, contents->len);
}

static int
count_available_themes (JsonObject *o, const char *theme_type, GHashTable *theme_status) {
    guint i, length;
    int available = 0;
    JsonArray *themes = json_object_get_array_member (o, theme_type);

    if (themes == NULL)
        return 0;

    length = json_array_get_length (themes);
    for (i = 0; i < length; i++) {
        const char *theme_name = json_array_get_string_element (themes, i);
        if (theme_name == NULL)
            continue;
        if (g_strcmp0 (g_hash_table_lookup (theme_status, theme_name), "available") == 0)
            available++;
    }
    return available;
}

static void
handle_themes (MockSnapd *self, SoupMessage *message)
{
    if (strcmp (message->method, "GET") == 0) {
        SoupURI *uri = soup_message_get_uri (message);
        g_autoptr(JsonBuilder) gtk_themes = json_builder_new ();
        g_autoptr(JsonBuilder) icon_themes = json_builder_new ();
        g_autoptr(JsonBuilder) sound_themes = json_builder_new ();

        json_builder_begin_object (gtk_themes);
        json_builder_begin_object (icon_themes);
        json_builder_begin_object (sound_themes);
        /* We are parsing the query parameters manually because the
         * GHashTable API loses duplicate values */
        g_auto(GStrv) pairs = g_strsplit (soup_uri_get_query (uri), "&", -1);
        char *const *pair;
        for (pair = pairs; *pair != NULL; pair++) {
            char *eq = strchr (*pair, '=');
            if (!eq)
                continue;
            *eq = '\0';
            g_autofree char *attr = soup_uri_decode (*pair);
            g_autofree char *value = soup_uri_decode (eq + 1);
            if (strcmp (attr, "gtk-theme") == 0) {
                const char *status = g_hash_table_lookup (self->gtk_theme_status, value);
                json_builder_set_member_name (gtk_themes, value);
                json_builder_add_string_value (gtk_themes, status ? status : "unavailable");
            } else if (strcmp (attr, "icon-theme") == 0) {
                const char *status = g_hash_table_lookup (self->icon_theme_status, value);
                json_builder_set_member_name (icon_themes, value);
                json_builder_add_string_value (icon_themes, status ? status : "unavailable");
            } else if (strcmp (attr, "sound-theme") == 0) {
                const char *status = g_hash_table_lookup (self->sound_theme_status, value);
                json_builder_set_member_name (sound_themes, value);
                json_builder_add_string_value (sound_themes, status ? status : "unavailable");
            }
        }
        json_builder_end_object (gtk_themes);
        json_builder_end_object (icon_themes);
        json_builder_end_object (sound_themes);

        g_autoptr(JsonBuilder) builder = json_builder_new ();
        json_builder_begin_object (builder);
        json_builder_set_member_name (builder, "gtk-themes");
        json_builder_add_value (builder, json_builder_get_root (gtk_themes));
        json_builder_set_member_name (builder, "icon-themes");
        json_builder_add_value (builder, json_builder_get_root (icon_themes));
        json_builder_set_member_name (builder, "sound-themes");
        json_builder_add_value (builder, json_builder_get_root (sound_themes));
        json_builder_end_object (builder);

        send_sync_response (self, message, 200, json_builder_get_root (builder), NULL);
    } else if (strcmp (message->method, "POST") == 0) {
        g_autoptr(JsonNode) request = get_json (message);
        if (request == NULL) {
            send_error_bad_request (self, message, "unknown content type", NULL);
            return;
        }
        JsonObject *o = json_node_get_object (request);
        int available = 0;

        available += count_available_themes (o, "gtk-themes", self->gtk_theme_status);
        available += count_available_themes (o, "icon-themes", self->icon_theme_status);
        available += count_available_themes (o, "sound-themes", self->sound_theme_status);
        if (available == 0) {
            send_error_bad_request (self, message, "no snaps to install", NULL);
            return;
        }

        MockChange *change = add_change (self);
        mock_change_set_spawn_time (change, self->spawn_time);
        mock_change_set_ready_time (change, self->ready_time);
        MockTask *task = mock_change_add_task (change, "install");
        task->snap = mock_snap_new ("theme-snap");
        mock_snap_set_channel (task->snap, "stable");
        mock_snap_set_revision (task->snap, "1");
        send_async_response (self, message, 202, change->id);
    } else {
        send_error_method_not_allowed (self, message, "method not allowed");
    }
}

static void
handle_request (SoupServer        *server,
                SoupMessage       *message,
                const char        *path,
                GHashTable        *query,
                SoupClientContext *client,
                gpointer           user_data)
{
    MockSnapd *self = MOCK_SNAPD (user_data);
    g_autoptr(GMutexLocker) locker = g_mutex_locker_new (&self->mutex);

    if (self->close_on_request) {
        g_autoptr(GIOStream) stream = soup_client_context_steal_connection (client);
        g_autoptr(GError) error = NULL;

        if (!g_io_stream_close (stream, NULL, &error))
            g_warning("Failed to close stream: %s", error->message);
        return;
    }

    g_clear_pointer (&self->last_request_headers, soup_message_headers_free);
    self->last_request_headers = g_boxed_copy (SOUP_TYPE_MESSAGE_HEADERS, message->request_headers);

    if (strcmp (path, "/v2/system-info") == 0)
        handle_system_info (self, message);
    else if (strcmp (path, "/v2/login") == 0)
        handle_login (self, message);
    else if (strcmp (path, "/v2/logout") == 0)
        handle_logout (self, message);
    else if (strcmp (path, "/v2/snaps") == 0)
        handle_snaps (self, message, query);
    else if (g_str_has_prefix (path, "/v2/snaps/")) {
        if (g_str_has_suffix (path, "/conf")) {
            g_autofree gchar *name = g_strndup (path + strlen ("/v2/snaps/"), strlen (path) - strlen ("/v2/snaps/") - strlen ("/conf"));
            handle_snap_conf (self, message, name, query);
        }
        else
            handle_snap (self, message, path + strlen ("/v2/snaps/"));
    }
    else if (strcmp (path, "/v2/apps") == 0)
        handle_apps (self, message, query);
    else if (g_str_has_prefix (path, "/v2/icons/"))
        handle_icon (self, message, path + strlen ("/v2/icons/"));
    else if (strcmp (path, "/v2/assertions") == 0)
        handle_assertions (self, message, NULL);
    else if (g_str_has_prefix (path, "/v2/assertions/"))
        handle_assertions (self, message, path + strlen ("/v2/assertions/"));
    else if (strcmp (path, "/v2/interfaces") == 0)
        handle_interfaces (self, message, query);
    else if (strcmp (path, "/v2/connections") == 0)
        handle_connections (self, message, query);
    else if (strcmp (path, "/v2/changes") == 0)
        handle_changes (self, message, query);
    else if (g_str_has_prefix (path, "/v2/changes/"))
        handle_change (self, message, path + strlen ("/v2/changes/"));
    else if (strcmp (path, "/v2/find") == 0)
        handle_find (self, message, query);
    else if (strcmp (path, "/v2/buy/ready") == 0)
        handle_buy_ready (self, message);
    else if (strcmp (path, "/v2/buy") == 0)
        handle_buy (self, message);
    else if (strcmp (path, "/v2/sections") == 0)
        handle_sections (self, message);
    else if (strcmp (path, "/v2/aliases") == 0)
        handle_aliases (self, message);
    else if (strcmp (path, "/v2/snapctl") == 0)
        handle_snapctl (self, message);
    else if (strcmp (path, "/v2/create-user") == 0)
        handle_create_user (self, message);
    else if (strcmp (path, "/v2/users") == 0)
        handle_users (self, message);
    else if (strcmp (path, "/v2/download") == 0)
        handle_download (self, message);
    else if (strcmp (path, "/v2/accessories/themes") == 0)
        handle_themes (self, message);
    else
        send_error_not_found (self, message, "not found", NULL);
}

static gboolean
mock_snapd_thread_quit (gpointer user_data)
{
    MockSnapd *self = MOCK_SNAPD (user_data);

    g_main_loop_quit (self->loop);

    return G_SOURCE_REMOVE;
}

static void
mock_snapd_finalize (GObject *object)
{
    MockSnapd *self = MOCK_SNAPD (object);

    /* shut down the server if it is running */
    mock_snapd_stop (self);

    if (g_unlink (self->socket_path) < 0 && errno != ENOENT)
        g_printerr ("Failed to unlink mock snapd socket: %s\n", g_strerror (errno));
    if (g_rmdir (self->dir_path) < 0)
        g_printerr ("Failed to remove temporary directory: %s\n", g_strerror (errno));

    g_clear_pointer (&self->dir_path, g_free);
    g_clear_pointer (&self->socket_path, g_free);
    g_list_free_full (self->accounts, (GDestroyNotify) mock_account_free);
    self->accounts = NULL;
    g_list_free_full (self->interfaces, (GDestroyNotify) mock_interface_free);
    self->interfaces = NULL;
    g_list_free_full (self->snaps, (GDestroyNotify) mock_snap_free);
    self->snaps = NULL;
    g_list_free_full (self->snapshots, (GDestroyNotify) mock_snapshot_free);
    self->snapshots = NULL;
    g_free (self->build_id);
    g_free (self->confinement);
    g_clear_pointer (&self->sandbox_features, g_hash_table_unref);
    g_free (self->store);
    g_free (self->maintenance_kind);
    g_free (self->maintenance_message);
    g_free (self->refresh_hold);
    g_free (self->refresh_last);
    g_free (self->refresh_next);
    g_free (self->refresh_schedule);
    g_free (self->refresh_timer);
    g_list_free_full (self->store_sections, g_free);
    self->store_sections = NULL;
    g_list_free_full (self->store_snaps, (GDestroyNotify) mock_snap_free);
    self->store_snaps = NULL;
    g_list_free_full (self->established_connections, (GDestroyNotify) mock_connection_free);
    self->established_connections = NULL;
    g_list_free_full (self->undesired_connections, (GDestroyNotify) mock_connection_free);
    self->undesired_connections = NULL;
    g_list_free_full (self->assertions, g_free);
    self->assertions = NULL;
    g_list_free_full (self->changes, (GDestroyNotify) mock_change_free);
    self->changes = NULL;
    g_clear_pointer (&self->suggested_currency, g_free);
    g_clear_pointer (&self->spawn_time, g_free);
    g_clear_pointer (&self->ready_time, g_free);
    g_clear_pointer (&self->last_request_headers, soup_message_headers_free);
    g_clear_pointer (&self->gtk_theme_status, g_hash_table_unref);
    g_clear_pointer (&self->icon_theme_status, g_hash_table_unref);
    g_clear_pointer (&self->sound_theme_status, g_hash_table_unref);
    g_clear_pointer (&self->context, g_main_context_unref);
    g_clear_pointer (&self->loop, g_main_loop_unref);

    g_cond_clear (&self->condition);
    g_mutex_clear (&self->mutex);

    G_OBJECT_CLASS (mock_snapd_parent_class)->finalize (object);
}

static GSocket *
open_listening_socket (SoupServer *server, const gchar *socket_path, GError **error)
{
    g_autoptr(GSocket) socket = NULL;
    g_autoptr(GSocketAddress) address = NULL;

    socket = g_socket_new (G_SOCKET_FAMILY_UNIX,
                           G_SOCKET_TYPE_STREAM,
                           G_SOCKET_PROTOCOL_DEFAULT,
                           error);
    if (socket == NULL)
        return NULL;

    address = g_unix_socket_address_new (socket_path);
    if (!g_socket_bind (socket, address, TRUE, error))
        return NULL;

    if (!g_socket_listen (socket, error))
        return NULL;

    if (!soup_server_listen_socket (server, socket, 0, error))
        return NULL;

    return g_steal_pointer (&socket);
}

gpointer
mock_snapd_init_thread (gpointer user_data)
{
    MockSnapd *self = MOCK_SNAPD (user_data);

    g_autoptr(GMutexLocker) locker = g_mutex_locker_new (&self->mutex);

    self->context = g_main_context_new ();
    g_main_context_push_thread_default (self->context);
    self->loop = g_main_loop_new (self->context, FALSE);

    g_autoptr(SoupServer) server = soup_server_new (SOUP_SERVER_SERVER_HEADER, "MockSnapd/1.0", NULL);
    soup_server_add_handler (server, NULL,
                             handle_request, self, NULL);

    g_autoptr(GError) error = NULL;
    g_autoptr(GSocket) socket = open_listening_socket (server, self->socket_path, &error);

    g_cond_signal (&self->condition);
    if (socket == NULL)
        g_propagate_error (self->thread_init_error, error);
    g_clear_pointer (&locker, g_mutex_locker_free);

    /* run until we're told to stop */
    if (socket != NULL)
        g_main_loop_run (self->loop);

    if (g_unlink (self->socket_path) < 0)
        g_printerr ("Failed to unlink mock snapd socket\n");

    g_clear_pointer (&self->loop, g_main_loop_unref);
    g_clear_pointer (&self->context, g_main_context_unref);

    return NULL;
}

gboolean
mock_snapd_start (MockSnapd *self, GError **dest_error)
{
    g_return_val_if_fail (MOCK_IS_SNAPD (self), FALSE);

    g_autoptr(GMutexLocker) locker = g_mutex_locker_new (&self->mutex);
    /* Has the server already started? */
    if (self->thread)
        return TRUE;

    g_autoptr(GError) error = NULL;
    self->thread_init_error = &error;
    self->thread = g_thread_new ("mock_snapd_thread",
                                  mock_snapd_init_thread,
                                  self);
    g_cond_wait (&self->condition, &self->mutex);
    self->thread_init_error = NULL;

    /* If an error occurred during thread startup, clean it up */
    if (error != NULL) {
        g_thread_join (self->thread);
        self->thread = NULL;

        if (dest_error)
            g_propagate_error (dest_error, error);
        else
            g_warning ("Failed to start server: %s", error->message);
    }

    g_assert_nonnull (self->loop);
    g_assert_nonnull (self->context);

    return error == NULL;
}

void
mock_snapd_stop (MockSnapd *self)
{
    g_return_if_fail (MOCK_IS_SNAPD (self));

    if (!self->thread)
        return;

    g_main_context_invoke (self->context, mock_snapd_thread_quit, self);
    g_thread_join (self->thread);
    self->thread = NULL;
    g_assert_null (self->loop);
    g_assert_null (self->context);
}

static void
mock_snapd_class_init (MockSnapdClass *klass)
{
    GObjectClass *gobject_class = G_OBJECT_CLASS (klass);

    gobject_class->finalize = mock_snapd_finalize;
}

static void
mock_snapd_init (MockSnapd *self)
{
    g_mutex_init (&self->mutex);
    g_cond_init (&self->condition);

    self->sandbox_features = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, (GDestroyNotify) g_ptr_array_unref);
    self->gtk_theme_status = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, g_free);
    self->icon_theme_status = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, g_free);
    self->sound_theme_status = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, g_free);
    g_autoptr(GError) error = NULL;
    self->dir_path = g_dir_make_tmp ("mock-snapd-XXXXXX", &error);
    if (self->dir_path == NULL)
        g_warning ("Failed to make temporary directory: %s", error->message);
    g_clear_error (&error);
    self->socket_path = g_build_filename (self->dir_path, "snapd.socket", NULL);
}
