/*
 * Seagull: A SQLite helper library
 * Copyright (C) Seagull Developers
 *
 * Seagull is the legal property of its developers, whose names are too
 * numerous to list here. Please refer to the AUTHORS file distributed with
 * this source distribution
 *
 * This program is free software; you can redistribute it and/or modify it
 * under the terms of the GNU Lesser General Public License as published by the
 * Free Software Foundation; either version 2.1 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 Lesser General Public License
 * for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with this library; if not, see <https://www.gnu.org/licenses/>.
 */

#include <sqlite3.h>

#include <seagull.h>

/******************************************************************************
 * Test Object
 *****************************************************************************/
G_DECLARE_FINAL_TYPE(TestSeagullObject, test_seagull_object, TEST_SEAGULL,
                     OBJECT, GObject)

struct _TestSeagullObject {
	GObject parent;

	gboolean v_boolean;
	GBytes *v_bytes;
	GDateTime *v_date_time;
	double v_double;
	GConverterResult v_enum;
	GError *v_error;
	GConverterFlags v_flags;
	int v_integer;
	gint64 v_integer64;
	char *v_string;
	GTimeZone *v_time_zone;
	guint v_unsigned_integer;
	guint64 v_unsigned_integer64;
};

enum {
	PROP_0,
	PROP_BOOLEAN,
	PROP_BYTES,
	PROP_DATE_TIME,
	PROP_DOUBLE,
	PROP_ENUM,
	PROP_ERROR,
	PROP_FLAGS,
	PROP_INTEGER,
	PROP_INTEGER64,
	PROP_STRING,
	PROP_TIME_ZONE,
	PROP_UNSIGNED_INTEGER,
	PROP_UNSIGNED_INTEGER64,
	N_PROPERTIES,
};
static GParamSpec *properties[N_PROPERTIES] = {NULL, };

G_DEFINE_FINAL_TYPE(TestSeagullObject, test_seagull_object, G_TYPE_OBJECT)

static void
test_seagull_object_finalize(GObject *obj) {
	TestSeagullObject *test_object = TEST_SEAGULL_OBJECT(obj);

	g_clear_pointer(&test_object->v_bytes, g_bytes_unref);
	g_clear_pointer(&test_object->v_date_time, g_date_time_unref);
	g_clear_error(&test_object->v_error);
	g_clear_pointer(&test_object->v_string, g_free);
	g_clear_pointer(&test_object->v_time_zone, g_time_zone_unref);

	G_OBJECT_CLASS(test_seagull_object_parent_class)->finalize(obj);
}

static void
test_seagull_object_get_property(GObject *obj, guint param_id, GValue *value,
                                 GParamSpec *pspec)
{
	TestSeagullObject *seagull_obj = TEST_SEAGULL_OBJECT(obj);

	switch(param_id) {
	case PROP_BOOLEAN:
		g_value_set_boolean(value, seagull_obj->v_boolean);
		break;
	case PROP_BYTES:
		g_value_set_boxed(value, seagull_obj->v_bytes);
		break;
	case PROP_DATE_TIME:
		g_value_set_boxed(value, seagull_obj->v_date_time);
		break;
	case PROP_DOUBLE:
		g_value_set_double(value, seagull_obj->v_double);
		break;
	case PROP_ENUM:
		g_value_set_enum(value, seagull_obj->v_enum);
		break;
	case PROP_ERROR:
		g_value_set_boxed(value, seagull_obj->v_error);
		break;
	case PROP_FLAGS:
		g_value_set_flags(value, seagull_obj->v_flags);
		break;
	case PROP_INTEGER:
		g_value_set_int(value, seagull_obj->v_integer);
		break;
	case PROP_INTEGER64:
		g_value_set_int64(value, seagull_obj->v_integer64);
		break;
	case PROP_STRING:
		g_value_set_string(value, seagull_obj->v_string);
		break;
	case PROP_TIME_ZONE:
		g_value_set_boxed(value, seagull_obj->v_time_zone);
		break;
	case PROP_UNSIGNED_INTEGER:
		g_value_set_uint(value, seagull_obj->v_unsigned_integer);
		break;
	case PROP_UNSIGNED_INTEGER64:
		g_value_set_uint64(value, seagull_obj->v_unsigned_integer64);
		break;
	default:
		G_OBJECT_WARN_INVALID_PROPERTY_ID(obj, param_id, pspec);
		break;
	}
}

static void
test_seagull_object_set_property(GObject *obj, guint param_id,
                                 const GValue *value, GParamSpec *pspec)
{
	TestSeagullObject *test_obj = TEST_SEAGULL_OBJECT(obj);

	switch(param_id) {
	case PROP_BOOLEAN:
		test_obj->v_boolean = g_value_get_boolean(value);
		break;
	case PROP_BYTES:
		g_clear_pointer(&test_obj->v_bytes, g_bytes_unref);
		test_obj->v_bytes = g_value_dup_boxed(value);
		break;
	case PROP_DATE_TIME:
		g_clear_pointer(&test_obj->v_date_time, g_date_time_unref);
		test_obj->v_date_time = g_value_dup_boxed(value);
		break;
	case PROP_DOUBLE:
		test_obj->v_double = g_value_get_double(value);
		break;
	case PROP_ENUM:
		test_obj->v_enum = g_value_get_enum(value);
		break;
	case PROP_ERROR:
		g_clear_error(&test_obj->v_error);
		test_obj->v_error = g_value_dup_boxed(value);
		break;
	case PROP_FLAGS:
		test_obj->v_flags = g_value_get_flags(value);
		break;
	case PROP_INTEGER:
		test_obj->v_integer = g_value_get_int(value);
		break;
	case PROP_INTEGER64:
		test_obj->v_integer64 = g_value_get_int64(value);
		break;
	case PROP_STRING:
		g_clear_pointer(&test_obj->v_string, g_free);
		test_obj->v_string = g_value_dup_string(value);
		break;
	case PROP_TIME_ZONE:
		g_clear_pointer(&test_obj->v_time_zone, g_time_zone_unref);
		test_obj->v_time_zone = g_value_dup_boxed(value);
		break;
	case PROP_UNSIGNED_INTEGER:
		test_obj->v_unsigned_integer = g_value_get_uint(value);
		break;
	case PROP_UNSIGNED_INTEGER64:
		test_obj->v_unsigned_integer64 = g_value_get_uint64(value);
		break;
	default:
		G_OBJECT_WARN_INVALID_PROPERTY_ID(obj, param_id, pspec);
		break;
	}
}

static void
test_seagull_object_init(G_GNUC_UNUSED TestSeagullObject *obj) {
}

static void
test_seagull_object_class_init(TestSeagullObjectClass *klass) {
	GObjectClass *obj_class = G_OBJECT_CLASS(klass);

	obj_class->finalize = test_seagull_object_finalize;
	obj_class->get_property = test_seagull_object_get_property;
	obj_class->set_property = test_seagull_object_set_property;

	properties[PROP_BOOLEAN] = g_param_spec_boolean(
		"vboolean", NULL, NULL,
		FALSE,
		G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);

	properties[PROP_BYTES] = g_param_spec_boxed(
		"vbytes", NULL, NULL,
		G_TYPE_BYTES,
		G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);

	properties[PROP_DATE_TIME] = g_param_spec_boxed(
		"vdatetime", NULL, NULL,
		G_TYPE_DATE_TIME,
		G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);

	properties[PROP_DOUBLE] = g_param_spec_double(
		"vdouble", NULL, NULL,
		0.0, 100.0, 0.0,
		G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);

	/* We use GConverterResult because it exists and we don't have to define
	 * the enum in the type system.
	 */
	properties[PROP_ENUM] = g_param_spec_enum(
		"venum", NULL, NULL,
		G_TYPE_CONVERTER_RESULT, G_CONVERTER_ERROR,
		G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);

	properties[PROP_ERROR] = g_param_spec_boxed(
		"verror", NULL, NULL,
		G_TYPE_ERROR,
		G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);

	/* We use GConverterFlags because it exists and we don't have to define the
	 * flags in the type system.
	 */
	properties[PROP_FLAGS] = g_param_spec_flags(
		"vflags", NULL, NULL,
		G_TYPE_CONVERTER_FLAGS, G_CONVERTER_NO_FLAGS,
		G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);

	properties[PROP_INTEGER] = g_param_spec_int(
		"vinteger", NULL, NULL,
		0, 100, 0,
		G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);

	properties[PROP_INTEGER64] = g_param_spec_int64(
		"vinteger64", NULL, NULL,
		G_MININT64, G_MAXINT64, 0,
		G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);

	properties[PROP_STRING] = g_param_spec_string(
		"vstring", NULL, NULL,
		NULL,
		G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);

	properties[PROP_TIME_ZONE] = g_param_spec_boxed(
		"vtimezone", NULL, NULL,
		G_TYPE_TIME_ZONE,
		G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);

	properties[PROP_UNSIGNED_INTEGER] = g_param_spec_uint(
		"vunsignedinteger", NULL, NULL,
		0, G_MAXUINT, 0,
		G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);

	properties[PROP_UNSIGNED_INTEGER64] = g_param_spec_uint64(
		"vunsignedinteger64", NULL, NULL,
		0, G_MAXUINT64, 0,
		G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);

	g_object_class_install_properties(obj_class, N_PROPERTIES, properties);
}

static TestSeagullObject *
test_seagull_object_new(void) {
	TestSeagullObject *ret = NULL;
	GBytes *bytes = NULL;
	GDateTime *dt = NULL;
	GError *error = NULL;
	GTimeZone *tz = NULL;

	bytes = g_bytes_new("seagull", 7);
	dt = g_date_time_new_from_iso8601("2025-02-04T21:54:18Z", NULL);
	error = g_error_new(SEAGULL_STATEMENT_ERROR, -1337, "test value");
	tz = g_time_zone_new_utc();
	ret = g_object_new(test_seagull_object_get_type(),
	                   "vboolean", TRUE,
	                   "vbytes", bytes,
	                   "vdatetime", dt,
	                   "vdouble", 3.14,
	                   "venum", G_CONVERTER_FINISHED,
	                   "verror", error,
	                   "vflags", G_CONVERTER_INPUT_AT_END | G_CONVERTER_FLUSH,
	                   "vinteger", 42,
	                   "vinteger64", G_GINT64_CONSTANT(0x1cedc0ffee),
	                   "vstring", "sammy",
	                   "vtimezone", tz,
	                   "vunsignedinteger", 0x80000000,
	                   "vunsignedinteger64", 0x8000000000000000,
	                   NULL);

	g_bytes_unref(bytes);
	g_date_time_unref(dt);
	g_clear_error(&error);
	g_time_zone_unref(tz);

	return ret;
}

static TestSeagullObject *
test_seagull_object_new_empty(void) {
	return g_object_new(test_seagull_object_get_type(), NULL);
}

static TestSeagullObject *
test_seagull_object_new_other(void) {
	TestSeagullObject *ret = NULL;
	GBytes *bytes = NULL;
	GDateTime *dt = NULL;
	GError *error = NULL;
	GTimeZone *tz = NULL;

	bytes = g_bytes_new("landgull", 8);
	dt = g_date_time_new_from_iso8601("2025-06-27T02:22:19Z", NULL);
	error = g_error_new(SEAGULL_STATEMENT_ERROR, 42, "other value");
	tz = g_time_zone_new_identifier("-0500");
	ret = g_object_new(test_seagull_object_get_type(),
	                   "vboolean", FALSE,
	                   "vbytes", bytes,
	                   "vdatetime", dt,
	                   "vdouble", 6.28,
	                   "venum", G_CONVERTER_FLUSHED,
	                   "verror", error,
	                   "vflags", G_CONVERTER_FLUSH,
	                   "vinteger", 24,
	                   "vinteger64", 0x1337,
	                   "vstring", "ymmas",
	                   "vtimezone", tz,
	                   "vunsignedinteger", 0x7f000000,
	                   "vunsignedinteger64", 0x7f00000000000000,
	                   NULL);

	g_bytes_unref(bytes);
	g_date_time_unref(dt);
	g_clear_error(&error);
	g_time_zone_unref(tz);

	return ret;
}

/******************************************************************************
 * Binds Tests
 *****************************************************************************/
static void
test_seagull_statement_bind_object_no_matches(void) {
	TestSeagullObject *obj = NULL;
	SeagullStatement *statement = NULL;
	SeagullSqlite3 *db = NULL;
	GError *error = NULL;
	gboolean result = FALSE;
	char *expanded = NULL;

	obj = test_seagull_object_new();

	db = seagull_db_new_in_memory(&error);
	g_assert_no_error(error);
	g_assert_nonnull(db);

	statement = seagull_statement_new(db, "SELECT 1", &error);
	g_assert_no_error(error);
	g_assert_true(SEAGULL_IS_STATEMENT(statement));

	result = seagull_statement_bind_object(statement, NULL, G_OBJECT(obj),
	                                       &error);
	g_assert_no_error(error);
	g_assert_true(result);

	expanded = seagull_statement_get_expanded_sql(statement);
	g_assert_cmpstr(expanded, ==, "SELECT 1");
	seagull_free(expanded);

	g_assert_finalize_object(statement);

	result = seagull_db_close(db, &error);
	g_assert_no_error(error);
	g_assert_true(result);

	g_assert_finalize_object(obj);
}

static void
test_seagull_statement_bind_object_no_prefix(void) {
	TestSeagullObject *obj = NULL;
	SeagullStatement *statement = NULL;
	SeagullSqlite3 *db = NULL;
	GError *error = NULL;
	gboolean result = FALSE;
	char *expanded = NULL;
	const char *migrations[] = {
		"CREATE TABLE obj(vboolean INTEGER, vbytes BLOB, vdatetime TEXT, "
		"                 vdouble DOUBLE, venum INTEGER, verror BYTES, "
		"                 vflags INTEGER, vinteger INTEGER, "
		"                 vinteger64 INTEGER, vstring TEXT, vtimezone TEXT, "
		"                 vunsignedinteger INTEGER, "
		"                 vunsignedinteger64 INTEGER);",
		NULL
	};
	const char *query = "INSERT INTO obj(vboolean, vbytes, vdatetime, "
	                    "vdouble, venum, verror, vflags, vinteger, "
	                    "vinteger64, vstring, vtimezone, vunsignedinteger, "
	                    "vunsignedinteger64) "
	                    "VALUES(:vboolean, :vbytes, :vdatetime, @vdouble, "
	                    "$venum, @verror, :vflags, $vinteger, @vinteger64, "
	                    ":vstring, @vtimezone, :vunsignedinteger, "
	                    ":vunsignedinteger64)";
	const char *expected = "INSERT INTO obj(vboolean, vbytes, vdatetime, "
	                       "vdouble, venum, verror, vflags, vinteger, "
	                       "vinteger64, vstring, vtimezone, vunsignedinteger, "
	                       "vunsignedinteger64) "
	                       "VALUES(1, x'73656167756c6c', "
	                       "'2025-02-04T21:54:18Z', 3.14, 2, "
	                       "x'73656167756c6c2d73746174656d656e742d6572726f7200c7faffff746573742076616c75650018', "
	                       "3, 42, 124247932910, 'sammy', 'UTC', -2147483648, "
	                       "-9223372036854775808)";

	obj = test_seagull_object_new();

	db = seagull_db_new_in_memory(&error);
	g_assert_no_error(error);
	g_assert_nonnull(db);

	result = seagull_migrations_run_from_strings(db, migrations, &error);
	g_assert_no_error(error);
	g_assert_true(result);

	statement = seagull_statement_new(db, query, &error);
	g_assert_no_error(error);
	g_assert_true(SEAGULL_IS_STATEMENT(statement));

	result = seagull_statement_bind_object(statement, NULL, G_OBJECT(obj),
	                                       &error);
	g_assert_no_error(error);
	g_assert_true(result);

	expanded = seagull_statement_get_expanded_sql(statement);
	g_assert_cmpstr(expanded, ==, expected);
	seagull_free(expanded);

	g_assert_finalize_object(statement);

	result = seagull_db_close(db, &error);
	g_assert_no_error(error);
	g_assert_true(result);

	g_assert_finalize_object(obj);
}

static void
test_seagull_statement_bind_object_with_prefix(void) {
	TestSeagullObject *obj = NULL;
	SeagullStatement *statement = NULL;
	SeagullSqlite3 *db = NULL;
	GError *error = NULL;
	gboolean result = FALSE;
	char *expanded = NULL;
	const char *migrations[] = {
		"CREATE TABLE obj(vboolean INTEGER, vbytes BLOB, vdatetime TEXT, "
		"                 vdouble DOUBLE, venum INTEGER, verror BYTES, "
		"                 vflags INTEGER, vinteger INTEGER, "
		"                 vinteger64 INTEGER, vstring TEXT, vtimezone TEXT, "
		"                 vunsignedinteger INTEGER, "
		"                 vunsignedinteger64 INTEGER);",
		NULL
	};
	const char *query = "INSERT INTO obj(vboolean, vbytes, vdatetime, "
	                    "vdouble, venum, verror, vflags, vinteger, "
	                    "vinteger64, vstring, vtimezone, vunsignedinteger, "
	                    "vunsignedinteger64) "
	                    "VALUES(:prefix_vboolean, :prefix_vbytes, "
	                    "$prefix_vdatetime, @prefix_vdouble, @prefix_venum, "
	                    "$prefix_verror, :prefix_vflags, $prefix_vinteger, "
	                    "@prefix_vinteger64, :prefix_vstring, "
	                    "@prefix_vtimezone, :prefix_vunsignedinteger, "
	                    ":prefix_vunsignedinteger64)";
	const char *expected = "INSERT INTO obj(vboolean, vbytes, vdatetime, "
	                       "vdouble, venum, verror, vflags, vinteger, "
	                       "vinteger64, vstring, vtimezone, vunsignedinteger, "
	                       "vunsignedinteger64) "
	                       "VALUES(1, x'73656167756c6c', "
	                       "'2025-02-04T21:54:18Z', 3.14, 2, "
	                       "x'73656167756c6c2d73746174656d656e742d6572726f7200c7faffff746573742076616c75650018', "
	                       "3, 42, 124247932910, 'sammy', 'UTC', -2147483648, "
	                       "-9223372036854775808)";

	obj = test_seagull_object_new();

	db = seagull_db_new_in_memory(&error);
	g_assert_no_error(error);
	g_assert_nonnull(db);

	result = seagull_migrations_run_from_strings(db, migrations, &error);
	g_assert_no_error(error);
	g_assert_true(result);

	statement = seagull_statement_new(db, query, &error);
	g_assert_no_error(error);
	g_assert_true(SEAGULL_IS_STATEMENT(statement));

	result = seagull_statement_bind_object(statement, "prefix_", G_OBJECT(obj),
	                                       &error);
	g_assert_no_error(error);
	g_assert_true(result);

	expanded = seagull_statement_get_expanded_sql(statement);
	g_assert_cmpstr(expanded, ==, expected);
	seagull_free(expanded);

	g_assert_finalize_object(statement);

	result = seagull_db_close(db, &error);
	g_assert_no_error(error);
	g_assert_true(result);

	g_assert_finalize_object(obj);
}

static void
test_seagull_statement_bind_object_multiple(void) {
	TestSeagullObject *obj1 = NULL;
	TestSeagullObject *obj2 = NULL;
	SeagullStatement *statement = NULL;
	SeagullSqlite3 *db = NULL;
	GError *error = NULL;
	gboolean result = FALSE;
	char *expanded = NULL;
	const char *migrations[] = {
		"CREATE TABLE obj(vboolean INTEGER, vdouble DOUBLE, vinteger INTEGER, "
		"                 vstring TEXT);",
		NULL
	};
	const char *query = "INSERT INTO obj(vboolean, vdouble, vinteger, vstring) "
	                    "VALUES(:obj1_vboolean, @obj1_vdouble, "
	                    "$obj2_vinteger, :obj2_vstring)";
	const char *expected = "INSERT INTO obj(vboolean, vdouble, vinteger, "
	                        "vstring) VALUES(1, 3.14, 24, 'ymmas')";

	obj1 = test_seagull_object_new();
	obj2 = test_seagull_object_new_other();

	db = seagull_db_new_in_memory(&error);
	g_assert_no_error(error);
	g_assert_nonnull(db);

	result = seagull_migrations_run_from_strings(db, migrations, &error);
	g_assert_no_error(error);
	g_assert_true(result);

	statement = seagull_statement_new(db, query, &error);
	g_assert_no_error(error);
	g_assert_true(SEAGULL_IS_STATEMENT(statement));

	result = seagull_statement_bind_object(statement, "obj1_", G_OBJECT(obj1),
	                                       &error);
	g_assert_no_error(error);
	g_assert_true(result);

	result = seagull_statement_bind_object(statement, "obj2_", G_OBJECT(obj2),
	                                       &error);
	g_assert_no_error(error);
	g_assert_true(result);

	expanded = seagull_statement_get_expanded_sql(statement);
	g_assert_cmpstr(expanded, ==, expected);
	seagull_free(expanded);

	g_assert_finalize_object(statement);

	result = seagull_db_close(db, &error);
	g_assert_no_error(error);
	g_assert_true(result);

	g_assert_finalize_object(obj1);
	g_assert_finalize_object(obj2);
}

static void
test_seagull_statement_bind_object_mixed(void) {
	TestSeagullObject *obj = NULL;
	SeagullStatement *statement = NULL;
	SeagullSqlite3 *db = NULL;
	GError *error = NULL;
	gboolean result = FALSE;
	char *expanded = NULL;
	const char *migrations[] = {
		"CREATE TABLE obj(vboolean INTEGER, vdouble DOUBLE, vinteger INTEGER, "
		"                 vstring TEXT);",
		NULL
	};
	const char *query = "INSERT INTO obj(vboolean, vdouble, vinteger, vstring) "
	                    "VALUES(:prefix_vboolean, @prefix_vdouble, "
	                    "$prefix_vinteger, :other_string)";
	const char *expected = "INSERT INTO obj(vboolean, vdouble, vinteger, "
	                        "vstring) VALUES(1, 3.14, 42, 'ymmas')";

	obj = test_seagull_object_new();

	db = seagull_db_new_in_memory(&error);
	g_assert_no_error(error);
	g_assert_nonnull(db);

	result = seagull_migrations_run_from_strings(db, migrations, &error);
	g_assert_no_error(error);
	g_assert_true(result);

	statement = seagull_statement_new(db, query, &error);
	g_assert_no_error(error);
	g_assert_true(SEAGULL_IS_STATEMENT(statement));

	result = seagull_statement_bind_object(statement, "prefix_", G_OBJECT(obj),
	                                       &error);
	g_assert_no_error(error);
	g_assert_true(result);

	result = seagull_statement_bind_text(statement, ":other_string", "ymmas",
	                                     -1, NULL, &error);
	g_assert_no_error(error);
	g_assert_true(result);

	expanded = seagull_statement_get_expanded_sql(statement);
	g_assert_cmpstr(expanded, ==, expected);
	seagull_free(expanded);

	g_assert_finalize_object(statement);

	result = seagull_db_close(db, &error);
	g_assert_no_error(error);
	g_assert_true(result);

	g_assert_finalize_object(obj);
}

static void
test_seagull_bind_object_prefix_must_match(void) {
	TestSeagullObject *obj = NULL;
	SeagullStatement *statement = NULL;
	SeagullSqlite3 *db = NULL;
	GError *error = NULL;
	gboolean result = FALSE;
	char *expanded = NULL;
	const char *migrations[] = {
		"CREATE TABLE obj(vboolean INTEGER, vinteger INTEGER);",
		NULL
	};
	const char *query = "INSERT INTO obj(vboolean, vinteger) "
	                    "VALUES(:prefix_vboolean, @vinteger)";
	const char *expected = "INSERT INTO obj(vboolean, vinteger) "
	                       "VALUES(1, NULL)";

	obj = test_seagull_object_new();

	db = seagull_db_new_in_memory(&error);
	g_assert_no_error(error);
	g_assert_nonnull(db);

	result = seagull_migrations_run_from_strings(db, migrations, &error);
	g_assert_no_error(error);
	g_assert_true(result);

	statement = seagull_statement_new(db, query, &error);
	g_assert_no_error(error);
	g_assert_true(SEAGULL_IS_STATEMENT(statement));

	result = seagull_statement_bind_object(statement, "prefix_", G_OBJECT(obj),
	                                       &error);
	g_assert_no_error(error);
	g_assert_true(result);

	expanded = seagull_statement_get_expanded_sql(statement);
	g_assert_cmpstr(expanded, ==, expected);
	seagull_free(expanded);

	g_assert_finalize_object(statement);

	result = seagull_db_close(db, &error);
	g_assert_no_error(error);
	g_assert_true(result);

	g_assert_finalize_object(obj);
}

/******************************************************************************
 * Column Tests
 *****************************************************************************/
static void
test_seagull_statement_column_object_no_matches(void) {
	TestSeagullObject *obj = NULL;
	SeagullStatement *statement = NULL;
	SeagullSqlite3 *db = NULL;
	GError *error = NULL;
	gboolean result = FALSE;

	obj = test_seagull_object_new_empty();

	db = seagull_db_new_in_memory(&error);
	g_assert_no_error(error);
	g_assert_nonnull(db);

	statement = seagull_statement_new(db, "SELECT 1", &error);
	g_assert_no_error(error);
	g_assert_true(SEAGULL_IS_STATEMENT(statement));

	result = seagull_statement_step(statement, &error);
	g_assert_no_error(error);
	g_assert_true(result);

	result = seagull_statement_column_object(statement, NULL, G_OBJECT(obj),
	                                         &error);
	g_assert_no_error(error);
	g_assert_true(result);

	g_assert_finalize_object(statement);

	result = seagull_db_close(db, &error);
	g_assert_no_error(error);
	g_assert_true(result);

	g_assert_finalize_object(obj);
}

static void
test_seagull_statement_column_object_no_prefix(void) {
	TestSeagullObject *obj = NULL;
	SeagullStatement *statement = NULL;
	SeagullSqlite3 *db = NULL;
	GDateTime *dt = NULL;
	GError *error = NULL;
	GTimeZone *tz = NULL;
	gboolean result = FALSE;
	gconstpointer data = NULL;
	gint64 v_integer64 = G_GINT64_CONSTANT(0x1cedc0ffee);
	gsize data_size = 0;
	const char *migrations[] = {
		"CREATE TABLE obj(vboolean INTEGER, vbytes BLOB, vdatetime TEXT, "
		"                 vdouble DOUBLE, venum INTEGER, verror BYTES, "
		"                 vflags INTEGER, vinteger INTEGER, "
		"                 vinteger64 INTEGER, vstring TEXT, vtimezone TEXT, "
		"                 vunsignedinteger INTEGER, "
		"                 vunsignedinteger64 INTEGER);"
		"INSERT INTO obj(vboolean, vbytes, vdatetime, vdouble, venum, verror, "
		"                vflags, vinteger, vinteger64, vstring, vtimezone, "
		"                vunsignedinteger, vunsignedinteger64) "
		"            VALUES (1, x'73656167756c6c', '2025-02-04T21:54:18Z', "
		"                    3.14, 2, "
		"                    x'73656167756c6c2d73746174656d656e742d6572726f7200c7faffff746573742076616c75650018',"
		"                    3, 42, 124247932910, 'sammy', 'UTC', "
		"                    -2147483648, -9223372036854775808);",
		NULL
	};
	const char *query = "SELECT vboolean, vbytes, vdatetime, vdouble, venum, "
	                    "verror, vflags, vinteger, vinteger64, vstring, "
	                    "vtimezone, vunsignedinteger, vunsignedinteger64 "
	                    "FROM obj";

	obj = test_seagull_object_new_empty();

	db = seagull_db_new_in_memory(&error);
	g_assert_no_error(error);
	g_assert_nonnull(db);

	result = seagull_migrations_run_from_strings(db, migrations, &error);
	g_assert_no_error(error);
	g_assert_true(result);

	statement = seagull_statement_new(db, query, &error);
	g_assert_no_error(error);
	g_assert_true(SEAGULL_IS_STATEMENT(statement));

	result = seagull_statement_step(statement, &error);
	g_assert_no_error(error);
	g_assert_true(result);

	result = seagull_statement_column_object(statement, NULL, G_OBJECT(obj),
	                                         &error);
	g_assert_no_error(error);
	g_assert_true(result);

	g_assert_true(obj->v_boolean);

	g_assert_nonnull(obj->v_bytes);
	data = g_bytes_get_data(obj->v_bytes, &data_size);
	g_assert_cmpmem(data, data_size, "seagull", 7);

	dt = g_date_time_new_from_iso8601("2025-02-04T21:54:18Z", NULL);
	g_assert_nonnull(obj->v_date_time);
	g_assert_true(g_date_time_equal(obj->v_date_time, dt));
	g_clear_pointer(&dt, g_date_time_unref);

	g_assert_error(obj->v_error, SEAGULL_STATEMENT_ERROR, -1337);
	g_assert_cmpstr(obj->v_error->message, ==, "test value");

	g_assert_cmpfloat(obj->v_double, ==, 3.14);
	g_assert_cmpint(obj->v_integer, ==, 42);
	g_assert_true(obj->v_integer64 == v_integer64);
	g_assert_cmpstr(obj->v_string, ==, "sammy");

	tz = g_time_zone_new_utc();
	g_assert_nonnull(obj->v_time_zone);
	g_assert_cmpint(g_time_zone_get_offset(obj->v_time_zone, 0), ==,
	                g_time_zone_get_offset(tz, 0));
	g_clear_pointer(&tz, g_time_zone_unref);

	g_assert_cmpuint(obj->v_unsigned_integer, ==, 0x80000000);
	g_assert_cmpuint(obj->v_unsigned_integer64, ==, 0x8000000000000000);

	result = seagull_statement_step(statement, &error);
	g_assert_no_error(error);
	g_assert_false(result);

	g_assert_finalize_object(statement);

	result = seagull_db_close(db, &error);
	g_assert_no_error(error);
	g_assert_true(result);

	g_assert_finalize_object(obj);
}

static void
test_seagull_statement_column_object_with_prefix(void) {
	TestSeagullObject *obj = NULL;
	SeagullStatement *statement = NULL;
	SeagullSqlite3 *db = NULL;
	GDateTime *dt = NULL;
	GError *error = NULL;
	GTimeZone *tz = NULL;
	gboolean result = FALSE;
	gconstpointer data = NULL;
	gsize data_size = 0;
	const char *migrations[] = {
		"CREATE TABLE obj(vboolean INTEGER, vbytes BLOB, vdatetime TEXT, "
		"                 vdouble DOUBLE, venum INTEGER, vflags INTEGER, "
		"                 vinteger INTEGER, vinteger64 INTEGER, vstring TEXT, "
		"                 vtimezone TEXT, vunsignedinteger INTEGER, "
		"                 vunsignedinteger64 INTEGER);"
		"INSERT INTO obj(vboolean, vbytes, vdatetime, vdouble, venum, vflags, "
		"                vinteger, vinteger64, vstring, vtimezone, "
		"                vunsignedinteger, vunsignedinteger64) "
		"            VALUES (1, x'73656167756c6c', '2025-02-04T21:54:18Z', "
		"                    3.14, 2, 3, 42, 124247932910, 'sammy', 'UTC', "
		"                    -2147483648, -9223372036854775808);",
		NULL
	};
	const char *query = "SELECT "
	                    "vboolean AS prefix_vboolean, "
	                    "vbytes AS prefix_vbytes, "
	                    "vdatetime AS prefix_vdatetime, "
	                    "vdouble AS prefix_vdouble, "
	                    "venum AS prefix_venum, "
	                    "vflags AS prefix_vflags, "
	                    "vinteger AS prefix_vinteger, "
	                    "vinteger64 AS prefix_vinteger64, "
	                    "vstring AS prefix_vstring, "
	                    "vtimezone AS prefix_vtimezone, "
	                    "vunsignedinteger AS prefix_vunsignedinteger, "
	                    "vunsignedinteger64 AS prefix_vunsignedinteger64 "
	                    "FROM obj";

	obj = test_seagull_object_new_empty();

	db = seagull_db_new_in_memory(&error);
	g_assert_no_error(error);
	g_assert_nonnull(db);

	result = seagull_migrations_run_from_strings(db, migrations, &error);
	g_assert_no_error(error);
	g_assert_true(result);

	statement = seagull_statement_new(db, query, &error);
	g_assert_no_error(error);
	g_assert_true(SEAGULL_IS_STATEMENT(statement));

	result = seagull_statement_step(statement, &error);
	g_assert_no_error(error);
	g_assert_true(result);

	result = seagull_statement_column_object(statement, "prefix_",
	                                         G_OBJECT(obj), &error);
	g_assert_no_error(error);
	g_assert_true(result);

	g_assert_true(obj->v_boolean);

	g_assert_nonnull(obj->v_bytes);
	data = g_bytes_get_data(obj->v_bytes, &data_size);
	g_assert_cmpmem(data, data_size, "seagull", 7);

	dt = g_date_time_new_from_iso8601("2025-02-04T21:54:18Z", NULL);
	g_assert_nonnull(obj->v_date_time);
	g_assert_true(g_date_time_equal(obj->v_date_time, dt));
	g_clear_pointer(&dt, g_date_time_unref);

	g_assert_cmpfloat(obj->v_double, ==, 3.14);
	g_assert_cmpint(obj->v_integer, ==, 42);
	g_assert_true(obj->v_integer64 == G_GINT64_CONSTANT(0x1cedc0ffee));
	g_assert_cmpstr(obj->v_string, ==, "sammy");

	tz = g_time_zone_new_utc();
	g_assert_nonnull(obj->v_time_zone);
	g_assert_cmpint(g_time_zone_get_offset(obj->v_time_zone, 0), ==,
	                g_time_zone_get_offset(tz, 0));
	g_clear_pointer(&tz, g_time_zone_unref);

	g_assert_cmpuint(obj->v_unsigned_integer, ==, 0x80000000);
	g_assert_cmpuint(obj->v_unsigned_integer64, ==, 0x8000000000000000);

	result = seagull_statement_step(statement, &error);
	g_assert_no_error(error);
	g_assert_false(result);

	g_assert_finalize_object(statement);

	result = seagull_db_close(db, &error);
	g_assert_no_error(error);
	g_assert_true(result);

	g_assert_finalize_object(obj);
}

static void
test_seagull_statement_column_object_multiple(void) {
	TestSeagullObject *obj1 = NULL;
	TestSeagullObject *obj2 = NULL;
	SeagullStatement *statement = NULL;
	SeagullSqlite3 *db = NULL;
	GError *error = NULL;
	gboolean result = FALSE;
	const char *migrations[] = {
		"CREATE TABLE obj(vboolean INTEGER, vdouble DOUBLE, vinteger INTEGER, "
		"                 vstring TEXT);"
		"INSERT INTO obj(vboolean, vdouble, vinteger, vstring) VALUES "
		"                (1, 3.14, 42, 'sammy');",
		"INSERT INTO obj(vboolean, vdouble, vinteger, vstring) VALUES "
		"                (0, 6.28, 24, 'ymmas');",
		NULL
	};
	const char *query = "SELECT vboolean, vdouble, vinteger, vstring FROM obj";

	obj1 = test_seagull_object_new_empty();
	obj2 = test_seagull_object_new_empty();

	db = seagull_db_new_in_memory(&error);
	g_assert_no_error(error);
	g_assert_nonnull(db);

	result = seagull_migrations_run_from_strings(db, migrations, &error);
	g_assert_no_error(error);
	g_assert_true(result);

	statement = seagull_statement_new(db, query, &error);
	g_assert_no_error(error);
	g_assert_true(SEAGULL_IS_STATEMENT(statement));

	result = seagull_statement_step(statement, &error);
	g_assert_no_error(error);
	g_assert_true(result);

	result = seagull_statement_column_object(statement, NULL, G_OBJECT(obj1),
	                                         &error);
	g_assert_no_error(error);
	g_assert_true(result);

	g_assert_cmpfloat(obj1->v_double, ==, 3.14);
	g_assert_true(obj1->v_boolean);
	g_assert_cmpint(obj1->v_integer, ==, 42);
	g_assert_cmpstr(obj1->v_string, ==, "sammy");

	result = seagull_statement_step(statement, &error);
	g_assert_no_error(error);
	g_assert_true(result);

	result = seagull_statement_column_object(statement, NULL, G_OBJECT(obj2),
	                                         &error);
	g_assert_no_error(error);
	g_assert_true(result);

	g_assert_cmpfloat(obj2->v_double, ==, 6.28);
	g_assert_false(obj2->v_boolean);
	g_assert_cmpint(obj2->v_integer, ==, 24);
	g_assert_cmpstr(obj2->v_string, ==, "ymmas");

	result = seagull_statement_step(statement, &error);
	g_assert_no_error(error);
	g_assert_false(result);

	g_assert_finalize_object(statement);

	result = seagull_db_close(db, &error);
	g_assert_no_error(error);
	g_assert_true(result);

	g_assert_finalize_object(obj1);
	g_assert_finalize_object(obj2);
}

static void
test_seagull_statement_column_object_mixed(void) {
	TestSeagullObject *obj = NULL;
	SeagullStatement *statement = NULL;
	SeagullSqlite3 *db = NULL;
	GError *error = NULL;
	gboolean result = FALSE;
	int other = 0;
	const char *migrations[] = {
		"CREATE TABLE obj(vboolean INTEGER, vdouble DOUBLE, vinteger INTEGER, "
		"                 vstring TEXT, other INTEGER);"
		"INSERT INTO obj(vboolean, vdouble, vinteger, vstring, other) VALUES "
		"                (1, 3.14, 42, 'sammy', 12345);",
		NULL
	};
	const char *query = "SELECT vboolean, vdouble, vinteger, vstring, other "
	                    "FROM obj";

	obj = test_seagull_object_new_empty();

	db = seagull_db_new_in_memory(&error);
	g_assert_no_error(error);
	g_assert_nonnull(db);

	result = seagull_migrations_run_from_strings(db, migrations, &error);
	g_assert_no_error(error);
	g_assert_true(result);

	statement = seagull_statement_new(db, query, &error);
	g_assert_no_error(error);
	g_assert_true(SEAGULL_IS_STATEMENT(statement));

	result = seagull_statement_step(statement, &error);
	g_assert_no_error(error);
	g_assert_true(result);

	result = seagull_statement_column_object(statement, NULL, G_OBJECT(obj),
	                                         &error);
	g_assert_no_error(error);
	g_assert_true(result);

	g_assert_cmpfloat(obj->v_double, ==, 3.14);
	g_assert_true(obj->v_boolean);
	g_assert_cmpint(obj->v_integer, ==, 42);
	g_assert_cmpstr(obj->v_string, ==, "sammy");

	other = seagull_statement_column_int(statement, "other", &error);
	g_assert_no_error(error);
	g_assert_cmpint(other, ==, 12345);

	result = seagull_statement_step(statement, &error);
	g_assert_no_error(error);
	g_assert_false(result);

	g_assert_finalize_object(statement);

	result = seagull_db_close(db, &error);
	g_assert_no_error(error);
	g_assert_true(result);

	g_assert_finalize_object(obj);
}

/******************************************************************************
 * Main
 *****************************************************************************/
int
main(int argc, char *argv[]) {
	g_test_init(&argc, &argv, NULL);

	g_test_add_func("/seagull/statement/bind-object/no-matches",
	                test_seagull_statement_bind_object_no_matches);
	g_test_add_func("/seagull/statement/bind-object/no-prefix",
	                test_seagull_statement_bind_object_no_prefix);
	g_test_add_func("/seagull/statement/bind-object/with-prefix",
	                test_seagull_statement_bind_object_with_prefix);
	g_test_add_func("/seagull/statement/bind-object/multiple",
	                test_seagull_statement_bind_object_multiple);
	g_test_add_func("/seagull/statement/bind-object/mixed",
	                test_seagull_statement_bind_object_mixed);

	g_test_add_func("/seagull/statement/bind-object/prefix-must-match",
	                test_seagull_bind_object_prefix_must_match);

	g_test_add_func("/seagull/statement/column-object/no-matches",
	                test_seagull_statement_column_object_no_matches);
	g_test_add_func("/seagull/statement/column-object/no-prefix",
	                test_seagull_statement_column_object_no_prefix);
	g_test_add_func("/seagull/statement/column-object/with-prefix",
	                test_seagull_statement_column_object_with_prefix);
	g_test_add_func("/seagull/statement/column-object/multiple",
	                test_seagull_statement_column_object_multiple);
	g_test_add_func("/seagull/statement/column-object/mixed",
	                test_seagull_statement_column_object_mixed);

	return g_test_run();
}
