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

#include "chrome/browser/ui/page_action/page_action_model.h"

#include "base/memory/raw_ref.h"
#include "base/types/pass_key.h"
#include "chrome/browser/ui/page_action/page_action_controller.h"
#include "chrome/browser/ui/page_action/page_action_model_observer.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "ui/actions/actions.h"
#include "ui/gfx/image/image_skia.h"
#include "ui/gfx/image/image_unittest_util.h"
#include "ui/menus/simple_menu_model.h"

namespace page_actions {
namespace {

using ::actions::ActionItem;

const std::u16string kOverrideText = u"Override";
const std::u16string kTestText = u"Test";
const std::u16string kTooltipText = u"Tooltip";
const std::u16string kDefaultText = u"DefaultName";
const std::u16string kOverrideName = u"OverrideName";
const ui::ImageModel kTestImage =
    ui::ImageModel::FromImageSkia(gfx::test::CreateImageSkia(/*size=*/16));

PageActionPassKey PassKey() {
  return PageActionPassKey::PassKeyForTesting();
}

class MockPageActionModelObserver : public PageActionModelObserver {
 public:
  MOCK_METHOD(void,
              OnPageActionModelChanged,
              (const PageActionModelInterface& model),
              (override));
  MOCK_METHOD(void,
              OnPageActionModelWillBeDeleted,
              (const PageActionModelInterface& model),
              (override));
};

class ReentrantPageActionModelObserver : public PageActionModelObserver {
 public:
  explicit ReentrantPageActionModelObserver(PageActionModel& model)
      : model_(model) {}

  void OnPageActionModelChanged(
      const PageActionModelInterface& model) override {
    ++change_count_;
    if (!did_trigger_reentrant_update_) {
      did_trigger_reentrant_update_ = true;
      model_->SetActionActive(PassKey(), true);
    }
  }

  void OnPageActionModelWillBeDeleted(
      const PageActionModelInterface& model) override {}

  int change_count() const { return change_count_; }

 private:
  const raw_ref<PageActionModel> model_;
  int change_count_ = 0;
  bool did_trigger_reentrant_update_ = false;
};

class PageActionModelTest : public ::testing::Test {
 protected:
  PageActionModelTest() : model_(0) {}

  void SetUp() override { model_.AddObserver(&observer_); }

  void TearDown() override { model_.RemoveObserver(&observer_); }

  PageActionModel model_;
  testing::NiceMock<MockPageActionModelObserver> observer_;
};

TEST_F(PageActionModelTest, DefaultVisibility) {
  EXPECT_FALSE(model_.GetVisible());
}

TEST_F(PageActionModelTest, VisibilityConditions) {
  EXPECT_CALL(observer_, OnPageActionModelChanged).Times(4);

  model_.SetShowRequested(PassKey(), true);
  EXPECT_FALSE(model_.GetVisible());

  model_.SetActionItemProperties(
      PassKey(),
      ActionItem::Builder().SetEnabled(true).SetVisible(true).Build().get());
  EXPECT_FALSE(model_.GetVisible());

  model_.SetTabActive(PassKey(), true);
  EXPECT_TRUE(model_.GetVisible());

  model_.SetHasPinnedIcon(PassKey(), true);
  EXPECT_FALSE(model_.GetVisible());
}

TEST_F(PageActionModelTest, ShouldChipBeVisible) {
  EXPECT_CALL(observer_, OnPageActionModelChanged).Times(2);

  model_.SetShouldShowSuggestionChip(PassKey(), true);
  EXPECT_EQ(model_.ShouldShowSuggestionChip(), true);

  model_.SetShouldShowSuggestionChip(PassKey(), false);
  EXPECT_EQ(model_.ShouldShowSuggestionChip(), false);
}

TEST_F(PageActionModelTest, ChipVisibility) {
  EXPECT_CALL(observer_, OnPageActionModelChanged).Times(2);

  model_.SetIsChipShowing(PassKey(), true);
  EXPECT_EQ(model_.IsChipShowing(), true);

  model_.SetIsChipShowing(PassKey(), false);
  EXPECT_EQ(model_.IsChipShowing(), false);
}

TEST_F(PageActionModelTest, ShouldAnnounceChip) {
  EXPECT_CALL(observer_, OnPageActionModelChanged).Times(1);
  model_.SetSuggestionChipConfig(PassKey(), {.should_announce_chip = true});
  EXPECT_EQ(model_.GetShouldAnnounceChip(), true);

  EXPECT_CALL(observer_, OnPageActionModelChanged).Times(1);
  model_.SetSuggestionChipConfig(PassKey(), {.should_announce_chip = false});
  EXPECT_EQ(model_.GetShouldAnnounceChip(), false);
}

TEST_F(PageActionModelTest, ShouldAnimateChip) {
  model_.SetSuggestionChipConfig(PassKey(), {.should_animate = true});
  EXPECT_EQ(model_.GetShouldAnimateChipOut(), true);
  EXPECT_EQ(model_.GetShouldAnimateChipIn(), true);

  EXPECT_CALL(observer_, OnPageActionModelChanged).Times(1);
  model_.SetSuggestionChipConfig(PassKey(), {.should_animate = false});
  EXPECT_EQ(model_.GetShouldAnimateChipOut(), false);
  EXPECT_EQ(model_.GetShouldAnimateChipIn(), false);
}

TEST_F(PageActionModelTest, ShouldAnimateIn) {
  model_.SetSuggestionChipConfig(PassKey(), {.should_animate = true});
  model_.SetShouldShowSuggestionChip(PassKey(), true);
  EXPECT_EQ(model_.GetShouldAnimateChipIn(), true);

  // Mark the chip as shown.
  model_.SetIsChipShowing(PassKey(), true);
  EXPECT_EQ(model_.GetShouldAnimateChipIn(), false);

  // Hiding the chip should not reset the animation state.
  model_.SetIsChipShowing(PassKey(), false);
  EXPECT_EQ(model_.GetShouldAnimateChipIn(), false);

  // Requesting to show the chip again should the animation state.
  model_.SetShouldShowSuggestionChip(PassKey(), true);
  EXPECT_EQ(model_.GetShouldAnimateChipIn(), true);
}

TEST_F(PageActionModelTest, OverrideText) {
  EXPECT_CALL(observer_, OnPageActionModelChanged).Times(2);

  model_.SetOverrideText(PassKey(), kOverrideText);
  EXPECT_EQ(model_.GetText(), kOverrideText);

  model_.SetOverrideText(PassKey(), std::nullopt);
  EXPECT_EQ(model_.GetText(), std::u16string());
}

TEST_F(PageActionModelTest, OverrideImage) {
  model_.SetActionItemProperties(
      PassKey(), ActionItem::Builder().SetImage(kTestImage).Build().get());
  EXPECT_EQ(model_.GetImage(), kTestImage);

  ui::ImageModel kOverrideImage =
      ui::ImageModel::FromImageSkia(gfx::test::CreateImageSkia(/*size=*/32));
  constexpr int kImageAnimationResourceId = 10;

  page_actions::PageActionAnimationParams params{.resource_id =
                                                     kImageAnimationResourceId};

  EXPECT_CALL(observer_, OnPageActionModelChanged).Times(1);
  model_.SetOverrideImage(PassKey(), kOverrideImage,
                          PageActionColorSource::kForeground, params);
  EXPECT_EQ(model_.GetImage(), kOverrideImage);
  EXPECT_TRUE(model_.GetShouldAnimateImage());
  EXPECT_EQ(model_.GetImageAnimationParameters(), params);

  EXPECT_CALL(observer_, OnPageActionModelChanged).Times(1);
  model_.SetOverrideImage(PassKey(), std::nullopt,
                          PageActionColorSource::kForeground, std::nullopt);
  EXPECT_EQ(model_.GetImage(), kTestImage);
  EXPECT_FALSE(model_.GetShouldAnimateImage());

  EXPECT_CALL(observer_, OnPageActionModelChanged).Times(1);
  model_.SetOverrideImage(PassKey(), kOverrideImage,
                          PageActionColorSource::kForeground, params);
  model_.SetDidAnimateImage(PassKey());
  EXPECT_FALSE(model_.GetShouldAnimateImage());
}

TEST_F(PageActionModelTest, OverrideTooltip) {
  auto action_item = ActionItem::Builder().SetTooltipText(kTooltipText).Build();
  EXPECT_CALL(observer_, OnPageActionModelChanged).Times(1);
  model_.SetActionItemProperties(PassKey(), action_item.get());
  EXPECT_EQ(model_.GetTooltipText(), kTooltipText);

  EXPECT_CALL(observer_, OnPageActionModelChanged).Times(1);
  model_.SetOverrideTooltip(PassKey(), kOverrideText);
  EXPECT_EQ(model_.GetTooltipText(), kOverrideText);

  EXPECT_CALL(observer_, OnPageActionModelChanged).Times(1);
  model_.SetOverrideTooltip(PassKey(), std::nullopt);
  EXPECT_EQ(model_.GetTooltipText(), kTooltipText);
}

TEST_F(PageActionModelTest, SetActionItemProperties) {
  // NOTE: The visibility is exercised by the test VisibilityConditions.
  EXPECT_CALL(observer_, OnPageActionModelChanged).Times(1);

  model_.SetActionItemProperties(PassKey(), ActionItem::Builder()
                                                .SetText(kTestText)
                                                .SetTooltipText(kTooltipText)
                                                .SetImage(kTestImage)
                                                .SetIsShowingBubble(true)
                                                .Build()
                                                .get());

  EXPECT_EQ(model_.GetText(), kTestText);
  EXPECT_EQ(model_.GetImage(), kTestImage);
  EXPECT_EQ(model_.GetTooltipText(), kTooltipText);
  EXPECT_EQ(model_.GetActionItemIsShowingBubble(), true);
}

TEST_F(PageActionModelTest, ShouldGetSuppressedByOmnibox) {
  model_.SetShowRequested(PassKey(), true);
  model_.SetActionItemProperties(
      PassKey(),
      ActionItem::Builder().SetEnabled(true).SetVisible(true).Build().get());
  model_.SetTabActive(PassKey(), true);
  // Confirm it's now visible by default.
  EXPECT_TRUE(model_.GetVisible());

  // Because we know we are about to toggle “hide” on and off once,
  // we expect 2 calls to OnPageActionModelChanged():
  //   1) false->true  => triggers a notify
  //   2) true->false  => triggers a notify
  EXPECT_CALL(observer_, OnPageActionModelChanged).Times(2);

  model_.SetIsSuppressedByOmnibox(PassKey(), true);
  EXPECT_FALSE(model_.GetVisible());

  model_.SetIsSuppressedByOmnibox(PassKey(), false);
  EXPECT_TRUE(model_.GetVisible());
}

TEST_F(PageActionModelTest, ShouldIgnoreOmniboxSuppression) {
  model_.SetShowRequested(PassKey(), true);
  model_.SetActionItemProperties(
      PassKey(),
      ActionItem::Builder().SetEnabled(true).SetVisible(true).Build().get());
  model_.SetTabActive(PassKey(), true);
  model_.SetExemptFromOmniboxSuppression(PassKey(), true);

  // Confirm it's now visible by default.
  EXPECT_TRUE(model_.GetVisible());

  EXPECT_CALL(observer_, OnPageActionModelChanged).Times(1);

  model_.SetIsSuppressedByOmnibox(PassKey(), true);
  EXPECT_TRUE(model_.GetVisible());
}

TEST_F(PageActionModelTest, OverrideAccessibleName) {
  // Set the default accessible name through SetActionItemProperties.
  EXPECT_CALL(observer_, OnPageActionModelChanged).Times(1);
  model_.SetActionItemProperties(
      PassKey(), ActionItem::Builder().SetText(kDefaultText).Build().get());

  EXPECT_EQ(model_.GetAccessibleName(), kDefaultText);

  // Set the override name — should notify and change value.
  EXPECT_CALL(observer_, OnPageActionModelChanged).Times(1);
  model_.SetOverrideAccessibleName(PassKey(), kOverrideName);
  EXPECT_EQ(model_.GetAccessibleName(), kOverrideName);

  // Clear the override — should notify and fallback to default.
  EXPECT_CALL(observer_, OnPageActionModelChanged).Times(1);
  model_.SetOverrideAccessibleName(PassKey(), std::nullopt);
  EXPECT_EQ(model_.GetAccessibleName(), kDefaultText);
}

TEST_F(PageActionModelTest, ActionActive) {
  // Default state should be inactive.
  EXPECT_FALSE(model_.GetActionActive());

  // Setting active should notify and update the state.
  EXPECT_CALL(observer_, OnPageActionModelChanged).Times(1);
  model_.SetActionActive(PassKey(), true);
  EXPECT_TRUE(model_.GetActionActive());
  testing::Mock::VerifyAndClearExpectations(&observer_);

  // Setting active again should not notify or change the state.
  EXPECT_CALL(observer_, OnPageActionModelChanged).Times(0);
  model_.SetActionActive(PassKey(), true);
  EXPECT_TRUE(model_.GetActionActive());
  testing::Mock::VerifyAndClearExpectations(&observer_);

  // Setting inactive should notify and update the state.
  EXPECT_CALL(observer_, OnPageActionModelChanged).Times(1);
  model_.SetActionActive(PassKey(), false);
  EXPECT_FALSE(model_.GetActionActive());
  testing::Mock::VerifyAndClearExpectations(&observer_);

  // Setting inactive again should not notify or change the state.
  EXPECT_CALL(observer_, OnPageActionModelChanged).Times(0);
  model_.SetActionActive(PassKey(), false);
  EXPECT_FALSE(model_.GetActionActive());
  testing::Mock::VerifyAndClearExpectations(&observer_);
}

TEST(PageActionModelReentrancyTest, AppliesReentrantUpdateWithoutSecondNotify) {
  PageActionModel model(0);
  ReentrantPageActionModelObserver observer(model);
  model.AddObserver(&observer);

  model.SetShowRequested(PassKey(), true);

  EXPECT_TRUE(model.GetActionActive());
  EXPECT_EQ(observer.change_count(), 1);

  model.RemoveObserver(&observer);
}

TEST_F(PageActionModelTest, OverrideImageWithColorSource) {
  model_.SetActionItemProperties(
      PassKey(), ActionItem::Builder().SetImage(kTestImage).Build().get());
  EXPECT_EQ(model_.GetImage(), kTestImage);
  EXPECT_EQ(model_.GetColorSource(), PageActionColorSource::kForeground);

  ui::ImageModel kOverrideImage =
      ui::ImageModel::FromImageSkia(gfx::test::CreateImageSkia(/*size=*/32));

  // Override with a new image and color source.
  EXPECT_CALL(observer_, OnPageActionModelChanged).Times(1);
  model_.SetOverrideImage(PassKey(), kOverrideImage,
                          PageActionColorSource::kCascadingAccent,
                          std::nullopt);
  EXPECT_EQ(model_.GetImage(), kOverrideImage);
  EXPECT_EQ(model_.GetColorSource(), PageActionColorSource::kCascadingAccent);

  // Override with the same image and color source should not notify.
  EXPECT_CALL(observer_, OnPageActionModelChanged).Times(0);
  model_.SetOverrideImage(PassKey(), kOverrideImage,
                          PageActionColorSource::kCascadingAccent,
                          std::nullopt);
  EXPECT_EQ(model_.GetImage(), kOverrideImage);
  EXPECT_EQ(model_.GetColorSource(), PageActionColorSource::kCascadingAccent);

  // Clear override image, should revert to default image and preserve color
  // source.
  EXPECT_CALL(observer_, OnPageActionModelChanged).Times(1);
  model_.SetOverrideImage(PassKey(), std::nullopt,
                          PageActionColorSource::kCascadingAccent,
                          std::nullopt);
  EXPECT_EQ(model_.GetImage(), kTestImage);
  EXPECT_EQ(model_.GetColorSource(), PageActionColorSource::kCascadingAccent);
}

TEST_F(PageActionModelTest, ShouldShowAnchoredMessage) {
  EXPECT_CALL(observer_, OnPageActionModelChanged).Times(2);

  model_.SetShouldShowAnchoredMessage(PassKey(), true);
  EXPECT_EQ(model_.ShouldShowAnchoredMessage(), true);

  model_.SetShouldShowAnchoredMessage(PassKey(), false);
  EXPECT_EQ(model_.ShouldShowAnchoredMessage(), false);
}

TEST_F(PageActionModelTest, AnchoredMessageVisibility) {
  EXPECT_CALL(observer_, OnPageActionModelChanged).Times(2);

  model_.SetIsAnchoredMessageShowing(PassKey(), true);
  EXPECT_EQ(model_.IsAnchoredMessageShowing(), true);

  model_.SetIsAnchoredMessageShowing(PassKey(), false);
  EXPECT_EQ(model_.IsAnchoredMessageShowing(), false);
}

TEST_F(PageActionModelTest, AnchoredMessageText) {
  EXPECT_CALL(observer_, OnPageActionModelChanged).Times(2);

  model_.SetAnchoredMessageText(PassKey(), kTestText);
  EXPECT_EQ(model_.GetAnchoredMessageText(), kTestText);

  model_.SetAnchoredMessageText(PassKey(), std::u16string());
  EXPECT_EQ(model_.GetAnchoredMessageText(), std::u16string());
}

TEST_F(PageActionModelTest, AnchoredMessageActionChange) {
  EXPECT_CALL(observer_, OnPageActionModelChanged).Times(3);

  model_.SetAnchoredMessageAction(
      PassKey(), AnchoredMessageActionIconType::kMenu,
      std::make_unique<ui::SimpleMenuModel>(nullptr));
  EXPECT_EQ(model_.GetAnchoredMessageActionIconType(),
            AnchoredMessageActionIconType::kMenu);
  EXPECT_NE(model_.GetAnchoredMessageMenuModel(), nullptr);

  model_.SetAnchoredMessageAction(
      PassKey(), AnchoredMessageActionIconType::kClose, nullptr);
  EXPECT_EQ(model_.GetAnchoredMessageMenuModel(), nullptr);
  EXPECT_EQ(model_.GetAnchoredMessageActionIconType(),
            AnchoredMessageActionIconType::kClose);

  model_.SetAnchoredMessageAction(
      PassKey(), AnchoredMessageActionIconType::kNone, nullptr);
  EXPECT_EQ(model_.GetAnchoredMessageActionIconType(),
            AnchoredMessageActionIconType::kNone);
}

}  // namespace
}  // namespace page_actions
