// 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/views/help_bubble_factory_views.h"

#include <memory>

#include "base/functional/bind.h"
#include "base/functional/callback_helpers.h"
#include "base/logging.h"
#include "base/task/single_thread_task_runner.h"
#include "base/test/bind.h"
#include "base/test/mock_callback.h"
#include "build/build_config.h"
#include "components/user_education/common/help_bubble/help_bubble_params.h"
#include "components/user_education/common/user_education_class_properties.h"
#include "components/user_education/views/help_bubble_view.h"
#include "components/user_education/views/help_bubble_views.h"
#include "components/user_education/views/help_bubble_views_test_util.h"
#include "ui/base/interaction/element_events.h"
#include "ui/base/interaction/element_identifier.h"
#include "ui/base/interaction/expect_call_in_scope.h"
#include "ui/base/interaction/interaction_sequence_test_util.h"
#include "ui/base/interaction/interaction_test_util.h"
#include "ui/base/metadata/metadata_header_macros.h"
#include "ui/base/metadata/metadata_impl_macros.h"
#include "ui/views/interaction/element_tracker_views.h"
#include "ui/views/interaction/interaction_test_util_views.h"
#include "ui/views/interaction/view_subregion_anchor.h"
#include "ui/views/layout/fill_layout.h"
#include "ui/views/test/views_test_base.h"
#include "ui/views/test/widget_test.h"
#include "ui/views/view.h"
#include "ui/views/view_class_properties.h"

namespace user_education {

namespace {

DEFINE_LOCAL_ELEMENT_IDENTIFIER_VALUE(kTestElementId);
DEFINE_LOCAL_ELEMENT_IDENTIFIER_VALUE(kTestAnchorId);

// View which can synchronously resize itself when a help bubble is added or
// removed. This simulates logic that happens in some toolbar icons, like the
// avatar button. This is not good behavior, but exists due to (among other
// things) https://crbug.com/40707582.
class HelpBubbleAwareView : public views::View {
  METADATA_HEADER(HelpBubbleAwareView, views::View)
 public:
  static constexpr gfx::Size kOriginalSize = gfx::Size(100, 100);
  static constexpr gfx::Size kWithBubbleSize = gfx::Size(150, 150);

  HelpBubbleAwareView() { SetPreferredSize(gfx::Size(100, 100)); }
  ~HelpBubbleAwareView() override = default;

  void set_respond_to_help_bubble(bool respond_to_help_bubble) {
    respond_to_help_bubble_ = respond_to_help_bubble;
  }

  void AfterPropertyChange(const void* key, int64_t old_value) override {
    if (respond_to_help_bubble_ && key == kHasInProductHelpPromoKey) {
      // This is the kind of bad behavior some toolbar buttons do.
      SetPreferredSize(GetProperty(kHasInProductHelpPromoKey) ? kWithBubbleSize
                                                              : kOriginalSize);
      SizeToPreferredSize();
    }
    View::AfterPropertyChange(key, old_value);
  }

 private:
  bool respond_to_help_bubble_ = false;
};

BEGIN_METADATA(HelpBubbleAwareView)
END_METADATA

}  // namespace

class HelpBubbleFactoryViewsTest : public views::ViewsTestBase {
 public:
  HelpBubbleFactoryViewsTest() = default;
  ~HelpBubbleFactoryViewsTest() override = default;

  void SetUp() override {
    ViewsTestBase::SetUp();
    widget_ = std::make_unique<test::TestThemedWidget>();
    widget_->Init(CreateParamsForTestWidget());
    contents_view_ = widget_->SetContentsView(std::make_unique<views::View>());
    contents_view_->SetLayoutManager(std::make_unique<views::FillLayout>());
    anchor_view_ =
        contents_view_->AddChildView(std::make_unique<HelpBubbleAwareView>());
    anchor_view_->SetProperty(views::kElementIdentifierKey, kTestElementId);
    views::test::WidgetVisibleWaiter waiter(widget_.get());
    widget_->Show();
    waiter.Wait();
    widget_->LayoutRootViewIfNecessary();
  }

  void TearDown() override {
    CloseWidget();
    ViewsTestBase::TearDown();
  }

 protected:
  std::unique_ptr<HelpBubble> CreateHelpBubble(
      views::View* view,
      base::RepeatingClosure button_callback) {
    HelpBubbleParams params;
    params.body_text = u"To X, do Y";
    params.arrow = HelpBubbleArrow::kTopRight;

    if (button_callback) {
      HelpBubbleButtonParams button_params;
      button_params.text = u"Go away";
      button_params.is_default = true;
      button_params.callback = std::move(button_callback);
      params.buttons.push_back(std::move(button_params));
    }

    views::TrackedElementViews* const element =
        views::ElementTrackerViews::GetInstance()->GetElementForView(view);
    CHECK(element);
    return factory_.CreateBubble(element, std::move(params));
  }

  void CloseWidget() {
    contents_view_ = nullptr;
    anchor_view_ = nullptr;
    widget_.reset();
  }

  void FlushEvents() {
    base::RunLoop run_loop(base::RunLoop::Type::kNestableTasksAllowed);
    base::SingleThreadTaskRunner::GetCurrentDefault()->PostTask(
        FROM_HERE, run_loop.QuitClosure());
    run_loop.Run();
  }

  void MaybeUpdateAnchor(ui::TrackedElement* anchor,
                         gfx::Rect local_anchor_region) {
    if (auto* const subregion_anchor =
            anchor->AsA<views::ViewSubregionAnchor>()) {
      subregion_anchor->MaybeUpdateAnchor(local_anchor_region);
    } else {
      ui::ElementTracker::GetFrameworkDelegate()->NotifyCustomEvent(
          anchor, views::ViewSubregionAnchor::kAnchorBoundsChangedEvent);
    }
  }

  test::TestHelpBubbleDelegate test_delegate_;
  HelpBubbleFactoryViews factory_{&test_delegate_};
  raw_ptr<views::View> contents_view_;
  raw_ptr<HelpBubbleAwareView> anchor_view_;
  std::unique_ptr<views::Widget> widget_;
};

TEST_F(HelpBubbleFactoryViewsTest, TestShowHelpBubble) {
  auto help_bubble = CreateHelpBubble(anchor_view_.get(), base::DoNothing());
  ASSERT_TRUE(help_bubble);
}

// Regression test for https://crbug.com/510523784; this crashes without the
// associated changes. This simulates what happens when a help bubble is
// attached to the avatar button during browser teardown.
TEST_F(HelpBubbleFactoryViewsTest,
       TestCloseWidgetWithHelpBubbleAndBubbleAwareAnchor) {
  anchor_view_->set_respond_to_help_bubble(true);
  auto help_bubble = CreateHelpBubble(anchor_view_.get(), base::DoNothing());
  FlushEvents();
  help_bubble->AsA<HelpBubbleViews>()
      ->bubble_view_for_testing()
      ->GetWidget()
      ->CloseNow();
  FlushEvents();
  EXPECT_FALSE(help_bubble->is_open());
}

// TODO(https://crbug.com/502638609): In Fuchsia, this test causes an unrelated
// crash on the GPU thread.
#if BUILDFLAG(IS_FUCHSIA)
#define MAYBE_HelpBubbleDismissedOnAnchorHidden \
  DISABLED_HelpBubbleDismissedOnAnchorHidden
#else
#define MAYBE_HelpBubbleDismissedOnAnchorHidden \
  HelpBubbleDismissedOnAnchorHidden
#endif
TEST_F(HelpBubbleFactoryViewsTest, MAYBE_HelpBubbleDismissedOnAnchorHidden) {
  UNCALLED_MOCK_CALLBACK(HelpBubble::ClosingCallback, closing);
  UNCALLED_MOCK_CALLBACK(HelpBubble::ClosedCallback, closed);

  auto help_bubble = CreateHelpBubble(anchor_view_.get(), base::DoNothing());
  auto subscription1 = help_bubble->AddOnClosingCallback(closing.Get());
  auto subscription2 = help_bubble->AddOnClosedCallback(closed.Get());

  // Wait for the help bubble to close.
  EXPECT_ASYNC_CALLS_IN_SCOPE_2(
      closing, Run(help_bubble.get(), HelpBubble::CloseReason::kAnchorHidden),
      closed, Run(HelpBubble::CloseReason::kAnchorHidden),
      anchor_view_->SetVisible(false));
}

class HelpBubbleFactoryViewsSubregionAnchorTest
    : public HelpBubbleFactoryViewsTest {
 public:
  HelpBubbleFactoryViewsSubregionAnchorTest() = default;
  ~HelpBubbleFactoryViewsSubregionAnchorTest() override = default;

  void SetUp() override {
    HelpBubbleFactoryViewsTest::SetUp();
    anchor_ = std::make_unique<views::ViewSubregionAnchor>(kTestAnchorId,
                                                           *anchor_view_);
  }

  void TearDown() override {
    anchor_.reset();
    HelpBubbleFactoryViewsTest::TearDown();
  }

 protected:
  bool AnchorVisible() const {
    return ui::ElementTracker::GetElementTracker()->IsElementVisible(
        kTestAnchorId,
        views::ElementTrackerViews::GetContextForWidget(widget_.get()));
  }

  std::unique_ptr<HelpBubble> CreateHelpBubble(
      base::RepeatingClosure button_callback) {
    CHECK(AnchorVisible());

    HelpBubbleParams params;
    params.body_text = u"To X, do Y";
    params.arrow = HelpBubbleArrow::kTopRight;

    if (button_callback) {
      HelpBubbleButtonParams button_params;
      button_params.text = u"Go away";
      button_params.is_default = true;
      button_params.callback = std::move(button_callback);
      params.buttons.push_back(std::move(button_params));
    }

    return factory_.CreateBubble(anchor_.get(), std::move(params));
  }

  std::unique_ptr<views::ViewSubregionAnchor> anchor_;
};

TEST_F(HelpBubbleFactoryViewsSubregionAnchorTest,
       SyntheticAnchorVisibilityTracksView) {
  EXPECT_TRUE(AnchorVisible());
  anchor_view_->SetVisible(false);
  EXPECT_FALSE(AnchorVisible());
  anchor_view_->SetVisible(true);
  EXPECT_TRUE(AnchorVisible());
  contents_view_->SetVisible(false);
  EXPECT_FALSE(AnchorVisible());
  widget_->Hide();
  // Make sure there's nothing left to process on the hide here.
  FlushEvents();
  EXPECT_FALSE(AnchorVisible());
  contents_view_->SetVisible(true);
  EXPECT_FALSE(AnchorVisible());
  widget_->Show();
  EXPECT_TRUE(AnchorVisible());
}

TEST_F(HelpBubbleFactoryViewsSubregionAnchorTest,
       SyntheticAnchorVisibilityManuallyHidden) {
  // Verify basic hiding and un-hiding.
  EXPECT_TRUE(AnchorVisible());
  anchor_->SetHidden(true);
  EXPECT_FALSE(AnchorVisible());
  anchor_->SetHidden(false);
  EXPECT_TRUE(AnchorVisible());

  // Verify that the view becoming visible doesn't override hidden state.
  anchor_->SetHidden(true);
  anchor_view_->SetVisible(false);
  anchor_view_->SetVisible(true);
  EXPECT_FALSE(AnchorVisible());
  anchor_->SetHidden(false);
  EXPECT_TRUE(AnchorVisible());

  // Verify that un-hiding doesn't override view visibility state.
  anchor_view_->SetVisible(false);
  anchor_->SetHidden(true);
  anchor_->SetHidden(false);
  EXPECT_FALSE(AnchorVisible());
  anchor_view_->SetVisible(true);
  EXPECT_TRUE(AnchorVisible());

  // Verify that deleting the anchor causes it to hide.
  anchor_.reset();
  EXPECT_FALSE(AnchorVisible());
}

TEST_F(HelpBubbleFactoryViewsSubregionAnchorTest, SyntheticAnchorBounds) {
  MaybeUpdateAnchor(anchor_.get(), gfx::Rect(1, 1, 1, 1));
  EXPECT_TRUE(
      anchor_view_->GetBoundsInScreen().Contains(anchor_->GetScreenBounds()));
  EXPECT_TRUE(
      widget_->GetWindowBoundsInScreen().Contains(anchor_->GetScreenBounds()));
}

TEST_F(HelpBubbleFactoryViewsSubregionAnchorTest, ShowBubbleOnSyntheticAnchor) {
  MaybeUpdateAnchor(anchor_.get(), gfx::Rect(1, 1, 1, 1));
  auto help_bubble = CreateHelpBubble(base::DoNothing());
  ASSERT_TRUE(help_bubble);
  EXPECT_EQ(help_bubble->AsA<HelpBubbleViews>()
                ->bubble_view_for_testing()
                ->GetAnchorRect(),
            anchor_->GetScreenBounds());
  EXPECT_EQ(widget_->GetNativeView(), anchor_->GetNativeView());
}

TEST_F(HelpBubbleFactoryViewsSubregionAnchorTest,
       BubbleMovesWithSyntheticAnchor) {
  MaybeUpdateAnchor(anchor_.get(), gfx::Rect(1, 1, 1, 1));
  auto help_bubble = CreateHelpBubble(base::DoNothing());
  auto* const help_bubble_view =
      help_bubble->AsA<HelpBubbleViews>()->bubble_view_for_testing();
  const gfx::Rect old_bounds = help_bubble_view->GetBoundsInScreen();
  MaybeUpdateAnchor(anchor_.get(), gfx::Rect(20, 30, 13, 17));
  EXPECT_EQ(help_bubble_view->GetAnchorRect(), anchor_->GetScreenBounds());
  const gfx::Rect new_bounds = help_bubble_view->GetBoundsInScreen();
  EXPECT_GT(new_bounds.x(), old_bounds.x());
  EXPECT_GT(new_bounds.y(), old_bounds.y());
}

// TODO(https://crbug.com/502638609): In Fuchsia, this test causes an unrelated
// crash on the GPU thread.
#if BUILDFLAG(IS_FUCHSIA)
#define MAYBE_HelpBubbleDismissedOnAnchorHidden \
  DISABLED_HelpBubbleDismissedOnAnchorHidden
#else
#define MAYBE_HelpBubbleDismissedOnAnchorHidden \
  HelpBubbleDismissedOnAnchorHidden
#endif
TEST_F(HelpBubbleFactoryViewsSubregionAnchorTest,
       MAYBE_HelpBubbleDismissedOnAnchorHidden) {
  UNCALLED_MOCK_CALLBACK(HelpBubble::ClosingCallback, closing);
  UNCALLED_MOCK_CALLBACK(HelpBubble::ClosedCallback, closed);

  auto help_bubble = CreateHelpBubble(base::DoNothing());
  auto subscription1 = help_bubble->AddOnClosingCallback(closing.Get());
  auto subscription2 = help_bubble->AddOnClosedCallback(closed.Get());

  // Wait for the help bubble to close.
  EXPECT_ASYNC_CALLS_IN_SCOPE_2(
      closing, Run(help_bubble.get(), HelpBubble::CloseReason::kAnchorHidden),
      closed, Run(HelpBubble::CloseReason::kAnchorHidden),
      anchor_view_->SetVisible(false));
}

}  // namespace user_education
