// Copyright 2020 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/passwords/password_save_update_view.h"

#include <memory>
#include <utility>

#include "base/functional/callback_helpers.h"
#include "base/i18n/rtl.h"
#include "base/notreached.h"
#include "base/scoped_observation.h"
#include "base/strings/string_util.h"
#include "chrome/browser/password_manager/factories/password_store_utils.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/signin/identity_manager_factory.h"
#include "chrome/browser/signin/signin_promo_util.h"
#include "chrome/browser/signin/signin_ui_util.h"
#include "chrome/browser/ui/hats/hats_service.h"
#include "chrome/browser/ui/hats/hats_service_factory.h"
#include "chrome/browser/ui/passwords/password_dialog_prompts.h"
#include "chrome/browser/ui/passwords/ui_utils.h"
#include "chrome/browser/ui/signin/promos/bubble_signin_promo_view.h"
#include "chrome/browser/ui/ui_features.h"
#include "chrome/browser/ui/user_education/browser_user_education_interface.h"
#include "chrome/browser/ui/views/chrome_layout_provider.h"
#include "chrome/browser/ui/views/passwords/credentials_item_view.h"
#include "chrome/browser/ui/views/passwords/views_utils.h"
#include "chrome/grit/branded_strings.h"
#include "chrome/grit/browser_resources.h"
#include "chrome/grit/generated_resources.h"
#include "components/feature_engagement/public/feature_constants.h"
#include "components/password_manager/core/browser/features/password_features.h"
#include "components/strings/grit/components_strings.h"
#include "content/public/browser/web_contents.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/base/metadata/metadata_impl_macros.h"
#include "ui/base/mojom/dialog_button.mojom.h"
#include "ui/events/keycodes/keyboard_codes.h"
#include "ui/views/accessibility/view_accessibility.h"
#include "ui/views/controls/button/button.h"
#include "ui/views/controls/editable_combobox/editable_combobox.h"
#include "ui/views/controls/editable_combobox/editable_password_combobox.h"
#include "ui/views/controls/menu/menu_runner.h"
#include "ui/views/controls/menu/menu_types.h"
#include "ui/views/controls/styled_label.h"
#include "ui/views/layout/fill_layout.h"
#include "ui/views/layout/flex_layout.h"
#include "ui/views/vector_icons.h"
#include "ui/views/view_class_properties.h"
#include "ui/views/view_utils.h"

namespace {

// This Split Button is a composite UI control consisting of two clickable
// zones:
// 1. A primary action button (here, the "Not now" button) triggering the
// default action.
// 2. A smaller secondary arrow/caret button that opens an associated dropdown
//    menu exposing alternative contextual options (e.g., "Never for this
//    site").
//
// This class implements the Split Button for cancellation operations when the
// dropdown menu experiment is enabled.
class CancelSplitButton : public views::View,
                          public ui::SimpleMenuModel::Delegate,
                          public views::FocusChangeListener {
  METADATA_HEADER(CancelSplitButton, views::View)

 public:
  enum class CommandId {
    kNeverForThisSite = 1,
  };

  ui::SimpleMenuModel* menu_model() const { return menu_model_.get(); }

  CancelSplitButton(base::RepeatingClosure no_thanks_callback,
                    base::RepeatingClosure never_callback)
      : no_thanks_callback_(std::move(no_thanks_callback)),
        never_callback_(std::move(never_callback)) {
    // create layout manager
    auto* layout = SetLayoutManager(std::make_unique<views::BoxLayout>(
        views::BoxLayout::Orientation::kHorizontal, gfx::Insets(), 1));
    layout->set_cross_axis_alignment(
        views::BoxLayout::CrossAxisAlignment::kStretch);
    // create "no thanks" button
    not_now_button_ = AddChildView(std::make_unique<views::MdTextButton>(
        base::BindRepeating(&CancelSplitButton::OnNoThanksClicked,
                            base::Unretained(this)),
        l10n_util::GetStringUTF16(IDS_NOT_NOW)));
    not_now_button_->SetStyle(ui::ButtonStyle::kTonal);
    not_now_button_->SetID(PasswordSaveUpdateView::kNotNowButton);
    not_now_button_->SetProperty(
        views::kElementIdentifierKey,
        PasswordSaveUpdateView::kNotNowButtonElementId);
    // create caret button that opens menu with "never" option
    caret_button_ = AddChildView(std::make_unique<views::MdTextButton>(
        base::BindRepeating(&CancelSplitButton::OnCaretClicked,
                            base::Unretained(this)),
        std::u16string()));
    caret_button_->SetID(PasswordSaveUpdateView::kCaretButton);
    caret_button_->SetProperty(views::kElementIdentifierKey,
                               PasswordSaveUpdateView::kCaretButtonElementId);
    caret_button_->GetViewAccessibility().SetName(
        l10n_util::GetStringUTF16(IDS_TAB_GROUP_MORE_OPTIONS));
    caret_button_->SetImageModel(views::Button::STATE_NORMAL,
                                 GetCaretImageModel(/*pointing_up=*/false));
    caret_button_->SetImageModel(views::Button::STATE_PRESSED,
                                 GetCaretImageModel(/*pointing_up=*/true));
    caret_button_->SetStyle(ui::ButtonStyle::kTonal);
    caret_button_->SetMinSize(gfx::Size(0, 0));
    caret_button_->SetBorder(views::CreateEmptyBorder(kCaretButtonBorders));

    not_now_button_->SetCornerRadii(GetNotNowButtonRadii());
    caret_button_->SetCornerRadii(GetCaretButtonRadii());

    menu_model_ = std::make_unique<ui::SimpleMenuModel>(this);
    menu_model_->AddItemWithStringId(
        static_cast<int>(CommandId::kNeverForThisSite),
        IDS_PASSWORD_MANAGER_TOOLTIP_BLOCKED);
    menu_model_->SetElementIdentifierAt(
        0, PasswordSaveUpdateView::kNeverMenuItemElementId);
  }

  void ExecuteCommand(int command_id, int event_flags) override {
    if (command_id == static_cast<int>(CommandId::kNeverForThisSite)) {
      if (never_callback_) {
        never_callback_.Run();
      }
    } else {
      NOTREACHED();
    }
  }

 private:
  static constexpr int kCaretIconSize = 26;
  static constexpr auto kCaretButtonBorders{gfx::Insets::TLBR(0, 4, 0, 8)};

  static ui::ImageModel GetCaretImageModel(bool pointing_up) {
    return ui::ImageModel::FromVectorIcon(
        pointing_up ? views::kArrowDropUpIcon : views::kArrowDropDownIcon,
        ui::kColorIcon, kCaretIconSize);
  }

  void OnNoThanksClicked() {
    if (no_thanks_callback_) {
      no_thanks_callback_.Run();
    }
  }

  void OnCaretClicked() {
    if (menu_runner_ && menu_runner_->IsRunning()) {
      menu_runner_->Cancel();
      return;
    }

    caret_button_->SetImageModel(views::Button::STATE_NORMAL,
                                 GetCaretImageModel(/*pointing_up=*/true));
    caret_button_->SetBorder(views::CreateEmptyBorder(kCaretButtonBorders));

    if (auto* focus_manager = GetWidget()->GetFocusManager()) {
      focus_observation_.Observe(focus_manager);
    }

    menu_runner_ = std::make_unique<views::MenuRunner>(
        menu_model_.get(), views::MenuRunner::HAS_MNEMONICS,
        base::BindRepeating(&CancelSplitButton::OnMenuClosed,
                            base::Unretained(this)));

    gfx::Rect anchor_bounds = caret_button_->GetBoundsInScreen();
    menu_runner_->RunMenuAt(caret_button_->GetWidget(), nullptr, anchor_bounds,
                            views::MenuAnchorPosition::kTopRight,
                            ui::mojom::MenuSourceType::kNone);
  }

  void OnMenuClosed() {
    caret_button_->SetImageModel(views::Button::STATE_NORMAL,
                                 GetCaretImageModel(/*pointing_up=*/false));
    caret_button_->SetBorder(views::CreateEmptyBorder(kCaretButtonBorders));

    focus_observation_.Reset();
  }

  void OnWillChangeFocus(views::View* focused_before,
                         views::View* focused_now) override {
    if (menu_runner_ && menu_runner_->IsRunning()) {
      menu_runner_->Cancel();
    }
  }

  int GetOuterRadius() const {
    return views::LayoutProvider::Get()->GetCornerRadiusMetric(
        views::ShapeContextTokens::kButtonRadius,
        not_now_button_->GetPreferredSize());
  }

  gfx::RoundedCornersF GetNotNowButtonRadii() const {
    int outer = GetOuterRadius();
    return base::i18n::IsRTL() ? gfx::RoundedCornersF(0, outer, outer, 0)
                               : gfx::RoundedCornersF(outer, 0, 0, outer);
  }

  gfx::RoundedCornersF GetCaretButtonRadii() const {
    int outer = GetOuterRadius();
    return base::i18n::IsRTL() ? gfx::RoundedCornersF(outer, 0, 0, outer)
                               : gfx::RoundedCornersF(0, outer, outer, 0);
  }

  base::RepeatingClosure no_thanks_callback_;
  base::RepeatingClosure never_callback_;

  raw_ptr<views::MdTextButton> not_now_button_ = nullptr;
  raw_ptr<views::MdTextButton> caret_button_ = nullptr;

  base::ScopedObservation<views::FocusManager, views::FocusChangeListener>
      focus_observation_{this};
  std::unique_ptr<ui::SimpleMenuModel> menu_model_;
  std::unique_ptr<views::MenuRunner> menu_runner_;
};

BEGIN_METADATA(CancelSplitButton)
END_METADATA

}  // namespace

// A custom horizontal button row used when the save password bubble dropdown
// experiment is enabled. This row contains:
// - An OK button (e.g., "Save" or "Update").
// - A standard Cancel button (used when the dialog is in an "Update" state).
// - A `CancelSplitButton` (used when the dialog is in a "Save" state, offering
//   both a primary "Not now" option and a dropdown menu with "Never for this
//   site").
class PasswordSaveUpdateExperimentButtonRow : public views::BoxLayoutView {
  METADATA_HEADER(PasswordSaveUpdateExperimentButtonRow, views::BoxLayoutView)

 public:
  PasswordSaveUpdateExperimentButtonRow(base::RepeatingClosure accept_callback,
                                        base::RepeatingClosure cancel_callback,
                                        base::RepeatingClosure never_callback) {
    SetOrientation(views::BoxLayout::Orientation::kHorizontal);
    SetMainAxisAlignment(views::BoxLayout::MainAxisAlignment::kEnd);
    SetBetweenChildSpacing(ChromeLayoutProvider::Get()->GetDistanceMetric(
        views::DISTANCE_RELATED_BUTTON_HORIZONTAL));

    cancel_button_ = AddChildView(std::make_unique<views::MdTextButton>(
        cancel_callback,
        l10n_util::GetStringUTF16(IDS_PASSWORD_MANAGER_CANCEL_BUTTON)));
    cancel_button_->SetStyle(ui::ButtonStyle::kTonal);
    cancel_button_->SetID(PasswordSaveUpdateView::kDismissUpdateButton);
    cancel_button_->SetVisible(false);

    split_button_ = AddChildView(
        std::make_unique<CancelSplitButton>(cancel_callback, never_callback));
    split_button_->SetID(PasswordSaveUpdateView::kSplitButton);
    split_button_->SetVisible(false);

    ok_button_ = AddChildView(std::make_unique<views::MdTextButton>(
        accept_callback, std::u16string()));
    ok_button_->SetStyle(ui::ButtonStyle::kProminent);
    ok_button_->SetID(PasswordSaveUpdateView::kOkButton);
  }

  void UpdateState(bool is_update,
                   const std::u16string& ok_button_text,
                   bool ok_button_enabled) {
    ok_button_->SetText(ok_button_text);
    ok_button_->SetEnabled(ok_button_enabled);

    cancel_button_->SetVisible(is_update);
    split_button_->SetVisible(!is_update);
  }

  views::MdTextButton* ok_button() const { return ok_button_; }
  views::MdTextButton* cancel_button() const { return cancel_button_; }
  views::View* split_button() const { return split_button_; }

 private:
  raw_ptr<views::MdTextButton> ok_button_ = nullptr;
  raw_ptr<views::MdTextButton> cancel_button_ = nullptr;
  raw_ptr<CancelSplitButton> split_button_ = nullptr;
};

BEGIN_METADATA(PasswordSaveUpdateExperimentButtonRow)
END_METADATA

PasswordSaveUpdateView::PasswordSaveUpdateView(
    content::WebContents* web_contents,
    views::BubbleAnchor anchor_view,
    DisplayReason reason)
    : PasswordBubbleViewBase(web_contents,
                             anchor_view,
                             /*easily_dismissable=*/reason == USER_GESTURE),
      controller_(
          PasswordsModelDelegateFromWebContents(web_contents),
          reason == AUTOMATIC
              ? PasswordBubbleControllerBase::DisplayReason::kAutomatic
              : PasswordBubbleControllerBase::DisplayReason::kUserAction),
      is_update_bubble_(controller_.state() ==
                        password_manager::ui::PENDING_PASSWORD_UPDATE_STATE) {
  DCHECK(controller_.state() == password_manager::ui::PENDING_PASSWORD_STATE ||
         controller_.state() ==
             password_manager::ui::PENDING_PASSWORD_UPDATE_STATE);

  const password_manager::PasswordForm& password_form =
      controller_.pending_password();
  views::View* root_view = nullptr;
  if (password_form.IsFederatedCredential()) {
    root_view = this;
    // The credential to be saved doesn't contain password but just the identity
    // provider (e.g. "Sign in with Google"). Thus, the layout is different.
    views::FlexLayout* flex_layout =
        SetLayoutManager(std::make_unique<views::FlexLayout>());
    flex_layout->SetOrientation(views::LayoutOrientation::kVertical)
        .SetCrossAxisAlignment(views::LayoutAlignment::kStretch)
        .SetIgnoreDefaultMainAxisMargins(true)
        .SetCollapseMargins(true)
        .SetDefault(
            views::kMarginsKey,
            gfx::Insets::VH(ChromeLayoutProvider::Get()->GetDistanceMetric(
                                views::DISTANCE_CONTROL_LIST_VERTICAL),
                            0));

    const auto titles = GetCredentialLabelsForAccountChooser(password_form);
    AddChildView(
        std::make_unique<CredentialsItemView>(
            views::Button::PressedCallback(), titles.first, titles.second,
            &password_form, GetURLLoaderForMainFrame(web_contents).get(),
            web_contents->GetPrimaryMainFrame()->GetLastCommittedOrigin()))
        ->SetEnabled(false);
  } else {
    std::unique_ptr<views::EditableCombobox> username_dropdown =
        CreateUsernameEditableCombobox(password_form);
    username_dropdown->SetCallback(base::BindRepeating(
        &PasswordSaveUpdateView::OnContentChanged, base::Unretained(this)));
    std::unique_ptr<views::EditablePasswordCombobox> password_dropdown =
        CreateEditablePasswordCombobox(
            password_form,
            base::BindRepeating(&PasswordSaveUpdateView::TogglePasswordRevealed,
                                base::Unretained(this)));
    password_dropdown->SetCallback(base::BindRepeating(
        &PasswordSaveUpdateView::OnContentChanged, base::Unretained(this)));
    // Set up layout:
    SetLayoutManager(std::make_unique<views::FillLayout>());
    root_view = AddChildView(std::make_unique<views::View>());
    views::AnimatingLayoutManager* animating_layout =
        root_view->SetLayoutManager(
            std::make_unique<views::AnimatingLayoutManager>());
    animating_layout
        ->SetBoundsAnimationMode(views::AnimatingLayoutManager::
                                     BoundsAnimationMode::kAnimateMainAxis)
        .SetOrientation(views::LayoutOrientation::kVertical);
    views::FlexLayout* flex_layout = animating_layout->SetTargetLayoutManager(
        std::make_unique<views::FlexLayout>());
    flex_layout->SetOrientation(views::LayoutOrientation::kVertical)
        .SetCrossAxisAlignment(views::LayoutAlignment::kStretch)
        .SetIgnoreDefaultMainAxisMargins(true)
        .SetCollapseMargins(true)
        .SetDefault(
            views::kMarginsKey,
            gfx::Insets::VH(ChromeLayoutProvider::Get()->GetDistanceMetric(
                                views::DISTANCE_CONTROL_LIST_VERTICAL),
                            0));

    username_dropdown_ = username_dropdown.get();
    password_dropdown_ = password_dropdown.get();
    BuildCredentialRows(root_view, std::move(username_dropdown),
                        std::move(password_dropdown));

    // Only non-federated credentials bubble has a username field and can
    // change states between Save and Update. Therefore, we need to have the
    // `accessibility_alert_` to inform screen readers about that change.
    accessibility_alert_ =
        root_view->AddChildView(std::make_unique<views::View>());
    AddChildViewRaw(accessibility_alert_.get());
  }

  {
    using Controller = SaveUpdateBubbleController;
    using ControllerNotifyFn = void (Controller::*)();
    auto button_clicked = [](PasswordSaveUpdateView* dialog,
                             ControllerNotifyFn func) {
      dialog->UpdateUsernameAndPasswordInModel();
      (dialog->controller_.*func)();
    };

    if (IsTrustedVaultErrorResolutionEnabled() &&
        controller_.IsSavingBlockedByTrustedVaultError()) {
      SetAcceptCallbackWithClose(
          base::BindRepeating(button_clicked, base::Unretained(this),
                              &Controller::OnTrustedVaultUnlockClicked)
              .Then(base::BindRepeating([]() {
                // Closing the bubble after opening a trusted vault unlock page:
                return true;
              })));
    } else {
      SetAcceptCallbackWithClose(
          base::BindRepeating(button_clicked, base::Unretained(this),
                              &Controller::OnSaveClicked)
              .Then(base::BindRepeating(
                  &PasswordSaveUpdateView::CloseOrReplaceWithPromo,
                  base::Unretained(this))));
    }

    if (is_update_bubble_) {
      SetCancelCallback(base::BindOnce(button_clicked, base::Unretained(this),
                                       &Controller::OnNoThanksClicked));
    } else if (IsSaveBubbleDropdownExperimentEnabled()) {
      if (controller_.IsMaxDismissalCountReached()) {
        SetCancelCallback(
            base::BindOnce(button_clicked, base::Unretained(this),
                           &Controller::OnNeverForThisSiteClicked));
      } else {
        SetButtons(static_cast<int>(ui::mojom::DialogButton::kNone));

        auto accept_callback = base::BindRepeating(
            [](PasswordSaveUpdateView* dialog) { dialog->AcceptDialog(); },
            base::Unretained(this));

        auto cancel_callback = base::BindRepeating(
            [](PasswordSaveUpdateView* dialog) { dialog->CancelDialog(); },
            base::Unretained(this));

        auto never_callback = base::BindRepeating(
            [](PasswordSaveUpdateView* dialog) {
              dialog->UpdateUsernameAndPasswordInModel();
              dialog->controller_.OnNeverForThisSiteClicked();
              dialog->GetWidget()->Close();
            },
            base::Unretained(this));

        auto button_row =
            std::make_unique<PasswordSaveUpdateExperimentButtonRow>(
                accept_callback, cancel_callback, never_callback);
        button_row->SetID(PasswordSaveUpdateView::kCustomButtonRow);
        custom_button_row_ = root_view->AddChildView(std::move(button_row));

        SetCancelCallback(base::BindOnce(button_clicked, base::Unretained(this),
                                         &Controller::OnNotNowClicked));
      }
    } else if (base::FeatureList::IsEnabled(
                   features::kThreeButtonPasswordSaveDialog)) {
      SetCancelCallback(base::BindOnce(button_clicked, base::Unretained(this),
                                       &Controller::OnNotNowClicked));

      // Use "Medium" dialog width, per UX preference for wider dialogs.
      set_fixed_width(views::LayoutProvider::Get()->GetDistanceMetric(
          views::DISTANCE_MODAL_DIALOG_PREFERRED_WIDTH));
      // 3-button save dialog variant.
      extra_view_ = SetExtraView(std::make_unique<views::MdTextButton>());
      extra_view_->SetProperty(views::kElementIdentifierKey,
                               kExtraButtonElementId);
      extra_view_->SetCallback(
          base::BindOnce(button_clicked, base::Unretained(this),
                         &Controller::OnNeverForThisSiteClicked));
      extra_view_->SetStyle(
          GetDialogButtonStyle(ui::mojom::DialogButton::kCancel));
    } else {
      // 2-button save dialog variant.
      SetCancelCallback(base::BindOnce(button_clicked, base::Unretained(this),
                                       &Controller::OnNeverForThisSiteClicked));
    }
  }

  SetShowIcon(true);
  SetFootnoteView(CreateFooterView());

  AddAccelerator(ui::Accelerator(ui::VKEY_RETURN, ui::EF_NONE));

  UpdateBubbleUIElements();

  Profile* profile =
      Profile::FromBrowserContext(web_contents->GetBrowserContext());
  HatsService* hats_service =
      HatsServiceFactory::GetForProfile(profile, /*create_if_necessary=*/true);
  CHECK(hats_service);
  hats_service->LaunchDelayedSurveyForWebContents(
      kHatsSurveyTriggerAutofillPassword, web_contents, 10000);
}

PasswordSaveUpdateView::~PasswordSaveUpdateView() = default;

bool PasswordSaveUpdateView::AcceleratorPressed(
    const ui::Accelerator& accelerator) {
  if (accelerator.key_code() == ui::VKEY_RETURN) {
    if (IsDialogButtonEnabled(ui::mojom::DialogButton::kOk)) {
      AcceptDialog();
      return true;
    }
  }
  return views::BubbleDialogDelegateView::AcceleratorPressed(accelerator);
}

bool PasswordSaveUpdateView::IsSaveBubbleDropdownExperimentEnabled() const {
  return !is_update_bubble_ &&
         base::FeatureList::IsEnabled(
             features::kPasswordSaveUpdateDropdownMenuExperiment);
}

bool PasswordSaveUpdateView::IsTrustedVaultErrorResolutionEnabled() const {
  return base::FeatureList::IsEnabled(
      password_manager::features::kPasswordSaveInContextErrorResolution);
}

PasswordBubbleControllerBase* PasswordSaveUpdateView::GetController() {
  return &controller_;
}

const PasswordBubbleControllerBase* PasswordSaveUpdateView::GetController()
    const {
  return &controller_;
}

bool PasswordSaveUpdateView::CloseOrReplaceWithPromo() {
  // hide extra button if it exists when closing or replacing dialog with promo
  if (custom_button_row_) {
    custom_button_row_->SetVisible(false);
    custom_button_row_ = nullptr;
  }

#if BUILDFLAG(ENABLE_DICE_SUPPORT)
  // Close the bubble if the sign in promo should not be shown.
  if (!signin::ShouldShowPasswordSignInPromo(*controller_.GetProfile())) {
    return true;
  }

  // Remove current elements.
  reveal_password_pin_ = nullptr;
  username_dropdown_ = nullptr;
  password_dropdown_ = nullptr;
  accessibility_alert_ = nullptr;
  custom_button_row_ = nullptr;
  RemoveAllChildViews();
  SetShowIcon(false);
  SetButtons(static_cast<int>(ui::mojom::DialogButton::kNone));
  GetBubbleFrameView()->SetFootnoteView(nullptr);
  // SetExtraView is not designed to be called multiple times, so hide the
  // extra button if it exists. Note that we're intentionally keeping the width
  // of the previous dialog, even if it's the wider 3-button width.
  if (extra_view_) {
    extra_view_->SetVisible(false);
  }

  SetLayoutManager(std::make_unique<views::FillLayout>());
  set_margins(BubbleSignInPromoView::GetBubbleSigninPromoMargins());

  SetTitle(IDS_AUTOFILL_SIGNIN_PROMO_TITLE_PASSWORD);
  SetSubtitle(std::u16string());

  // Add the accessibility alert view first so that it does not overlap with
  // any other child view. Also make the view invisible.
  auto accessibility_view = std::make_unique<views::View>();
  accessibility_view->SetVisible(false);
  accessibility_alert_ = AddChildView(std::move(accessibility_view));

  // Show the sign in promo.
  auto sign_in_promo = std::make_unique<BubbleSignInPromoView>(
      controller_.GetWebContents(),
      signin_metrics::AccessPoint::kPasswordBubble,
      PasswordFormUniqueKey(controller_.pending_password()));
  AddChildView(std::move(sign_in_promo));
  // TODO(crbug.com/41493925) remove this SizeToContents() when the subsequent
  // code no longer depends on the sync auto-size here.
  SizeToContents();

  // Notify the screen reader that the bubble changed.
  AnnounceBubbleChange();

  GetBubbleFrameView()->SetProperty(views::kElementIdentifierKey,
                                    kPasswordBubbleElementId);

  return false;
#else
  return true;
#endif  // BUILDFLAG(ENABLE_DICE_SUPPORT)
}

views::View* PasswordSaveUpdateView::GetInitiallyFocusedView() {
  if (username_dropdown_ && username_dropdown_->GetText().empty()) {
    return username_dropdown_;
  }
  View* initial_view = PasswordBubbleViewBase::GetInitiallyFocusedView();
  // |initial_view| will normally be the 'Save' button, but in case it's not
  // focusable, we return nullptr so the Widget doesn't give focus to the next
  // focusable View, which would be |username_dropdown_|, and which would
  // bring up the menu without a user interaction. We only allow initial focus
  // on |username_dropdown_| above, when the text is empty.
  return (initial_view && initial_view->IsFocusable()) ? initial_view : nullptr;
}

bool PasswordSaveUpdateView::IsDialogButtonEnabled(
    ui::mojom::DialogButton button) const {
  return button != ui::mojom::DialogButton::kOk ||
         controller_.pending_password().IsFederatedCredential() ||
         !controller_.pending_password().password_value.empty();
}

ui::ImageModel PasswordSaveUpdateView::GetWindowIcon() {
  return ui::ImageModel::FromVectorIcon(GooglePasswordManagerVectorIcon(),
                                        ui::kColorIcon);
}

void PasswordSaveUpdateView::AddedToWidget() {
  static_cast<views::Label*>(GetBubbleFrameView()->title())
      ->SetAllowCharacterBreak(true);
  SetBubbleHeaderLottie(IDR_AUTOFILL_SAVE_PASSWORD_LOTTIE);
  GetBubbleFrameView()->SetProperty(views::kElementIdentifierKey,
                                    kPasswordBubbleElementId);
  if (BrowserUserEducationInterface* user_ed =
          BrowserUserEducationInterface::MaybeGetForWebContentsInTab(
              controller_.GetWebContents())) {
    if (user_ed->IsFeaturePromoActive(
            feature_engagement::kIPHPasswordsSaveRecoveryPromoFeature)) {
      user_ed->NotifyFeaturePromoFeatureUsed(
          feature_engagement::kIPHPasswordsSaveRecoveryPromoFeature,
          FeaturePromoFeatureUsedAction::kClosePromoIfPresent);
    }
  }
  UpdateBubbleUIElements();
}

void PasswordSaveUpdateView::UpdateUsernameAndPasswordInModel() {
  if (!username_dropdown_ && !password_dropdown_) {
    return;
  }
  std::u16string new_username = controller_.pending_password().username_value;
  std::u16string new_password = controller_.pending_password().password_value;
  if (username_dropdown_) {
    new_username = username_dropdown_->GetText();
    base::TrimString(new_username, u" ", &new_username);
  }
  if (password_dropdown_) {
    new_password = password_dropdown_->GetText();
  }
  controller_.OnCredentialEdited(std::move(new_username),
                                 std::move(new_password));
}

void PasswordSaveUpdateView::UpdateBubbleUIElements() {
  SetButtons(static_cast<int>(ui::mojom::DialogButton::kOk) |
             static_cast<int>(ui::mojom::DialogButton::kCancel));
  std::u16string ok_button_text = l10n_util::GetStringUTF16(
      controller_.IsCurrentStateUpdate() ? IDS_PASSWORD_MANAGER_UPDATE_BUTTON
                                         : IDS_PASSWORD_MANAGER_SAVE_BUTTON);
  if (IsTrustedVaultErrorResolutionEnabled() &&
      controller_.IsSavingBlockedByTrustedVaultError()) {
    ok_button_text = l10n_util::GetStringUTF16(IDS_CONTINUE);
  }
  SetButtonLabel(ui::mojom::DialogButton::kOk, ok_button_text);
  if (is_update_bubble_) {
    SetButtonLabel(
        ui::mojom::DialogButton::kCancel,
        l10n_util::GetStringUTF16(IDS_PASSWORD_MANAGER_CANCEL_BUTTON));
  } else if (IsSaveBubbleDropdownExperimentEnabled()) {
    if (controller_.IsMaxDismissalCountReached()) {
      SetButtonLabel(ui::mojom::DialogButton::kCancel,
                     l10n_util::GetStringUTF16(
                         IDS_PASSWORD_MANAGER_BUBBLE_BLOCKLIST_BUTTON));
    } else {
      SetButtons(static_cast<int>(ui::mojom::DialogButton::kNone));

      bool is_update = controller_.IsCurrentStateUpdate();
      std::u16string ok_text;
      if (IsTrustedVaultErrorResolutionEnabled() &&
          controller_.IsSavingBlockedByTrustedVaultError()) {
        ok_text = l10n_util::GetStringUTF16(IDS_CONTINUE);
      } else {
        ok_text = l10n_util::GetStringUTF16(
            is_update ? IDS_PASSWORD_MANAGER_SHORT_UPDATE_BUTTON
                      : IDS_PASSWORD_MANAGER_SAVE_BUTTON);
      }
      bool ok_enabled = IsDialogButtonEnabled(ui::mojom::DialogButton::kOk);

      if (custom_button_row_) {
        custom_button_row_->UpdateState(is_update, ok_text, ok_enabled);
      }
    }
  } else if (extra_view_) {
    // 3-button save dialog variant.
    SetButtonLabel(
        ui::mojom::DialogButton::kCancel,
        l10n_util::GetStringUTF16(IDS_PASSWORD_MANAGER_CANCEL_BUTTON));

    extra_view_->SetText(l10n_util::GetStringUTF16(
        IDS_PASSWORD_MANAGER_BUBBLE_BLOCKLIST_BUTTON));
  } else {
    // 2-button save dialog variant.
    SetButtonLabel(ui::mojom::DialogButton::kCancel,
                   l10n_util::GetStringUTF16(
                       IDS_PASSWORD_MANAGER_BUBBLE_BLOCKLIST_BUTTON));
  }

  std::u16string title = controller_.GetTitle();
  if (IsSaveBubbleDropdownExperimentEnabled()) {
    if (controller_.GetDomainForSubhead()) {
      title = l10n_util::GetStringUTF16(controller_.IsCurrentStateUpdate()
                                            ? IDS_UPDATE_PASSWORD
                                            : IDS_SAVE_PASSWORD);
    }
  }

  // If the title is going to change, we should announce it to the screen
  // readers.
  bool should_announce_save_update_change = GetWindowTitle() != title;
  SetTitle(title);
  if (IsTrustedVaultErrorResolutionEnabled() &&
      controller_.IsSavingBlockedByTrustedVaultError()) {
    SetSubtitle(l10n_util::GetStringUTF16(
        IDS_PASSWORD_BUBBLES_SUBTITLE_TRUSTED_VAULT_ERROR));
  } else if (IsSaveBubbleDropdownExperimentEnabled()) {
    std::optional<std::u16string> domain_subhead =
        controller_.GetDomainForSubhead();
    SetSubtitle(domain_subhead.value_or(std::u16string()));
  } else {
    // In other cases the subtitle is absent.
    SetSubtitle(std::u16string());
  }
  // Nothing to do if the bubble isn't visible yet.
  if (!GetWidget()) {
    return;
  }

  UpdateFootnote();

  if (should_announce_save_update_change) {
    AnnounceBubbleChange();
  }
}

std::unique_ptr<views::View> PasswordSaveUpdateView::CreateFooterView() {
  base::RepeatingClosure open_password_manager_closure = base::BindRepeating(
      [](PasswordSaveUpdateView* dialog) {
        dialog->controller_.OnGooglePasswordManagerLinkClicked(
            password_manager::ManagePasswordsReferrer::kSaveUpdateBubble);
      },
      base::Unretained(this));
  if (IsTrustedVaultErrorResolutionEnabled() &&
      controller_.IsSavingBlockedByTrustedVaultError()) {
    return CreateGooglePasswordManagerLabel(
        /*text_message_id=*/
        IDS_PASSWORD_BUBBLES_FOOTER_TRUSTED_VAULT_ERROR,
        /*link_message_id=*/
        IDS_PASSWORD_BUBBLES_PASSWORD_MANAGER_LINK_TEXT_SYNCED_TO_ACCOUNT,
        controller_.GetPrimaryAccountEmail(), open_password_manager_closure);
  }
  if (controller_.IsCurrentStateAffectingPasswordsStoredInTheGoogleAccount()) {
    return CreateGooglePasswordManagerLabel(
        /*text_message_id=*/
        IDS_PASSWORD_BUBBLES_FOOTER_SYNCED_TO_ACCOUNT,
        /*link_message_id=*/
        IDS_PASSWORD_BUBBLES_PASSWORD_MANAGER_LINK_TEXT_SYNCED_TO_ACCOUNT,
        controller_.GetPrimaryAccountEmail(), open_password_manager_closure);
  }
  return CreateGooglePasswordManagerLabel(
      /*text_message_id=*/
      IDS_PASSWORD_BUBBLES_FOOTER_SAVING_ON_DEVICE,
      /*link_message_id=*/
      IDS_PASSWORD_MANAGER_BRAND_NAME, open_password_manager_closure);
}

void PasswordSaveUpdateView::AnnounceBubbleChange() {
  // Federated credentials bubbles don't change the state between Update and
  // Save, and hence they don't have an `accessibility_alert_` view created.
  if (!accessibility_alert_) {
    return;
  }

  views::ViewAccessibility& ax = accessibility_alert_->GetViewAccessibility();
  ax.SetRole(ax::mojom::Role::kAlert);
  ax.SetName(GetWindowTitle(), ax::mojom::NameFrom::kAttribute);
  accessibility_alert_->NotifyAccessibilityEventDeprecated(
      ax::mojom::Event::kAlert, true);
}

void PasswordSaveUpdateView::OnContentChanged() {
  bool is_update_state_before = controller_.IsCurrentStateUpdate();
  bool is_ok_button_enabled_before =
      IsDialogButtonEnabled(ui::mojom::DialogButton::kOk);
  bool changes_synced_to_account_before =
      controller_.IsCurrentStateAffectingPasswordsStoredInTheGoogleAccount();
  UpdateUsernameAndPasswordInModel();
  // Maybe the buttons should be updated.
  if (is_update_state_before != controller_.IsCurrentStateUpdate() ||
      is_ok_button_enabled_before !=
          IsDialogButtonEnabled(ui::mojom::DialogButton::kOk)) {
    UpdateBubbleUIElements();
    DialogModelChanged();
  } else if (changes_synced_to_account_before !=
             controller_
                 .IsCurrentStateAffectingPasswordsStoredInTheGoogleAccount()) {
    // For account store users, there is a different footnote when affecting the
    // account store.
    UpdateFootnote();
  }
}

void PasswordSaveUpdateView::UpdateFootnote() {
  DCHECK(GetBubbleFrameView());
  GetBubbleFrameView()->SetFootnoteView(CreateFooterView());
}

void PasswordSaveUpdateView::TogglePasswordRevealed() {
  if (password_dropdown_->ArePasswordsRevealed()) {
    password_dropdown_->RevealPasswords(false);
    return;
  }
  // User authentication might be required, query the controller to determine
  // whether the user is allowed to unmask the password.

  // Prevent the bubble from closing for the duration of the lifetime of the
  // `pin`. This is to keep it open while the user authentication is in action.
  // Store pin as a class member so it can be destroyed early if needed.
  reveal_password_pin_ = PreventCloseOnDeactivate();
  controller_.ShouldRevealPasswords(base::BindOnce(
      [](PasswordSaveUpdateView* view, bool reveal) {
        auto pin = std::exchange(view->reveal_password_pin_, nullptr);
        if (!view->password_dropdown_) {
          return;
        }
        view->password_dropdown_->RevealPasswords(reveal);
        // This is necessary on Windows since the bubble isn't activated
        // again after the conlusion of the auth flow.
        view->GetWidget()->Activate();
        // Delay the destruction of `pin` for 1 sec to make sure the
        // bubble remains open till the OS closes the authentication
        // dialog and reactivates the bubble.
        base::SequencedTaskRunner::GetCurrentDefault()->PostDelayedTask(
            FROM_HERE, base::DoNothingWithBoundArgs(std::move(pin)),
            base::Seconds(1));
      },
      base::Unretained(this)));
}

views::MdTextButton* PasswordSaveUpdateView::GetOkButtonForTesting() const {
  if (IsSaveBubbleDropdownExperimentEnabled() && custom_button_row_) {
    return custom_button_row_->ok_button();
  }
  return views::BubbleDialogDelegateView::GetOkButton();
}

views::MdTextButton* PasswordSaveUpdateView::GetCancelButtonForTesting() const {
  if (IsSaveBubbleDropdownExperimentEnabled() && custom_button_row_) {
    return custom_button_row_->cancel_button()->GetVisible()
               ? custom_button_row_->cancel_button()
               : nullptr;
  }
  return views::BubbleDialogDelegateView::GetCancelButton();
}

ui::SimpleMenuModel* PasswordSaveUpdateView::MenuModelForTesting() const {
  if (!custom_button_row_ || !custom_button_row_->split_button()) {
    return nullptr;
  }
  return views::AsViewClass<CancelSplitButton>(
             custom_button_row_->split_button())
      ->menu_model();
}

BEGIN_METADATA(PasswordSaveUpdateView)
END_METADATA

DEFINE_CLASS_ELEMENT_IDENTIFIER_VALUE(PasswordSaveUpdateView,
                                      kPasswordBubbleElementId);
DEFINE_CLASS_ELEMENT_IDENTIFIER_VALUE(PasswordSaveUpdateView,
                                      kExtraButtonElementId);
DEFINE_CLASS_ELEMENT_IDENTIFIER_VALUE(PasswordSaveUpdateView,
                                      kNotNowButtonElementId);
DEFINE_CLASS_ELEMENT_IDENTIFIER_VALUE(PasswordSaveUpdateView,
                                      kCaretButtonElementId);
DEFINE_CLASS_ELEMENT_IDENTIFIER_VALUE(PasswordSaveUpdateView,
                                      kNeverMenuItemElementId);
