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

#include "components/user_education/webui/help_bubble_handler.h"

#include <memory>
#include <vector>

#include "base/callback_list.h"
#include "base/memory/raw_ptr.h"
#include "base/notreached.h"
#include "base/strings/utf_string_conversions.h"
#include "base/test/bind.h"
#include "components/user_education/common/help_bubble/help_bubble_factory_registry.h"
#include "components/user_education/common/help_bubble/help_bubble_params.h"
#include "components/user_education/test/test_help_bubble.h"
#include "components/user_education/webui/help_bubble_webui.h"
#include "components/vector_icons/vector_icons.h"
#include "content/public/browser/visibility.h"
#include "content/public/browser/web_ui_controller.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "ui/base/interaction/element_identifier.h"
#include "ui/base/interaction/element_tracker.h"
#include "ui/base/interaction/expect_call_in_scope.h"
#include "ui/base/ui_base_features.h"
#include "ui/webui/resources/cr_components/help_bubble/help_bubble.mojom.h"
#include "ui/webui/resources/js/tracked_element/tracked_element.mojom-forward.h"
#include "ui/webui/resources/js/tracked_element/tracked_element.mojom.h"
#include "ui/webui/tracked_element/tracked_element_handler.h"
#include "ui/webui/tracked_element/tracked_element_web_ui.h"

namespace user_education {

namespace {

DEFINE_LOCAL_ELEMENT_IDENTIFIER_VALUE(kHelpBubbleHandlerTestElementIdentifier);
DEFINE_LOCAL_ELEMENT_IDENTIFIER_VALUE(kHelpBubbleHandlerTestElementIdentifier2);
constexpr std::string_view kSecondaryId1 = "1";
constexpr std::string_view kSecondaryId2 = "2";
constexpr gfx::RectF kElementBounds{50, 51, 51, 53};

// Mock version of the help bubble client so we don't need a remote (while being
// able to know when the remote methods would have been called).
class MockHelpBubbleClient : public help_bubble::mojom::HelpBubbleClient {
 public:
  MockHelpBubbleClient() = default;
  ~MockHelpBubbleClient() override = default;

  MOCK_METHOD(void,
              ShowHelpBubble,
              (help_bubble::mojom::HelpBubbleParamsPtr data),
              (override));
  MOCK_METHOD(void,
              ToggleFocusForAccessibility,
              (tracked_element::mojom::TrackedElementIdentifierPtr),
              (override));
  MOCK_METHOD(void,
              HideHelpBubble,
              (tracked_element::mojom::TrackedElementIdentifierPtr),
              (override));
  MOCK_METHOD(void,
              ExternalHelpBubbleUpdated,
              (tracked_element::mojom::TrackedElementIdentifierPtr, bool shown),
              (override));
};

// Handler that mocks the remote connection to the web side of the component.
// The mock is a strict mock and can be retrieved by calling the `mock()`
// method.
class TestHelpBubbleHandler : public HelpBubbleHandlerBase {
 public:
  explicit TestHelpBubbleHandler(
      base::WeakPtr<ui::TrackedElementHandler> tracked_element_handler)
      : HelpBubbleHandlerBase(std::make_unique<ClientProvider>(),
                              tracked_element_handler) {}

  ~TestHelpBubbleHandler() override = default;

  testing::StrictMock<MockHelpBubbleClient>& mock() {
    return static_cast<ClientProvider*>(client_provider())->client_;
  }

 private:
  // Provides a mock client.
  class ClientProvider : public HelpBubbleHandlerBase::ClientProvider {
   public:
    ClientProvider() = default;
    ~ClientProvider() override = default;

    help_bubble::mojom::HelpBubbleClient* GetClient() override {
      return &client_;
    }

   private:
    friend class TestHelpBubbleHandler;

    testing::StrictMock<MockHelpBubbleClient> client_;
  };
};

MATCHER_P2(MatchesTrackedElementIdentifier, p, s, "") {
  EXPECT_EQ(p.GetName(), arg->native_identifier);
  EXPECT_EQ(s, arg->secondary_identifier);
  return true;
}

MATCHER_P(MatchesHelpBubbleParams, expected, "") {
  EXPECT_EQ(expected->id->native_identifier, arg->id->native_identifier);
  EXPECT_EQ(expected->id->secondary_identifier, arg->id->secondary_identifier);
  EXPECT_EQ(expected->body_text, arg->body_text);
  EXPECT_EQ(expected->close_button_alt_text, arg->close_button_alt_text);
  EXPECT_EQ(expected->timeout, arg->timeout);
  EXPECT_EQ(expected->body_icon_name, arg->body_icon_name);
  EXPECT_EQ(expected->body_icon_alt_text, arg->body_icon_alt_text);
  EXPECT_EQ(expected->position, arg->position);
  EXPECT_EQ(expected->title_text, arg->title_text);
  EXPECT_EQ(!!expected->progress, !!arg->progress);
  if (expected->progress && arg->progress) {
    EXPECT_EQ(expected->progress->current, arg->progress->current);
    EXPECT_EQ(expected->progress->total, arg->progress->total);
  }

  EXPECT_EQ(expected->buttons.size(), arg->buttons.size());
  if (expected->buttons.size() == arg->buttons.size()) {
    for (size_t i = 0; i < expected->buttons.size(); ++i) {
      EXPECT_EQ(expected->buttons[i]->text, arg->buttons[i]->text);
      EXPECT_EQ(expected->buttons[i]->is_default, arg->buttons[i]->is_default);
    }
  }

  return true;
}

}  // namespace

// Tests the interaction of HelpBubbleHandler, HelpBubbleWebUI, and
// TrackedElementHelpBubbleWebUIAnchor. The three form a single system that all
// work together.
class HelpBubbleHandlerTest : public testing::Test {
 public:
  HelpBubbleHandlerTest() {
    help_bubble_factory_registry_.MaybeRegister<HelpBubbleFactoryWebUI>();
  }
  ~HelpBubbleHandlerTest() override = default;

  void SetUp() override {
    tracked_element_handler_ = std::make_unique<ui::TrackedElementHandler>(
        nullptr, ui::ElementContext::CreateFakeContextForTesting(this),
        std::vector{kHelpBubbleHandlerTestElementIdentifier,
                    kHelpBubbleHandlerTestElementIdentifier2});
    tracked_element_handler_->BindInterface(mojo::NullReceiver());

    test_handler_ = std::make_unique<TestHelpBubbleHandler>(
        tracked_element_handler_->GetWeakPtr());

    tracked_element_handler()->OnVisibilityChanged(
        content::Visibility::VISIBLE);
  }

  void TearDown() override {}

 protected:
  help_bubble::mojom::HelpBubbleHandler* handler() {
    return test_handler_.get();
  }

  ui::TrackedElementHandler* tracked_element_handler() {
    return tracked_element_handler_.get();
  }

  static tracked_element::mojom::TrackedElementIdentifierPtr GetId(
      ui::ElementIdentifier id,
      std::string_view secondary_id) {
    return tracked_element::mojom::TrackedElementIdentifier::New(
        id.GetName(), std::string(secondary_id));
  }

  std::unique_ptr<ui::TrackedElementHandler> tracked_element_handler_;
  std::unique_ptr<TestHelpBubbleHandler> test_handler_;
  HelpBubbleFactoryRegistry help_bubble_factory_registry_;
};

TEST_F(HelpBubbleHandlerTest, StartsWithNoElement) {
  EXPECT_FALSE(ui::ElementTracker::GetElementTracker()->GetElementInAnyContext(
      kHelpBubbleHandlerTestElementIdentifier));
  EXPECT_FALSE(ui::ElementTracker::GetElementTracker()->GetElementInAnyContext(
      kHelpBubbleHandlerTestElementIdentifier2));
}

TEST_F(HelpBubbleHandlerTest, ElementCreatedOnEvent) {
  tracked_element_handler()->TrackedElementVisibilityChanged(
      GetId(kHelpBubbleHandlerTestElementIdentifier, kSecondaryId1), true,
      kElementBounds);
  EXPECT_TRUE(ui::ElementTracker::GetElementTracker()->GetElementInAnyContext(
      kHelpBubbleHandlerTestElementIdentifier));
  EXPECT_FALSE(ui::ElementTracker::GetElementTracker()->GetElementInAnyContext(
      kHelpBubbleHandlerTestElementIdentifier2));

  // Verify that we don't leave elements dangling if the handler is destroyed.
  test_handler_.reset();
  tracked_element_handler_.reset();
  EXPECT_FALSE(ui::ElementTracker::GetElementTracker()->GetElementInAnyContext(
      kHelpBubbleHandlerTestElementIdentifier));
  EXPECT_FALSE(ui::ElementTracker::GetElementTracker()->GetElementInAnyContext(
      kHelpBubbleHandlerTestElementIdentifier2));
}

TEST_F(HelpBubbleHandlerTest, ElementHiddenOnEvent) {
  tracked_element_handler()->TrackedElementVisibilityChanged(
      GetId(kHelpBubbleHandlerTestElementIdentifier, kSecondaryId1), true,
      kElementBounds);
  tracked_element_handler()->TrackedElementVisibilityChanged(
      GetId(kHelpBubbleHandlerTestElementIdentifier, kSecondaryId1), false,
      gfx::RectF());
  EXPECT_FALSE(ui::ElementTracker::GetElementTracker()->GetElementInAnyContext(
      kHelpBubbleHandlerTestElementIdentifier));
  EXPECT_FALSE(ui::ElementTracker::GetElementTracker()->GetElementInAnyContext(
      kHelpBubbleHandlerTestElementIdentifier2));
}

TEST_F(HelpBubbleHandlerTest, ElementActivatedOnEvent) {
  UNCALLED_MOCK_CALLBACK(ui::ElementTracker::Callback, activated);
  const std::string name = kHelpBubbleHandlerTestElementIdentifier.GetName();
  tracked_element_handler()->TrackedElementVisibilityChanged(
      GetId(kHelpBubbleHandlerTestElementIdentifier, kSecondaryId1), true,
      kElementBounds);
  auto* const tracker = ui::ElementTracker::GetElementTracker();
  auto* const element =
      tracker->GetElementInAnyContext(kHelpBubbleHandlerTestElementIdentifier);
  auto subscription =
      ui::ElementTracker::GetElementTracker()->AddElementActivatedCallback(
          element->identifier(), element->context(), activated.Get());
  EXPECT_CALL_IN_SCOPE(
      activated, Run(element),
      tracked_element_handler()->TrackedElementActivated(
          GetId(kHelpBubbleHandlerTestElementIdentifier, kSecondaryId1)));
}

TEST_F(HelpBubbleHandlerTest, ElementCustomEventOnEvent) {
  DEFINE_LOCAL_CUSTOM_ELEMENT_EVENT_TYPE(kCustomEvent);
  const std::string event_name = kCustomEvent.GetName();
  UNCALLED_MOCK_CALLBACK(ui::ElementTracker::Callback, custom_event);
  tracked_element_handler()->TrackedElementVisibilityChanged(
      GetId(kHelpBubbleHandlerTestElementIdentifier, kSecondaryId1), true,
      kElementBounds);
  auto* const tracker = ui::ElementTracker::GetElementTracker();
  auto* const element =
      tracker->GetElementInAnyContext(kHelpBubbleHandlerTestElementIdentifier);
  auto subscription =
      ui::ElementTracker::GetElementTracker()->AddCustomEventCallback(
          kCustomEvent, element->context(), custom_event.Get());
  EXPECT_CALL_IN_SCOPE(
      custom_event, Run(element),
      tracked_element_handler()->TrackedElementCustomEvent(
          GetId(kHelpBubbleHandlerTestElementIdentifier, kSecondaryId1),
          event_name));
}

TEST_F(HelpBubbleHandlerTest, MultipleIdentifiers) {
  // Show two elements.
  tracked_element_handler()->TrackedElementVisibilityChanged(
      GetId(kHelpBubbleHandlerTestElementIdentifier, kSecondaryId1), true,
      kElementBounds);
  tracked_element_handler()->TrackedElementVisibilityChanged(
      GetId(kHelpBubbleHandlerTestElementIdentifier2, kSecondaryId2), true,
      kElementBounds);
  EXPECT_TRUE(ui::ElementTracker::GetElementTracker()->GetElementInAnyContext(
      kHelpBubbleHandlerTestElementIdentifier));
  EXPECT_TRUE(ui::ElementTracker::GetElementTracker()->GetElementInAnyContext(
      kHelpBubbleHandlerTestElementIdentifier2));

  // Hide one element.
  tracked_element_handler()->TrackedElementVisibilityChanged(
      GetId(kHelpBubbleHandlerTestElementIdentifier, kSecondaryId1), false,
      gfx::RectF());
  EXPECT_FALSE(ui::ElementTracker::GetElementTracker()->GetElementInAnyContext(
      kHelpBubbleHandlerTestElementIdentifier));
  EXPECT_TRUE(ui::ElementTracker::GetElementTracker()->GetElementInAnyContext(
      kHelpBubbleHandlerTestElementIdentifier2));

  // Hide the other element.
  tracked_element_handler()->TrackedElementVisibilityChanged(
      GetId(kHelpBubbleHandlerTestElementIdentifier2, kSecondaryId2), false,
      gfx::RectF());
  EXPECT_FALSE(ui::ElementTracker::GetElementTracker()->GetElementInAnyContext(
      kHelpBubbleHandlerTestElementIdentifier));
  EXPECT_FALSE(ui::ElementTracker::GetElementTracker()->GetElementInAnyContext(
      kHelpBubbleHandlerTestElementIdentifier2));

  // Re-show an element.
  tracked_element_handler()->TrackedElementVisibilityChanged(
      GetId(kHelpBubbleHandlerTestElementIdentifier, kSecondaryId1), true,
      kElementBounds);
  EXPECT_TRUE(ui::ElementTracker::GetElementTracker()->GetElementInAnyContext(
      kHelpBubbleHandlerTestElementIdentifier));
  EXPECT_FALSE(ui::ElementTracker::GetElementTracker()->GetElementInAnyContext(
      kHelpBubbleHandlerTestElementIdentifier2));
}

TEST_F(HelpBubbleHandlerTest, ShowHelpBubble) {
  tracked_element_handler()->TrackedElementVisibilityChanged(
      GetId(kHelpBubbleHandlerTestElementIdentifier, kSecondaryId1), true,
      kElementBounds);
  auto* const element =
      ui::ElementTracker::GetElementTracker()->GetUniqueElement(
          kHelpBubbleHandlerTestElementIdentifier, test_handler_->context());
  ASSERT_NE(nullptr, element);
  HelpBubbleParams params;
  params.body_text = u"Help bubble body.";
  params.close_button_alt_text = u"Close button alt text.";
  params.body_icon =
      &(features::IsRoundedIconsEnabled() ? vector_icons::kCelebrationIcon
                                          : vector_icons::kCelebrationOldIcon);
  params.body_icon_alt_text = u"Celebration";
  params.arrow = HelpBubbleArrow::kTopCenter;

  // Check the parameters passed to the ShowHelpBubble mojo method.
  help_bubble::mojom::HelpBubbleParamsPtr expected =
      help_bubble::mojom::HelpBubbleParams::New();
  expected->id = GetId(kHelpBubbleHandlerTestElementIdentifier, kSecondaryId1);
  expected->body_text = base::UTF16ToUTF8(params.body_text);
  expected->close_button_alt_text =
      base::UTF16ToUTF8(params.close_button_alt_text);
  expected->body_icon_name = "celebration_old";
  expected->body_icon_alt_text = "Celebration";
  expected->position = help_bubble::mojom::HelpBubbleArrowPosition::TOP_CENTER;
  expected->timeout = base::Seconds(10);

  EXPECT_CALL(test_handler_->mock(),
              ShowHelpBubble(MatchesHelpBubbleParams(expected.get())));
  auto help_bubble = help_bubble_factory_registry_.CreateHelpBubble(
      element, std::move(params));
  EXPECT_CALL(test_handler_->mock(), ShowHelpBubble(testing::_)).Times(0);

  EXPECT_TRUE(help_bubble);
  EXPECT_TRUE(help_bubble->is_open());

  EXPECT_CALL(test_handler_->mock(),
              HideHelpBubble(MatchesTrackedElementIdentifier(
                  kHelpBubbleHandlerTestElementIdentifier, kSecondaryId1)));
  EXPECT_TRUE(
      help_bubble->Close(HelpBubble::CloseReason::kProgrammaticallyClosed));
  EXPECT_CALL(test_handler_->mock(), HideHelpBubble).Times(0);

  EXPECT_FALSE(help_bubble->is_open());
}

// Regression test for possible cause of crbug.com/1474307.
TEST_F(HelpBubbleHandlerTest, ShowHelpBubbleTwice) {
  tracked_element_handler()->TrackedElementVisibilityChanged(
      GetId(kHelpBubbleHandlerTestElementIdentifier, kSecondaryId1), true,
      kElementBounds);
  auto* const element =
      ui::ElementTracker::GetElementTracker()->GetUniqueElement(
          kHelpBubbleHandlerTestElementIdentifier, test_handler_->context());
  ASSERT_NE(nullptr, element);

  auto get_params = []() {
    HelpBubbleParams params;
    params.body_text = u"Help bubble body.";
    params.close_button_alt_text = u"Close button alt text.";
    params.body_icon = &(features::IsRoundedIconsEnabled()
                             ? vector_icons::kCelebrationIcon
                             : vector_icons::kCelebrationOldIcon);
    params.body_icon_alt_text = u"Celebration";
    params.arrow = HelpBubbleArrow::kTopCenter;
    return params;
  };

  EXPECT_CALL(test_handler_->mock(), ShowHelpBubble(testing::_));
  auto help_bubble =
      help_bubble_factory_registry_.CreateHelpBubble(element, get_params());
  EXPECT_CALL(test_handler_->mock(), HideHelpBubble(testing::_));
  EXPECT_CALL(test_handler_->mock(), ShowHelpBubble(testing::_));
  auto help_bubble2 =
      help_bubble_factory_registry_.CreateHelpBubble(element, get_params());
  EXPECT_CALL(test_handler_->mock(), ShowHelpBubble(testing::_)).Times(0);
  EXPECT_CALL(test_handler_->mock(), HideHelpBubble(testing::_));

  EXPECT_FALSE(help_bubble->is_open());
  EXPECT_TRUE(help_bubble2->is_open());
}

TEST_F(HelpBubbleHandlerTest, ShowHelpBubbleWithButtonsAndProgress) {
  tracked_element_handler()->TrackedElementVisibilityChanged(
      GetId(kHelpBubbleHandlerTestElementIdentifier, kSecondaryId1), true,
      kElementBounds);
  auto* const element =
      ui::ElementTracker::GetElementTracker()->GetUniqueElement(
          kHelpBubbleHandlerTestElementIdentifier, test_handler_->context());
  ASSERT_NE(nullptr, element);
  HelpBubbleParams params;
  params.body_text = u"Help bubble body.";
  params.close_button_alt_text = u"Close button alt text.";
  params.body_icon = &(features::IsRoundedIconsEnabled()
                           ? vector_icons::kLightbulb2Icon
                           : vector_icons::kLightbulbOutlineOldIcon);
  params.body_icon_alt_text = u"Body icon alt text.";
  params.arrow = HelpBubbleArrow::kTopCenter;
  params.progress = std::make_pair(1, 3);

  HelpBubbleButtonParams button;
  button.is_default = true;
  button.text = u"button1";
  params.buttons.emplace_back(std::move(button));

  // Check the parameters passed to the ShowHelpBubble mojo method.
  help_bubble::mojom::HelpBubbleParamsPtr expected =
      help_bubble::mojom::HelpBubbleParams::New();
  expected->id = GetId(kHelpBubbleHandlerTestElementIdentifier, kSecondaryId1);
  expected->body_text = base::UTF16ToUTF8(params.body_text);
  expected->close_button_alt_text =
      base::UTF16ToUTF8(params.close_button_alt_text);
  expected->body_icon_name = "lightbulb_outline_old";
  expected->body_icon_alt_text = "Body icon alt text.";
  expected->position = help_bubble::mojom::HelpBubbleArrowPosition::TOP_CENTER;

  auto expected_button = help_bubble::mojom::HelpBubbleButtonParams::New();
  expected_button->text = "button1";
  expected_button->is_default = true;
  expected->buttons.emplace_back(std::move(expected_button));

  auto expected_progress = help_bubble::mojom::Progress::New();
  expected_progress->current = 1;
  expected_progress->total = 3;
  expected->progress = std::move(expected_progress);

  EXPECT_CALL(test_handler_->mock(),
              ShowHelpBubble(MatchesHelpBubbleParams(expected.get())));
  auto help_bubble = help_bubble_factory_registry_.CreateHelpBubble(
      element, std::move(params));

  EXPECT_TRUE(help_bubble);
  EXPECT_TRUE(help_bubble->is_open());

  EXPECT_CALL(test_handler_->mock(),
              HideHelpBubble(MatchesTrackedElementIdentifier(
                  kHelpBubbleHandlerTestElementIdentifier, kSecondaryId1)));
  EXPECT_TRUE(
      help_bubble->Close(HelpBubble::CloseReason::kProgrammaticallyClosed));

  EXPECT_FALSE(help_bubble->is_open());
}

TEST_F(HelpBubbleHandlerTest, FocusHelpBubble) {
  tracked_element_handler()->TrackedElementVisibilityChanged(
      GetId(kHelpBubbleHandlerTestElementIdentifier, kSecondaryId1), true,
      kElementBounds);
  auto* const element =
      ui::ElementTracker::GetElementTracker()->GetUniqueElement(
          kHelpBubbleHandlerTestElementIdentifier, test_handler_->context());
  ASSERT_NE(nullptr, element);
  HelpBubbleParams params;
  params.body_text = u"Help bubble body.";
  params.arrow = HelpBubbleArrow::kTopCenter;

  EXPECT_CALL(test_handler_->mock(), ShowHelpBubble(testing::_));
  auto help_bubble = help_bubble_factory_registry_.CreateHelpBubble(
      element, std::move(params));

  EXPECT_CALL(test_handler_->mock(),
              ToggleFocusForAccessibility(MatchesTrackedElementIdentifier(
                  kHelpBubbleHandlerTestElementIdentifier, kSecondaryId1)));
  help_bubble_factory_registry_.ToggleFocusForAccessibility(
      test_handler_->context());
  EXPECT_CALL(test_handler_->mock(), ToggleFocusForAccessibility).Times(0);

  EXPECT_CALL(test_handler_->mock(),
              HideHelpBubble(MatchesTrackedElementIdentifier(
                  kHelpBubbleHandlerTestElementIdentifier, kSecondaryId1)));
  EXPECT_TRUE(
      help_bubble->Close(HelpBubble::CloseReason::kProgrammaticallyClosed));
}

TEST_F(HelpBubbleHandlerTest, ExternalHelpBubbleUpdated) {
  // Generate and retrieve a tracked element.
  tracked_element_handler()->TrackedElementVisibilityChanged(
      GetId(kHelpBubbleHandlerTestElementIdentifier, kSecondaryId1), true,
      kElementBounds);
  auto* const element =
      ui::ElementTracker::GetElementTracker()->GetUniqueElement(
          kHelpBubbleHandlerTestElementIdentifier, test_handler_->context());

  // Create a dummy external help bubble.
  HelpBubbleParams params;
  params.body_text = u"Help bubble body.";
  params.close_button_alt_text = u"Close button alt text.";
  HelpBubbleButtonParams button_params;
  button_params.is_default = true;
  button_params.text = u"Button";
  params.buttons.emplace_back(std::move(button_params));

  std::unique_ptr<HelpBubble> help_bubble =
      std::make_unique<test::TestHelpBubble>(element, std::move(params));

  // Call the floating help bubble created method and ensure that the correct
  // message is sent over to the client.
  EXPECT_CALL(test_handler_->mock(),
              ExternalHelpBubbleUpdated(
                  MatchesTrackedElementIdentifier(
                      kHelpBubbleHandlerTestElementIdentifier, kSecondaryId1),
                  true));
  test_handler_->OnFloatingHelpBubbleCreated(
      element->AsA<ui::TrackedElementWebUI>(), help_bubble.get());

  // Close the bubble and verify that the correct message is sent, this that
  // there is no longer a bubble.
  EXPECT_CALL(test_handler_->mock(),
              ExternalHelpBubbleUpdated(
                  MatchesTrackedElementIdentifier(
                      kHelpBubbleHandlerTestElementIdentifier, kSecondaryId1),
                  false));
  EXPECT_TRUE(
      help_bubble->Close(HelpBubble::CloseReason::kProgrammaticallyClosed));
}

TEST_F(HelpBubbleHandlerTest, HelpBubbleClosedWhenVisibilityChanges) {
  tracked_element_handler()->TrackedElementVisibilityChanged(
      GetId(kHelpBubbleHandlerTestElementIdentifier, kSecondaryId1), true,
      kElementBounds);
  auto* const element =
      ui::ElementTracker::GetElementTracker()->GetUniqueElement(
          kHelpBubbleHandlerTestElementIdentifier, test_handler_->context());
  ASSERT_NE(nullptr, element);
  HelpBubbleParams params;
  params.body_text = u"Help bubble body.";
  params.arrow = HelpBubbleArrow::kTopCenter;

  EXPECT_CALL(test_handler_->mock(), ShowHelpBubble(testing::_));
  auto help_bubble = help_bubble_factory_registry_.CreateHelpBubble(
      element, std::move(params));
  EXPECT_TRUE(
      test_handler_->IsHelpBubbleShowingForTesting(element->identifier()));

  // This should have no effect since it's the wrong element.
  tracked_element_handler()->TrackedElementVisibilityChanged(
      GetId(kHelpBubbleHandlerTestElementIdentifier2, kSecondaryId2), false,
      gfx::RectF());
  EXPECT_TRUE(help_bubble->is_open());
  EXPECT_TRUE(
      test_handler_->IsHelpBubbleShowingForTesting(element->identifier()));

  // WebContents becoming invisible should NOT close the bubble because of the
  // visibility lock.
  tracked_element_handler()->OnVisibilityChanged(content::Visibility::HIDDEN);
  EXPECT_TRUE(help_bubble->is_open());
  EXPECT_TRUE(
      test_handler_->IsHelpBubbleShowingForTesting(element->identifier()));

  // Element becoming invisible in WebUI SHOULD close the bubble.
  EXPECT_CALL(test_handler_->mock(), HideHelpBubble(testing::_));
  tracked_element_handler()->TrackedElementVisibilityChanged(
      GetId(kHelpBubbleHandlerTestElementIdentifier, kSecondaryId1), false,
      gfx::RectF());
  EXPECT_FALSE(help_bubble->is_open());
  EXPECT_FALSE(test_handler_->IsHelpBubbleShowingForTesting(
      kHelpBubbleHandlerTestElementIdentifier));
}

TEST_F(HelpBubbleHandlerTest, HelpBubbleClosedWhenClosedRemotely) {
  UNCALLED_MOCK_CALLBACK(HelpBubble::ClosingCallback, closing);
  UNCALLED_MOCK_CALLBACK(HelpBubble::ClosedCallback, closed);

  tracked_element_handler()->TrackedElementVisibilityChanged(
      GetId(kHelpBubbleHandlerTestElementIdentifier, kSecondaryId1), true,
      kElementBounds);
  auto* const element =
      ui::ElementTracker::GetElementTracker()->GetUniqueElement(
          kHelpBubbleHandlerTestElementIdentifier, test_handler_->context());
  ASSERT_NE(nullptr, element);
  HelpBubbleParams params;
  params.body_text = u"Help bubble body.";
  params.arrow = HelpBubbleArrow::kTopCenter;

  EXPECT_CALL(test_handler_->mock(), ShowHelpBubble(testing::_));
  auto help_bubble = help_bubble_factory_registry_.CreateHelpBubble(
      element, std::move(params));
  auto subscription1 = help_bubble->AddOnClosingCallback(closing.Get());
  auto subscription2 = help_bubble->AddOnClosedCallback(closed.Get());
  EXPECT_TRUE(
      test_handler_->IsHelpBubbleShowingForTesting(element->identifier()));

  EXPECT_CALLS_IN_SCOPE_2(
      closing,
      Run(help_bubble.get(), HelpBubble::CloseReason::kProgrammaticallyClosed),
      closed, Run(HelpBubble::CloseReason::kProgrammaticallyClosed),
      handler()->HelpBubbleClosed(
          GetId(kHelpBubbleHandlerTestElementIdentifier, kSecondaryId1),
          help_bubble::mojom::HelpBubbleClosedReason::kPageChanged));
  EXPECT_FALSE(help_bubble->is_open());
  EXPECT_FALSE(
      test_handler_->IsHelpBubbleShowingForTesting(element->identifier()));
}

TEST_F(HelpBubbleHandlerTest, DestroyHandlerCleansUpElement) {
  tracked_element_handler()->TrackedElementVisibilityChanged(
      GetId(kHelpBubbleHandlerTestElementIdentifier, kSecondaryId1), true,
      kElementBounds);
  const ui::ElementContext context = test_handler_->context();
  EXPECT_TRUE(ui::ElementTracker::GetElementTracker()->IsElementVisible(
      kHelpBubbleHandlerTestElementIdentifier, context));
  test_handler_.reset();
  tracked_element_handler_.reset();
  EXPECT_FALSE(ui::ElementTracker::GetElementTracker()->IsElementVisible(
      kHelpBubbleHandlerTestElementIdentifier, context));
}

// Asserts that closing the HelpBubble handle to a bubble instance destroys
// the bubble.
TEST_F(HelpBubbleHandlerTest, DestroyBubbleWrapperClosesHelpBubble) {
  UNCALLED_MOCK_CALLBACK(HelpBubble::ClosingCallback, closing);
  UNCALLED_MOCK_CALLBACK(HelpBubble::ClosedCallback, closed);

  tracked_element_handler()->TrackedElementVisibilityChanged(
      GetId(kHelpBubbleHandlerTestElementIdentifier, kSecondaryId1), true,
      kElementBounds);
  auto* const element =
      ui::ElementTracker::GetElementTracker()->GetUniqueElement(
          kHelpBubbleHandlerTestElementIdentifier, test_handler_->context());
  ASSERT_NE(nullptr, element);
  HelpBubbleParams params;
  params.body_text = u"Help bubble body.";
  params.arrow = HelpBubbleArrow::kTopCenter;

  EXPECT_CALL(test_handler_->mock(), ShowHelpBubble(testing::_));
  auto help_bubble = help_bubble_factory_registry_.CreateHelpBubble(
      element, std::move(params));
  auto subscription1 = help_bubble->AddOnClosingCallback(closing.Get());
  auto subscription2 = help_bubble->AddOnClosedCallback(closed.Get());

  EXPECT_CALL(test_handler_->mock(),
              HideHelpBubble(MatchesTrackedElementIdentifier(
                  kHelpBubbleHandlerTestElementIdentifier, kSecondaryId1)));
  EXPECT_CALLS_IN_SCOPE_2(
      closing,
      Run(help_bubble.get(), HelpBubble::CloseReason::kBubbleDestroyed), closed,
      Run(HelpBubble::CloseReason::kBubbleDestroyed), help_bubble.reset());
}

TEST_F(HelpBubbleHandlerTest, HelpBubbleClosedWhenClosedByUserCallsDismiss) {
  UNCALLED_MOCK_CALLBACK(base::OnceClosure, dismissed);

  tracked_element_handler()->TrackedElementVisibilityChanged(
      GetId(kHelpBubbleHandlerTestElementIdentifier, kSecondaryId1), true,
      kElementBounds);
  auto* const element =
      ui::ElementTracker::GetElementTracker()->GetUniqueElement(
          kHelpBubbleHandlerTestElementIdentifier, test_handler_->context());
  ASSERT_NE(nullptr, element);
  HelpBubbleParams params;
  params.body_text = u"Help bubble body.";
  params.arrow = HelpBubbleArrow::kTopCenter;
  params.dismiss_callback = dismissed.Get();

  EXPECT_CALL(test_handler_->mock(), ShowHelpBubble(testing::_));
  auto help_bubble = help_bubble_factory_registry_.CreateHelpBubble(
      element, std::move(params));
  EXPECT_TRUE(
      test_handler_->IsHelpBubbleShowingForTesting(element->identifier()));

  EXPECT_CALL_IN_SCOPE(
      dismissed, Run,
      handler()->HelpBubbleClosed(
          GetId(kHelpBubbleHandlerTestElementIdentifier, kSecondaryId1),
          help_bubble::mojom::HelpBubbleClosedReason::kDismissedByUser));
  EXPECT_FALSE(help_bubble->is_open());
  EXPECT_FALSE(
      test_handler_->IsHelpBubbleShowingForTesting(element->identifier()));
}

TEST_F(HelpBubbleHandlerTest, ButtonPressedCallsCallback) {
  UNCALLED_MOCK_CALLBACK(base::OnceClosure, button1_pressed);
  UNCALLED_MOCK_CALLBACK(base::OnceClosure, button2_pressed);

  tracked_element_handler()->TrackedElementVisibilityChanged(
      GetId(kHelpBubbleHandlerTestElementIdentifier, kSecondaryId1), true,
      kElementBounds);
  auto* const element =
      ui::ElementTracker::GetElementTracker()->GetUniqueElement(
          kHelpBubbleHandlerTestElementIdentifier, test_handler_->context());
  ASSERT_NE(nullptr, element);

  HelpBubbleParams params;
  params.body_text = u"Help bubble body.";
  params.arrow = HelpBubbleArrow::kTopCenter;

  HelpBubbleButtonParams button1;
  button1.is_default = true;
  button1.text = u"button1";
  button1.callback = button1_pressed.Get();
  params.buttons.emplace_back(std::move(button1));

  HelpBubbleButtonParams button2;
  button2.is_default = false;
  button2.text = u"button2";
  button2.callback = button2_pressed.Get();
  params.buttons.emplace_back(std::move(button2));

  EXPECT_CALL(test_handler_->mock(), ShowHelpBubble(testing::_));
  auto help_bubble = help_bubble_factory_registry_.CreateHelpBubble(
      element, std::move(params));
  EXPECT_TRUE(
      test_handler_->IsHelpBubbleShowingForTesting(element->identifier()));

  EXPECT_CALL_IN_SCOPE(
      button2_pressed, Run,
      handler()->HelpBubbleButtonPressed(
          GetId(kHelpBubbleHandlerTestElementIdentifier, kSecondaryId1), 1));
  EXPECT_FALSE(help_bubble->is_open());
  EXPECT_FALSE(
      test_handler_->IsHelpBubbleShowingForTesting(element->identifier()));
}

TEST_F(HelpBubbleHandlerTest, ShowMultipleBubblesAndCloseOneViaVisibility) {
  tracked_element_handler()->TrackedElementVisibilityChanged(
      GetId(kHelpBubbleHandlerTestElementIdentifier, kSecondaryId1), true,
      kElementBounds);
  tracked_element_handler()->TrackedElementVisibilityChanged(
      GetId(kHelpBubbleHandlerTestElementIdentifier2, kSecondaryId2), true,
      kElementBounds);
  auto* const element =
      ui::ElementTracker::GetElementTracker()->GetUniqueElement(
          kHelpBubbleHandlerTestElementIdentifier, test_handler_->context());
  auto* const element2 =
      ui::ElementTracker::GetElementTracker()->GetUniqueElement(
          kHelpBubbleHandlerTestElementIdentifier2, test_handler_->context());
  ASSERT_NE(nullptr, element);
  ASSERT_NE(nullptr, element2);

  HelpBubbleParams params;
  params.body_text = u"Help bubble body.";
  params.arrow = HelpBubbleArrow::kTopCenter;

  EXPECT_CALL(test_handler_->mock(), ShowHelpBubble(testing::_));
  auto help_bubble = help_bubble_factory_registry_.CreateHelpBubble(
      element, std::move(params));
  EXPECT_TRUE(
      test_handler_->IsHelpBubbleShowingForTesting(element->identifier()));

  HelpBubbleParams params2;
  params2.body_text = u"Help bubble body 2.";
  params2.arrow = HelpBubbleArrow::kBottomLeft;
  EXPECT_CALL(test_handler_->mock(), ShowHelpBubble(testing::_));
  auto help_bubble2 = help_bubble_factory_registry_.CreateHelpBubble(
      element2, std::move(params2));
  EXPECT_TRUE(
      test_handler_->IsHelpBubbleShowingForTesting(element2->identifier()));

  EXPECT_TRUE(help_bubble->is_open());
  EXPECT_TRUE(help_bubble2->is_open());

  // Close one bubble without closing the other.
  EXPECT_CALL(test_handler_->mock(),
              HideHelpBubble(MatchesTrackedElementIdentifier(
                  kHelpBubbleHandlerTestElementIdentifier, kSecondaryId1)));
  tracked_element_handler()->TrackedElementVisibilityChanged(
      GetId(kHelpBubbleHandlerTestElementIdentifier, kSecondaryId1), false,
      gfx::RectF());
  EXPECT_FALSE(help_bubble->is_open());
  EXPECT_TRUE(help_bubble2->is_open());
  EXPECT_FALSE(test_handler_->IsHelpBubbleShowingForTesting(
      kHelpBubbleHandlerTestElementIdentifier));
  EXPECT_TRUE(test_handler_->IsHelpBubbleShowingForTesting(
      kHelpBubbleHandlerTestElementIdentifier2));

  // When the second bubble goes away, it will attempt to close the bubble on
  // the remote.
  EXPECT_CALL(test_handler_->mock(),
              HideHelpBubble(MatchesTrackedElementIdentifier(
                  kHelpBubbleHandlerTestElementIdentifier2, kSecondaryId2)));
}

TEST_F(HelpBubbleHandlerTest, ShowMultipleBubblesAndCloseOneViaCallback) {
  tracked_element_handler()->TrackedElementVisibilityChanged(
      GetId(kHelpBubbleHandlerTestElementIdentifier, kSecondaryId1), true,
      kElementBounds);
  tracked_element_handler()->TrackedElementVisibilityChanged(
      GetId(kHelpBubbleHandlerTestElementIdentifier2, kSecondaryId2), true,
      kElementBounds);
  auto* const element =
      ui::ElementTracker::GetElementTracker()->GetUniqueElement(
          kHelpBubbleHandlerTestElementIdentifier, test_handler_->context());
  auto* const element2 =
      ui::ElementTracker::GetElementTracker()->GetUniqueElement(
          kHelpBubbleHandlerTestElementIdentifier2, test_handler_->context());
  ASSERT_NE(nullptr, element);
  ASSERT_NE(nullptr, element2);

  HelpBubbleParams params;
  params.body_text = u"Help bubble body.";
  params.arrow = HelpBubbleArrow::kTopCenter;

  EXPECT_CALL(test_handler_->mock(), ShowHelpBubble(testing::_));
  auto help_bubble = help_bubble_factory_registry_.CreateHelpBubble(
      element, std::move(params));
  EXPECT_TRUE(
      test_handler_->IsHelpBubbleShowingForTesting(element->identifier()));

  HelpBubbleParams params2;
  params2.body_text = u"Help bubble body 2.";
  params2.arrow = HelpBubbleArrow::kBottomLeft;
  EXPECT_CALL(test_handler_->mock(), ShowHelpBubble(testing::_));
  auto help_bubble2 = help_bubble_factory_registry_.CreateHelpBubble(
      element2, std::move(params2));
  EXPECT_TRUE(
      test_handler_->IsHelpBubbleShowingForTesting(element2->identifier()));

  EXPECT_TRUE(help_bubble->is_open());
  EXPECT_TRUE(help_bubble2->is_open());

  // Close one bubble without closing the other.
  handler()->HelpBubbleClosed(
      GetId(kHelpBubbleHandlerTestElementIdentifier, kSecondaryId1),
      help_bubble::mojom::HelpBubbleClosedReason::kPageChanged);
  EXPECT_FALSE(help_bubble->is_open());
  EXPECT_TRUE(help_bubble2->is_open());
  EXPECT_FALSE(
      test_handler_->IsHelpBubbleShowingForTesting(element->identifier()));
  EXPECT_TRUE(
      test_handler_->IsHelpBubbleShowingForTesting(element2->identifier()));

  // When the second bubble goes away, it will attempt to close the bubble on
  // the remote.
  EXPECT_CALL(test_handler_->mock(),
              HideHelpBubble(MatchesTrackedElementIdentifier(
                  kHelpBubbleHandlerTestElementIdentifier2, kSecondaryId2)));
}

// The following tests check that the WebContents visibility logic.

TEST_F(HelpBubbleHandlerTest, WebContentsNotVisibleResultsInNoElement) {
  UNCALLED_MOCK_CALLBACK(ui::ElementTracker::Callback, element_shown);
  const auto subscription =
      ui::ElementTracker::GetElementTracker()
          ->AddElementShownInAnyContextCallback(
              kHelpBubbleHandlerTestElementIdentifier, element_shown.Get());

  tracked_element_handler()->OnVisibilityChanged(content::Visibility::HIDDEN);
  tracked_element_handler()->TrackedElementVisibilityChanged(
      GetId(kHelpBubbleHandlerTestElementIdentifier, kSecondaryId1), true,
      kElementBounds);
}

TEST_F(HelpBubbleHandlerTest, ElementHiddenWebContentsBecomingInvisible) {
  UNCALLED_MOCK_CALLBACK(ui::ElementTracker::Callback, element_hidden);

  ui::TrackedElement* el = nullptr;
  const auto sub1 = ui::ElementTracker::GetElementTracker()
                        ->AddElementShownInAnyContextCallback(
                            kHelpBubbleHandlerTestElementIdentifier,
                            base::BindLambdaForTesting(
                                [&el](ui::TrackedElement* el_) { el = el_; }));
  tracked_element_handler()->TrackedElementVisibilityChanged(
      GetId(kHelpBubbleHandlerTestElementIdentifier, kSecondaryId1), true,
      kElementBounds);

  const auto sub2 =
      ui::ElementTracker::GetElementTracker()->AddElementHiddenCallback(
          el->identifier(), el->context(), element_hidden.Get());

  EXPECT_CALL_IN_SCOPE(element_hidden, Run,
                       tracked_element_handler()->OnVisibilityChanged(
                           content::Visibility::HIDDEN));
}

TEST_F(HelpBubbleHandlerTest, ElementHiddenWebContentsBecomingUnknown) {
  UNCALLED_MOCK_CALLBACK(ui::ElementTracker::Callback, element_shown);
  UNCALLED_MOCK_CALLBACK(ui::ElementTracker::Callback, element_hidden);

  const base::CallbackListSubscription sub1 =
      ui::ElementTracker::GetElementTracker()
          ->AddElementShownInAnyContextCallback(
              kHelpBubbleHandlerTestElementIdentifier, element_shown.Get());
  base::CallbackListSubscription sub2;

  EXPECT_CALL(element_shown, Run).WillOnce([&](ui::TrackedElement* el) {
    sub2 = ui::ElementTracker::GetElementTracker()->AddElementHiddenCallback(
        el->identifier(), el->context(), element_hidden.Get());
  });
  tracked_element_handler()->TrackedElementVisibilityChanged(
      GetId(kHelpBubbleHandlerTestElementIdentifier, kSecondaryId1), true,
      kElementBounds);

  EXPECT_CALL_IN_SCOPE(element_hidden, Run, {
    test_handler_.reset();
    tracked_element_handler_.reset();
  });
}

TEST_F(HelpBubbleHandlerTest, WebContentsVisibilityCanChangeMultipleTimes) {
  UNCALLED_MOCK_CALLBACK(ui::ElementTracker::Callback, element_shown);
  const auto subscription =
      ui::ElementTracker::GetElementTracker()
          ->AddElementShownInAnyContextCallback(
              kHelpBubbleHandlerTestElementIdentifier, element_shown.Get());

  EXPECT_CALL_IN_SCOPE(
      element_shown, Run,
      tracked_element_handler()->TrackedElementVisibilityChanged(
          GetId(kHelpBubbleHandlerTestElementIdentifier, kSecondaryId1), true,
          kElementBounds));

  tracked_element_handler()->OnVisibilityChanged(content::Visibility::HIDDEN);
  EXPECT_CALL_IN_SCOPE(element_shown, Run,
                       tracked_element_handler()->OnVisibilityChanged(
                           content::Visibility::VISIBLE));
  tracked_element_handler()->OnVisibilityChanged(content::Visibility::HIDDEN);
  EXPECT_CALL_IN_SCOPE(element_shown, Run,
                       tracked_element_handler()->OnVisibilityChanged(
                           content::Visibility::VISIBLE));
}

TEST_F(HelpBubbleHandlerTest, DestroyHandlerDuringCallback) {
  UNCALLED_MOCK_CALLBACK(ui::ElementTracker::Callback, element_shown);
  const auto subscription =
      ui::ElementTracker::GetElementTracker()
          ->AddElementShownInAnyContextCallback(
              kHelpBubbleHandlerTestElementIdentifier, element_shown.Get());

  EXPECT_CALL_IN_SCOPE(
      element_shown, Run,
      tracked_element_handler()->TrackedElementVisibilityChanged(
          GetId(kHelpBubbleHandlerTestElementIdentifier, kSecondaryId1), true,
          kElementBounds));
  tracked_element_handler()->TrackedElementVisibilityChanged(
      GetId(kHelpBubbleHandlerTestElementIdentifier2, kSecondaryId2), true,
      kElementBounds);

  tracked_element_handler()->OnVisibilityChanged(content::Visibility::HIDDEN);
  EXPECT_CALL(element_shown, Run).WillOnce([&]() { test_handler_.reset(); });
  tracked_element_handler()->OnVisibilityChanged(content::Visibility::VISIBLE);
}

TEST_F(HelpBubbleHandlerTest,
       WebUIHelpBubblePreventsHideWhenVisibilityChanges) {
  UNCALLED_MOCK_CALLBACK(ui::ElementTracker::Callback, element_hidden);

  tracked_element_handler()->TrackedElementVisibilityChanged(
      GetId(kHelpBubbleHandlerTestElementIdentifier, kSecondaryId1), true,
      kElementBounds);
  auto* const element =
      ui::ElementTracker::GetElementTracker()->GetUniqueElement(
          kHelpBubbleHandlerTestElementIdentifier, test_handler_->context());
  ASSERT_NE(nullptr, element);
  const auto subscription =
      ui::ElementTracker::GetElementTracker()->AddElementHiddenCallback(
          element->identifier(), element->context(), element_hidden.Get());

  HelpBubbleParams params;
  params.body_text = u"Help bubble body.";
  params.arrow = HelpBubbleArrow::kTopCenter;

  EXPECT_CALL(test_handler_->mock(), ShowHelpBubble(testing::_));
  auto help_bubble = help_bubble_factory_registry_.CreateHelpBubble(
      element, std::move(params));

  tracked_element_handler()->OnVisibilityChanged(content::Visibility::HIDDEN);
  EXPECT_TRUE(help_bubble->is_open());

  EXPECT_CALL(test_handler_->mock(), HideHelpBubble(testing::_));
  EXPECT_CALL_IN_SCOPE(
      element_hidden, Run,
      help_bubble->Close(HelpBubble::CloseReason::kProgrammaticallyClosed));
}

}  // namespace user_education
