// Copyright 2017 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "ui/accessibility/platform/ax_platform_node_auralinux.h"

#include <atk/atk.h>

#include <array>
#include <utility>
#include <vector>

#include "base/compiler_specific.h"
#include "base/functional/bind.h"
#include "base/memory/raw_ptr.h"
#include "base/version.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "ui/accessibility/ax_enums.mojom.h"
#include "ui/accessibility/platform/atk_util_auralinux.h"
#include "ui/accessibility/platform/ax_platform_for_test.h"
#include "ui/accessibility/platform/ax_platform_node_unittest.h"
#include "ui/accessibility/platform/test_ax_node_wrapper.h"
#include "ui/base/glib/scoped_gsignal.h"

// TODO(crbug.com/40248581): Remove this again.
#pragma GCC diagnostic ignored "-Wdeprecated-declarations"

namespace {

template <typename T>
T& GArrayFirst(GArray* array) {
  CHECK(array);
  CHECK_GT(array->len, 0u);
  CHECK_EQ(sizeof(T), g_array_get_element_size(array));
  return *reinterpret_cast<T*>(array->data);
}

// ATK window activated event will be held until AT-SPI bridge is ready. For
// those tests using this event, we work that around by faking the state of the
// AT-SPI bridge. Creating an instance of this class will set it to true during
// the test and set it back to its default state when the test ends, so it
// doesn't affect other tests run in the same batch.
class ScopedAtSpiReady final {
 public:
  explicit ScopedAtSpiReady() {
    ui::AtkUtilAuraLinux::GetInstance()->SetAtSpiReady(true);
  }

  ~ScopedAtSpiReady() {
    ui::AtkUtilAuraLinux::GetInstance()->SetAtSpiReady(false);
  }
};

}  // namespace

namespace ui {

class AXPlatformNodeAuraLinuxTest : public AXPlatformNodeTest {
 public:
  AXPlatformNodeAuraLinuxTest() : ax_mode_setter_(kAXModeComplete) {}
  ~AXPlatformNodeAuraLinuxTest() override = default;
  AXPlatformNodeAuraLinuxTest(const AXPlatformNodeAuraLinuxTest&) = delete;
  AXPlatformNodeAuraLinuxTest& operator=(const AXPlatformNodeAuraLinuxTest&) =
      delete;

 protected:
  AXPlatformNodeAuraLinux* GetPlatformNode(AXNode* node) {
    TestAXNodeWrapper* wrapper =
        TestAXNodeWrapper::GetOrCreate(GetTree(), node);
    if (!wrapper)
      return nullptr;
    return static_cast<AXPlatformNodeAuraLinux*>(wrapper->ax_platform_node());
  }

  AXPlatformNodeAuraLinux* GetRootPlatformNode() {
    return GetPlatformNode(GetRoot());
  }

  AtkObject* AtkObjectFromNode(AXNode* node) {
    if (AXPlatformNode* ax_platform_node = GetPlatformNode(node)) {
      return ax_platform_node->GetNativeViewAccessible();
    } else {
      return nullptr;
    }
  }

  TestAXNodeWrapper* GetRootWrapper() {
    return TestAXNodeWrapper::GetOrCreate(GetTree(), GetRoot());
  }

  AtkObject* GetRootAtkObject() { return AtkObjectFromNode(GetRoot()); }

  // If we were compiled with a newer version of ATK than the runtime version,
  // it's possible that the state we want to expose and/or emit an event for
  // is not present. This will generate a runtime error.
  bool PlatformSupportsState(AtkStateType atk_state_type) {
    static std::optional<int> max_state_type = std::nullopt;
    if (!max_state_type.has_value()) {
      GEnumClass* enum_class =
          G_ENUM_CLASS(g_type_class_ref(atk_state_type_get_type()));
      max_state_type = enum_class->maximum;
      g_type_class_unref(enum_class);
    }
    return atk_state_type < max_state_type.value();
  }

  // If we were compiled with a newer version of ATK than the runtime version,
  // it's possible that the relation type we want to expose and/or emit an event
  // for is not present. This will generate a runtime error.
  bool PlatformSupportsRelation(AtkRelationType atk_relation_type) {
    static std::optional<int> max_relation_type = std::nullopt;
    if (!max_relation_type.has_value()) {
      GEnumClass* enum_class =
          G_ENUM_CLASS(g_type_class_ref(atk_relation_type_get_type()));
      max_relation_type = enum_class->maximum;
      g_type_class_unref(enum_class);
    }
    return atk_relation_type < max_relation_type.value();
  }

 private:
  ScopedAXModeSetter ax_mode_setter_;
};

static void EnsureAtkObjectHasAttributeWithValue(
    AtkObject* atk_object,
    const gchar* attribute_name,
    const gchar* attribute_value) {
  AtkAttributeSet* attributes = atk_object_get_attributes(atk_object);
  bool saw_attribute = false;

  AtkAttributeSet* current = attributes;
  while (current) {
    AtkAttribute* attribute = static_cast<AtkAttribute*>(current->data);

    if (0 == UNSAFE_TODO(strcmp(attribute_name, attribute->name))) {
      // Ensure that we only see this attribute once.
      ASSERT_FALSE(saw_attribute) << attribute_name;

      EXPECT_STREQ(attribute_value, attribute->value);
      saw_attribute = true;
    }

    current = current->next;
  }

  ASSERT_TRUE(saw_attribute);
  atk_attribute_set_free(attributes);
}

static void EnsureAtkObjectDoesNotHaveAttribute(
    AtkObject* atk_object,
    const gchar* attribute_name) {
  AtkAttributeSet* attributes = atk_object_get_attributes(atk_object);
  AtkAttributeSet* current = attributes;
  while (current) {
    AtkAttribute* attribute = static_cast<AtkAttribute*>(current->data);
    ASSERT_STRNE(attribute_name, attribute->name) << attribute_name;
    current = current->next;
  }
  atk_attribute_set_free(attributes);
}

static void SetStringAttributeOnNode(
    AXNode* ax_node,
    ax::mojom::StringAttribute attribute,
    const char* attribute_value,
    std::optional<ax::mojom::Role> role = std::nullopt) {
  AXNodeData new_data = AXNodeData();
  new_data.role = role.value_or(ax::mojom::Role::kApplication);
  new_data.id = ax_node->id();
  new_data.AddStringAttribute(attribute, attribute_value);
  ax_node->SetData(new_data);
}

static void TestAtkObjectIntAttribute(
    AXNode* ax_node,
    AtkObject* atk_object,
    ax::mojom::IntAttribute mojom_attribute,
    const gchar* attribute_name,
    std::optional<ax::mojom::Role> role = std::nullopt) {
  AXNodeData new_data = AXNodeData();
  new_data.role = role.value_or(ax::mojom::Role::kApplication);
  new_data.id = ax_node->id();
  ax_node->SetData(new_data);
  EnsureAtkObjectDoesNotHaveAttribute(atk_object, attribute_name);

  std::pair<int, const char*> tests[] = {
      std::make_pair(0, "0"),       std::make_pair(1, "1"),
      std::make_pair(2, "2"),       std::make_pair(-100, "-100"),
      std::make_pair(1000, "1000"),
  };

  for (const auto& test : tests) {
    AXNodeData newer_data = AXNodeData();
    newer_data.role = role.value_or(ax::mojom::Role::kApplication);
    newer_data.id = ax_node->id();
    newer_data.AddIntAttribute(mojom_attribute, test.first);
    ax_node->SetData(newer_data);
    EnsureAtkObjectHasAttributeWithValue(atk_object, attribute_name,
                                         test.second);
  }
}

static void TestAtkObjectStringAttribute(
    AXNode* ax_node,
    AtkObject* atk_object,
    ax::mojom::StringAttribute mojom_attribute,
    const gchar* attribute_name,
    std::optional<ax::mojom::Role> role = std::nullopt) {
  AXNodeData new_data = AXNodeData();
  new_data.role = role.value_or(ax::mojom::Role::kApplication);
  new_data.id = ax_node->id();
  ax_node->SetData(new_data);
  EnsureAtkObjectDoesNotHaveAttribute(atk_object, attribute_name);

  const char* tests[] = {
      "", "a string with spaces", "a string with , a comma",
      "\xE2\x98\xBA",  // The smiley emoji.
  };

  for (const char* test : tests) {
    SetStringAttributeOnNode(ax_node, mojom_attribute, test, role);
    EnsureAtkObjectHasAttributeWithValue(atk_object, attribute_name, test);
  }
}

static void TestAtkObjectBoolAttribute(
    AXNode* ax_node,
    AtkObject* atk_object,
    ax::mojom::BoolAttribute mojom_attribute,
    const gchar* attribute_name,
    std::optional<ax::mojom::Role> role = std::nullopt) {
  AXNodeData new_data = AXNodeData();
  new_data.role = role.value_or(ax::mojom::Role::kApplication);
  new_data.id = ax_node->id();
  ax_node->SetData(new_data);
  EnsureAtkObjectDoesNotHaveAttribute(atk_object, attribute_name);

  new_data = AXNodeData();
  new_data.role = role.value_or(ax::mojom::Role::kApplication);
  new_data.id = ax_node->id();
  new_data.AddBoolAttribute(mojom_attribute, true);
  ax_node->SetData(new_data);
  EnsureAtkObjectHasAttributeWithValue(atk_object, attribute_name, "true");

  new_data = AXNodeData();
  new_data.role = role.value_or(ax::mojom::Role::kApplication);
  new_data.id = ax_node->id();
  new_data.AddBoolAttribute(mojom_attribute, false);
  ax_node->SetData(new_data);
  EnsureAtkObjectHasAttributeWithValue(atk_object, attribute_name, "false");
}

static bool AtkObjectHasState(AtkObject* atk_object, AtkStateType state) {
  AtkStateSet* state_set = atk_object_ref_state_set(atk_object);
  EXPECT_TRUE(ATK_IS_STATE_SET(state_set));
  bool in_state_set = atk_state_set_contains_state(state_set, state);
  g_object_unref(state_set);
  return in_state_set;
}

//
// AtkObject tests
//
#if defined(ATK_CHECK_VERSION) && ATK_CHECK_VERSION(2, 16, 0)
#define ATK_216
#endif

#if defined(ATK_CHECK_VERSION) && ATK_CHECK_VERSION(2, 26, 0)
#define ATK_226
#endif

TEST_F(AXPlatformNodeAuraLinuxTest, TestAtkObjectDetachedObject) {
  AXNodeData root;
  root.id = 1;
  root.role = ax::mojom::Role::kRootWebArea;
  root.SetName("Name");
  Init(root);

  AtkObject* root_obj(GetRootAtkObject());
  ASSERT_TRUE(ATK_IS_OBJECT(root_obj));
  g_object_ref(root_obj);

  const gchar* name = atk_object_get_name(root_obj);
  EXPECT_STREQ("Name", name);

  AtkStateSet* state_set = atk_object_ref_state_set(root_obj);
  ASSERT_TRUE(ATK_IS_STATE_SET(state_set));
  EXPECT_FALSE(atk_state_set_contains_state(state_set, ATK_STATE_DEFUNCT));
  g_object_unref(state_set);

  // Create an empty tree.
  SetTree(std::make_unique<AXTree>());
  EXPECT_EQ(nullptr, atk_object_get_name(root_obj));

  state_set = atk_object_ref_state_set(root_obj);
  ASSERT_TRUE(ATK_IS_STATE_SET(state_set));
  EXPECT_TRUE(atk_state_set_contains_state(state_set, ATK_STATE_DEFUNCT));
  g_object_unref(state_set);

  g_object_unref(root_obj);
}

TEST_F(AXPlatformNodeAuraLinuxTest, TestAtkObjectName) {
  AXNodeData root;
  root.id = 1;
  root.role = ax::mojom::Role::kRootWebArea;
  root.SetName("Name");
  Init(root);

  AtkObject* root_obj(GetRootAtkObject());
  ASSERT_TRUE(ATK_IS_OBJECT(root_obj));
  g_object_ref(root_obj);

  const gchar* name = atk_object_get_name(root_obj);
  EXPECT_STREQ("Name", name);

  g_object_unref(root_obj);
}

TEST_F(AXPlatformNodeAuraLinuxTest, TestAtkObjectDescription) {
  AXNodeData root;
  root.id = 1;
  root.AddStringAttribute(ax::mojom::StringAttribute::kDescription,
                          "Description");
  Init(root);

  AtkObject* root_obj(GetRootAtkObject());
  ASSERT_TRUE(ATK_IS_OBJECT(root_obj));
  g_object_ref(root_obj);

  const gchar* description = atk_object_get_description(root_obj);
  EXPECT_STREQ("Description", description);

  g_object_unref(root_obj);
}

TEST_F(AXPlatformNodeAuraLinuxTest, TestAtkObjectRole) {
  AXNodeData root;
  root.id = 1;
  root.child_ids.push_back(2);
  root.role = ax::mojom::Role::kApplication;

  AXNodeData child;
  child.id = 2;

  Init(root, child);
  AXNode* child_node = GetRoot()->children()[0];

  AtkObject* root_obj(AtkObjectFromNode(GetRoot()));
  ASSERT_TRUE(ATK_IS_OBJECT(root_obj));
  g_object_ref(root_obj);
  EXPECT_EQ(ATK_ROLE_APPLICATION, atk_object_get_role(root_obj));
  g_object_unref(root_obj);

  child.role = ax::mojom::Role::kAlert;
  child_node->SetData(child);
  AtkObject* child_obj(AtkObjectFromNode(child_node));
  ASSERT_TRUE(ATK_IS_OBJECT(child_obj));
  g_object_ref(child_obj);
  EXPECT_EQ(ATK_ROLE_NOTIFICATION, atk_object_get_role(child_obj));
  g_object_unref(child_obj);

  child.role = ax::mojom::Role::kAlertDialog;
  child_node->SetData(child);
  child_obj = AtkObjectFromNode(child_node);
  ASSERT_TRUE(ATK_IS_OBJECT(child_obj));
  g_object_ref(child_obj);
  EXPECT_EQ(ATK_ROLE_ALERT, atk_object_get_role(child_obj));
  g_object_unref(child_obj);

  child.role = ax::mojom::Role::kButton;
  child_node->SetData(child);
  child_obj = AtkObjectFromNode(child_node);
  ASSERT_TRUE(ATK_IS_OBJECT(child_obj));
  g_object_ref(child_obj);
  EXPECT_EQ(ATK_ROLE_PUSH_BUTTON, atk_object_get_role(child_obj));
  g_object_unref(child_obj);

  child.role = ax::mojom::Role::kCanvas;
  child_node->SetData(child);
  child_obj = AtkObjectFromNode(child_node);
  ASSERT_TRUE(ATK_IS_OBJECT(child_obj));
  g_object_ref(child_obj);
  EXPECT_EQ(ATK_ROLE_CANVAS, atk_object_get_role(child_obj));
  g_object_unref(child_obj);

  child.role = ax::mojom::Role::kApplication;
  child_node->SetData(child);
  child_obj = AtkObjectFromNode(child_node);
  ASSERT_TRUE(ATK_IS_OBJECT(child_obj));
  g_object_ref(child_obj);
  EXPECT_EQ(ATK_ROLE_EMBEDDED, atk_object_get_role(child_obj));
  g_object_unref(child_obj);

  child.role = ax::mojom::Role::kWindow;
  child_node->SetData(child);
  child_obj = AtkObjectFromNode(child_node);
  ASSERT_TRUE(ATK_IS_OBJECT(child_obj));
  g_object_ref(child_obj);
  EXPECT_EQ(ATK_ROLE_FRAME, atk_object_get_role(child_obj));
  g_object_unref(child_obj);
}

TEST_F(AXPlatformNodeAuraLinuxTest, TestAtkObjectState) {
  AXNodeData root;
  root.id = 1;
  Init(root);

  AtkObject* root_obj(GetRootAtkObject());
  ASSERT_TRUE(ATK_IS_OBJECT(root_obj));
  g_object_ref(root_obj);

  AtkStateSet* state_set = atk_object_ref_state_set(root_obj);
  ASSERT_TRUE(ATK_IS_STATE_SET(state_set));
  ASSERT_TRUE(atk_state_set_contains_state(state_set, ATK_STATE_ENABLED));
  ASSERT_TRUE(atk_state_set_contains_state(state_set, ATK_STATE_SENSITIVE));
  ASSERT_TRUE(atk_state_set_contains_state(state_set, ATK_STATE_SHOWING));
  ASSERT_TRUE(atk_state_set_contains_state(state_set, ATK_STATE_VISIBLE));
  ASSERT_FALSE(atk_state_set_contains_state(state_set, ATK_STATE_BUSY));
#if defined(ATK_216)
  // Runtime check in case we were compiled with a newer version of ATK.
  if (PlatformSupportsState(ATK_STATE_CHECKABLE))
    ASSERT_FALSE(atk_state_set_contains_state(state_set, ATK_STATE_CHECKABLE));
#endif
  ASSERT_FALSE(atk_state_set_contains_state(state_set, ATK_STATE_CHECKED));
  ASSERT_FALSE(atk_state_set_contains_state(state_set, ATK_STATE_DEFAULT));
  ASSERT_FALSE(atk_state_set_contains_state(state_set, ATK_STATE_EDITABLE));
  ASSERT_FALSE(atk_state_set_contains_state(state_set, ATK_STATE_EXPANDABLE));
  ASSERT_FALSE(atk_state_set_contains_state(state_set, ATK_STATE_EXPANDED));
  ASSERT_FALSE(atk_state_set_contains_state(state_set, ATK_STATE_FOCUSABLE));
  ASSERT_FALSE(atk_state_set_contains_state(state_set, ATK_STATE_FOCUSED));
#if defined(ATK_216)
  // Runtime check in case we were compiled with a newer version of ATK.
  if (PlatformSupportsState(ATK_STATE_HAS_POPUP))
    ASSERT_FALSE(atk_state_set_contains_state(state_set, ATK_STATE_HAS_POPUP));
#endif
  ASSERT_FALSE(atk_state_set_contains_state(state_set, ATK_STATE_HORIZONTAL));
  ASSERT_FALSE(
      atk_state_set_contains_state(state_set, ATK_STATE_INVALID_ENTRY));
  ASSERT_FALSE(atk_state_set_contains_state(state_set, ATK_STATE_MODAL));
  ASSERT_FALSE(atk_state_set_contains_state(state_set, ATK_STATE_MULTI_LINE));
  ASSERT_FALSE(
      atk_state_set_contains_state(state_set, ATK_STATE_MULTISELECTABLE));
  ASSERT_FALSE(atk_state_set_contains_state(state_set, ATK_STATE_REQUIRED));
  ASSERT_FALSE(atk_state_set_contains_state(state_set, ATK_STATE_SELECTABLE));
  ASSERT_FALSE(
      atk_state_set_contains_state(state_set, ATK_STATE_SELECTABLE_TEXT));
  ASSERT_FALSE(atk_state_set_contains_state(state_set, ATK_STATE_SELECTED));
  ASSERT_FALSE(atk_state_set_contains_state(state_set, ATK_STATE_SINGLE_LINE));
  ASSERT_FALSE(atk_state_set_contains_state(state_set,
                                            ATK_STATE_SUPPORTS_AUTOCOMPLETION));
  ASSERT_FALSE(atk_state_set_contains_state(state_set, ATK_STATE_VERTICAL));
  ASSERT_FALSE(atk_state_set_contains_state(state_set, ATK_STATE_VISITED));
  g_object_unref(state_set);

  root = AXNodeData();
  root.id = 1;
  root.AddState(ax::mojom::State::kDefault);
  root.AddState(ax::mojom::State::kEditable);
  root.AddState(ax::mojom::State::kExpanded);
  root.AddState(ax::mojom::State::kFocusable);
  root.AddState(ax::mojom::State::kMultiselectable);
  root.AddState(ax::mojom::State::kRequired);
  root.AddState(ax::mojom::State::kVertical);
  root.AddBoolAttribute(ax::mojom::BoolAttribute::kBusy, true);
  root.SetInvalidState(ax::mojom::InvalidState::kTrue);
  root.AddStringAttribute(ax::mojom::StringAttribute::kAutoComplete, "foo");
  GetRoot()->SetData(root);

  state_set = atk_object_ref_state_set(root_obj);
  ASSERT_TRUE(ATK_IS_STATE_SET(state_set));
  ASSERT_TRUE(atk_state_set_contains_state(state_set, ATK_STATE_BUSY));
  ASSERT_TRUE(atk_state_set_contains_state(state_set, ATK_STATE_DEFAULT));
  ASSERT_TRUE(atk_state_set_contains_state(state_set, ATK_STATE_EDITABLE));
  ASSERT_TRUE(atk_state_set_contains_state(state_set, ATK_STATE_EXPANDABLE));
  ASSERT_TRUE(atk_state_set_contains_state(state_set, ATK_STATE_EXPANDED));
  ASSERT_TRUE(atk_state_set_contains_state(state_set, ATK_STATE_FOCUSABLE));
  ASSERT_TRUE(atk_state_set_contains_state(state_set, ATK_STATE_INVALID_ENTRY));
  ASSERT_TRUE(
      atk_state_set_contains_state(state_set, ATK_STATE_MULTISELECTABLE));
  ASSERT_TRUE(atk_state_set_contains_state(state_set, ATK_STATE_REQUIRED));
  ASSERT_TRUE(atk_state_set_contains_state(state_set,
                                           ATK_STATE_SUPPORTS_AUTOCOMPLETION));
  ASSERT_TRUE(atk_state_set_contains_state(state_set, ATK_STATE_VERTICAL));
  ASSERT_FALSE(atk_state_set_contains_state(state_set, ATK_STATE_FOCUSED));
  ASSERT_FALSE(atk_state_set_contains_state(state_set, ATK_STATE_HORIZONTAL));
  g_object_unref(state_set);

  root = AXNodeData();
  root.AddState(ax::mojom::State::kCollapsed);
  root.AddState(ax::mojom::State::kHorizontal);
  root.AddState(ax::mojom::State::kVisited);
  root.AddBoolAttribute(ax::mojom::BoolAttribute::kSelected, true);
  root.SetHasPopup(ax::mojom::HasPopup::kTrue);
  GetRoot()->SetData(root);

  state_set = atk_object_ref_state_set(root_obj);
  ASSERT_TRUE(ATK_IS_STATE_SET(state_set));
  ASSERT_TRUE(atk_state_set_contains_state(state_set, ATK_STATE_EXPANDABLE));
#if defined(ATK_216)
  // Runtime check in case we were compiled with a newer version of ATK.
  if (PlatformSupportsState(ATK_STATE_HAS_POPUP))
    ASSERT_TRUE(atk_state_set_contains_state(state_set, ATK_STATE_HAS_POPUP));
#endif
  ASSERT_TRUE(atk_state_set_contains_state(state_set, ATK_STATE_HORIZONTAL));
  ASSERT_TRUE(atk_state_set_contains_state(state_set, ATK_STATE_SELECTABLE));
  ASSERT_TRUE(atk_state_set_contains_state(state_set, ATK_STATE_SELECTED));
  ASSERT_TRUE(atk_state_set_contains_state(state_set, ATK_STATE_VISITED));
  ASSERT_FALSE(atk_state_set_contains_state(state_set, ATK_STATE_EXPANDED));
  ASSERT_FALSE(atk_state_set_contains_state(state_set, ATK_STATE_VERTICAL));
  g_object_unref(state_set);

  root = AXNodeData();
  root.id = 1;
  root.AddState(ax::mojom::State::kInvisible);
  root.AddBoolAttribute(ax::mojom::BoolAttribute::kModal, true);
  GetRoot()->SetData(root);

  state_set = atk_object_ref_state_set(root_obj);
  ASSERT_TRUE(ATK_IS_STATE_SET(state_set));
  ASSERT_TRUE(atk_state_set_contains_state(state_set, ATK_STATE_MODAL));
  ASSERT_FALSE(atk_state_set_contains_state(state_set, ATK_STATE_SHOWING));
  ASSERT_FALSE(atk_state_set_contains_state(state_set, ATK_STATE_VISIBLE));
  g_object_unref(state_set);

  g_object_unref(root_obj);
}

TEST_F(AXPlatformNodeAuraLinuxTest, TestAtkObjectChildAndParent) {
  AXNodeData root;
  root.id = 1;
  root.child_ids.push_back(2);
  root.child_ids.push_back(3);

  AXNodeData button;
  button.role = ax::mojom::Role::kButton;
  button.id = 2;

  AXNodeData checkbox;
  checkbox.role = ax::mojom::Role::kCheckBox;
  checkbox.id = 3;

  Init(root, button, checkbox);
  AXNode* button_node = GetRoot()->children()[0];
  AXNode* checkbox_node = GetRoot()->children()[1];
  AtkObject* root_obj = GetRootAtkObject();
  AtkObject* button_obj = AtkObjectFromNode(button_node);
  AtkObject* checkbox_obj = AtkObjectFromNode(checkbox_node);

  ASSERT_TRUE(ATK_IS_OBJECT(root_obj));
  EXPECT_EQ(2, atk_object_get_n_accessible_children(root_obj));
  ASSERT_TRUE(ATK_IS_OBJECT(button_obj));
  EXPECT_EQ(0, atk_object_get_n_accessible_children(button_obj));
  ASSERT_TRUE(ATK_IS_OBJECT(checkbox_obj));
  EXPECT_EQ(0, atk_object_get_n_accessible_children(checkbox_obj));

  {
    AtkObject* result = atk_object_ref_accessible_child(root_obj, 0);
    EXPECT_TRUE(ATK_IS_OBJECT(root_obj));
    EXPECT_EQ(result, button_obj);
    g_object_unref(result);
  }
  {
    AtkObject* result = atk_object_ref_accessible_child(root_obj, 1);
    EXPECT_TRUE(ATK_IS_OBJECT(root_obj));
    EXPECT_EQ(result, checkbox_obj);
    g_object_unref(result);
  }

  // Now check parents.
  {
    AtkObject* result = atk_object_get_parent(button_obj);
    EXPECT_TRUE(ATK_IS_OBJECT(result));
    EXPECT_EQ(result, root_obj);
  }
  {
    AtkObject* result = atk_object_get_parent(checkbox_obj);
    EXPECT_TRUE(ATK_IS_OBJECT(result));
    EXPECT_EQ(result, root_obj);
  }

  // Test invalid indices.
  AtkObject* result = atk_object_ref_accessible_child(root_obj, -1);
  EXPECT_EQ(result, nullptr);
  result = atk_object_ref_accessible_child(root_obj, -88);
  EXPECT_EQ(result, nullptr);
  result = atk_object_ref_accessible_child(root_obj, 3);
  EXPECT_EQ(result, nullptr);
  result = atk_object_ref_accessible_child(root_obj, 1000);
  EXPECT_EQ(result, nullptr);
  result = atk_object_ref_accessible_child(root_obj, 828282);
  EXPECT_EQ(result, nullptr);
}

TEST_F(AXPlatformNodeAuraLinuxTest, TestAtkObjectIndexInParent) {
  AXNodeData root;
  root.id = 1;
  root.child_ids.push_back(2);
  root.child_ids.push_back(3);

  AXNodeData left;
  left.id = 2;

  AXNodeData right;
  right.id = 3;

  Init(root, left, right);

  AtkObject* root_obj(GetRootAtkObject());
  ASSERT_TRUE(ATK_IS_OBJECT(root_obj));
  g_object_ref(root_obj);

  AtkObject* left_obj = atk_object_ref_accessible_child(root_obj, 0);
  ASSERT_TRUE(ATK_IS_OBJECT(left_obj));
  AtkObject* right_obj = atk_object_ref_accessible_child(root_obj, 1);
  ASSERT_TRUE(ATK_IS_OBJECT(right_obj));

  EXPECT_EQ(0, atk_object_get_index_in_parent(left_obj));
  EXPECT_EQ(1, atk_object_get_index_in_parent(right_obj));

  g_object_unref(left_obj);
  g_object_unref(right_obj);
  g_object_unref(root_obj);
}

TEST_F(AXPlatformNodeAuraLinuxTest, TestAtkObjectStringAttributes) {
  AXNodeData root_data;
  root_data.id = 1;

  Init(root_data);

  AXNode* root_node = GetRoot();
  AtkObject* root_atk_object(AtkObjectFromNode(root_node));
  ASSERT_TRUE(ATK_IS_OBJECT(root_atk_object));
  g_object_ref(root_atk_object);

  std::pair<ax::mojom::StringAttribute, const char*> tests[] = {
      std::make_pair(ax::mojom::StringAttribute::kDisplay, "display"),
      std::make_pair(ax::mojom::StringAttribute::kHtmlTag, "tag"),
      std::make_pair(ax::mojom::StringAttribute::kRole, "xml-roles"),
      std::make_pair(ax::mojom::StringAttribute::kPlaceholder, "placeholder"),
      std::make_pair(ax::mojom::StringAttribute::kRoleDescription,
                     "roledescription"),
      std::make_pair(ax::mojom::StringAttribute::kKeyShortcuts, "keyshortcuts"),
      std::make_pair(ax::mojom::StringAttribute::kLiveStatus, "live"),
      std::make_pair(ax::mojom::StringAttribute::kLiveRelevant, "relevant"),
      std::make_pair(ax::mojom::StringAttribute::kContainerLiveStatus,
                     "container-live"),
      std::make_pair(ax::mojom::StringAttribute::kContainerLiveRelevant,
                     "container-relevant"),
  };

  for (const auto& test : tests) {
    TestAtkObjectStringAttribute(root_node, root_atk_object, test.first,
                                 test.second);
  }

  g_object_unref(root_atk_object);
}

TEST_F(AXPlatformNodeAuraLinuxTest, TestAtkObjectBoolAttributes) {
  AXNodeData root_data;
  root_data.id = 1;

  Init(root_data);

  AXNode* root_node = GetRoot();
  AtkObject* root_atk_object(AtkObjectFromNode(root_node));
  ASSERT_TRUE(ATK_IS_OBJECT(root_atk_object));
  g_object_ref(root_atk_object);

  std::pair<ax::mojom::BoolAttribute, const char*> tests[] = {
      std::make_pair(ax::mojom::BoolAttribute::kLiveAtomic, "atomic"),
      std::make_pair(ax::mojom::BoolAttribute::kBusy, "busy"),
      std::make_pair(ax::mojom::BoolAttribute::kContainerLiveAtomic,
                     "container-atomic"),
      std::make_pair(ax::mojom::BoolAttribute::kContainerLiveBusy,
                     "container-busy"),
  };

  for (const auto& test : tests) {
    TestAtkObjectBoolAttribute(root_node, root_atk_object, test.first,
                               test.second);
  }

  g_object_unref(root_atk_object);
}

TEST_F(AXPlatformNodeAuraLinuxTest, DISABLED_TestAtkObjectIntAttributes) {
  AXNodeData root_data;
  root_data.id = 1;
  Init(root_data);

  AXNode* root_node = GetRoot();
  AtkObject* root_atk_object(AtkObjectFromNode(root_node));
  ASSERT_TRUE(ATK_IS_OBJECT(root_atk_object));
  g_object_ref(root_atk_object);

  TestAtkObjectIntAttribute(root_node, root_atk_object,
                            ax::mojom::IntAttribute::kHierarchicalLevel,
                            "level");
  TestAtkObjectIntAttribute(root_node, root_atk_object,
                            ax::mojom::IntAttribute::kAriaColumnCount,
                            "colcount", ax::mojom::Role::kTable);
  TestAtkObjectIntAttribute(root_node, root_atk_object,
                            ax::mojom::IntAttribute::kAriaColumnCount,
                            "colcount", ax::mojom::Role::kGrid);
  TestAtkObjectIntAttribute(root_node, root_atk_object,
                            ax::mojom::IntAttribute::kAriaColumnCount,
                            "colcount", ax::mojom::Role::kTreeGrid);

  TestAtkObjectIntAttribute(root_node, root_atk_object,
                            ax::mojom::IntAttribute::kAriaRowCount, "rowcount",
                            ax::mojom::Role::kTable);
  TestAtkObjectIntAttribute(root_node, root_atk_object,
                            ax::mojom::IntAttribute::kAriaRowCount, "rowcount",
                            ax::mojom::Role::kGrid);
  TestAtkObjectIntAttribute(root_node, root_atk_object,
                            ax::mojom::IntAttribute::kAriaRowCount, "rowcount",
                            ax::mojom::Role::kTreeGrid);

  TestAtkObjectIntAttribute(root_node, root_atk_object,
                            ax::mojom::IntAttribute::kAriaCellColumnIndex,
                            "colindex", ax::mojom::Role::kCell);
  TestAtkObjectIntAttribute(root_node, root_atk_object,
                            ax::mojom::IntAttribute::kAriaCellRowIndex,
                            "rowindex", ax::mojom::Role::kCell);

  g_object_unref(root_atk_object);
}

//
// AtkComponent tests
//

TEST_F(AXPlatformNodeAuraLinuxTest, TestAtkComponentRefAtPoint) {
  AXNodeData root;
  root.id = 1;
  root.relative_bounds.bounds = gfx::RectF(0, 0, 30, 30);

  AXNodeData node1;
  node1.id = 2;
  node1.role = ax::mojom::Role::kGenericContainer;
  node1.relative_bounds.bounds = gfx::RectF(0, 0, 10, 10);
  node1.SetName("Name1");
  root.child_ids.push_back(node1.id);

  AXNodeData node2;
  node2.id = 3;
  node2.role = ax::mojom::Role::kGenericContainer;
  node2.relative_bounds.bounds = gfx::RectF(20, 20, 10, 10);
  node2.SetName("Name2");
  root.child_ids.push_back(node2.id);

  Init(root, node1, node2);

  AtkObject* root_obj(GetRootAtkObject());
  EXPECT_TRUE(ATK_IS_OBJECT(root_obj));
  EXPECT_TRUE(ATK_IS_COMPONENT(root_obj));
  g_object_ref(root_obj);

  AtkObject* child_obj = atk_component_ref_accessible_at_point(
      ATK_COMPONENT(root_obj), 50, 50, ATK_XY_SCREEN);
  EXPECT_EQ(nullptr, child_obj);

  // this is directly on node 1.
  child_obj = atk_component_ref_accessible_at_point(ATK_COMPONENT(root_obj), 5,
                                                    5, ATK_XY_SCREEN);
  ASSERT_NE(nullptr, child_obj);
  EXPECT_TRUE(ATK_IS_OBJECT(child_obj));

  const gchar* name = atk_object_get_name(child_obj);
  EXPECT_STREQ("Name1", name);

  g_object_unref(child_obj);
  g_object_unref(root_obj);
}

TEST_F(AXPlatformNodeAuraLinuxTest, TestAtkComponentsGetExtentsPositionSize) {
  AXNodeData root;
  root.id = 1;
  root.role = ax::mojom::Role::kWindow;
  root.relative_bounds.bounds = gfx::RectF(10, 40, 800, 600);
  root.child_ids.push_back(2);

  AXNodeData child;
  child.id = 2;
  child.relative_bounds.bounds = gfx::RectF(100, 150, 200, 200);
  Init(root, child);

  TestAXNodeWrapper::SetGlobalCoordinateOffset(gfx::Vector2d(100, 200));

  AtkObject* root_obj = GetRootAtkObject();
  ASSERT_TRUE(ATK_IS_OBJECT(root_obj));
  ASSERT_TRUE(ATK_IS_COMPONENT(root_obj));
  g_object_ref(root_obj);

  gint x_left, y_top, width, height;
  atk_component_get_extents(ATK_COMPONENT(root_obj), &x_left, &y_top, &width,
                            &height, ATK_XY_SCREEN);
  EXPECT_EQ(110, x_left);
  EXPECT_EQ(240, y_top);
  EXPECT_EQ(800, width);
  EXPECT_EQ(600, height);

  AtkObject* hit_test_result = atk_component_ref_accessible_at_point(
      ATK_COMPONENT(root_obj), x_left, y_top, ATK_XY_SCREEN);
  ASSERT_EQ(hit_test_result, root_obj);
  g_object_unref(hit_test_result);

  atk_component_get_position(ATK_COMPONENT(root_obj), &x_left, &y_top,
                             ATK_XY_SCREEN);
  EXPECT_EQ(110, x_left);
  EXPECT_EQ(240, y_top);

  atk_component_get_extents(ATK_COMPONENT(root_obj), &x_left, &y_top, &width,
                            &height, ATK_XY_WINDOW);
  EXPECT_EQ(0, x_left);
  EXPECT_EQ(0, y_top);
  EXPECT_EQ(800, width);
  EXPECT_EQ(600, height);

  hit_test_result = atk_component_ref_accessible_at_point(
      ATK_COMPONENT(root_obj), x_left + 2, y_top + 2, ATK_XY_WINDOW);
  ASSERT_EQ(hit_test_result, root_obj);
  g_object_unref(hit_test_result);

  atk_component_get_position(ATK_COMPONENT(root_obj), &x_left, &y_top,
                             ATK_XY_WINDOW);
  EXPECT_EQ(0, x_left);
  EXPECT_EQ(0, y_top);

  atk_component_get_size(ATK_COMPONENT(root_obj), &width, &height);
  EXPECT_EQ(800, width);
  EXPECT_EQ(600, height);

  AXNode* child_node = GetRoot()->children()[0];
  AtkObject* child_obj = AtkObjectFromNode(child_node);
  ASSERT_TRUE(ATK_IS_OBJECT(child_obj));
  ASSERT_TRUE(ATK_IS_COMPONENT(child_obj));
  g_object_ref(child_obj);

  atk_component_get_extents(ATK_COMPONENT(child_obj), &x_left, &y_top, &width,
                            &height, ATK_XY_SCREEN);
  EXPECT_EQ(200, x_left);
  EXPECT_EQ(350, y_top);
  EXPECT_EQ(200, width);
  EXPECT_EQ(200, height);

  hit_test_result = atk_component_ref_accessible_at_point(
      ATK_COMPONENT(root_obj), x_left, y_top, ATK_XY_SCREEN);
  ASSERT_EQ(hit_test_result, child_obj);
  g_object_unref(hit_test_result);

  atk_component_get_extents(ATK_COMPONENT(child_obj), &x_left, &y_top, &width,
                            &height, ATK_XY_WINDOW);
  EXPECT_EQ(90, x_left);
  EXPECT_EQ(110, y_top);
  EXPECT_EQ(200, width);
  EXPECT_EQ(200, height);

  hit_test_result = atk_component_ref_accessible_at_point(
      ATK_COMPONENT(root_obj), x_left, y_top, ATK_XY_WINDOW);
  ASSERT_EQ(hit_test_result, child_obj);
  g_object_unref(hit_test_result);

  atk_component_get_extents(ATK_COMPONENT(child_obj), nullptr, &y_top, &width,
                            &height, ATK_XY_SCREEN);
  EXPECT_EQ(200, height);
  atk_component_get_extents(ATK_COMPONENT(child_obj), &x_left, nullptr, &width,
                            &height, ATK_XY_SCREEN);
  EXPECT_EQ(200, x_left);
  atk_component_get_extents(ATK_COMPONENT(child_obj), &x_left, &y_top, nullptr,
                            &height, ATK_XY_SCREEN);
  EXPECT_EQ(350, y_top);
  atk_component_get_extents(ATK_COMPONENT(child_obj), &x_left, &y_top, &width,
                            nullptr, ATK_XY_SCREEN);
  EXPECT_EQ(200, width);

  g_object_unref(child_obj);
  g_object_unref(root_obj);

  // Un-set the global offset so that it doesn't affect subsequent tests.
  TestAXNodeWrapper::SetGlobalCoordinateOffset(gfx::Vector2d(0, 0));
}

#if ATK_CHECK_VERSION(2, 30, 0)
typedef bool (*ScrollToPointFunc)(AtkComponent* component,
                                  AtkCoordType coords,
                                  gint x,
                                  gint y);
typedef bool (*ScrollToFunc)(AtkComponent* component, AtkScrollType type);

TEST_F(AXPlatformNodeAuraLinuxTest, AtkComponentScrollToPoint) {
  AXNodeData root;
  root.id = 1;
  root.role = ax::mojom::Role::kRootWebArea;
  root.relative_bounds.bounds = gfx::RectF(0, 0, 2000, 2000);

  AXNodeData child1;
  child1.id = 2;
  child1.role = ax::mojom::Role::kStaticText;
  child1.relative_bounds.bounds = gfx::RectF(10, 10, 10, 10);
  root.child_ids.push_back(2);

  Init(root, child1);

  AXNode* child_node = GetRoot()->children()[0];
  AtkObject* child_obj = AtkObjectFromNode(child_node);
  ASSERT_TRUE(ATK_IS_OBJECT(child_obj));
  ASSERT_TRUE(ATK_IS_COMPONENT(child_obj));
  g_object_ref(child_obj);

  int x_left, y_top, width, height;
  atk_component_get_extents(ATK_COMPONENT(child_obj), &x_left, &y_top, &width,
                            &height, ATK_XY_SCREEN);
  EXPECT_EQ(10, x_left);
  EXPECT_EQ(10, y_top);
  EXPECT_EQ(10, width);
  EXPECT_EQ(10, height);

  atk_component_scroll_to_point(ATK_COMPONENT(child_obj), ATK_XY_SCREEN, 600,
                                650);
  atk_component_get_extents(ATK_COMPONENT(child_obj), &x_left, &y_top, &width,
                            &height, ATK_XY_SCREEN);
  EXPECT_EQ(610, x_left);
  EXPECT_EQ(660, y_top);
  EXPECT_EQ(10, width);
  EXPECT_EQ(10, height);

  atk_component_scroll_to_point(ATK_COMPONENT(child_obj), ATK_XY_PARENT, 10,
                                10);
  atk_component_get_extents(ATK_COMPONENT(child_obj), &x_left, &y_top, &width,
                            &height, ATK_XY_SCREEN);
  // The test wrapper scrolls every element when scrolling, so this should be
  // 10 pixels to bottom and left of the current coordinates of the root.
  EXPECT_EQ(620, x_left);
  EXPECT_EQ(670, y_top);
  EXPECT_EQ(10, width);
  EXPECT_EQ(10, height);

  g_object_unref(child_obj);

  // Un-set the global offset so that it doesn't affect subsequent tests.
  TestAXNodeWrapper::SetGlobalCoordinateOffset(gfx::Vector2d(0, 0));
}

TEST_F(AXPlatformNodeAuraLinuxTest, AtkComponentScrollTo) {
  AXNodeData root;
  root.id = 1;
  root.role = ax::mojom::Role::kRootWebArea;
  root.relative_bounds.bounds = gfx::RectF(0, 0, 2000, 2000);

  AXNodeData child1;
  child1.id = 2;
  child1.role = ax::mojom::Role::kStaticText;
  child1.relative_bounds.bounds = gfx::RectF(10, 10, 10, 10);
  root.child_ids.push_back(2);

  Init(root, child1);

  AXNode* child_node = GetRoot()->children()[0];
  AtkObject* child_obj = AtkObjectFromNode(child_node);
  ASSERT_TRUE(ATK_IS_OBJECT(child_obj));
  ASSERT_TRUE(ATK_IS_COMPONENT(child_obj));
  g_object_ref(child_obj);

  int x_left, y_top, width, height;
  atk_component_get_extents(ATK_COMPONENT(child_obj), &x_left, &y_top, &width,
                            &height, ATK_XY_SCREEN);
  EXPECT_EQ(10, x_left);
  EXPECT_EQ(10, y_top);
  EXPECT_EQ(10, width);
  EXPECT_EQ(10, height);

  atk_component_scroll_to(ATK_COMPONENT(child_obj), ATK_SCROLL_ANYWHERE);
  atk_component_get_extents(ATK_COMPONENT(child_obj), &x_left, &y_top, &width,
                            &height, ATK_XY_SCREEN);
  EXPECT_EQ(0, x_left);
  EXPECT_EQ(0, y_top);
  EXPECT_EQ(10, width);
  EXPECT_EQ(10, height);

  // Un-set the global offset so that it doesn't affect subsequent tests.
  TestAXNodeWrapper::SetGlobalCoordinateOffset(gfx::Vector2d(0, 0));
}
#endif  //  ATK_CHECK_VERSION(2, 30, 0)

//
// AtkAction tests
//

TEST_F(AXPlatformNodeAuraLinuxTest, TestAtkActionGetNActions) {
  AXNodeData root;
  root.id = 1;
  root.role = ax::mojom::Role::kSlider;
  root.SetDefaultActionVerb(ax::mojom::DefaultActionVerb::kClick);
  root.AddAction(ax::mojom::Action::kDecrement);
  root.AddAction(ax::mojom::Action::kIncrement);
  // Additionally, any object will have a context menu action, that makes it 4
  Init(root);

  AtkObject* root_obj(GetRootAtkObject());
  ASSERT_TRUE(ATK_IS_OBJECT(root_obj));
  ASSERT_TRUE(ATK_IS_ACTION(root_obj));
  g_object_ref(root_obj);

  gint number_of_actions = atk_action_get_n_actions(ATK_ACTION(root_obj));

  EXPECT_EQ(4, number_of_actions);

  g_object_unref(root_obj);
}

TEST_F(AXPlatformNodeAuraLinuxTest, TestAtkActionGetNActionsNoActions) {
  AXNodeData root;
  root.id = 1;
  root.role = ax::mojom::Role::kRootWebArea;
  Init(root);

  AtkObject* root_obj(GetRootAtkObject());
  ASSERT_TRUE(ATK_IS_OBJECT(root_obj));
  ASSERT_TRUE(ATK_IS_ACTION(root_obj));
  g_object_ref(root_obj);

  gint number_of_actions = atk_action_get_n_actions(ATK_ACTION(root_obj));

  // In absence of any other actions, we would expose the default and the
  // context menu actions.
  EXPECT_EQ(2, number_of_actions);

  g_object_unref(root_obj);
}

TEST_F(AXPlatformNodeAuraLinuxTest, TestAtkActionGetName) {
  AXNodeData root;
  root.id = 1;
  root.role = ax::mojom::Role::kSlider;
  root.SetDefaultActionVerb(ax::mojom::DefaultActionVerb::kClick);
  root.AddAction(ax::mojom::Action::kDecrement);
  root.AddAction(ax::mojom::Action::kIncrement);
  Init(root);

  AtkObject* root_obj(GetRootAtkObject());
  ASSERT_TRUE(ATK_IS_OBJECT(root_obj));
  ASSERT_TRUE(ATK_IS_ACTION(root_obj));
  g_object_ref(root_obj);

  const gchar* action_name = atk_action_get_name(ATK_ACTION(root_obj), 0);
  // The index 0 is reserved for the default action, and the index 1 to the
  // context menu action. The rest of actions are presented in the order they
  // were added.
  EXPECT_STREQ("click", action_name);
  action_name = atk_action_get_name(ATK_ACTION(root_obj), 1);
  EXPECT_STREQ("showContextMenu", action_name);
  action_name = atk_action_get_name(ATK_ACTION(root_obj), 2);
  EXPECT_STREQ("decrement", action_name);
  action_name = atk_action_get_name(ATK_ACTION(root_obj), 3);
  EXPECT_STREQ("increment", action_name);

  g_object_unref(root_obj);
}

TEST_F(AXPlatformNodeAuraLinuxTest, TestAtkActionDoAction) {
  AXNodeData root;
  root.id = 1;
  root.role = ax::mojom::Role::kSlider;
  root.SetDefaultActionVerb(ax::mojom::DefaultActionVerb::kClick);
  root.AddAction(ax::mojom::Action::kDecrement);
  root.AddAction(ax::mojom::Action::kIncrement);
  Init(root);

  AtkObject* root_obj(GetRootAtkObject());
  ASSERT_TRUE(ATK_IS_OBJECT(root_obj));
  ASSERT_TRUE(ATK_IS_ACTION(root_obj));
  g_object_ref(root_obj);
  auto* root_node = GetRoot();

  EXPECT_TRUE(atk_action_do_action(ATK_ACTION(root_obj), 0));
  EXPECT_EQ(root_node, TestAXNodeWrapper::GetNodeFromLastDefaultAction());
  EXPECT_TRUE(atk_action_do_action(ATK_ACTION(root_obj), 1));
  EXPECT_TRUE(atk_action_do_action(ATK_ACTION(root_obj), 2));
  EXPECT_TRUE(atk_action_do_action(ATK_ACTION(root_obj), 3));

  // Test that querying actions out of bounds doesn't crash
  EXPECT_FALSE(atk_action_do_action(ATK_ACTION(root_obj), -1));
  EXPECT_FALSE(atk_action_do_action(ATK_ACTION(root_obj), 4));

  g_object_unref(root_obj);
}

TEST_F(AXPlatformNodeAuraLinuxTest, TestAtkActionAriaAction) {
  AXNodeData root;
  root.id = 1;
  root.role = ax::mojom::Role::kRootWebArea;
  root.AddIntListAttribute(ax::mojom::IntListAttribute::kActionsIds, {2, 3});

  AXNodeData child1;
  child1.id = 2;
  child1.role = ax::mojom::Role::kButton;
  child1.SetName("close");
  root.child_ids.push_back(2);

  AXNodeData child2;
  child2.id = 3;
  child2.role = ax::mojom::Role::kButton;
  child2.SetName("open");
  child2.AddStringAttribute(ax::mojom::StringAttribute::kHtmlId, "open-button");
  root.child_ids.push_back(3);

  Init(root, child1, child2);

  AtkObject* root_obj(GetRootAtkObject());
  ASSERT_TRUE(ATK_IS_OBJECT(root_obj));
  ASSERT_TRUE(ATK_IS_ACTION(root_obj));
  g_object_ref(root_obj);

  // Root node should have the default 2 actions (kDoDefault and
  // kShowContextMenu) + 2 actions from aria-actions (child1 and child2).
  gint number_of_actions = atk_action_get_n_actions(ATK_ACTION(root_obj));
  EXPECT_EQ(4, number_of_actions);

  // The third action refers to child1.
  const gchar* action_name = atk_action_get_name(ATK_ACTION(root_obj), 2);
  const gchar* action_localized_name =
      atk_action_get_localized_name(ATK_ACTION(root_obj), 2);
  EXPECT_STREQ("custom", action_name);
  EXPECT_STREQ("close", action_localized_name);
  EXPECT_TRUE(atk_action_do_action(ATK_ACTION(root_obj), 2));
  EXPECT_EQ(GetRoot()->GetChildAtIndex(0),
            TestAXNodeWrapper::GetNodeFromLastDefaultAction());

  // The fourth action refers to child2.
  action_name = atk_action_get_name(ATK_ACTION(root_obj), 3);
  action_localized_name =
      atk_action_get_localized_name(ATK_ACTION(root_obj), 3);
  EXPECT_STREQ("custom_open-button", action_name);
  EXPECT_STREQ("open", action_localized_name);
  EXPECT_TRUE(atk_action_do_action(ATK_ACTION(root_obj), 3));
  EXPECT_EQ(GetRoot()->GetChildAtIndex(1),
            TestAXNodeWrapper::GetNodeFromLastDefaultAction());

  g_object_unref(root_obj);
}

//
// AtkValue tests
//

TEST_F(AXPlatformNodeAuraLinuxTest, TestAtkValueGetCurrentValue) {
  AXNodeData root;
  root.id = 1;
  root.role = ax::mojom::Role::kSlider;
  root.AddFloatAttribute(ax::mojom::FloatAttribute::kValueForRange, 5.0);
  Init(root);

  AtkObject* root_obj(GetRootAtkObject());
  ASSERT_TRUE(ATK_IS_OBJECT(root_obj));
  ASSERT_TRUE(ATK_IS_VALUE(root_obj));
  g_object_ref(root_obj);

  GValue current_value = G_VALUE_INIT;
  atk_value_get_current_value(ATK_VALUE(root_obj), &current_value);

  EXPECT_EQ(G_TYPE_FLOAT, G_VALUE_TYPE(&current_value));
  EXPECT_EQ(5.0, g_value_get_float(&current_value));

  g_value_unset(&current_value);
  g_object_unref(root_obj);
}

TEST_F(AXPlatformNodeAuraLinuxTest, TestAtkValueGetMaximumValue) {
  AXNodeData root;
  root.id = 1;
  root.role = ax::mojom::Role::kSlider;
  root.AddFloatAttribute(ax::mojom::FloatAttribute::kMaxValueForRange, 5.0);
  Init(root);

  AtkObject* root_obj(GetRootAtkObject());
  ASSERT_TRUE(ATK_IS_OBJECT(root_obj));
  ASSERT_TRUE(ATK_IS_VALUE(root_obj));
  g_object_ref(root_obj);

  GValue max_value = G_VALUE_INIT;
  atk_value_get_maximum_value(ATK_VALUE(root_obj), &max_value);

  EXPECT_EQ(G_TYPE_FLOAT, G_VALUE_TYPE(&max_value));
  EXPECT_EQ(5.0, g_value_get_float(&max_value));

  g_value_unset(&max_value);
  g_object_unref(root_obj);
}

TEST_F(AXPlatformNodeAuraLinuxTest, TestAtkValueGetMinimumValue) {
  AXNodeData root;
  root.id = 1;
  root.role = ax::mojom::Role::kSlider;
  root.AddFloatAttribute(ax::mojom::FloatAttribute::kMinValueForRange, 5.0);
  Init(root);

  AtkObject* root_obj(GetRootAtkObject());
  ASSERT_TRUE(ATK_IS_OBJECT(root_obj));
  ASSERT_TRUE(ATK_IS_VALUE(root_obj));
  g_object_ref(root_obj);

  GValue min_value = G_VALUE_INIT;
  atk_value_get_minimum_value(ATK_VALUE(root_obj), &min_value);

  EXPECT_EQ(G_TYPE_FLOAT, G_VALUE_TYPE(&min_value));
  EXPECT_EQ(5.0, g_value_get_float(&min_value));

  g_value_unset(&min_value);
  g_object_unref(root_obj);
}

TEST_F(AXPlatformNodeAuraLinuxTest, TestAtkValueGetMinimumIncrement) {
  AXNodeData root;
  root.id = 1;
  root.role = ax::mojom::Role::kSlider;
  root.AddFloatAttribute(ax::mojom::FloatAttribute::kStepValueForRange, 5.0);
  Init(root);

  AtkObject* root_obj(GetRootAtkObject());
  ASSERT_TRUE(ATK_IS_OBJECT(root_obj));
  ASSERT_TRUE(ATK_IS_VALUE(root_obj));
  g_object_ref(root_obj);

  GValue increment = G_VALUE_INIT;
  atk_value_get_minimum_increment(ATK_VALUE(root_obj), &increment);

  EXPECT_EQ(G_TYPE_FLOAT, G_VALUE_TYPE(&increment));
  EXPECT_EQ(5.0, g_value_get_float(&increment));

  g_value_unset(&increment);
  g_object_unref(root_obj);
}

TEST_F(AXPlatformNodeAuraLinuxTest, TestAtkValueChangedSignal) {
  AXNodeData root;
  root.id = 1;
  root.role = ax::mojom::Role::kSlider;
  root.AddFloatAttribute(ax::mojom::FloatAttribute::kMaxValueForRange, 5.0);
  Init(root);

  AtkObject* root_object(GetRootAtkObject());
  ASSERT_TRUE(ATK_IS_OBJECT(root_object));
  ASSERT_TRUE(ATK_IS_VALUE(root_object));
  g_object_ref(root_object);

  bool saw_value_change = false;
  g_signal_connect(
      root_object, "property-change::accessible-value",
      G_CALLBACK(+[](AtkObject*, void* property, bool* saw_value_change) {
        *saw_value_change = true;
      }),
      &saw_value_change);

  GValue new_value = G_VALUE_INIT;
  g_value_init(&new_value, G_TYPE_FLOAT);

  g_value_set_float(&new_value, 24.0);
  ASSERT_TRUE(atk_value_set_current_value(ATK_VALUE(root_object), &new_value));
  GetRootPlatformNode()->NotifyAccessibilityEvent(
      ax::mojom::Event::kValueChanged);

  GValue current_value = G_VALUE_INIT;
  atk_value_get_current_value(ATK_VALUE(root_object), &current_value);
  EXPECT_EQ(G_TYPE_FLOAT, G_VALUE_TYPE(&current_value));
  EXPECT_EQ(24.0, g_value_get_float(&current_value));
  EXPECT_TRUE(saw_value_change);

  saw_value_change = false;
  g_value_set_float(&new_value, 100.0);
  ASSERT_TRUE(atk_value_set_current_value(ATK_VALUE(root_object), &new_value));
  GetRootPlatformNode()->NotifyAccessibilityEvent(
      ax::mojom::Event::kValueChanged);

  g_value_unset(&current_value);
  atk_value_get_current_value(ATK_VALUE(root_object), &current_value);
  EXPECT_EQ(G_TYPE_FLOAT, G_VALUE_TYPE(&current_value));
  EXPECT_EQ(100.0, g_value_get_float(&current_value));
  EXPECT_TRUE(saw_value_change);

  g_value_unset(&current_value);
  g_value_unset(&new_value);
  g_object_unref(root_object);
}

//
// AtkHyperlinkImpl interface
//

TEST_F(AXPlatformNodeAuraLinuxTest, TestAtkHyperlink) {
  AXNodeData root;
  root.id = 1;
  root.role = ax::mojom::Role::kLink;
  root.AddStringAttribute(ax::mojom::StringAttribute::kUrl, "http://foo.com");
  Init(root);

  AtkObject* root_obj(GetRootAtkObject());
  ASSERT_TRUE(ATK_IS_OBJECT(root_obj));
  ASSERT_TRUE(ATK_IS_HYPERLINK_IMPL(root_obj));
  g_object_ref(root_obj);

  AtkHyperlink* hyperlink(
      atk_hyperlink_impl_get_hyperlink(ATK_HYPERLINK_IMPL(root_obj)));
  ASSERT_TRUE(ATK_IS_HYPERLINK(hyperlink));

  EXPECT_EQ(1, atk_hyperlink_get_n_anchors(hyperlink));
  gchar* uri = atk_hyperlink_get_uri(hyperlink, 0);
  EXPECT_STREQ("http://foo.com", uri);
  g_free(uri);

  g_object_unref(hyperlink);
  g_object_unref(root_obj);
}

TEST_F(AXPlatformNodeAuraLinuxTest, TestAtkHyperlinkActions) {
  AXNodeData root;
  root.id = 1;
  root.role = ax::mojom::Role::kLink;
  root.AddStringAttribute(ax::mojom::StringAttribute::kUrl, "http://foo.com");
  root.SetDefaultActionVerb(ax::mojom::DefaultActionVerb::kClick);
  Init(root);

  AtkObject* root_obj(GetRootAtkObject());
  ASSERT_TRUE(ATK_IS_OBJECT(root_obj));
  ASSERT_TRUE(ATK_IS_HYPERLINK_IMPL(root_obj));
  ASSERT_TRUE(ATK_IS_ACTION(root_obj));
  g_object_ref(root_obj);
  auto* root_node = GetRoot();

  gint number_of_actions = atk_action_get_n_actions(ATK_ACTION(root_obj));
  EXPECT_EQ(2, number_of_actions);

  // The index 0 is reserved for the default action, and the index 1 to the
  // context menu action. The rest of actions are presented in the order they
  // were added.
  const gchar* action_name = atk_action_get_name(ATK_ACTION(root_obj), 0);
  EXPECT_STREQ("click", action_name);
  action_name = atk_action_get_name(ATK_ACTION(root_obj), 1);
  EXPECT_STREQ("showContextMenu", action_name);

  EXPECT_TRUE(atk_action_do_action(ATK_ACTION(root_obj), 0));
  EXPECT_EQ(root_node, TestAXNodeWrapper::GetNodeFromLastDefaultAction());

  g_object_unref(root_obj);
}

//
// AtkText interface
//
//

TEST_F(AXPlatformNodeAuraLinuxTest, TestAtkTextGetText) {
  AXNodeData root;
  root.id = 1;
  root.role = ax::mojom::Role::kTextField;
  root.AddStringAttribute(ax::mojom::StringAttribute::kValue, "A string.");
  Init(root);

  AtkObject* root_obj(GetRootAtkObject());
  ASSERT_TRUE(ATK_IS_OBJECT(root_obj));
  g_object_ref(root_obj);

  ASSERT_TRUE(ATK_IS_TEXT(root_obj));
  AtkText* atk_text = ATK_TEXT(root_obj);

  auto verify_text = [&](const char* expected, int start, int end) {
    char* actual = atk_text_get_text(atk_text, start, end);
    EXPECT_STREQ(expected, actual);
    g_free(actual);
  };

  verify_text("A string.", 0, -1);
  verify_text("A string.", 0, 20);
  verify_text("A string", 0, 8);
  verify_text("str", 2, 5);
  verify_text(".", 8, 9);
  verify_text("", 0, 0);
  verify_text(nullptr, -1, -1);
  verify_text(nullptr, 5, 2);
  verify_text(nullptr, 10, 20);

  g_object_unref(root_obj);
}

TEST_F(AXPlatformNodeAuraLinuxTest, TestAtkTextCharacterGranularity) {
  AXNodeData root;
  root.id = 1;
  root.role = ax::mojom::Role::kTextField;
  root.SetValue("A decently long string \xE2\x98\xBA with an emoji.");
  Init(root);

  AtkObject* root_obj(GetRootAtkObject());
  ASSERT_TRUE(ATK_IS_OBJECT(root_obj));
  g_object_ref(root_obj);

  ASSERT_TRUE(ATK_IS_TEXT(root_obj));
  AtkText* atk_text = ATK_TEXT(root_obj);

  EXPECT_EQ(static_cast<gunichar>('d'),
            atk_text_get_character_at_offset(atk_text, 2));
  EXPECT_EQ(0u, atk_text_get_character_at_offset(atk_text, -1));
  EXPECT_EQ(0u, atk_text_get_character_at_offset(atk_text, 42342));
  EXPECT_EQ(0x263Au, atk_text_get_character_at_offset(atk_text, 23));
  EXPECT_EQ(static_cast<gunichar>(' '),
            atk_text_get_character_at_offset(atk_text, 24));

  auto verify_text = [&](const char* expected_text, char* text,
                         int expected_start, int expected_end, int start,
                         int end) {
    EXPECT_STREQ(expected_text, text);
    EXPECT_EQ(start, expected_start);
    EXPECT_EQ(end, expected_end);
    g_free(text);
  };

  auto verify_text_at_offset = [&](const char* expected_text, int offset,
                                   int expected_start, int expected_end) {
    ::testing::Message message;
    message << "While checking at offset " << offset;
    SCOPED_TRACE(message);

    int start = 0, end = 0;
    char* text = atk_text_get_text_at_offset(
        atk_text, offset, ATK_TEXT_BOUNDARY_CHAR, &start, &end);
    verify_text(expected_text, text, expected_start, expected_end, start, end);
  };

  verify_text_at_offset("d", 2, 2, 3);
  verify_text_at_offset(nullptr, -1, 0, 0);
  verify_text_at_offset(nullptr, 42342, 0, 0);
  verify_text_at_offset("\xE2\x98\xBA", 23, 23, 24);
  verify_text_at_offset(" ", 24, 24, 25);

  auto verify_text_after_offset = [&](const char* expected_text, int offset,
                                      int expected_start, int expected_end) {
    ::testing::Message message;
    message << "While checking after offset " << offset;
    SCOPED_TRACE(message);

    int start = 0, end = 0;
    char* text = atk_text_get_text_after_offset(
        atk_text, offset, ATK_TEXT_BOUNDARY_CHAR, &start, &end);
    verify_text(expected_text, text, expected_start, expected_end, start, end);
  };

  verify_text_after_offset("d", 1, 2, 3);
  verify_text_after_offset(nullptr, 42342, -1, -1);
  verify_text_after_offset("\xE2\x98\xBA", 22, 23, 24);
  verify_text_after_offset(" ", 23, 24, 25);

  // This boundary condition is enforced by ATK for some reason.
  verify_text_after_offset(nullptr, -1, 0, 0);

  auto verify_text_before_offset = [&](const char* expected_text, int offset,
                                       int expected_start, int expected_end) {
    ::testing::Message message;
    message << "While checking before offset " << offset;
    SCOPED_TRACE(message);

    int start = 0, end = 0;
    char* text = atk_text_get_text_before_offset(
        atk_text, offset, ATK_TEXT_BOUNDARY_CHAR, &start, &end);
    verify_text(expected_text, text, expected_start, expected_end, start, end);
  };

  verify_text_before_offset("d", 3, 2, 3);
  verify_text_before_offset(nullptr, 42342, -1, -1);
  verify_text_before_offset("\xE2\x98\xBA", 24, 23, 24);
  verify_text_before_offset(" ", 25, 24, 25);
  verify_text_after_offset(nullptr, -1, 0, 0);

  g_object_unref(root_obj);
}

TEST_F(AXPlatformNodeAuraLinuxTest,
       AtkTextLineBoundaryRequestsInlineTextBoxes) {
  TestAXNodeWrapper::SetGlobalIsWebContent(true);
  Init(BuildTextField());

  AtkObject* root_obj = GetRootAtkObject();
  ASSERT_TRUE(ATK_IS_TEXT(root_obj));
  AtkText* atk_text = ATK_TEXT(root_obj);

  const int initial_call_count =
      AXPlatformForTest::GetInstance()
          .inline_text_boxes_used_in_web_content_count();

  EXPECT_GT(atk_text_get_character_count(atk_text), 0);
  EXPECT_EQ(initial_call_count,
            AXPlatformForTest::GetInstance()
                .inline_text_boxes_used_in_web_content_count());

  int start_offset = -1;
  int end_offset = -1;
  char* text = atk_text_get_text_at_offset(
      atk_text, 0, ATK_TEXT_BOUNDARY_LINE_START, &start_offset, &end_offset);
  g_free(text);
  EXPECT_EQ(initial_call_count + 1,
            AXPlatformForTest::GetInstance()
                .inline_text_boxes_used_in_web_content_count());
}

TEST_F(AXPlatformNodeAuraLinuxTest, AtkTextGeometryRequestsInlineTextBoxes) {
  TestAXNodeWrapper::SetGlobalIsWebContent(true);
  Init(BuildTextField());

  AtkObject* root_obj = GetRootAtkObject();
  ASSERT_TRUE(ATK_IS_TEXT(root_obj));
  AtkText* atk_text = ATK_TEXT(root_obj);

  const int initial_call_count =
      AXPlatformForTest::GetInstance()
          .inline_text_boxes_used_in_web_content_count();

  int x;
  int y;
  int width;
  int height;
  atk_text_get_character_extents(atk_text, 0, &x, &y, &width, &height,
                                 ATK_XY_SCREEN);

  AtkTextRectangle rectangle;
  atk_text_get_range_extents(atk_text, 0, 1, ATK_XY_SCREEN, &rectangle);

  atk_text_get_offset_at_point(atk_text, 0, 0, ATK_XY_SCREEN);

  EXPECT_EQ(initial_call_count + 3,
            AXPlatformForTest::GetInstance()
                .inline_text_boxes_used_in_web_content_count());
}

struct GetTextSegmentTest {
  int offset;
  const char* content;
  int start_offset;
  int end_offset;
};

TEST_F(AXPlatformNodeAuraLinuxTest, TestAtkTextWordGranularity) {
  AXNodeData root;
  root.id = 1;
  root.role = ax::mojom::Role::kTextField;
  root.AddStringAttribute(ax::mojom::StringAttribute::kValue,
                          "A decently long string.");
  Init(root);

  AtkObject* root_obj(GetRootAtkObject());
  ASSERT_TRUE(ATK_IS_OBJECT(root_obj));
  g_object_ref(root_obj);

  ASSERT_TRUE(ATK_IS_TEXT(root_obj));
  AtkText* atk_text = ATK_TEXT(root_obj);

  static GetTextSegmentTest tests[] = {{0, "A ", 0, 2},
                                       {2, "decently ", 2, 11},
                                       {-1, nullptr, -1, -1},
                                       {1000, nullptr, -1, -1}};

  for (const auto& test : tests) {
    ::testing::Message message;
    message << "While checking at index " << test.offset << " for \'"
            << test.content << "\' at " << test.start_offset << '-'
            << test.end_offset << '.';
    SCOPED_TRACE(message);

    int start_offset = -1, end_offset = -1;
    char* content = atk_text_get_text_at_offset(atk_text, test.offset,
                                                ATK_TEXT_BOUNDARY_WORD_START,
                                                &start_offset, &end_offset);
    EXPECT_STREQ(content, test.content);
    EXPECT_EQ(start_offset, test.start_offset);
    EXPECT_EQ(end_offset, test.end_offset);
    g_free(content);
  }

#if ATK_CHECK_VERSION(2, 10, 0)
  for (const auto& test : tests) {
    ::testing::Message message;
    message << "While checking at index " << test.offset << " for \'"
            << test.content << "\' at " << test.start_offset << '-'
            << test.end_offset << '.';
    SCOPED_TRACE(message);

    int start_offset = -1, end_offset = -1;
    char* content = atk_text_get_string_at_offset(atk_text, test.offset,
                                                  ATK_TEXT_GRANULARITY_WORD,
                                                  &start_offset, &end_offset);
    ASSERT_STREQ(content, test.content) << "with test offset=" << test.offset;
    ASSERT_EQ(start_offset, test.start_offset)
        << "with test offset=" << test.offset;
    ASSERT_EQ(end_offset, test.end_offset)
        << "with test offset=" << test.offset;
    g_free(content);
  }
#endif

  g_object_unref(root_obj);
}

TEST_F(AXPlatformNodeAuraLinuxTest, TestAtkTextSentenceGranularity) {
  AXNodeData root;
  root.id = 1;
  root.role = ax::mojom::Role::kTextField;
  root.AddStringAttribute(ax::mojom::StringAttribute::kValue,
                          "A short sentence. Another sentence.     A third...");
  Init(root);

  AtkObject* root_obj(GetRootAtkObject());
  ASSERT_TRUE(ATK_IS_OBJECT(root_obj));
  g_object_ref(root_obj);

  ASSERT_TRUE(ATK_IS_TEXT(root_obj));
  AtkText* atk_text = ATK_TEXT(root_obj);

  static GetTextSegmentTest tests[] = {
      {0, "A short sentence. ", 0, 18},
      {20, "Another sentence.     ", 18, 40},
      {37, "Another sentence.     ", 18, 40},
      {49, "A third...", 40, 50},
      {-1, nullptr, -1, -1},
      {-1000, nullptr, -1, -1},
      {1000, nullptr, -1, -1},
  };

  for (const auto& test : tests) {
    ::testing::Message message;
    message << "While checking at index " << test.offset << " for \'"
            << test.content << "\' at " << test.start_offset << '-'
            << test.end_offset << '.';
    SCOPED_TRACE(message);

    int start_offset = -1, end_offset = -1;
    char* content = atk_text_get_text_at_offset(
        atk_text, test.offset, ATK_TEXT_BOUNDARY_SENTENCE_START, &start_offset,
        &end_offset);
    ASSERT_STREQ(content, test.content);
    ASSERT_EQ(start_offset, test.start_offset);
    ASSERT_EQ(end_offset, test.end_offset);
    g_free(content);
  }

#if ATK_CHECK_VERSION(2, 10, 0)
  for (const auto& test : tests) {
    ::testing::Message message;
    message << "While checking at index " << test.offset << " for \'"
            << test.content << "\' at " << test.start_offset << '-'
            << test.end_offset << '.';

    int start_offset = -1, end_offset = -1;
    char* content = atk_text_get_string_at_offset(atk_text, test.offset,
                                                  ATK_TEXT_GRANULARITY_SENTENCE,
                                                  &start_offset, &end_offset);
    ASSERT_STREQ(content, test.content);
    ASSERT_EQ(start_offset, test.start_offset);
    ASSERT_EQ(end_offset, test.end_offset);
    g_free(content);
  }
#endif

  g_object_unref(root_obj);
}

TEST_F(AXPlatformNodeAuraLinuxTest, TestAtkTextInvalidBoundaryAndGranularity) {
  AXNodeData root;
  root.id = 1;
  root.role = ax::mojom::Role::kTextField;
  root.AddStringAttribute(ax::mojom::StringAttribute::kValue,
                          "A decently long string.");
  Init(root);

  AtkObject* root_obj(GetRootAtkObject());
  ASSERT_TRUE(ATK_IS_OBJECT(root_obj));
  g_object_ref(root_obj);

  ASSERT_TRUE(ATK_IS_TEXT(root_obj));
  AtkText* atk_text = ATK_TEXT(root_obj);

  // The AT-SPI bridge can pass a boundary as an unvalidated integer.
  // No text is returned for an unrecognized boundary (7 in this case).
  int start_offset = 0, end_offset = 0;
  char* content = atk_text_get_text_at_offset(
      atk_text, 0, static_cast<AtkTextBoundary>(7), &start_offset, &end_offset);
  EXPECT_EQ(content, nullptr);
  EXPECT_EQ(start_offset, -1);
  EXPECT_EQ(end_offset, -1);
  g_free(content);

#if ATK_CHECK_VERSION(2, 10, 0)
  // Likewise for an unrecognized granularity (5 in this case).
  start_offset = 0;
  end_offset = 0;
  content = atk_text_get_string_at_offset(atk_text, 0,
                                          static_cast<AtkTextGranularity>(5),
                                          &start_offset, &end_offset);
  EXPECT_EQ(content, nullptr);
  EXPECT_EQ(start_offset, -1);
  EXPECT_EQ(end_offset, -1);
  g_free(content);
#endif

  g_object_unref(root_obj);
}

#if ATK_CHECK_VERSION(2, 10, 0)
TEST_F(AXPlatformNodeAuraLinuxTest, DISABLED_TestAtkTextParagraphGranularity) {
  // TODO(nektar): Enable navigating by paragraphs in plain text.
  AXNodeData root;
  root.id = 1;
  root.role = ax::mojom::Role::kTextField;
  root.AddStringAttribute(
      ax::mojom::StringAttribute::kValue,
      "A short paragraph. \nAnother paragraph.\nA third...");
  Init(root);

  AtkObject* root_obj(GetRootAtkObject());
  ASSERT_TRUE(ATK_IS_OBJECT(root_obj));
  g_object_ref(root_obj);

  ASSERT_TRUE(ATK_IS_TEXT(root_obj));
  AtkText* atk_text = ATK_TEXT(root_obj);

  static GetTextSegmentTest tests[] = {
      {0, "A short paragraph. ", 0, 19},
      {25, "Another paragraph.", 20, 38},
      {-1, nullptr, -1, -1},
      {12345, nullptr, -1, -1},
  };

  for (const auto& test : tests) {
    int start_offset = -1, end_offset = -1;
    char* content = atk_text_get_string_at_offset(
        atk_text, test.offset, ATK_TEXT_GRANULARITY_PARAGRAPH, &start_offset,
        &end_offset);
    ASSERT_STREQ(content, test.content) << "with test offset=" << test.offset;
    ASSERT_EQ(start_offset, test.start_offset)
        << "with test offset=" << test.offset;
    ASSERT_EQ(end_offset, test.end_offset)
        << "with test offset=" << test.offset;
    g_free(content);
  }
#endif

  g_object_unref(root_obj);
}

TEST_F(AXPlatformNodeAuraLinuxTest, TestAtkTextWithNonBMPCharacters) {
  AXNodeData root;
  root.id = 1;
  root.role = ax::mojom::Role::kTextField;

  // The playing card emoji in this string should be considered a single
  // character offset for all AtkText API calls.
  static const char root_text[] =
      "\xF0\x9F\x83\x8f a decently long \xF0\x9F\x83\x8f string "
      "\xF0\x9F\x83\x8f.";
  root.AddStringAttribute(ax::mojom::StringAttribute::kValue, root_text);
  Init(root);

  AtkObject* root_obj(GetRootAtkObject());
  ASSERT_TRUE(ATK_IS_OBJECT(root_obj));
  g_object_ref(root_obj);

  ASSERT_TRUE(ATK_IS_TEXT(root_obj));
  AtkText* atk_text = ATK_TEXT(root_obj);

  int root_text_length = g_utf8_strlen(root_text, -1);
  ASSERT_EQ(atk_text_get_character_count(atk_text), root_text_length);

  for (int i = 0; i < root_text_length; i++) {
    ::testing::Message message;
    message << "Checking character at offset " << i;
    SCOPED_TRACE(message);

    gunichar character = atk_text_get_character_at_offset(atk_text, i);
    gunichar expected_character =
        g_utf8_get_char_validated(g_utf8_offset_to_pointer(root_text, i), -1);
    ASSERT_EQ(character, expected_character);

    int start_offset = -1, end_offset = -1;
    char* char_string = atk_text_get_text_at_offset(
        atk_text, i, ATK_TEXT_BOUNDARY_CHAR, &start_offset, &end_offset);
    character = g_utf8_get_char_validated(char_string, -1);
    ASSERT_EQ(character, expected_character);
    ASSERT_EQ(start_offset, i);
    ASSERT_EQ(end_offset, i + 1);
    g_free(char_string);

#if ATK_CHECK_VERSION(2, 10, 0)
    start_offset = -1;
    end_offset = -1;
    char_string = atk_text_get_string_at_offset(
        atk_text, i, ATK_TEXT_GRANULARITY_CHAR, &start_offset, &end_offset);

    character = g_utf8_get_char_validated(char_string, -1);
    ASSERT_EQ(character, expected_character);
    ASSERT_EQ(start_offset, i);
    ASSERT_EQ(end_offset, i + 1);
    g_free(char_string);
#endif
  }

  static auto tests = std::to_array<GetTextSegmentTest>(
      {{0, "\xF0\x9F\x83\x8f ", 0, 2}, {6, "decently ", 4, 13}});

  for (const auto& test : tests) {
    int start_offset = -1, end_offset = -1;
    char* word = atk_text_get_text_at_offset(atk_text, test.offset,
                                             ATK_TEXT_BOUNDARY_WORD_START,
                                             &start_offset, &end_offset);
    ::testing::Message message;
    message << "Checking test with index=" << test.offset
            << " and expected text=\'" << test.content << "\' at "
            << tests[1].start_offset << '-' << tests[1].end_offset << '.';
    SCOPED_TRACE(message);

    ASSERT_STREQ(word, test.content);
    ASSERT_EQ(start_offset, test.start_offset);
    ASSERT_EQ(end_offset, test.end_offset);

    g_free(word);
  }

  g_object_unref(root_obj);
}

TEST_F(AXPlatformNodeAuraLinuxTest, TestAtkTextCaretMoved) {
  Init(BuildTextField());

  AtkObject* root_atk_object(GetRootAtkObject());
  EXPECT_TRUE(ATK_IS_OBJECT(root_atk_object));
  g_object_ref(root_atk_object);

  ASSERT_TRUE(ATK_IS_TEXT(root_atk_object));
  AtkText* atk_text = ATK_TEXT(root_atk_object);

  int caret_position_from_event = -1;
  g_signal_connect(atk_text, "text-caret-moved",
                   G_CALLBACK(+[](AtkText*, int new_position, gpointer data) {
                     int* caret_position_from_event = static_cast<int*>(data);
                     *caret_position_from_event = new_position;
                   }),
                   &caret_position_from_event);

  atk_text_set_caret_offset(atk_text, 4);
  GetRootPlatformNode()->NotifyAccessibilityEvent(
      ax::mojom::Event::kTextSelectionChanged);
  ASSERT_EQ(atk_text_get_caret_offset(atk_text), 4);
  ASSERT_EQ(caret_position_from_event, 4);

  // Setting the same position should not trigger another event.
  caret_position_from_event = -1;
  atk_text_set_caret_offset(atk_text, 4);
  GetRootPlatformNode()->NotifyAccessibilityEvent(
      ax::mojom::Event::kTextSelectionChanged);
  ASSERT_EQ(atk_text_get_caret_offset(atk_text), 4);
  ASSERT_EQ(caret_position_from_event, -1);

  int character_count = atk_text_get_character_count(atk_text);
  atk_text_set_caret_offset(atk_text, -1);
  GetRootPlatformNode()->NotifyAccessibilityEvent(
      ax::mojom::Event::kTextSelectionChanged);
  ASSERT_EQ(atk_text_get_caret_offset(atk_text), character_count);
  ASSERT_EQ(caret_position_from_event, character_count);

  atk_text_set_caret_offset(atk_text, 0);  // Reset position.
  GetRootPlatformNode()->NotifyAccessibilityEvent(
      ax::mojom::Event::kTextSelectionChanged);

  caret_position_from_event = -1;
  atk_text_set_caret_offset(atk_text, -1000);
  GetRootPlatformNode()->NotifyAccessibilityEvent(
      ax::mojom::Event::kTextSelectionChanged);
  ASSERT_EQ(atk_text_get_caret_offset(atk_text), character_count);
  ASSERT_EQ(caret_position_from_event, character_count);

  atk_text_set_caret_offset(atk_text, 0);  // Reset position.
  GetRootPlatformNode()->NotifyAccessibilityEvent(
      ax::mojom::Event::kTextSelectionChanged);

  caret_position_from_event = -1;
  atk_text_set_caret_offset(atk_text, 1000);
  GetRootPlatformNode()->NotifyAccessibilityEvent(
      ax::mojom::Event::kTextSelectionChanged);
  ASSERT_EQ(atk_text_get_caret_offset(atk_text), character_count);
  ASSERT_EQ(caret_position_from_event, character_count);

  caret_position_from_event = -1;
  atk_text_set_caret_offset(atk_text, character_count - 1);
  GetRootPlatformNode()->NotifyAccessibilityEvent(
      ax::mojom::Event::kTextSelectionChanged);
  ASSERT_EQ(atk_text_get_caret_offset(atk_text), character_count - 1);
  ASSERT_EQ(caret_position_from_event, character_count - 1);

  g_object_unref(root_atk_object);
}

class ActivationTester {
 public:
  explicit ActivationTester(AtkObject* target) : target_(target) {
    auto callback = G_CALLBACK(+[](AtkWindow*, bool* flag) { *flag = true; });
    activate_id_ =
        g_signal_connect(target, "activate", callback, &saw_activate_);
    deactivate_id_ =
        g_signal_connect(target, "deactivate", callback, &saw_deactivate_);

    DCHECK(activate_id_);
    DCHECK(deactivate_id_);
    DCHECK(activate_id_ != deactivate_id_);
  }

  bool IsActivatedInStateSet() {
    return AtkObjectHasState(target_, ATK_STATE_ACTIVE);
  }

  void Reset() {
    saw_activate_ = false;
    saw_deactivate_ = false;
  }

  ~ActivationTester() {
    g_signal_handler_disconnect(target_, activate_id_);
    g_signal_handler_disconnect(target_, deactivate_id_);
  }

  raw_ptr<AtkObject> target_;
  bool saw_activate_ = false;
  bool saw_deactivate_ = false;
  gulong activate_id_ = 0;
  gulong deactivate_id_ = 0;
};

//
// AtkWindow interface and active state
//
//
TEST_F(AXPlatformNodeAuraLinuxTest, TestAtkWindowActive) {
  ScopedAtSpiReady enable_at_spi;

  AXNodeData root;
  root.id = 1;
  root.role = ax::mojom::Role::kWindow;
  root.child_ids.push_back(2);

  AXNodeData child;
  child.id = 2;
  child.role = ax::mojom::Role::kCheckBox;

  Init(root, child);

  AtkObject* root_atk_object(GetRootAtkObject());
  EXPECT_TRUE(ATK_IS_OBJECT(root_atk_object));
  g_object_ref(root_atk_object);

  EXPECT_TRUE(ATK_IS_WINDOW(root_atk_object));

  AXNode* checkbox_node = GetRoot()->children()[0];
  AtkObject* checkbox_atk_obj = AtkObjectFromNode(checkbox_node);

  // Focus the checkbox to ensure that it also gets new focus events when
  // the toplevel window goes from unfocused to focused.
  GetPlatformNode(checkbox_node)
      ->NotifyAccessibilityEvent(ax::mojom::Event::kFocus);

  bool saw_active_focus_state_change = false;
  g_signal_connect(checkbox_atk_obj, "state-change",
                   G_CALLBACK(+[](AtkObject* atkobject, gchar* state_changed,
                                  gboolean new_value, bool* flag) {
                     if (!g_strcmp0(state_changed, "focused") && new_value)
                       *flag = true;
                   }),
                   &saw_active_focus_state_change);

  {
    ActivationTester tester(root_atk_object);
    EXPECT_FALSE(tester.IsActivatedInStateSet());
    static_cast<AXPlatformNodeAuraLinux*>(GetRootPlatformNode())
        ->NotifyAccessibilityEvent(ax::mojom::Event::kWindowActivated);
    EXPECT_TRUE(tester.saw_activate_);
    EXPECT_FALSE(tester.saw_deactivate_);
    EXPECT_TRUE(tester.IsActivatedInStateSet());
    EXPECT_TRUE(saw_active_focus_state_change);
  }

  {
    saw_active_focus_state_change = false;

    ActivationTester tester(root_atk_object);
    static_cast<AXPlatformNodeAuraLinux*>(GetRootPlatformNode())
        ->NotifyAccessibilityEvent(ax::mojom::Event::kWindowDeactivated);
    EXPECT_FALSE(tester.saw_activate_);
    EXPECT_TRUE(tester.saw_deactivate_);
    EXPECT_FALSE(tester.IsActivatedInStateSet());
    EXPECT_FALSE(saw_active_focus_state_change);
  }

  g_object_unref(root_atk_object);
}

TEST_F(AXPlatformNodeAuraLinuxTest, TestPostponedAtkWindowActive) {
  AXNodeData root;
  root.id = 1;
  root.role = ax::mojom::Role::kWindow;
  Init(root);

  AtkObject* root_atk_object(GetRootAtkObject());
  EXPECT_TRUE(ATK_IS_OBJECT(root_atk_object));
  g_object_ref(root_atk_object);
  EXPECT_TRUE(ATK_IS_WINDOW(root_atk_object));

  AtkUtilAuraLinux* atk_util = AtkUtilAuraLinux::GetInstance();

  {
    ActivationTester tester(root_atk_object);
    EXPECT_FALSE(tester.IsActivatedInStateSet());
    static_cast<AXPlatformNodeAuraLinux*>(GetRootPlatformNode())
        ->NotifyAccessibilityEvent(ax::mojom::Event::kWindowActivated);

    // ATK window activated event will be held until AT-SPI bridge is ready.
    EXPECT_FALSE(tester.saw_activate_);
    EXPECT_FALSE(tester.saw_deactivate_);

    // We force the AT-SPI ready flag to flush any held events.
    atk_util->SetAtSpiReady(true);
    EXPECT_TRUE(tester.saw_activate_);
    EXPECT_FALSE(tester.saw_deactivate_);
    EXPECT_TRUE(tester.IsActivatedInStateSet());
  }

  {
    ActivationTester tester(root_atk_object);

    static_cast<AXPlatformNodeAuraLinux*>(GetRootPlatformNode())
        ->NotifyAccessibilityEvent(ax::mojom::Event::kWindowDeactivated);

    EXPECT_FALSE(tester.saw_activate_);
    EXPECT_TRUE(tester.saw_deactivate_);
    EXPECT_FALSE(tester.IsActivatedInStateSet());
  }

  {
    atk_util->SetAtSpiReady(false);

    ActivationTester tester(root_atk_object);

    static_cast<AXPlatformNodeAuraLinux*>(GetRootPlatformNode())
        ->NotifyAccessibilityEvent(ax::mojom::Event::kWindowActivated);

    // Window deactivated will cancel the previously held activated event.
    static_cast<AXPlatformNodeAuraLinux*>(GetRootPlatformNode())
        ->NotifyAccessibilityEvent(ax::mojom::Event::kWindowDeactivated);

    // We force the AT-SPI ready flag to flush any held events.
    atk_util->SetAtSpiReady(true);

    // No events seen because they cancelled each other.
    EXPECT_FALSE(tester.saw_activate_);
    EXPECT_FALSE(tester.saw_deactivate_);
  }

  // Set AtSpiReady state back to its default state, so it doesn't affect other
  // tests run in the same batch.
  atk_util->SetAtSpiReady(false);

  g_object_unref(root_atk_object);
}

//
// AtkWindow interface and iconified state
//
TEST_F(AXPlatformNodeAuraLinuxTest, TestAtkWindowMinimized) {
  AXNodeData root;
  root.id = 1;
  root.role = ax::mojom::Role::kWindow;
  Init(root);

  AtkObject* root_atk_object(GetRootAtkObject());
  EXPECT_TRUE(ATK_IS_OBJECT(root_atk_object));
  g_object_ref(root_atk_object);

  EXPECT_TRUE(ATK_IS_WINDOW(root_atk_object));
  EXPECT_FALSE(AtkObjectHasState(root_atk_object, ATK_STATE_ICONIFIED));

  GetRootWrapper()->set_minimized(true);
  EXPECT_TRUE(AtkObjectHasState(root_atk_object, ATK_STATE_ICONIFIED));

  bool saw_state_change = false;
  g_signal_connect(root_atk_object, "state-change",
                   G_CALLBACK(+[](AtkObject* atkobject, gchar* state_changed,
                                  gboolean new_value, bool* flag) {
                     if (!g_strcmp0(state_changed, "iconified"))
                       *flag = true;
                   }),
                   &saw_state_change);

  AXPlatformNodeAuraLinux* root_node = GetRootPlatformNode();
  static_cast<AXPlatformNodeAuraLinux*>(root_node)->NotifyAccessibilityEvent(
      ax::mojom::Event::kWindowVisibilityChanged);

  EXPECT_TRUE(saw_state_change);

  saw_state_change = false;
  static_cast<AXPlatformNodeAuraLinux*>(root_node)->NotifyAccessibilityEvent(
      ax::mojom::Event::kWindowVisibilityChanged);
  EXPECT_FALSE(saw_state_change);

  GetRootWrapper()->set_minimized(false);
  static_cast<AXPlatformNodeAuraLinux*>(root_node)->NotifyAccessibilityEvent(
      ax::mojom::Event::kWindowVisibilityChanged);
  EXPECT_TRUE(saw_state_change);

  g_object_unref(root_atk_object);
}

TEST_F(AXPlatformNodeAuraLinuxTest, TestFocusTriggersAtkWindowActive) {
  AXNodeData root;
  root.id = 1;
  root.role = ax::mojom::Role::kWindow;
  root.child_ids.push_back(2);

  AXNodeData child_node_data;
  child_node_data.id = 2;
  child_node_data.role = ax::mojom::Role::kButton;

  Init(root, child_node_data);

  AtkObject* root_atk_object(GetRootAtkObject());
  EXPECT_TRUE(ATK_IS_OBJECT(root_atk_object));
  EXPECT_TRUE(ATK_IS_WINDOW(root_atk_object));

  g_object_ref(root_atk_object);

  AXNode* child_node = GetRoot()->children()[0];

  // A focus event on a child node should not cause the window to
  // activate.
  {
    ActivationTester tester(root_atk_object);
    GetPlatformNode(child_node)
        ->NotifyAccessibilityEvent(ax::mojom::Event::kFocus);
    EXPECT_FALSE(tester.saw_activate_);
    EXPECT_FALSE(tester.saw_deactivate_);
    EXPECT_FALSE(tester.IsActivatedInStateSet());
  }

  // A focus event on the window itself should cause the window to activate.
  {
    ActivationTester tester(root_atk_object);
    GetRootPlatformNode()->NotifyAccessibilityEvent(ax::mojom::Event::kFocus);
    EXPECT_TRUE(tester.saw_activate_);
    EXPECT_FALSE(tester.saw_deactivate_);
    EXPECT_TRUE(tester.IsActivatedInStateSet());
  }

  // Since the window is already active, we shouldn't see another activation
  // event, but it should still be active.
  {
    ActivationTester tester(root_atk_object);
    GetRootPlatformNode()->NotifyAccessibilityEvent(ax::mojom::Event::kFocus);
    EXPECT_FALSE(tester.saw_activate_);
    EXPECT_FALSE(tester.saw_deactivate_);
    EXPECT_TRUE(tester.IsActivatedInStateSet());
  }

  g_object_unref(root_atk_object);
}

TEST_F(AXPlatformNodeAuraLinuxTest, TestAtkPopupWindowActive) {
  ScopedAtSpiReady enable_at_spi;

  AXNodeData root;
  root.id = 1;
  root.role = ax::mojom::Role::kApplication;
  root.child_ids.push_back(2);
  root.child_ids.push_back(4);

  AXNodeData window_node_data;
  window_node_data.id = 2;
  window_node_data.role = ax::mojom::Role::kWindow;
  window_node_data.child_ids.push_back(3);

  AXNodeData document_node_data;
  document_node_data.id = 3;
  document_node_data.role = ax::mojom::Role::kRootWebArea;

  AXNodeData menu_node_data;
  menu_node_data.id = 4;
  menu_node_data.role = ax::mojom::Role::kWindow;
  menu_node_data.child_ids.push_back(5);

  AXNodeData menu_item_data;
  menu_item_data.id = 5;

  Init(root, window_node_data, document_node_data, menu_node_data,
       menu_item_data);

  AtkObject* root_atk_object(GetRootAtkObject());
  EXPECT_TRUE(ATK_IS_OBJECT(root_atk_object));
  g_object_ref(root_atk_object);

  AXNode* window_node = GetRoot()->children()[0];
  AtkObject* window_atk_node(AtkObjectFromNode(window_node));

  AXNode* document_node = window_node->children()[0];
  AtkObject* document_atk_node(AtkObjectFromNode(document_node));
  EXPECT_EQ(ATK_ROLE_DOCUMENT_WEB, atk_object_get_role(document_atk_node));
  int focus_events_on_original_node = 0;
  g_signal_connect(
      document_atk_node, "focus-event",
      G_CALLBACK(+[](AtkObject* atkobject, gint focused, int* focus_events) {
        if (focused)
          *focus_events += 1;
      }),
      &focus_events_on_original_node);
  atk_component_grab_focus(ATK_COMPONENT(document_atk_node));

  ActivationTester toplevel_tester(window_atk_node);
  GetPlatformNode(window_node)
      ->NotifyAccessibilityEvent(ax::mojom::Event::kWindowActivated);
  EXPECT_TRUE(toplevel_tester.saw_activate_);
  EXPECT_FALSE(toplevel_tester.saw_deactivate_);
  EXPECT_TRUE(toplevel_tester.IsActivatedInStateSet());

  toplevel_tester.Reset();

  AXNode* menu_node = GetRoot()->children()[1];
  AtkObject* menu_atk_node(AtkObjectFromNode(menu_node));
  {
    ActivationTester tester(menu_atk_node);
    GetPlatformNode(menu_node)->NotifyAccessibilityEvent(
        ax::mojom::Event::kMenuPopupStart);
    EXPECT_TRUE(tester.saw_activate_);
    EXPECT_FALSE(tester.saw_deactivate_);
    EXPECT_TRUE(tester.IsActivatedInStateSet());
    EXPECT_EQ(focus_events_on_original_node, 0);
  }

  EXPECT_FALSE(toplevel_tester.saw_activate_);
  EXPECT_TRUE(toplevel_tester.saw_deactivate_);

  toplevel_tester.Reset();

  {
    ActivationTester tester(menu_atk_node);
    GetPlatformNode(menu_node)->NotifyAccessibilityEvent(
        ax::mojom::Event::kMenuPopupEnd);
    EXPECT_FALSE(tester.saw_activate_);
    EXPECT_TRUE(tester.saw_deactivate_);
    EXPECT_FALSE(tester.IsActivatedInStateSet());
    EXPECT_EQ(focus_events_on_original_node, 1);
  }

  // Now that the menu is definitively closed, activation should have returned
  // to the previously activated toplevel frame.
  EXPECT_TRUE(toplevel_tester.saw_activate_);
  EXPECT_FALSE(toplevel_tester.saw_deactivate_);

  // Now we test opening the menu and closing it without hiding any submenus.
  // The toplevel should lose and then regain focus.
  focus_events_on_original_node = 0;
  toplevel_tester.Reset();

  GetPlatformNode(menu_node)->NotifyAccessibilityEvent(
      ax::mojom::Event::kMenuPopupStart);
  GetPlatformNode(menu_node)->NotifyAccessibilityEvent(
      ax::mojom::Event::kMenuPopupEnd);
  EXPECT_TRUE(toplevel_tester.saw_activate_);
  EXPECT_TRUE(toplevel_tester.saw_deactivate_);

  // The menu has closed so the original node should have received focus again.
  EXPECT_EQ(focus_events_on_original_node, 1);

  g_object_unref(root_atk_object);
}

TEST_F(AXPlatformNodeAuraLinuxTest, TestAtkSelectionInterface) {
  TestAXTreeUpdate update(std::string(R"HTML(
    ++1 kListBox states=kFocusable,kMultiselectable
    ++++2 kListBoxOption
    ++++3 kListBoxOption
    ++++4 kListBoxOption
    ++++5 kListItem
  )HTML"));

  Init(update);

  AtkObject* root_atk_object(GetRootAtkObject());
  EXPECT_TRUE(ATK_IS_OBJECT(root_atk_object));
  g_object_ref(root_atk_object);

  ASSERT_TRUE(ATK_IS_SELECTION(root_atk_object));

  ASSERT_TRUE(ATK_IS_SELECTION(root_atk_object));
  AtkSelection* selection = ATK_SELECTION(root_atk_object);
  ASSERT_EQ(atk_selection_get_selection_count(selection), 0);
  ASSERT_FALSE(atk_selection_is_child_selected(selection, 0));
  ASSERT_FALSE(atk_selection_is_child_selected(selection, 1));
  ASSERT_FALSE(atk_selection_is_child_selected(selection, 2));
  ASSERT_FALSE(atk_selection_is_child_selected(selection, 3));

  ASSERT_FALSE(atk_selection_is_child_selected(selection, -1));
  ASSERT_FALSE(atk_selection_is_child_selected(selection, -100));
  ASSERT_FALSE(atk_selection_is_child_selected(selection, 4));
  ASSERT_FALSE(atk_selection_is_child_selected(selection, 3000));

  ASSERT_TRUE(atk_selection_select_all_selection(selection));
  ASSERT_EQ(atk_selection_get_selection_count(selection), 3);
  ASSERT_TRUE(atk_selection_is_child_selected(selection, 0));
  ASSERT_TRUE(atk_selection_is_child_selected(selection, 1));
  ASSERT_TRUE(atk_selection_is_child_selected(selection, 2));
  ASSERT_FALSE(atk_selection_is_child_selected(selection, 3));

  ASSERT_FALSE(atk_selection_is_child_selected(selection, -1));
  ASSERT_FALSE(atk_selection_is_child_selected(selection, -100));
  ASSERT_FALSE(atk_selection_is_child_selected(selection, 4));
  ASSERT_FALSE(atk_selection_is_child_selected(selection, 3000));

  ASSERT_TRUE(atk_selection_clear_selection(selection));
  ASSERT_EQ(atk_selection_get_selection_count(selection), 0);
  ASSERT_FALSE(atk_selection_is_child_selected(selection, 0));
  ASSERT_FALSE(atk_selection_is_child_selected(selection, 1));
  ASSERT_FALSE(atk_selection_is_child_selected(selection, 2));
  ASSERT_FALSE(atk_selection_is_child_selected(selection, 3));

  ASSERT_TRUE(atk_selection_add_selection(selection, 1));
  ASSERT_EQ(atk_selection_get_selection_count(selection), 1);
  ASSERT_FALSE(atk_selection_is_child_selected(selection, 0));
  ASSERT_TRUE(atk_selection_is_child_selected(selection, 1));

  // The index to this function is the index into the selected elements, not
  // into the children.
  ASSERT_TRUE(atk_selection_remove_selection(selection, 0));
  ASSERT_EQ(atk_selection_get_selection_count(selection), 0);
  ASSERT_FALSE(atk_selection_is_child_selected(selection, 1));

  // We should not be able to select an item with a role that is not
  // selectable.
  ASSERT_FALSE(atk_selection_add_selection(selection, 3));
  ASSERT_EQ(atk_selection_get_selection_count(selection), 0);
  ASSERT_FALSE(atk_selection_is_child_selected(selection, 3));

  // Test some out of bounds use of atk_selection_add_selection.
  ASSERT_FALSE(atk_selection_add_selection(selection, -1));
  ASSERT_FALSE(atk_selection_add_selection(selection, -100));
  ASSERT_FALSE(atk_selection_add_selection(selection, 4));
  ASSERT_FALSE(atk_selection_add_selection(selection, 100));
  ASSERT_EQ(atk_selection_get_selection_count(selection), 0);

  ASSERT_TRUE(atk_selection_select_all_selection(selection));
  ASSERT_EQ(atk_selection_get_selection_count(selection), 3);
  ASSERT_FALSE(atk_selection_remove_selection(selection, -1));
  ASSERT_FALSE(atk_selection_remove_selection(selection, -100));
  ASSERT_FALSE(atk_selection_remove_selection(selection, 4));
  ASSERT_FALSE(atk_selection_remove_selection(selection, 100));
  ASSERT_EQ(atk_selection_get_selection_count(selection), 3);

  g_object_unref(root_atk_object);
}

TEST_F(AXPlatformNodeAuraLinuxTest, SelectionEventReflectsSelectedState) {
  AXNodeData list_box;
  list_box.id = 1;
  list_box.role = ax::mojom::Role::kListBox;
  list_box.child_ids = {2, 3, 4};

  AXNodeData selected_option;
  selected_option.id = 2;
  selected_option.role = ax::mojom::Role::kListBoxOption;
  selected_option.AddBoolAttribute(ax::mojom::BoolAttribute::kSelected, true);

  AXNodeData deselected_option;
  deselected_option.id = 3;
  deselected_option.role = ax::mojom::Role::kListBoxOption;
  deselected_option.AddBoolAttribute(ax::mojom::BoolAttribute::kSelected,
                                     false);

  AXNodeData option_without_selected_state;
  option_without_selected_state.id = 4;
  option_without_selected_state.role = ax::mojom::Role::kListBoxOption;

  AXTreeUpdate update;
  update.root_id = list_box.id;
  update.nodes = {list_box, selected_option, deselected_option,
                  option_without_selected_state};
  update.has_tree_data = true;
  update.tree_data.tree_id = AXTreeID::CreateNewAXTreeID();
  Init(update);

  struct StateChangeCount {
    int selected = 0;
    int unselected = 0;
  };

  auto selected_state_change_callback = [](StateChangeCount* count) {
    return base::BindRepeating(
        +[](StateChangeCount* count, AtkObject*, gchar* state_changed,
            gboolean new_value) {
          if (g_strcmp0(state_changed, "selected")) {
            return;
          }

          if (new_value) {
            ++count->selected;
          } else {
            ++count->unselected;
          }
        },
        count);
  };

  StateChangeCount selected_count;
  AtkObject* selected_atk_object = AtkObjectFromNode(GetNode(2));
  ASSERT_TRUE(ATK_IS_OBJECT(selected_atk_object));
  ScopedGSignal selected_signal(
      selected_atk_object, "state-change",
      selected_state_change_callback(&selected_count));
  ASSERT_TRUE(selected_signal.Connected());
  GetPlatformNode(GetNode(2))
      ->NotifyAccessibilityEvent(ax::mojom::Event::kSelection);
  EXPECT_EQ(1, selected_count.selected);
  EXPECT_EQ(0, selected_count.unselected);

  StateChangeCount deselected_count;
  AtkObject* deselected_atk_object = AtkObjectFromNode(GetNode(3));
  ASSERT_TRUE(ATK_IS_OBJECT(deselected_atk_object));
  ScopedGSignal deselected_signal(
      deselected_atk_object, "state-change",
      selected_state_change_callback(&deselected_count));
  ASSERT_TRUE(deselected_signal.Connected());
  GetPlatformNode(GetNode(3))
      ->NotifyAccessibilityEvent(ax::mojom::Event::kSelection);
  EXPECT_EQ(0, deselected_count.selected);
  EXPECT_EQ(1, deselected_count.unselected);

  StateChangeCount no_state_count;
  AtkObject* no_state_atk_object = AtkObjectFromNode(GetNode(4));
  ASSERT_TRUE(ATK_IS_OBJECT(no_state_atk_object));
  ScopedGSignal no_state_signal(
      no_state_atk_object, "state-change",
      selected_state_change_callback(&no_state_count));
  ASSERT_TRUE(no_state_signal.Connected());
  GetPlatformNode(GetNode(4))
      ->NotifyAccessibilityEvent(ax::mojom::Event::kSelection);
  EXPECT_EQ(1, no_state_count.selected);
  EXPECT_EQ(0, no_state_count.unselected);
}

// Tests GetPosInSet() and GetSetSize() functions of AXPlatformNodeBase.
// PosInSet and SetSize must be tested separately from other IntAttributes
// because they can be either assigned values or calculated dynamically.
TEST_F(AXPlatformNodeAuraLinuxTest, TestAtkObjectSetSizePosInSet) {
  AXTreeUpdate update;
  update.root_id = 1;
  update.nodes.resize(4);
  update.nodes[0].id = 1;
  update.nodes[0].role = ax::mojom::Role::kRadioGroup;
  update.nodes[0].child_ids = {2, 3, 4};
  update.nodes[1].id = 2;
  update.nodes[1].role =
      ax::mojom::Role::kRadioButton;  // kRadioButton posinset = 2, setsize = 5.
  update.nodes[1].AddIntAttribute(ax::mojom::IntAttribute::kPosInSet, 2);
  update.nodes[2].id = 3;
  update.nodes[2].role =
      ax::mojom::Role::kRadioButton;  // kRadioButton posinset = 3, setsize = 5.
  update.nodes[3].id = 4;
  update.nodes[3].role =
      ax::mojom::Role::kRadioButton;  // kRadioButton posinset = 5, stesize = 5
  update.nodes[3].AddIntAttribute(ax::mojom::IntAttribute::kPosInSet, 5);
  Init(update);

  AXNode* radiobutton1 = GetRoot()->children()[0];
  AtkObject* radiobutton1_atk_object(AtkObjectFromNode(radiobutton1));
  EXPECT_TRUE(ATK_IS_OBJECT(radiobutton1_atk_object));

  AXNode* radiobutton2 = GetRoot()->children()[1];
  AtkObject* radiobutton2_atk_object(AtkObjectFromNode(radiobutton2));
  EXPECT_TRUE(ATK_IS_OBJECT(radiobutton2_atk_object));

  AXNode* radiobutton3 = GetRoot()->children()[2];
  AtkObject* radiobutton3_atk_object(AtkObjectFromNode(radiobutton3));
  EXPECT_TRUE(ATK_IS_OBJECT(radiobutton3_atk_object));

  // Notice that setsize was never assigned to any of the kRadioButtons, but was
  // inferred.
  EnsureAtkObjectHasAttributeWithValue(radiobutton1_atk_object, "posinset",
                                       "2");
  EnsureAtkObjectHasAttributeWithValue(radiobutton1_atk_object, "setsize", "5");
  EnsureAtkObjectHasAttributeWithValue(radiobutton2_atk_object, "posinset",
                                       "3");
  EnsureAtkObjectHasAttributeWithValue(radiobutton2_atk_object, "setsize", "5");
  EnsureAtkObjectHasAttributeWithValue(radiobutton3_atk_object, "posinset",
                                       "5");
  EnsureAtkObjectHasAttributeWithValue(radiobutton3_atk_object, "setsize", "5");
}

TEST_F(AXPlatformNodeAuraLinuxTest, TestAtkRelations) {
  AXNodeData root;
  root.id = 1;
  root.role = ax::mojom::Role::kRootWebArea;
  // Add 999 as a target relation id to test that invalid relations are dropped.
  root.AddIntListAttribute(ax::mojom::IntListAttribute::kDetailsIds, {2, 999});

  AXNodeData child1;
  child1.id = 2;
  child1.role = ax::mojom::Role::kStaticText;

  root.child_ids.push_back(2);

  AXNodeData child2;
  child2.id = 3;
  child2.role = ax::mojom::Role::kStaticText;
  std::vector<int32_t> labelledby_ids = {1, 999, 4};
  child2.AddIntListAttribute(ax::mojom::IntListAttribute::kLabelledbyIds,
                             labelledby_ids);

  root.child_ids.push_back(3);

  AXNodeData child3;
  child3.id = 4;
  child3.role = ax::mojom::Role::kStaticText;
  child3.AddIntListAttribute(ax::mojom::IntListAttribute::kDetailsIds, {2});
  child3.AddIntAttribute(ax::mojom::IntAttribute::kMemberOfId, 1);

  root.child_ids.push_back(4);

  Init(root, child1, child2, child3);

  // We don't test relations that are too new for the runtime version of ATK.
  GEnumClass* enum_class =
      G_ENUM_CLASS(g_type_class_ref(atk_relation_type_get_type()));
  int max_relation_type = enum_class->maximum;
  g_type_class_unref(enum_class);

  auto assert_contains_relation = [&](AtkObject* object, AtkObject* target,
                                      AtkRelationType relation) {
    if (relation > max_relation_type)
      return;

    AtkRelationSet* relations = atk_object_ref_relation_set(object);
    ASSERT_TRUE(atk_relation_set_contains(relations, relation));
    ASSERT_TRUE(atk_relation_set_contains_target(relations, relation, target));
    g_object_unref(G_OBJECT(relations));
  };

  AtkObject* root_atk_object(GetRootAtkObject());
  EXPECT_TRUE(ATK_IS_OBJECT(root_atk_object));
  g_object_ref(root_atk_object);

  AtkObject* atk_child1(AtkObjectFromNode(GetRoot()->children()[0]));
  AtkObject* atk_child2(AtkObjectFromNode(GetRoot()->children()[1]));
  AtkObject* atk_child3(AtkObjectFromNode(GetRoot()->children()[2]));

#if defined(ATK_226)
  // Runtime check in case we were compiled with a newer version of ATK.
  if (PlatformSupportsRelation(ATK_RELATION_DETAILS)) {
    assert_contains_relation(root_atk_object, atk_child1, ATK_RELATION_DETAILS);
    assert_contains_relation(atk_child1, root_atk_object,
                             ATK_RELATION_DETAILS_FOR);
    assert_contains_relation(atk_child3, atk_child1, ATK_RELATION_DETAILS);
    assert_contains_relation(atk_child1, atk_child3, ATK_RELATION_DETAILS_FOR);
  }
#endif
  assert_contains_relation(atk_child2, root_atk_object,
                           ATK_RELATION_LABELLED_BY);
  assert_contains_relation(root_atk_object, atk_child2, ATK_RELATION_LABEL_FOR);
  assert_contains_relation(atk_child2, atk_child3, ATK_RELATION_LABELLED_BY);
  assert_contains_relation(atk_child3, atk_child2, ATK_RELATION_LABEL_FOR);

  assert_contains_relation(atk_child3, root_atk_object, ATK_RELATION_MEMBER_OF);

  g_object_unref(root_atk_object);
}

TEST_F(AXPlatformNodeAuraLinuxTest, TestAllReverseAtkRelations) {
  // We don't test relations that are too new for the runtime version of ATK.
  GEnumClass* enum_class =
      G_ENUM_CLASS(g_type_class_ref(atk_relation_type_get_type()));
  int max_relation_type = enum_class->maximum;
  g_type_class_unref(enum_class);

  auto test_relation = [&](auto attribute_setter,
                           AtkRelationType expected_relation,
                           AtkRelationType expected_reverse_relation) {
    if (expected_relation > max_relation_type ||
        expected_reverse_relation > max_relation_type)
      return;

    AXNodeData root_data;
    root_data.id = 1;
    root_data.role = ax::mojom::Role::kRootWebArea;
    attribute_setter(&root_data, 2);

    AXNodeData child_data;
    child_data.id = 2;
    child_data.role = ax::mojom::Role::kStaticText;
    root_data.child_ids.push_back(2);
    Init(root_data, child_data);

    AtkObject* source(GetRootAtkObject());
    AtkObject* target(AtkObjectFromNode(GetRoot()->children()[0]));

    AtkRelationSet* relations = atk_object_ref_relation_set(source);
    ASSERT_TRUE(atk_relation_set_contains(relations, expected_relation));
    ASSERT_TRUE(
        atk_relation_set_contains_target(relations, expected_relation, target));
    g_object_unref(G_OBJECT(relations));

    relations = atk_object_ref_relation_set(target);
    ASSERT_TRUE(
        atk_relation_set_contains(relations, expected_reverse_relation));
    ASSERT_TRUE(atk_relation_set_contains_target(
        relations, expected_reverse_relation, source));
    g_object_unref(G_OBJECT(relations));
  };

  auto test_int_list_relation = [&](ax::mojom::IntListAttribute relation,
                                    AtkRelationType expected_relation,
                                    AtkRelationType expected_reverse_relation) {
    auto setter = [&](AXNodeData* data, int target_id) {
      std::vector<int32_t> ids = {target_id};
      data->AddIntListAttribute(relation, ids);
    };
    test_relation(setter, expected_relation, expected_reverse_relation);
  };

  test_int_list_relation(ax::mojom::IntListAttribute::kDetailsIds,
                         ATK_RELATION_DETAILS, ATK_RELATION_DETAILS_FOR);
  test_int_list_relation(ax::mojom::IntListAttribute::kErrormessageIds,
                         ATK_RELATION_ERROR_MESSAGE, ATK_RELATION_ERROR_FOR);
  test_int_list_relation(ax::mojom::IntListAttribute::kControlsIds,
                         ATK_RELATION_CONTROLLER_FOR,
                         ATK_RELATION_CONTROLLED_BY);
  test_int_list_relation(ax::mojom::IntListAttribute::kDescribedbyIds,
                         ATK_RELATION_DESCRIBED_BY,
                         ATK_RELATION_DESCRIPTION_FOR);
  test_int_list_relation(ax::mojom::IntListAttribute::kFlowtoIds,
                         ATK_RELATION_FLOWS_TO, ATK_RELATION_FLOWS_FROM);
  test_int_list_relation(ax::mojom::IntListAttribute::kLabelledbyIds,
                         ATK_RELATION_LABELLED_BY, ATK_RELATION_LABEL_FOR);
}

TEST_F(AXPlatformNodeAuraLinuxTest, TestAtkRelationsTargetIndex) {
  AXNodeData root;
  root.id = 1;
  root.role = ax::mojom::Role::kRootWebArea;

  AXNodeData label1;
  label1.id = 2;
  label1.role = ax::mojom::Role::kStaticText;
  root.child_ids.push_back(2);

  AXNodeData label2;
  label2.id = 3;
  label2.role = ax::mojom::Role::kList;
  root.child_ids.push_back(3);

  AXNodeData label3;
  label3.id = 4;
  label3.role = ax::mojom::Role::kTable;
  root.child_ids.push_back(4);

  AXNodeData button1;
  button1.id = 5;
  button1.role = ax::mojom::Role::kButton;
  button1.AddIntListAttribute(ax::mojom::IntListAttribute::kLabelledbyIds,
                              {2, 3, 4});
  root.child_ids.push_back(5);

  AXNodeData button2;
  button2.id = 6;
  button2.role = ax::mojom::Role::kButton;
  button2.AddIntListAttribute(ax::mojom::IntListAttribute::kLabelledbyIds,
                              {4, 3, 2});
  root.child_ids.push_back(6);

  AXNodeData button3;
  button3.id = 7;
  button3.role = ax::mojom::Role::kButton;
  button3.AddIntListAttribute(ax::mojom::IntListAttribute::kLabelledbyIds,
                              {4, 4, 2, 2, 3});
  root.child_ids.push_back(7);

  Init(root, label1, label2, label3, button1, button2, button3);

  auto test_index = [&](AtkObject* source, AtkObject* target,
                        AtkRelationType relation_type, int index) {
    AtkRelationSet* relation_set = atk_object_ref_relation_set(source);
    ASSERT_TRUE(atk_relation_set_contains(relation_set, relation_type));

    AtkRelation* relation =
        atk_relation_set_get_relation_by_type(relation_set, relation_type);
    GPtrArray* targets = atk_relation_get_target(relation);
    UNSAFE_TODO(ASSERT_TRUE(ATK_IS_OBJECT(g_ptr_array_index(targets, index))));
    UNSAFE_TODO(
        ASSERT_TRUE(ATK_OBJECT(g_ptr_array_index(targets, index)) == target));

    g_object_unref(G_OBJECT(relation_set));
  };

  AtkObject* root_atk_object(GetRootAtkObject());
  EXPECT_TRUE(ATK_IS_OBJECT(root_atk_object));
  g_object_ref(root_atk_object);

  AtkObject* atk_label1(AtkObjectFromNode(GetRoot()->children()[0]));
  AtkObject* atk_label2(AtkObjectFromNode(GetRoot()->children()[1]));
  AtkObject* atk_label3(AtkObjectFromNode(GetRoot()->children()[2]));
  AtkObject* atk_button1(AtkObjectFromNode(GetRoot()->children()[3]));
  AtkObject* atk_button2(AtkObjectFromNode(GetRoot()->children()[4]));
  AtkObject* atk_button3(AtkObjectFromNode(GetRoot()->children()[5]));

  test_index(atk_button1, atk_label1, ATK_RELATION_LABELLED_BY, 0);
  test_index(atk_button1, atk_label2, ATK_RELATION_LABELLED_BY, 1);
  test_index(atk_button1, atk_label3, ATK_RELATION_LABELLED_BY, 2);

  test_index(atk_button2, atk_label3, ATK_RELATION_LABELLED_BY, 0);
  test_index(atk_button2, atk_label2, ATK_RELATION_LABELLED_BY, 1);
  test_index(atk_button2, atk_label1, ATK_RELATION_LABELLED_BY, 2);

  test_index(atk_button3, atk_label3, ATK_RELATION_LABELLED_BY, 0);
  test_index(atk_button3, atk_label1, ATK_RELATION_LABELLED_BY, 1);
  test_index(atk_button3, atk_label2, ATK_RELATION_LABELLED_BY, 2);

  g_object_unref(root_atk_object);
}

TEST_F(AXPlatformNodeAuraLinuxTest, TestAtkDocumentTextSelections) {
  AXNodeData root_data;
  root_data.id = 1;
  root_data.role = ax::mojom::Role::kRootWebArea;
  root_data.child_ids = {2, 4};

  AXNodeData heading_data;
  heading_data.id = 2;
  heading_data.role = ax::mojom::Role::kHeading;
  heading_data.child_ids = {3};

  AXNodeData text1_data;
  text1_data.id = 3;
  text1_data.role = ax::mojom::Role::kStaticText;
  text1_data.SetName("a😀b");

  AXNodeData paragraph_data;
  paragraph_data.id = 4;
  paragraph_data.role = ax::mojom::Role::kParagraph;
  paragraph_data.child_ids = {5};

  AXNodeData text2_data;
  text2_data.id = 5;
  text2_data.role = ax::mojom::Role::kStaticText;
  text2_data.SetName("c😀d");

  AXTreeUpdate update;
  update.root_id = root_data.id;
  update.nodes = {root_data, heading_data, text1_data, paragraph_data,
                  text2_data};
  update.has_tree_data = true;
  update.tree_data.tree_id = AXTreeID::CreateNewAXTreeID();
  update.tree_data.sel_anchor_object_id = text1_data.id;
  update.tree_data.sel_anchor_offset = 3;
  update.tree_data.sel_focus_object_id = text2_data.id;
  update.tree_data.sel_focus_offset = 1;
  AXTree* tree = Init(update);

  AXPlatformNodeAuraLinux* document = GetRootPlatformNode();
  AtkObject* heading = AtkObjectFromNode(GetRoot()->children()[0]);
  AtkObject* paragraph = AtkObjectFromNode(GetRoot()->children()[1]);

  GArray* selections = document->GetDocumentTextSelections();
  ASSERT_TRUE(selections);
  ASSERT_EQ(1u, selections->len);
  const auto& selection = GArrayFirst<AtkTextSelectionCompat>(selections);
  EXPECT_EQ(heading, selection.start_object);
  EXPECT_EQ(2, selection.start_offset);
  EXPECT_EQ(paragraph, selection.end_object);
  EXPECT_EQ(1, selection.end_offset);
  EXPECT_FALSE(selection.start_is_active);
  g_array_free(selections, true);

  AtkTextSelectionCompat new_selection = {
      .start_object = heading,
      .start_offset = 1,
      .end_object = paragraph,
      .end_offset = 2,
      .start_is_active = true,
  };
  selections = g_array_new(false, true, sizeof(AtkTextSelectionCompat));
  g_array_append_vals(selections, &new_selection, 1);
  EXPECT_TRUE(document->SetDocumentTextSelections(selections));
  EXPECT_EQ(text2_data.id, tree->data().sel_anchor_object_id);
  EXPECT_EQ(3, tree->data().sel_anchor_offset);
  EXPECT_EQ(text1_data.id, tree->data().sel_focus_object_id);
  EXPECT_EQ(1, tree->data().sel_focus_offset);

  new_selection.start_offset = 5;
  GArrayFirst<AtkTextSelectionCompat>(selections) = new_selection;
  EXPECT_FALSE(document->SetDocumentTextSelections(selections));

  new_selection = {
      .start_object = heading,
      .start_offset = 1,
      .end_object = heading,
      .end_offset = 1,
      .start_is_active = false,
  };
  GArrayFirst<AtkTextSelectionCompat>(selections) = new_selection;
  EXPECT_TRUE(document->SetDocumentTextSelections(selections));
  EXPECT_EQ(text1_data.id, tree->data().sel_anchor_object_id);
  EXPECT_EQ(1, tree->data().sel_anchor_offset);
  EXPECT_EQ(text1_data.id, tree->data().sel_focus_object_id);
  EXPECT_EQ(1, tree->data().sel_focus_offset);

  GArray* collapsed_selections = document->GetDocumentTextSelections();
  ASSERT_TRUE(collapsed_selections);
  EXPECT_EQ(0u, collapsed_selections->len);
  g_array_free(collapsed_selections, true);
  g_array_free(selections, true);
}

TEST_F(AXPlatformNodeAuraLinuxTest,
       TestAtkDocumentTextSelectionsNestedHypertextRoundTrip) {
  AXNodeData root_data;
  root_data.id = 1;
  root_data.role = ax::mojom::Role::kRootWebArea;
  root_data.child_ids = {2};

  AXNodeData paragraph_data;
  paragraph_data.id = 2;
  paragraph_data.role = ax::mojom::Role::kParagraph;
  paragraph_data.child_ids = {3, 4, 6};

  AXNodeData text_before_data;
  text_before_data.id = 3;
  text_before_data.role = ax::mojom::Role::kStaticText;
  text_before_data.SetName("abc");

  AXNodeData link_data;
  link_data.id = 4;
  link_data.role = ax::mojom::Role::kLink;
  link_data.child_ids = {5};

  AXNodeData link_text_data;
  link_text_data.id = 5;
  link_text_data.role = ax::mojom::Role::kStaticText;
  link_text_data.SetName("def");

  AXNodeData text_after_data;
  text_after_data.id = 6;
  text_after_data.role = ax::mojom::Role::kStaticText;
  text_after_data.SetName("ghi");

  AXTreeUpdate update;
  update.root_id = root_data.id;
  update.nodes = {root_data, paragraph_data, text_before_data,
                  link_data, link_text_data, text_after_data};
  update.has_tree_data = true;
  update.tree_data.tree_id = AXTreeID::CreateNewAXTreeID();
  update.tree_data.sel_anchor_object_id = link_text_data.id;
  update.tree_data.sel_anchor_offset = 1;
  update.tree_data.sel_focus_object_id = text_after_data.id;
  update.tree_data.sel_focus_offset = 0;
  Init(update);

  AXPlatformNodeAuraLinux* document = GetRootPlatformNode();
  AXNode* paragraph_node = GetRoot()->children()[0];
  AtkObject* paragraph = AtkObjectFromNode(paragraph_node);
  AtkObject* link = AtkObjectFromNode(paragraph_node->children()[1]);

  GArray* selections = document->GetDocumentTextSelections();
  ASSERT_TRUE(selections);
  ASSERT_EQ(1u, selections->len);
  const auto& selection = GArrayFirst<AtkTextSelectionCompat>(selections);
  EXPECT_EQ(link, selection.start_object);
  EXPECT_EQ(1, selection.start_offset);
  EXPECT_EQ(paragraph, selection.end_object);
  EXPECT_EQ(4, selection.end_offset);
  EXPECT_TRUE(document->SetDocumentTextSelections(selections));
  g_array_free(selections, true);
}

TEST_F(AXPlatformNodeAuraLinuxTest,
       TestAtkDocumentTextSelectionsCollapsedAtLeafBoundary) {
  AXNodeData root_data;
  root_data.id = 1;
  root_data.role = ax::mojom::Role::kRootWebArea;
  root_data.child_ids = {2};

  AXNodeData paragraph_data;
  paragraph_data.id = 2;
  paragraph_data.role = ax::mojom::Role::kParagraph;
  paragraph_data.child_ids = {3, 4};

  AXNodeData text1_data;
  text1_data.id = 3;
  text1_data.role = ax::mojom::Role::kStaticText;
  text1_data.SetName("abc");

  AXNodeData text2_data;
  text2_data.id = 4;
  text2_data.role = ax::mojom::Role::kStaticText;
  text2_data.SetName("def");

  AXTreeUpdate update;
  update.root_id = root_data.id;
  update.nodes = {root_data, paragraph_data, text1_data, text2_data};
  update.has_tree_data = true;
  update.tree_data.tree_id = AXTreeID::CreateNewAXTreeID();
  update.tree_data.sel_anchor_object_id = text1_data.id;
  update.tree_data.sel_anchor_offset = 3;
  update.tree_data.sel_focus_object_id = text2_data.id;
  update.tree_data.sel_focus_offset = 0;
  Init(update);

  GArray* selections = GetRootPlatformNode()->GetDocumentTextSelections();
  ASSERT_TRUE(selections);
  EXPECT_EQ(0u, selections->len);
  g_array_free(selections, true);
}

TEST_F(AXPlatformNodeAuraLinuxTest, TestAtkTextTextFieldGetNSelectionsZero) {
  Init(BuildTextField());
  AtkObject* root_atk_object(GetRootAtkObject());
  g_object_ref(root_atk_object);

  AtkText* atk_text = ATK_TEXT(root_atk_object);
  ASSERT_NE(nullptr, atk_text);
  EXPECT_EQ(0, atk_text_get_n_selections(atk_text));

  g_object_unref(root_atk_object);
}

TEST_F(AXPlatformNodeAuraLinuxTest,
       TestAtkTextContentEditableGetNSelectionsZero) {
  Init(BuildContentEditable());
  AtkObject* root_atk_object(GetRootAtkObject());
  g_object_ref(root_atk_object);

  AtkText* atk_text = ATK_TEXT(root_atk_object);
  ASSERT_NE(nullptr, atk_text);
  EXPECT_EQ(0, atk_text_get_n_selections(atk_text));

  g_object_unref(root_atk_object);
}

TEST_F(AXPlatformNodeAuraLinuxTest, TestAtkTextContentEditableGetNSelections) {
  Init(BuildContentEditableWithSelectionRange(1, 2));
  AtkObject* root_atk_object(GetRootAtkObject());
  g_object_ref(root_atk_object);

  AtkText* atk_text = ATK_TEXT(root_atk_object);
  ASSERT_NE(nullptr, atk_text);
  EXPECT_EQ(1, atk_text_get_n_selections(atk_text));

  g_object_unref(root_atk_object);
}

TEST_F(AXPlatformNodeAuraLinuxTest, TestAtkTextTextFieldSetSelection) {
  Init(BuildTextField());
  AtkObject* root_atk_object(GetRootAtkObject());
  g_object_ref(root_atk_object);

  AtkText* atk_text = ATK_TEXT(root_atk_object);
  ASSERT_NE(nullptr, atk_text);

  bool saw_selection_change = false;
  g_signal_connect(
      atk_text, "text-selection-changed",
      G_CALLBACK(+[](AtkObject* atkobject, bool* flag) { *flag = true; }),
      &saw_selection_change);

  int selection_start, selection_end;

  EXPECT_TRUE(atk_text_set_selection(atk_text, 0, 0, 1));
  GetRootPlatformNode()->NotifyAccessibilityEvent(
      ax::mojom::Event::kTextSelectionChanged);
  EXPECT_TRUE(saw_selection_change);
  g_free(atk_text_get_selection(atk_text, 0, &selection_start, &selection_end));
  EXPECT_EQ(selection_start, 0);
  EXPECT_EQ(selection_end, 1);

  // Reset position.
  EXPECT_TRUE(atk_text_set_selection(atk_text, 0, 0, 0));
  GetRootPlatformNode()->NotifyAccessibilityEvent(
      ax::mojom::Event::kTextSelectionChanged);

  saw_selection_change = false;
  EXPECT_TRUE(atk_text_set_selection(atk_text, 0, 1, 0));
  GetRootPlatformNode()->NotifyAccessibilityEvent(
      ax::mojom::Event::kTextSelectionChanged);
  EXPECT_TRUE(saw_selection_change);
  g_free(atk_text_get_selection(atk_text, 0, &selection_start, &selection_end));
  EXPECT_EQ(selection_start, 0);
  EXPECT_EQ(selection_end, 1);

  saw_selection_change = false;
  EXPECT_TRUE(atk_text_set_selection(atk_text, 0, 2, 4));
  GetRootPlatformNode()->NotifyAccessibilityEvent(
      ax::mojom::Event::kTextSelectionChanged);
  EXPECT_TRUE(saw_selection_change);
  g_free(atk_text_get_selection(atk_text, 0, &selection_start, &selection_end));
  EXPECT_EQ(selection_start, 2);
  EXPECT_EQ(selection_end, 4);

  saw_selection_change = false;
  EXPECT_FALSE(atk_text_set_selection(atk_text, 1, 0, 0));
  EXPECT_FALSE(saw_selection_change);
  g_free(atk_text_get_selection(atk_text, 0, &selection_start, &selection_end));
  EXPECT_EQ(selection_start, 2);
  EXPECT_EQ(selection_end, 4);

  saw_selection_change = false;
  EXPECT_FALSE(atk_text_set_selection(atk_text, 0, 0, 50));

  saw_selection_change = false;
  int n_characters = atk_text_get_character_count(atk_text);
  EXPECT_TRUE(atk_text_set_selection(atk_text, 0, 0, -1));
  GetRootPlatformNode()->NotifyAccessibilityEvent(
      ax::mojom::Event::kTextSelectionChanged);
  EXPECT_TRUE(saw_selection_change);
  g_free(atk_text_get_selection(atk_text, 0, &selection_start, &selection_end));
  EXPECT_EQ(selection_start, 0);
  EXPECT_EQ(selection_end, n_characters);

  saw_selection_change = false;
  EXPECT_TRUE(atk_text_set_selection(atk_text, 0, 0, 1));
  GetRootPlatformNode()->NotifyAccessibilityEvent(
      ax::mojom::Event::kTextSelectionChanged);
  EXPECT_EQ(1, atk_text_get_n_selections(atk_text));
  EXPECT_TRUE(atk_text_remove_selection(atk_text, 0));
  EXPECT_TRUE(saw_selection_change);
  EXPECT_EQ(0, atk_text_get_n_selections(atk_text));

  // Reset position.
  EXPECT_TRUE(atk_text_set_selection(atk_text, 0, 0, 0));
  GetRootPlatformNode()->NotifyAccessibilityEvent(
      ax::mojom::Event::kTextSelectionChanged);

  saw_selection_change = false;
  EXPECT_TRUE(atk_text_set_selection(atk_text, 0, 0, 1));
  GetRootPlatformNode()->NotifyAccessibilityEvent(
      ax::mojom::Event::kTextSelectionChanged);
  EXPECT_EQ(1, atk_text_get_n_selections(atk_text));
  EXPECT_FALSE(atk_text_remove_selection(atk_text, 1));
  EXPECT_TRUE(saw_selection_change);
  EXPECT_EQ(1, atk_text_get_n_selections(atk_text));

  g_object_unref(root_atk_object);
}

TEST_F(AXPlatformNodeAuraLinuxTest, TestAtkTextTextFieldGetSelection) {
  Init(BuildTextField());
  AtkObject* root_atk_object(GetRootAtkObject());
  g_object_ref(root_atk_object);

  AtkText* atk_text = ATK_TEXT(root_atk_object);
  ASSERT_NE(nullptr, atk_text);

  int selection_start = 0, selection_end = 0;
  EXPECT_TRUE(atk_text_set_selection(atk_text, 0, 0, 3));
  gchar* selected_text =
      atk_text_get_selection(atk_text, 0, &selection_start, &selection_end);
  EXPECT_STREQ("How", selected_text);
  EXPECT_EQ(selection_start, 0);
  EXPECT_EQ(selection_end, 3);
  g_free(selected_text);

  selection_start = 0;
  selection_end = 0;

  EXPECT_TRUE(atk_text_remove_selection(atk_text, 0));
  selected_text =
      atk_text_get_selection(atk_text, 0, &selection_start, &selection_end);
  EXPECT_EQ(nullptr, selected_text);
  EXPECT_EQ(selection_start, 0);
  EXPECT_EQ(selection_end, 0);

  EXPECT_TRUE(atk_text_set_selection(atk_text, 0, 0, 3));

  selected_text =
      atk_text_get_selection(atk_text, 1, &selection_start, &selection_end);
  EXPECT_EQ(nullptr, selected_text);
  EXPECT_EQ(selection_start, 0);
  EXPECT_EQ(selection_end, 0);

  selected_text =
      atk_text_get_selection(atk_text, -1, &selection_start, &selection_end);
  EXPECT_EQ(nullptr, selected_text);
  EXPECT_EQ(selection_start, 0);
  EXPECT_EQ(selection_end, 0);

  g_object_unref(root_atk_object);
}

TEST_F(AXPlatformNodeAuraLinuxTest, TestAtkObjectExpandRebuildsPlatformNode) {
  AXNodeData root_data;
  root_data.id = 1;
  root_data.role = ax::mojom::Role::kUnknown;

  Init(root_data);

  AtkObject* original_atk_object = GetRootAtkObject();
  ASSERT_TRUE(ATK_IS_OBJECT(original_atk_object));
  ASSERT_FALSE(ATK_IS_SELECTION(original_atk_object));
  g_object_ref(original_atk_object);

  root_data = AXNodeData();
  root_data.id = 1;
  root_data.role = ax::mojom::Role::kListBox;
  root_data.AddState(ax::mojom::State::kCollapsed);
  GetRoot()->SetData(root_data);

  ASSERT_EQ(original_atk_object, GetRootAtkObject());

  GetRootPlatformNode()->NotifyAccessibilityEvent(
      ax::mojom::Event::kExpandedChanged);

  AtkObject* new_atk_object = GetRootAtkObject();
  ASSERT_NE(original_atk_object, new_atk_object);
  ASSERT_TRUE(ATK_IS_SELECTION(new_atk_object));

  g_object_unref(original_atk_object);
}

#if defined(ATK_216)
TEST_F(AXPlatformNodeAuraLinuxTest, TestReadonlyChanged) {
  // Runtime check in case we were compiled with a newer version of ATK.
  if (!PlatformSupportsState(ATK_STATE_READ_ONLY))
    return;

  AXNodeData root_data;
  root_data.id = 1;
  root_data.role = ax::mojom::Role::kTextField;
  Init(root_data);

  AXNode* root = GetRoot();
  AtkObject* atk_object = AtkObjectFromNode(root);
  AXPlatformNodeAuraLinux* node = GetPlatformNode(root);

  bool is_read_only = false;
  g_signal_connect(atk_object, "state-change",
                   G_CALLBACK(+[](AtkObject* atkobject, gchar* state_changed,
                                  gboolean new_value, bool* flag) {
                     if (!g_strcmp0(state_changed, "read-only"))
                       *flag = new_value;
                   }),
                   &is_read_only);

  root_data.AddIntAttribute(
      ax::mojom::IntAttribute::kRestriction,
      static_cast<int32_t>(ax::mojom::Restriction::kReadOnly));
  root->SetData(root_data);
  node->OnReadonlyChanged();
  ASSERT_TRUE(is_read_only);

  root_data.AddIntAttribute(
      ax::mojom::IntAttribute::kRestriction,
      static_cast<int32_t>(ax::mojom::Restriction::kNone));
  root->SetData(root_data);
  node->OnReadonlyChanged();
  ASSERT_FALSE(is_read_only);

  root_data.AddIntAttribute(
      ax::mojom::IntAttribute::kRestriction,
      static_cast<int32_t>(ax::mojom::Restriction::kReadOnly));
  root->SetData(root_data);
  node->OnReadonlyChanged();
  ASSERT_TRUE(is_read_only);
}
#endif

TEST_F(AXPlatformNodeAuraLinuxTest, TestAtkObjectParentChanged) {
  AXNodeData root_data;
  root_data.id = 1;
  root_data.role = ax::mojom::Role::kListBox;
  root_data.child_ids.push_back(2);

  AXNodeData item_1_data;
  item_1_data.id = 2;
  item_1_data.role = ax::mojom::Role::kListBoxOption;

  Init(root_data, item_1_data);

  AXNode* item_1 = GetRoot()->children()[0];
  AtkObject* atk_object = AtkObjectFromNode(item_1);
  AXPlatformNodeAuraLinux* node = GetPlatformNode(item_1);

  bool saw_parent_changed = false;
  g_signal_connect(
      atk_object, "property-change::accessible-parent",
      G_CALLBACK(+[](AtkObject*, void* property, bool* saw_parent_changed) {
        *saw_parent_changed = true;
      }),
      &saw_parent_changed);

  ASSERT_FALSE(saw_parent_changed);
  node->OnParentChanged();
  ASSERT_TRUE(saw_parent_changed);
}

TEST_F(AXPlatformNodeAuraLinuxTest, TestScrolledToAnchorEvent) {
  AXNodeData root_data;
  root_data.id = 1;
  root_data.role = ax::mojom::Role::kListBox;
  root_data.child_ids.push_back(2);

  AXNodeData item_1_data;
  item_1_data.id = 2;
  item_1_data.role = ax::mojom::Role::kListBoxOption;

  Init(root_data, item_1_data);

  AXNode* item_1 = GetRoot()->children()[0];
  AtkObject* atk_object = AtkObjectFromNode(item_1);

  bool saw_caret_moved = false;
  g_signal_connect(
      atk_object, "text-caret-moved",
      G_CALLBACK(+[](AtkObject*, int position, bool* saw_caret_moved) {
        *saw_caret_moved = true;
      }),
      &saw_caret_moved);

  GetPlatformNode(item_1)->OnScrolledToAnchor();

  ASSERT_TRUE(saw_caret_moved);
}

TEST_F(AXPlatformNodeAuraLinuxTest, TestDialogActiveWhenChildFocused) {
  AXNodeData root;
  root.id = 1;
  root.role = ax::mojom::Role::kWindow;
  root.child_ids.push_back(2);
  root.child_ids.push_back(3);

  AXNodeData dialog;
  dialog.id = 2;
  dialog.role = ax::mojom::Role::kDialog;
  dialog.child_ids.push_back(4);

  AXNodeData node_outside_dialog;
  node_outside_dialog.id = 3;
  node_outside_dialog.role = ax::mojom::Role::kTextField;

  AXNodeData entry;
  entry.id = 4;
  entry.role = ax::mojom::Role::kTextField;

  Init(root, dialog, node_outside_dialog, entry);

  AtkObject* root_atk_object(GetRootAtkObject());
  EXPECT_TRUE(ATK_IS_OBJECT(root_atk_object));

  AXNode* dialog_node = GetRoot()->children()[0];
  AtkObject* dialog_obj = AtkObjectFromNode(dialog_node);
  bool saw_active_state_change = false;
  g_signal_connect(dialog_obj, "state-change",
                   G_CALLBACK(+[](AtkObject* atkobject, gchar* state_changed,
                                  gboolean new_value, bool* flag) {
                     if (!g_strcmp0(state_changed, "active"))
                       *flag = true;
                   }),
                   &saw_active_state_change);

  AXNode* entry_node = dialog_node->children()[0];
  GetPlatformNode(entry_node)
      ->NotifyAccessibilityEvent(ax::mojom::Event::kFocus);
  EXPECT_TRUE(saw_active_state_change);
  EXPECT_TRUE(AtkObjectHasState(dialog_obj, ATK_STATE_ACTIVE));

  saw_active_state_change = false;

  AXNode* outside_node = GetRoot()->children()[1];
  GetPlatformNode(outside_node)
      ->NotifyAccessibilityEvent(ax::mojom::Event::kFocus);
  EXPECT_TRUE(saw_active_state_change);
  EXPECT_FALSE(AtkObjectHasState(dialog_obj, ATK_STATE_ACTIVE));
}

TEST_F(AXPlatformNodeAuraLinuxTest, AccessibleURL) {
  const std::string& test_url = "https://example.com";

  AXNodeData root;
  root.id = 1;
  root.role = ax::mojom::Role::kWindow;
  root.AddStringAttribute(ax::mojom::StringAttribute::kUrl, test_url);
  root.child_ids.push_back(2);

  AXNodeData child;
  child.id = 2;
  child.role = ax::mojom::Role::kGenericContainer;
  Init(root, child);

  AXPlatformNodeAuraLinux* root_obj = GetPlatformNode(GetRoot());
  ASSERT_TRUE(root_obj);
  EXPECT_EQ(root_obj->GetRootURL(), test_url);

  AXPlatformNodeAuraLinux* child_obj =
      GetPlatformNode(GetRoot()->children()[0]);
  ASSERT_TRUE(child_obj);
  EXPECT_EQ(child_obj->GetRootURL(), test_url);
}

TEST_F(AXPlatformNodeAuraLinuxTest, AriaNotification) {
  if (base::Version(atk_get_version()).CompareTo(base::Version("2.50.0")) >=
      0) {
    AXNodeData root_data;
    root_data.id = 1;
    root_data.role = ax::mojom::Role::kRootWebArea;
    Init(root_data);

    AXNode* root_node = GetRoot();
    AtkObject* root_object = AtkObjectFromNode(root_node);
    AXPlatformNodeAuraLinux* root_platform_node = GetPlatformNode(root_node);

    bool notification_signal_sent = false;
    ScopedGSignal notification_signal(
        root_object, "notification",
        base::BindRepeating(
            [](bool* flag, AtkObject* object, gchar announcement,
               gint atk_live) { *flag = true; },
            base::Unretained(&notification_signal_sent)));

    root_platform_node->OnAriaNotificationPosted(
        "Hello, world!", ax::mojom::AriaNotificationPriority::kNormal);
    EXPECT_TRUE(notification_signal_sent);
  } else {
    AXNodeData root_data;
    root_data.id = 1;
    root_data.role = ax::mojom::Role::kRootWebArea;

    AXNodeData child_data;
    child_data.id = 2;
    child_data.role = ax::mojom::Role::kTextField;
    child_data.AddStringAttribute(
        ax::mojom::StringAttribute::kContainerLiveStatus, "assertive");
    root_data.child_ids.push_back(2);
    Init(root_data, child_data);

    AXNode* text_node = GetRoot()->children()[0];
    AtkObject* text_object = AtkObjectFromNode(text_node);
    AXPlatformNodeAuraLinux* text_platform_node = GetPlatformNode(text_node);
    ASSERT_TRUE(ATK_IS_TEXT(text_object));

    bool text_insert_signal_sent = false;
    ScopedGSignal text_insert_signal(
        text_object, "text-insert",
        base::BindRepeating(
            [](bool* flag, AtkObject*, gint text_offset, gint announcement_size,
               gchar announcement) { *flag = true; },
            base::Unretained(&text_insert_signal_sent)));

    text_platform_node->OnAriaNotificationPosted(
        "Hello, world!", ax::mojom::AriaNotificationPriority::kNormal);
    EXPECT_TRUE(text_insert_signal_sent);
  }
}

TEST_F(AXPlatformNodeAuraLinuxTest, FindStartOfStyleWithNoStyles) {
  AXNodeData root;
  root.id = 1;
  root.role = ax::mojom::Role::kStaticText;
  root.SetName("text");
  Init(root);

  AXPlatformNodeAuraLinux* node = GetRootPlatformNode();
  ASSERT_NE(nullptr, node);

  EXPECT_EQ(0, node->FindStartOfStyle(0, ax::mojom::MoveDirection::kBackward));
  EXPECT_EQ(0, node->FindStartOfStyle(4, ax::mojom::MoveDirection::kBackward));
}

}  // namespace ui
