/*
 *  $Id: serialization.c 29697 2026-03-31 16:21:05Z yeti-dn $
 *  Copyright (C) 2009-2025 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 {
    ITEM_FLAG,
    ITEM_DATA,
    ITEM_S,
    ITEM_RAW,
    ITEM_CHILD,
    ITEM_DBL,
    ITEM_FLT,
    ITEM_I16,
    ITEM_I32,
    ITEM_I64,
    ITEM_SS,
    NUM_ITEMS
};

struct _GwySerTestClass {
    GObjectClass parent_class;
};

struct _GwySerTest {
    GObject parent_instance;

    guint len;
    gboolean flag;
    gdouble *data;
    gchar *s;
    guchar raw[4];
    gdouble dbl;
    gfloat flt;
    gint16 i16;
    gint32 i32;
    gint64 i64;
    gchar **strlist;
    GwySerTest *child;

    /* Testing stuff. */
    gint done_called;
    gint add_extra_item;
};

// The remaining members get zero-initialized which saves us from doing it.
static const GwySerializableItem serializable_items[NUM_ITEMS] = {
    { .name = "flag",  .ctype = GWY_SERIALIZABLE_BOOLEAN,      },
    { .name = "data",  .ctype = GWY_SERIALIZABLE_DOUBLE_ARRAY, },
    { .name = "s",     .ctype = GWY_SERIALIZABLE_STRING,       },
    { .name = "raw",   .ctype = GWY_SERIALIZABLE_INT8_ARRAY,   },
    { .name = "child", .ctype = GWY_SERIALIZABLE_OBJECT,       },
    { .name = "dbl",   .ctype = GWY_SERIALIZABLE_DOUBLE,       },
    { .name = "flt",   .ctype = GWY_SERIALIZABLE_FLOAT,        },
    { .name = "i16",   .ctype = GWY_SERIALIZABLE_INT16,        },
    { .name = "i32",   .ctype = GWY_SERIALIZABLE_INT32,        },
    { .name = "i64",   .ctype = GWY_SERIALIZABLE_INT64,        },
    { .name = "ss",    .ctype = GWY_SERIALIZABLE_STRING_ARRAY, },
};

static void             gwy_ser_test_serializable_init     (GwySerializableInterface *iface);
static void             gwy_ser_test_dispose               (GObject *object);
static void             gwy_ser_test_finalize              (GObject *object);
static void             gwy_ser_test_itemize               (GwySerializable *serializable,
                                                            GwySerializableGroup *items);
static void             gwy_ser_test_done                  (GwySerializable *serializable);
static gboolean         gwy_ser_test_construct             (GwySerializable *serializable,
                                                            GwySerializableGroup *items,
                                                            GwyErrorList **error_list);
static GwySerializable* gwy_ser_test_copy_impl             (GwySerializable *serializable);
static void             gwy_ser_test_assign_impl           (GwySerializable *destination,
                                                            GwySerializable *source);
static void             ser_test_assert_equal              (GObject *object,
                                                            GObject *reference);
static void             serialize_object_and_back_with_size(GObject *object,
                                                            CompareObjectDataFunc compare,
                                                            GwySerializeSizeType sizetype,
                                                            GwyErrorList *expected_errors);

G_DEFINE_TYPE_WITH_CODE(GwySerTest, gwy_ser_test, G_TYPE_OBJECT,
                        G_IMPLEMENT_INTERFACE(GWY_TYPE_SERIALIZABLE, gwy_ser_test_serializable_init));

static void
gwy_ser_test_serializable_init(GwySerializableInterface *iface)
{
    iface->itemize   = gwy_ser_test_itemize;
    iface->done      = gwy_ser_test_done;
    iface->construct = gwy_ser_test_construct;
    iface->copy      = gwy_ser_test_copy_impl;
    iface->assign    = gwy_ser_test_assign_impl;
}

static void
gwy_ser_test_class_init(GwySerTestClass *klass)
{
    GObjectClass *g_object_class = G_OBJECT_CLASS(klass);

    g_object_class->dispose = gwy_ser_test_dispose;
    g_object_class->finalize = gwy_ser_test_finalize;
}

static void
gwy_ser_test_init(GwySerTest *sertest)
{
    sertest->dbl = G_LN2;
    sertest->add_extra_item = -1;
}

static void
gwy_ser_test_dispose(GObject *object)
{
    GwySerTest *sertest = GWY_SER_TEST(object);

    g_clear_object(&sertest->child);
    G_OBJECT_CLASS(gwy_ser_test_parent_class)->dispose(object);
}

static void
gwy_ser_test_finalize(GObject *object)
{
    GwySerTest *sertest = GWY_SER_TEST(object);

    GWY_FREE(sertest->data);
    GWY_FREE(sertest->s);
    G_OBJECT_CLASS(gwy_ser_test_parent_class)->finalize(object);
    g_strfreev(sertest->strlist);
}

#define add_item(id) \
    g_return_val_if_fail(items->len - items->n, 0); \
    items->items[items->n++] = it[id]; \
    n_items++

static void
maybe_add_bogus_item(GwySerializableGroup *group, gboolean addit)
{
    if (!addit)
        return;

    GwySerializableItem bogus_item;
    gwy_clear1(bogus_item);
    bogus_item.ctype = GWY_SERIALIZABLE_INT32;
    bogus_item.name = "bogus";
    bogus_item.value.v_int32 = 17;
    gwy_serializable_group_append(group, &bogus_item, 1);
}

static void
gwy_ser_test_itemize(GwySerializable *serializable,
                     GwySerializableGroup *group)
{
    GwySerTest *sertest = GWY_SER_TEST(serializable);

    gwy_serializable_group_alloc_size(group, NUM_ITEMS+1);
    maybe_add_bogus_item(group, sertest->add_extra_item == ITEM_FLAG);
    if (sertest->flag)
        gwy_serializable_group_append_boolean(group, serializable_items + ITEM_FLAG, sertest->flag);

    maybe_add_bogus_item(group, sertest->add_extra_item == ITEM_DATA);
    gwy_serializable_group_append_double_array(group, serializable_items + ITEM_DATA, sertest->data, sertest->len);

    maybe_add_bogus_item(group, sertest->add_extra_item == ITEM_S);
    gwy_serializable_group_append_string(group, serializable_items + ITEM_S, sertest->s);

    maybe_add_bogus_item(group, sertest->add_extra_item == ITEM_RAW);
    gwy_serializable_group_append_int8_array(group, serializable_items + ITEM_RAW, sertest->raw, 4);

    maybe_add_bogus_item(group, sertest->add_extra_item == ITEM_CHILD);
    if (sertest->child)
        gwy_serializable_group_append_object(group, serializable_items + ITEM_CHILD, G_OBJECT(sertest->child));

    maybe_add_bogus_item(group, sertest->add_extra_item == ITEM_DBL);
    gwy_serializable_group_append_double(group, serializable_items + ITEM_DBL, sertest->dbl);

    maybe_add_bogus_item(group, sertest->add_extra_item == ITEM_FLT);
    gwy_serializable_group_append_float(group, serializable_items + ITEM_FLT, sertest->flt);

    maybe_add_bogus_item(group, sertest->add_extra_item == ITEM_I16);
    if (sertest->i16)
        gwy_serializable_group_append_int16(group, serializable_items + ITEM_I16, sertest->i16);

    maybe_add_bogus_item(group, sertest->add_extra_item == ITEM_I32);
    gwy_serializable_group_append_int32(group, serializable_items + ITEM_I32, sertest->i32);

    maybe_add_bogus_item(group, sertest->add_extra_item == ITEM_I64);
    gwy_serializable_group_append_int64(group, serializable_items + ITEM_I64, sertest->i64);

    maybe_add_bogus_item(group, sertest->add_extra_item == ITEM_SS);
    gwy_serializable_group_append_string_array(group, serializable_items + ITEM_SS,
                                               sertest->strlist,
                                               sertest->strlist ? g_strv_length(sertest->strlist) : 0);

    maybe_add_bogus_item(group, sertest->add_extra_item >= NUM_ITEMS);

    gwy_serializable_group_itemize(group);

    sertest->done_called--;
}

static gboolean
gwy_ser_test_construct(GwySerializable *serializable,
                       GwySerializableGroup *group,
                       GwyErrorList **error_list)
{
    GwySerializableItem it[NUM_ITEMS];

    gwy_assign(it, serializable_items, NUM_ITEMS);
    gwy_deserialize_filter_items(it, G_N_ELEMENTS(it), group, "GwySerTest", error_list);
    gchar **ss = it[ITEM_SS].value.v_string_array;

    if (it[ITEM_RAW].array_size != 4) {
        gwy_error_list_add(error_list, GWY_DESERIALIZE_ERROR, GWY_DESERIALIZE_ERROR_INVALID,
                           "Item ‘raw’ has %" G_GSIZE_FORMAT " bytes instead of 4.",
                           it[ITEM_RAW].array_size);
        goto fail;
    }
    if (it[ITEM_CHILD].value.v_object
        && !gwy_check_object_component(it + ITEM_CHILD, "GwySerTest", GWY_TYPE_SER_TEST, error_list))
        goto fail;

    GwySerTest *sertest = GWY_SER_TEST(serializable);

    sertest->flag  = it[ITEM_FLAG].value.v_boolean;
    sertest->len   = it[ITEM_DATA].array_size;
    sertest->data  = it[ITEM_DATA].value.v_double_array;
    sertest->s     = it[ITEM_S].value.v_string;
    sertest->child = (GwySerTest*)it[ITEM_CHILD].value.v_object;
    sertest->dbl   = it[ITEM_DBL].value.v_double;
    sertest->flt   = it[ITEM_FLT].value.v_float;
    sertest->i16   = it[ITEM_I16].value.v_int16;
    sertest->i32   = it[ITEM_I32].value.v_int32;
    sertest->i64   = it[ITEM_I64].value.v_int64;
    gwy_assign(sertest->raw, it[ITEM_RAW].value.v_uint8_array, 4);
    g_free(it[ITEM_RAW].value.v_uint8_array);

    if (ss) {
        guint len = it[ITEM_SS].array_size;
        sertest->strlist = g_new(gchar*, len + 1);
        gwy_assign(sertest->strlist, ss, len);
        sertest->strlist[len] = NULL;
        g_free(ss);
    }

    return TRUE;

fail:
    g_free(it[ITEM_DATA].value.v_double_array);
    g_free(it[ITEM_S].value.v_string);
    g_free(it[ITEM_RAW].value.v_uint8_array);
    g_clear_object(&it[ITEM_CHILD].value.v_object);
    g_free(ss);

    return FALSE;
}

static void
gwy_ser_test_done(GwySerializable *serializable)
{
    GwySerTest *sertest = GWY_SER_TEST(serializable);

    sertest->done_called++;
}

GwySerTest*
gwy_ser_test_new_filled(gboolean flag,
                        const gdouble *data,
                        guint ndata,
                        const gchar *str,
                        guint32 raw)
{
    GwySerTest *sertest = g_object_newv(GWY_TYPE_SER_TEST, 0, NULL);

    sertest->flag = flag;
    sertest->len = ndata;
    if (sertest->len)
        sertest->data = g_memdup(data, ndata*sizeof(gdouble));
    sertest->s = g_strdup(str);
    memcpy(sertest->raw, &raw, 4);

    return sertest;
}

static GwySerializable*
gwy_ser_test_copy_impl(GwySerializable *serializable)
{
    GwySerTest *sertest = GWY_SER_TEST(serializable);
    GwySerTest *copy = gwy_ser_test_new_filled(sertest->flag, sertest->data, sertest->len, sertest->s, 0);
    copy->dbl = sertest->dbl;
    copy->flt = sertest->flt;
    copy->i16 = sertest->i16;
    copy->i32 = sertest->i32;
    copy->i64 = sertest->i64;
    gwy_assign(copy->raw, sertest->raw, 4);
    copy->strlist = g_strdupv(sertest->strlist);
    if (sertest->child)
        copy->child = GWY_SER_TEST(gwy_serializable_copy(GWY_SERIALIZABLE(sertest->child)));

    return GWY_SERIALIZABLE(copy);
}

static void
gwy_ser_test_assign_impl(GwySerializable *destination, GwySerializable *source)
{
    GwySerTest *destser = GWY_SER_TEST(destination), *srcser = GWY_SER_TEST(source);

    gwy_assign_string(&destser->s, srcser->s);
    destser->flag = srcser->flag;
    destser->dbl = srcser->dbl;
    destser->flt = srcser->flt;
    destser->i16 = srcser->i16;
    destser->i32 = srcser->i32;
    destser->i64 = srcser->i64;
    gwy_assign(destser->raw, srcser->raw, 4);
    g_strfreev(destser->strlist);
    destser->strlist = g_strdupv(srcser->strlist);
    destser->len = srcser->len;
    destser->data = g_renew(gdouble, destser->data, destser->len);
    gwy_assign(destser->data, srcser->data, destser->len);
    if (destser->child != srcser->child) {
        if (!srcser->child)
            g_clear_object(&destser->child);
        else if (!destser->child)
            destser->child = GWY_SER_TEST(gwy_serializable_copy(GWY_SERIALIZABLE(srcser->child)));
        else
            gwy_serializable_assign(GWY_SERIALIZABLE(destser->child), GWY_SERIALIZABLE(srcser->child));
    }
}

static void
ser_test_assert_equal(GObject *object, GObject *reference)
{
    g_assert_true(GWY_IS_SER_TEST(object));
    g_assert_true(GWY_IS_SER_TEST(reference));

    GwySerTest *sertest = GWY_SER_TEST(object), *serref = GWY_SER_TEST(reference);
    g_assert_cmpmem(sertest->data, sertest->len, serref->data, serref->len);
    g_assert_true(!sertest->flag == !serref->flag);
    g_assert_cmpstr(sertest->s, ==, serref->s);
    g_assert_cmpfloat(sertest->dbl, ==, serref->dbl);
    g_assert_cmpfloat(sertest->flt, ==, serref->flt);
    g_assert_cmpint(sertest->i16, ==, serref->i16);
    g_assert_cmpint(sertest->i32, ==, serref->i32);
    g_assert_cmpint(sertest->i64, ==, serref->i64);
    g_assert_cmpmem(sertest->raw, 4, serref->raw, 4);
    g_assert_cmpstrv(sertest->strlist, serref->strlist);
    g_assert_true(!sertest->child == !serref->child);
    if (sertest->child)
        ser_test_assert_equal(G_OBJECT(sertest->child), G_OBJECT(serref->child));
}

void
test_serialization_trivial_native(void)
{
    GwySerTest *sertest = g_object_new(GWY_TYPE_SER_TEST, NULL);
    serialize_object_and_back(G_OBJECT(sertest), ser_test_assert_equal, FALSE, NULL);
    g_assert_finalize_object(sertest);
}

void
test_serialization_trivial_32bit(void)
{
    GwySerTest *sertest = g_object_new(GWY_TYPE_SER_TEST, NULL);
    serialize_object_and_back_with_size(G_OBJECT(sertest), ser_test_assert_equal, GWY_SERIALIZE_SIZE_32BIT, NULL);
    g_assert_finalize_object(sertest);
}

void
test_serialization_trivial_64bit(void)
{
    GwySerTest *sertest = g_object_new(GWY_TYPE_SER_TEST, NULL);
    serialize_object_and_back_with_size(G_OBJECT(sertest), ser_test_assert_equal, GWY_SERIALIZE_SIZE_64BIT, NULL);
    g_assert_finalize_object(sertest);
}

static GwySerTest*
make_interesting_ser_test(void)
{
    static const gdouble data[] = { 1.0, G_PI, HUGE_VAL, -0.0 };
    GwySerTest *sertest = gwy_ser_test_new_filled(TRUE, data, G_N_ELEMENTS(data), "Test Test", 0x12345678);
    sertest->i16 = 0x1234;
    sertest->i32 = (gint32)0xdeadbeef;
    sertest->i64 = G_GINT64_CONSTANT(0x1020304050607080);
    sertest->dbl = sin(G_PI/5);
    sertest->flt = cos(G_PI/5);
    sertest->strlist = g_new0(gchar*, 3);
    sertest->strlist[0] = g_strdup("First things first");
    sertest->strlist[1] = g_strdup("Wait a second...");
    return sertest;
}

void
test_serialization_less_trivial_native(void)
{
    GwySerTest *sertest = make_interesting_ser_test();
    serialize_object_and_back(G_OBJECT(sertest), ser_test_assert_equal, FALSE, NULL);
    g_assert_cmpint(sertest->done_called, ==, 0);
    g_assert_finalize_object(sertest);
}

void
test_serialization_less_trivial_32bit(void)
{
    GwySerTest *sertest = make_interesting_ser_test();
    serialize_object_and_back_with_size(G_OBJECT(sertest), ser_test_assert_equal, GWY_SERIALIZE_SIZE_32BIT, NULL);
    g_assert_cmpint(sertest->done_called, ==, 0);
    g_assert_finalize_object(sertest);
}

void
test_serialization_less_trivial_64bit(void)
{
    GwySerTest *sertest = make_interesting_ser_test();
    serialize_object_and_back_with_size(G_OBJECT(sertest), ser_test_assert_equal, GWY_SERIALIZE_SIZE_64BIT, NULL);
    g_assert_cmpint(sertest->done_called, ==, 0);
    g_assert_finalize_object(sertest);
}

static void
test_serialization_nested(guint sizetype)
{
    GwySerTest *sertest = g_object_newv(GWY_TYPE_SER_TEST, 0, NULL);
    GwySerTest *child = sertest->child = g_object_newv(GWY_TYPE_SER_TEST, 0, NULL);
    GwySerTest *grandchild = child->child = g_object_newv(GWY_TYPE_SER_TEST, 0, NULL);
    gsize canary = 0, child_canary = 0, grandchild_canary = 0;
    g_object_weak_ref(G_OBJECT(sertest), record_finalisation, &canary);
    g_object_weak_ref(G_OBJECT(child), record_finalisation, &child_canary);
    g_object_weak_ref(G_OBJECT(grandchild), record_finalisation, &grandchild_canary);
    if (!sizetype)
        serialize_object_and_back(G_OBJECT(sertest), ser_test_assert_equal, FALSE, NULL);
    else
        serialize_object_and_back_with_size(G_OBJECT(sertest), ser_test_assert_equal, sizetype, NULL);
    g_assert_cmpint(sertest->done_called, ==, 0);
    g_assert_finalize_object(sertest);
    g_assert_cmpuint(canary, !=, 0);
    g_assert_cmpuint(child_canary, !=, 0);
    g_assert_cmpuint(grandchild_canary, !=, 0);
}

void
test_serialization_nested_native(void)
{
    test_serialization_nested(0);
}

void
test_serialization_nested_32bit(void)
{
    test_serialization_nested(GWY_SERIALIZE_SIZE_32BIT);
}

void
test_serialization_nested_64bit(void)
{
    test_serialization_nested(GWY_SERIALIZE_SIZE_64BIT);
}

void
test_serialization_extra_item(void)
{
    GwySerTest *sertest = make_interesting_ser_test();
    sertest->add_extra_item = 2;

    GwyErrorList *expected_errors = NULL;
    gwy_error_list_add(&expected_errors, GWY_DESERIALIZE_ERROR, GWY_DESERIALIZE_ERROR_ITEM,
                       "Unexpected item ‘%s’ of type 0x%02x in the representation of object ‘%s’ was ignored.",
                       "bogus", GWY_SERIALIZABLE_INT32, G_OBJECT_TYPE_NAME(sertest));

    serialize_object_and_back(G_OBJECT(sertest), ser_test_assert_equal, FALSE, expected_errors);
    gwy_error_list_clear(&expected_errors);
    g_assert_finalize_object(sertest);
}

/* Serialization to a too short buffer, check failure. */
void
test_serialization_write_error(void)
{
    static const gdouble data[] = { 1.0, G_PI, HUGE_VAL, -0.0 };

    /* Too small buffer (would need 88 bytes with 32bit GWY files). */
    GwySerTest *sertest = gwy_ser_test_new_filled(TRUE, data, G_N_ELEMENTS(data), "Test Test", 0x12345678);
    for (gsize i = 1; i < 87; i++) {
        GOutputStream *stream = g_memory_output_stream_new(malloc(i), i, NULL, &free);
        GError *error = NULL;
        gboolean ok = gwy_serialize_gio(GWY_SERIALIZABLE(sertest), stream, &error);
        g_assert_false(ok);
        g_assert_error(error, G_IO_ERROR, G_IO_ERROR_NO_SPACE);
        g_assert_cmpint(sertest->done_called, ==, 0);
        g_assert_finalize_object(stream);
        g_clear_error(&error);
    }
    g_assert_finalize_object(sertest);
}

/* Randomly perturb serialized object representations above and try to deserialize the result. */
static void
test_serialization_fuzz(GwySerializeSizeType sizetype)
{
    GwySerTest *sertest = make_interesting_ser_test();
    static const gdouble data_child[] = { 3.0, 1e14 };
    sertest->child = gwy_ser_test_new_filled(FALSE, data_child, G_N_ELEMENTS(data_child), "Child", -111);

    GOutputStream *stream = g_memory_output_stream_new_resizable();
    GError *error = NULL;
    gboolean ok;
    if (sizetype) {
        GwySerializableGroup *group = gwy_serializable_itemize(GWY_SERIALIZABLE(sertest));
        ok = gwy_serialize_group_gio(group, sizetype, stream, &error);
        gwy_serializable_group_done(group);
        gwy_serializable_done(GWY_SERIALIZABLE(sertest));
        gwy_serializable_group_free(group, FALSE, NULL);
    }
    else {
        ok = gwy_serialize_gio(GWY_SERIALIZABLE(sertest), stream, &error);
    }
    g_assert_true(ok);
    g_assert_no_error(error);

    gchar *buffer = g_memory_output_stream_get_data(G_MEMORY_OUTPUT_STREAM(stream));
    gsize data_size = g_memory_output_stream_get_data_size(G_MEMORY_OUTPUT_STREAM(stream));
    guchar *fuzz_buffer = g_new(guchar, 2*data_size);
    GRand *rng = g_rand_new_with_seed(42);
    gsize niter = g_test_slow() ? 100000 : 10000;

    for (gsize i = 0; i < niter; i++) {
        gsize pos = 0;
        gsize nchanges = 0;
        for (guint j = 0; j < data_size; j++) {
            guint b = g_rand_int_range(rng, 0, 200);
            nchanges += (b >= 1 && b <= 6);
            if (b == 1)
                continue;
            if (b == 2 && pos) {
                fuzz_buffer[pos] = fuzz_buffer[pos-1];
                pos++;
            }
            fuzz_buffer[pos] = buffer[j];
            if (b == 3)
                fuzz_buffer[pos] = 0xff ^ fuzz_buffer[pos];
            else if (b == 4)
                fuzz_buffer[pos] = 0;
            else if (b == 5)
                fuzz_buffer[pos] = 0xff;
            else if (b == 6 && pos)
                GWY_SWAP(guchar, fuzz_buffer[pos], fuzz_buffer[pos-1]);

            pos++;
        }

        gsize bytes_consumed = 0;
        GwyErrorList *error_list = NULL;
        GObject *object = NULL;
        if (sizetype) {
            GwySerializableGroup *group = gwy_deserialize_group_memory(fuzz_buffer, pos, sizetype, &error_list);
            if (group)
                object = gwy_deserialize_construct(group, &error_list);
        }
        else
            object = gwy_deserialize_memory(fuzz_buffer, pos, &bytes_consumed, &error_list);

        /* We occasionally make valid serialised data by chance. So do not check anything. Just see if we manage not
         * to crash. */
        g_clear_object(&object);
        gwy_error_list_clear(&error_list);
    }

    g_free(fuzz_buffer);
    g_assert_finalize_object(stream);
    g_rand_free(rng);
    g_assert_finalize_object(sertest);
}

void
test_serialization_fuzz_native(void)
{
    test_serialization_fuzz(0);
}

void
test_serialization_fuzz_32bit(void)
{
    test_serialization_fuzz(GWY_SERIALIZE_SIZE_32BIT);
}

void
test_serialization_fuzz_64bit(void)
{
    test_serialization_fuzz(GWY_SERIALIZE_SIZE_64BIT);
}

gboolean
values_are_equal(const GValue *value1,
                 const GValue *value2)
{
    if (!value1 || !value2)
        return FALSE;

    if (!G_IS_VALUE(value1) || !G_IS_VALUE(value2))
        return FALSE;

    GType type = G_VALUE_TYPE(value1);
    if (type != G_VALUE_TYPE(value2))
        return FALSE;

    switch (type) {
        case G_TYPE_BOOLEAN:
        return !g_value_get_boolean(value1) == !g_value_get_boolean(value2);

        case G_TYPE_CHAR:
        return g_value_get_schar(value1) == g_value_get_schar(value2);

        case G_TYPE_UCHAR:
        return g_value_get_uchar(value1) == g_value_get_uchar(value2);

        case G_TYPE_INT:
        return g_value_get_int(value1) == g_value_get_int(value2);

        case G_TYPE_UINT:
        return g_value_get_uint(value1) == g_value_get_uint(value2);

        case G_TYPE_INT64:
        return g_value_get_int64(value1) == g_value_get_int64(value2);

        case G_TYPE_UINT64:
        return g_value_get_uint64(value1) == g_value_get_uint64(value2);

        case G_TYPE_DOUBLE:
        return (fabs(g_value_get_double(value1) - g_value_get_double(value2))
                <= 2e-16*(fabs(g_value_get_double(value1)) + fabs(g_value_get_double(value2))));

        case G_TYPE_STRING:
        return ((!g_value_get_string(value1) && !g_value_get_string(value2))
                || gwy_strequal(g_value_get_string(value1), g_value_get_string(value2)));
    }

    if (g_type_is_a(type, G_TYPE_ENUM))
        return g_value_get_enum(value1) == g_value_get_enum(value2);

    if (g_type_is_a(type, GWY_TYPE_UNIT)) {
        GObject *obj1 = g_value_get_object(value1), *obj2 = g_value_get_object(value2);
        return gwy_unit_equal(GWY_UNIT(obj1), GWY_UNIT(obj2));
    }

    if (g_type_is_a(type, G_TYPE_OBJECT)) {
        GObject *obj1 = g_value_get_object(value1), *obj2 = g_value_get_object(value2);
        if (!obj1 || !obj2)
            return !obj1 && !obj2;
        if (G_OBJECT_TYPE(obj1) != G_OBJECT_TYPE(obj2))
            return FALSE;
        if (GWY_IS_DICT(obj1))
            return compare_dicts(GWY_DICT(obj1), GWY_DICT(obj2));
        return assert_properties_equal(obj1, obj2);
    }

    // FIXME: Are NULLs acceptable also here?
    if (g_type_is_a(type, G_TYPE_BOXED))
        return gwy_serializable_boxed_equal(type, g_value_get_boxed(value1), g_value_get_boxed(value2));

    g_warning("Cannot test values of type %s for equality.  Extend me!\n", g_type_name(type));
    return TRUE;
}

gboolean
assert_properties_equal(GObject *object, GObject *reference)
{
    g_assert_cmpuint(G_OBJECT_TYPE(object), ==, G_OBJECT_TYPE(reference));
    GObjectClass *klass = G_OBJECT_GET_CLASS(reference);
    guint nprops;
    GParamSpec **props = g_object_class_list_properties(klass, &nprops);
    for (guint i = 0; i < nprops; i++) {
        GValue value1, value2;
        //g_printerr("%s\n", props[i]->name);
        gwy_clear1(value1);
        gwy_clear1(value2);
        g_value_init(&value1, props[i]->value_type);
        g_value_init(&value2, props[i]->value_type);
        g_object_get_property(object, props[i]->name, &value1);
        g_object_get_property(reference, props[i]->name, &value2);
        g_assert_true(values_are_equal(&value1, &value2));
        g_value_unset(&value1);
        g_value_unset(&value2);
    }
    g_free(props);
    return TRUE;
}

/* This function can be used to check reporting of non-fatal errors. It cannot be used with fatal errors because it
 * asserts success. */
GObject*
serialize_object_and_back(GObject *object, CompareObjectDataFunc compare,
                          gboolean return_reconstructed,
                          GwyErrorList *expected_errors)
{
    g_assert_true(GWY_IS_SERIALIZABLE(object));
    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(object), stream, &error);
    g_assert_true(ok);
    g_assert_no_error(error);
    gsize datalen = g_memory_output_stream_get_data_size(memstream);
    gpointer data = g_memory_output_stream_get_data(memstream);
    //g_file_set_contents("ser.gwy", data, datalen, NULL);

    gsize bytes_consumed = 0;
    GwyErrorList *error_list = NULL;
    GObject *retval = gwy_deserialize_memory(data, datalen, &bytes_consumed, &error_list);
    assert_error_list(error_list, expected_errors, FALSE);
    gwy_error_list_clear(&error_list);
    g_assert_nonnull(retval);
    g_assert_cmpuint(G_OBJECT_TYPE(retval), ==, G_OBJECT_TYPE(object));
    g_assert_cmpuint(bytes_consumed, ==, datalen);
    g_assert_finalize_object(stream);
    assert_properties_equal(retval, object);
    if (compare)
        compare(retval, object);
    if (return_reconstructed)
        return retval;
    g_assert_finalize_object(retval);
    return NULL;
}

static void
serialize_object_and_back_with_size(GObject *object, CompareObjectDataFunc compare,
                                    GwySerializeSizeType sizetype,
                                    GwyErrorList *expected_errors)
{
    g_assert_true(GWY_IS_SERIALIZABLE(object));
    GOutputStream *stream = g_memory_output_stream_new_resizable();
    GMemoryOutputStream *memstream = G_MEMORY_OUTPUT_STREAM(stream);
    GError *error = NULL;
    GwySerializableGroup *group = gwy_serializable_itemize(GWY_SERIALIZABLE(object));
    g_assert_nonnull(group);
    gboolean ok = gwy_serialize_group_gio(group, sizetype, stream, &error);
    g_assert_true(ok);
    g_assert_no_error(error);
    gwy_serializable_group_done(group);
    gwy_serializable_done(GWY_SERIALIZABLE(object));
    gwy_serializable_group_free(group, FALSE, NULL);
    gsize datalen = g_memory_output_stream_get_data_size(memstream);
    gpointer data = g_memory_output_stream_get_data(memstream);
    //g_file_set_contents("ser.gwy", data, datalen, NULL);

    //gsize bytes_consumed = 0;
    GwyErrorList *error_list = NULL;
    group = gwy_deserialize_group_memory(data, datalen, sizetype, &error_list);
    GObject *retval = gwy_deserialize_construct(group, &error_list);
    assert_error_list(error_list, expected_errors, FALSE);
    gwy_error_list_clear(&error_list);
    g_assert_nonnull(retval);
    g_assert_cmpuint(G_OBJECT_TYPE(retval), ==, G_OBJECT_TYPE(object));
    // XXX: Size in bytes is now private, so we cannot easily check it.
    //g_assert_cmpuint(bytes_consumed, ==, datalen);
    g_assert_finalize_object(stream);
    assert_properties_equal(retval, object);
    if (compare)
        compare(retval, object);
    g_assert_finalize_object(retval);
}

void
serializable_test_copy(GwySerializable *serializable, CompareObjectDataFunc compare)
{
    g_assert_true(GWY_IS_SERIALIZABLE(serializable));
    GwySerializable *copy = GWY_SERIALIZABLE(gwy_serializable_copy(serializable));
    g_assert_true(GWY_IS_SERIALIZABLE(copy));
    assert_properties_equal(G_OBJECT(copy), G_OBJECT(serializable));
    if (compare)
        compare(G_OBJECT(copy), G_OBJECT(serializable));
    g_assert_finalize_object(copy);
}

void
serializable_test_assign(GwySerializable *serializable, GwySerializable *assign_to, CompareObjectDataFunc compare)
{
    g_assert_true(GWY_IS_SERIALIZABLE(serializable));
    GType type = G_OBJECT_TYPE(serializable);
    g_assert_cmpuint(type, !=, 0);
    GwySerializable *copy = GWY_SERIALIZABLE(assign_to ? assign_to : g_object_new(type, NULL));
    gwy_serializable_assign(copy, serializable);
    assert_properties_equal(G_OBJECT(copy), G_OBJECT(serializable));
    if (compare)
        compare(G_OBJECT(copy), G_OBJECT(serializable));
    if (!assign_to)
        g_assert_finalize_object(copy);
    /* Otherwise the object is owned by the caller. */
}

void
deserialize_assert_failure(GMemoryOutputStream *stream, GwyErrorList *expected_errors)
{
    gpointer data = g_memory_output_stream_get_data(stream);
    gsize datalen = g_memory_output_stream_get_data_size(stream);

    guint namelen = strlen((const gchar*)data) + 1;
    guint64 sz = datalen - namelen - sizeof(guint64);
    sz = GUINT64_TO_LE(sz);
    memcpy((guchar*)data + namelen, &sz, sizeof(guint64));

    GwyErrorList *error_list = NULL;
    GObject *obj = gwy_deserialize_memory((const guchar*)data, datalen, NULL, &error_list);
    g_assert_null(obj);
    assert_error_list(error_list, expected_errors, FALSE);
    gwy_error_list_clear(&error_list);
}

/* 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 : */
