// Copyright 2024 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/digital_credentials/digital_identity_multi_step_dialog.h"

#include "base/scoped_observation.h"
#include "base/test/scoped_feature_list.h"
#include "chrome/browser/digital_credentials/digital_identity_provider_desktop.h"
#include "chrome/browser/ui/browser.h"
#include "chrome/browser/ui/tabs/tab_strip_model.h"
#include "chrome/grit/generated_resources.h"
#include "chrome/test/base/in_process_browser_test.h"
#include "content/public/browser/cross_device_request_info.h"
#include "content/public/browser/web_contents.h"
#include "content/public/common/content_features.h"
#include "content/public/test/browser_test.h"
#include "testing/gtest/include/gtest/gtest-spi.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/base/mojom/dialog_button.mojom.h"
#include "ui/events/test/test_event.h"
#include "ui/views/bubble/bubble_dialog_delegate_view.h"
#include "ui/views/controls/label.h"
#include "ui/views/test/widget_test.h"
#include "ui/views/view.h"
#include "ui/views/widget/any_widget_observer.h"
#include "ui/views/widget/widget.h"
#include "ui/views/widget/widget_delegate.h"
#include "ui/views/widget/widget_observer.h"

namespace {

using ButtonParams = ui::DialogModel::Button::Params;

// Returns whether `parent` or one of its children is a views::Label with text
// `find_text`.
bool HasChildLabelViewWithText(const views::View* parent,
                               const std::u16string& find_text) {
  for (views::View* child : parent->children()) {
    if (child->GetObjectName() == "Label") {
      const views::Label* child_label = static_cast<const views::Label*>(child);
      if (child_label->GetText() == find_text) {
        return true;
      }
    }

    if (HasChildLabelViewWithText(child, find_text)) {
      return true;
    }
  }
  return false;
}

// Observes visibility changes on the `widget` passed to the constructor.
class WidgetVisibilityObserver : public views::WidgetObserver {
 public:
  explicit WidgetVisibilityObserver(views::Widget* widget) {
    observer_.Observe(widget);
  }

  void OnWidgetVisibilityChanged(views::Widget* widget, bool visible) override {
    did_widget_visibility_change_ = true;
  }

  bool did_widget_visiblity_change() const {
    return did_widget_visibility_change_;
  }

 private:
  bool did_widget_visibility_change_ = false;

  base::ScopedObservation<views::Widget, WidgetVisibilityObserver> observer_{
      this};
};

}  // anonymous namespace

class DigitalIdentityMultiStepDialogBrowserTest : public InProcessBrowserTest {
 public:
  DigitalIdentityMultiStepDialogBrowserTest() = default;
  ~DigitalIdentityMultiStepDialogBrowserTest() override = default;

  content::WebContents* GetActiveWebContents() {
    return browser()->tab_strip_model()->GetActiveWebContents();
  }

 private:
  base::test::ScopedFeatureList scoped_feature_list_{
      features::kWebIdentityDigitalCredentials};
};

// Check that DigitalIdentityMultiStepDialog::TryShow() updates the dialog's
// contents if the dialog is already showing.
IN_PROC_BROWSER_TEST_F(DigitalIdentityMultiStepDialogBrowserTest,
                       UpdateDialog) {
  constexpr char16_t kStep1Title[] = u"Title Step 1";
  constexpr char16_t kStep1Body[] = u"Body Step 1";
  constexpr char16_t kStep1AcceptButton[] = u"Accept Step 1";
  constexpr char16_t kStep1CancelButton[] = u"Cancel Step 1";

  constexpr char16_t kStep2Title[] = u"Title Step 1";
  constexpr char16_t kStep2Body[] = u"Body Step 1";
  constexpr char16_t kStep2AcceptButton[] = u"Accept Step 1";
  constexpr char16_t kStep2CancelButton[] = u"Cancel Step 1";

  auto dialog = std::make_unique<DigitalIdentityMultiStepDialog>(
      GetActiveWebContents()->GetWeakPtr());
  DigitalIdentityMultiStepDialog::TestApi dialog_test_api(dialog.get());

  {
    std::optional<ButtonParams> accept_button_params =
        std::make_optional<ButtonParams>();
    accept_button_params->SetLabel(kStep1AcceptButton);

    ButtonParams cancel_button_params;
    cancel_button_params.SetLabel(kStep1CancelButton);

    dialog->TryShow(accept_button_params, base::DoNothing(),
                    cancel_button_params, base::DoNothing(), kStep1Title,
                    kStep1Body, /*custom_body_field=*/nullptr,
                    /*show_progress_bar=*/false);
  }

  views::Widget* widget = dialog_test_api.GetWidget();
  views::BubbleDialogDelegate* widget_delegate =
      dialog_test_api.GetWidgetDelegate();

  // Observe `widget` to ensure that it does not get hidden as a result of the
  // second DigitalIdentityMultiStepDialog::TryShow() call.
  auto visibility_observer = std::make_unique<WidgetVisibilityObserver>(widget);

  EXPECT_TRUE(widget->IsVisible());
  EXPECT_EQ(kStep1Title, widget_delegate->GetWindowTitle());
  EXPECT_TRUE(HasChildLabelViewWithText(widget->GetRootView(), kStep1Body));
  EXPECT_EQ(static_cast<int>(ui::mojom::DialogButton::kOk) |
                static_cast<int>(ui::mojom::DialogButton::kCancel),
            widget_delegate->buttons());
  EXPECT_EQ(kStep1AcceptButton, widget_delegate->GetDialogButtonLabel(
                                    ui::mojom::DialogButton::kOk));
  EXPECT_EQ(kStep1CancelButton, widget_delegate->GetDialogButtonLabel(
                                    ui::mojom::DialogButton::kCancel));

  {
    std::optional<ButtonParams> accept_button_params =
        std::make_optional<ButtonParams>();
    accept_button_params->SetLabel(kStep2AcceptButton);

    ButtonParams cancel_button_params;
    cancel_button_params.SetLabel(kStep2CancelButton);

    dialog->TryShow(accept_button_params, base::DoNothing(),
                    cancel_button_params, base::DoNothing(), kStep2Title,
                    kStep2Body, /*custom_body_field=*/nullptr,
                    /*show_progress_bar=*/false);
  }

  // The same widget should be showing.
  EXPECT_EQ(widget, dialog_test_api.GetWidget());
  EXPECT_FALSE(visibility_observer->did_widget_visiblity_change());

  EXPECT_EQ(kStep2Title, widget_delegate->GetWindowTitle());
  EXPECT_TRUE(HasChildLabelViewWithText(widget->GetRootView(), kStep2Body));
  EXPECT_EQ(static_cast<int>(ui::mojom::DialogButton::kOk) |
                static_cast<int>(ui::mojom::DialogButton::kCancel),
            widget_delegate->buttons());
  EXPECT_EQ(kStep2AcceptButton, widget_delegate->GetDialogButtonLabel(
                                    ui::mojom::DialogButton::kOk));
  EXPECT_EQ(kStep2CancelButton, widget_delegate->GetDialogButtonLabel(
                                    ui::mojom::DialogButton::kCancel));
}

// Check that pressing the "OK" button in the dialog does not run the
// "cancel/close" callback.
IN_PROC_BROWSER_TEST_F(DigitalIdentityMultiStepDialogBrowserTest,
                       NotCanceledAfterOk) {
  bool was_ok_callback_called = false;
  bool was_cancel_callback_called = false;

  auto ok_callback = [](bool* was_ok_callback_called) {
    *was_ok_callback_called = true;
  };

  auto cancel_callback = [](bool* was_cancel_callback_called) {
    *was_cancel_callback_called = true;
  };

  auto dialog = std::make_unique<DigitalIdentityMultiStepDialog>(
      GetActiveWebContents()->GetWeakPtr());
  auto dialog_test_api =
      std::make_unique<DigitalIdentityMultiStepDialog::TestApi>(dialog.get());
  dialog->TryShow(
      std::make_optional<ButtonParams>(),
      base::BindRepeating(ok_callback, &was_ok_callback_called), ButtonParams(),
      base::BindOnce(cancel_callback, &was_cancel_callback_called), u"Title",
      u"Body", /*custom_body_field=*/nullptr, /*show_progress_bar=*/false);
  EXPECT_TRUE(dialog_test_api->GetWidget()->IsVisible());

  // Accept dialog and run any pending tasks.
  dialog_test_api->GetWidgetDelegate()->AcceptDialog();
  base::RunLoop().RunUntilIdle();
  EXPECT_TRUE(was_ok_callback_called);

  views::test::WidgetDestroyedWaiter destroyed_waiter(
      dialog_test_api->GetWidget());
  dialog_test_api.reset();
  dialog.reset();
  destroyed_waiter.Wait();

  EXPECT_FALSE(was_cancel_callback_called);
}

// Check that toggling button enabled state via DialogModel::Button::Params
// works.
IN_PROC_BROWSER_TEST_F(DigitalIdentityMultiStepDialogBrowserTest,
                       DisableButton) {
  auto dialog = std::make_unique<DigitalIdentityMultiStepDialog>(
      GetActiveWebContents()->GetWeakPtr());
  DigitalIdentityMultiStepDialog::TestApi dialog_test_api(dialog.get());

  {
    std::optional<ButtonParams> accept_button_params =
        std::make_optional<ButtonParams>();
    accept_button_params->SetEnabled(false);
    dialog->TryShow(accept_button_params, base::DoNothing(), ButtonParams(),
                    base::DoNothing(), u"Title", u"Body",
                    /*custom_body_field=*/nullptr, /*show_progress_bar=*/false);
    EXPECT_FALSE(dialog_test_api.GetWidgetDelegate()->IsDialogButtonEnabled(
        ui::mojom::DialogButton::kOk));
  }

  {
    std::optional<ButtonParams> accept_button_params =
        std::make_optional<ButtonParams>();
    accept_button_params->SetEnabled(true);
    dialog->TryShow(accept_button_params, base::DoNothing(), ButtonParams(),
                    base::DoNothing(), u"Title", u"Body",
                    /*custom_body_field=*/nullptr, /*show_progress_bar=*/false);
    EXPECT_TRUE(dialog_test_api.GetWidgetDelegate()->IsDialogButtonEnabled(
        ui::mojom::DialogButton::kOk));
  }
}

// -----------------------------------------------------------------------------
// Regression test for a heap-use-after-free in
// DigitalIdentityProviderDesktop::EndRequestWithError.
//
// EndRequestWithError() does:
//     dialog_.reset();                          // closes the Views widget
//     std::move(callback_).Run(...);            // dereferences `this->callback_`
//
// `dialog_.reset()` synchronously runs ~DigitalIdentityMultiStepDialog which
// calls Widget::CloseWithReason(). The developers acknowledge in
// content/browser/digital_credentials/digital_identity_request_impl.cc that
// this CloseWithReason path "(via activation observers) may destroy the hosting
// WebContents and therefore `this`". When the hosting WebContents dies,
// ~DigitalIdentityRequestImpl runs and its `provider_` unique_ptr deletes the
// DigitalIdentityProviderDesktop *while it is still on the stack in
// EndRequestWithError*. The subsequent read of `callback_` is then a UAF.
//
// This test models the activation-observer chain with a WidgetObserver that
// resets the owning unique_ptr from inside OnWidgetClosing - exactly what the
// production chain (extension popup -> WebContents teardown ->
// ~DigitalIdentityRequestImpl -> provider_.reset()) does synchronously.

namespace {

class UafTestDigitalIdentityProvider : public DigitalIdentityProviderDesktop {
 public:
  UafTestDigitalIdentityProvider() = default;
  ~UafTestDigitalIdentityProvider() override = default;

  void SetUpAndShowQrDialog(content::WebContents* web_contents) {
    set_web_contents_for_testing(web_contents->GetWeakPtr());
    set_rp_origin_for_testing(url::Origin::Create(GURL("https://rp.example")));
    set_callback_for_testing(base::BindOnce(
        [](base::expected<content::DigitalIdentityProvider::DigitalCredential,
                          content::DigitalIdentityProvider::
                              RequestStatusForMetrics>) {}));
    ShowQrCodeDialog("FIDO:/0123456789",
                     content::digital_credentials::cross_device::RequestInfo::
                         RequestType::kGet);
  }
};

// Stands in for the production activation-observer chain: when the dialog
// widget closes, the transient host (e.g. extension popup) tears down its
// WebContents, whose RenderFrameHost destruction runs
// ~DigitalIdentityRequestImpl, which owns the provider via std::unique_ptr.
class ProviderDestroyingWidgetObserver : public views::WidgetObserver {
 public:
  explicit ProviderDestroyingWidgetObserver(
      std::unique_ptr<DigitalIdentityProviderDesktop>* owner)
      : owner_(owner) {}

  void OnWidgetClosing(views::Widget* widget) override {
    widget->RemoveObserver(this);
    owner_->reset();
  }

 private:
  raw_ptr<std::unique_ptr<DigitalIdentityProviderDesktop>> owner_;
};

}  // namespace

IN_PROC_BROWSER_TEST_F(DigitalIdentityMultiStepDialogBrowserTest,
                       EndRequestWithErrorOwnerDestroyedDuringDialogClose) {
  // The provider is owned via unique_ptr exactly as in
  // DigitalIdentityRequestImpl::provider_.
  std::unique_ptr<DigitalIdentityProviderDesktop> provider =
      std::make_unique<UafTestDigitalIdentityProvider>();

  // Capture the QR dialog widget when it is created.
  views::Widget* dialog_widget = nullptr;
  std::u16string qr_title =
      l10n_util::GetStringUTF16(IDS_WEB_DIGITAL_CREDENTIALS_QR_TITLE);
  views::AnyWidgetObserver any_observer{views::test::AnyWidgetTestPasskey()};
  any_observer.set_initialized_callback(base::BindRepeating(
      [](views::Widget** out, const std::u16string& title,
         views::Widget* widget) {
        if (widget->widget_delegate() &&
            widget->widget_delegate()->GetWindowTitle() == title) {
          *out = widget;
        }
      },
      &dialog_widget, qr_title));

  static_cast<UafTestDigitalIdentityProvider*>(provider.get())
      ->SetUpAndShowQrDialog(GetActiveWebContents());
  ASSERT_NE(dialog_widget, nullptr);
  base::WeakPtr<views::Widget> weak_dialog_widget =
      dialog_widget->GetWeakPtr();

  // When EndRequestWithError() resets `dialog_` and the widget fires
  // OnWidgetClosing, this observer deletes the provider - mimicking the
  // activation-observer -> WebContents teardown -> ~DigitalIdentityRequestImpl
  // -> provider_.reset() chain.
  ProviderDestroyingWidgetObserver destroyer(&provider);
  dialog_widget->AddObserver(&destroyer);

  // Click "Cancel" on the QR dialog. OnDialogCanceled() PostTasks the bound
  // OnCanceled() callback and returns false so the widget stays open.
  static_cast<views::DialogDelegate*>(dialog_widget->widget_delegate())
      ->CancelDialog();
  ASSERT_FALSE(dialog_widget->IsClosed());

  // Runs the posted OnCanceled() -> EndRequestWithError():
  //   dialog_.reset()
  //     -> ~DigitalIdentityMultiStepDialog
  //       -> Widget::CloseWithReason()
  //         -> OnWidgetClosing -> provider.reset() -> delete provider
  //   std::move(callback_).Run(...)   <-- reads freed `this->callback_`
  base::RunLoop().RunUntilIdle();

  // If we get here without ASAN tripping, the bug is fixed.
  EXPECT_EQ(provider.get(), nullptr);
  if (weak_dialog_widget) {
    weak_dialog_widget->RemoveObserver(&destroyer);
    views::test::WidgetDestroyedWaiter(weak_dialog_widget.get()).Wait();
  }
}
