/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
/*
 * Copyright (C) 2013 Intel Corporation
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of version 2 of the GNU Lesser General Public
 * License as published by the Free Software Foundation.
 *
 * 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 Lesser 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.
 *
 * Authors: Tristan Van Berkom <tristanvb@openismus.com>
 */

#include <stdlib.h>
#include <locale.h>

#include "client-test-utils.h"
#include "e-test-server-utils.h"

static void setup_custom_book (ESource            *scratch,
			       ETestServerClosure *closure);

static ETestServerClosure book_closure = { E_TEST_SERVER_ADDRESS_BOOK, setup_custom_book, 0 };

static void
setup_custom_book (ESource *scratch,
                   ETestServerClosure *closure)
{
	ESourceRevisionGuards *guards;

	g_type_ensure (E_TYPE_SOURCE_REVISION_GUARDS);
	guards = e_source_get_extension (scratch, E_SOURCE_EXTENSION_REVISION_GUARDS);
	e_source_revision_guards_set_enabled (guards, TRUE);
}

typedef struct {
	EContactField field;
	const gchar  *value;
} TestData;

typedef struct {
	GThread       *thread;
	const gchar   *book_uid;
	const gchar   *contact_uid;
	EContactField  field;
	const gchar   *value;
	EBookClient   *client;
	GMainLoop     *loop;
} ThreadData;

/* Special attention needed for this array:
 *
 * Some contact fields cannot be used together, for instance
 * E_CONTACT_PHONE_OTHER will conflict with E_CONTACT_PHONE_HOME and others,
 * E_CONTACT_EMAIL_[1-4] can get mixed up if not set in proper sequence.
 *
 * For this test case to work properly, all fields must not conflict with eachother.
 */
static const TestData field_tests[] = {
	{ E_CONTACT_GIVEN_NAME,          "Elvis" },
	{ E_CONTACT_FAMILY_NAME,         "Presley" },
	{ E_CONTACT_NICKNAME,            "The King" },
	{ E_CONTACT_EMAIL_1,             "elvis@presley.com" },
	{ E_CONTACT_ADDRESS_LABEL_HOME,  "3764 Elvis Presley Boulevard, Graceland" },
	{ E_CONTACT_ADDRESS_LABEL_WORK,  "Workin on the road again..." },
	{ E_CONTACT_ADDRESS_LABEL_OTHER, "Another address to reach the king" },
	{ E_CONTACT_PHONE_ASSISTANT,     "+1234567890" },
	{ E_CONTACT_PHONE_BUSINESS,      "+99-123-4352-9943" },
	{ E_CONTACT_PHONE_BUSINESS_FAX,  "+44-123456789" },
	{ E_CONTACT_PHONE_CALLBACK,      "+11-222-3333-4444" },
	{ E_CONTACT_PHONE_CAR,           "555-123-4567" },
	{ E_CONTACT_PHONE_COMPANY,       "666-666-6666" },
	{ E_CONTACT_PHONE_HOME,          "333-4444-5678" },
	{ E_CONTACT_PHONE_HOME_FAX,      "+993355556666" },
	{ E_CONTACT_PHONE_ISDN,          "+88-777-6666-5555" },
	{ E_CONTACT_PHONE_MOBILE,        "333-3333" }
};

static gboolean try_write_field_thread_idle (ThreadData *data);

static void
test_write_thread_contact_modified (GObject *source_object,
                                    GAsyncResult *res,
                                    ThreadData *data)
{
	GError   *error = NULL;
	gboolean  retry = FALSE;

	if (!e_book_client_modify_contact_finish (E_BOOK_CLIENT (source_object), res, &error)) {

		/* For bad revision errors, retry the transaction after fetching the
		 * contact again first: The backend is telling us that this commit would have
		 * caused some data loss since we dont have the right contact in the first place.
		 */
		if (g_error_matches (error, E_CLIENT_ERROR,
				     E_CLIENT_ERROR_OUT_OF_SYNC))
			retry = TRUE;
		else
			g_error (
				"Error updating '%s' field: %s\n",
				e_contact_field_name (data->field),
				error->message);

		g_error_free (error);
	}

	if (retry)
		try_write_field_thread_idle (data);
	else
		g_main_loop_quit (data->loop);
}

static void
test_write_thread_contact_fetched (GObject *source_object,
                                   GAsyncResult *res,
                                   ThreadData *data)
{
	EContact *contact = NULL;
	GError   *error = NULL;

	if (!e_book_client_get_contact_finish (E_BOOK_CLIENT (source_object), res, &contact, &error))
		g_error (
			"Failed to fetch contact in thread '%s': %s",
			e_contact_field_name (data->field), error->message);

	e_contact_set (contact, data->field, data->value);

	e_book_client_modify_contact (
		data->client, contact, NULL,
		(GAsyncReadyCallback) test_write_thread_contact_modified, data);

	g_object_unref (contact);
}

static gboolean
try_write_field_thread_idle (ThreadData *data)
{
	e_book_client_get_contact (
		data->client, data->contact_uid, NULL,
		(GAsyncReadyCallback) test_write_thread_contact_fetched, data);

	return FALSE;
}

static void
test_write_thread_client_opened (GObject *source_object,
                                 GAsyncResult *res,
                                 ThreadData *data)
{
	GMainContext *context;
	GSource      *gsource;
	GError       *error = NULL;

	if (!e_client_open_finish (E_CLIENT (source_object), res, &error))
		g_error (
			"Error opening client for thread '%s': %s",
			e_contact_field_name (data->field),
			error->message);

	context = g_main_loop_get_context (data->loop);
	gsource = g_idle_source_new ();
	g_source_set_callback (gsource, (GSourceFunc) try_write_field_thread_idle, data, NULL);
	g_source_attach (gsource, context);
}

static gboolean
test_write_thread_open_idle (ThreadData *data)
{
	/* Open the book client, only if it exists, it should be the same book created by the main thread */
	e_client_open (E_CLIENT (data->client), TRUE, NULL, (GAsyncReadyCallback) test_write_thread_client_opened, data);

	return FALSE;
}

static gpointer
test_write_thread (ThreadData *data)
{
	GMainContext    *context;
	ESourceRegistry *registry;
	GSource         *gsource;
	ESource         *source;
	GError          *error = NULL;

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

	/* Open the test book client in this thread */
	registry = e_source_registry_new_sync (NULL, &error);
	if (!registry)
		g_error ("Unable to create the registry: %s", error->message);

	source = e_source_registry_ref_source (registry, data->book_uid);
	if (!source)
		g_error ("Unable to fetch source uid '%s' from the registry", data->book_uid);

	data->client = e_book_client_new (source, &error);
	if (!data->client)
		g_error ("Unable to create EBookClient for uid '%s': %s", data->book_uid, error->message);

	/* Retry setting the contact field until we succeed setting the field
	 */
	gsource = g_idle_source_new ();
	g_source_set_callback (gsource, (GSourceFunc) test_write_thread_open_idle, data, NULL);
	g_source_attach (gsource, context);
	g_main_loop_run (data->loop);

	g_object_unref (source);
	g_object_unref (registry);

	g_object_unref (data->client);
	g_main_context_pop_thread_default (context);
	g_main_loop_unref (data->loop);
	g_main_context_unref (context);

	return NULL;
}

static ThreadData *
create_test_thread (const gchar *book_uid,
                    const gchar *contact_uid,
                    EContactField field,
                    const gchar *value)
{
	ThreadData  *data = g_slice_new0 (ThreadData);
	const gchar *name = e_contact_field_name (field);

	data->book_uid = book_uid;
	data->contact_uid = contact_uid;
	data->field = field;
	data->value = value;

	data->thread = g_thread_new (name, (GThreadFunc) test_write_thread, data);

	return data;
}

static void
wait_thread_test (ThreadData *data)
{
	g_thread_join (data->thread);
	g_slice_free (ThreadData, data);
}

static void
test_concurrent_writes (ETestServerFixture *fixture,
                       gconstpointer user_data)
{
	EBookClient *main_client;
	ESource *source;
	EContact *contact;
	GError *error = NULL;
	const gchar *book_uid = NULL;
	gchar *contact_uid = NULL;
	ThreadData **tests;
	gint i;

	main_client = E_TEST_SERVER_UTILS_SERVICE (fixture, EBookClient);
	source = e_client_get_source (E_CLIENT (main_client));
	book_uid = e_source_get_uid (source);

	/* Create out test contact */
	if (!add_contact_from_test_case_verify (main_client, "simple-1", &contact))
		g_error ("Failed to add the test contact");

	contact_uid = e_contact_get (contact, E_CONTACT_UID);
	g_object_unref (contact);

	/* Create all concurrent threads accessing the same addressbook */
	tests = g_new0 (ThreadData *, G_N_ELEMENTS (field_tests));
	for (i = 0; i < G_N_ELEMENTS (field_tests); i++)
		tests[i] = create_test_thread (
			book_uid, contact_uid,
			field_tests[i].field,
			field_tests[i].value);

	/* Wait for all threads to complete */
	for (i = 0; i < G_N_ELEMENTS (field_tests); i++)
		wait_thread_test (tests[i]);

	/* Fetch the updated contact */
	if (!e_book_client_get_contact_sync (main_client, contact_uid, &contact, NULL, &error))
		g_error ("Failed to fetch test contact after updates: %s", error->message);

	/* Ensure that every value written to the contact concurrently was actually updated in
	 * the final contact
	 */
	for (i = 0; i < G_N_ELEMENTS (field_tests); i++) {
		gchar *value = e_contact_get (contact, field_tests[i].field);

		if (g_strcmp0 (field_tests[i].value, value) != 0) {
			gchar *vcard;

			vcard = e_vcard_to_string (E_VCARD (contact), EVC_FORMAT_VCARD_30);

			g_error (
				"Lost data in concurrent writes, expected "
				"value for field '%s' was '%s', actual value "
				"is '%s', vcard:\n%s\n",
				e_contact_field_name (field_tests[i].field),
				field_tests[i].value, value, vcard);
		}

		g_free (value);
	}
	g_object_unref (contact);

	g_free (contact_uid);
}

gint
main (gint argc,
      gchar **argv)
{
#if !GLIB_CHECK_VERSION (2, 35, 1)
	g_type_init ();
#endif
	g_test_init (&argc, &argv, NULL);
	g_test_bug_base ("http://bugzilla.gnome.org/");

	setlocale (LC_ALL, "en_US.UTF-8");

	g_test_add (
		"/EBookClient/ConcurrentWrites",
		ETestServerFixture,
		&book_closure,
		e_test_server_utils_setup,
		test_concurrent_writes,
		e_test_server_utils_teardown);

	return e_test_server_utils_run ();
}
