/*
 *  $Id: file.c 29945 2026-05-05 17:43:35Z yeti-dn $
 *  Copyright (C) 2025-2026 David Nečas (Yeti).
 *  E-mail: yeti@gwyddion.net.
 *
 *  This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public
 *  License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any
 *  later version.
 *
 *  This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
 *  warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License for more
 *  details.
 *
 *  You should have received a copy of the GNU General Public License along with this program; if not, write to the
 *  Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
 */

#include "tests/testlibgwy.h"

enum {
    GWY_FILE_N_KINDS = GWY_FILE_CMAP + 1,
};

typedef struct {
    GArray *ids;
    GwyDataKind data_kind;
} IdsOfAKind;

typedef GQuark  (*GetKeyFunc)(gint id);
typedef void    (*SetObjectFunc)(GwyFile *file,
                                 gint id,
                                 gpointer obj);
typedef gpointer(*GetObjectFunc)(GwyFile *file,
                                 gint id);

static void
assert_file_idsv(GwyFile *file, GwyDataKind data_kind, GArray *ids_ref)
{
    guint n, n_ref = ids_ref->len;
    gint *ids = gwy_file_get_idsv(file, data_kind, &n);
    g_assert_cmpuint(n, ==, n_ref);
    g_assert_cmpuint(gwy_file_get_n_data(file, data_kind), ==, n_ref);
    for (guint j = 0; j < n_ref; j++)
        g_assert_cmpint(ids[j], ==, g_array_index(ids_ref, gint, j));

    g_free(ids);
}

static void
assert_file_ids(GwyFile *file, GwyDataKind data_kind, ...)
{
    va_list ap;
    va_start(ap, data_kind);

    GArray *ids_ref = g_array_new(FALSE, FALSE, sizeof(gint));
    gint id;
    while ((id = va_arg(ap, gint)) >= 0)
        g_array_append_val(ids_ref, id);
    va_end(ap);

    assert_file_idsv(file, data_kind, ids_ref);
    g_array_free(ids_ref, TRUE);
}

static void
file_assert_equal(GObject *object, GObject *reference)
{
    g_assert_true(GWY_IS_FILE(object));
    g_assert_true(GWY_IS_FILE(reference));

    GwyFile *file = GWY_FILE(object), *file_ref = GWY_FILE(reference);
    /* Extra check the bookkeeping has been done correctly. Checking the stored data is the same as for Dict. */
    for (guint i = 0; i < GWY_FILE_N_KINDS; i++) {
        guint n, n_ref;
        gint *ids = gwy_file_get_idsv(file, i, &n);
        gint *ids_ref = gwy_file_get_idsv(file_ref, i, &n_ref);
        g_assert_cmpuint(n, ==, n_ref);
        for (guint j = 0; j < n_ref; j++)
            g_assert_cmpint(ids[j], ==, ids_ref[j]);
        g_free(ids_ref);
        g_free(ids);
    }
    /* Extra check the links are correct. */
    const gchar **strkeys = gwy_dict_keys_by_name(GWY_DICT(reference));
    if (strkeys) {
        for (gsize i = 0; strkeys[i]; i++) {
            g_assert_true(!gwy_file_value_is_link(file, strkeys[i]) == !gwy_file_value_is_link(file_ref, strkeys[i]));
        }
        g_free(strkeys);
    }

    dict_assert_equal(object, reference);
}

static GwyFile*
create_file_for_serialization(void)
{
    GwyFile *file = gwy_file_new();
    GwyDict *dict = GWY_DICT(file);
    gwy_dict_pass_object(dict, gwy_file_key_image(4), gwy_field_new(2, 3, 1.0, 1.0, TRUE));
    gwy_dict_pass_object(dict, gwy_file_key_image(5), gwy_field_new(3, 4, 1.0, 1.0, TRUE));
    gwy_dict_pass_object(dict, gwy_file_key_image(0), gwy_field_new(1, 1, 1.0, 1.0, TRUE));
    gwy_dict_pass_object(dict, gwy_file_key_volume(0), gwy_brick_new(2, 2, 2, 1.0, 1.0, 1.0, TRUE));
    gwy_dict_pass_object(dict, gwy_file_key_graph(0), gwy_graph_model_new());
    gwy_dict_pass_object(dict, gwy_file_key_cmap(3), gwy_lawn_new(3, 1, 1.0, 1.0, 2, 0));
    return file;
}

static void
set_file_links_for_serialization(GwyFile *file)
{
    gwy_file_set_link(file, "/image/0/ref/spectra", "/spectra/1");
    gwy_file_set_link(file, "/image/4/ref/spectra", "/spectra/1");
}

void
test_file_serialization(void)
{
    GwyFile *file = create_file_for_serialization();
    serialize_object_and_back(G_OBJECT(file), file_assert_equal, FALSE, NULL);

    set_file_links_for_serialization(file);
    serialize_object_and_back(G_OBJECT(file), file_assert_equal, FALSE, NULL);

    g_assert_finalize_object(file);
}

void
test_file_copy(void)
{
    GwyFile *file = create_file_for_serialization();
    serializable_test_copy(GWY_SERIALIZABLE(file), file_assert_equal);

    set_file_links_for_serialization(file);
    serializable_test_copy(GWY_SERIALIZABLE(file), file_assert_equal);

    g_assert_finalize_object(file);
}

void
test_file_assign(void)
{
    GwyFile *file = create_file_for_serialization();
    serializable_test_assign(GWY_SERIALIZABLE(file), NULL, file_assert_equal);

    set_file_links_for_serialization(file);
    serializable_test_assign(GWY_SERIALIZABLE(file), NULL, file_assert_equal);

    g_assert_finalize_object(file);
}

void
test_file_in_construction(void)
{
    GwyFile *file;

    file = gwy_file_new();
    g_assert_true(GWY_IS_FILE(file));
    g_assert_false(gwy_dict_is_being_constructed(GWY_DICT(file)));
    g_assert_finalize_object(file);

    file = gwy_file_new_in_construction();
    g_assert_true(GWY_IS_FILE(file));
    g_assert_true(gwy_dict_is_being_constructed(GWY_DICT(file)));
    gwy_dict_finish_construction(GWY_DICT(file));
    g_assert_false(gwy_dict_is_being_constructed(GWY_DICT(file)));
    g_assert_finalize_object(file);
}

void
test_file_ids(void)
{
    GwyFile *file = gwy_file_new();
    GwyDict *dict = GWY_DICT(file);

    gwy_dict_pass_object(dict, gwy_file_key_image(4), gwy_field_new(2, 3, 1.0, 1.0, TRUE));
    assert_file_ids(file, GWY_FILE_IMAGE, 4, -1);
    gwy_dict_pass_object(dict, gwy_file_key_image(5), gwy_field_new(3, 4, 1.0, 1.0, TRUE));
    assert_file_ids(file, GWY_FILE_IMAGE, 4, 5, -1);
    gwy_dict_pass_object(dict, gwy_file_key_image(0), gwy_field_new(1, 1, 1.0, 1.0, TRUE));
    assert_file_ids(file, GWY_FILE_IMAGE, 0, 4, 5, -1);
    gwy_dict_pass_object(dict, gwy_file_key_image(0), gwy_field_new(2, 2, 1.0, 1.0, TRUE));
    assert_file_ids(file, GWY_FILE_IMAGE, 0, 4, 5, -1);
    gwy_dict_pass_object(dict, gwy_file_key_image(2), gwy_field_new(1, 1, 1.0, 1.0, TRUE));
    assert_file_ids(file, GWY_FILE_IMAGE, 0, 2, 4, 5, -1);

    gwy_dict_remove(dict, gwy_file_key_image(4));
    assert_file_ids(file, GWY_FILE_IMAGE, 0, 2, 5, -1);
    gwy_dict_remove(dict, gwy_file_key_image(0));
    assert_file_ids(file, GWY_FILE_IMAGE, 2, 5, -1);

    gint newid;
    GwyField *field = gwy_field_new(3, 5, 1.0, 1.0, TRUE);
    newid = gwy_file_add_image(file, field);
    g_assert_cmpint(newid, >, 5);
    assert_file_ids(file, GWY_FILE_IMAGE, 2, 5, newid, -1);

    g_assert_finalize_object(file);
    g_assert_finalize_object(field);
}

static void
parse_one_key(const gchar *strkey, const GwyFileKeyParsed *expected)
{
    GwyFileKeyParsed parsed;
    g_assert_true(gwy_parse_file_key(strkey, &parsed));
    g_assert_cmpint(parsed.data_kind, ==, expected->data_kind);
    g_assert_cmpint(parsed.id, ==, expected->id);
    g_assert_cmpint(parsed.piece, ==, expected->piece);
    g_assert_cmpstr(parsed.suffix, ==, expected->suffix);

    GQuark quark = gwy_form_file_key(&parsed);
    g_assert_true(quark);
    const gchar *reconstructed = g_quark_to_string(quark);
    g_assert_cmpstr(reconstructed, ==, strkey);
}

void
test_file_parse_key(void)
{
    parse_one_key("/image/0", &(GwyFileKeyParsed){0, GWY_FILE_IMAGE, GWY_FILE_PIECE_NONE, NULL});
    parse_one_key("/image/5674", &(GwyFileKeyParsed){5674, GWY_FILE_IMAGE, GWY_FILE_PIECE_NONE, NULL});
    parse_one_key("/image/5/meta", &(GwyFileKeyParsed){5, GWY_FILE_IMAGE, GWY_FILE_PIECE_META, NULL});
    parse_one_key("/image/7/visible", &(GwyFileKeyParsed){7, GWY_FILE_IMAGE, GWY_FILE_PIECE_VISIBLE, NULL});
    parse_one_key("/image/3/range/min", &(GwyFileKeyParsed){3, GWY_FILE_IMAGE, GWY_FILE_PIECE_RANGE, "min"});
    parse_one_key("/image/2/real-square", &(GwyFileKeyParsed){2, GWY_FILE_IMAGE, GWY_FILE_PIECE_REAL_SQUARE, NULL});
    parse_one_key("/image/1/selection/ellipse",
                  &(GwyFileKeyParsed){1, GWY_FILE_IMAGE, GWY_FILE_PIECE_SELECTION, "ellipse"});
    parse_one_key("/image/2/ref/spectra", &(GwyFileKeyParsed){2, GWY_FILE_IMAGE, GWY_FILE_PIECE_REF, "spectra"});

    parse_one_key("/sps/3", &(GwyFileKeyParsed){3, GWY_FILE_SPECTRA, GWY_FILE_PIECE_NONE, NULL});
    parse_one_key("/cmap/1", &(GwyFileKeyParsed){1, GWY_FILE_CMAP, GWY_FILE_PIECE_NONE, NULL});
    parse_one_key("/cmap/7/show", &(GwyFileKeyParsed){7, GWY_FILE_CMAP, GWY_FILE_PIECE_PICTURE, NULL});
    parse_one_key("/cmap/0/palette", &(GwyFileKeyParsed){0, GWY_FILE_CMAP, GWY_FILE_PIECE_PALETTE, NULL});
    parse_one_key("/cmap/666/log", &(GwyFileKeyParsed){666, GWY_FILE_CMAP, GWY_FILE_PIECE_LOG, NULL});
    parse_one_key("/volume/8311", &(GwyFileKeyParsed){8311, GWY_FILE_VOLUME, GWY_FILE_PIECE_NONE, NULL});
    parse_one_key("/volume/6/show", &(GwyFileKeyParsed){6, GWY_FILE_VOLUME, GWY_FILE_PIECE_PICTURE, NULL});
    parse_one_key("/volume/12/palette", &(GwyFileKeyParsed){12, GWY_FILE_VOLUME, GWY_FILE_PIECE_PALETTE, NULL});
    parse_one_key("/xyz/0/meta", &(GwyFileKeyParsed){0, GWY_FILE_XYZ, GWY_FILE_PIECE_META, NULL});
    parse_one_key("/xyz/3/title", &(GwyFileKeyParsed){3, GWY_FILE_XYZ, GWY_FILE_PIECE_TITLE, NULL});
    parse_one_key("/xyz/4/visible", &(GwyFileKeyParsed){4, GWY_FILE_XYZ, GWY_FILE_PIECE_VISIBLE, NULL});
    parse_one_key("/xyz/5/show/type", &(GwyFileKeyParsed){5, GWY_FILE_XYZ, GWY_FILE_PIECE_PICTURE, "type"});
    parse_one_key("/volume/2/show/skip-outliers",
                  &(GwyFileKeyParsed){2, GWY_FILE_VOLUME, GWY_FILE_PIECE_PICTURE, "skip-outliers"});
    parse_one_key("/cmap/1/show/index", &(GwyFileKeyParsed){1, GWY_FILE_CMAP, GWY_FILE_PIECE_PICTURE, "index"});
}

void
test_file_set_link(void)
{
    GwyFile *file = gwy_file_new();
    g_assert_false(gwy_file_value_is_link(file, "/blah/123"));

    // NB: A sufficiently smart File will does not allow plain strings for things that should be links, not links for
    // things that should be plain strings. To avoid a test that will start failing in the future, we must restrict
    // the meddling to valid items. In particular, changing the same key between a link and plain string is not
    // a valid case to test.
    gwy_file_set_link(file, "/image/0/ref/spectra", "/spectra/1");
    g_assert_true(gwy_file_value_is_link(file, "/image/0/ref/spectra"));
    g_assert_false(gwy_file_value_is_link(file, "/spectra/1"));

    gwy_file_set_link(file, "/image/3/ref/spectra", "/spectra/0");
    gwy_file_set_link(file, "/image/1/ref/spectra", "/spectra/0");
    g_assert_true(gwy_file_value_is_link(file, "/image/0/ref/spectra"));
    g_assert_true(gwy_file_value_is_link(file, "/image/1/ref/spectra"));
    g_assert_true(!gwy_file_value_is_link(file, "/image/2/ref/spectra"));
    g_assert_true(gwy_file_value_is_link(file, "/image/3/ref/spectra"));

    gwy_file_set_link(file, "/image/1/ref/spectra", "/spectra/1");
    g_assert_true(gwy_file_value_is_link(file, "/image/0/ref/spectra"));
    g_assert_true(gwy_file_value_is_link(file, "/image/1/ref/spectra"));
    g_assert_true(!gwy_file_value_is_link(file, "/image/2/ref/spectra"));
    g_assert_true(gwy_file_value_is_link(file, "/image/3/ref/spectra"));

    gwy_file_set_link(file, "/image/1/ref/spectra", "/spectra/1");
    g_assert_true(gwy_file_value_is_link(file, "/image/0/ref/spectra"));
    g_assert_true(gwy_file_value_is_link(file, "/image/1/ref/spectra"));
    g_assert_true(!gwy_file_value_is_link(file, "/image/2/ref/spectra"));
    g_assert_true(gwy_file_value_is_link(file, "/image/3/ref/spectra"));

    gwy_dict_remove_by_name(GWY_DICT(file), "/image/1/ref/spectra");
    g_assert_true(gwy_file_value_is_link(file, "/image/0/ref/spectra"));
    g_assert_true(!gwy_file_value_is_link(file, "/image/1/ref/spectra"));
    g_assert_true(!gwy_file_value_is_link(file, "/image/2/ref/spectra"));
    g_assert_true(gwy_file_value_is_link(file, "/image/3/ref/spectra"));

    g_assert_finalize_object(file);
}

static void
set_title_one(GwyFile *file, GwyDataKind data_kind, gint id)
{
    gchar *display_title;

    g_assert_null(gwy_file_get_title(file, data_kind, id));
    display_title = gwy_file_get_display_title(file, data_kind, id);
    g_assert_nonnull(display_title);
    g_free(display_title);
    /* Repeat to ensure the title is not set as a side-effect. */
    g_assert_null(gwy_file_get_title(file, data_kind, id));

    gwy_file_set_title(file, data_kind, id, "Plain", FALSE);

    g_assert_nonnull(gwy_file_get_title(file, data_kind, id));
    g_assert_cmpstr(gwy_file_get_title(file, data_kind, id), ==, "Plain");
    display_title = gwy_file_get_display_title(file, data_kind, id);
    g_assert_nonnull(display_title);
    g_assert_cmpstr(display_title, ==, "Plain");
    g_free(display_title);

    gwy_file_set_title(file, data_kind, id, "Numbered", TRUE);

    g_assert_nonnull(gwy_file_get_title(file, data_kind, id));
    /* It may be a bit controversial what is the expected behavious. Giving the data some smallest unused-yet positive
     * number would be nicer from casual user's perspective. However, giving it the same number it will get in the
     * file is just sooo consistent. So that is what we test. */
    g_assert_cmpstr(gwy_file_get_title(file, data_kind, id), ==, "Numbered 0");
    display_title = gwy_file_get_display_title(file, data_kind, id);
    g_assert_nonnull(display_title);
    g_assert_cmpstr(display_title, ==, "Numbered 0");
    g_free(display_title);
}

void
test_file_set_title_image(void)
{
    GwyFile *file = gwy_file_new();

    GwyField *field = gwy_field_new(4, 2, 1.0, 1.0, TRUE);
    gint id = gwy_file_add_image(file, field);
    set_title_one(file, GWY_FILE_IMAGE, id);
    g_assert_finalize_object(file);
    g_assert_finalize_object(field);
}

void
test_file_set_title_graph(void)
{
    GwyFile *file = gwy_file_new();

    GwyGraphModel *gmodel = gwy_graph_model_new();
    gint id = gwy_file_add_graph(file, gmodel);
    set_title_one(file, GWY_FILE_GRAPH, id);
    g_assert_finalize_object(file);
    g_assert_finalize_object(gmodel);
}

void
test_file_set_title_volume(void)
{
    GwyFile *file = gwy_file_new();

    GwyBrick *brick = gwy_brick_new(4, 2, 2, 1.0, 1.0, 1.0, TRUE);
    gint id = gwy_file_add_volume(file, brick, NULL);
    set_title_one(file, GWY_FILE_VOLUME, id);
    g_assert_finalize_object(file);
    g_assert_finalize_object(brick);
}

void
test_file_set_title_cmap(void)
{
    GwyFile *file = gwy_file_new();

    GwyLawn *lawn = gwy_lawn_new(4, 2, 1.0, 1.0, 1, 0);
    gint id = gwy_file_add_cmap(file, lawn, NULL);
    set_title_one(file, GWY_FILE_CMAP, id);
    g_assert_finalize_object(file);
    g_assert_finalize_object(lawn);
}

void
test_file_set_title_xyz(void)
{
    GwyFile *file = gwy_file_new();

    GwyScatter *scatter = gwy_scatter_new(1);
    gint id = gwy_file_add_xyz(file, scatter);
    set_title_one(file, GWY_FILE_XYZ, id);
    g_assert_finalize_object(file);
    g_assert_finalize_object(scatter);
}

static void
test_file_object_key(GwyDataKind data_kind, GwySerializable *template_obj,
                     GetKeyFunc get_key,
                     SetObjectFunc set_object, SetObjectFunc pass_object, GetObjectFunc get_object)
{
    guint n = 1000;
    guint nobjects = 5;

    for (guint itr = 0; itr < n; itr++) {
        GArray *ids_added = g_array_new(FALSE, FALSE, sizeof(gint));
        GwyFile *file = gwy_file_new_in_construction();
        GwyDict *container = GWY_DICT(file);
        GwySerializable *objects[nobjects];
        gwy_clear(objects, nobjects);

        for (guint j = 0; j < nobjects+1; j++) {
            if (gwy_dict_is_being_constructed(container) && g_test_rand_int_range(0, 1 << nobjects) == 0)
                gwy_dict_finish_construction(container);

            /* Put objects into the file in a random order. */
            guint iobj = g_test_rand_int_range(0, nobjects);
            if (objects[iobj])
                continue;

            objects[iobj] = gwy_serializable_copy(template_obj);
            g_array_append_val(ids_added, iobj);

            /* And using a random method. */
            gint method = g_test_rand_int_range(0, 4);
            if (method == 0)
                gwy_dict_set_object(container, get_key(iobj), objects[iobj]);
            else if (method == 1) {
                GwyFileKeyParsed parsed = {
                    .id = iobj, .data_kind = data_kind, .piece = GWY_FILE_PIECE_NONE, .suffix = NULL
                };
                gwy_dict_set_object(container, gwy_form_file_key(&parsed), objects[iobj]);
            }
            else if (method == 2) {
                set_object(file, iobj, objects[iobj]);
            }
            else {
                g_object_ref(objects[iobj]);
                pass_object(file, iobj, objects[iobj]);
            }
        }
        if (gwy_dict_is_being_constructed(container))
            gwy_dict_finish_construction(container);

        /* Now check if we get the same objects back using different methods. */
        gwy_guint_sort(&g_array_index(ids_added, guint, 0), ids_added->len);
        assert_file_idsv(file, data_kind, ids_added);

        for (guint iobj = 0; iobj < nobjects; iobj++) {
            g_assert_true(get_object(file, iobj) == objects[iobj]);
            if (objects[iobj]) {
                g_assert_true(gwy_dict_get_object(container, get_key(iobj)) == objects[iobj]);
                GwyFileKeyParsed parsed = {
                    .id = iobj, .data_kind = data_kind, .piece = GWY_FILE_PIECE_NONE, .suffix = NULL
                };
                g_assert_true(gwy_dict_get_object(container, gwy_form_file_key(&parsed)) == objects[iobj]);
            }
            else {
                g_assert_false(gwy_dict_contains(container, get_key(iobj)));
                GwyFileKeyParsed parsed = {
                    .id = iobj, .data_kind = data_kind, .piece = GWY_FILE_PIECE_NONE, .suffix = NULL
                };
                g_assert_false(gwy_dict_contains(container, gwy_form_file_key(&parsed)));
            }
        }

        /* Clean up. */
        g_assert_finalize_object(file);
        for (guint iobj = 0; iobj < nobjects; iobj++) {
            if (objects[iobj])
                g_assert_finalize_object(objects[iobj]);
        }
        g_array_free(ids_added, TRUE);
    }

    g_assert_finalize_object(template_obj);
}

void
test_file_key_image(void)
{
    test_file_object_key(GWY_FILE_IMAGE, GWY_SERIALIZABLE(gwy_field_new(1, 1, 1.0, 1.0, TRUE)),
                         gwy_file_key_image,
                         (SetObjectFunc)gwy_file_set_image,
                         (SetObjectFunc)gwy_file_pass_image,
                         (GetObjectFunc)gwy_file_get_image);
}

void
test_file_key_graph(void)
{
    test_file_object_key(GWY_FILE_GRAPH, GWY_SERIALIZABLE(gwy_graph_model_new()),
                         gwy_file_key_graph,
                         (SetObjectFunc)gwy_file_set_graph,
                         (SetObjectFunc)gwy_file_pass_graph,
                         (GetObjectFunc)gwy_file_get_graph);
}

void
test_file_key_volume(void)
{
    test_file_object_key(GWY_FILE_VOLUME, GWY_SERIALIZABLE(gwy_brick_new(1, 1, 1, 1.0, 1.0, 1.0, TRUE)),
                         gwy_file_key_volume,
                         (SetObjectFunc)gwy_file_set_volume,
                         (SetObjectFunc)gwy_file_pass_volume,
                         (GetObjectFunc)gwy_file_get_volume);
}

void
test_file_key_xyz(void)
{
    test_file_object_key(GWY_FILE_XYZ, GWY_SERIALIZABLE(gwy_scatter_new(1)),
                         gwy_file_key_xyz,
                         (SetObjectFunc)gwy_file_set_xyz,
                         (SetObjectFunc)gwy_file_pass_xyz,
                         (GetObjectFunc)gwy_file_get_xyz);
}

void
test_file_key_cmap(void)
{
    test_file_object_key(GWY_FILE_CMAP, GWY_SERIALIZABLE(gwy_lawn_new(1, 1, 1.0, 1.0, 1, 0)),
                         gwy_file_key_cmap,
                         (SetObjectFunc)gwy_file_set_cmap,
                         (SetObjectFunc)gwy_file_pass_cmap,
                         (GetObjectFunc)gwy_file_get_cmap);
}

static void
assert_ids(GQuark key, G_GNUC_UNUSED GValue *value, gpointer user_data)
{
    IdsOfAKind *ioak = (IdsOfAKind*)user_data;
    GArray *ids = ioak->ids;
    GwyFileKeyParsed parsed;
    g_assert_true(gwy_parse_file_key(g_quark_to_string(key), &parsed));
    g_assert_cmpint(parsed.data_kind, ==, ioak->data_kind);
    gboolean id_found = FALSE;
    for (guint i = 0; i < ids->len; i++) {
        if (parsed.id == g_array_index(ids, gint, i)) {
            id_found = TRUE;
            break;
        }
    }
    g_assert_true(id_found);
}

void
test_file_remove_image(void)
{
    enum { n = 5 };
    GwyFile *file = gwy_file_new();
    GwyDict *dict = GWY_DICT(file);
    GArray *ids_array = g_array_new(FALSE, FALSE, sizeof(gint));
    GwyDataKind data_kind = GWY_FILE_IMAGE;
    IdsOfAKind ioak = { .data_kind = data_kind, .ids = ids_array };
    GPtrArray *objects = g_ptr_array_new();
    for (guint i = 0; i < n; i++) {
        GwyField *field = gwy_field_new(2, 2, 1.0, 1.0, TRUE);
        gint id = gwy_file_add_image(file, field);
        g_array_append_val(ids_array, id);
        g_ptr_array_add(objects, field);
        gwy_file_set_title(file, data_kind, id, "Image", TRUE);
        gwy_file_set_palette(file, data_kind, id, "Sky");
        gwy_file_set_visible(file, data_kind, id, TRUE);
        gwy_file_set_color_mapping(file, data_kind, id, GWY_COLOR_MAPPING_ADAPT);
        gwy_file_set_fixed_range(file, data_kind, id, -1.0, 1.0);
        GwyRGBA color = { 1.0, 1.0, 1.0, 0.5 };
        gwy_dict_set_boxed(dict, GWY_TYPE_RGBA, gwy_file_key_image_mask_color(id), &color);
        gwy_dict_set_boolean(dict, gwy_file_key_image_real_square(id), TRUE);
        g_ptr_array_add(objects, gwy_selection_line_new());
        gwy_dict_set_object(dict, gwy_file_key_image_selection(id, "line"),
                            g_ptr_array_index(objects, objects->len-1));
        g_ptr_array_add(objects, gwy_selection_point_new());
        gwy_dict_set_object(dict, gwy_file_key_image_selection(id, "point"),
                            g_ptr_array_index(objects, objects->len-1));
        g_ptr_array_add(objects, gwy_field_new_nield_alike(field));
        gwy_dict_set_object(dict, gwy_file_key_image_mask(id), g_ptr_array_index(objects, objects->len-1));
        gwy_file_pass_meta(file, data_kind, id, gwy_dict_new());
        /* Fake the log entirely. We are not running any module function and we need a pretty complete app setup for
         * logging to work correctly. */
        GwyFileKeyParsed parsed = { .data_kind = data_kind, .id = id, .piece = GWY_FILE_PIECE_LOG, .suffix = NULL };
        g_ptr_array_add(objects, gwy_string_list_new());
        gwy_dict_set_object(dict, gwy_form_file_key(&parsed), g_ptr_array_index(objects, objects->len-1));
    }
    g_assert_true(gwy_dict_get_n_items(dict) % n == 0);
    gint nperobject = gwy_dict_get_n_items(dict)/n;

    gwy_dict_foreach(dict, NULL, assert_ids, &ioak);

    while (ids_array->len) {
        guint idx = g_test_rand_int_range(0, ids_array->len);
        gint id = g_array_index(ids_array, gint, idx);
        g_array_remove_index_fast(ids_array, idx);
        gwy_file_remove(file, data_kind, id);
        gwy_dict_foreach(dict, NULL, assert_ids, &ioak);
        /* A proxy for checking that we have not removed anything else. */
        g_assert_cmpint(gwy_dict_get_n_items(dict), ==, nperobject*(gint)ids_array->len);
    }

    g_array_free(ids_array, TRUE);
    g_assert_finalize_object(file);
    for (guint i = 0; i < objects->len; i++) {
        g_assert_finalize_object(g_ptr_array_index(objects, i));
    }
    g_ptr_array_free(objects, TRUE);
}

void
test_file_remove_graph(void)
{
    enum { n = 5 };
    GwyFile *file = gwy_file_new();
    GwyDict *dict = GWY_DICT(file);
    GArray *ids_array = g_array_new(FALSE, FALSE, sizeof(gint));
    GwyDataKind data_kind = GWY_FILE_GRAPH;
    IdsOfAKind ioak = { .data_kind = data_kind, .ids = ids_array };
    GPtrArray *objects = g_ptr_array_new();
    for (guint i = 0; i < n; i++) {
        GwyGraphModel *gmodel = gwy_graph_model_new();
        g_object_set(gmodel,
                     "title", "My first graph",
                     NULL);
        GwyGraphCurveModel *gcmodel = gwy_graph_curve_model_new();
        gwy_graph_model_add_curve(gmodel, gcmodel);
        g_object_unref(gcmodel);
        gint id = gwy_file_add_graph(file, gmodel);
        g_array_append_val(ids_array, id);
        g_ptr_array_add(objects, gmodel);
        gwy_file_pass_meta(file, data_kind, id, gwy_dict_new());
        /* Fake the log entirely. We are not running any module function and we need a pretty complete app setup for
         * logging to work correctly. */
        GwyFileKeyParsed parsed = { .data_kind = data_kind, .id = id, .piece = GWY_FILE_PIECE_LOG, .suffix = NULL };
        g_ptr_array_add(objects, gwy_string_list_new());
        gwy_dict_set_object(dict, gwy_form_file_key(&parsed), g_ptr_array_index(objects, objects->len-1));
    }
    g_assert_true(gwy_dict_get_n_items(dict) % n == 0);
    gint nperobject = gwy_dict_get_n_items(dict)/n;

    gwy_dict_foreach(dict, NULL, assert_ids, &ioak);

    while (ids_array->len) {
        guint idx = g_test_rand_int_range(0, ids_array->len);
        gint id = g_array_index(ids_array, gint, idx);
        g_array_remove_index_fast(ids_array, idx);
        gwy_file_remove(file, data_kind, id);
        gwy_dict_foreach(dict, NULL, assert_ids, &ioak);
        /* A proxy for checking that we have not removed anything else. */
        g_assert_cmpint(gwy_dict_get_n_items(dict), ==, nperobject*(gint)ids_array->len);
    }

    g_array_free(ids_array, TRUE);
    g_assert_finalize_object(file);
    for (guint i = 0; i < objects->len; i++) {
        g_assert_finalize_object(g_ptr_array_index(objects, i));
    }
    g_ptr_array_free(objects, TRUE);
}

void
test_file_remove_volume(void)
{
    enum { n = 5 };
    GwyFile *file = gwy_file_new();
    GwyDict *dict = GWY_DICT(file);
    GArray *ids_array = g_array_new(FALSE, FALSE, sizeof(gint));
    GwyDataKind data_kind = GWY_FILE_VOLUME;
    IdsOfAKind ioak = { .data_kind = data_kind, .ids = ids_array };
    GPtrArray *objects = g_ptr_array_new();
    for (guint i = 0; i < n; i++) {
        GwyBrick *brick = gwy_brick_new(2, 2, 2, 1.0, 1.0, 1.0, TRUE);
        GwyField *preview = gwy_field_new(2, 2, 1.0, 1.0, TRUE);
        gint id = gwy_file_add_volume(file, brick, preview);
        g_array_append_val(ids_array, id);
        g_ptr_array_add(objects, brick);
        g_ptr_array_add(objects, preview);
        gwy_file_set_title(file, data_kind, id, "Volume data", TRUE);
        gwy_file_set_palette(file, data_kind, id, "Sky");
        gwy_file_set_visible(file, data_kind, id, TRUE);
        gwy_file_set_color_mapping(file, data_kind, id, GWY_COLOR_MAPPING_ADAPT);
        gwy_file_set_fixed_range(file, data_kind, id, -1.0, 1.0);
        GwyFileKeyParsed parsed = {
            .data_kind = data_kind, .id = id, .piece = GWY_FILE_PIECE_REAL_SQUARE, .suffix = NULL
        };
        gwy_dict_set_boolean(dict, gwy_form_file_key(&parsed), TRUE);
        gwy_file_pass_meta(file, data_kind, id, gwy_dict_new());
        /* Fake the log entirely. We are not running any module function and we need a pretty complete app setup for
         * logging to work correctly. */
        parsed.piece = GWY_FILE_PIECE_LOG;
        g_ptr_array_add(objects, gwy_string_list_new());
        gwy_dict_set_object(dict, gwy_form_file_key(&parsed), g_ptr_array_index(objects, objects->len-1));
    }
    g_assert_true(gwy_dict_get_n_items(dict) % n == 0);
    gint nperobject = gwy_dict_get_n_items(dict)/n;

    gwy_dict_foreach(dict, NULL, assert_ids, &ioak);

    while (ids_array->len) {
        guint idx = g_test_rand_int_range(0, ids_array->len);
        gint id = g_array_index(ids_array, gint, idx);
        g_array_remove_index_fast(ids_array, idx);
        gwy_file_remove(file, data_kind, id);
        gwy_dict_foreach(dict, NULL, assert_ids, &ioak);
        /* A proxy for checking that we have not removed anything else. */
        g_assert_cmpint(gwy_dict_get_n_items(dict), ==, nperobject*(gint)ids_array->len);
    }

    g_array_free(ids_array, TRUE);
    g_assert_finalize_object(file);
    for (guint i = 0; i < objects->len; i++) {
        g_assert_finalize_object(g_ptr_array_index(objects, i));
    }
    g_ptr_array_free(objects, TRUE);
}

void
test_file_sync_items_image_nodelete(void)
{
    GwyFile *file = gwy_file_new();

    GwyField *field1 = gwy_field_new(3, 5, 1.0, 1.0, TRUE);
    gint id1 = gwy_file_add_image(file, field1);
    GwyField *field2 = gwy_field_new(4, 4, 1.0, 1.0, TRUE);
    gint id2 = gwy_file_add_image(file, field2);
    g_assert_cmpint(id1, !=, id2);

    GwyDict *container = GWY_DICT(file);
    gwy_dict_set_enum(container, gwy_file_key_image_color_mapping(id1), GWY_COLOR_MAPPING_AUTO);
    gwy_dict_set_string(container, gwy_file_key_image_palette(id1), GWY_GRADIENT_DEFAULT);
    gwy_dict_set_boolean(container, gwy_file_key_image_real_square(id2), TRUE);
    gwy_file_set_title(file, GWY_FILE_IMAGE, id1, "Title", FALSE);

    gwy_file_sync_items(file, GWY_FILE_IMAGE, id1, file, GWY_FILE_IMAGE, id2,
                        GWY_FILE_ITEM_COLOR_MAPPING
                        | GWY_FILE_ITEM_REAL_SQUARE
                        | GWY_FILE_ITEM_PALETTE
                        | GWY_FILE_ITEM_TITLE,
                        FALSE);

    g_assert_true(gwy_dict_contains(container, gwy_file_key_image_color_mapping(id2)));
    g_assert_cmpuint(gwy_dict_get_enum(container, gwy_file_key_image_color_mapping(id2)),
                     ==, GWY_COLOR_MAPPING_AUTO);

    g_assert_true(gwy_dict_contains(container, gwy_file_key_image_palette(id2)));
    g_assert_cmpstr(gwy_dict_get_string(container, gwy_file_key_image_palette(id2)), ==, GWY_GRADIENT_DEFAULT);

    g_assert_true(gwy_dict_contains(container, gwy_file_key_image_title(id2)));
    g_assert_cmpstr(gwy_dict_get_string(container, gwy_file_key_image_title(id2)), ==, "Title");

    g_assert_true(gwy_dict_contains(container, gwy_file_key_image_real_square(id2)));
    g_assert_true(gwy_dict_get_boolean(container, gwy_file_key_image_real_square(id2)));

    g_assert_finalize_object(file);
    g_assert_finalize_object(field2);
    g_assert_finalize_object(field1);
}

void
test_file_sync_items_image_delete(void)
{
    GwyFile *file = gwy_file_new();

    GwyField *field1 = gwy_field_new(3, 5, 1.0, 1.0, TRUE);
    gint id1 = gwy_file_add_image(file, field1);
    GwyField *field2 = gwy_field_new(4, 4, 1.0, 1.0, TRUE);
    gint id2 = gwy_file_add_image(file, field2);
    g_assert_cmpint(id1, !=, id2);

    GwyDict *container = GWY_DICT(file);
    gwy_dict_set_enum(container, gwy_file_key_image_color_mapping(id1), GWY_COLOR_MAPPING_AUTO);
    gwy_dict_set_string(container, gwy_file_key_image_palette(id1), GWY_GRADIENT_DEFAULT);
    gwy_dict_set_boolean(container, gwy_file_key_image_real_square(id2), TRUE);
    gwy_file_set_title(file, GWY_FILE_IMAGE, id1, "Title", FALSE);

    gwy_file_sync_items(file, GWY_FILE_IMAGE, id1, file, GWY_FILE_IMAGE, id2,
                        GWY_FILE_ITEM_COLOR_MAPPING
                        | GWY_FILE_ITEM_REAL_SQUARE
                        | GWY_FILE_ITEM_PALETTE
                        | GWY_FILE_ITEM_TITLE,
                        TRUE);

    g_assert_true(gwy_dict_contains(container, gwy_file_key_image_color_mapping(id2)));
    g_assert_cmpuint(gwy_dict_get_enum(container, gwy_file_key_image_color_mapping(id2)),
                     ==, GWY_COLOR_MAPPING_AUTO);

    g_assert_true(gwy_dict_contains(container, gwy_file_key_image_palette(id2)));
    g_assert_cmpstr(gwy_dict_get_string(container, gwy_file_key_image_palette(id2)), ==, GWY_GRADIENT_DEFAULT);

    g_assert_true(gwy_dict_contains(container, gwy_file_key_image_title(id2)));
    g_assert_cmpstr(gwy_dict_get_string(container, gwy_file_key_image_title(id2)), ==, "Title");

    g_assert_false(gwy_dict_contains(container, gwy_file_key_image_real_square(id2)));

    g_assert_finalize_object(file);
    g_assert_finalize_object(field2);
    g_assert_finalize_object(field1);
}

void
test_file_copy_data_image(void)
{
    GwyFile *file = gwy_file_new();

    GwyField *field = gwy_field_new(3, 5, 1.0, 1.0, TRUE);
    gint id = gwy_file_add_image(file, field);

    GwyDict *container = GWY_DICT(file);
    gwy_dict_set_enum(container, gwy_file_key_image_color_mapping(id), GWY_COLOR_MAPPING_AUTO);
    gwy_dict_set_string(container, gwy_file_key_image_palette(id), GWY_GRADIENT_DEFAULT);
    gwy_dict_set_boolean(container, gwy_file_key_image_real_square(id), TRUE);
    gwy_dict_pass_object(container, gwy_file_key_image_mask(id), gwy_field_new_nield_alike(field));
    gwy_file_set_title(file, GWY_FILE_IMAGE, id, "Title", FALSE);

    gint newid = gwy_file_copy_data(file, GWY_FILE_IMAGE, id, file);
    g_assert_cmpint(newid, !=, id);

    g_assert_true(gwy_dict_contains(container, gwy_file_key_image(newid)));
    g_assert_true(!!gwy_dict_get_object(container, gwy_file_key_image(newid)));
    g_assert_true(gwy_dict_get_object(container, gwy_file_key_image(newid))
                  !=
                  gwy_dict_get_object(container, gwy_file_key_image(id)));

    g_assert_true(gwy_dict_contains(container, gwy_file_key_image_color_mapping(newid)));
    g_assert_cmpuint(gwy_dict_get_enum(container, gwy_file_key_image_color_mapping(newid)),
                     ==, GWY_COLOR_MAPPING_AUTO);

    g_assert_true(gwy_dict_contains(container, gwy_file_key_image_palette(newid)));
    g_assert_cmpstr(gwy_dict_get_string(container, gwy_file_key_image_palette(newid)), ==, GWY_GRADIENT_DEFAULT);

    g_assert_true(gwy_dict_contains(container, gwy_file_key_image_title(newid)));
    g_assert_cmpstr(gwy_dict_get_string(container, gwy_file_key_image_title(newid)), ==, "Title");

    g_assert_true(gwy_dict_contains(container, gwy_file_key_image_real_square(newid)));
    g_assert_true(gwy_dict_get_boolean(container, gwy_file_key_image_real_square(newid)));

    g_assert_true(gwy_dict_contains(container, gwy_file_key_image_mask(newid)));
    g_assert_true(!!gwy_dict_get_object(container, gwy_file_key_image_mask(newid)));
    g_assert_true(gwy_dict_get_object(container, gwy_file_key_image_mask(newid))
                  !=
                  gwy_dict_get_object(container, gwy_file_key_image_mask(id)));

    g_assert_finalize_object(file);
    g_assert_finalize_object(field);
}

// Helper function for making a shoddy (or outright) invalid File by taking an arbitrary Dict and changing the object
// name to GwyFile.
static guchar*
make_fake_serialized_file(GwyDict *dict, gsize *size)
{
    g_type_class_ref(GWY_TYPE_FILE);
    GOutputStream *stream = g_memory_output_stream_new_resizable();
    GMemoryOutputStream *memstream = G_MEMORY_OUTPUT_STREAM(stream);
    GError *error = NULL;
    gboolean ok = gwy_serialize_gio(GWY_SERIALIZABLE(dict), stream, &error);
    g_assert_true(ok);
    g_assert_no_error(error);
    *size = g_memory_output_stream_get_data_size(memstream);
    guchar *data = g_memdup2(g_memory_output_stream_get_data(memstream), *size);
    g_assert_finalize_object(stream);
    g_assert_cmpint(memcmp(data, "GwyDict", 8), ==, 0);
    memcpy(data, "GwyFile", 7);
    return data;
}

static void
add_bad_key_error(GwyErrorList **error_list, const gchar *key)
{
    gwy_error_list_add(error_list, GWY_FILE_ERROR, GWY_FILE_ERROR_KEY,
                       "Uknown/invalid file key ‘%s’.", key);
}

void
test_file_validate_bad_key(void)
{
    GwyDict *dict = gwy_dict_new();
    // Add also some good.
    gwy_dict_pass_object_by_name(dict, "/image/3", gwy_field_new(1, 1, 1.0, 1.0, TRUE));
    GwyDict *good_dict = gwy_dict_copy(dict);

    gwy_dict_set_int32_by_name(dict, "blah blah!", 1);
    gwy_dict_set_int32_by_name(dict, "///", 2);
    gwy_dict_set_boolean_by_name(dict, "/image/-3/visible", TRUE);
    gwy_dict_set_int32_by_name(dict, "/image/", 0);
    gwy_dict_set_int32_by_name(dict, "/image/1/", 0);

    gsize size;
    gchar *data = make_fake_serialized_file(dict, &size);
    g_assert_finalize_object(dict);

    gsize bytes_consumed;
    GwyErrorList *error_list = NULL, *expected_errors = NULL;
    add_bad_key_error(&expected_errors, "blah blah!");
    add_bad_key_error(&expected_errors, "///");
    add_bad_key_error(&expected_errors, "/image/-3/visible");
    add_bad_key_error(&expected_errors, "/image/");
    add_bad_key_error(&expected_errors, "/image/1/");
    GObject *file = gwy_deserialize_memory(data, size, &bytes_consumed, &error_list);

    assert_error_list(error_list, expected_errors, TRUE);

    gwy_error_list_clear(&error_list);
    gwy_error_list_clear(&expected_errors);

    dict_assert_equal(G_OBJECT(file), G_OBJECT(good_dict));

    g_assert_finalize_object(file);
    g_assert_finalize_object(good_dict);
}

static void
add_item_type_error(GwyErrorList **error_list, const gchar *key, const gchar *goodtype, const gchar *badtype)
{
    gwy_error_list_add(error_list, GWY_FILE_ERROR, GWY_FILE_ERROR_TYPE,
                       "Wrong type of ‘%s’ value (%s instead of %s).",
                       key, badtype, goodtype);
}

void
test_file_validate_item_type_image(void)
{
    GwyDict *dict = gwy_dict_new();

    // Add also some good.
    gwy_dict_pass_object_by_name(dict, "/image/0", gwy_field_new(1, 1, 1.0, 1.0, TRUE));
    GwyDict *good_dict = gwy_dict_copy(dict);

    gwy_dict_pass_object_by_name(dict, "/image/1", gwy_nield_new(1, 1));
    gwy_dict_set_string_by_name(dict, "/image/3", "Surprise!");
    gwy_dict_set_int32_by_name(dict, "/image/4", 12345);
    gwy_dict_set_boolean_by_name(dict, "/image/0/title", TRUE);
    gwy_dict_set_double_by_name(dict, "/image/0/visible", G_PI);
    gwy_dict_set_string_by_name(dict, "/image/0/range/min", "minimum");
    gwy_dict_set_string_by_name(dict, "/image/0/range/max", "maximum");
    gwy_dict_pass_object_by_name(dict, "/image/0/color-mapping", gwy_dict_new());
    gwy_dict_set_boolean_by_name(dict, "/image/0/log", TRUE);
    gwy_dict_set_int64_by_name(dict, "/image/0/meta", 666);
    gwy_dict_pass_object_by_name(dict, "/image/0/mask", gwy_dict_new());

    gsize size;
    gchar *data = make_fake_serialized_file(dict, &size);
    g_assert_finalize_object(dict);

    gsize bytes_consumed;
    GwyErrorList *error_list = NULL, *expected_errors = NULL;
    add_item_type_error(&expected_errors, "/image/1", "GwyField", "GwyNield");
    add_item_type_error(&expected_errors, "/image/3", "GwyField", "gchararray");
    add_item_type_error(&expected_errors, "/image/4", "GwyField", "gint");
    add_item_type_error(&expected_errors, "/image/0/title", "gchararray", "gboolean");
    add_item_type_error(&expected_errors, "/image/0/visible", "gboolean", "gdouble");
    add_item_type_error(&expected_errors, "/image/0/range/min", "gdouble", "gchararray");
    add_item_type_error(&expected_errors, "/image/0/range/max", "gdouble", "gchararray");
    add_item_type_error(&expected_errors, "/image/0/color-mapping", "gint", "GwyDict");
    add_item_type_error(&expected_errors, "/image/0/log", "GwyStringList", "gboolean");
    add_item_type_error(&expected_errors, "/image/0/meta", "GwyDict", "gint64");
    add_item_type_error(&expected_errors, "/image/0/mask", "GwyNield", "GwyDict");
    GObject *file = gwy_deserialize_memory(data, size, &bytes_consumed, &error_list);

    assert_error_list(error_list, expected_errors, TRUE);

    gwy_error_list_clear(&error_list);
    gwy_error_list_clear(&expected_errors);

    dict_assert_equal(G_OBJECT(file), G_OBJECT(good_dict));

    g_assert_finalize_object(file);
    g_assert_finalize_object(good_dict);
}

void
test_file_validate_item_type_graph(void)
{
    GwyDict *dict = gwy_dict_new();
    // Add also some good.
    gwy_dict_pass_object_by_name(dict, "/graph/0", gwy_graph_model_new());
    GwyDict *good_dict = gwy_dict_copy(dict);

    gwy_dict_pass_object_by_name(dict, "/graph/1", gwy_nield_new(1, 1));
    gwy_dict_set_string_by_name(dict, "/graph/3", "Surprise!");
    gwy_dict_set_int32_by_name(dict, "/graph/4", 12345);
    gwy_dict_set_int64_by_name(dict, "/graph/0/visible", 17000);
    gwy_dict_set_double_by_name(dict, "/graph/0/log", 3.3);
    gwy_dict_set_boolean_by_name(dict, "/graph/0/meta", TRUE);
    gwy_dict_set_boolean_by_name(dict, "/graph/0/view/width", TRUE);
    gwy_dict_set_boolean_by_name(dict, "/graph/0/view/height", FALSE);

    gsize size;
    gchar *data = make_fake_serialized_file(dict, &size);
    g_assert_finalize_object(dict);

    gsize bytes_consumed;
    GwyErrorList *error_list = NULL, *expected_errors = NULL;
    add_item_type_error(&expected_errors, "/graph/1", "GwyGraphModel", "GwyNield");
    add_item_type_error(&expected_errors, "/graph/3", "GwyGraphModel", "gchararray");
    add_item_type_error(&expected_errors, "/graph/4", "GwyGraphModel", "gint");
    add_item_type_error(&expected_errors, "/graph/0/visible", "gboolean", "gint64");
    add_item_type_error(&expected_errors, "/graph/0/log", "GwyStringList", "gdouble");
    add_item_type_error(&expected_errors, "/graph/0/meta", "GwyDict", "gboolean");
    add_item_type_error(&expected_errors, "/graph/0/view/width", "gint", "gboolean");
    add_item_type_error(&expected_errors, "/graph/0/view/height", "gint", "gboolean");
    GObject *file = gwy_deserialize_memory(data, size, &bytes_consumed, &error_list);

    assert_error_list(error_list, expected_errors, TRUE);

    gwy_error_list_clear(&error_list);
    gwy_error_list_clear(&expected_errors);

    dict_assert_equal(G_OBJECT(file), G_OBJECT(good_dict));

    g_assert_finalize_object(file);
    g_assert_finalize_object(good_dict);
}

void
test_file_validate_item_type_volume(void)
{
    GwyDict *dict = gwy_dict_new();
    // Add also some good.
    gwy_dict_pass_object_by_name(dict, "/volume/0", gwy_brick_new(1, 1, 1, 1.0, 1.0, 1.0, TRUE));
    GwyDict *good_dict = gwy_dict_copy(dict);

    gwy_dict_pass_object_by_name(dict, "/volume/1", gwy_field_new(1, 1, 1.0, 1.0, TRUE));
    gwy_dict_set_string_by_name(dict, "/volume/3", "Surprise!");
    gwy_dict_set_int32_by_name(dict, "/volume/4", 12345);
    gwy_dict_set_boolean_by_name(dict, "/volume/0/title", TRUE);
    gwy_dict_set_double_by_name(dict, "/volume/0/visible", G_PI);
    gwy_dict_pass_object_by_name(dict, "/volume/0/color-mapping", gwy_dict_new());
    gwy_dict_set_boolean_by_name(dict, "/volume/0/log", TRUE);
    gwy_dict_set_int64_by_name(dict, "/volume/0/meta", 666);
    gwy_dict_pass_object_by_name(dict, "/volume/0/show", gwy_nield_new(1, 1));

    gsize size;
    gchar *data = make_fake_serialized_file(dict, &size);
    g_assert_finalize_object(dict);

    gsize bytes_consumed;
    GwyErrorList *error_list = NULL, *expected_errors = NULL;
    add_item_type_error(&expected_errors, "/volume/1", "GwyBrick", "GwyField");
    add_item_type_error(&expected_errors, "/volume/3", "GwyBrick", "gchararray");
    add_item_type_error(&expected_errors, "/volume/4", "GwyBrick", "gint");
    add_item_type_error(&expected_errors, "/volume/0/title", "gchararray", "gboolean");
    add_item_type_error(&expected_errors, "/volume/0/visible", "gboolean", "gdouble");
    add_item_type_error(&expected_errors, "/volume/0/color-mapping", "gint", "GwyDict");
    add_item_type_error(&expected_errors, "/volume/0/log", "GwyStringList", "gboolean");
    add_item_type_error(&expected_errors, "/volume/0/meta", "GwyDict", "gint64");
    add_item_type_error(&expected_errors, "/volume/0/show", "GwyField", "GwyNield");
    GObject *file = gwy_deserialize_memory(data, size, &bytes_consumed, &error_list);

    assert_error_list(error_list, expected_errors, TRUE);

    gwy_error_list_clear(&error_list);
    gwy_error_list_clear(&expected_errors);

    dict_assert_equal(G_OBJECT(file), G_OBJECT(good_dict));

    g_assert_finalize_object(file);
    g_assert_finalize_object(good_dict);
}

void
test_file_validate_item_type_xyz(void)
{
    GwyDict *dict = gwy_dict_new();
    // Add also some good.
    gwy_dict_pass_object_by_name(dict, "/xyz/0", gwy_scatter_new_from_xyz(&(GwyXYZ){ 0, 0, 0 }, 1));
    GwyDict *good_dict = gwy_dict_copy(dict);

    gwy_dict_pass_object_by_name(dict, "/xyz/1", gwy_field_new(1, 1, 1.0, 1.0, TRUE));
    gwy_dict_set_string_by_name(dict, "/xyz/3", "Surprise!");
    gwy_dict_set_int32_by_name(dict, "/xyz/4", 12345);
    gwy_dict_set_boolean_by_name(dict, "/xyz/0/title", TRUE);
    gwy_dict_set_double_by_name(dict, "/xyz/0/visible", G_PI);
    gwy_dict_pass_object_by_name(dict, "/xyz/0/color-mapping", gwy_dict_new());
    gwy_dict_set_boolean_by_name(dict, "/xyz/0/log", TRUE);
    gwy_dict_set_int64_by_name(dict, "/xyz/0/meta", 666);

    gsize size;
    gchar *data = make_fake_serialized_file(dict, &size);
    g_assert_finalize_object(dict);

    gsize bytes_consumed;
    GwyErrorList *error_list = NULL, *expected_errors = NULL;
    add_item_type_error(&expected_errors, "/xyz/1", "GwyScatter", "GwyField");
    add_item_type_error(&expected_errors, "/xyz/3", "GwyScatter", "gchararray");
    add_item_type_error(&expected_errors, "/xyz/4", "GwyScatter", "gint");
    add_item_type_error(&expected_errors, "/xyz/0/title", "gchararray", "gboolean");
    add_item_type_error(&expected_errors, "/xyz/0/visible", "gboolean", "gdouble");
    add_item_type_error(&expected_errors, "/xyz/0/color-mapping", "gint", "GwyDict");
    add_item_type_error(&expected_errors, "/xyz/0/log", "GwyStringList", "gboolean");
    add_item_type_error(&expected_errors, "/xyz/0/meta", "GwyDict", "gint64");
    GObject *file = gwy_deserialize_memory(data, size, &bytes_consumed, &error_list);

    assert_error_list(error_list, expected_errors, TRUE);

    gwy_error_list_clear(&error_list);
    gwy_error_list_clear(&expected_errors);

    dict_assert_equal(G_OBJECT(file), G_OBJECT(good_dict));

    g_assert_finalize_object(file);
    g_assert_finalize_object(good_dict);
}

void
test_file_validate_item_type_cmap(void)
{
    GwyDict *dict = gwy_dict_new();
    // Add also some good.
    gwy_dict_pass_object_by_name(dict, "/cmap/0", gwy_lawn_new(1, 1, 1.0, 1.0, 1, 0));
    GwyDict *good_dict = gwy_dict_copy(dict);

    gwy_dict_pass_object_by_name(dict, "/cmap/1", gwy_field_new(1, 1, 1.0, 1.0, TRUE));
    gwy_dict_set_string_by_name(dict, "/cmap/3", "Surprise!");
    gwy_dict_set_int32_by_name(dict, "/cmap/4", 12345);
    gwy_dict_set_boolean_by_name(dict, "/cmap/0/title", TRUE);
    gwy_dict_set_double_by_name(dict, "/cmap/0/visible", G_PI);
    gwy_dict_pass_object_by_name(dict, "/cmap/0/color-mapping", gwy_dict_new());
    gwy_dict_set_boolean_by_name(dict, "/cmap/0/log", TRUE);
    gwy_dict_set_int64_by_name(dict, "/cmap/0/meta", 666);
    gwy_dict_pass_object_by_name(dict, "/cmap/0/show", gwy_nield_new(1, 1));

    gsize size;
    gchar *data = make_fake_serialized_file(dict, &size);
    g_assert_finalize_object(dict);

    gsize bytes_consumed;
    GwyErrorList *error_list = NULL, *expected_errors = NULL;
    add_item_type_error(&expected_errors, "/cmap/1", "GwyLawn", "GwyField");
    add_item_type_error(&expected_errors, "/cmap/3", "GwyLawn", "gchararray");
    add_item_type_error(&expected_errors, "/cmap/4", "GwyLawn", "gint");
    add_item_type_error(&expected_errors, "/cmap/0/title", "gchararray", "gboolean");
    add_item_type_error(&expected_errors, "/cmap/0/visible", "gboolean", "gdouble");
    add_item_type_error(&expected_errors, "/cmap/0/color-mapping", "gint", "GwyDict");
    add_item_type_error(&expected_errors, "/cmap/0/log", "GwyStringList", "gboolean");
    add_item_type_error(&expected_errors, "/cmap/0/meta", "GwyDict", "gint64");
    add_item_type_error(&expected_errors, "/cmap/0/show", "GwyField", "GwyNield");
    GObject *file = gwy_deserialize_memory(data, size, &bytes_consumed, &error_list);

    assert_error_list(error_list, expected_errors, TRUE);

    gwy_error_list_clear(&error_list);
    gwy_error_list_clear(&expected_errors);

    dict_assert_equal(G_OBJECT(file), G_OBJECT(good_dict));

    g_assert_finalize_object(file);
    g_assert_finalize_object(good_dict);
}

static void
add_orphaned_error(GwyErrorList **error_list, const gchar *key)
{
    gwy_error_list_add(error_list, GWY_FILE_ERROR, GWY_FILE_ERROR_ORPHAN,
                       "Orphaned file item ‘%s’.", key);
}

void
test_file_validate_orphaned_image(void)
{
    GwyDict *dict = gwy_dict_new();

    // Add also some good.
    gwy_dict_pass_object_by_name(dict, "/image/5", gwy_field_new(1, 1, 1.0, 1.0, TRUE));
    GwyDict *good_dict = gwy_dict_copy(dict);

    gwy_dict_set_string_by_name(dict, "/image/1/title", "Title");
    gwy_dict_set_boolean_by_name(dict, "/image/1/visible", G_PI);
    gwy_dict_set_double_by_name(dict, "/image/1/range/min", 0.0);
    gwy_dict_set_double_by_name(dict, "/image/1/range/max", 1.0);
    gwy_dict_set_enum_by_name(dict, "/image/1/color-mapping", GWY_COLOR_MAPPING_AUTO);
    gwy_dict_pass_object_by_name(dict, "/image/1/log", gwy_string_list_new());
    gwy_dict_pass_object_by_name(dict, "/image/1/meta", gwy_dict_new());
    gwy_dict_pass_object_by_name(dict, "/image/1/mask", gwy_nield_new(1, 1));

    gsize size;
    gchar *data = make_fake_serialized_file(dict, &size);
    g_assert_finalize_object(dict);

    gsize bytes_consumed;
    GwyErrorList *error_list = NULL, *expected_errors = NULL;
    add_orphaned_error(&expected_errors, "/image/1/title");
    add_orphaned_error(&expected_errors, "/image/1/visible");
    add_orphaned_error(&expected_errors, "/image/1/range/min");
    add_orphaned_error(&expected_errors, "/image/1/range/max");
    add_orphaned_error(&expected_errors, "/image/1/color-mapping");
    add_orphaned_error(&expected_errors, "/image/1/log");
    add_orphaned_error(&expected_errors, "/image/1/meta");
    add_orphaned_error(&expected_errors, "/image/1/mask");
    GObject *file = gwy_deserialize_memory(data, size, &bytes_consumed, &error_list);

    assert_error_list(error_list, expected_errors, TRUE);

    gwy_error_list_clear(&error_list);
    gwy_error_list_clear(&expected_errors);

    dict_assert_equal(G_OBJECT(file), G_OBJECT(good_dict));

    g_assert_finalize_object(file);
    g_assert_finalize_object(good_dict);
}

void
test_file_validate_orphaned_graph(void)
{
    GwyDict *dict = gwy_dict_new();

    // Add also some good.
    gwy_dict_pass_object_by_name(dict, "/graph/5", gwy_graph_model_new());
    GwyDict *good_dict = gwy_dict_copy(dict);

    gwy_dict_set_boolean_by_name(dict, "/graph/1/visible", G_PI);
    gwy_dict_pass_object_by_name(dict, "/graph/1/log", gwy_string_list_new());
    gwy_dict_pass_object_by_name(dict, "/graph/1/meta", gwy_dict_new());

    gsize size;
    gchar *data = make_fake_serialized_file(dict, &size);
    g_assert_finalize_object(dict);

    gsize bytes_consumed;
    GwyErrorList *error_list = NULL, *expected_errors = NULL;
    add_orphaned_error(&expected_errors, "/graph/1/visible");
    add_orphaned_error(&expected_errors, "/graph/1/log");
    add_orphaned_error(&expected_errors, "/graph/1/meta");
    GObject *file = gwy_deserialize_memory(data, size, &bytes_consumed, &error_list);

    assert_error_list(error_list, expected_errors, TRUE);

    gwy_error_list_clear(&error_list);
    gwy_error_list_clear(&expected_errors);

    dict_assert_equal(G_OBJECT(file), G_OBJECT(good_dict));

    g_assert_finalize_object(file);
    g_assert_finalize_object(good_dict);
}

void
test_file_validate_orphaned_volume(void)
{
    GwyDict *dict = gwy_dict_new();

    // Add also some good.
    gwy_dict_pass_object_by_name(dict, "/volume/5", gwy_brick_new(1, 1, 1, 1.0, 1.0, 1.0, TRUE));
    GwyDict *good_dict = gwy_dict_copy(dict);

    gwy_dict_set_string_by_name(dict, "/volume/1/title", "Title");
    gwy_dict_set_boolean_by_name(dict, "/volume/1/visible", G_PI);
    gwy_dict_set_double_by_name(dict, "/volume/1/range/min", 0.0);
    gwy_dict_set_double_by_name(dict, "/volume/1/range/max", 1.0);
    gwy_dict_set_enum_by_name(dict, "/volume/1/color-mapping", GWY_COLOR_MAPPING_AUTO);
    gwy_dict_pass_object_by_name(dict, "/volume/1/log", gwy_string_list_new());
    gwy_dict_pass_object_by_name(dict, "/volume/1/meta", gwy_dict_new());

    gsize size;
    gchar *data = make_fake_serialized_file(dict, &size);
    g_assert_finalize_object(dict);

    gsize bytes_consumed;
    GwyErrorList *error_list = NULL, *expected_errors = NULL;
    add_orphaned_error(&expected_errors, "/volume/1/title");
    add_orphaned_error(&expected_errors, "/volume/1/visible");
    add_orphaned_error(&expected_errors, "/volume/1/range/min");
    add_orphaned_error(&expected_errors, "/volume/1/range/max");
    add_orphaned_error(&expected_errors, "/volume/1/color-mapping");
    add_orphaned_error(&expected_errors, "/volume/1/log");
    add_orphaned_error(&expected_errors, "/volume/1/meta");
    GObject *file = gwy_deserialize_memory(data, size, &bytes_consumed, &error_list);

    assert_error_list(error_list, expected_errors, TRUE);

    gwy_error_list_clear(&error_list);
    gwy_error_list_clear(&expected_errors);

    dict_assert_equal(G_OBJECT(file), G_OBJECT(good_dict));

    g_assert_finalize_object(file);
    g_assert_finalize_object(good_dict);
}

void
test_file_validate_orphaned_xyz(void)
{
    GwyDict *dict = gwy_dict_new();

    // Add also some good.
    gwy_dict_pass_object_by_name(dict, "/xyz/5", gwy_scatter_new_from_xyz(&(GwyXYZ){ 0, 0, 0 }, 1));
    GwyDict *good_dict = gwy_dict_copy(dict);

    gwy_dict_set_string_by_name(dict, "/xyz/1/title", "Title");
    gwy_dict_set_boolean_by_name(dict, "/xyz/1/visible", G_PI);
    gwy_dict_set_double_by_name(dict, "/xyz/1/range/min", 0.0);
    gwy_dict_set_double_by_name(dict, "/xyz/1/range/max", 1.0);
    gwy_dict_set_enum_by_name(dict, "/xyz/1/color-mapping", GWY_COLOR_MAPPING_AUTO);
    gwy_dict_pass_object_by_name(dict, "/xyz/1/log", gwy_string_list_new());
    gwy_dict_pass_object_by_name(dict, "/xyz/1/meta", gwy_dict_new());

    gsize size;
    gchar *data = make_fake_serialized_file(dict, &size);
    g_assert_finalize_object(dict);

    gsize bytes_consumed;
    GwyErrorList *error_list = NULL, *expected_errors = NULL;
    add_orphaned_error(&expected_errors, "/xyz/1/title");
    add_orphaned_error(&expected_errors, "/xyz/1/visible");
    add_orphaned_error(&expected_errors, "/xyz/1/range/min");
    add_orphaned_error(&expected_errors, "/xyz/1/range/max");
    add_orphaned_error(&expected_errors, "/xyz/1/color-mapping");
    add_orphaned_error(&expected_errors, "/xyz/1/log");
    add_orphaned_error(&expected_errors, "/xyz/1/meta");
    GObject *file = gwy_deserialize_memory(data, size, &bytes_consumed, &error_list);

    assert_error_list(error_list, expected_errors, TRUE);

    gwy_error_list_clear(&error_list);
    gwy_error_list_clear(&expected_errors);

    dict_assert_equal(G_OBJECT(file), G_OBJECT(good_dict));

    g_assert_finalize_object(file);
    g_assert_finalize_object(good_dict);
}

void
test_file_validate_orphaned_cmap(void)
{
    GwyDict *dict = gwy_dict_new();

    // Add also some good.
    gwy_dict_pass_object_by_name(dict, "/cmap/5", gwy_lawn_new(1, 1, 1.0, 1.0, 1, 0));
    GwyDict *good_dict = gwy_dict_copy(dict);

    gwy_dict_set_string_by_name(dict, "/cmap/1/title", "Title");
    gwy_dict_set_boolean_by_name(dict, "/cmap/1/visible", G_PI);
    gwy_dict_set_double_by_name(dict, "/cmap/1/range/min", 0.0);
    gwy_dict_set_double_by_name(dict, "/cmap/1/range/max", 1.0);
    gwy_dict_set_enum_by_name(dict, "/cmap/1/color-mapping", GWY_COLOR_MAPPING_AUTO);
    gwy_dict_pass_object_by_name(dict, "/cmap/1/log", gwy_string_list_new());
    gwy_dict_pass_object_by_name(dict, "/cmap/1/meta", gwy_dict_new());

    gsize size;
    gchar *data = make_fake_serialized_file(dict, &size);
    g_assert_finalize_object(dict);

    gsize bytes_consumed;
    GwyErrorList *error_list = NULL, *expected_errors = NULL;
    add_orphaned_error(&expected_errors, "/cmap/1/title");
    add_orphaned_error(&expected_errors, "/cmap/1/visible");
    add_orphaned_error(&expected_errors, "/cmap/1/range/min");
    add_orphaned_error(&expected_errors, "/cmap/1/range/max");
    add_orphaned_error(&expected_errors, "/cmap/1/color-mapping");
    add_orphaned_error(&expected_errors, "/cmap/1/log");
    add_orphaned_error(&expected_errors, "/cmap/1/meta");
    GObject *file = gwy_deserialize_memory(data, size, &bytes_consumed, &error_list);

    assert_error_list(error_list, expected_errors, TRUE);

    gwy_error_list_clear(&error_list);
    gwy_error_list_clear(&expected_errors);

    dict_assert_equal(G_OBJECT(file), G_OBJECT(good_dict));

    g_assert_finalize_object(file);
    g_assert_finalize_object(good_dict);
}

static void
add_size_mismatch_error(GwyErrorList **error_list, const gchar *key)
{
    gwy_error_list_add(error_list, GWY_FILE_ERROR, GWY_FILE_ERROR_SIZE_MISMATCH,
                       "Item ‘%s’ is incompatible with its main data.", key);
}

void
test_file_validate_size_image_mask(void)
{
    GwyDict *dict = gwy_dict_new();

    // Add also some good.
    gwy_dict_pass_object_by_name(dict, "/image/5", gwy_field_new(5, 3, 1.0, 1.0, TRUE));
    gwy_dict_pass_object_by_name(dict, "/image/5/mask", gwy_nield_new(5, 3));
    gwy_dict_pass_object_by_name(dict, "/image/1", gwy_field_new(4, 2, 1.0, 1.0, TRUE));
    gwy_dict_pass_object_by_name(dict, "/image/2", gwy_field_new(4, 2, 1.0, 1.0, TRUE));
    GwyDict *good_dict = gwy_dict_copy(dict);

    gwy_dict_pass_object_by_name(dict, "/image/1/mask", gwy_nield_new(4, 3));
    gwy_dict_pass_object_by_name(dict, "/image/2/mask", gwy_nield_new(3, 2));

    gsize size;
    gchar *data = make_fake_serialized_file(dict, &size);
    g_assert_finalize_object(dict);

    gsize bytes_consumed;
    GwyErrorList *error_list = NULL, *expected_errors = NULL;
    add_size_mismatch_error(&expected_errors, "/image/1/mask");
    add_size_mismatch_error(&expected_errors, "/image/2/mask");
    GObject *file = gwy_deserialize_memory(data, size, &bytes_consumed, &error_list);

    assert_error_list(error_list, expected_errors, TRUE);

    gwy_error_list_clear(&error_list);
    gwy_error_list_clear(&expected_errors);

    dict_assert_equal(G_OBJECT(file), G_OBJECT(good_dict));

    g_assert_finalize_object(file);
    g_assert_finalize_object(good_dict);
}

void
test_file_validate_size_image_show(void)
{
    GwyDict *dict = gwy_dict_new();

    // Add also some good.
    gwy_dict_pass_object_by_name(dict, "/image/5", gwy_field_new(5, 3, 1.0, 1.0, TRUE));
    gwy_dict_pass_object_by_name(dict, "/image/5/show", gwy_field_new(5, 3, 1.0, 1.0, TRUE));
    gwy_dict_pass_object_by_name(dict, "/image/1", gwy_field_new(4, 2, 1.0, 1.0, TRUE));
    gwy_dict_pass_object_by_name(dict, "/image/2", gwy_field_new(4, 2, 1.0, 1.0, TRUE));
    GwyDict *good_dict = gwy_dict_copy(dict);

    gwy_dict_pass_object_by_name(dict, "/image/1/show", gwy_field_new(3, 2, 1.0, 1.0, TRUE));
    gwy_dict_pass_object_by_name(dict, "/image/2/show", gwy_field_new(4, 3, 1.0, 2.0, TRUE));

    gsize size;
    gchar *data = make_fake_serialized_file(dict, &size);
    g_assert_finalize_object(dict);

    gsize bytes_consumed;
    GwyErrorList *error_list = NULL, *expected_errors = NULL;
    add_size_mismatch_error(&expected_errors, "/image/1/show");
    add_size_mismatch_error(&expected_errors, "/image/2/show");
    GObject *file = gwy_deserialize_memory(data, size, &bytes_consumed, &error_list);

    assert_error_list(error_list, expected_errors, TRUE);

    gwy_error_list_clear(&error_list);
    gwy_error_list_clear(&expected_errors);

    dict_assert_equal(G_OBJECT(file), G_OBJECT(good_dict));

    g_assert_finalize_object(file);
    g_assert_finalize_object(good_dict);
}

void
test_file_validate_size_volume_preview(void)
{
    GwyDict *dict = gwy_dict_new();

    // Add also some good.
    gwy_dict_pass_object_by_name(dict, "/volume/5", gwy_brick_new(5, 3, 2, 1.0, 1.0, 1.0, TRUE));
    gwy_dict_pass_object_by_name(dict, "/volume/5/show", gwy_field_new(5, 3, 1.0, 1.0, TRUE));
    gwy_dict_pass_object_by_name(dict, "/volume/1", gwy_brick_new(4, 2, 3, 1.0, 1.0, 1.0, TRUE));
    gwy_dict_pass_object_by_name(dict, "/volume/2", gwy_brick_new(4, 2, 3, 1.0, 1.0, 1.0, TRUE));
    GwyDict *good_dict = gwy_dict_copy(dict);

    gwy_dict_pass_object_by_name(dict, "/volume/1/show", gwy_field_new(3, 2, 1.0, 1.0, TRUE));
    gwy_dict_pass_object_by_name(dict, "/volume/2/show", gwy_field_new(4, 2, 1.0, 2.0, TRUE));

    gsize size;
    gchar *data = make_fake_serialized_file(dict, &size);
    g_assert_finalize_object(dict);

    gsize bytes_consumed;
    GwyErrorList *error_list = NULL, *expected_errors = NULL;
    add_size_mismatch_error(&expected_errors, "/volume/1/show");
    add_size_mismatch_error(&expected_errors, "/volume/2/show");
    GObject *file = gwy_deserialize_memory(data, size, &bytes_consumed, &error_list);

    assert_error_list(error_list, expected_errors, TRUE);

    gwy_error_list_clear(&error_list);
    gwy_error_list_clear(&expected_errors);

    dict_assert_equal(G_OBJECT(file), G_OBJECT(good_dict));

    g_assert_finalize_object(file);
    g_assert_finalize_object(good_dict);
}

void
test_file_validate_size_cmap_preview(void)
{
    GwyDict *dict = gwy_dict_new();

    // Add also some good.
    gwy_dict_pass_object_by_name(dict, "/cmap/5", gwy_lawn_new(5, 3, 1.0, 1.0, 1, 0));
    gwy_dict_pass_object_by_name(dict, "/cmap/5/show", gwy_field_new(5, 3, 1.0, 1.0, TRUE));
    gwy_dict_pass_object_by_name(dict, "/cmap/1", gwy_lawn_new(4, 2, 1.0, 1.0, 1, 0));
    gwy_dict_pass_object_by_name(dict, "/cmap/2", gwy_lawn_new(4, 2, 1.0, 1.0, 1, 0));
    GwyDict *good_dict = gwy_dict_copy(dict);

    gwy_dict_pass_object_by_name(dict, "/cmap/1/show", gwy_field_new(3, 2, 1.0, 1.0, TRUE));
    gwy_dict_pass_object_by_name(dict, "/cmap/2/show", gwy_field_new(4, 2, 1.0, 2.0, TRUE));

    gsize size;
    gchar *data = make_fake_serialized_file(dict, &size);
    g_assert_finalize_object(dict);

    gsize bytes_consumed;
    GwyErrorList *error_list = NULL, *expected_errors = NULL;
    add_size_mismatch_error(&expected_errors, "/cmap/1/show");
    add_size_mismatch_error(&expected_errors, "/cmap/2/show");
    GObject *file = gwy_deserialize_memory(data, size, &bytes_consumed, &error_list);

    assert_error_list(error_list, expected_errors, TRUE);

    gwy_error_list_clear(&error_list);
    gwy_error_list_clear(&expected_errors);

    dict_assert_equal(G_OBJECT(file), G_OBJECT(good_dict));

    g_assert_finalize_object(file);
    g_assert_finalize_object(good_dict);
}

static void
add_huge_id_error(GwyErrorList **error_list, const gchar *key)
{
    gwy_error_list_add(error_list, GWY_FILE_ERROR, GWY_FILE_ERROR_HUGE_ID,
                       "Disproportionally huge data id in ‘%s’.", key);
}

void
test_file_validate_huge_id_image(void)
{
    GwyDict *dict = gwy_dict_new();

    // Add also some good.
    gwy_dict_pass_object_by_name(dict, "/image/0", gwy_field_new(2, 2, 1.0, 1.0, TRUE));
    gwy_dict_set_string_by_name(dict, "/image/0/title", "Image 0");
    gwy_dict_set_boolean_by_name(dict, "/image/0/visible", TRUE);
    gwy_dict_pass_object_by_name(dict, "/image/20", gwy_field_new(1, 1, 1.0, 1.0, TRUE));
    gwy_dict_set_string_by_name(dict, "/image/20/title", "Image 20");
    gwy_dict_set_boolean_by_name(dict, "/image/20/visible", FALSE);
    GwyDict *good_dict = gwy_dict_copy(dict);

    gwy_dict_pass_object_by_name(dict, "/image/1234567", gwy_field_new(3, 2, 1.0, 1.0, TRUE));
    gwy_dict_set_string_by_name(dict, "/image/1234567/title", "Image 1234567");
    gwy_dict_set_boolean_by_name(dict, "/image/1234567/visible", TRUE);
    // The image should be moved to the first free id, i.e. to 21.
    gwy_dict_pass_object_by_name(good_dict, "/image/21", gwy_field_new(3, 2, 1.0, 1.0, TRUE));
    gwy_dict_set_string_by_name(good_dict, "/image/21/title", "Image 1234567");
    gwy_dict_set_boolean_by_name(good_dict, "/image/21/visible", TRUE);

    gwy_dict_pass_object_by_name(dict, "/image/12345678", gwy_field_new(3, 4, 1.0, 1.0, TRUE));
    gwy_dict_pass_object_by_name(dict, "/image/12345678/meta", gwy_dict_new());
    gwy_dict_set_enum_by_name(dict, "/image/12345678/color-mapping", GWY_COLOR_MAPPING_FIXED);
    // The image should be moved to the next free id, i.e. to 22.
    gwy_dict_pass_object_by_name(good_dict, "/image/22", gwy_field_new(3, 4, 1.0, 1.0, TRUE));
    gwy_dict_pass_object_by_name(good_dict, "/image/22/meta", gwy_dict_new());
    gwy_dict_set_enum_by_name(good_dict, "/image/22/color-mapping", GWY_COLOR_MAPPING_FIXED);

    gsize size;
    gchar *data = make_fake_serialized_file(dict, &size);
    g_assert_finalize_object(dict);

    gsize bytes_consumed;
    GwyErrorList *error_list = NULL, *expected_errors = NULL;
    add_huge_id_error(&expected_errors, "/image/1234567");
    add_huge_id_error(&expected_errors, "/image/12345678");
    GObject *file = gwy_deserialize_memory(data, size, &bytes_consumed, &error_list);

    assert_error_list(error_list, expected_errors, TRUE);

    gwy_error_list_clear(&error_list);
    gwy_error_list_clear(&expected_errors);

    dict_assert_equal(G_OBJECT(file), G_OBJECT(good_dict));

    g_assert_finalize_object(file);
    g_assert_finalize_object(good_dict);
}

/* vim: set cin columns=120 tw=118 et ts=4 sw=4 cino=>1s,e0,n0,f0,{0,}0,^0,\:1s,=0,g1s,h0,t0,+1s,c3,(0,u0 : */
