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

#include <memory>
#include <utility>

#include "ash/accessibility/accessibility_controller.h"
#include "ash/accessibility/drag_event_rewriter.h"
#include "ash/accessibility/ui/accessibility_confirmation_dialog.h"
#include "ash/constants/ash_pref_names.h"
#include "ash/shell.h"
#include "ash/system/accessibility/accessibility_feature_disable_dialog.h"
#include "base/metrics/statistics_recorder.h"
#include "base/run_loop.h"
#include "base/test/bind.h"
#include "chrome/browser/ash/accessibility/accessibility_feature_browsertest.h"
#include "chrome/browser/ash/accessibility/accessibility_manager.h"
#include "chrome/browser/ash/accessibility/accessibility_test_utils.h"
#include "chrome/browser/ash/accessibility/facegaze_bubble_test_helper.h"
#include "chrome/browser/ash/accessibility/facegaze_test_utils.h"
#include "chrome/test/base/in_process_browser_test.h"
#include "components/prefs/pref_change_registrar.h"
#include "components/prefs/pref_service.h"
#include "content/public/test/browser_test.h"
#include "ui/accessibility/accessibility_features.h"
#include "ui/events/event.h"
#include "ui/events/event_constants.h"
#include "ui/events/event_handler.h"
#include "ui/events/keycodes/keyboard_codes_posix.h"
#include "ui/events/types/event_type.h"
#include "ui/gfx/geometry/point.h"

namespace ash {

using Config = FaceGazeTestUtils::Config;
using FaceGazeGesture = FaceGazeTestUtils::FaceGazeGesture;
using MacroName = FaceGazeTestUtils::MacroName;
using MediapipeGesture = FaceGazeTestUtils::MediapipeGesture;
using MockFaceLandmarkerResult = FaceGazeTestUtils::MockFaceLandmarkerResult;

namespace {

// Default forehead location.
constexpr std::pair<double, double> kDefaultForeheadLocation =
    std::make_pair(0.11, 0.21);
// Expected cursor location when the above forehead location is used.
constexpr gfx::Point kDefaultCursorLocation = gfx::Point(360, 560);
// The center of the screen.
constexpr gfx::Point kCenter = gfx::Point(600, 400);

PrefService* GetPrefs() {
  return AccessibilityManager::Get()->profile()->GetPrefs();
}

aura::Window* GetRootWindow() {
  auto* root_window = Shell::GetRootWindowForNewWindows();
  if (!root_window) {
    root_window = Shell::GetPrimaryRootWindow();
  }

  return root_window;
}

// A class that records mouse and key events.
class MockEventHandler : public ui::EventHandler {
 public:
  MockEventHandler() = default;
  ~MockEventHandler() override = default;
  MockEventHandler(const MockEventHandler&) = delete;
  MockEventHandler& operator=(const MockEventHandler&) = delete;

  void OnKeyEvent(ui::KeyEvent* event) override {
    key_events_.push_back(*event);
  }

  void OnMouseEvent(ui::MouseEvent* event) override {
    if (event->source_device_id() != ui::EventDeviceId::ED_UNKNOWN_DEVICE) {
      return;
    }

    ui::EventType type = event->type();
    if (type == ui::EventType::kMousePressed ||
        type == ui::EventType::kMouseReleased ||
        type == ui::EventType::kMouseMoved ||
        type == ui::EventType::kMousewheel ||
        type == ui::EventType::kMouseDragged) {
      mouse_events_.push_back(*event);
    }
  }

  void ClearEvents() {
    key_events_.clear();
    mouse_events_.clear();
  }

  const std::vector<ui::KeyEvent>& key_events() const { return key_events_; }
  const std::vector<ui::MouseEvent>& mouse_events() const {
    return mouse_events_;
  }

  std::vector<ui::MouseEvent> mouse_events(ui::EventType type) const {
    std::vector<ui::MouseEvent> events;
    for (const auto& event : mouse_events_) {
      if (event.type() == type) {
        events.push_back(event);
      }
    }

    return events;
  }

 private:
  std::vector<ui::KeyEvent> key_events_;
  std::vector<ui::MouseEvent> mouse_events_;
};

}  // namespace

class FaceGazeIntegrationTest : public AccessibilityFeatureBrowserTest {
 public:
  FaceGazeIntegrationTest() = default;
  ~FaceGazeIntegrationTest() override = default;
  FaceGazeIntegrationTest(const FaceGazeIntegrationTest&) = delete;
  FaceGazeIntegrationTest& operator=(const FaceGazeIntegrationTest&) = delete;

 protected:
  // InProcessBrowserTest:

  void SetUpOnMainThread() override {
    InProcessBrowserTest::SetUpOnMainThread();
    utils_ = std::make_unique<FaceGazeTestUtils>();
    bubble_helper_ = std::make_unique<FaceGazeBubbleTestHelper>();
    GetRootWindow()->AddPreTargetHandler(&event_handler_);
  }

  void TearDownOnMainThread() override {
    GetRootWindow()->RemovePreTargetHandler(&event_handler_);
    InProcessBrowserTest::TearDownOnMainThread();
  }

  void AssertLatestMouseEvent(size_t num_events,
                              ui::EventType type,
                              const gfx::Point& root_location) {
    std::vector<ui::MouseEvent> mouse_events = event_handler().mouse_events();
    ASSERT_GT(mouse_events.size(), 0u);
    ASSERT_EQ(num_events, mouse_events.size());
    ASSERT_EQ(type, mouse_events[0].type());
    ASSERT_EQ(root_location, mouse_events[0].root_location());
  }

  MockEventHandler& event_handler() { return event_handler_; }
  FaceGazeTestUtils* utils() { return utils_.get(); }
  FaceGazeBubbleTestHelper* bubble_helper() { return bubble_helper_.get(); }

 private:
  std::unique_ptr<FaceGazeTestUtils> utils_;
  std::unique_ptr<FaceGazeBubbleTestHelper> bubble_helper_;
  MockEventHandler event_handler_;
};

IN_PROC_BROWSER_TEST_F(FaceGazeIntegrationTest, UpdateCursorLocation) {
  utils()->EnableFaceGaze(Config().Default());
  event_handler().ClearEvents();

  utils()->ProcessFaceLandmarkerResult(
      MockFaceLandmarkerResult().WithNormalizedForeheadLocation(
          kDefaultForeheadLocation));
  utils()->TriggerMouseControllerInterval();
  utils()->AssertCursorAt(kDefaultCursorLocation);

  // We expect two mouse move events to be received because the FaceGaze
  // extension calls two APIs to update the cursor position.
  // EventGenerator generates a non-synthetic event. 'setCursorPosition'
  // generates a synthetic event.
  const std::vector<ui::MouseEvent> mouse_events =
      event_handler().mouse_events();
  ASSERT_EQ(2u, mouse_events.size());
  ASSERT_EQ(ui::EventType::kMouseMoved, mouse_events[0].type());
  ASSERT_EQ(kDefaultCursorLocation, mouse_events[0].root_location());
  ASSERT_FALSE(mouse_events[0].IsSynthesized());
  ASSERT_EQ(ui::EventType::kMouseMoved, mouse_events[1].type());
  ASSERT_EQ(kDefaultCursorLocation, mouse_events[1].root_location());
  ASSERT_TRUE(mouse_events[1].IsSynthesized());
}

IN_PROC_BROWSER_TEST_F(FaceGazeIntegrationTest, ResetCursor) {
  const base::flat_map<FaceGazeGesture, MacroName> gestures_to_macros = {
      {FaceGazeGesture::JAW_OPEN, MacroName::RESET_CURSOR}};
  const base::flat_map<FaceGazeGesture, int> gestures_to_confidences = {
      {FaceGazeGesture::JAW_OPEN, 70}};
  utils()->EnableFaceGaze(Config().Default().WithBindings(
      gestures_to_macros, gestures_to_confidences));

  // Move cursor.
  utils()->ProcessFaceLandmarkerResult(
      MockFaceLandmarkerResult().WithNormalizedForeheadLocation(
          kDefaultForeheadLocation));
  utils()->TriggerMouseControllerInterval();
  utils()->AssertCursorAt(kDefaultCursorLocation);

  event_handler().ClearEvents();

  // Reset the cursor to the center of the screen using a gesture.
  utils()->ProcessFaceLandmarkerResult(
      MockFaceLandmarkerResult().WithGesture(MediapipeGesture::JAW_OPEN, 90));
  utils()->AssertCursorAt(kCenter);

  // We expect one mouse move event to be received because the FaceGaze
  // extension only calls one API to reset the cursor position, which sends a
  // synthesized event.
  const std::vector<ui::MouseEvent> mouse_events =
      event_handler().mouse_events();
  ASSERT_EQ(1u, mouse_events.size());
  ASSERT_EQ(ui::EventType::kMouseMoved, mouse_events[0].type());
  ASSERT_EQ(kCenter, mouse_events[0].root_location());
  ASSERT_TRUE(mouse_events[0].IsSynthesized());
}

IN_PROC_BROWSER_TEST_F(FaceGazeIntegrationTest,
                       IgnoreGesturesWithLowConfidence) {
  const base::flat_map<FaceGazeGesture, MacroName> gestures_to_macros = {
      {FaceGazeGesture::JAW_OPEN, MacroName::RESET_CURSOR}};
  const base::flat_map<FaceGazeGesture, int> gestures_to_confidences = {
      {FaceGazeGesture::JAW_OPEN, 100}};
  utils()->EnableFaceGaze(Config().Default().WithBindings(
      gestures_to_macros, gestures_to_confidences));

  // Move cursor.
  utils()->ProcessFaceLandmarkerResult(
      MockFaceLandmarkerResult().WithNormalizedForeheadLocation(
          kDefaultForeheadLocation));
  utils()->TriggerMouseControllerInterval();
  utils()->AssertCursorAt(kDefaultCursorLocation);

  // Attempt to reset the cursor to the center of the screen using a gesture.
  // This gesture will be ignored because the gesture doesn't have high enough
  // confidence.
  event_handler().ClearEvents();
  utils()->ProcessFaceLandmarkerResult(
      MockFaceLandmarkerResult().WithGesture(MediapipeGesture::JAW_OPEN, 90));
  utils()->AssertCursorAt(kDefaultCursorLocation);
  ASSERT_EQ(0u, event_handler().mouse_events().size());
}

IN_PROC_BROWSER_TEST_F(FaceGazeIntegrationTest,
                       UpdateCursorLocationWithSpeed1) {
  utils()->EnableFaceGaze(Config().Default().WithCursorSpeeds(
      {/*up=*/1, /*down=*/1, /*left=*/1, /*right=*/1}));

  // With cursor acceleration off and buffer size 1, one-pixel head movements
  // correspond to one-pixel changes on screen.
  double px = 1.0 / 1200;
  double py = 1.0 / 800;
  for (int i = 1; i < 10; ++i) {
    utils()->ProcessFaceLandmarkerResult(
        MockFaceLandmarkerResult().WithNormalizedForeheadLocation(
            std::make_pair(0.1 + px * i, 0.2 + py * i)));
    utils()->TriggerMouseControllerInterval();
    utils()->AssertCursorAt(gfx::Point(600 - i, 400 + i));
  }
}

IN_PROC_BROWSER_TEST_F(FaceGazeIntegrationTest, SpaceKeyEvents) {
  const base::flat_map<FaceGazeGesture, MacroName> gestures_to_macros = {
      {FaceGazeGesture::MOUTH_LEFT, MacroName::KEY_PRESS_SPACE}};
  const base::flat_map<FaceGazeGesture, int> gestures_to_confidences = {
      {FaceGazeGesture::MOUTH_LEFT, 70}};
  utils()->EnableFaceGaze(Config().Default().WithBindings(
      gestures_to_macros, gestures_to_confidences));

  // Open jaw for space key press.
  event_handler().ClearEvents();
  utils()->ProcessFaceLandmarkerResult(
      MockFaceLandmarkerResult().WithGesture(MediapipeGesture::MOUTH_LEFT, 90));
  ASSERT_EQ(0u, event_handler().mouse_events().size());
  std::vector<ui::KeyEvent> key_events = event_handler().key_events();
  ASSERT_EQ(1u, key_events.size());
  ASSERT_EQ(ui::KeyboardCode::VKEY_SPACE, key_events[0].key_code());
  ASSERT_EQ(ui::EventType::kKeyPressed, key_events[0].type());

  // Release gesture for space key release.
  utils()->ProcessFaceLandmarkerResult(
      MockFaceLandmarkerResult().WithGesture(MediapipeGesture::MOUTH_LEFT, 10));
  ASSERT_EQ(0u, event_handler().mouse_events().size());
  key_events = event_handler().key_events();
  ASSERT_EQ(2u, event_handler().key_events().size());
  ASSERT_EQ(ui::KeyboardCode::VKEY_SPACE, key_events[1].key_code());
  ASSERT_EQ(ui::EventType::kKeyReleased, key_events[1].type());
}

// The BrowsDown gesture is special because it is the combination of two
// separate facial gestures (BROW_DOWN_LEFT and BROW_DOWN_RIGHT). This test
// ensures that the associated action is performed if either of the gestures is
// detected.
// TODO(crbug.com/486105659): Test is flaky crashing on ChromeOS
#if BUILDFLAG(IS_CHROMEOS)
#define MAYBE_BrowsDownGesture DISABLED_BrowsDownGesture
#else
#define MAYBE_BrowsDownGesture BrowsDownGesture
#endif
IN_PROC_BROWSER_TEST_F(FaceGazeIntegrationTest, MAYBE_BrowsDownGesture) {
  const base::flat_map<FaceGazeGesture, MacroName> gestures_to_macros = {
      {FaceGazeGesture::BROWS_DOWN, MacroName::RESET_CURSOR}};
  const base::flat_map<FaceGazeGesture, int> gestures_to_confidences = {
      {FaceGazeGesture::BROWS_DOWN, 40}};
  utils()->EnableFaceGaze(
      Config()
          .Default()
          .WithCursorLocation(gfx::Point(0, 0))
          .WithBindings(gestures_to_macros, gestures_to_confidences)
          .WithGestureRepeatDelayMs(0));

  // If neither gesture is detected, then don't perform the associated action.
  event_handler().ClearEvents();
  utils()->ProcessFaceLandmarkerResult(
      MockFaceLandmarkerResult()
          .WithGesture(MediapipeGesture::BROW_DOWN_LEFT, 30)
          .WithGesture(MediapipeGesture::BROW_DOWN_RIGHT, 30));
  ASSERT_EQ(0u, event_handler().mouse_events().size());

  // If BROW_DOWN_LEFT is recognized, then perform the action.
  event_handler().ClearEvents();
  utils()->ProcessFaceLandmarkerResult(
      MockFaceLandmarkerResult()
          .WithGesture(MediapipeGesture::BROW_DOWN_LEFT, 50)
          .WithGesture(MediapipeGesture::BROW_DOWN_RIGHT, 30));
  utils()->AssertCursorAt(kCenter);
  AssertLatestMouseEvent(1, ui::EventType::kMouseMoved, kCenter);

  // Reset the mouse cursor away from the center.
  utils()->MoveMouseTo(gfx::Point(0, 0));
  utils()->AssertCursorAt(gfx::Point(0, 0));

  // If BROW_DOWN_RIGHT is recognized, then perform the action.
  event_handler().ClearEvents();
  utils()->ProcessFaceLandmarkerResult(
      MockFaceLandmarkerResult()
          .WithGesture(MediapipeGesture::BROW_DOWN_LEFT, 30)
          .WithGesture(MediapipeGesture::BROW_DOWN_RIGHT, 50));
  utils()->AssertCursorAt(kCenter);
  AssertLatestMouseEvent(1, ui::EventType::kMouseMoved, kCenter);

  // Reset the mouse cursor away from the center.
  utils()->MoveMouseTo(gfx::Point(0, 0));
  utils()->AssertCursorAt(gfx::Point(0, 0));

  // If both of the gestures are recognized, then perform the action.
  event_handler().ClearEvents();
  utils()->ProcessFaceLandmarkerResult(
      MockFaceLandmarkerResult()
          .WithGesture(MediapipeGesture::BROW_DOWN_LEFT, 50)
          .WithGesture(MediapipeGesture::BROW_DOWN_RIGHT, 50));
  utils()->AssertCursorAt(kCenter);
  AssertLatestMouseEvent(1, ui::EventType::kMouseMoved, kCenter);
}

IN_PROC_BROWSER_TEST_F(FaceGazeIntegrationTest, MousePressAndReleaseEvents) {
  const base::flat_map<FaceGazeGesture, MacroName> gestures_to_macros = {
      {FaceGazeGesture::MOUTH_PUCKER, MacroName::MOUSE_CLICK_LEFT}};
  const base::flat_map<FaceGazeGesture, int> gestures_to_confidences = {
      {FaceGazeGesture::MOUTH_PUCKER, 50}};
  utils()->EnableFaceGaze(Config().Default().WithBindings(
      gestures_to_macros, gestures_to_confidences));
  event_handler().ClearEvents();

  // Move mouth right to trigger mouse press event.
  utils()->ProcessFaceLandmarkerResult(MockFaceLandmarkerResult().WithGesture(
      MediapipeGesture::MOUTH_PUCKER, 60));
  auto press_events =
      event_handler().mouse_events(ui::EventType::kMousePressed);
  auto release_events =
      event_handler().mouse_events(ui::EventType::kMouseReleased);
  ASSERT_EQ(1u, press_events.size());
  ASSERT_EQ(1u, release_events.size());
  ASSERT_TRUE(press_events.back().IsOnlyLeftMouseButton());
  ASSERT_EQ(kCenter, press_events[0].root_location());
  ASSERT_TRUE(release_events.back().IsOnlyLeftMouseButton());
  ASSERT_EQ(kCenter, release_events[0].root_location());

  // Release doesn't trigger anything else.
  event_handler().ClearEvents();
  utils()->ProcessFaceLandmarkerResult(MockFaceLandmarkerResult().WithGesture(
      MediapipeGesture::MOUTH_PUCKER, 30));
  ASSERT_EQ(0u, event_handler().mouse_events().size());
}

IN_PROC_BROWSER_TEST_F(FaceGazeIntegrationTest, MouseLongClick) {
  const base::flat_map<FaceGazeGesture, MacroName> gestures_to_macros = {
      {FaceGazeGesture::MOUTH_RIGHT, MacroName::MOUSE_LONG_CLICK_LEFT}};
  const base::flat_map<FaceGazeGesture, int> gestures_to_confidences = {
      {FaceGazeGesture::MOUTH_RIGHT, 30}};
  utils()->EnableFaceGaze(
      Config()
          .Default()
          .WithBindings(gestures_to_macros, gestures_to_confidences)
          .WithGestureRepeatDelayMs(0));
  event_handler().ClearEvents();

  auto* drag_event_rewriter = ash::Shell::Get()
                                  ->accessibility_controller()
                                  ->GetDragEventRewriterForTest();
  ASSERT_NE(drag_event_rewriter, nullptr);
  ASSERT_FALSE(drag_event_rewriter->IsEnabled());

  // Move mouth right to trigger mouse press event.
  utils()->ProcessFaceLandmarkerResult(MockFaceLandmarkerResult().WithGesture(
      MediapipeGesture::MOUTH_RIGHT, 40));
  std::vector<ui::MouseEvent> mouse_events =
      event_handler().mouse_events(ui::EventType::kMousePressed);
  ASSERT_EQ(1u, mouse_events.size());
  ASSERT_EQ(ui::EventType::kMousePressed, mouse_events.back().type());
  ASSERT_TRUE(mouse_events.back().IsOnlyLeftMouseButton());
  ASSERT_EQ(kCenter, mouse_events.back().root_location());
  ASSERT_FALSE(mouse_events.back().IsSynthesized());
  ASSERT_TRUE(drag_event_rewriter->IsEnabled());

  // Move forehead to trigger a kMouseDragged event.
  event_handler().ClearEvents();
  utils()->ProcessFaceLandmarkerResult(
      MockFaceLandmarkerResult().WithNormalizedForeheadLocation(
          std::make_pair(0.9, 0.9)));
  utils()->TriggerMouseControllerInterval();
  mouse_events = event_handler().mouse_events(ui::EventType::kMouseDragged);
  ASSERT_EQ(1u, mouse_events.size());
  ASSERT_EQ(ui::EventType::kMouseDragged, mouse_events.back().type());
  ASSERT_TRUE(mouse_events.back().IsOnlyLeftMouseButton());
  ASSERT_NE(kCenter, mouse_events.back().root_location());
  ASSERT_FALSE(mouse_events.back().IsSynthesized());
  ASSERT_TRUE(drag_event_rewriter->IsEnabled());

  // Move mouth right again to trigger mouse release event.
  event_handler().ClearEvents();
  utils()->ProcessFaceLandmarkerResult(MockFaceLandmarkerResult().WithGesture(
      MediapipeGesture::MOUTH_RIGHT, 40));
  mouse_events = event_handler().mouse_events(ui::EventType::kMouseReleased);
  ASSERT_EQ(1u, mouse_events.size());
  ASSERT_EQ(ui::EventType::kMouseReleased, mouse_events.back().type());
  ASSERT_TRUE(mouse_events.back().IsOnlyLeftMouseButton());
  ASSERT_NE(kCenter, mouse_events.back().root_location());
  ASSERT_FALSE(mouse_events.back().IsSynthesized());
  ASSERT_FALSE(drag_event_rewriter->IsEnabled());
}

// TODO(crbug.com/367758998): Performance histograms are unreliable on MSan
// due to overhead. Furthermore, the test consistently crashes on MSan
// builders during GPU initialization due to uninstrumented Vulkan drivers.
#if defined(MEMORY_SANITIZER)
#define MAYBE_PerformanceHistogram DISABLED_PerformanceHistogram
#else
#define MAYBE_PerformanceHistogram PerformanceHistogram
#endif
IN_PROC_BROWSER_TEST_F(FaceGazeIntegrationTest, MAYBE_PerformanceHistogram) {
  const base::flat_map<FaceGazeGesture, MacroName> gestures_to_macros = {
      {FaceGazeGesture::MOUTH_PUCKER, MacroName::MOUSE_CLICK_LEFT}};
  const base::flat_map<FaceGazeGesture, int> gestures_to_confidences = {
      {FaceGazeGesture::MOUTH_PUCKER, 50}};
  utils()->EnableFaceGaze(Config().Default().WithBindings(
      gestures_to_macros, gestures_to_confidences));

  base::StatisticsRecorder::HistogramWaiter waiter(
      "Accessibility.FaceGaze.AverageFaceLandmarkerLatency");
  for (int i = 0; i < 100; ++i) {
    utils()->ProcessFaceLandmarkerResult(
        MockFaceLandmarkerResult().WithLatency(i));
  }

  waiter.Wait();
}

IN_PROC_BROWSER_TEST_F(FaceGazeIntegrationTest, OpenSettingsPage) {
  const base::flat_map<FaceGazeGesture, MacroName> gestures_to_macros = {
      {FaceGazeGesture::MOUTH_RIGHT, MacroName::OPEN_FACEGAZE_SETTINGS}};
  const base::flat_map<FaceGazeGesture, int> gestures_to_confidences = {
      {FaceGazeGesture::MOUTH_RIGHT, 30}};
  utils()->EnableFaceGaze(Config().Default().WithBindings(
      gestures_to_macros, gestures_to_confidences));

  base::RunLoop waiter;
  AccessibilityManager::Get()->SetOpenSettingsSubpageObserverForTest(
      base::BindLambdaForTesting([&waiter]() { waiter.Quit(); }));

  // Move mouth right to open the FaceGaze settings page.
  utils()->ProcessFaceLandmarkerResult(MockFaceLandmarkerResult().WithGesture(
      MediapipeGesture::MOUTH_RIGHT, 40));
  waiter.Run();
}

IN_PROC_BROWSER_TEST_F(FaceGazeIntegrationTest, ToggleVirtualKeyboard) {
  const base::flat_map<FaceGazeGesture, MacroName> gestures_to_macros = {
      {FaceGazeGesture::JAW_OPEN, MacroName::TOGGLE_VIRTUAL_KEYBOARD}};
  const base::flat_map<FaceGazeGesture, int> gestures_to_confidences = {
      {FaceGazeGesture::JAW_OPEN, 30}};
  utils()->EnableFaceGaze(Config().Default().WithBindings(
      gestures_to_macros, gestures_to_confidences));

  base::RunLoop waiter;
  ash::Shell::Get()
      ->accessibility_controller()
      ->SetVirtualKeyboardVisibleCallbackForTesting(
          base::BindLambdaForTesting([&waiter]() { waiter.Quit(); }));

  // Open jaw to toggle the virtual keyboard.
  utils()->ProcessFaceLandmarkerResult(
      MockFaceLandmarkerResult().WithGesture(MediapipeGesture::JAW_OPEN, 40));
  waiter.Run();
}

IN_PROC_BROWSER_TEST_F(FaceGazeIntegrationTest, DoubleClick) {
  const base::flat_map<FaceGazeGesture, MacroName> gestures_to_macros = {
      {FaceGazeGesture::MOUTH_FUNNEL, MacroName::MOUSE_CLICK_LEFT_DOUBLE}};
  const base::flat_map<FaceGazeGesture, int> gestures_to_confidences = {
      {FaceGazeGesture::MOUTH_FUNNEL, 50}};
  utils()->EnableFaceGaze(Config().Default().WithBindings(
      gestures_to_macros, gestures_to_confidences));
  event_handler().ClearEvents();

  // Mouth funnel to trigger double click event.
  utils()->ProcessFaceLandmarkerResult(MockFaceLandmarkerResult().WithGesture(
      MediapipeGesture::MOUTH_FUNNEL, 60));
  auto press_events =
      event_handler().mouse_events(ui::EventType::kMousePressed);
  auto release_events =
      event_handler().mouse_events(ui::EventType::kMouseReleased);

  ASSERT_EQ(1u, press_events.size());
  ASSERT_EQ(1u, release_events.size());
  const auto& press_event = press_events.back();
  const auto& release_event = release_events.back();

  ASSERT_TRUE(press_event.IsOnlyLeftMouseButton());
  ASSERT_EQ(kCenter, press_event.root_location());
  // Assert that the press event is for a double click.
  ASSERT_TRUE(ui::EF_IS_DOUBLE_CLICK & press_event.flags());

  ASSERT_TRUE(release_event.IsOnlyLeftMouseButton());
  ASSERT_EQ(kCenter, release_event.root_location());
  // Assert that the release event is for a double click.
  ASSERT_TRUE(ui::EF_IS_DOUBLE_CLICK & release_event.flags());

  // Release doesn't trigger anything else.
  event_handler().ClearEvents();
  utils()->ProcessFaceLandmarkerResult(MockFaceLandmarkerResult().WithGesture(
      MediapipeGesture::MOUTH_FUNNEL, 30));
  ASSERT_EQ(0u, event_handler().mouse_events().size());
}

IN_PROC_BROWSER_TEST_F(FaceGazeIntegrationTest, TripleClick) {
  const base::flat_map<FaceGazeGesture, MacroName> gestures_to_macros = {
      {FaceGazeGesture::MOUTH_FUNNEL, MacroName::MOUSE_CLICK_LEFT_TRIPLE}};
  const base::flat_map<FaceGazeGesture, int> gestures_to_confidences = {
      {FaceGazeGesture::MOUTH_FUNNEL, 50}};
  utils()->EnableFaceGaze(Config().Default().WithBindings(
      gestures_to_macros, gestures_to_confidences));
  event_handler().ClearEvents();

  // Mouth funnel to trigger triple click event.
  utils()->ProcessFaceLandmarkerResult(MockFaceLandmarkerResult().WithGesture(
      MediapipeGesture::MOUTH_FUNNEL, 60));
  auto press_events =
      event_handler().mouse_events(ui::EventType::kMousePressed);
  auto release_events =
      event_handler().mouse_events(ui::EventType::kMouseReleased);

  ASSERT_EQ(1u, press_events.size());
  ASSERT_EQ(1u, release_events.size());
  const auto& press_event = press_events.back();
  const auto& release_event = release_events.back();

  ASSERT_TRUE(press_event.IsOnlyLeftMouseButton());
  ASSERT_EQ(kCenter, press_event.root_location());
  // Assert that the press event is for a triple click.
  ASSERT_TRUE(ui::EF_IS_TRIPLE_CLICK & press_event.flags());

  ASSERT_TRUE(release_event.IsOnlyLeftMouseButton());
  ASSERT_EQ(kCenter, release_event.root_location());
  // Assert that the release event is for a triple click.
  ASSERT_TRUE(ui::EF_IS_TRIPLE_CLICK & release_event.flags());

  // Release doesn't trigger anything else.
  event_handler().ClearEvents();
  utils()->ProcessFaceLandmarkerResult(MockFaceLandmarkerResult().WithGesture(
      MediapipeGesture::MOUTH_FUNNEL, 30));
  ASSERT_EQ(0u, event_handler().mouse_events().size());
}

IN_PROC_BROWSER_TEST_F(FaceGazeIntegrationTest, AcceptDialog) {
  auto* controller = ash::Shell::Get()->accessibility_controller();
  auto* prefs = GetPrefs();

  base::RunLoop dialog_waiter;
  controller->AddShowConfirmationDialogCallbackForTesting(
      base::BindLambdaForTesting([&dialog_waiter]() { dialog_waiter.Quit(); }));
  // Enabling FaceGaze should show the confirmation dialog.
  utils()->EnableFaceGaze(Config().Default().WithDialogAccepted(false));
  dialog_waiter.Run();
  ASSERT_TRUE(prefs->GetBoolean(prefs::kAccessibilityFaceGazeEnabled));
  ASSERT_FALSE(prefs->GetBoolean(
      prefs::kAccessibilityFaceGazeAcceleratorDialogHasBeenAccepted));
  ASSERT_NE(nullptr, controller->GetConfirmationDialogForTest());

  base::RunLoop settings_waiter;
  AccessibilityManager::Get()->SetOpenSettingsSubpageObserverForTest(
      base::BindLambdaForTesting(
          [&settings_waiter]() { settings_waiter.Quit(); }));
  // Accepting the dialog should initialize the FaceLandmarker and open the
  // settings page.
  controller->GetConfirmationDialogForTest()->Accept();
  utils()->WaitForFaceLandmarker();
  settings_waiter.Run();
  ASSERT_TRUE(prefs->GetBoolean(prefs::kAccessibilityFaceGazeEnabled));
  // Verify that the dialog accepted pref is now true.
  ASSERT_TRUE(prefs->GetBoolean(
      prefs::kAccessibilityFaceGazeAcceleratorDialogHasBeenAccepted));
}

IN_PROC_BROWSER_TEST_F(FaceGazeIntegrationTest, CancelDialog) {
  auto* controller = ash::Shell::Get()->accessibility_controller();
  auto* prefs = GetPrefs();

  base::RunLoop dialog_waiter;
  controller->AddShowConfirmationDialogCallbackForTesting(
      base::BindLambdaForTesting([&dialog_waiter]() { dialog_waiter.Quit(); }));
  // Enabling FaceGaze should show the confirmation dialog.
  utils()->EnableFaceGaze(Config().Default().WithDialogAccepted(false));
  dialog_waiter.Run();
  ASSERT_TRUE(prefs->GetBoolean(prefs::kAccessibilityFaceGazeEnabled));
  ASSERT_FALSE(prefs->GetBoolean(
      prefs::kAccessibilityFaceGazeAcceleratorDialogHasBeenAccepted));
  ASSERT_NE(nullptr, controller->GetConfirmationDialogForTest());

  base::RunLoop pref_waiter;
  PrefChangeRegistrar change_observer;
  change_observer.Init(prefs);
  change_observer.Add(prefs::kAccessibilityFaceGazeEnabled,
                      pref_waiter.QuitClosure());

  // Canceling the dialog should turn off FaceGaze.
  controller->GetConfirmationDialogForTest()->Cancel();
  pref_waiter.Run();

  ASSERT_FALSE(prefs->GetBoolean(prefs::kAccessibilityFaceGazeEnabled));
  // Verify that the dialog accepted pref is still false.
  ASSERT_FALSE(prefs->GetBoolean(
      prefs::kAccessibilityFaceGazeAcceleratorDialogHasBeenAccepted));
}

// TODO(crbug.com/486105659): Test is flaky crashing on ChromeOS
#if BUILDFLAG(IS_CHROMEOS)
#define MAYBE_ScrollMode DISABLED_ScrollMode
#else
#define MAYBE_ScrollMode ScrollMode
#endif
IN_PROC_BROWSER_TEST_F(FaceGazeIntegrationTest, MAYBE_ScrollMode) {
  const base::flat_map<FaceGazeGesture, MacroName> gestures_to_macros = {
      {FaceGazeGesture::JAW_LEFT, MacroName::TOGGLE_SCROLL_MODE}};
  const base::flat_map<FaceGazeGesture, int> gestures_to_confidences = {
      {FaceGazeGesture::JAW_LEFT, 30}};
  utils()->EnableFaceGaze(
      Config()
          .Default()
          .WithBindings(gestures_to_macros, gestures_to_confidences)
          // Ensure speeds are high so that head movements exceed the scroll
          // mode movement threshold.
          .WithCursorSpeeds({/*up=*/5, /*down=*/5, /*left=*/5, /*right=*/5})
          .WithGestureRepeatDelayMs(0));

  // Move jaw left to enter scroll mode.
  utils()->ProcessFaceLandmarkerResult(
      MockFaceLandmarkerResult().WithGesture(MediapipeGesture::JAW_LEFT, 40));
  utils()->AssertScrollMode(true);

  event_handler().ClearEvents();
  utils()->ProcessFaceLandmarkerResult(
      MockFaceLandmarkerResult()
          .WithNormalizedForeheadLocation(std::make_pair(0.9, 0.9))
          .WithGesture(MediapipeGesture::JAW_LEFT, 0));
  utils()->TriggerMouseControllerInterval();

  // Head movement should cause one scroll event to be sent.
  ASSERT_EQ(1u,
            event_handler().mouse_events(ui::EventType::kMousewheel).size());
  // No mouse movement events should be sent.
  ASSERT_EQ(0u,
            event_handler().mouse_events(ui::EventType::kMouseMoved).size());

  // Move jaw left again to exit scroll mode.
  utils()->ProcessFaceLandmarkerResult(
      MockFaceLandmarkerResult().WithGesture(MediapipeGesture::JAW_LEFT, 40));
  utils()->AssertScrollMode(false);
}

IN_PROC_BROWSER_TEST_F(FaceGazeIntegrationTest, DefaultBehavior) {
  utils()->EnableFaceGaze(Config().Default());
  // Default gesture-to-macro and gesture-to-confidence mappings should be
  // installed if we didn't specify them.
  const auto& gestures_to_macros =
      GetPrefs()->GetDict(prefs::kAccessibilityFaceGazeGesturesToMacros);
  const auto& gestures_to_confidences =
      GetPrefs()->GetDict(prefs::kAccessibilityFaceGazeGesturesToConfidence);
  ASSERT_EQ(gestures_to_macros.size(), 2u);
  ASSERT_EQ(gestures_to_confidences.size(), 2u);
  ASSERT_EQ(/* MOUSE_CLICK_LEFT */ 35,
            gestures_to_macros.FindInt(
                FaceGazeTestUtils::ToString(FaceGazeGesture::MOUTH_SMILE)));
  ASSERT_EQ(/* SCROLL */ 50,
            gestures_to_macros.FindInt(
                FaceGazeTestUtils::ToString(FaceGazeGesture::JAW_OPEN)));
  ASSERT_EQ(60, gestures_to_confidences.FindInt(
                    FaceGazeTestUtils::ToString(FaceGazeGesture::MOUTH_SMILE)));
  ASSERT_EQ(60, gestures_to_confidences.FindInt(
                    FaceGazeTestUtils::ToString(FaceGazeGesture::JAW_OPEN)));
}

IN_PROC_BROWSER_TEST_F(FaceGazeIntegrationTest, DisableDialogAccept) {
  auto* controller = ash::Shell::Get()->accessibility_controller();
  auto* prefs = GetPrefs();

  base::RunLoop dialog_waiter;
  controller->AddFeatureDisableDialogCallbackForTesting(
      base::BindLambdaForTesting([&dialog_waiter]() { dialog_waiter.Quit(); }));

  // Enabling FaceGaze should not show the feature disable dialog.
  utils()->EnableFaceGaze(Config().Default());
  ASSERT_TRUE(prefs->GetBoolean(prefs::kAccessibilityFaceGazeEnabled));
  ASSERT_EQ(nullptr, controller->GetFeatureDisableDialogForTest());

  // Showing the feature disable dialog should leave the enabled pref unchanged.
  controller->RequestDisableFaceGaze();
  dialog_waiter.Run();
  ASSERT_TRUE(prefs->GetBoolean(prefs::kAccessibilityFaceGazeEnabled));
  ASSERT_NE(nullptr, controller->GetFeatureDisableDialogForTest());

  base::RunLoop pref_waiter;
  PrefChangeRegistrar change_observer;
  change_observer.Init(prefs);
  change_observer.Add(prefs::kAccessibilityFaceGazeEnabled,
                      pref_waiter.QuitClosure());

  // Accepting the dialog should turn off FaceGaze.
  controller->GetFeatureDisableDialogForTest()->Accept();
  pref_waiter.Run();
  ASSERT_FALSE(prefs->GetBoolean(prefs::kAccessibilityFaceGazeEnabled));
}

IN_PROC_BROWSER_TEST_F(FaceGazeIntegrationTest, DisableDialogCancel) {
  auto* controller = ash::Shell::Get()->accessibility_controller();
  auto* prefs = GetPrefs();

  base::RunLoop dialog_waiter;
  controller->AddFeatureDisableDialogCallbackForTesting(
      base::BindLambdaForTesting([&dialog_waiter]() { dialog_waiter.Quit(); }));

  // Enabling FaceGaze should not show the feature disable dialog.
  utils()->EnableFaceGaze(Config().Default());
  ASSERT_TRUE(prefs->GetBoolean(prefs::kAccessibilityFaceGazeEnabled));
  ASSERT_EQ(nullptr, controller->GetFeatureDisableDialogForTest());

  // Showing the feature disable dialog should leave the enabled pref unchanged.
  controller->RequestDisableFaceGaze();
  dialog_waiter.Run();
  ASSERT_TRUE(prefs->GetBoolean(prefs::kAccessibilityFaceGazeEnabled));
  ASSERT_NE(nullptr, controller->GetFeatureDisableDialogForTest());

  base::RunLoop pref_waiter;
  PrefChangeRegistrar change_observer;
  change_observer.Init(prefs);
  change_observer.Add(prefs::kAccessibilityFaceGazeEnabledSentinel,
                      pref_waiter.QuitClosure());

  // Cancelling the dialog should leave FaceGaze on.
  controller->GetFeatureDisableDialogForTest()->Cancel();
  ASSERT_TRUE(prefs->GetBoolean(prefs::kAccessibilityFaceGazeEnabled));
}

// TODO(crbug.com/383757982): Add test API for .WithCursorControlEnabled() and
// .WithActionsEnabled() and update tests accordingly.
IN_PROC_BROWSER_TEST_F(FaceGazeIntegrationTest, EnableCursorControlNoDialog) {
  auto* controller = ash::Shell::Get()->accessibility_controller();
  auto* prefs = GetPrefs();

  base::RunLoop dialog_waiter;
  controller->AddFeatureDisableDialogCallbackForTesting(
      base::BindLambdaForTesting([&dialog_waiter]() { dialog_waiter.Quit(); }));

  // Enabling FaceGaze should not show the feature disable dialog.
  utils()->EnableFaceGaze(Config().Default());
  ASSERT_TRUE(prefs->GetBoolean(prefs::kAccessibilityFaceGazeEnabled));
  ASSERT_EQ(nullptr, controller->GetFeatureDisableDialogForTest());

  // Setting sentinel value to true should not show the feature disable dialog.
  prefs->SetBoolean(prefs::kAccessibilityFaceGazeCursorControlEnabledSentinel,
                    true);
  ASSERT_TRUE(
      prefs->GetBoolean(prefs::kAccessibilityFaceGazeCursorControlEnabled));
  ASSERT_EQ(nullptr, controller->GetFeatureDisableDialogForTest());
}

IN_PROC_BROWSER_TEST_F(FaceGazeIntegrationTest,
                       DisableCursorControlDialogAccept) {
  auto* controller = ash::Shell::Get()->accessibility_controller();
  auto* prefs = GetPrefs();

  base::RunLoop dialog_waiter;
  controller->AddFeatureDisableDialogCallbackForTesting(
      base::BindLambdaForTesting([&dialog_waiter]() { dialog_waiter.Quit(); }));

  // Enabling FaceGaze should not show the feature disable dialog.
  utils()->EnableFaceGaze(Config().Default());
  ASSERT_TRUE(prefs->GetBoolean(prefs::kAccessibilityFaceGazeEnabled));
  ASSERT_EQ(nullptr, controller->GetFeatureDisableDialogForTest());
  prefs->SetBoolean(prefs::kAccessibilityFaceGazeCursorControlEnabled, true);
  ASSERT_TRUE(prefs->GetBoolean(
      prefs::kAccessibilityFaceGazeCursorControlEnabledSentinel));

  // Setting sentinel value to false should show the feature disable dialog and
  // leave the behavior pref unchanged.
  prefs->SetBoolean(prefs::kAccessibilityFaceGazeCursorControlEnabledSentinel,
                    false);
  dialog_waiter.Run();
  ASSERT_TRUE(
      prefs->GetBoolean(prefs::kAccessibilityFaceGazeCursorControlEnabled));
  ASSERT_FALSE(prefs->GetBoolean(
      prefs::kAccessibilityFaceGazeCursorControlEnabledSentinel));
  ASSERT_NE(nullptr, controller->GetFeatureDisableDialogForTest());

  base::RunLoop pref_waiter;
  PrefChangeRegistrar change_observer;
  change_observer.Init(prefs);
  change_observer.Add(prefs::kAccessibilityFaceGazeCursorControlEnabled,
                      pref_waiter.QuitClosure());

  // Accepting the dialog should turn off FaceGaze cursor control.
  controller->GetFeatureDisableDialogForTest()->Accept();
  pref_waiter.Run();

  // Assert behavior and sentinel prefs are in sync.
  ASSERT_FALSE(
      prefs->GetBoolean(prefs::kAccessibilityFaceGazeCursorControlEnabled));
  ASSERT_FALSE(prefs->GetBoolean(
      prefs::kAccessibilityFaceGazeCursorControlEnabledSentinel));
}

IN_PROC_BROWSER_TEST_F(FaceGazeIntegrationTest,
                       DisableCursorControlDialogCancel) {
  auto* controller = ash::Shell::Get()->accessibility_controller();
  auto* prefs = GetPrefs();

  base::RunLoop dialog_waiter;
  controller->AddFeatureDisableDialogCallbackForTesting(
      base::BindLambdaForTesting([&dialog_waiter]() { dialog_waiter.Quit(); }));

  // Enabling FaceGaze should not show the feature disable dialog.
  utils()->EnableFaceGaze(Config().Default());
  ASSERT_TRUE(prefs->GetBoolean(prefs::kAccessibilityFaceGazeEnabled));
  ASSERT_EQ(nullptr, controller->GetFeatureDisableDialogForTest());
  prefs->SetBoolean(prefs::kAccessibilityFaceGazeCursorControlEnabled, true);
  ASSERT_TRUE(prefs->GetBoolean(
      prefs::kAccessibilityFaceGazeCursorControlEnabledSentinel));

  // Setting sentinel value to false should show the feature disable dialog and
  // leave the behavior pref unchanged.
  prefs->SetBoolean(prefs::kAccessibilityFaceGazeCursorControlEnabledSentinel,
                    false);
  dialog_waiter.Run();
  ASSERT_TRUE(
      prefs->GetBoolean(prefs::kAccessibilityFaceGazeCursorControlEnabled));
  ASSERT_NE(nullptr, controller->GetFeatureDisableDialogForTest());

  base::RunLoop pref_waiter;
  PrefChangeRegistrar change_observer;
  change_observer.Init(prefs);
  change_observer.Add(prefs::kAccessibilityFaceGazeCursorControlEnabledSentinel,
                      pref_waiter.QuitClosure());

  // Cancelling the dialog should leave FaceGaze cursor control on and set the
  // sentinel to true.
  controller->GetFeatureDisableDialogForTest()->Cancel();
  pref_waiter.Run();

  // Assert behavior and sentinel prefs are in sync.
  ASSERT_TRUE(
      prefs->GetBoolean(prefs::kAccessibilityFaceGazeCursorControlEnabled));
  ASSERT_TRUE(prefs->GetBoolean(
      prefs::kAccessibilityFaceGazeCursorControlEnabledSentinel));
}

IN_PROC_BROWSER_TEST_F(FaceGazeIntegrationTest, EnableActionsNoDialog) {
  auto* controller = ash::Shell::Get()->accessibility_controller();
  auto* prefs = GetPrefs();

  base::RunLoop dialog_waiter;
  controller->AddFeatureDisableDialogCallbackForTesting(
      base::BindLambdaForTesting([&dialog_waiter]() { dialog_waiter.Quit(); }));

  // Enabling FaceGaze should not show the feature disable dialog.
  utils()->EnableFaceGaze(Config().Default());
  ASSERT_TRUE(prefs->GetBoolean(prefs::kAccessibilityFaceGazeEnabled));
  ASSERT_EQ(nullptr, controller->GetFeatureDisableDialogForTest());

  // Setting sentinel value to true should not show the feature disable dialog.
  prefs->SetBoolean(prefs::kAccessibilityFaceGazeActionsEnabledSentinel, true);
  ASSERT_TRUE(prefs->GetBoolean(prefs::kAccessibilityFaceGazeActionsEnabled));
  ASSERT_EQ(nullptr, controller->GetFeatureDisableDialogForTest());
}

IN_PROC_BROWSER_TEST_F(FaceGazeIntegrationTest, DisableActionsDialogAccept) {
  auto* controller = ash::Shell::Get()->accessibility_controller();
  auto* prefs = GetPrefs();

  base::RunLoop dialog_waiter;
  controller->AddFeatureDisableDialogCallbackForTesting(
      base::BindLambdaForTesting([&dialog_waiter]() { dialog_waiter.Quit(); }));

  // Setting sentinel value to true should not show the feature disable dialog.
  utils()->EnableFaceGaze(Config().Default());
  ASSERT_TRUE(prefs->GetBoolean(prefs::kAccessibilityFaceGazeEnabled));
  ASSERT_EQ(nullptr, controller->GetFeatureDisableDialogForTest());
  prefs->SetBoolean(prefs::kAccessibilityFaceGazeActionsEnabled, true);
  ASSERT_TRUE(
      prefs->GetBoolean(prefs::kAccessibilityFaceGazeActionsEnabledSentinel));

  // Setting sentinel value to false should show the feature disable dialog and
  // leave the behavior pref unchanged.
  prefs->SetBoolean(prefs::kAccessibilityFaceGazeActionsEnabledSentinel, false);
  dialog_waiter.Run();
  ASSERT_TRUE(prefs->GetBoolean(prefs::kAccessibilityFaceGazeActionsEnabled));
  ASSERT_FALSE(
      prefs->GetBoolean(prefs::kAccessibilityFaceGazeActionsEnabledSentinel));
  ASSERT_NE(nullptr, controller->GetFeatureDisableDialogForTest());

  base::RunLoop pref_waiter;
  PrefChangeRegistrar change_observer;
  change_observer.Init(prefs);
  change_observer.Add(prefs::kAccessibilityFaceGazeActionsEnabled,
                      pref_waiter.QuitClosure());

  // Accepting the dialog should turn off FaceGaze actions.
  controller->GetFeatureDisableDialogForTest()->Accept();
  pref_waiter.Run();

  // Assert behavior and sentinel prefs are in sync.
  ASSERT_FALSE(prefs->GetBoolean(prefs::kAccessibilityFaceGazeActionsEnabled));
  ASSERT_FALSE(
      prefs->GetBoolean(prefs::kAccessibilityFaceGazeActionsEnabledSentinel));
}

// TODO(crbug.com/486105659): Test is flaky crashing on ChromeOS
#if BUILDFLAG(IS_CHROMEOS)
#define MAYBE_DisableActionsDialogCancel DISABLED_DisableActionsDialogCancel
#else
#define MAYBE_DisableActionsDialogCancel DisableActionsDialogCancel
#endif
IN_PROC_BROWSER_TEST_F(FaceGazeIntegrationTest,
                       MAYBE_DisableActionsDialogCancel) {
  auto* controller = ash::Shell::Get()->accessibility_controller();
  auto* prefs = GetPrefs();

  base::RunLoop dialog_waiter;
  controller->AddFeatureDisableDialogCallbackForTesting(
      base::BindLambdaForTesting([&dialog_waiter]() { dialog_waiter.Quit(); }));

  // Setting sentinel value to true should not show the feature disable dialog.
  utils()->EnableFaceGaze(Config().Default());
  ASSERT_TRUE(prefs->GetBoolean(prefs::kAccessibilityFaceGazeEnabled));
  ASSERT_EQ(nullptr, controller->GetFeatureDisableDialogForTest());
  prefs->SetBoolean(prefs::kAccessibilityFaceGazeActionsEnabled, true);
  ASSERT_TRUE(
      prefs->GetBoolean(prefs::kAccessibilityFaceGazeActionsEnabledSentinel));

  // Setting sentinel value to false should show the feature disable dialog and
  // leave the behavior pref unchanged.
  prefs->SetBoolean(prefs::kAccessibilityFaceGazeActionsEnabledSentinel, false);
  dialog_waiter.Run();
  ASSERT_TRUE(prefs->GetBoolean(prefs::kAccessibilityFaceGazeActionsEnabled));
  ASSERT_NE(nullptr, controller->GetFeatureDisableDialogForTest());

  base::RunLoop pref_waiter;
  PrefChangeRegistrar change_observer;
  change_observer.Init(prefs);
  change_observer.Add(prefs::kAccessibilityFaceGazeActionsEnabledSentinel,
                      pref_waiter.QuitClosure());

  // Cancelling the dialog should leave FaceGaze actions on and set the sentinel
  // to true.
  controller->GetFeatureDisableDialogForTest()->Cancel();
  pref_waiter.Run();

  // Assert behavior and sentinel prefs are in sync.
  ASSERT_TRUE(prefs->GetBoolean(prefs::kAccessibilityFaceGazeActionsEnabled));
  ASSERT_TRUE(
      prefs->GetBoolean(prefs::kAccessibilityFaceGazeActionsEnabledSentinel));
}

// TODO(crbug.com/423267032): Fix and re-enable flaky test.
#if defined(MEMORY_SANITIZER)
#define MAYBE_CloseButton DISABLED_CloseButton
#else
#define MAYBE_CloseButton CloseButton
#endif
IN_PROC_BROWSER_TEST_F(FaceGazeIntegrationTest, MAYBE_CloseButton) {
  auto* controller = ash::Shell::Get()->accessibility_controller();
  auto* prefs = GetPrefs();

  base::RunLoop dialog_waiter;
  controller->AddFeatureDisableDialogCallbackForTesting(
      base::BindLambdaForTesting([&dialog_waiter]() { dialog_waiter.Quit(); }));

  // Setup FaceGaze.
  const base::flat_map<FaceGazeGesture, MacroName> gestures_to_macros = {
      {FaceGazeGesture::MOUTH_PUCKER, MacroName::MOUSE_CLICK_LEFT}};
  const base::flat_map<FaceGazeGesture, int> gestures_to_confidences = {
      {FaceGazeGesture::MOUTH_PUCKER, 50}};
  utils()->EnableFaceGaze(Config().Default().WithBindings(
      gestures_to_macros, gestures_to_confidences));

  // Assert initial state.
  ASSERT_TRUE(prefs->GetBoolean(prefs::kAccessibilityFaceGazeEnabled));
  ASSERT_EQ(nullptr, controller->GetFeatureDisableDialogForTest());

  // Move mouse to close button.
  gfx::Point close_button = bubble_helper()->GetCloseButtonCenterPoint();
  utils()->MoveMouseTo(close_button);
  utils()->AssertCursorAt(close_button);
  ASSERT_TRUE(bubble_helper()->IsVisible());

  // Clicking the close button will show the dialog to turn off FaceGaze. Note
  // that the feature remains on until the dialog is accepted.
  utils()->ProcessFaceLandmarkerResult(MockFaceLandmarkerResult().WithGesture(
      MediapipeGesture::MOUTH_PUCKER, 95));
  dialog_waiter.Run();
  ASSERT_TRUE(prefs->GetBoolean(prefs::kAccessibilityFaceGazeEnabled));
  ASSERT_NE(nullptr, controller->GetFeatureDisableDialogForTest());

  base::RunLoop pref_waiter;
  PrefChangeRegistrar change_observer;
  change_observer.Init(prefs);
  change_observer.Add(prefs::kAccessibilityFaceGazeEnabled,
                      pref_waiter.QuitClosure());

  // Accepting the dialog should turn off FaceGaze.
  controller->GetFeatureDisableDialogForTest()->Accept();
  pref_waiter.Run();
}

}  // namespace ash
