// Copyright 2026 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/views/location_bar/webui_content_setting_image_control.h"

#include <memory>
#include <vector>

#include "base/functional/callback_helpers.h"
#include "base/memory/raw_ptr.h"
#include "base/test/test_future.h"
#include "chrome/browser/ui/content_settings/content_setting_image_model.h"
#include "chrome/browser/ui/content_settings/content_setting_image_view_delegate.h"
#include "chrome/browser/ui/views/toolbar/mock_webui_toolbar_control_delegate.h"
#include "chrome/grit/generated_resources.h"
#include "chrome/test/base/chrome_render_view_host_test_harness.h"
#include "content/public/browser/page.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "ui/base/l10n/l10n_util.h"

namespace {

using ImageType = ContentSettingImageModel::ImageType;

class TestDelegate : public ContentSettingImageViewDelegate {
 public:
  TestDelegate() = default;
  ~TestDelegate() override = default;

  bool ShouldHideContentSettingImage() override { return should_hide_; }
  content::WebContents* GetContentSettingWebContents() override {
    return web_contents_;
  }
  ContentSettingBubbleModelDelegate* GetContentSettingBubbleModelDelegate()
      override {
    return nullptr;
  }

  void SetWebContents(content::WebContents* web_contents) {
    web_contents_ = web_contents;
  }

  void set_should_hide(bool v) { should_hide_ = v; }

 private:
  bool should_hide_ = false;

  // Safe, this delegate is destroyed in TearDown(), before the WebContents.
  raw_ptr<content::WebContents> web_contents_ = nullptr;
};

class FakeContentSettingImageModel : public ContentSettingSimpleImageModel {
 public:
  FakeContentSettingImageModel(
      ImageType image_type,
      ContentSettingsType content_type,
      bool image_type_should_notify_accessibility = false)
      : ContentSettingSimpleImageModel(
            image_type,
            content_type,
            /*image_type_should_notify_accessibility=*/
            image_type_should_notify_accessibility) {}

  bool UpdateAndGetVisibility(content::WebContents* web_contents) override {
    return visible_;
  }

  void set_visible(bool visible) { visible_ = visible; }
  void set_blocked(bool blocked) { SetIcon(content_type(), blocked); }
  using ContentSettingImageModel::set_accessibility_string_id;
  using ContentSettingImageModel::set_explanatory_string_id;
  using ContentSettingImageModel::set_should_auto_open_bubble;
  using ContentSettingImageModel::set_tooltip;

 private:
  bool visible_ = false;
};

class MockContentSettingImageModel : public FakeContentSettingImageModel {
 public:
  MockContentSettingImageModel(ImageType image_type,
                               ContentSettingsType content_type)
      : FakeContentSettingImageModel(image_type, content_type) {}

  MOCK_METHOD(std::unique_ptr<ContentSettingBubbleModel>,
              CreateBubbleModelImpl,
              (ContentSettingBubbleModel::Delegate*, content::Page&),
              (override));
};

}  // namespace

class WebUIContentSettingImageControlTest
    : public ChromeRenderViewHostTestHarness {
 public:
  void SetUp() override {
    ChromeRenderViewHostTestHarness::SetUp();

    delegate_ = std::make_unique<TestDelegate>();
    delegate_->SetWebContents(web_contents());
    control_ =
        std::make_unique<WebUIContentSettingImageControl>(delegate_.get());
  }

  void TearDown() override {
    control_.reset();
    delegate_.reset();
    ChromeRenderViewHostTestHarness::TearDown();
  }

  void CloseBubble() { control_->bubble_reopen_suppressor_.CloseForTesting(); }
  void SetCurrentBubbleType(std::optional<ImageType> type) {
    control_->last_tracked_bubble_type_ = type;
  }

 protected:
  std::unique_ptr<TestDelegate> delegate_;
  std::unique_ptr<WebUIContentSettingImageControl> control_;
};

TEST_F(WebUIContentSettingImageControlTest, ProcessContentSettingState_Empty) {
  control_->InitForTesting({});
  auto state = control_->ProcessContentSettingState(web_contents());
  EXPECT_EQ(0u, state.size());
}

TEST_F(WebUIContentSettingImageControlTest,
       ProcessContentSettingState_Mapping) {
  std::vector<std::unique_ptr<ContentSettingImageModel>> models;
  auto cookies_model_ptr = std::make_unique<FakeContentSettingImageModel>(
      ImageType::kCookies, ContentSettingsType::COOKIES);
  auto* cookies_model = cookies_model_ptr.get();
  models.push_back(std::move(cookies_model_ptr));

  control_->InitForTesting(std::move(models));

  // Test hidden state.
  cookies_model->set_visible(false);
  auto state = control_->ProcessContentSettingState(web_contents());
  EXPECT_EQ(0u, state.size());

  // Test visible and blocked state.
  cookies_model->set_visible(true);
  cookies_model->set_blocked(true);
  cookies_model->set_tooltip(u"Cookie Tooltip");
  cookies_model->set_explanatory_string_id(
      IDS_BLOCKED_DISPLAYING_INSECURE_CONTENT);
  cookies_model->set_accessibility_string_id(IDS_BLOCKED_POPUPS_TOOLTIP);

  state = control_->ProcessContentSettingState(web_contents());
  ASSERT_EQ(1u, state.size());
  const auto& cookie_state = state[0];

  EXPECT_EQ(ImageType::kCookies, cookie_state->type);
  EXPECT_TRUE(cookie_state->is_blocked);
  EXPECT_EQ(u"Cookie Tooltip", cookie_state->tooltip);
  EXPECT_EQ(l10n_util::GetStringUTF16(IDS_BLOCKED_DISPLAYING_INSECURE_CONTENT),
            cookie_state->explanatory_string);
  EXPECT_EQ(l10n_util::GetStringUTF16(IDS_BLOCKED_POPUPS_TOOLTIP),
            cookie_state->accessibility_string);

  // Turn off via delegate.
  delegate_->set_should_hide(true);
  state = control_->ProcessContentSettingState(web_contents());
  ASSERT_EQ(0u, state.size());
}

TEST_F(WebUIContentSettingImageControlTest,
       ProcessContentSettingState_Mapping_Multiple) {
  std::vector<std::unique_ptr<ContentSettingImageModel>> models;
  auto cookies_model_ptr = std::make_unique<FakeContentSettingImageModel>(
      ImageType::kCookies, ContentSettingsType::COOKIES);
  auto* cookies_model = cookies_model_ptr.get();
  models.push_back(std::move(cookies_model_ptr));
  auto popup_model_ptr = std::make_unique<FakeContentSettingImageModel>(
      ImageType::kPopups, ContentSettingsType::POPUPS);
  auto* popup_model = popup_model_ptr.get();
  models.push_back(std::move(popup_model_ptr));

  control_->InitForTesting(std::move(models));

  // Test hidden state.
  cookies_model->set_visible(false);
  popup_model->set_visible(true);
  auto state = control_->ProcessContentSettingState(web_contents());
  EXPECT_EQ(1u, state.size());

  // Test visible and blocked state.
  cookies_model->set_visible(true);
  cookies_model->set_blocked(true);
  cookies_model->set_tooltip(u"Cookie Tooltip");

  popup_model->set_visible(true);
  popup_model->set_blocked(false);
  popup_model->set_tooltip(u"Popup Tooltip");

  state = control_->ProcessContentSettingState(web_contents());
  ASSERT_EQ(2u, state.size());
  const auto& cookie_state = state[0];
  const auto& popup_state = state[1];

  EXPECT_EQ(ImageType::kCookies, cookie_state->type);
  EXPECT_TRUE(cookie_state->is_blocked);
  EXPECT_EQ(u"Cookie Tooltip", cookie_state->tooltip);

  EXPECT_EQ(ImageType::kPopups, popup_state->type);
  EXPECT_FALSE(popup_state->is_blocked);
  EXPECT_EQ(u"Popup Tooltip", popup_state->tooltip);
}

TEST_F(WebUIContentSettingImageControlTest, ShowContentSettingsBubble) {
  std::vector<std::unique_ptr<ContentSettingImageModel>> models;
  auto cookies_model_ptr = std::make_unique<MockContentSettingImageModel>(
      ImageType::kCookies, ContentSettingsType::COOKIES);
  auto* cookies_model = cookies_model_ptr.get();
  cookies_model->set_visible(true);
  models.push_back(std::move(cookies_model_ptr));

  auto popups_model_ptr = std::make_unique<MockContentSettingImageModel>(
      ImageType::kPopups, ContentSettingsType::POPUPS);
  auto* popups_model = popups_model_ptr.get();
  popups_model->set_visible(true);
  models.push_back(std::move(popups_model_ptr));

  control_->InitForTesting(std::move(models));

  // The cookies model should be called.
  EXPECT_CALL(
      *cookies_model,
      CreateBubbleModelImpl(delegate_->GetContentSettingBubbleModelDelegate(),
                            testing::Ref(web_contents()->GetPrimaryPage())))
      .WillOnce(testing::Return(
          testing::ByMove(std::unique_ptr<ContentSettingBubbleModel>())));

  // The popups model should NOT be called.
  EXPECT_CALL(*popups_model, CreateBubbleModelImpl(testing::_, testing::_))
      .Times(0);

  control_->ShowContentSettingsBubble(
      ImageType::kCookies, /*is_pointer_interaction=*/true, base::DoNothing());
}

TEST_F(WebUIContentSettingImageControlTest, AutoOpenBubble) {
  std::vector<std::unique_ptr<ContentSettingImageModel>> models;
  auto cookies_model_ptr = std::make_unique<MockContentSettingImageModel>(
      ImageType::kCookies, ContentSettingsType::COOKIES);
  auto* cookies_model = cookies_model_ptr.get();
  cookies_model->set_visible(true);
  cookies_model->set_should_auto_open_bubble(true);
  models.push_back(std::move(cookies_model_ptr));

  auto popups_model_ptr = std::make_unique<MockContentSettingImageModel>(
      ImageType::kPopups, ContentSettingsType::POPUPS);
  auto* popups_model = popups_model_ptr.get();
  popups_model->set_visible(true);
  models.push_back(std::move(popups_model_ptr));

  control_->InitForTesting(std::move(models));

  // The cookies model should be called.
  EXPECT_CALL(
      *cookies_model,
      CreateBubbleModelImpl(delegate_->GetContentSettingBubbleModelDelegate(),
                            testing::Ref(web_contents()->GetPrimaryPage())))
      .WillOnce(testing::Return(
          testing::ByMove(std::unique_ptr<ContentSettingBubbleModel>())));

  // The popups model should NOT be called.
  EXPECT_CALL(*popups_model, CreateBubbleModelImpl(testing::_, testing::_))
      .Times(0);

  auto state = control_->ProcessContentSettingState(web_contents());
  EXPECT_EQ(2u, state.size());
}

TEST_F(WebUIContentSettingImageControlTest, AccessibilityAnnouncement) {
  std::vector<std::unique_ptr<ContentSettingImageModel>> models;
  auto cookies_model_ptr = std::make_unique<FakeContentSettingImageModel>(
      ImageType::kCookies, ContentSettingsType::COOKIES,
      /*image_type_should_notify_accessibility=*/true);
  auto* cookies_model = cookies_model_ptr.get();
  cookies_model->set_visible(true);
  cookies_model->set_accessibility_string_id(IDS_BLOCKED_POPUPS_TOOLTIP);
  models.push_back(std::move(cookies_model_ptr));

  MockWebUIToolbarControlDelegate webui_delegate;
  control_->InitForTesting(std::move(models), &webui_delegate);

  std::u16string name = l10n_util::GetStringUTF16(IDS_BLOCKED_POPUPS_TOOLTIP);
  std::u16string expected_announcement = l10n_util::GetStringFUTF16(
      IDS_A11Y_INDICATORS_ANNOUNCEMENT, name,
      l10n_util::GetStringUTF16(IDS_A11Y_OMNIBOX_CHIP_HINT));

  EXPECT_CALL(webui_delegate, AnnounceAlert(expected_announcement)).Times(1);

  auto state = control_->ProcessContentSettingState(web_contents());
  ASSERT_EQ(1u, state.size());
}

TEST_F(WebUIContentSettingImageControlTest, AnimationAnnouncement) {
  std::vector<std::unique_ptr<ContentSettingImageModel>> models;
  auto popups_model_ptr = std::make_unique<FakeContentSettingImageModel>(
      ImageType::kPopups, ContentSettingsType::POPUPS,
      /*image_type_should_notify_accessibility=*/false);
  auto* popups_model = popups_model_ptr.get();
  popups_model->set_visible(true);
  popups_model->set_explanatory_string_id(IDS_BLOCKED_POPUPS_EXPLANATORY_TEXT);
  models.push_back(std::move(popups_model_ptr));

  MockWebUIToolbarControlDelegate webui_delegate;
  control_->InitForTesting(std::move(models), &webui_delegate);

  std::u16string expected_announcement =
      l10n_util::GetStringUTF16(IDS_BLOCKED_POPUPS_EXPLANATORY_TEXT);

  EXPECT_CALL(webui_delegate, AnnounceAlert(expected_announcement)).Times(1);

  auto state = control_->ProcessContentSettingState(web_contents());
  ASSERT_EQ(1u, state.size());
  EXPECT_EQ(expected_announcement, state[0]->explanatory_string);
  EXPECT_TRUE(state[0]->should_run_animation);
}

TEST_F(WebUIContentSettingImageControlTest,
       NoAnimationAnnounceOnSubsequentUpdates) {
  std::vector<std::unique_ptr<ContentSettingImageModel>> models;
  auto popups_model_ptr = std::make_unique<FakeContentSettingImageModel>(
      ImageType::kPopups, ContentSettingsType::POPUPS,
      /*image_type_should_notify_accessibility=*/false);
  auto* popups_model = popups_model_ptr.get();
  popups_model->set_visible(true);
  popups_model->set_explanatory_string_id(IDS_BLOCKED_POPUPS_EXPLANATORY_TEXT);
  models.push_back(std::move(popups_model_ptr));

  testing::StrictMock<MockWebUIToolbarControlDelegate> webui_delegate;
  control_->InitForTesting(std::move(models), &webui_delegate);

  std::u16string expected_announcement =
      l10n_util::GetStringUTF16(IDS_BLOCKED_POPUPS_EXPLANATORY_TEXT);

  // The alert should only be announced once during the first update when the
  // animation actually runs.
  EXPECT_CALL(webui_delegate, AnnounceAlert(expected_announcement)).Times(1);

  auto state1 = control_->ProcessContentSettingState(web_contents());
  ASSERT_EQ(1u, state1.size());
  EXPECT_TRUE(state1[0]->should_run_animation);

  // WebUI finishes animation and calls OnContentSettingImageAnimationEnded.
  control_->OnContentSettingImageAnimationEnded(ImageType::kPopups);

  // Second update: animation has already run, so AnnounceAlert should not be
  // called again.
  auto state2 = control_->ProcessContentSettingState(web_contents());
  ASSERT_EQ(1u, state2.size());
  EXPECT_FALSE(state2[0]->should_run_animation);
}

TEST_F(WebUIContentSettingImageControlTest, MouseClickSuppression) {
  std::vector<std::unique_ptr<ContentSettingImageModel>> models;
  auto cookies_model_ptr = std::make_unique<MockContentSettingImageModel>(
      ImageType::kCookies, ContentSettingsType::COOKIES);
  auto* cookies_model = cookies_model_ptr.get();
  cookies_model->set_visible(true);
  models.push_back(std::move(cookies_model_ptr));

  auto popups_model_ptr = std::make_unique<MockContentSettingImageModel>(
      ImageType::kPopups, ContentSettingsType::POPUPS);
  auto* popups_model = popups_model_ptr.get();
  popups_model->set_visible(true);
  models.push_back(std::move(popups_model_ptr));

  control_->InitForTesting(std::move(models));
  control_->SetSuppressionThresholdForTesting(base::Seconds(1));

  // A mouse press on the chip should NOT suppress if the bubble wasn't just
  // closed.
  control_->OnContentSettingImagePointerDown(ImageType::kCookies);

  // Show the bubble for Cookies.
  EXPECT_CALL(
      *cookies_model,
      CreateBubbleModelImpl(delegate_->GetContentSettingBubbleModelDelegate(),
                            testing::Ref(web_contents()->GetPrimaryPage())))
      .WillOnce(testing::Return(
          testing::ByMove(std::unique_ptr<ContentSettingBubbleModel>())));

  control_->ShowContentSettingsBubble(
      ImageType::kCookies, /*is_pointer_interaction=*/true, base::DoNothing());

  // A mouse press immediately after closing should trigger suppression.
  // We mock last_tracked_bubble_type_ because in the pure test environment
  // lacking a native window, ShowContentSettingsBubbleImpl won't successfully
  // spawn a Widget, thus failing to set last_tracked_bubble_type_ organically.
  SetCurrentBubbleType(ImageType::kCookies);
  CloseBubble();
  control_->OnContentSettingImagePointerDown(ImageType::kCookies);

  // A non-mouse click (e.g., keyboard Enter) should NOT consume the suppression
  // flag and should successfully open the bubble (since keyboard interactions
  // always open the bubble and never trigger suppression).
  EXPECT_CALL(
      *cookies_model,
      CreateBubbleModelImpl(delegate_->GetContentSettingBubbleModelDelegate(),
                            testing::Ref(web_contents()->GetPrimaryPage())))
      .WillOnce(testing::Return(
          testing::ByMove(std::unique_ptr<ContentSettingBubbleModel>())));
  control_->ShowContentSettingsBubble(
      ImageType::kCookies, /*is_pointer_interaction=*/false, base::DoNothing());

  // A true mouse click SHOULD consume the suppression flag and return early,
  // without calling CreateBubbleModelImpl.
  SetCurrentBubbleType(ImageType::kCookies);
  CloseBubble();
  control_->OnContentSettingImagePointerDown(ImageType::kCookies);
  EXPECT_CALL(*cookies_model, CreateBubbleModelImpl(testing::_, testing::_))
      .Times(0);
  base::test::TestFuture<
      base::expected<std::monostate, mojo_base::mojom::ErrorPtr>>
      future;
  control_->ShowContentSettingsBubble(ImageType::kCookies,
                                      /*is_pointer_interaction=*/true,
                                      future.GetCallback());
  // The callback should immediately complete with std::monostate without
  // showing anything.
  EXPECT_TRUE(future.IsReady());
  EXPECT_TRUE(future.Get().has_value());

  // Clicking on a DIFFERENT chip should NOT suppress the new chip's bubble,
  // even if the previous chip's bubble was just closed.
  // We simulate opening kCookies, closing it, and then clicking kPopups.
  EXPECT_CALL(
      *cookies_model,
      CreateBubbleModelImpl(delegate_->GetContentSettingBubbleModelDelegate(),
                            testing::Ref(web_contents()->GetPrimaryPage())))
      .WillOnce(testing::Return(
          testing::ByMove(std::unique_ptr<ContentSettingBubbleModel>())));
  control_->ShowContentSettingsBubble(
      ImageType::kCookies, /*is_pointer_interaction=*/true, base::DoNothing());
  SetCurrentBubbleType(ImageType::kCookies);
  CloseBubble();

  // Mouse press on kPopups. Since last_tracked_bubble_type_ is kCookies, it
  // should NOT suppress.
  control_->OnContentSettingImagePointerDown(ImageType::kPopups);

  // And the popups chip should successfully open.
  EXPECT_CALL(
      *popups_model,
      CreateBubbleModelImpl(delegate_->GetContentSettingBubbleModelDelegate(),
                            testing::Ref(web_contents()->GetPrimaryPage())))
      .WillOnce(testing::Return(
          testing::ByMove(std::unique_ptr<ContentSettingBubbleModel>())));
  control_->ShowContentSettingsBubble(
      ImageType::kPopups, /*is_pointer_interaction=*/true, base::DoNothing());
}

TEST_F(WebUIContentSettingImageControlTest, TestPressed) {
  std::vector<std::unique_ptr<ContentSettingImageModel>> models;
  auto cookies_model_ptr = std::make_unique<MockContentSettingImageModel>(
      ImageType::kCookies, ContentSettingsType::COOKIES);
  auto* cookies_model = cookies_model_ptr.get();
  models.push_back(std::move(cookies_model_ptr));

  auto popups_model_ptr = std::make_unique<MockContentSettingImageModel>(
      ImageType::kPopups, ContentSettingsType::POPUPS);
  models.push_back(std::move(popups_model_ptr));

  control_->InitForTesting(std::move(models));

  // Out of bounds index should return false.
  EXPECT_FALSE(control_->TestPressed(2));

  // Hidden model should return false.
  cookies_model->set_visible(false);
  control_->ProcessContentSettingState(web_contents());
  EXPECT_FALSE(control_->TestPressed(0));

  // Visible model should attempt to show bubble and return true.
  cookies_model->set_visible(true);
  control_->ProcessContentSettingState(web_contents());
  EXPECT_CALL(
      *cookies_model,
      CreateBubbleModelImpl(delegate_->GetContentSettingBubbleModelDelegate(),
                            testing::Ref(web_contents()->GetPrimaryPage())))
      .WillOnce(testing::Return(
          testing::ByMove(std::unique_ptr<ContentSettingBubbleModel>())));
  EXPECT_TRUE(control_->TestPressed(0));
}
