#include <gio/gio.h>
#ifdef G_OS_UNIX
#include <fcntl.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <signal.h>
#endif

#include "gdbus-sessionbus.h"

static gint appeared;
static gint disappeared;
static gint changed;

static void
name_appeared (GDBusConnection *connection,
               const gchar     *name,
               const gchar     *name_owner,
               gpointer         user_data)
{
  GMainLoop *loop = user_data;

  appeared++;

  if (loop)
    g_main_loop_quit (loop);
}

static void
name_disappeared (GDBusConnection *connection,
                  const gchar     *name,
                  gpointer         user_data)
{
  GMainLoop *loop = user_data;

  disappeared++;

  if (loop)
    g_main_loop_quit (loop);
}

#ifdef G_OS_UNIX
void
child_setup_pipe (gpointer user_data)
{
  int *fds = user_data;

  close (fds[0]);
  dup2 (fds[1], 3);
  g_setenv ("_G_TEST_SLAVE_FD", "3", TRUE);
  close (fds[1]);
}
#endif

static gboolean
spawn_async_with_monitor_pipe (const gchar *argv[], GPid *pid, int *fd)
{
#ifdef G_OS_UNIX
  int fds[2];
  gboolean result;

  pipe (fds);

  result = g_spawn_async (NULL, (char**)argv, NULL, G_SPAWN_DO_NOT_REAP_CHILD, child_setup_pipe, &fds, pid, NULL);
  close (fds[1]);
  *fd = fds[0];
  return result;
#else
  *fd = -1;
  return g_spawn_async (NULL, argv, 0, NULL, NULL, pid, NULL);
#endif
}

static gboolean
start_application (GPid *pid, int *fd)
{
  const gchar *argv[] = { "./testapp", NULL };

  g_assert (spawn_async_with_monitor_pipe (argv, pid, fd));

  return FALSE;
}

typedef struct {
  GMainContext *context;
  GSource *child_watch;
  GSource *timeout;
  GPid pid;
  int fd;

  gboolean child_exited;
  GMainLoop *loop;
} AwaitChildTerminationData;

static void
on_child_termination_exited (GPid pid,
                             gint status,
                             gpointer user_data)
{
  AwaitChildTerminationData *data = user_data;
  data->child_exited = TRUE;
  g_spawn_close_pid (data->pid);
  g_main_loop_quit (data->loop);
}

static gboolean
on_child_termination_timeout (gpointer user_data)
{
  AwaitChildTerminationData *data = user_data;
  g_main_loop_quit (data->loop);
  return FALSE;
}

static void
await_child_termination_init (AwaitChildTerminationData *data,
                              GPid                       pid,
                              int                        fd)
{
  data->context = g_main_context_get_thread_default ();
  data->child_exited = FALSE;
  data->pid = pid;
  data->fd = fd;
}

static void
await_child_termination_terminate (AwaitChildTerminationData *data)
{
#ifdef G_OS_UNIX
  kill (data->pid, SIGTERM);
  close (data->fd);
#endif
}

static gboolean
await_child_termination_run (AwaitChildTerminationData *data)
{
  GSource *timeout_source;
  GSource *child_watch_source;

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

  child_watch_source = g_child_watch_source_new (data->pid);
  g_source_set_callback (child_watch_source, (GSourceFunc) on_child_termination_exited, data, NULL);
  g_source_attach (child_watch_source, data->context);

  timeout_source = g_timeout_source_new_seconds (5);
  g_source_set_callback (timeout_source, on_child_termination_timeout, data, NULL);
  g_source_attach (timeout_source, data->context);

  g_main_loop_run (data->loop);

  g_source_destroy (child_watch_source);
  g_source_unref (child_watch_source);
  g_source_destroy (timeout_source);
  g_source_unref (timeout_source);

  g_main_loop_unref (data->loop);

  return data->child_exited;
}

static void
terminate_child_sync (GPid pid, int fd)
{
  AwaitChildTerminationData data;

  await_child_termination_init (&data, pid, fd);
  await_child_termination_terminate (&data);
  await_child_termination_run (&data);
}

typedef void (*RunWithApplicationFunc) (void);

typedef struct {
  GMainLoop *loop;
  RunWithApplicationFunc func;
  guint timeout_id;
} RunWithAppNameAppearedData;

static void
on_run_with_application_name_appeared (GDBusConnection *connection,
                                       const gchar     *name,
                                       const gchar     *name_owner,
                                       gpointer         user_data)
{
  RunWithAppNameAppearedData *data = user_data;

  data->func ();

  g_main_loop_quit (data->loop);
}

static gboolean
on_run_with_application_timeout (gpointer user_data)
{
  RunWithAppNameAppearedData *data = user_data;
  data->timeout_id = 0;
  g_error ("Timed out starting testapp");
  return FALSE;
}

static void
run_with_application (RunWithApplicationFunc test_func)
{
  GMainLoop *loop;
  RunWithAppNameAppearedData data;
  gint watch;
  GPid main_pid;
  gint main_fd;

  loop = g_main_loop_new (NULL, FALSE);

  data.timeout_id = 0;
  data.func = test_func;
  data.loop = loop;

  watch = g_bus_watch_name (G_BUS_TYPE_SESSION,
                            "org.gtk.test.app",
                            0,
                            on_run_with_application_name_appeared,
                            NULL,
                            &data,
                            NULL);

  data.timeout_id = g_timeout_add_seconds (5, on_run_with_application_timeout, &data);

  start_application (&main_pid, &main_fd);

  g_main_loop_run (loop);

  if (data.timeout_id)
    {
      g_source_remove (data.timeout_id);
      data.timeout_id = 0;
    }

  g_main_loop_unref (loop);

  g_bus_unwatch_name (watch);

  terminate_child_sync (main_pid, main_fd);
}

/* This test starts an application, checks that its name appears
 * on the bus, then starts it again and checks that the second
 * instance exits right away.
 */
static void
test_unique_on_app_appeared (void)
{
  GPid sub_pid;
  int sub_fd;
  int watch;
  AwaitChildTerminationData data;

  appeared = 0;
  disappeared = 0;

  watch = g_bus_watch_name (G_BUS_TYPE_SESSION,
                            "org.gtk.test.app",
                            0,
                            name_appeared,
                            name_disappeared,
                            NULL,
                            NULL);

  start_application (&sub_pid, &sub_fd);
  await_child_termination_init (&data, sub_pid, sub_fd);
  await_child_termination_run (&data);

  g_bus_unwatch_name (watch);

  g_assert_cmpint (appeared, ==, 1);
  g_assert_cmpint (disappeared, ==, 0);
}

static void
test_unique (void)
{
  run_with_application (test_unique_on_app_appeared);
}

static void
on_name_disappeared_quit (GDBusConnection *connection,
                          const gchar     *name,
                          gpointer         user_data)
{
  GMainLoop *loop = user_data;

  g_main_loop_quit (loop);
}

static GVariant *
create_empty_vardict ()
{
  GVariantBuilder builder;

  g_variant_builder_init (&builder, G_VARIANT_TYPE ("a{sv}"));
  return g_variant_builder_end (&builder);
}

static gboolean
call_quit (gpointer data)
{
  GDBusConnection *connection;
  GError *error = NULL;
  GVariant *res;

  connection = g_bus_get_sync (G_BUS_TYPE_SESSION, NULL, NULL);
  res = g_dbus_connection_call_sync (connection,
                                     "org.gtk.test.app",
                                     "/org/gtk/test/app",
                                     "org.gtk.Application",
                                     "Quit",
                                     g_variant_new ("(@a{sv})", create_empty_vardict ()),
				     NULL,
                                     G_DBUS_CALL_FLAGS_NONE,
                                     -1,
                                     NULL,
                                     &error);
  if (error)
    {
      g_assert_error (error, G_DBUS_ERROR, G_DBUS_ERROR_NO_REPLY);
      g_error_free (error);
    }

  if (res)
    g_variant_unref (res);

  return FALSE;
}

/* This test starts an application, checks that its name appears on
 * the bus, then calls Quit, and verifies that the name disappears and
 * the application exits.
 */
static void
test_quit_on_app_appeared (void)
{
  GMainLoop *loop;
  int quit_disappeared_watch;

  loop = g_main_loop_new (NULL, FALSE);
  quit_disappeared_watch = g_bus_watch_name (G_BUS_TYPE_SESSION,
                            "org.gtk.test.app",
                            0,
                            NULL,
                            on_name_disappeared_quit,
                            loop,
                            NULL);
  call_quit (NULL);

  g_main_loop_run (loop);

  g_bus_unwatch_name (quit_disappeared_watch);

  g_main_loop_unref (loop);
}

static void
test_quit (void)
{
  run_with_application (test_quit_on_app_appeared);
}

static gboolean
_g_strv_has_string (const gchar* const * haystack,
                    const gchar *needle)
{
  guint n;

  for (n = 0; haystack != NULL && haystack[n] != NULL; n++)
    {
      if (g_strcmp0 (haystack[n], needle) == 0)
        return TRUE;
    }
  return FALSE;
}

static gchar **
list_actions (void)
{
  GDBusConnection *connection;
  GVariant *res;
  gchar **strv;
  gchar *str;
  GVariantIter *iter;
  gint i;

  connection = g_bus_get_sync (G_BUS_TYPE_SESSION, NULL, NULL);
  res = g_dbus_connection_call_sync (connection,
                                     "org.gtk.test.app",
                                     "/org/gtk/test/app",
                                     "org.gtk.Application",
                                     "ListActions",
                                     NULL,
                                     NULL,
                                     G_DBUS_CALL_FLAGS_NONE,
                                     -1,
                                     NULL,
                                     NULL);

  strv = g_new0 (gchar *, 32);
  g_variant_get (res, "(a{s(sb)})", &iter);
  i = 0;
  while (g_variant_iter_loop (iter, "{s(sb)}", &str, NULL, NULL))
    {
      strv[i] = g_strdup (str);
      i++;
      g_assert (i < 32);
    }
  g_variant_iter_free (iter);

  strv[i] = NULL;

  g_variant_unref (res);
  g_object_unref (connection);

  return strv;
}

static void
test_list_actions_on_app_appeared (void)
{
  gchar **actions;

  actions = list_actions ();

  g_assert (g_strv_length  (actions) == 2);
  g_assert (_g_strv_has_string ((const char *const *)actions, "action1"));
  g_assert (_g_strv_has_string ((const char *const *)actions, "action2"));

  g_strfreev (actions);
}

/* This test start an application, waits for its name to appear on
 * the bus, then calls ListActions, and verifies that it gets the expected
 * actions back.
 */
static void
test_list_actions (void)
{
  run_with_application (test_list_actions_on_app_appeared);
}

static gboolean
invoke_action (gpointer user_data)
{
  const gchar *action = user_data;
  GDBusConnection *connection;
  GVariant *res;

  connection = g_bus_get_sync (G_BUS_TYPE_SESSION, NULL, NULL);
  res = g_dbus_connection_call_sync (connection,
                                     "org.gtk.test.app",
                                     "/org/gtk/test/app",
                                     "org.gtk.Application",
                                     "InvokeAction",
                                     g_variant_new ("(s@a{sv})",
                                                    action,
                                                    create_empty_vardict ()),
                                     NULL,
                                     G_DBUS_CALL_FLAGS_NONE,
                                     -1,
                                     NULL,
                                     NULL);
  if (res)
    g_variant_unref (res);
  g_object_unref (connection);

  return FALSE;
}

/* This test starts an application, waits for it to appear,
 * then invokes 'action1' and checks that it causes the application
 * to exit with an exit code of 1.
 */
static void
test_invoke (void)
{
  GMainLoop *loop;
  int quit_disappeared_watch;

  loop = g_main_loop_new (NULL, FALSE);

  quit_disappeared_watch = g_bus_watch_name (G_BUS_TYPE_SESSION,
                                             "org.gtk.test.app",
                                             0,
                                             NULL,
                                             on_name_disappeared_quit,
                                             loop,
                                             NULL);

  g_timeout_add (0, invoke_action, "action1");

  g_main_loop_run (loop);

  g_bus_unwatch_name (quit_disappeared_watch);
}

static void
test_remote_on_application_appeared (void)
{
  GPid sub_pid;
  int sub_fd;
  AwaitChildTerminationData data;
  gchar *argv[] = { "./testapp", "--non-unique", NULL };

  spawn_async_with_monitor_pipe ((const char **) argv, &sub_pid, &sub_fd);

  await_child_termination_init (&data, sub_pid, sub_fd);
  await_child_termination_run (&data);
}

static void
test_remote (void)
{
  run_with_application (test_remote_on_application_appeared);
}

static void
actions_changed (GDBusConnection *connection,
                 const gchar     *sender_name,
                 const gchar     *object_path,
                 const gchar     *interface_name,
                 const gchar     *signal_name,
                 GVariant        *parameters,
                 gpointer         user_data)
{
  GMainLoop *loop = user_data;

  g_assert_cmpstr (interface_name, ==, "org.gtk.Application");
  g_assert_cmpstr (signal_name, ==, "ActionsChanged");

  changed++;

  g_main_loop_quit (loop);
}

static void
test_change_action_on_application_appeared (void)
{
  GMainLoop *loop;
  guint id;
  GDBusConnection *connection;

  loop = g_main_loop_new (NULL, FALSE);

  connection = g_bus_get_sync (G_BUS_TYPE_SESSION, NULL, NULL);
  id = g_dbus_connection_signal_subscribe (connection,
                                           NULL,
                                           "org.gtk.Application",
                                           "ActionsChanged",
                                           "/org/gtk/test/app",
                                           NULL,
                                           G_DBUS_SIGNAL_FLAGS_NONE,
                                           actions_changed,
                                           loop,
                                           NULL);

  g_timeout_add (0, invoke_action, "action2");

  g_main_loop_run (loop);

  g_assert_cmpint (changed, >, 0);

  g_dbus_connection_signal_unsubscribe (connection, id);
  g_object_unref (connection);
  g_main_loop_unref (loop);
}

static void
test_change_action (void)
{
  run_with_application (test_change_action_on_application_appeared);
}

int
main (int argc, char *argv[])
{
  gint ret;

  g_type_init ();
  g_test_init (&argc, &argv, NULL);

  g_unsetenv ("DISPLAY");
  g_setenv ("DBUS_SESSION_BUS_ADDRESS", session_bus_get_temporary_address (), TRUE);

  session_bus_up ();

  g_test_add_func ("/application/unique", test_unique);
  g_test_add_func ("/application/quit", test_quit);
  g_test_add_func ("/application/list-actions", test_list_actions);
  g_test_add_func ("/application/invoke", test_invoke);
  g_test_add_func ("/application/remote", test_remote);
  g_test_add_func ("/application/change-action", test_change_action);

  ret = g_test_run ();

  session_bus_down ();

  return ret;
}