/*
 * Copyright 2015 Lars Uebernickel
 *
 * SPDX-License-Identifier: LGPL-2.1-or-later
 *
 * 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 <http://www.gnu.org/licenses/>.
 *
 * Authors: Lars Uebernickel <lars@uebernic.de>
 */

#include <gio/gio.h>

#include <string.h>

/* Wrapper around g_list_model_get_item() and g_list_model_get_object() which
 * checks they return the same thing. */
static gpointer
list_model_get (GListModel *model,
                guint       position)
{
  GObject *item = g_list_model_get_item (model, position);
  GObject *object = g_list_model_get_object (model, position);

  g_assert_true (item == object);

  g_clear_object (&object);
  return g_steal_pointer (&item);
}

#define assert_cmpitems(store, cmp, n_items) G_STMT_START{ \
  guint tmp; \
  g_assert_cmpuint (g_list_model_get_n_items (G_LIST_MODEL (store)), cmp, n_items); \
  g_object_get (store, "n-items", &tmp, NULL); \
  g_assert_cmpuint (tmp, cmp, n_items); \
}G_STMT_END

/* Test that constructing/getting/setting properties on a #GListStore works. */
static void
test_store_properties (void)
{
  GListStore *store = NULL;
  GType item_type;

  store = g_list_store_new (G_TYPE_MENU_ITEM);
  g_object_get (store, "item-type", &item_type, NULL);
  g_assert_cmpint (item_type, ==, G_TYPE_MENU_ITEM);

  g_clear_object (&store);
}

/* Test that #GListStore rejects non-GObject item types. */
static void
test_store_non_gobjects (void)
{
  if (g_test_subprocess ())
    {
      /* We have to use g_object_new() since g_list_store_new() checks the item
       * type. We want to check the property setter code works properly. */
      g_object_new (G_TYPE_LIST_STORE, "item-type", G_TYPE_LONG, NULL);
      return;
    }

  g_test_trap_subprocess (NULL, 0, G_TEST_SUBPROCESS_DEFAULT);
  g_test_trap_assert_failed ();
  g_test_trap_assert_stderr ("*CRITICAL*value * of type 'GType' is invalid or "
                             "out of range for property 'item-type'*");
}

static void
test_store_boundaries (void)
{
  GListStore *store;
  GMenuItem *item;

  store = g_list_store_new (G_TYPE_MENU_ITEM);

  item = g_menu_item_new (NULL, NULL);

  /* remove an item from an empty list */
  g_test_expect_message (G_LOG_DOMAIN, G_LOG_LEVEL_CRITICAL, "*g_sequence*");
  g_list_store_remove (store, 0);
  g_test_assert_expected_messages ();

  /* don't allow inserting an item past the end ... */
  g_test_expect_message (G_LOG_DOMAIN, G_LOG_LEVEL_CRITICAL, "*g_sequence*");
  g_list_store_insert (store, 1, item);
  assert_cmpitems (store, ==, 0);
  g_test_assert_expected_messages ();

  /* ... except exactly at the end */
  g_list_store_insert (store, 0, item);
  assert_cmpitems (store, ==, 1);

  /* remove a non-existing item at exactly the end of the list */
  g_test_expect_message (G_LOG_DOMAIN, G_LOG_LEVEL_CRITICAL, "*g_sequence*");
  g_list_store_remove (store, 1);
  g_test_assert_expected_messages ();

  g_list_store_remove (store, 0);
  assert_cmpitems (store, ==, 0);

  /* splice beyond the end of the list */
  g_test_expect_message (G_LOG_DOMAIN, G_LOG_LEVEL_CRITICAL, "*position*");
  g_list_store_splice (store, 1, 0, NULL, 0);
  g_test_assert_expected_messages ();

  /* remove items from an empty list */
  g_test_expect_message (G_LOG_DOMAIN, G_LOG_LEVEL_CRITICAL, "*position*");
  g_list_store_splice (store, 0, 1, NULL, 0);
  g_test_assert_expected_messages ();

  g_list_store_append (store, item);
  g_list_store_splice (store, 0, 1, (gpointer *) &item, 1);
  assert_cmpitems (store, ==, 1);

  /* remove more items than exist */
  g_test_expect_message (G_LOG_DOMAIN, G_LOG_LEVEL_CRITICAL, "*position*");
  g_list_store_splice (store, 0, 5, NULL, 0);
  g_test_assert_expected_messages ();
  assert_cmpitems (store, ==, 1);

  g_object_unref (store);
  g_assert_finalize_object (item);
}

static void
test_store_refcounts (void)
{
  GListStore *store;
  GMenuItem *items[10];
  GMenuItem *tmp;
  guint i;
  guint n_items;

  store = g_list_store_new (G_TYPE_MENU_ITEM);

  assert_cmpitems (store, ==, 0);
  g_assert_null (list_model_get (G_LIST_MODEL (store), 0));

  n_items = G_N_ELEMENTS (items);
  for (i = 0; i < n_items; i++)
    {
      items[i] = g_menu_item_new (NULL, NULL);
      g_object_add_weak_pointer (G_OBJECT (items[i]), (gpointer *) &items[i]);
      g_list_store_append (store, items[i]);

      g_object_unref (items[i]);
      g_assert_nonnull (items[i]);
    }

  assert_cmpitems (store, ==, n_items);
  g_assert_null (list_model_get (G_LIST_MODEL (store), n_items));

  tmp = list_model_get (G_LIST_MODEL (store), 3);
  g_assert_true (tmp == items[3]);
  g_object_unref (tmp);

  g_list_store_remove (store, 4);
  g_assert_null (items[4]);
  n_items--;
  assert_cmpitems (store, ==, n_items);
  g_assert_null (list_model_get (G_LIST_MODEL (store), n_items));

  g_object_unref (store);
  for (i = 0; i < G_N_ELEMENTS (items); i++)
    g_assert_null (items[i]);
}

static gchar *
make_random_string (void)
{
  gchar *str = g_malloc (10);
  gint i;

  for (i = 0; i < 9; i++)
    str[i] = g_test_rand_int_range ('a', 'z');
  str[i] = '\0';

  return str;
}

static gint
compare_items (gconstpointer a_p,
               gconstpointer b_p,
               gpointer      user_data)
{
  GObject *a_o = (GObject *) a_p;
  GObject *b_o = (GObject *) b_p;

  gchar *a = g_object_get_data (a_o, "key");
  gchar *b = g_object_get_data (b_o, "key");

  g_assert (user_data == GUINT_TO_POINTER(0x1234u));

  return strcmp (a, b);
}

static void
insert_string (GListStore  *store,
               const gchar *str)
{
  GObject *obj;

  obj = g_object_new (G_TYPE_OBJECT, NULL);
  g_object_set_data_full (obj, "key", g_strdup (str), g_free);

  g_list_store_insert_sorted (store, obj, compare_items, GUINT_TO_POINTER(0x1234u));

  g_object_unref (obj);
}

static void
test_store_sorted (void)
{
  GListStore *store;
  guint i;

  store = g_list_store_new (G_TYPE_OBJECT);

  for (i = 0; i < 1000; i++)
    {
      gchar *str = make_random_string ();
      insert_string (store, str);
      insert_string (store, str); /* multiple copies of the same are OK */
      g_free (str);
    }

  assert_cmpitems (store, ==, 2000);

  for (i = 0; i < 1000; i++)
    {
      GObject *a, *b;

      /* should see our two copies */
      a = list_model_get (G_LIST_MODEL (store), i * 2);
      b = list_model_get (G_LIST_MODEL (store), i * 2 + 1);

      g_assert (compare_items (a, b, GUINT_TO_POINTER(0x1234)) == 0);
      g_assert (a != b);

      if (i)
        {
          GObject *c;

          c = list_model_get (G_LIST_MODEL (store), i * 2 - 1);
          g_assert (c != a);
          g_assert (c != b);

          g_assert (compare_items (b, c, GUINT_TO_POINTER(0x1234)) > 0);
          g_assert (compare_items (a, c, GUINT_TO_POINTER(0x1234)) > 0);

          g_object_unref (c);
        }

      g_object_unref (a);
      g_object_unref (b);
    }

  g_object_unref (store);
}

/* Test that using splice() to replace the middle element in a list store works. */
static void
test_store_splice_replace_middle (void)
{
  GListStore *store;
  GListModel *model;
  GAction *item;
  GPtrArray *array;

  g_test_bug ("https://bugzilla.gnome.org/show_bug.cgi?id=795307");

  store = g_list_store_new (G_TYPE_SIMPLE_ACTION);
  model = G_LIST_MODEL (store);

  array = g_ptr_array_new_full (0, g_object_unref);
  g_ptr_array_add (array, g_simple_action_new ("1", NULL));
  g_ptr_array_add (array, g_simple_action_new ("2", NULL));
  g_ptr_array_add (array, g_simple_action_new ("3", NULL));
  g_ptr_array_add (array, g_simple_action_new ("4", NULL));
  g_ptr_array_add (array, g_simple_action_new ("5", NULL));

  /* Add three items through splice */
  g_list_store_splice (store, 0, 0, array->pdata, 3);
  assert_cmpitems (store, ==, 3);

  item = list_model_get (model, 0);
  g_assert_cmpstr (g_action_get_name (item), ==, "1");
  g_object_unref (item);
  item = list_model_get (model, 1);
  g_assert_cmpstr (g_action_get_name (item), ==, "2");
  g_object_unref (item);
  item = list_model_get (model, 2);
  g_assert_cmpstr (g_action_get_name (item), ==, "3");
  g_object_unref (item);

  /* Replace the middle one with two new items */
  g_list_store_splice (store, 1, 1, array->pdata + 3, 2);
  assert_cmpitems (store, ==, 4);

  item = list_model_get (model, 0);
  g_assert_cmpstr (g_action_get_name (item), ==, "1");
  g_object_unref (item);
  item = list_model_get (model, 1);
  g_assert_cmpstr (g_action_get_name (item), ==, "4");
  g_object_unref (item);
  item = list_model_get (model, 2);
  g_assert_cmpstr (g_action_get_name (item), ==, "5");
  g_object_unref (item);
  item = list_model_get (model, 3);
  g_assert_cmpstr (g_action_get_name (item), ==, "3");
  g_object_unref (item);

  g_ptr_array_unref (array);
  g_object_unref (store);
}

/* Test that using splice() to replace the whole list store works. */
static void
test_store_splice_replace_all (void)
{
  GListStore *store;
  GListModel *model;
  GPtrArray *array;
  GAction *item;

  g_test_bug ("https://bugzilla.gnome.org/show_bug.cgi?id=795307");

  store = g_list_store_new (G_TYPE_SIMPLE_ACTION);
  model = G_LIST_MODEL (store);

  array = g_ptr_array_new_full (0, g_object_unref);
  g_ptr_array_add (array, g_simple_action_new ("1", NULL));
  g_ptr_array_add (array, g_simple_action_new ("2", NULL));
  g_ptr_array_add (array, g_simple_action_new ("3", NULL));
  g_ptr_array_add (array, g_simple_action_new ("4", NULL));

  /* Add the first two */
  g_list_store_splice (store, 0, 0, array->pdata, 2);

  assert_cmpitems (store, ==, 2);
  item = list_model_get (model, 0);
  g_assert_cmpstr (g_action_get_name (item), ==, "1");
  g_object_unref (item);
  item = list_model_get (model, 1);
  g_assert_cmpstr (g_action_get_name (item), ==, "2");
  g_object_unref (item);

  /* Replace all with the last two */
  g_list_store_splice (store, 0, 2, array->pdata + 2, 2);

  assert_cmpitems (store, ==, 2);
  item = list_model_get (model, 0);
  g_assert_cmpstr (g_action_get_name (item), ==, "3");
  g_object_unref (item);
  item = list_model_get (model, 1);
  g_assert_cmpstr (g_action_get_name (item), ==, "4");
  g_object_unref (item);

  g_ptr_array_unref (array);
  g_object_unref (store);
}

/* Test that using splice() without removing or adding anything works */
static void
test_store_splice_noop (void)
{
  GListStore *store;
  GListModel *model;
  GAction *item;

  store = g_list_store_new (G_TYPE_SIMPLE_ACTION);
  model = G_LIST_MODEL (store);

  /* splice noop with an empty list */
  g_list_store_splice (store, 0, 0, NULL, 0);
  assert_cmpitems (store, ==, 0);

  /* splice noop with a non-empty list */
  item = G_ACTION (g_simple_action_new ("1", NULL));
  g_list_store_append (store, item);
  g_object_unref (item);

  g_list_store_splice (store, 0, 0, NULL, 0);
  assert_cmpitems (store, ==, 1);

  g_list_store_splice (store, 1, 0, NULL, 0);
  assert_cmpitems (store, ==, 1);

  item = list_model_get (model, 0);
  g_assert_cmpstr (g_action_get_name (item), ==, "1");
  g_object_unref (item);

  g_object_unref (store);
}

static gboolean
model_array_equal (GListModel *model, GPtrArray *array)
{
  guint i;

  if (g_list_model_get_n_items (model) != array->len)
    return FALSE;

  for (i = 0; i < array->len; i++)
    {
      GObject *ptr;
      gboolean ptrs_equal;

      ptr = list_model_get (model, i);
      ptrs_equal = (g_ptr_array_index (array, i) == ptr);
      g_object_unref (ptr);
      if (!ptrs_equal)
        return FALSE;
    }

  return TRUE;
}

/* Test that using splice() to remove multiple items at different
 * positions works */
static void
test_store_splice_remove_multiple (void)
{
  GListStore *store;
  GListModel *model;
  GPtrArray *array;

  store = g_list_store_new (G_TYPE_SIMPLE_ACTION);
  model = G_LIST_MODEL (store);

  array = g_ptr_array_new_full (0, g_object_unref);
  g_ptr_array_add (array, g_simple_action_new ("1", NULL));
  g_ptr_array_add (array, g_simple_action_new ("2", NULL));
  g_ptr_array_add (array, g_simple_action_new ("3", NULL));
  g_ptr_array_add (array, g_simple_action_new ("4", NULL));
  g_ptr_array_add (array, g_simple_action_new ("5", NULL));
  g_ptr_array_add (array, g_simple_action_new ("6", NULL));
  g_ptr_array_add (array, g_simple_action_new ("7", NULL));
  g_ptr_array_add (array, g_simple_action_new ("8", NULL));
  g_ptr_array_add (array, g_simple_action_new ("9", NULL));
  g_ptr_array_add (array, g_simple_action_new ("10", NULL));

  /* Add all */
  g_list_store_splice (store, 0, 0, array->pdata, array->len);
  g_assert_true (model_array_equal (model, array));

  /* Remove the first two */
  g_list_store_splice (store, 0, 2, NULL, 0);
  g_assert_false (model_array_equal (model, array));
  g_ptr_array_remove_range (array, 0, 2);
  g_assert_true (model_array_equal (model, array));
  assert_cmpitems (store, ==, 8);

  /* Remove two in the middle */
  g_list_store_splice (store, 2, 2, NULL, 0);
  g_assert_false (model_array_equal (model, array));
  g_ptr_array_remove_range (array, 2, 2);
  g_assert_true (model_array_equal (model, array));
  assert_cmpitems (store, ==, 6);

  /* Remove two at the end */
  g_list_store_splice (store, 4, 2, NULL, 0);
  g_assert_false (model_array_equal (model, array));
  g_ptr_array_remove_range (array, 4, 2);
  g_assert_true (model_array_equal (model, array));
  assert_cmpitems (store, ==, 4);

  g_ptr_array_unref (array);
  g_object_unref (store);
}

/* Test that using splice() to add multiple items at different
 * positions works */
static void
test_store_splice_add_multiple (void)
{
  GListStore *store;
  GListModel *model;
  GPtrArray *array;

  store = g_list_store_new (G_TYPE_SIMPLE_ACTION);
  model = G_LIST_MODEL (store);

  array = g_ptr_array_new_full (0, g_object_unref);
  g_ptr_array_add (array, g_simple_action_new ("1", NULL));
  g_ptr_array_add (array, g_simple_action_new ("2", NULL));
  g_ptr_array_add (array, g_simple_action_new ("3", NULL));
  g_ptr_array_add (array, g_simple_action_new ("4", NULL));
  g_ptr_array_add (array, g_simple_action_new ("5", NULL));
  g_ptr_array_add (array, g_simple_action_new ("6", NULL));

  /* Add two at the beginning */
  g_list_store_splice (store, 0, 0, array->pdata, 2);

  /* Add two at the end */
  g_list_store_splice (store, 2, 0, array->pdata + 4, 2);

  /* Add two in the middle */
  g_list_store_splice (store, 2, 0, array->pdata + 2, 2);

  g_assert_true (model_array_equal (model, array));

  g_ptr_array_unref (array);
  g_object_unref (store);
}

/* Test that get_item_type() returns the right type */
static void
test_store_item_type (void)
{
  GListStore *store;
  GType gtype;

  store = g_list_store_new (G_TYPE_SIMPLE_ACTION);
  gtype = g_list_model_get_item_type (G_LIST_MODEL (store));
  g_assert (gtype == G_TYPE_SIMPLE_ACTION);

  g_object_unref (store);
}

/* Test that remove_all() removes all items */
static void
test_store_remove_all (void)
{
  GListStore *store;
  GSimpleAction *item;

  store = g_list_store_new (G_TYPE_SIMPLE_ACTION);

  /* Test with an empty list */
  g_list_store_remove_all (store);
  assert_cmpitems (store, ==, 0);

  /* Test with a non-empty list */
  item = g_simple_action_new ("42", NULL);
  g_list_store_append (store, item);
  g_list_store_append (store, item);
  g_object_unref (item);
  assert_cmpitems (store, ==, 2);
  g_list_store_remove_all (store);
  assert_cmpitems (store, ==, 0);

  g_object_unref (store);
}

/* Test that splice() logs an error when passed the wrong item type */
static void
test_store_splice_wrong_type (void)
{
  GListStore *store;

  store = g_list_store_new (G_TYPE_SIMPLE_ACTION);

  g_test_expect_message (G_LOG_DOMAIN,
                         G_LOG_LEVEL_CRITICAL,
                         "*GListStore instead of a GSimpleAction*");
  g_list_store_splice (store, 0, 0, (gpointer)&store, 1);

  g_object_unref (store);
}

static gint
cmp_action_by_name (GAction *a, GAction *b, gpointer user_data)
{
  return g_strcmp0 (g_action_get_name (a), g_action_get_name (b));
}

/* Test if sort() works */
static void
test_store_sort (void)
{
  GListStore *store;
  GListModel *model;
  GPtrArray *array;

  store = g_list_store_new (G_TYPE_SIMPLE_ACTION);
  model = G_LIST_MODEL (store);

  array = g_ptr_array_new_full (0, g_object_unref);
  g_ptr_array_add (array, g_simple_action_new ("2", NULL));
  g_ptr_array_add (array, g_simple_action_new ("3", NULL));
  g_ptr_array_add (array, g_simple_action_new ("9", NULL));
  g_ptr_array_add (array, g_simple_action_new ("4", NULL));
  g_ptr_array_add (array, g_simple_action_new ("5", NULL));
  g_ptr_array_add (array, g_simple_action_new ("8", NULL));
  g_ptr_array_add (array, g_simple_action_new ("6", NULL));
  g_ptr_array_add (array, g_simple_action_new ("7", NULL));
  g_ptr_array_add (array, g_simple_action_new ("1", NULL));

  /* Sort an empty list */
  g_list_store_sort (store, (GCompareDataFunc) cmp_action_by_name, NULL);

  /* Add all */
  g_list_store_splice (store, 0, 0, array->pdata, array->len);
  g_assert_true (model_array_equal (model, array));

  /* Sort both and check if the result is the same */
  g_ptr_array_sort_values (array, (GCompareFunc)cmp_action_by_name);
  g_assert_false (model_array_equal (model, array));
  g_list_store_sort (store, (GCompareDataFunc) cmp_action_by_name, NULL);
  g_assert_true (model_array_equal (model, array));

  g_ptr_array_unref (array);
  g_object_unref (store);
}

/* Test the cases where the item store tries to speed up item access by caching
 * the last iter/position */
static void
test_store_get_item_cache (void)
{
  GListStore *store;
  GListModel *model;
  GSimpleAction *item1, *item2, *temp;

  store = g_list_store_new (G_TYPE_SIMPLE_ACTION);
  model = G_LIST_MODEL (store);

  /* Add two */
  item1 = g_simple_action_new ("1", NULL);
  g_list_store_append (store, item1);
  item2 = g_simple_action_new ("2", NULL);
  g_list_store_append (store, item2);

  /* Clear the cache */
  g_assert_null (list_model_get (model, 42));

  /* Access the same position twice */
  temp = list_model_get (model, 1);
  g_assert (temp == item2);
  g_object_unref (temp);
  temp = list_model_get (model, 1);
  g_assert (temp == item2);
  g_object_unref (temp);

  g_assert_null (list_model_get (model, 42));

  /* Access forwards */
  temp = list_model_get (model, 0);
  g_assert (temp == item1);
  g_object_unref (temp);
  temp = list_model_get (model, 1);
  g_assert (temp == item2);
  g_object_unref (temp);

  g_assert_null (list_model_get (model, 42));

  /* Access backwards */
  temp = list_model_get (model, 1);
  g_assert (temp == item2);
  g_object_unref (temp);
  temp = list_model_get (model, 0);
  g_assert (temp == item1);
  g_object_unref (temp);

  g_object_unref (item1);
  g_object_unref (item2);
  g_object_unref (store);
}

struct ItemsChangedData
{
  guint position;
  guint removed;
  guint added;
  gboolean called;
  gboolean notified;
};

static void
expect_items_changed (struct ItemsChangedData *expected,
                      guint position,
                      guint removed,
                      guint added)
{
  expected->position = position;
  expected->removed = removed;
  expected->added = added;
  expected->called = FALSE;
  expected->notified = FALSE;
}

static void
on_items_changed (GListModel *model,
                  guint position,
                  guint removed,
                  guint added,
                  struct ItemsChangedData *expected)
{
  g_assert_false (expected->called);
  g_assert_cmpuint (expected->position, ==, position);
  g_assert_cmpuint (expected->removed, ==, removed);
  g_assert_cmpuint (expected->added, ==, added);
  expected->called = TRUE;
}

static void
on_notify_n_items (GListModel *model,
                   GParamSpec *pspec,
                   struct ItemsChangedData *expected)
{
  g_assert_false (expected->notified);
  expected->notified = TRUE;
}

/* Test that all operations on the list emit the items-changed signal */
static void
test_store_signal_items_changed (void)
{
  GListStore *store;
  GListModel *model;
  GSimpleAction *item;
  struct ItemsChangedData expected = {0};

  store = g_list_store_new (G_TYPE_SIMPLE_ACTION);
  model = G_LIST_MODEL (store);

  g_object_connect (model, "signal::items-changed",
                    on_items_changed, &expected, NULL);
  g_object_connect (model, "signal::notify::n-items",
                    on_notify_n_items, &expected, NULL);

  /* Emit the signal manually */
  expect_items_changed (&expected, 0, 0, 0);
  g_list_model_items_changed (model, 0, 0, 0);
  g_assert_true (expected.called);
  g_assert_false (expected.notified);

  /* Append an item */
  expect_items_changed (&expected, 0, 0, 1);
  item = g_simple_action_new ("2", NULL);
  g_list_store_append (store, item);
  g_object_unref (item);
  g_assert_true (expected.called);
  g_assert_true (expected.notified);

  /* Insert an item */
  expect_items_changed (&expected, 1, 0, 1);
  item = g_simple_action_new ("1", NULL);
  g_list_store_insert (store, 1, item);
  g_object_unref (item);
  g_assert_true (expected.called);
  g_assert_true (expected.notified);

  /* Sort the list */
  expect_items_changed (&expected, 0, 2, 2);
  g_list_store_sort (store,
                     (GCompareDataFunc) cmp_action_by_name,
                     NULL);
  g_assert_true (expected.called);
  g_assert_false (expected.notified);

  /* Insert sorted */
  expect_items_changed (&expected, 2, 0, 1);
  item = g_simple_action_new ("3", NULL);
  g_list_store_insert_sorted (store,
                              item,
                              (GCompareDataFunc) cmp_action_by_name,
                              NULL);
  g_object_unref (item);
  g_assert_true (expected.called);
  g_assert_true (expected.notified);

  /* Remove an item */
  expect_items_changed (&expected, 1, 1, 0);
  g_list_store_remove (store, 1);
  g_assert_true (expected.called);
  g_assert_true (expected.notified);

  /* Splice */
  expect_items_changed (&expected, 0, 2, 1);
  item = g_simple_action_new ("4", NULL);
  assert_cmpitems (store, >=, 2);
  g_list_store_splice (store, 0, 2, (gpointer)&item, 1);
  g_object_unref (item);
  g_assert_true (expected.called);
  g_assert_true (expected.notified);

  /* Splice to replace */
  expect_items_changed (&expected, 0, 1, 1);
  item = g_simple_action_new ("5", NULL);
  assert_cmpitems (store, >=, 1);
  g_list_store_splice (store, 0, 1, (gpointer)&item, 1);
  g_object_unref (item);
  g_assert_true (expected.called);
  g_assert_false (expected.notified);

  /* Remove all */
  expect_items_changed (&expected, 0, 1, 0);
  assert_cmpitems (store, ==, 1);
  g_list_store_remove_all (store);
  g_assert_true (expected.called);
  g_assert_true (expected.notified);

  g_object_unref (store);
}

/* Due to an overflow in the list store last-iter optimization,
 * the sequence 'lookup 0; lookup MAXUINT' was returning the
 * same item twice, and not NULL for the second lookup.
 * See #1639.
 */
static void
test_store_past_end (void)
{
  GListStore *store;
  GListModel *model;
  GSimpleAction *item;

  store = g_list_store_new (G_TYPE_SIMPLE_ACTION);
  model = G_LIST_MODEL (store);

  item = g_simple_action_new ("2", NULL);
  g_list_store_append (store, item);
  g_object_unref (item);

  assert_cmpitems (store, ==, 1);
  item = g_list_model_get_item (model, 0);
  g_assert_nonnull (item);
  g_object_unref (item);
  item = g_list_model_get_item (model, G_MAXUINT);
  g_assert_null (item);

  g_object_unref (store);
}

static gboolean
list_model_casecmp_action_by_name (gconstpointer a,
                                   gconstpointer b)
{
  return g_ascii_strcasecmp (g_action_get_name (G_ACTION (a)),
                             g_action_get_name (G_ACTION (b))) == 0;
}

static gboolean
list_model_casecmp_action_by_name_full (gconstpointer a,
                                        gconstpointer b,
                                        gpointer      user_data)
{
  char buf[4];
  const char *suffix = user_data;

  g_snprintf (buf, sizeof buf, "%s%s", g_action_get_name (G_ACTION (b)), suffix);
  return g_strcmp0 (g_action_get_name (G_ACTION (a)), buf) == 0;
}

/* Test if find() and find_with_equal_func() works */
static void
test_store_find (void)
{
  GListStore *store;
  guint position = 100;
  const gchar *item_strs[4] = { "aaa", "bbb", "xxx", "ccc" };
  GSimpleAction *items[4] = { NULL, };
  GSimpleAction *other_item;
  guint i;

  store = g_list_store_new (G_TYPE_SIMPLE_ACTION);

  for (i = 0; i < G_N_ELEMENTS (item_strs); i++)
    items[i] = g_simple_action_new (item_strs[i], NULL);

  /* Shouldn't crash on an empty list, or change the position pointer */
  g_assert_false (g_list_store_find (store, items[0], NULL));
  g_assert_false (g_list_store_find (store, items[0], &position));
  g_assert_cmpint (position, ==, 100);

  for (i = 0; i < G_N_ELEMENTS (item_strs); i++)
    g_list_store_append (store, items[i]);

  /* Check whether it could still find the the elements */
  for (i = 0; i < G_N_ELEMENTS (item_strs); i++)
    {
      g_assert_true (g_list_store_find (store, items[i], &position));
      g_assert_cmpint (position, ==, i);
      /* Shouldn't try to write to position pointer if NULL given */
      g_assert_true (g_list_store_find (store, items[i], NULL));
    }

  /* try to find element not part of the list */
  other_item = g_simple_action_new ("111", NULL);
  g_assert_false (g_list_store_find (store, other_item, NULL));
  g_clear_object (&other_item);

  /* Re-add item; find() should only return the first position */
  g_list_store_append (store, items[0]);
  g_assert_true (g_list_store_find (store, items[0], &position));
  g_assert_cmpint (position, ==, 0);

  /* try to find element which should only work with custom equality check */
  other_item = g_simple_action_new ("XXX", NULL);
  g_assert_false (g_list_store_find (store, other_item, NULL));
  g_assert_true (g_list_store_find_with_equal_func (store,
                                                    other_item,
                                                    list_model_casecmp_action_by_name,
                                                    &position));
  g_assert_cmpint (position, ==, 2);
  g_clear_object (&other_item);

  /* try to find element which should only work with custom equality check and string concat */
  other_item = g_simple_action_new ("c", NULL);
  g_assert_false (g_list_store_find (store, other_item, NULL));
  g_assert_true (g_list_store_find_with_equal_func_full (store,
                                                         other_item,
                                                         list_model_casecmp_action_by_name_full,
                                                         "cc",
                                                         &position));
  g_assert_cmpint (position, ==, 3);
  g_clear_object (&other_item);

  for (i = 0; i < G_N_ELEMENTS (item_strs); i++)
    g_clear_object(&items[i]);
  g_clear_object (&store);
}

int main (int argc, char *argv[])
{
  g_test_init (&argc, &argv, NULL);

  g_test_add_func ("/glistmodel/store/properties", test_store_properties);
  g_test_add_func ("/glistmodel/store/non-gobjects", test_store_non_gobjects);
  g_test_add_func ("/glistmodel/store/boundaries", test_store_boundaries);
  g_test_add_func ("/glistmodel/store/refcounts", test_store_refcounts);
  g_test_add_func ("/glistmodel/store/sorted", test_store_sorted);
  g_test_add_func ("/glistmodel/store/splice-replace-middle",
                   test_store_splice_replace_middle);
  g_test_add_func ("/glistmodel/store/splice-replace-all",
                   test_store_splice_replace_all);
  g_test_add_func ("/glistmodel/store/splice-noop", test_store_splice_noop);
  g_test_add_func ("/glistmodel/store/splice-remove-multiple",
                   test_store_splice_remove_multiple);
  g_test_add_func ("/glistmodel/store/splice-add-multiple",
                   test_store_splice_add_multiple);
  g_test_add_func ("/glistmodel/store/splice-wrong-type",
                   test_store_splice_wrong_type);
  g_test_add_func ("/glistmodel/store/item-type",
                   test_store_item_type);
  g_test_add_func ("/glistmodel/store/remove-all",
                   test_store_remove_all);
  g_test_add_func ("/glistmodel/store/sort",
                   test_store_sort);
  g_test_add_func ("/glistmodel/store/get-item-cache",
                   test_store_get_item_cache);
  g_test_add_func ("/glistmodel/store/items-changed",
                   test_store_signal_items_changed);
  g_test_add_func ("/glistmodel/store/past-end", test_store_past_end);
  g_test_add_func ("/glistmodel/store/find", test_store_find);

  return g_test_run ();
}