/*
 * Copyright © 2025 Khalid Abu Shawarib <kas@gnome.org>
 *
 * SPDX-License-Identifier: GPL-3.0-or-later
 */
#define G_LOG_DOMAIN "test-files-view"

#include <test-utilities.h>

#include <nautilus-application.h>
#include <nautilus-enums.h>
#include <nautilus-file.h>
#include <nautilus-file-undo-manager.h>
#include <nautilus-file-utilities.h>
#include <nautilus-files-view.h>
#include <nautilus-global-preferences.h>
#include <nautilus-list-base.h>
#include <nautilus-resources.h>
#include <nautilus-query.h>
#include <nautilus-tag-manager.h>
#include <nautilus-view-info.h>
#include <nautilus-view-model.h>
#include <nautilus-window-slot.h>

#define G_LOG_DOMAIN "test-files-view"

static gboolean
ptr_arrays_equal_unordered (GPtrArray *a,
                            GPtrArray *b)
{
    if (a->len != b->len)
    {
        return FALSE;
    }

    for (guint i = 0; i < a->len; i++)
    {
        if (!g_ptr_array_find (b, a->pdata[i], NULL))
        {
            return FALSE;
        }
    }

    return TRUE;
}

static gboolean
ptr_array_is_subset (GPtrArray *subset,
                     GPtrArray *set)
{
    if (subset->len > set->len)
    {
        return FALSE;
    }

    for (guint i = 0; i < subset->len; i++)
    {
        if (!g_ptr_array_find (set, subset->pdata[i], NULL))
        {
            return FALSE;
        }
    }

    return TRUE;
}

static void
set_true (gboolean *data)
{
    *data = TRUE;
}

static void
collect_renamed_files (NautilusFile *file,
                       GFile        *result_location,
                       GError       *error,
                       gpointer      callback_data)
{
    GPtrArray *renamed_files_arr = callback_data;

    g_ptr_array_add (renamed_files_arr, nautilus_file_ref (file));
}

static void
collect_changed_files (NautilusFilesView *view,
                       NautilusFile      *file,
                       NautilusDirectory *directory,
                       GPtrArray         *data_files)
{
    /* Ignore duplicate change emissions originating from nautilus-directory */
    if (!g_ptr_array_find (data_files, file, NULL))
    {
        g_ptr_array_add (data_files, nautilus_file_ref (file));
    }
}

static void
file_changes_done (NautilusFilesView *view,
                   gpointer           user_data)
{
    gboolean *end_of_changes = user_data;

    *end_of_changes = TRUE;
}

static void
run_view_zoom_testcase (GSettings   *view_setting,
                        const gchar *zoom_setting_key,
                        uint         default_zoom_level,
                        uint         view_id)
{
    g_settings_set_enum (view_setting, zoom_setting_key, default_zoom_level);

    g_autoptr (NautilusWindowSlot) slot = nautilus_window_slot_new (NAUTILUS_MODE_BROWSE);
    g_autoptr (NautilusFilesView) files_view = nautilus_files_view_new (view_id, slot);
    g_autoptr (GFile) tmp_location = g_file_new_for_path (test_get_tmp_dir ());
    NautilusListBase *list_base = nautilus_files_view_get_private_list_base (files_view);
    NautilusViewInfo view_info = nautilus_list_base_get_view_info (list_base);
    const guint file_count = 10;

    create_multiple_files ("zoom_test", file_count);

    nautilus_files_view_set_location (files_view, tmp_location);
    ITER_CONTEXT_WHILE (nautilus_files_view_get_loading (files_view));

    GActionGroup *group = nautilus_files_view_get_private_action_group (files_view);
    GAction *action_zoom_in = g_action_map_lookup_action (G_ACTION_MAP (group), "zoom-in");
    GAction *action_zoom_out = g_action_map_lookup_action (G_ACTION_MAP (group), "zoom-out");
    GAction *action_zoom_standard = g_action_map_lookup_action (G_ACTION_MAP (group),
                                                                "zoom-standard");

    /* Zoom in until we reach the max (action becomes disabled) */
    while (g_action_get_enabled (action_zoom_in))
    {
        gtk_widget_activate_action (GTK_WIDGET (files_view), "view.zoom-in", NULL);
        g_assert_true (g_action_get_enabled (action_zoom_out));
    }
    g_assert_true (g_action_get_enabled (action_zoom_standard));
    g_assert_cmpint (g_settings_get_enum (view_setting, zoom_setting_key),
                     ==,
                     view_info.zoom_level_max);

    /* Zoom out until we reach the max (action becomes disabled) */
    while (g_action_get_enabled (action_zoom_out))
    {
        gtk_widget_activate_action (GTK_WIDGET (files_view), "view.zoom-out", NULL);
        g_assert_true (g_action_get_enabled (action_zoom_in));
    }
    g_assert_true (g_action_get_enabled (action_zoom_standard));
    g_assert_cmpint (g_settings_get_enum (view_setting, zoom_setting_key),
                     ==,
                     view_info.zoom_level_min);

    /* Now set to standard and ensure the standard action becomes disabled */
    gtk_widget_activate_action (GTK_WIDGET (files_view), "view.zoom-standard", NULL);
    g_assert_true (g_action_get_enabled (action_zoom_in));
    g_assert_true (g_action_get_enabled (action_zoom_out));
    g_assert_false (g_action_get_enabled (action_zoom_standard));
    g_assert_cmpint (g_settings_get_enum (view_setting, zoom_setting_key),
                     ==,
                     view_info.zoom_level_standard);

    test_clear_tmp_dir ();
}

static void
test_zoom_actions (void)
{
    struct
    {
        GSettings *view_setting;
        const gchar *zoom_setting_key;
        uint default_zoom_level;
        uint view_id;
    } testcases[] =
    {
        {
            nautilus_list_view_preferences,
            NAUTILUS_PREFERENCES_LIST_VIEW_DEFAULT_ZOOM_LEVEL,
            NAUTILUS_LIST_ZOOM_LEVEL_MEDIUM,
            NAUTILUS_VIEW_LIST_ID,
        },
        {
            nautilus_icon_view_preferences,
            NAUTILUS_PREFERENCES_ICON_VIEW_DEFAULT_ZOOM_LEVEL,
            NAUTILUS_GRID_ZOOM_LEVEL_MEDIUM,
            NAUTILUS_VIEW_GRID_ID,
        },
    };

    for (uint i = 0; i < G_N_ELEMENTS (testcases); i++)
    {
        run_view_zoom_testcase (testcases[i].view_setting,
                                testcases[i].zoom_setting_key,
                                testcases[i].default_zoom_level,
                                testcases[i].view_id);
    }
}

const GStrv hidden_files_hierarchy = (char *[])
{
    "my_file",
    "my.file",
    "my_file.",
    "~my_file",
    "hidden_temp_file~",
    ".my_hidden_file",
    ".very_hidden_file~",
    "my_indirectly_hidden_file",
    "my_indirectly_hidden_tmp_file~",
    ".hidden",
    NULL
};

static void
test_selection_actions (void)
{
    g_autoptr (NautilusWindowSlot) slot = nautilus_window_slot_new (NAUTILUS_MODE_BROWSE);
    g_autoptr (NautilusFilesView) files_view = nautilus_files_view_new (NAUTILUS_VIEW_GRID_ID, slot);
    NautilusViewModel *model = nautilus_files_view_get_private_model (files_view);
    g_autoptr (GFile) tmp_location = g_file_new_for_path (test_get_tmp_dir ());
    const guint file_count = 10, selected_count = 4;
    g_autoptr (NautilusFileList) selection_set = NULL;
    NautilusFileList *got_selection = NULL;
    guint got_count;

    /* Need to subtract one since it creates an extra directory. */
    create_multiple_files ("select_test", file_count - 1);

    nautilus_files_view_set_location (files_view, tmp_location);
    ITER_CONTEXT_WHILE (nautilus_files_view_get_loading (files_view));

    for (guint i = 0; i < selected_count; i++)
    {
        g_autofree gchar *name = g_strdup_printf ("select_test_file_%u", i);
        g_autoptr (GFile) location = g_file_get_child (tmp_location, name);
        g_autoptr (NautilusFile) file = nautilus_file_get (location);

        selection_set = g_list_append (selection_set, nautilus_file_ref (file));
    }

    /* Set the selection on the view */
    nautilus_files_view_set_selection (files_view,
                                       selection_set,
                                       NAUTILUS_SELECTION_SOURCE_AUTO);

    /* Retrieve selection and verify it matches what we set */
    got_selection = nautilus_files_view_get_selection (files_view);
    got_count = g_list_length (got_selection);

    g_assert_cmpint (nautilus_files_view_get_selection_source (files_view),
                     ==,
                     NAUTILUS_SELECTION_SOURCE_AUTO);
    g_assert_cmpint (got_count, ==, selected_count);

    for (NautilusFileList *l = got_selection; l != NULL; l = l->next)
    {
        NautilusFile *file = l->data;
        NautilusViewItem *item = nautilus_view_model_get_item_for_file (model, file);

        g_assert_nonnull (item);
        g_assert_nonnull (g_list_find (selection_set, file));
    }
    g_clear_pointer (&got_selection, nautilus_file_list_free);

    /* Invert selection */
    gtk_widget_activate_action (GTK_WIDGET (files_view), "view.invert-selection", NULL);
    got_selection = nautilus_files_view_get_selection (files_view);
    got_count = g_list_length (got_selection);

    g_assert_cmpint (nautilus_files_view_get_selection_source (files_view),
                     ==,
                     NAUTILUS_SELECTION_SOURCE_MANUAL);
    g_assert_cmpint (got_count, ==, file_count - selected_count);

    for (NautilusFileList *l = got_selection; l != NULL; l = l->next)
    {
        NautilusFile *file = l->data;
        NautilusViewItem *item = nautilus_view_model_get_item_for_file (model, file);

        g_assert_nonnull (item);
        g_assert_null (g_list_find (selection_set, file));
    }
    g_clear_pointer (&got_selection, nautilus_file_list_free);

    /* Select all */
    gtk_widget_activate_action (GTK_WIDGET (files_view), "view.select-all", NULL);
    got_selection = nautilus_files_view_get_selection (files_view);
    got_count = g_list_length (got_selection);

    g_assert_cmpint (nautilus_files_view_get_selection_source (files_view),
                     ==,
                     NAUTILUS_SELECTION_SOURCE_MANUAL);
    g_assert_cmpint (got_count, ==, file_count);
    g_assert_cmpint (got_count, ==, g_list_model_get_n_items (G_LIST_MODEL (model)));
    g_clear_pointer (&got_selection, nautilus_file_list_free);

    /* Invert selection to clear selection. */
    gtk_widget_activate_action (GTK_WIDGET (files_view), "view.invert-selection", NULL);
    got_selection = nautilus_files_view_get_selection (files_view);

    g_assert_cmpint (nautilus_files_view_get_selection_source (files_view),
                     ==,
                     NAUTILUS_SELECTION_SOURCE_MANUAL);
    g_assert_null (got_selection);
    g_clear_pointer (&got_selection, nautilus_file_list_free);

    test_clear_tmp_dir ();
}

static void
test_selection_source_in_search (void)
{
    g_autoptr (NautilusWindowSlot) slot = nautilus_window_slot_new (NAUTILUS_MODE_BROWSE);
    g_autoptr (NautilusFilesView) files_view = nautilus_files_view_new (NAUTILUS_VIEW_GRID_ID, slot);
    NautilusViewModel *model = nautilus_files_view_get_private_model (files_view);
    g_autoptr (GFile) tmp_location = g_file_new_for_path (test_get_tmp_dir ());
    const guint file_count = 10;
    g_autoptr (NautilusQuery) search_query = nautilus_query_new ();
    NautilusFileList *got_selection = NULL;

    /* Need to subtract one since it creates an extra directory. */
    create_multiple_files ("select_test", file_count - 1);

    nautilus_files_view_set_location (files_view, tmp_location);
    ITER_CONTEXT_WHILE (nautilus_files_view_get_loading (files_view));

    g_assert_cmpint (g_list_model_get_n_items (G_LIST_MODEL (model)), ==, file_count);

    nautilus_query_set_text (search_query, "2");
    nautilus_query_set_location (search_query, tmp_location);
    nautilus_files_view_set_search_query (files_view, search_query);
    ITER_CONTEXT_WHILE (nautilus_files_view_get_loading (files_view));

    g_assert_cmpint (nautilus_files_view_get_selection_source (files_view),
                     ==,
                     NAUTILUS_SELECTION_SOURCE_IN_SEARCH);

    got_selection = nautilus_files_view_get_selection (files_view);
    g_assert_cmpint (g_list_length (got_selection), ==, 1);
    g_clear_pointer (&got_selection, nautilus_file_list_free);

    /* Cannot check for NAUTILUS_SELECTION_SOURCE_AFTER_SEARCH because it is
     * set through selection restoration through the slot. */

    test_clear_tmp_dir ();
}

static void
test_selection_source_operation (void)
{
    GStrv files_hierarchy = (char *[]) {
        "file_1",
        "file_2",
        "file_3",
        "file_4",
        NULL
    };
    g_autoptr (NautilusWindowSlot) slot = nautilus_window_slot_new (NAUTILUS_MODE_BROWSE);
    g_autoptr (NautilusFilesView) files_view = nautilus_files_view_new (NAUTILUS_VIEW_GRID_ID, slot);
    NautilusViewModel *model = nautilus_files_view_get_private_model (files_view);
    g_autoptr (GFile) tmp_location = g_file_new_for_path (test_get_tmp_dir ());
    const guint file_count = 4;
    const guint selected_count = 2;
    g_autolist (GFile) file_list = NULL;
    g_autoptr (NautilusFileList) first_selection = NULL;
    g_autoptr (NautilusFileList) second_selection = NULL;
    NautilusFileList *got_selection = NULL;

    file_hierarchy_create (files_hierarchy, "");

    nautilus_files_view_set_location (files_view, tmp_location);
    ITER_CONTEXT_WHILE (nautilus_files_view_get_loading (files_view));

    g_assert_cmpint (g_list_model_get_n_items (G_LIST_MODEL (model)), ==, file_count);

    /* Build two disjoint selection sets. */
    file_list = file_hierarchy_get_files_list (files_hierarchy, "", TRUE);

    for (guint i = 0; i < selected_count; i++)
    {
        GFile *location = g_list_nth_data (file_list, i);
        g_autoptr (NautilusFile) file = nautilus_file_get (location);

        first_selection = g_list_append (first_selection, g_steal_pointer (&file));
    }
    for (guint i = selected_count; i < file_count; i++)
    {
        GFile *location = g_list_nth_data (file_list, i);
        g_autoptr (NautilusFile) file = nautilus_file_get (location);

        second_selection = g_list_append (second_selection, g_steal_pointer (&file));
    }

    /* Set a selection with MANUAL source, then start a copy operation, then
     * wait for the operation to complete, verifying the selection source
     * transitions correctly at each step.
     */

    /*
     * #1: No manual selection before the operation ends
     */
    nautilus_files_view_set_selection (files_view,
                                       first_selection,
                                       NAUTILUS_SELECTION_SOURCE_MANUAL);
    g_assert_cmpint (nautilus_files_view_get_selection_source (files_view),
                     ==,
                     NAUTILUS_SELECTION_SOURCE_MANUAL);
    got_selection = nautilus_files_view_get_selection (files_view);
    g_assert_cmpint (g_list_length (got_selection), ==, selected_count);
    g_clear_pointer (&got_selection, nautilus_file_list_free);

    gtk_widget_activate_action (GTK_WIDGET (files_view), "view.copy", NULL);
    gtk_widget_activate_action (GTK_WIDGET (files_view), "view.paste", NULL);
    got_selection = nautilus_files_view_get_selection (files_view);
    g_assert_cmpint (g_list_length (got_selection), ==, selected_count);
    g_assert_cmpint (nautilus_files_view_get_selection_source (files_view),
                     ==,
                     NAUTILUS_SELECTION_SOURCE_OP_START);
    g_clear_pointer (&got_selection, nautilus_file_list_free);

    ITER_CONTEXT_WHILE (nautilus_files_view_get_selection_source (files_view) !=
                        NAUTILUS_SELECTION_SOURCE_OP_DONE);
    got_selection = nautilus_files_view_get_selection (files_view);
    g_assert_cmpint (g_list_length (got_selection), ==, selected_count);
    g_clear_pointer (&got_selection, nautilus_file_list_free);

    /*
     * #2: Manual selection before the operation ends
     */
    nautilus_files_view_set_selection (files_view,
                                       first_selection,
                                       NAUTILUS_SELECTION_SOURCE_MANUAL);
    g_assert_cmpint (nautilus_files_view_get_selection_source (files_view),
                     ==,
                     NAUTILUS_SELECTION_SOURCE_MANUAL);
    got_selection = nautilus_files_view_get_selection (files_view);
    g_assert_cmpint (g_list_length (got_selection), ==, selected_count);
    g_clear_pointer (&got_selection, nautilus_file_list_free);

    gtk_widget_activate_action (GTK_WIDGET (files_view), "view.copy", NULL);
    gtk_widget_activate_action (GTK_WIDGET (files_view), "view.paste", NULL);
    got_selection = nautilus_files_view_get_selection (files_view);
    g_assert_cmpint (g_list_length (got_selection), ==, selected_count);
    g_assert_cmpint (nautilus_files_view_get_selection_source (files_view),
                     ==,
                     NAUTILUS_SELECTION_SOURCE_OP_START);
    g_clear_pointer (&got_selection, nautilus_file_list_free);

    nautilus_files_view_set_selection (files_view,
                                       second_selection,
                                       NAUTILUS_SELECTION_SOURCE_MANUAL);
    g_assert_cmpint (nautilus_files_view_get_selection_source (files_view),
                     ==,
                     NAUTILUS_SELECTION_SOURCE_MANUAL);

    ITER_CONTEXT_WHILE (nautilus_file_undo_manager_get_state () ==
                        NAUTILUS_FILE_UNDO_MANAGER_STATE_NONE);
    got_selection = nautilus_files_view_get_selection (files_view);
    g_assert_cmpint (g_list_length (got_selection), ==, selected_count);
    g_assert_cmpint (nautilus_files_view_get_selection_source (files_view),
                     ==,
                     NAUTILUS_SELECTION_SOURCE_MANUAL);
    g_clear_pointer (&got_selection, nautilus_file_list_free);

    test_clear_tmp_dir ();
}

static void
create_hidden_files (void)
{
    g_autoptr (GFile) hidden_list_file = g_file_new_build_filename (test_get_tmp_dir (),
                                                                    ".hidden",
                                                                    NULL);
    const gchar *content = "my_indirectly_hidden_file\n"
                           "my_indirectly_hidden_tmp_file~\n";
    g_autoptr (GError) error = NULL;

    file_hierarchy_create (hidden_files_hierarchy, "");

    g_file_replace_contents (hidden_list_file,
                             content,
                             strlen (content),
                             NULL,
                             FALSE,
                             G_FILE_CREATE_NONE,
                             NULL,
                             NULL,
                             &error);
    g_assert_no_error (error);
}

static void
test_hidden_files_renamed (void)
{
    g_settings_set_boolean (gtk_filechooser_preferences,
                            NAUTILUS_PREFERENCES_SHOW_HIDDEN_FILES,
                            FALSE);

    g_autoptr (NautilusWindowSlot) slot = nautilus_window_slot_new (NAUTILUS_MODE_BROWSE);
    g_autoptr (NautilusFilesView) files_view = nautilus_files_view_new (NAUTILUS_VIEW_GRID_ID, slot);
    NautilusViewModel *model = nautilus_files_view_get_private_model (files_view);
    g_autoptr (GFile) tmp_location = g_file_new_for_path (test_get_tmp_dir ());
    const guint renamed_file_count = 4;
    g_autoptr (GPtrArray) renamed_files_arr = g_ptr_array_new_full (renamed_file_count,
                                                                    (GDestroyNotify) nautilus_file_unref);
    g_autoptr (GPtrArray) changed_files_arr = g_ptr_array_new_full (renamed_file_count,
                                                                    (GDestroyNotify) nautilus_file_unref);

    create_hidden_files ();

    /* Load the directory with hidden files not shown. */
    nautilus_files_view_set_location (NAUTILUS_FILES_VIEW (files_view), tmp_location);
    ITER_CONTEXT_WHILE (nautilus_files_view_get_loading (files_view));

    /* Rename a few hidden files to visible names and verify they appear. */
    g_signal_connect (files_view, "file-changed",
                      G_CALLBACK (collect_changed_files), changed_files_arr);

    for (guint i = 0, renamed = 0; hidden_files_hierarchy[i] != NULL && renamed < renamed_file_count; i++)
    {
        const gchar *orig_name = hidden_files_hierarchy[i];

        /* Only pick hidden entries */
        if (strstr (orig_name, "hidden") == NULL)
        {
            continue;
        }

        g_autoptr (GFile) location = g_file_new_build_filename (test_get_tmp_dir (), orig_name, NULL);
        g_autoptr (NautilusFile) file = nautilus_file_get (location);
        NautilusViewItem *item = nautilus_view_model_get_item_for_file (model, file);
        g_autofree gchar *new_name = g_strdup_printf ("renamed_visible_%u", renamed);

        /* Ensure hidden files are not visible initially. */
        g_assert_true (nautilus_file_is_hidden_file (file));
        g_assert_null (item);

        nautilus_file_rename (file, new_name, collect_renamed_files, renamed_files_arr);
        renamed++;
    }

    ITER_CONTEXT_WHILE (changed_files_arr->len < renamed_file_count ||
                        renamed_files_arr->len < renamed_file_count ||
                        !ptr_array_is_subset (renamed_files_arr, changed_files_arr));

    /* The renamed files should now be present in the view model. */
    for (guint i = 0; i < renamed_files_arr->len; i++)
    {
        NautilusFile *file = renamed_files_arr->pdata[i];
        NautilusViewItem *item = nautilus_view_model_get_item_for_file (model, file);

        g_assert_false (nautilus_file_is_hidden_file (file));
        g_assert_nonnull (item);
    }

    test_clear_tmp_dir ();
}

static void
test_hidden_files_change (void)
{
    g_settings_set_boolean (gtk_filechooser_preferences,
                            NAUTILUS_PREFERENCES_SHOW_HIDDEN_FILES,
                            FALSE);

    g_autoptr (NautilusWindowSlot) slot = nautilus_window_slot_new (NAUTILUS_MODE_BROWSE);
    g_autoptr (NautilusFilesView) files_view = nautilus_files_view_new (NAUTILUS_VIEW_GRID_ID, slot);
    NautilusViewModel *model = nautilus_files_view_get_private_model (files_view);
    g_autoptr (GFile) tmp_location = g_file_new_for_path (test_get_tmp_dir ());

    create_hidden_files ();

    /* Test visibility when hidden files are not shown. */
    nautilus_files_view_set_location (NAUTILUS_FILES_VIEW (files_view), tmp_location);
    ITER_CONTEXT_WHILE (nautilus_files_view_get_loading (files_view));

    for (gchar **filename = hidden_files_hierarchy; *filename != NULL; filename++)
    {
        g_autoptr (GFile) location = g_file_new_build_filename (test_get_tmp_dir (),
                                                                *filename,
                                                                NULL);
        g_autoptr (NautilusFile) file = nautilus_file_get (location);
        NautilusViewItem *item = nautilus_view_model_get_item_for_file (model, file);

        if (strstr (*filename, "hidden"))
        {
            g_assert_true (nautilus_file_is_hidden_file (file));
            g_assert_null (item);
        }
        else
        {
            g_assert_false (nautilus_file_is_hidden_file (file));
            g_assert_nonnull (item);
        }
    }

    /* Test visibility when hidden files are shown. */
    gtk_widget_activate_action (GTK_WIDGET (files_view), "view.show-hidden-files", NULL);

    ITER_CONTEXT_WHILE (nautilus_files_view_get_loading (files_view));

    for (gchar **filename = hidden_files_hierarchy; *filename != NULL; filename++)
    {
        g_autoptr (GFile) location = g_file_new_build_filename (test_get_tmp_dir (),
                                                                *filename,
                                                                NULL);
        g_autoptr (NautilusFile) file = nautilus_file_get (location);
        NautilusViewItem *item = nautilus_view_model_get_item_for_file (model, file);

        g_assert_nonnull (item);

        if (strstr (*filename, "hidden"))
        {
            g_assert_true (nautilus_file_is_hidden_file (file));
        }
        else
        {
            g_assert_false (nautilus_file_is_hidden_file (file));
        }
    }

    test_clear_tmp_dir ();
}

static void
replace_cb (GObject      *object,
            GAsyncResult *result,
            gpointer      user_data)
{
    GFile *file = G_FILE (object);
    g_autoptr (GError) error = NULL;
    GPtrArray *replaced_files = user_data;

    g_file_replace_contents_finish (file, result, NULL, &error);
    g_assert_no_error (error);

    g_ptr_array_add (replaced_files, g_object_ref (file));
}

static void
test_replace_files (void)
{
    /* This is not reproducible using files created in tmpfs. */
    g_test_bug ("https://gitlab.gnome.org/GNOME/nautilus/-/issues/3959");

    g_settings_set_boolean (gtk_filechooser_preferences,
                            NAUTILUS_PREFERENCES_SHOW_HIDDEN_FILES,
                            FALSE);

    g_autoptr (NautilusWindowSlot) slot = nautilus_window_slot_new (NAUTILUS_MODE_BROWSE);
    g_autoptr (NautilusFilesView) files_view = nautilus_files_view_new (NAUTILUS_VIEW_GRID_ID, slot);
    NautilusViewModel *model = nautilus_files_view_get_private_model (files_view);
    g_autoptr (GFile) tmp_location = g_file_new_for_path (test_get_tmp_dir ());
    const gsize file_count = 10, replaced_file_count = 5;
    g_autoptr (GPtrArray) file_arr =
        g_ptr_array_new_full (file_count, (GDestroyNotify) nautilus_file_unref);
    g_autoptr (GPtrArray) replacment_files_arr =
        g_ptr_array_new_full (replaced_file_count, (GDestroyNotify) g_object_unref);
    g_autoptr (GPtrArray) changed_files_arr =
        g_ptr_array_new_full (replaced_file_count, (GDestroyNotify) nautilus_file_unref);
    gboolean end_of_changes = FALSE;

    /* Create the files before loading the view and keep them in an array. */
    for (gsize i = 0; i < file_count; i++)
    {
        g_autofree gchar *file_name = g_strdup_printf ("test_file_%zu", i);
        g_autoptr (GFile) file = g_file_get_child (tmp_location, file_name);
        g_autofree gchar *text = g_uuid_string_random ();
        g_autoptr (GError) error = NULL;
        g_autoptr (GFileOutputStream) out = g_file_create (file, G_FILE_CREATE_NONE, NULL, &error);

        g_assert_nonnull (out);
        g_assert_no_error (error);
        g_output_stream_write_all (G_OUTPUT_STREAM (out),
                                   text,
                                   strlen (text),
                                   NULL,
                                   NULL,
                                   &error);
        g_assert_no_error (error);

        g_ptr_array_add (file_arr, nautilus_file_get (file));
    }

    nautilus_files_view_set_location (files_view, tmp_location);
    ITER_CONTEXT_WHILE (nautilus_files_view_get_loading (files_view));

    g_assert_cmpint (g_list_model_get_n_items (G_LIST_MODEL (model)), ==, file_count);

    /* Replace only some of the files and verify that changes are emitted. */
    g_signal_connect (files_view, "file-changed",
                      G_CALLBACK (collect_changed_files), changed_files_arr);
    g_signal_connect (files_view, "end-file-changes",
                      G_CALLBACK (file_changes_done), &end_of_changes);

    for (gsize i = 0; i < replaced_file_count; i++)
    {
        NautilusFile *file = file_arr->pdata[i];
        g_autoptr (GFile) location = nautilus_file_get_location (file);
        g_autofree gchar *text = g_uuid_string_random ();
        gsize text_len = strlen (text);
        g_autoptr (GBytes) bytes = g_bytes_new_take (g_steal_pointer (&text), text_len);
        g_autoptr (GError) error = NULL;
        g_file_replace_contents_bytes_async (location,
                                             bytes,
                                             NULL,
                                             FALSE,
                                             G_FILE_CREATE_REPLACE_DESTINATION,
                                             NULL,
                                             replace_cb,
                                             replacment_files_arr);

        g_assert_no_error (error);
    }

    ITER_CONTEXT_WHILE (!end_of_changes ||
                        replacment_files_arr->len < replaced_file_count ||
                        changed_files_arr->len < replaced_file_count);

    g_assert_cmpint (g_list_model_get_n_items (G_LIST_MODEL (model)), ==, file_count);

    for (gsize i = 0; i < file_arr->len; i++)
    {
        NautilusFile *file = file_arr->pdata[i];
        NautilusViewItem *item = nautilus_view_model_get_item_for_file (model, file);
        g_autoptr (GFile) location = nautilus_file_get_location (file);

        /* Ignore replaced files as they are not guaranteed to stay the same */
        if (!g_ptr_array_find_with_equal_func (replacment_files_arr, location,
                                               (GEqualFunc) g_file_equal, NULL))
        {
            g_assert_nonnull (item);
        }
    }
    for (gsize i = 0; i < replacment_files_arr->len; i++)
    {
        GFile *location = replacment_files_arr->pdata[i];
        g_autoptr (NautilusFile) file = nautilus_file_get (location);
        NautilusViewItem *item = nautilus_view_model_get_item_for_file (model, file);

        /* GIO might use a temperarly hidden file for writing that starts with
         * a dot like ".goutputstream-XXXXXX". Ensure the file is visible after
         * the rename to the intended file. */
        g_assert_nonnull (item);
    }
    for (gsize i = 0; i < changed_files_arr->len; i++)
    {
        NautilusFile *file = changed_files_arr->pdata[i];
        NautilusViewItem *item = nautilus_view_model_get_item_for_file (model, file);

        g_assert_nonnull (item);
    }

    test_clear_tmp_dir ();
}

static void
test_rename_files (void)
{
    g_autoptr (NautilusWindowSlot) slot = nautilus_window_slot_new (NAUTILUS_MODE_BROWSE);
    g_autoptr (NautilusFilesView) files_view = nautilus_files_view_new (NAUTILUS_VIEW_GRID_ID, slot);
    NautilusViewModel *model = nautilus_files_view_get_private_model (files_view);
    g_autoptr (GFile) tmp_location = g_file_new_for_path (test_get_tmp_dir ());
    const guint file_count = 10, renamed_file_count = 5;
    g_autoptr (GPtrArray) file_arr = g_ptr_array_new_full (file_count,
                                                           (GDestroyNotify) nautilus_file_unref);
    g_autoptr (GPtrArray) renamed_files_arr = g_ptr_array_new_full (renamed_file_count,
                                                                    (GDestroyNotify) nautilus_file_unref);
    g_autoptr (GPtrArray) changed_files_arr =
        g_ptr_array_new_full (renamed_file_count, (GDestroyNotify) nautilus_file_unref);

    /* Create the files before loading the view and keep them in an array. */
    for (guint i = 0; i < file_count; i++)
    {
        g_autofree gchar *file_name = g_strdup_printf ("test_file_%i", i);
        g_autoptr (GFile) file = g_file_get_child (tmp_location, file_name);
        g_autoptr (GFileOutputStream) out = g_file_create (file, G_FILE_CREATE_NONE, NULL, NULL);

        g_assert_nonnull (out);
        g_ptr_array_add (file_arr, nautilus_file_get (file));
    }

    nautilus_files_view_set_location (files_view, tmp_location);
    ITER_CONTEXT_WHILE (nautilus_files_view_get_loading (files_view));

    g_assert_cmpint (g_list_model_get_n_items (G_LIST_MODEL (model)), ==, file_count);

    /* Rename only some of the files and verify that changes are emitted. */
    g_signal_connect (files_view, "file-changed",
                      G_CALLBACK (collect_changed_files), changed_files_arr);

    for (guint i = 0; i < renamed_file_count; i++)
    {
        NautilusFile *file = file_arr->pdata[i];
        g_autoptr (GFile) location = nautilus_file_get_location (file);
        g_autofree gchar *file_name = g_strdup_printf ("test_file_%i.txt", i);
        nautilus_file_rename (file, file_name, collect_renamed_files, renamed_files_arr);
    }

    ITER_CONTEXT_WHILE (changed_files_arr->len < renamed_file_count ||
                        renamed_files_arr->len != renamed_file_count ||
                        !ptr_array_is_subset (renamed_files_arr, changed_files_arr));

    g_assert_cmpint (g_list_model_get_n_items (G_LIST_MODEL (model)), ==, file_count);

    /* Both renamed and non-renamed nautilus file pointers from before renaming
     * should still point to the same files in the view. Renaming should not
     * make the old pointers invalid since we're using nautilus_file_rename(). */
    for (guint i = 0; i < file_arr->len; i++)
    {
        NautilusFile *file = file_arr->pdata[i];
        NautilusViewItem *item = nautilus_view_model_get_item_for_file (model, file);

        g_assert_nonnull (item);
    }
    for (guint i = 0; i < renamed_files_arr->len; i++)
    {
        NautilusFile *file = renamed_files_arr->pdata[i];
        NautilusViewItem *item = nautilus_view_model_get_item_for_file (model, file);

        g_assert_nonnull (item);
    }

    test_clear_tmp_dir ();
}

static void
collect_removed_files_cb (NautilusFilesView *view,
                          GList             *removed_files,
                          NautilusDirectory *directory,
                          GPtrArray         *data_files)
{
    for (GList *link = removed_files; link != NULL; link = link->next)
    {
        g_ptr_array_add (data_files, nautilus_file_ref (NAUTILUS_FILE (link->data)));
    }
}

static void
test_remove_files (void)
{
    g_autoptr (NautilusWindowSlot) slot = nautilus_window_slot_new (NAUTILUS_MODE_BROWSE);
    g_autoptr (NautilusFilesView) files_view = nautilus_files_view_new (NAUTILUS_VIEW_GRID_ID, slot);
    NautilusViewModel *model = nautilus_files_view_get_private_model (files_view);
    g_autoptr (GFile) tmp_location = g_file_new_for_path (test_get_tmp_dir ());
    g_autofree gchar *uri = g_file_get_uri (tmp_location);
    const guint file_count = 10, deleted_file_count = 5;
    g_autoptr (GPtrArray) file_arr = g_ptr_array_new_full (file_count,
                                                           (GDestroyNotify) nautilus_file_unref);
    g_autoptr (GPtrArray) deleted_files_arr = g_ptr_array_new ();
    g_autoptr (GPtrArray) callback_arr = g_ptr_array_new_full (file_count,
                                                               (GDestroyNotify) nautilus_file_unref);

    /* Create the files before loading the view and keep them in an array. */
    for (guint i = 0; i < file_count; i++)
    {
        g_autofree gchar *file_name = g_strdup_printf ("test_file_%i", i);
        g_autoptr (GFile) file = g_file_get_child (tmp_location, file_name);
        g_autoptr (GFileOutputStream) out = g_file_create (file, G_FILE_CREATE_NONE, NULL, NULL);

        g_assert_nonnull (out);
        g_ptr_array_add (file_arr, nautilus_file_get (file));
    }

    nautilus_files_view_set_location (files_view, tmp_location);

    ITER_CONTEXT_WHILE (nautilus_files_view_get_loading (files_view));

    g_assert_true (g_file_equal (tmp_location,
                                 nautilus_files_view_get_location (files_view)));
    g_assert_cmpint (g_list_model_get_n_items (G_LIST_MODEL (model)), ==, file_count);


    /* Delete only some of the files and verify that they are emitted */
    g_signal_connect (files_view, "remove-files",
                      G_CALLBACK (collect_removed_files_cb), callback_arr);

    for (guint i = 0; i < deleted_file_count; i++)
    {
        g_autoptr (GFile) location = nautilus_file_get_location (file_arr->pdata[i]);
        g_autoptr (GError) error = NULL;
        g_file_delete (location, NULL, &error);

        g_assert_null (error);
        g_ptr_array_add (deleted_files_arr, file_arr->pdata[i]);
    }

    ITER_CONTEXT_WHILE (!ptr_arrays_equal_unordered (deleted_files_arr, callback_arr));

    g_assert_cmpint (g_list_model_get_n_items (G_LIST_MODEL (model)),
                     ==,
                     file_count - deleted_file_count);

    for (guint i = 0; i < file_arr->len; i++)
    {
        NautilusFile *file = file_arr->pdata[i];
        NautilusViewItem *item = nautilus_view_model_get_item_for_file (model, file);

        if (g_ptr_array_find (deleted_files_arr, file, NULL))
        {
            g_assert_null (item);
        }
        else
        {
            g_assert_nonnull (item);
        }
    }

    test_clear_tmp_dir ();
}

static void
collect_added_files_cb (NautilusFilesView *view,
                        GList             *added_files,
                        GPtrArray         *data_files)
{
    for (GList *link = added_files; link != NULL; link = link->next)
    {
        g_ptr_array_add (data_files, nautilus_file_ref (NAUTILUS_FILE (link->data)));
    }
}

static void
test_add_files (void)
{
    g_autoptr (NautilusWindowSlot) slot = nautilus_window_slot_new (NAUTILUS_MODE_BROWSE);
    g_autoptr (NautilusFilesView) files_view = nautilus_files_view_new (NAUTILUS_VIEW_GRID_ID, slot);
    NautilusViewModel *model = nautilus_files_view_get_private_model (files_view);
    g_autoptr (GFile) tmp_location = g_file_new_for_path (test_get_tmp_dir ());
    g_autofree gchar *uri = g_file_get_uri (tmp_location);
    const guint file_count = 10;
    g_autoptr (GPtrArray) file_arr = g_ptr_array_new_full (file_count,
                                                           (GDestroyNotify) nautilus_file_unref);
    g_autoptr (GPtrArray) callback_arr = g_ptr_array_new_full (file_count,
                                                               (GDestroyNotify) nautilus_file_unref);

    nautilus_files_view_set_location (files_view, tmp_location);

    ITER_CONTEXT_WHILE (nautilus_files_view_get_loading (files_view));

    g_assert_true (g_file_equal (tmp_location,
                                 nautilus_files_view_get_location (files_view)));
    g_assert_cmpint (g_list_model_get_n_items (G_LIST_MODEL (model)), ==, 0);

    /* Keep a list of NautilusFiles */
    for (guint i = 0; i < file_count; i++)
    {
        g_autofree gchar *file_name = g_strdup_printf ("test_file_%i", i);
        g_autoptr (GFile) location = g_file_get_child (tmp_location, file_name);

        g_ptr_array_add (file_arr, nautilus_file_get (location));
    }

    /* Create files and verify that the view added the correct files */
    g_signal_connect (files_view, "add-files", G_CALLBACK (collect_added_files_cb), callback_arr);

    for (guint i = 0; i < file_arr->len; i++)
    {
        g_autoptr (GFile) location = nautilus_file_get_location (file_arr->pdata[i]);
        g_autoptr (GFileOutputStream) out = g_file_create (location,
                                                           G_FILE_CREATE_NONE, NULL, NULL);

        g_assert_nonnull (out);
    }

    ITER_CONTEXT_WHILE (!ptr_arrays_equal_unordered (file_arr, callback_arr));

    g_assert_cmpint (g_list_model_get_n_items (G_LIST_MODEL (model)), ==, file_count);

    for (guint i = 0; i < file_arr->len; i++)
    {
        NautilusFile *file = file_arr->pdata[i];

        g_assert_nonnull (nautilus_view_model_get_item_for_file (model, file));
    }

    test_clear_tmp_dir ();
}

static void
test_load_dir (void)
{
    g_autoptr (NautilusWindowSlot) slot = nautilus_window_slot_new (NAUTILUS_MODE_BROWSE);
    g_autoptr (NautilusFilesView) files_view = nautilus_files_view_new (NAUTILUS_VIEW_GRID_ID, slot);
    NautilusViewModel *model = nautilus_files_view_get_private_model (files_view);
    g_autoptr (GFile) tmp_location = g_file_new_for_path (test_get_tmp_dir ());
    g_autofree gchar *uri = g_file_get_uri (tmp_location);
    gboolean loading_started = FALSE, loading_ended = FALSE;
    const guint file_count = 10;

    create_multiple_files ("view_test", file_count);

    /* Verify loading signals and location */
    nautilus_files_view_set_location (files_view, tmp_location);
    g_signal_connect_swapped (files_view, "begin-loading", G_CALLBACK (set_true), &loading_started);
    g_signal_connect_swapped (files_view, "end-loading", G_CALLBACK (set_true), &loading_ended);

    g_assert_true (nautilus_files_view_get_loading (files_view));

    ITER_CONTEXT_WHILE (nautilus_files_view_get_loading (files_view));

    GFile *view_location = nautilus_files_view_get_location (files_view);
    g_autofree gchar *view_uri = g_file_get_uri (view_location);

    g_assert_true (g_file_equal (tmp_location, view_location));
    g_assert_cmpstr (view_uri, ==, uri);
    g_assert_true (loading_started);
    g_assert_true (loading_ended);

    /* Verify that files exist exist in the model */
    g_autoptr (GFileEnumerator) enumerator = NULL;
    g_autoptr (GError) error = NULL;
    GFile *child;
    guint counter = 0;

    enumerator = g_file_enumerate_children (tmp_location,
                                            G_FILE_ATTRIBUTE_STANDARD_NAME,
                                            G_TYPE_FILE_QUERY_INFO_FLAGS,
                                            NULL,
                                            &error);
    g_assert_no_error (error);

    /* Account for the extra folder */
    g_assert_cmpint (g_list_model_get_n_items (G_LIST_MODEL (model)), ==, file_count + 1);
    while (g_file_enumerator_iterate (enumerator, NULL, &child, NULL, NULL) && child != NULL)
    {
        g_autoptr (NautilusFile) file = nautilus_file_get (child);
        g_assert_nonnull (nautilus_view_model_get_item_for_file (model, file));
        counter++;
    }
    g_assert_cmpint (counter, ==, file_count + 1);

    test_clear_tmp_dir ();
}

int
main (int   argc,
      char *argv[])
{
    g_autoptr (NautilusTagManager) tag_manager = NULL;

    gtk_test_init (&argc, &argv, NULL);

    if (nautilus_application_is_sandboxed ())
    {
        g_message ("files-view tests don't work inside a sandbox.");

        return 77;
    }

    nautilus_register_resource ();
    nautilus_ensure_extension_points ();
    nautilus_global_preferences_init ();
    tag_manager = nautilus_tag_manager_new_dummy ();
    test_init_config_dir ();

    g_autoptr (NautilusApplication) app = nautilus_application_new ();

    g_test_add_func ("/view/load_dir",
                     test_load_dir);
    g_test_add_func ("/view/add_files",
                     test_add_files);
    g_test_add_func ("/view/remove_files",
                     test_remove_files);
    g_test_add_func ("/view/change_files/rename",
                     test_rename_files);
    g_test_add_func ("/view/change_files/replace",
                     test_replace_files);
    g_test_add_func ("/view/hidden_files/change",
                     test_hidden_files_change);
    g_test_add_func ("/view/hidden_files/rename_files",
                     test_hidden_files_renamed);
    g_test_add_func ("/view/selection/actions",
                     test_selection_actions);
    g_test_add_func ("/view/selection/in_search",
                     test_selection_source_in_search);
    g_test_add_func ("/view/selection/operation",
                     test_selection_source_operation);
    g_test_add_func ("/view/actions/zoom",
                     test_zoom_actions);

    return g_test_run ();
}
