/*
 * Copyright (C) 2022-2025 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 library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * This library 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;
	GDateTime *v_date_time;
	double v_double;
	GConverterResult v_enum;
	GConverterFlags v_flags;
	int v_integer;
	char *v_string;
};

enum {
	PROP_0,
	PROP_BOOLEAN,
	PROP_DATE_TIME,
	PROP_DOUBLE,
	PROP_ENUM,
	PROP_FLAGS,
	PROP_INTEGER,
	PROP_STRING,
	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_date_time, g_date_time_unref);
	g_clear_pointer(&test_object->v_string, g_free);

	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_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_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_STRING:
		g_value_set_string(value, seagull_obj->v_string);
		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_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_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_STRING:
		g_clear_pointer(&test_obj->v_string, g_free);
		test_obj->v_string = g_value_dup_string(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_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);

	/* 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_STRING] = g_param_spec_string(
		"vstring", NULL, NULL,
		NULL,
		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;
	GDateTime *dt = NULL;

	dt = g_date_time_new_from_iso8601("2025-02-04T21:54:18Z", NULL);
	ret = g_object_new(test_seagull_object_get_type(),
	                   "vboolean", TRUE,
	                   "vdatetime", dt,
	                   "vdouble", 3.14,
	                   "venum", G_CONVERTER_FINISHED,
	                   "vflags", G_CONVERTER_INPUT_AT_END | G_CONVERTER_FLUSH,
	                   "vinteger", 42,
	                   "vstring", "sammy",
	                   NULL);

	g_date_time_unref(dt);

	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;
	GDateTime *dt = NULL;

	dt = g_date_time_new_from_iso8601("2025-06-27T02:22:19Z", NULL);
	ret = g_object_new(test_seagull_object_get_type(),
	                   "vboolean", FALSE,
	                   "vdatetime", dt,
	                   "vdouble", 6.28,
	                   "venum", G_CONVERTER_FLUSHED,
	                   "vflags", G_CONVERTER_FLUSH,
	                   "vinteger", 24,
	                   "vstring", "ymmas",
	                   NULL);

	g_date_time_unref(dt);

	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, vdatetime TEXT, vdouble DOUBLE, "
		"                 venum INTEGER, vflags INTEGER, vinteger INTEGER, "
		"                 vstring TEXT);",
		NULL
	};
	const char *query = "INSERT INTO obj(vboolean, vdatetime, vdouble, venum, "
	                    "vflags, vinteger, vstring) "
	                    "VALUES(:vboolean, :vdatetime, @vdouble, $venum, "
	                    ":vflags, $vinteger, :vstring)";
	const char *expected = "INSERT INTO obj(vboolean, vdatetime, vdouble, "
	                       "venum, vflags, vinteger, vstring) "
	                       "VALUES(1, '2025-02-04T21:54:18Z', 3.14, 2, 3, "
	                       "42, 'sammy')";

	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, vdatetime TEXT, vdouble DOUBLE, "
		"                 venum INTEGER, vflags INTEGER, vinteger INTEGER, "
		"                 vstring TEXT);",
		NULL
	};
	const char *query = "INSERT INTO obj(vboolean, vdatetime, vdouble, venum, "
	                    "vflags, vinteger, vstring) "
	                    "VALUES(:prefix_vboolean, $prefix_vdatetime, "
	                    "@prefix_vdouble, @prefix_venum, :prefix_vflags, "
	                    "$prefix_vinteger, :prefix_vstring)";
	const char *expected = "INSERT INTO obj(vboolean, vdatetime, vdouble, "
	                       "venum, vflags, vinteger, vstring) "
	                       "VALUES(1, '2025-02-04T21:54:18Z', 3.14, 2, 3, 42, "
	                       "'sammy')";

	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;
	gboolean result = FALSE;
	const char *migrations[] = {
		"CREATE TABLE obj(vboolean INTEGER, vdatetime TEXT, vdouble DOUBLE, "
		"                 venum INTEGER, vflags INTEGER, vinteger INTEGER, "
		"                 vstring TEXT);"
		"INSERT INTO obj(vboolean, vdatetime, vdouble, venum, vflags, "
		"                vinteger, vstring) "
		"            VALUES (1, '2025-02-04T21:54:18Z', 3.14, 2, 3, 42, "
		"                    'sammy');",
		NULL
	};
	const char *query = "SELECT vboolean, vdatetime, vdouble, venum, vflags, "
	                    "vinteger, vstring 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);

	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_cmpstr(obj->v_string, ==, "sammy");

	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;
	gboolean result = FALSE;
	const char *migrations[] = {
		"CREATE TABLE obj(vboolean INTEGER, vdatetime TEXT, vdouble DOUBLE, "
		"                 venum INTEGER, vflags INTEGER, vinteger INTEGER, "
		"                 vstring TEXT);"
		"INSERT INTO obj(vboolean, vdatetime, vdouble, venum, vflags, "
		"                vinteger, vstring) "
		"            VALUES (1, '2025-02-04T21:54:18Z', 3.14, 2, 3, 42, "
		"                    'sammy');",
		NULL
	};
	const char *query = "SELECT "
	                    "vboolean AS prefix_vboolean, "
	                    "vdatetime AS prefix_vdatetime, "
	                    "vdouble AS prefix_vdouble, "
	                    "venum AS prefix_venum, "
	                    "vflags AS prefix_vflags, "
	                    "vinteger AS prefix_vinteger, "
	                    "vstring AS prefix_vstring "
	                    "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);

	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_cmpstr(obj->v_string, ==, "sammy");

	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();
}
