/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*-
 *
 * Copyright © 2018 Endless Mobile, Inc.
 * Copyright © 2026 Philip Withnall
 *
 * 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, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
 *
 * Authors:
 *  - Philip Withnall <withnall@endlessm.com>
 *  - Philip Withnall <pwithnall@gnome.org>
 */

#include <gio/gio.h>
#include <glib.h>
#include <libglib-testing/dbus-queue.h>
#include <locale.h>
#include "test-service-iface.h"

/* Test that creating and destroying a D-Bus queue works. A basic smoketest. */
static void
test_dbus_queue_construction (void)
{
  g_autoptr(GtDBusQueue) queue = NULL;
  queue = gt_dbus_queue_new ();

  /* Call a method to avoid warnings about unused variables. */
  g_assert_cmpuint (gt_dbus_queue_get_n_messages (queue), ==, 0);
}

/* Fixture for tests which interact with the com.example.Test service over
 * D-Bus.
 *
 * It exports one object (with ID 123) and an manager object. The method return
 * values from ID 123 are up to the test in question.
 */
typedef struct
{
  GtDBusQueue *queue;  /* (owned) */
  guint valid_id;
} BusFixture;

static void
bus_set_up (BusFixture    *fixture,
            gconstpointer  test_data)
{
  g_autoptr(GError) local_error = NULL;
  g_autofree gchar *object_path = NULL;

  fixture->valid_id = 123;  /* arbitrarily chosen */
  fixture->queue = gt_dbus_queue_new ();

  gt_dbus_queue_connect (fixture->queue, &local_error);
  g_assert_no_error (local_error);

  gt_dbus_queue_own_name (fixture->queue, "com.example.Test");

  object_path = g_strdup_printf ("/com/example/Test/Object%u", fixture->valid_id);
  gt_dbus_queue_export_object (fixture->queue,
                               object_path,
                               (GDBusInterfaceInfo *) &object_interface_info,
                               &local_error);
  g_assert_no_error (local_error);

  gt_dbus_queue_export_object (fixture->queue,
                               "/com/example/Test",
                               (GDBusInterfaceInfo *) &manager_interface_info,
                               &local_error);
  g_assert_no_error (local_error);
}

static void
bus_tear_down (BusFixture    *fixture,
               gconstpointer  test_data)
{
  gt_dbus_queue_disconnect (fixture->queue, TRUE);
  g_clear_pointer (&fixture->queue, gt_dbus_queue_free);
}

/* Helper #GAsyncReadyCallback which returns the #GAsyncResult in its @user_data. */
static void
async_result_cb (GObject      *obj,
                 GAsyncResult *result,
                 gpointer      user_data)
{
  GAsyncResult **result_out = (GAsyncResult **) user_data;

  g_assert_null (*result_out);
  *result_out = g_object_ref (result);
}

typedef struct {
  char *sender_name;
  char *object_path;
  char *interface_name;
  char *signal_name;  /* (nullable) */
  GVariant *parameters;
} SignalData;

static void
signal_data_free (SignalData *data)
{
  g_variant_unref (data->parameters);
  g_free (data->signal_name);
  g_free (data->interface_name);
  g_free (data->object_path);
  g_free (data->sender_name);
  g_free (data);
}

G_DEFINE_AUTOPTR_CLEANUP_FUNC (SignalData, signal_data_free)

/* Helper #GDBusSignalCallback which returns the signal data in its @user_data. */
static void
signal_cb (GDBusConnection *connection,
           const char      *sender_name,
           const char      *object_path,
           const char      *interface_name,
           const char      *signal_name,
           GVariant        *parameters,
           void            *user_data)
{
  SignalData **data_out = user_data;
  g_autoptr(SignalData) data = NULL;

  data = g_new0 (SignalData, 1);
  data->sender_name = g_strdup (sender_name);
  data->object_path = g_strdup (object_path);
  data->interface_name = g_strdup (interface_name);
  data->signal_name = g_strdup (signal_name);
  data->parameters = g_variant_ref (parameters);

  g_assert_null (*data_out);
  *data_out = g_steal_pointer (&data);

  g_main_context_wakeup (g_main_context_get_thread_default ());
}

/* Test that making two calls in series to a mock D-Bus service works. The
 * @test_data is a boolean value indicating whether to do the calls
 * synchronously (%FALSE) or asynchronously (%TRUE).
 *
 * The mock D-Bus replies are generated in series_server_cb(), which is
 * used for both synchronous and asynchronous calls. */
static void series_server_cb (GtDBusQueue *queue,
                              gpointer     user_data);

static void
test_dbus_queue_series (BusFixture    *fixture,
                        gconstpointer  test_data)
{
  g_autoptr(GVariant) reply = NULL;
  g_autoptr(GError) local_error = NULL;
  gboolean test_async = GPOINTER_TO_UINT (test_data);
  GDBusConnection *client_connection = gt_dbus_queue_get_client_connection (fixture->queue);
  g_autofree gchar *object_path = NULL;
  g_autoptr(GVariant) properties = NULL;
  const gchar *some_str;
  guint some_int;

  gt_dbus_queue_set_server_func (fixture->queue, series_server_cb, fixture);

  if (test_async)
    {
      g_autoptr(GAsyncResult) result = NULL;

      g_dbus_connection_call (client_connection,
                              "com.example.Test",
                              "/com/example/Test",
                              "com.example.Test.Manager",
                              "GetObjectPath",
                              g_variant_new ("(u)", 123),
                              G_VARIANT_TYPE ("(o)"),
                              G_DBUS_CALL_FLAGS_NONE,
                              -1,  /* timeout (ms) */
                              NULL,  /* cancellable */
                              async_result_cb,
                              &result);

      while (result == NULL)
        g_main_context_iteration (NULL, TRUE);
      reply = g_dbus_connection_call_finish (client_connection, result, &local_error);
    }
  else
    {
      reply = g_dbus_connection_call_sync (client_connection,
                                           "com.example.Test",
                                           "/com/example/Test",
                                           "com.example.Test.Manager",
                                           "GetObjectPath",
                                           g_variant_new ("(u)", 123),
                                           G_VARIANT_TYPE ("(o)"),
                                           G_DBUS_CALL_FLAGS_NONE,
                                           -1,  /* timeout (ms) */
                                           NULL,  /* cancellable */
                                           &local_error);
    }

  g_assert_no_error (local_error);
  g_assert_nonnull (reply);

  g_variant_get (reply, "(o)", &object_path);

  g_clear_pointer (&reply, g_variant_unref);

  /* Get and check the object properties. */
  if (test_async)
    {
      g_autoptr(GAsyncResult) result = NULL;

      g_dbus_connection_call (client_connection,
                              "com.example.Test",
                              object_path,
                              "org.freedesktop.DBus.Properties",
                              "GetAll",
                              g_variant_new ("(s)", "com.example.Test.Object"),
                              G_VARIANT_TYPE ("(a{sv})"),
                              G_DBUS_CALL_FLAGS_NONE,
                              -1,  /* timeout (ms) */
                              NULL,  /* cancellable */
                              async_result_cb,
                              &result);

      while (result == NULL)
        g_main_context_iteration (NULL, TRUE);
      reply = g_dbus_connection_call_finish (client_connection, result, &local_error);
    }
  else
    {
      reply = g_dbus_connection_call_sync (client_connection,
                                           "com.example.Test",
                                           object_path,
                                           "org.freedesktop.DBus.Properties",
                                           "GetAll",
                                           g_variant_new ("(s)", "com.example.Test.Object"),
                                           G_VARIANT_TYPE ("(a{sv})"),
                                           G_DBUS_CALL_FLAGS_NONE,
                                           -1,  /* timeout (ms) */
                                           NULL,  /* cancellable */
                                           &local_error);
    }

  g_assert_no_error (local_error);
  g_assert_nonnull (reply);

  properties = g_variant_get_child_value (reply, 0);
  g_variant_lookup (properties, "some-str", "&s", &some_str);
  g_assert_cmpstr (some_str, ==, "hello");
  g_variant_lookup (properties, "some-int", "u", &some_int);
  g_assert_cmpuint (some_int, ==, 11);

  /* Test a void call. */
  if (test_async)
    {
      g_autoptr(GAsyncResult) result = NULL;

      g_dbus_connection_call (client_connection,
                              "com.example.Test",
                              "/com/example/Test",
                              "com.example.Test.Manager",
                              "VoidCall",
                              NULL,
                              G_VARIANT_TYPE ("()"),
                              G_DBUS_CALL_FLAGS_NONE,
                              -1,  /* timeout (ms) */
                              NULL,  /* cancellable */
                              async_result_cb,
                              &result);

      while (result == NULL)
        g_main_context_iteration (NULL, TRUE);
      reply = g_dbus_connection_call_finish (client_connection, result, &local_error);
    }
  else
    {
      reply = g_dbus_connection_call_sync (client_connection,
                                           "com.example.Test",
                                           "/com/example/Test",
                                           "com.example.Test.Manager",
                                           "VoidCall",
                                           NULL,
                                           G_VARIANT_TYPE ("()"),
                                           G_DBUS_CALL_FLAGS_NONE,
                                           -1,  /* timeout (ms) */
                                           NULL,  /* cancellable */
                                           &local_error);
    }

  g_assert_no_error (local_error);
  g_assert_nonnull (reply);

  g_assert_true (g_variant_is_of_type (reply, G_VARIANT_TYPE ("()")));
}

/* This is run in a worker thread. */
static void
series_server_cb (GtDBusQueue *queue,
                  gpointer     user_data)
{
  BusFixture *fixture = user_data;
  g_autoptr(GDBusMethodInvocation) invocation1 = NULL;
  g_autoptr(GDBusMethodInvocation) invocation2 = NULL;
  g_autoptr(GDBusMethodInvocation) invocation3 = NULL;
  g_autofree gchar *object_path = NULL;
  g_autofree gchar *reply1 = NULL;

  /* Handle the GetObjectPath() call. */
  guint object_id;
  invocation1 =
      gt_dbus_queue_assert_pop_message (queue,
                                        "/com/example/Test",
                                        "com.example.Test.Manager",
                                        "GetObjectPath", "(u)", &object_id);
  g_assert_cmpint (object_id, ==, fixture->valid_id);

  object_path = g_strdup_printf ("/com/example/Test/Object%u", object_id);
  reply1 = g_strdup_printf ("(@o '%s',)", object_path);
  g_dbus_method_invocation_return_value (invocation1, g_variant_new_parsed (reply1));

  /* Handle the Properties.GetAll() call and return some arbitrary values for
   * the given object. */
  const gchar *property_interface;
  invocation2 =
      gt_dbus_queue_assert_pop_message (queue,
                                        object_path,
                                        "org.freedesktop.DBus.Properties",
                                        "GetAll", "(&s)", &property_interface);
  g_assert_cmpstr (property_interface, ==, "com.example.Test.Object");

  const gchar *reply2 =
    "({"
      "'some-str': <'hello'>,"
      "'some-int': <@u 11>"
    "},)";
  g_dbus_method_invocation_return_value (invocation2, g_variant_new_parsed (reply2));

  /* Handle a call with no arguments. */
  invocation3 =
      gt_dbus_queue_assert_pop_message (queue,
                                        "/com/example/Test",
                                        "com.example.Test.Manager",
                                        "VoidCall", "()");
  g_dbus_method_invocation_return_value (invocation3, NULL);
}

static void
test_dbus_queue_emit_signal (BusFixture *fixture,
                             const void *test_data)
{
  g_autoptr(GError) local_error = NULL;
  GDBusConnection *client_connection = gt_dbus_queue_get_client_connection (fixture->queue);
  g_autoptr(GVariant) parameters = NULL;
  unsigned int signal_id;
  g_autoptr(SignalData) signal_data = NULL;
  gboolean success;
  g_autoptr(GAsyncResult) result = NULL;
  g_autoptr(GVariant) reply = NULL;

  signal_id = g_dbus_connection_signal_subscribe (client_connection,
                                                  "com.example.Test",
                                                  "com.example.Test.Manager",
                                                  "SomeSignal",
                                                  "/com/example/Test",
                                                  NULL,
                                                  G_DBUS_SIGNAL_FLAGS_NONE,
                                                  signal_cb,
                                                  &signal_data,
                                                  NULL);

  /* Call a method on the bus daemon to force synchronisation, ensuring our
   * signal subscription is in place before we emit the signal. */
  g_dbus_connection_call (client_connection,
                          "org.freedesktop.DBus",
                          "/org/freedesktop/DBus",
                          "org.freedesktop.DBus.Peer",
                          "Ping",
                          NULL,
                          NULL,
                          G_DBUS_CALL_FLAGS_NONE,
                          -1,  /* timeout (ms) */
                          NULL,  /* cancellable */
                          async_result_cb,
                          &result);

  while (result == NULL)
    g_main_context_iteration (NULL, TRUE);
  reply = g_dbus_connection_call_finish (client_connection, result, &local_error);

  g_assert_no_error (local_error);
  g_clear_pointer (&reply, g_variant_unref);

  parameters = g_variant_ref_sink (g_variant_new ("(ss)", "hello", "there"));
  success = gt_dbus_queue_emit_signal (fixture->queue,
                                       NULL,
                                       "/com/example/Test",
                                       "com.example.Test.Manager",
                                       "SomeSignal",
                                       parameters,
                                       &local_error);
  g_assert_no_error (local_error);
  g_assert_true (success);

  while (signal_data == NULL)
    g_main_context_iteration (NULL, TRUE);

  g_assert_cmpstr (signal_data->sender_name, !=, g_dbus_connection_get_unique_name (client_connection));
  g_assert_cmpstr (signal_data->object_path, ==, "/com/example/Test");
  g_assert_cmpstr (signal_data->interface_name, ==, "com.example.Test.Manager");
  g_assert_cmpstr (signal_data->signal_name, ==, "SomeSignal");
  g_assert_cmpvariant (signal_data->parameters, parameters);

  g_clear_dbus_signal_subscription (&signal_id, client_connection);
}

static int
strcmp_data (const void *a,
             const void *b,
             void       *user_data)
{
  return strcmp (*((const char **) a),
                 *((const char **) b));
}

static void assert_activatable_names (GDBusConnection *client_connection,
                                      ...) G_GNUC_NULL_TERMINATED;

/* Takes a NULL-terminated list of expected activatable well-known bus names
 * (each of type `const char *`).
 *
 * Typically you should always include `org.freedesktop.DBus` in the list of
 * expected names. */
static void
assert_activatable_names (GDBusConnection *client_connection,
                          ...)
{
  g_autoptr(GVariant) reply = NULL;
  g_autoptr(GError) local_error = NULL;
  g_autofree const char **activatable_names = NULL;
  g_auto(GStrv) expected_activatable_names = NULL;
  va_list va;
  const char *n;
  g_autoptr(GStrvBuilder) builder = NULL;

  reply = g_dbus_connection_call_sync (client_connection,
                                       "org.freedesktop.DBus",
                                       "/org/freedesktop/DBus",
                                       "org.freedesktop.DBus",
                                       "ListActivatableNames",
                                       NULL,
                                       G_VARIANT_TYPE ("(as)"),
                                       G_DBUS_CALL_FLAGS_NONE,
                                       -1,  /* timeout (ms) */
                                       NULL,  /* cancellable */
                                       &local_error);
  g_assert_no_error (local_error);

  g_variant_get (reply, "(^a&s)", &activatable_names);

  builder = g_strv_builder_new ();

  va_start (va, client_connection);
  while ((n = va_arg (va, const char *)) != NULL)
    g_strv_builder_add (builder, n);
  va_end (va);

  expected_activatable_names = g_strv_builder_end (builder);

  g_sort_array (activatable_names, g_strv_length ((char **) activatable_names),
                sizeof (*activatable_names), strcmp_data, NULL);
  g_sort_array (expected_activatable_names, g_strv_length (expected_activatable_names),
                sizeof (*expected_activatable_names), strcmp_data, NULL);

  g_assert_cmpstrv (activatable_names, expected_activatable_names);
}

/* Force a synchronisation point with the dbus-daemon by asking it to reload its
 * config. This is needed to avoid races when adding/removing activatable names
 * very quickly.
 *
 * In real code, it can be avoided by adding the activatable name before
 * connecting the #GtDBusQueue, which is what people will typically do. */
static void
assert_daemon_reload_config (GDBusConnection *client_connection)
{
  g_autoptr(GVariant) reply = NULL;
  g_autoptr(GError) local_error = NULL;

  reply = g_dbus_connection_call_sync (client_connection,
                                       "org.freedesktop.DBus",
                                       "/org/freedesktop/DBus",
                                       "org.freedesktop.DBus",
                                       "ReloadConfig",
                                       NULL,
                                       NULL,
                                       G_DBUS_CALL_FLAGS_NONE,
                                       -1,  /* timeout (ms) */
                                       NULL,  /* cancellable */
                                       &local_error);
  g_assert_no_error (local_error);
  g_clear_pointer (&reply, g_variant_unref);
}

static void
test_dbus_queue_activatable_name (BusFixture *fixture,
                                  const void *test_data)
{
  g_autoptr(GError) local_error = NULL;
  GDBusConnection *client_connection = gt_dbus_queue_get_client_connection (fixture->queue);
  gboolean success;

  success = gt_dbus_queue_add_activatable_name (fixture->queue,
                                                "com.example.ActivatableTest",
                                                &local_error);
  g_assert_no_error (local_error);
  g_assert_true (success);

  /* FIXME: assert_daemon_reload_config(), and the rest of this test, depends on
   * https://gitlab.gnome.org/GNOME/glib/-/merge_requests/5094 which isn’t
   * currently in a stable release of GLib. Try reloading the config and see if
   * we get an error. If so, it’s not in the version of GLib we’re using and we
   * should skip the test.
   *
   * This block can be removed once we depend on a suitably recent version of
   * GLib. */
  {
    g_autoptr(GVariant) reload_reply = NULL;
    g_autoptr(GError) reload_error = NULL;

    reload_reply = g_dbus_connection_call_sync (client_connection,
                                                "org.freedesktop.DBus",
                                                "/org/freedesktop/DBus",
                                                "org.freedesktop.DBus",
                                                "ReloadConfig",
                                                NULL,
                                                NULL,
                                                G_DBUS_CALL_FLAGS_NONE,
                                                -1,  /* timeout (ms) */
                                                NULL,  /* cancellable */
                                                &reload_error);

    if (g_error_matches (reload_error, G_DBUS_ERROR, G_DBUS_ERROR_FILE_NOT_FOUND))
      {
        g_test_skip ("Need a more recent version of GLib to test reloading dbus-daemon config");
        return;
      }

    g_clear_pointer (&reload_reply, g_variant_unref);
  }

  assert_daemon_reload_config (client_connection);
  assert_activatable_names (client_connection,
                            "com.example.ActivatableTest",
                            "org.freedesktop.DBus",
                            NULL);

  success = gt_dbus_queue_add_activatable_name (fixture->queue,
                                                "com.example.AnotherActivatableTest",
                                                &local_error);
  g_assert_no_error (local_error);
  g_assert_true (success);

  assert_daemon_reload_config (client_connection);
  assert_activatable_names (client_connection,
                            "com.example.ActivatableTest",
                            "com.example.AnotherActivatableTest",
                            "org.freedesktop.DBus",
                            NULL);

  success = gt_dbus_queue_remove_activatable_name (fixture->queue,
                                                   "com.example.ActivatableTest",
                                                   &local_error);
  g_assert_no_error (local_error);
  g_assert_true (success);

  assert_daemon_reload_config (client_connection);
  assert_activatable_names (client_connection,
                            "com.example.AnotherActivatableTest",
                            "org.freedesktop.DBus",
                            NULL);

  /* Try adding a duplicate activatable name */
  success = gt_dbus_queue_add_activatable_name (fixture->queue,
                                                "com.example.AnotherActivatableTest",
                                                &local_error);
  g_assert_error (local_error, G_IO_ERROR, G_IO_ERROR_EXISTS);
  g_assert_false (success);
  g_clear_error (&local_error);

  /* Clean up */
  success = gt_dbus_queue_remove_activatable_name (fixture->queue,
                                                   "com.example.AnotherActivatableTest",
                                                   &local_error);
  g_assert_no_error (local_error);
  g_assert_true (success);

  /* Try removing a now-non-existent activatable name */
  success = gt_dbus_queue_remove_activatable_name (fixture->queue,
                                                   "com.example.AnotherActivatableTest",
                                                   &local_error);
  g_assert_error (local_error, G_IO_ERROR, G_IO_ERROR_NOT_FOUND);
  g_assert_false (success);
  g_clear_error (&local_error);
}

int
main (int   argc,
      char *argv[])
{
  setlocale (LC_ALL, "");

  g_test_init (&argc, &argv, NULL);

  g_test_add_func ("/dbus-queue/construction",
                   test_dbus_queue_construction);

  g_test_add ("/dbus-queue/series-async", BusFixture, GUINT_TO_POINTER (TRUE),
              bus_set_up, test_dbus_queue_series, bus_tear_down);
  g_test_add ("/dbus-queue/series-sync", BusFixture, GUINT_TO_POINTER (FALSE),
              bus_set_up, test_dbus_queue_series, bus_tear_down);
  g_test_add ("/dbus-queue/emit-signal", BusFixture, NULL,
              bus_set_up, test_dbus_queue_emit_signal, bus_tear_down);
  g_test_add ("/dbus-queue/activatable-name", BusFixture, NULL,
              bus_set_up, test_dbus_queue_activatable_name, bus_tear_down);

  return g_test_run ();
}
