/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*- */

#include "test-utils.h"
#include "soup-session-private.h"

static GUri *base_uri;
static gboolean server_processed_message;
static gboolean timeout;
static GMainLoop *loop;
static SoupMessagePriority expected_priorities[3];
static GBytes *index_bytes;

static gboolean
timeout_cb (gpointer user_data)
{
	gboolean *timeout = user_data;

	*timeout = TRUE;
	return G_SOURCE_REMOVE;
}

static void
server_handler (SoupServer        *server,
		SoupServerMessage *msg,
		const char        *path,
		GHashTable        *query,
		gpointer           user_data)
{
	if (!strcmp (path, "/request-timeout")) {
		GMainContext *context = g_main_context_get_thread_default ();
		GSource *timer;

		timer = g_timeout_source_new (100);
		g_source_set_callback (timer, timeout_cb, &timeout, NULL);
		g_source_attach (timer, context);
		g_source_unref (timer);
	} else if (!strcmp (path, "/index.txt")) {
		soup_server_message_set_status (msg, SOUP_STATUS_OK, NULL);
		soup_server_message_set_response (msg, "text/plain",
						  SOUP_MEMORY_STATIC,
						  g_bytes_get_data (index_bytes, NULL),
						  g_bytes_get_size (index_bytes));
		return;
	} else
		server_processed_message = TRUE;

	soup_server_message_set_status (msg, SOUP_STATUS_OK, NULL);
	soup_server_message_set_response (msg, "text/plain",
					  SOUP_MEMORY_STATIC,
					  "ok\r\n", 4);
}

static void
finished_cb (SoupMessage *msg,
	     gboolean    *finished)
{
	*finished = TRUE;
}

static void
cancel_message_cb (SoupMessage  *msg,
		   GCancellable *cancellable)
{
	g_cancellable_cancel (cancellable);
}

static void
cancel_message_send_done (SoupSession  *session,
			  GAsyncResult *result,
			  GError      **error)
{
	g_assert_null (soup_session_send_finish (session, result, error));
	g_main_loop_quit (loop);
}

static void
do_test_for_session (SoupSession *session,
		     gboolean queue_is_async,
		     gboolean send_is_blocking)
{
	SoupMessage *msg;
	gboolean finished, local_timeout;
	guint timeout_id;
	GUri *timeout_uri;
	GBytes *body;
	GCancellable *cancellable;
	GError *error = NULL;

	debug_printf (1, "  queue_message\n");
	debug_printf (2, "    requesting timeout\n");
	timeout_uri = g_uri_parse_relative (base_uri, "/request-timeout", SOUP_HTTP_URI_FLAGS, NULL);
	msg = soup_message_new_from_uri ("GET", timeout_uri);
	body = soup_session_send_and_read (session, msg, NULL, NULL);
	g_bytes_unref (body);
	g_uri_unref (timeout_uri);

	msg = soup_message_new_from_uri ("GET", base_uri);
	server_processed_message = timeout = finished = FALSE;
	g_signal_connect (msg, "finished",
			  G_CALLBACK (finished_cb), &finished);
	soup_session_send_async (session, msg, G_PRIORITY_DEFAULT, NULL, NULL, NULL);
	g_object_unref (msg);
	while (!timeout)
		g_usleep (100);
	debug_printf (2, "    got timeout\n");

	if (queue_is_async) {
		g_assert_false (server_processed_message);
		debug_printf (2, "    waiting for finished\n");
		while (!finished)
			g_main_context_iteration (NULL, TRUE);
		g_assert_true (server_processed_message);
	} else {
		g_assert_true (server_processed_message);
		g_assert_false (finished);
		debug_printf (2, "    waiting for finished\n");
		while (!finished)
			g_main_context_iteration (NULL, TRUE);
	}

	debug_printf (1, "  send_message\n");
	msg = soup_message_new_from_uri ("GET", base_uri);
	server_processed_message = local_timeout = FALSE;
	timeout_id = g_idle_add_full (G_PRIORITY_HIGH, timeout_cb, &local_timeout, NULL);
	body = soup_session_send_and_read (session, msg, NULL, NULL);
        g_bytes_unref (body);
	g_object_unref (msg);

	g_assert_true (server_processed_message);

	if (send_is_blocking) {
		soup_test_assert (!local_timeout,
				  "send_message ran main loop");
	} else {
		soup_test_assert (local_timeout,
				  "send_message didn't run main loop");
	}

	if (!local_timeout)
		g_source_remove (timeout_id);

	if (!queue_is_async)
		return;

	debug_printf (1, "  cancel_message\n");
	msg = soup_message_new_from_uri ("GET", base_uri);
	cancellable = g_cancellable_new ();
	soup_session_send_async (session, msg, G_PRIORITY_DEFAULT, cancellable,
				 (GAsyncReadyCallback)cancel_message_send_done,
				 &error);
	g_signal_connect (msg, "wrote-headers",
			  G_CALLBACK (cancel_message_cb), cancellable);

	loop = g_main_loop_new (NULL, FALSE);
	g_main_loop_run (loop);

	g_assert_error (error, G_IO_ERROR, G_IO_ERROR_CANCELLED);

	g_main_loop_unref (loop);
	g_clear_error (&error);
	g_object_unref (cancellable);
	g_object_unref (msg);
}

static void
do_plain_tests (void)
{
	SoupSession *session;

	session = soup_test_session_new (NULL);
	do_test_for_session (session, TRUE, TRUE);
	soup_test_session_abort_unref (session);
}

static void
priority_test_finished_cb (SoupMessage *msg,
			   guint       *finished_count)
{
	SoupMessagePriority priority = soup_message_get_priority (msg);

	debug_printf (1, "  received message %d with priority %d\n",
		      *finished_count, priority);

	soup_test_assert (priority == expected_priorities[*finished_count],
			  "message %d should have priority %d (%d found)",
			  *finished_count, expected_priorities[*finished_count], priority);

	(*finished_count)++;
}

static void
do_priority_tests (void)
{
	SoupSession *session;
	int i, finished_count = 0;
	SoupMessagePriority priorities[] =
		{ SOUP_MESSAGE_PRIORITY_LOW,
		  SOUP_MESSAGE_PRIORITY_HIGH,
		  SOUP_MESSAGE_PRIORITY_NORMAL };

	g_test_bug ("696277");

	session = soup_test_session_new ("max-conns", 1, NULL);

	expected_priorities[0] = SOUP_MESSAGE_PRIORITY_HIGH;
	expected_priorities[1] = SOUP_MESSAGE_PRIORITY_NORMAL;
	expected_priorities[2] = SOUP_MESSAGE_PRIORITY_LOW;

	for (i = 0; i < 3; i++) {
		GUri *msg_uri;
		SoupMessage *msg;
		char buf[5];

		g_snprintf (buf, sizeof (buf), "%d", i);
		msg_uri = g_uri_parse_relative (base_uri, buf, SOUP_HTTP_URI_FLAGS, NULL);
		msg = soup_message_new_from_uri ("GET", msg_uri);
		g_uri_unref (msg_uri);

		soup_message_set_priority (msg, priorities[i]);
		g_signal_connect (msg, "finished",
				  G_CALLBACK (priority_test_finished_cb), &finished_count);
		soup_session_send_async (session, msg, G_PRIORITY_DEFAULT, NULL, NULL, NULL);
		g_object_unref (msg);
	}

	debug_printf (2, "    waiting for finished\n");
	while (finished_count != 3)
		g_main_context_iteration (NULL, TRUE);

	soup_test_session_abort_unref (session);
}

static void
do_priority_change_test (void)
{
        SoupSession *session;
        SoupMessage *msgs[3];
        int i, finished_count = 0;
        SoupMessagePriority priorities[] =
                { SOUP_MESSAGE_PRIORITY_LOW,
                  SOUP_MESSAGE_PRIORITY_HIGH,
                  SOUP_MESSAGE_PRIORITY_NORMAL };

        session = soup_test_session_new ("max-conns", 1, NULL);

        expected_priorities[0] = SOUP_MESSAGE_PRIORITY_HIGH;
        expected_priorities[1] = SOUP_MESSAGE_PRIORITY_LOW;
        expected_priorities[2] = SOUP_MESSAGE_PRIORITY_VERY_LOW;

        for (i = 0; i < 3; i++) {
                GUri *msg_uri;
                char buf[5];

                g_snprintf (buf, sizeof (buf), "%d", i);
                msg_uri = g_uri_parse_relative (base_uri, buf, SOUP_HTTP_URI_FLAGS, NULL);
                msgs[i] = soup_message_new_from_uri ("GET", msg_uri);
                g_uri_unref (msg_uri);

                soup_message_set_priority (msgs[i], priorities[i]);
                g_signal_connect (msgs[i], "finished",
                                  G_CALLBACK (priority_test_finished_cb), &finished_count);
                soup_session_send_async (session, msgs[i], G_PRIORITY_DEFAULT, NULL, NULL, NULL);
        }

        soup_message_set_priority (msgs[2], SOUP_MESSAGE_PRIORITY_VERY_LOW);

        debug_printf (2, "    waiting for finished\n");
        while (finished_count != 3)
                g_main_context_iteration (NULL, TRUE);

        for (i = 0; i < 3; i++)
                g_object_unref (msgs[i]);

        soup_test_session_abort_unref (session);
}

static void
test_session_properties (const char *name,
			 SoupSession *session,
			 GProxyResolver *expected_proxy_resolver,
			 GTlsDatabase *expected_tls_database)
{
	GProxyResolver *proxy_resolver = soup_session_get_proxy_resolver (session);
	GTlsDatabase *tlsdb = soup_session_get_tls_database (session);

	soup_test_assert (proxy_resolver == expected_proxy_resolver,
			  "%s has %s proxy resolver",
			  name, proxy_resolver ? (expected_proxy_resolver ? "wrong" : "a") : "no");
	soup_test_assert (tlsdb == expected_tls_database,
			  "%s has %s TLS database",
			  name, tlsdb ? (expected_tls_database ? "wrong" : "a") : "no");
}

static void
do_property_tests (void)
{
	SoupSession *session;
	GProxyResolver *proxy_resolver, *default_proxy_resolver;
	GTlsDatabase *tlsdb, *default_tlsdb;

	g_test_bug ("708696");

	default_proxy_resolver = g_proxy_resolver_get_default ();
	default_tlsdb = g_tls_backend_get_default_database (g_tls_backend_get_default ());

	/* NOTE: We intentionally do not use soup_test_session_new() here */

	session = g_object_new (SOUP_TYPE_SESSION,
				NULL);
	test_session_properties ("Base plain session", session,
				 default_proxy_resolver, default_tlsdb);
	g_object_unref (session);

	session = g_object_new (SOUP_TYPE_SESSION,
				"proxy-resolver", NULL,
				NULL);
	test_session_properties ("Session with NULL :proxy-resolver", session,
				 NULL, default_tlsdb);
	g_object_unref (session);

	proxy_resolver = g_simple_proxy_resolver_new (NULL, NULL);
	session = g_object_new (SOUP_TYPE_SESSION,
				"proxy-resolver", proxy_resolver,
				NULL);
	test_session_properties ("Session with non-NULL :proxy-resolver", session,
				 proxy_resolver, default_tlsdb);
	g_object_unref (proxy_resolver);
	g_object_unref (session);

	session = g_object_new (SOUP_TYPE_SESSION,
				"tls-database", NULL,
				NULL);
	test_session_properties ("Session with NULL :tls-database", session,
				 default_proxy_resolver, NULL);
	g_object_unref (session);

	/* g_tls_file_database_new() will fail with the dummy backend,
	 * so we can only do this test if we have a real TLS backend.
	 */
	if (tls_available) {
		GError *error = NULL;
                char *db_path;

                db_path = soup_test_build_filename_abs (G_TEST_DIST, "test-cert.pem", NULL);
		tlsdb = g_tls_file_database_new (db_path, &error);
		g_assert_no_error (error);
                g_free (db_path);

		session = g_object_new (SOUP_TYPE_SESSION,
					"tls-database", tlsdb,
					NULL);
		test_session_properties ("Session with non-NULL :tls-database", session,
					 default_proxy_resolver, tlsdb);
		g_object_unref (tlsdb);
		g_object_unref (session);
	}
}

static gint
compare_by_gtype (gconstpointer a,
		  gconstpointer b)
{
	return G_TYPE_CHECK_INSTANCE_TYPE (a, GPOINTER_TO_SIZE (b)) ? 0 : 1;
}

static void
do_features_test (void)
{
	SoupSession *session;
	GSList *features;
	SoupSessionFeature *feature;

	session = soup_test_session_new (NULL);

	features = soup_session_get_features (session, SOUP_TYPE_SESSION_FEATURE);
	/* SoupAuthManager is always added */
	g_assert_cmpuint (g_slist_length (features), >=, 1);
	g_assert_nonnull (g_slist_find_custom (features, GSIZE_TO_POINTER (SOUP_TYPE_AUTH_MANAGER), compare_by_gtype));
	g_assert_true (soup_session_has_feature (session, SOUP_TYPE_AUTH_MANAGER));
	feature = soup_session_get_feature (session, SOUP_TYPE_AUTH_MANAGER);
	g_assert_true (SOUP_IS_AUTH_MANAGER (feature));
	soup_session_remove_feature (session, feature);
	g_assert_false (soup_session_has_feature (session, SOUP_TYPE_AUTH_MANAGER));
	g_assert_null (soup_session_get_feature (session, SOUP_TYPE_AUTH_MANAGER));
	g_slist_free (features);

	soup_test_session_abort_unref (session);
}

static void
queue_order_test_message_finished (SoupMessage *msg,
				   guint       *finished_count)
{
	(*finished_count)++;
}

static void
queue_order_test_message_network_event (SoupMessage       *msg,
					GSocketClientEvent event,
					GIOStream         *connection,
					SoupMessage      **queue)
{
	int i;

	if (event != G_SOCKET_CLIENT_RESOLVING)
                return;

	for (i = 0; i < 3; i++) {
		if (queue[i] == NULL) {
			queue[i] = msg;
			return;
		}
	}
	g_assert_cmpstr ("This code", ==, "should not be reached");
}

static void
do_queue_order_test (void)
{
	SoupSession *session;
	SoupMessage *msg1, *msg2, *msg3;
	guint finished_count = 0;
	SoupMessage *queue[3] = { NULL, NULL, NULL };

	session = soup_test_session_new (NULL);

	msg1 = soup_message_new_from_uri ("GET", base_uri);
	g_signal_connect (msg1, "network-event",
			  G_CALLBACK (queue_order_test_message_network_event),
			  queue);
	g_signal_connect (msg1, "finished",
			  G_CALLBACK (queue_order_test_message_finished),
			  &finished_count);
	msg2 = soup_message_new_from_uri ("GET", base_uri);
	g_signal_connect (msg2, "network-event",
			  G_CALLBACK (queue_order_test_message_network_event),
			  queue);
	g_signal_connect (msg2, "finished",
			  G_CALLBACK (queue_order_test_message_finished),
			  &finished_count);
	msg3 = soup_message_new_from_uri ("GET", base_uri);
	g_signal_connect (msg3, "network-event",
			  G_CALLBACK (queue_order_test_message_network_event),
			  queue);
	g_signal_connect (msg3, "finished",
			  G_CALLBACK (queue_order_test_message_finished),
			  &finished_count);

	soup_session_send_async (session, msg1, G_PRIORITY_DEFAULT, NULL, NULL, NULL);
	soup_session_send_async (session, msg2, G_PRIORITY_DEFAULT, NULL, NULL, NULL);
	soup_session_send_async (session, msg3, G_PRIORITY_DEFAULT, NULL, NULL, NULL);
	while (queue[2] == NULL)
		g_main_context_iteration (NULL, TRUE);

	g_assert_true (queue[0] == msg1);
	g_assert_true (queue[1] == msg2);
	g_assert_true (queue[2] == msg3);
	g_object_unref (msg1);
	g_object_unref (msg2);
	g_object_unref (msg3);

	while (finished_count != 3)
		g_main_context_iteration (NULL, TRUE);

	soup_test_session_abort_unref (session);
}

static void
do_user_agent_test(void)
{
        SoupSession *session;
        SoupMessage *msg;
        SoupMessageHeaders *request_headers;
        gchar dest_str[128];
        gchar *result_str;

        session = soup_test_session_new (NULL);
        msg = soup_message_new_from_uri ("GET", base_uri);
        request_headers = soup_message_get_request_headers (msg);

        // Default value of `priv->user_agent` should be NULL
        g_assert_null (soup_session_get_user_agent (session));
        soup_test_session_send_message (session, msg);
        g_assert_null (soup_message_headers_get_one (request_headers, "User-Agent"));

        // Set value to "libsoup Session Test"
        soup_session_set_user_agent (session, "libsoup Session Test");
        g_assert_cmpstr ("libsoup Session Test", ==,
                        soup_session_get_user_agent (session));
        soup_test_session_send_message (session, msg);
        g_assert_cmpstr ("libsoup Session Test", ==,
                         soup_message_headers_get_one (request_headers, "User-Agent"));

        // Set value to "Session Test " will append default
        g_strlcpy (dest_str, "Session Test libsoup/", sizeof (dest_str));
        result_str = g_strconcat (dest_str, PACKAGE_VERSION, NULL);
        soup_session_set_user_agent (session, "Session Test ");
        g_assert_cmpstr (result_str, ==, soup_session_get_user_agent (session));
        soup_test_session_send_message (session, msg);
        g_assert_cmpstr (result_str, ==,
                         soup_message_headers_get_one (request_headers, "User-Agent"));

        // Set value to "Session Test " while this is already the saved value
        soup_session_set_user_agent (session, "Session Test ");
        g_assert_cmpstr (result_str, ==, soup_session_get_user_agent (session));
        soup_test_session_send_message (session, msg);
        g_assert_cmpstr (result_str, ==,
                         soup_message_headers_get_one (request_headers, "User-Agent"));
        g_free (result_str);

        // Set value to "" should result in the default string, call TWICE
        g_strlcpy (dest_str, "libsoup/", sizeof (dest_str));
        result_str = g_strconcat (dest_str, PACKAGE_VERSION, NULL);
        soup_session_set_user_agent (session, "");
        g_assert_cmpstr (result_str, ==, soup_session_get_user_agent (session));
        soup_session_set_user_agent (session, "");       // To reach early return
        soup_test_session_send_message (session, msg);
        g_assert_cmpstr (result_str, ==,
                         soup_message_headers_get_one (request_headers, "User-Agent"));

        // Set value to the full string which equals the default string
        soup_session_set_user_agent (session, result_str);
        g_assert_cmpstr (result_str, ==, soup_session_get_user_agent (session));
        soup_test_session_send_message (session, msg);
        g_assert_cmpstr (result_str, ==,
                         soup_message_headers_get_one (request_headers, "User-Agent"));
        g_free (result_str);

        // Set value to NULL after it has already been set to something else
        soup_session_set_user_agent (session, NULL);
        g_assert_null (soup_session_get_user_agent (session));
        // implementation in soup_session_send_queue_item() will skip over NULL value user_agent property
        // therefore the existing non-null User-Agent header will remain until explicitly removed
        soup_message_headers_remove (request_headers, "User-Agent");
        soup_test_session_send_message (session, msg);
        g_assert_null (soup_message_headers_get_one (request_headers, "User-Agent"));

        // Test soup_message_headers_append of "User-Agent" right after being NULL
        g_test_bug("https://gitlab.gnome.org/GNOME/libsoup/-/issues/405");
        soup_session_set_user_agent (session, NULL);
        soup_message_headers_remove (request_headers, "User-Agent");
        g_assert_null (soup_session_get_user_agent (session));
        g_assert_null (soup_message_headers_get_one (request_headers, "User-Agent"));
        soup_message_headers_append (request_headers, "User-Agent", "#405");
        g_assert_null (soup_session_get_user_agent (session));  // property remains NULL
        soup_test_session_send_message (session, msg);
        g_assert_cmpstr ("#405", ==,
                         soup_message_headers_get_one (request_headers, "User-Agent"));

        // Test soup_message_headers_replace of "User-Agent" right after being NULL
        g_test_bug("https://gitlab.gnome.org/GNOME/libsoup/-/issues/405");
        soup_session_set_user_agent (session, NULL);
        soup_message_headers_remove (request_headers, "User-Agent");
        g_assert_null (soup_session_get_user_agent (session));
        g_assert_null (soup_message_headers_get_one (request_headers, "User-Agent"));
        soup_message_headers_replace (request_headers, "User-Agent", "#405");
        g_assert_null (soup_session_get_user_agent (session));  // property remains NULL
        soup_test_session_send_message (session, msg);
        g_assert_cmpstr ("#405", ==,
                         soup_message_headers_get_one (request_headers, "User-Agent"));

        soup_test_session_abort_unref (session);
}

int
main (int argc, char **argv)
{
	SoupServer *server;
	int ret;

	test_init (argc, argv, NULL);

	server = soup_test_server_new (SOUP_TEST_SERVER_IN_THREAD);
	soup_server_add_handler (server, NULL, server_handler, NULL, NULL);
	base_uri = soup_test_server_get_uri (server, "http", NULL);
	index_bytes = soup_test_get_index ();
 	soup_test_register_resources ();

	g_test_add_func ("/session/SoupSession", do_plain_tests);
	g_test_add_func ("/session/priority", do_priority_tests);
        g_test_add_func ("/session/priority-change", do_priority_change_test);
	g_test_add_func ("/session/property", do_property_tests);
	g_test_add_func ("/session/features", do_features_test);
	g_test_add_func ("/session/queue-order", do_queue_order_test);
	g_test_add_func ("/session/user-agent", do_user_agent_test);

	ret = g_test_run ();

	g_uri_unref (base_uri);
	soup_test_server_quit_unref (server);

	test_cleanup ();
	return ret;
}
