// 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/webui/webui_toolbar/webui_toolbar_ui.h"

#include <memory>
#include <utility>

#include "base/json/json_reader.h"
#include "base/run_loop.h"
#include "base/values.h"
#include "chrome/browser/glic/public/glic_enabling.h"
#include "chrome/browser/ui/browser_window/public/browser_window_features.h"
#include "chrome/browser/ui/browser_window/public/browser_window_interface.h"
#include "chrome/browser/ui/webui/theme_colors_source_manager.h"
#include "chrome/browser/ui/webui/theme_colors_source_manager_factory.h"
#include "chrome/browser/ui/webui/webui_embedding_context.h"
#include "chrome/browser/ui/webui/webui_toolbar/adapters/navigation_controls_state_fetcher_impl.h"
#include "chrome/browser/ui/webui/webui_toolbar/webui_toolbar_test_utils.h"
#include "chrome/common/chrome_features.h"
#include "chrome/test/base/chrome_test_utils.h"
#include "chrome/test/base/in_process_browser_test.h"
#include "chrome/test/base/testing_profile.h"
#include "components/browser_apis/browser_controls/browser_controls_api.mojom.h"
#include "components/browser_apis/ui_controllers/toolbar/toolbar_ui_api_data_model.mojom.h"
#include "content/public/common/content_features.h"
#include "content/public/test/browser_test.h"
#include "content/public/test/browser_test_utils.h"
#include "content/public/test/test_web_ui.h"
#include "mojo/public/cpp/bindings/pending_receiver.h"
#include "mojo/public/cpp/bindings/pending_remote.h"
#include "mojo/public/cpp/bindings/receiver.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/blink/public/mojom/loader/local_resource_loader_config.mojom.h"
#include "ui/color/color_provider.h"
#include "ui/gfx/geometry/rect_f.h"
#include "url/gurl.h"

namespace {

// Helper class to manage mojo remote to the WebUIToolbarUI.
class ToolbarUIServiceConnectionManager {
 public:
  explicit ToolbarUIServiceConnectionManager(WebUIToolbarUI* ui) {
    ui->BindInterface(service_remote_.BindNewPipeAndPassReceiver());
  }

  // Not movable or copyable.
  ToolbarUIServiceConnectionManager(const ToolbarUIServiceConnectionManager&) =
      delete;
  ToolbarUIServiceConnectionManager& operator=(
      const ToolbarUIServiceConnectionManager&) = delete;

  void RegisterObserver() {
    service_remote_->Bind(base::BindOnce(
        [](ToolbarUIServiceConnectionManager* self,
           base::expected<toolbar_ui_api::mojom::InitialStatePtr,
                          mojo_base::mojom::ErrorPtr> result) {
          ASSERT_TRUE(result.has_value());
          self->mock_observer_.Bind(std::move(result.value()->update_stream));
        },
        base::Unretained(this)));
    service_remote_.FlushForTesting();
  }

  void FlushForTesting() { service_remote_.FlushForTesting(); }

  bool is_bound() { return service_remote_.is_bound(); }
  bool is_connected() { return service_remote_.is_connected(); }

  MockToolbarUIObserver& mock_observer() { return mock_observer_; }

 private:
  testing::StrictMock<MockToolbarUIObserver> mock_observer_;
  mojo::Remote<toolbar_ui_api::mojom::ToolbarUIService> service_remote_;
};

// Helper class to manage mojo remote to the WebUIToolbarUI.
class BrowserControlsServiceConnectionManager {
 public:
  explicit BrowserControlsServiceConnectionManager(WebUIToolbarUI* ui) {
    ui->BindInterface(service_remote_.BindNewPipeAndPassReceiver());
  }

  // Not movable or copyable.
  BrowserControlsServiceConnectionManager(
      const BrowserControlsServiceConnectionManager&) = delete;
  BrowserControlsServiceConnectionManager& operator=(
      const BrowserControlsServiceConnectionManager&) = delete;

  void FlushForTesting() { service_remote_.FlushForTesting(); }

  bool is_bound() { return service_remote_.is_bound(); }
  bool is_connected() { return service_remote_.is_connected(); }

 private:
  mojo::Remote<browser_controls_api::mojom::BrowserControlsService>
      service_remote_;
};

class BrowserControlsDelegate
    : public browser_controls_api::BrowserControlsService::
          BrowserControlsServiceDelegate {
 public:
  BrowserControlsDelegate() = default;
  ~BrowserControlsDelegate() override = default;

  void PermitLaunchUrl() override {}
  base::TimeTicks GetNavigationStartTicks() const override {
    return base::TimeTicks::Now();
  }
};

class MockToolbarUIDelegate
    : public toolbar_ui_api::ToolbarUIService::ToolbarUIServiceDelegate {
 public:
  MockToolbarUIDelegate() = default;
  ~MockToolbarUIDelegate() override = default;

  MOCK_METHOD(void,
              HandleContextMenu,
              (toolbar_ui_api::mojom::ContextMenuType menu_type,
               const gfx::RectF& bounds,
               ui::mojom::MenuSourceType source),
              (override));
  MOCK_METHOD(void,
              ShowOverflowMenu,
              (std::vector<toolbar_ui_api::mojom::OverflowMenuItemPtr> controls,
               const gfx::RectF& bounds,
               ui::mojom::MenuSourceType source,
               toolbar_ui_api::mojom::ToolbarUIService::ShowOverflowMenuCallback
                   callback),
              (override));
  MOCK_METHOD(void,
              ShowContentSettingsBubble,
              (::toolbar_ui_api::mojom::ContentSettingImageType type,
               bool is_pointer_interaction,
               ::toolbar_ui_api::mojom::ToolbarUIService::
                   ShowContentSettingsBubbleCallback callback),
              (override));
  MOCK_METHOD(void,
              OnContentSettingImagePointerDown,
              (::toolbar_ui_api::mojom::ContentSettingImageType type),
              (override));
  MOCK_METHOD(void,
              OnContentSettingImageAnimationEnded,
              (::toolbar_ui_api::mojom::ContentSettingImageType),
              (override));
  MOCK_METHOD(
      void,
      OnPageActionClick,
      (::toolbar_ui_api::mojom::PageActionId action_id,
       ::toolbar_ui_api::mojom::PageActionTrigger trigger,
       ::toolbar_ui_api::mojom::ToolbarUIService::OnPageActionClickCallback
           callback),
      (override));
  MOCK_METHOD(void,
              OnPageActionChipShowingChanged,
              (::toolbar_ui_api::mojom::PageActionId action_id,
               ::toolbar_ui_api::mojom::ToolbarUIService::
                   OnPageActionChipShowingChangedCallback callback),
              (override));
  MOCK_METHOD(void, OnPageInitialized, (), (override));
  MOCK_METHOD(void,
              InvokePinnedToolbarAction,
              (toolbar_ui_api::mojom::PinnedToolbarAction),
              (override));
  MOCK_METHOD(void, OnLocationBarFocusWithinChanged, (bool), (override));
  MOCK_METHOD(void,
              MovePinnedToolbarAction,
              (toolbar_ui_api::mojom::PinnedToolbarAction, int32_t),
              (override));
  MOCK_METHOD(void,
              MovePinnedToolbarActionBy,
              (toolbar_ui_api::mojom::PinnedToolbarAction, int32_t),
              (override));
  MOCK_METHOD(void,
              MoveExtensionAction,
              (const std::string& extension_id, int32_t target_index),
              (override));
  MOCK_METHOD(void,
              MoveExtensionActionBy,
              (const std::string& extension_id, int32_t delta),
              (override));

  MOCK_METHOD(void,
              OnLhsChipMousePressed,
              (toolbar_ui_api::mojom::LhsChipIdentifier, bool),
              (override));
  MOCK_METHOD(void,
              OnLhsChipClicked,
              (toolbar_ui_api::mojom::LhsChipIdentifier, bool),
              (override));
  MOCK_METHOD(void,
              OnLhsChipPointerEntered,
              (toolbar_ui_api::mojom::LhsChipIdentifier),
              (override));
  MOCK_METHOD(void,
              OnLhsChipPointerExited,
              (toolbar_ui_api::mojom::LhsChipIdentifier),
              (override));
  MOCK_METHOD(void,
              OnLhsChipExpandAnimationEnded,
              (toolbar_ui_api::mojom::LhsChipIdentifier),
              (override));
  MOCK_METHOD(void,
              OnLhsChipCollapseAnimationEnded,
              (toolbar_ui_api::mojom::LhsChipIdentifier),
              (override));
  MOCK_METHOD(void,
              OnLhsChipDrag,
              (toolbar_ui_api::mojom::LhsChipIdentifier,
               ui::mojom::DragEventSource),
              (override));
  MOCK_METHOD(void, OnHomeButtonDropUrl, (const GURL&), (override));
  MOCK_METHOD(void, OnHomeButtonDropFile, (const gfx::PointF&), (override));
  MOCK_METHOD(void, OnToolbarDropFile, (const gfx::PointF&), (override));
  MOCK_METHOD(void,
              OnPerformanceInterventionButtonClicked,
              (bool is_mouse_interaction),
              (override));
  MOCK_METHOD(void,
              OnPerformanceInterventionButtonMousePressed,
              (),
              (override));
  MOCK_METHOD((base::expected<std::monostate, mojo_base::mojom::ErrorPtr>),
              OnOmniboxAction,
              (toolbar_ui_api::mojom::OmniboxActionPtr action_ptr),
              (override));
  MOCK_METHOD(void, ShowAvatarMenu, ());
  MOCK_METHOD(void, SetAvatarButtonHovered, (bool));
  MOCK_METHOD(void, SetAvatarButtonFocused, (bool));
  MOCK_METHOD(void, SetAvatarButtonIPHPromoShowing, (bool));
  MOCK_METHOD(void, OnAppMenuFocusChanged, (bool), (override));
  MOCK_METHOD(void,
              ExecuteExtensionAction,
              (const std::string& extension_id),
              (override));
  MOCK_METHOD(void,
              ShowExtensionContextMenu,
              (const std::string& extension_id,
               ui::mojom::MenuSourceType source),
              (override));
  MOCK_METHOD(
      (base::expected<toolbar_ui_api::mojom::AdjustOmniboxTextForCopyResultPtr,
                      mojo_base::mojom::ErrorPtr>),
      AdjustOmniboxTextForCopy,
      (const std::u16string&, int32_t),
      (override));
};

// Test fixture for WebUIToolbarUI. These tests test the connectivity between
// the components in the web ui toolbar component. It is not intended to deeply
// exercise each component. For example, it might test that data flows
// correctly from the browser controls service to some observer, but it will
// not validate the behaviours.
class WebUIToolbarUIBrowserTest : public InProcessBrowserTest,
                                  public WebUIToolbarUI::DependencyProvider {
 public:
  WebUIToolbarUIBrowserTest() {
    feature_list_.InitWithFeatures(
        {features::kInitialWebUI, features::kWebUIReloadButton,
         features::kWebUIInProcessResourceLoadingV2,
         features::kWebUIPinnedToolbarActions, features::kDebugTopChromeWebUI},
        {});
  }
  ~WebUIToolbarUIBrowserTest() override = default;

  // Not movable or copyable.
  WebUIToolbarUIBrowserTest(const WebUIToolbarUIBrowserTest&) = delete;
  WebUIToolbarUIBrowserTest& operator=(const WebUIToolbarUIBrowserTest&) =
      delete;

  void SetUpOnMainThread() override {
    InProcessBrowserTest::SetUpOnMainThread();

    ASSERT_TRUE(
        content::NavigateToURL(chrome_test_utils::GetActiveWebContents(this),
                               GURL("chrome://webui-toolbar.top-chrome/")));

    web_ui_ = std::make_unique<content::TestWebUI>();
    web_ui_->set_web_contents(chrome_test_utils::GetActiveWebContents(this));
    WebUIToolbarUIDependencyProviderUserData::CreateForWebContents(
        web_ui_->GetWebContents(), this);
    ui_ = std::make_unique<WebUIToolbarUI>(web_ui_.get());
    ui_->Init(this);
  }

  void TearDownOnMainThread() override {
    ui_.reset();
    web_ui_.reset();

    InProcessBrowserTest::TearDownOnMainThread();
  }

  // WebUIToolbarUI::DependencyProvider:
  base::WeakPtr<DependencyProvider> GetWeakPtr() override {
    return weak_factory_.GetWeakPtr();
  }
  browser_controls_api::BrowserControlsService::BrowserControlsServiceDelegate*
  GetBrowserControlsDelegate() override {
    return &browser_controls_delegate_;
  }
  toolbar_ui_api::ToolbarUIService::ToolbarUIServiceDelegate*
  GetToolbarUIServiceDelegate() override {
    return &toolbar_ui_delegate_;
  }
  std::unique_ptr<toolbar_ui_api::NavigationControlsStateFetcher>
  GetNavigationControlsStateFetcher() override {
    return std::make_unique<toolbar_ui_api::NavigationControlsStateFetcherImpl>(
        base::BindRepeating(
            [&] { return CreateValidNavigationControlsState(); }));
  }
  std::unique_ptr<toolbar_ui_api::IconTableFetcher> GetIconTableFetcher()
      override {
    return std::make_unique<FakeIconTableFetcher>();
  }

  CommandUpdater* GetCommandUpdater() override {
    return reinterpret_cast<CommandUpdater*>(
        webui::GetBrowserWindowInterface(web_ui()->GetWebContents())
            ->GetFeatures()
            .browser_command_controller());
  }

  OmniboxController* GetOmniboxController() override { return nullptr; }

  content::TestWebUI* web_ui() { return web_ui_.get(); }
  WebUIToolbarUI* ui() { return ui_.get(); }
  MockToolbarUIDelegate& toolbar_ui_delegate() { return toolbar_ui_delegate_; }

 private:
  base::test::ScopedFeatureList feature_list_;
  BrowserControlsDelegate browser_controls_delegate_;
  testing::NiceMock<MockToolbarUIDelegate> toolbar_ui_delegate_;
  std::unique_ptr<content::TestWebUI> web_ui_;
  std::unique_ptr<WebUIToolbarUI> ui_;
  base::WeakPtrFactory<DependencyProvider> weak_factory_{this};
};

// Tests that calling InvokePinnedToolbarAction from Mojo calls the delegate.
IN_PROC_BROWSER_TEST_F(WebUIToolbarUIBrowserTest, InvokePinnedToolbarAction) {
  mojo::Remote<toolbar_ui_api::mojom::ToolbarUIService> service_remote;
  ui()->BindInterface(service_remote.BindNewPipeAndPassReceiver());

  EXPECT_CALL(toolbar_ui_delegate(),
              InvokePinnedToolbarAction(
                  toolbar_ui_api::mojom::PinnedToolbarAction::kPrint))
      .Times(1);

  service_remote->InvokePinnedToolbarAction(
      toolbar_ui_api::mojom::PinnedToolbarAction::kPrint);
  service_remote.FlushForTesting();
}

IN_PROC_BROWSER_TEST_F(WebUIToolbarUIBrowserTest, MovePinnedToolbarAction) {
  mojo::Remote<toolbar_ui_api::mojom::ToolbarUIService> service_remote;
  ui()->BindInterface(service_remote.BindNewPipeAndPassReceiver());

  EXPECT_CALL(toolbar_ui_delegate(),
              MovePinnedToolbarAction(
                  toolbar_ui_api::mojom::PinnedToolbarAction::kPrint, 0))
      .Times(1);

  service_remote->MovePinnedToolbarAction(
      toolbar_ui_api::mojom::PinnedToolbarAction::kPrint, 0);
  service_remote.FlushForTesting();
}

IN_PROC_BROWSER_TEST_F(WebUIToolbarUIBrowserTest, MovePinnedToolbarActionBy) {
  mojo::Remote<toolbar_ui_api::mojom::ToolbarUIService> service_remote;
  ui()->BindInterface(service_remote.BindNewPipeAndPassReceiver());

  EXPECT_CALL(toolbar_ui_delegate(),
              MovePinnedToolbarActionBy(
                  toolbar_ui_api::mojom::PinnedToolbarAction::kPrint, -1))
      .Times(1);

  service_remote->MovePinnedToolbarActionBy(
      toolbar_ui_api::mojom::PinnedToolbarAction::kPrint, -1);
  service_remote.FlushForTesting();
}

// Tests that OnNavigationControlsStateChanged calls the browser controls
// observer with the correct parameters.
IN_PROC_BROWSER_TEST_F(WebUIToolbarUIBrowserTest, SetReloadButtonState) {
  ToolbarUIServiceConnectionManager connection(ui());

  auto state = CreateValidNavigationControlsState();
  state->reload_control_state->is_navigation_loading = true;
  connection.RegisterObserver();

  EXPECT_CALL(
      connection.mock_observer(),
      OnNavigationControlsStateChanged(
          testing::_, testing::Pointee(testing::Field(
                          &toolbar_ui_api::mojom::NavigationControlsState::
                              reload_control_state,
                          testing::Pointee(testing::Field(
                              &toolbar_ui_api::mojom::ReloadControlState::
                                  is_navigation_loading,
                              true))))))
      .Times(1);
  ui()->OnNavigationControlsStateChanged(*state);
  connection.mock_observer().FlushForTesting();
}

// Tests that the BindInterface method for BrowserControlsService works
// correctly.
IN_PROC_BROWSER_TEST_F(WebUIToolbarUIBrowserTest, BindBrowserControlsService) {
  BrowserControlsServiceConnectionManager connection(ui());

  EXPECT_TRUE(connection.is_bound());
  EXPECT_TRUE(connection.is_connected());
}

// Tests that the BindInterface method for ToolbarUIService works
// correctly.
IN_PROC_BROWSER_TEST_F(WebUIToolbarUIBrowserTest, BindToolbarUIService) {
  ToolbarUIServiceConnectionManager connection(ui());

  EXPECT_TRUE(connection.is_bound());
  EXPECT_TRUE(connection.is_connected());
}

// Tests that connecting to the service instantiates the BrowserControlsService.
IN_PROC_BROWSER_TEST_F(WebUIToolbarUIBrowserTest,
                       CreateBrowserControlsService) {
  BrowserControlsServiceConnectionManager connection(ui());

  EXPECT_TRUE(connection.is_connected());
}

// Tests that connecting to the service instantiates the ToolbarUIService.
IN_PROC_BROWSER_TEST_F(WebUIToolbarUIBrowserTest, CreateToolbarUIService) {
  ToolbarUIServiceConnectionManager connection(ui());

  EXPECT_TRUE(connection.is_connected());
}

// Tests that out of order Init,Bind will not cause issues. This can happen
// because navigation owns the WebUI toolbar before it hands it over to views.
// A corner case could occur if the browser starts shutting down, at which
// point the WebUI controller enters a bad state while JS is starting up. When
// JS attempts to connect to WebUI toolbar services, this will cause a crash.
//
// Since this is difficult to synthesize in test, we simply test an out of
// order init. That is, we attempt to establish a connection to the controller
// before the controller has been initialized.
IN_PROC_BROWSER_TEST_F(WebUIToolbarUIBrowserTest, CreateService_DelayedInit) {
  auto web_ui = std::make_unique<content::TestWebUI>();
  web_ui->set_web_contents(chrome_test_utils::GetActiveWebContents(this));
  auto ui = std::make_unique<WebUIToolbarUI>(web_ui.get());

  BrowserControlsServiceConnectionManager connection(ui.get());

  // When the controller is not connected, one end of the pipe is left open.
  // Mojo has a bug where this is considered connected on the client side.
  EXPECT_TRUE(connection.is_connected());

  // Bonus check, we can actually support delayed Init. Note that our WebUI
  // does not require this. Once we initialize, the connection should be
  // complete the connection.
  ui->Init(this);

  // This only checks that the connection remains connected. A more meaningful
  // test would ensure that the service actually responds to requests.
  EXPECT_TRUE(connection.is_connected());
}

// Tests that PopulateLocalResourceLoaderConfig provides the theme source.
IN_PROC_BROWSER_TEST_F(WebUIToolbarUIBrowserTest,
                       PopulateLocalResourceLoaderConfig) {
  // Create a mock ColorProvider.
  ui::ColorProvider color_provider;

  // Set the ColorProvider for testing via ThemeColorsSourceManager.
  auto* profile = chrome_test_utils::GetProfile(this);
  auto* theme_colors_manager =
      ThemeColorsSourceManagerFactory::GetForProfile(profile);
  ASSERT_THAT(theme_colors_manager, testing::NotNull());
  theme_colors_manager->SetColorProviderForTesting(&color_provider);

  blink::mojom::LocalResourceLoaderConfig config;
  ui()->PopulateLocalResourceLoaderConfig(
      &config, url::Origin::Create(GURL("chrome://webui-toolbar.top-chrome/")));

  // Verify that the color CSS is added.
  url::Origin theme_origin = url::Origin::Create(GURL("chrome://theme/"));
  auto source_it = config.sources.find(theme_origin);
  ASSERT_TRUE(source_it != config.sources.end());

  auto resource_it =
      source_it->second->path_to_resource_map.find("colors.css?sets=ui,chrome");
  ASSERT_TRUE(resource_it != source_it->second->path_to_resource_map.end());
  EXPECT_TRUE(resource_it->second->is_response_body());
}

// Verifies that `WebUIRenderFrameCreated()` populates all required JSON keys
// into `SetWebUIProperty("initialState", ...)`, including
// `initialWebUISurfaceSyncEnabled`.
IN_PROC_BROWSER_TEST_F(WebUIToolbarUIBrowserTest,
                       PopulateInitialState_ContainsAllRequiredKeys) {
  content::RenderFrameHost* rfh =
      web_ui()->GetWebContents()->GetPrimaryMainFrame();
  ASSERT_TRUE(rfh);

  ui()->WebUIRenderFrameCreated(rfh);

  std::string initial_state_json =
      content::EvalJs(rfh, "chrome.getVariableValue('initialState')")
          .ExtractString();
  EXPECT_FALSE(initial_state_json.empty());

  std::optional<base::DictValue> dict = base::JSONReader::ReadDict(
      initial_state_json, base::JSON_PARSE_CHROMIUM_EXTENSIONS);
  ASSERT_TRUE(dict.has_value());

  EXPECT_TRUE(dict->contains("isNavigationLoading"));
  EXPECT_TRUE(dict->contains("reloadCanShowMenu"));
  EXPECT_TRUE(dict->contains("backButtonEnabled"));
  EXPECT_TRUE(dict->contains("forwardButtonEnabled"));
  EXPECT_TRUE(dict->contains("homeButtonShouldBeShown"));
  EXPECT_TRUE(dict->contains("batterySaverButtonVisible"));
  EXPECT_TRUE(dict->contains("layoutConstantsVersion"));
  EXPECT_TRUE(dict->contains("touchUi"));
  EXPECT_TRUE(dict->contains("isFallbackPrewarming"));

  EXPECT_TRUE(dict->contains("initialWebUISurfaceSyncEnabled"));
}

}  // namespace
