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

#include "ui/views/controls/webview/web_dialog_view.h"

#include "base/functional/bind.h"
#include "base/functional/callback_helpers.h"
#include "base/location.h"
#include "base/memory/raw_ptr.h"
#include "base/run_loop.h"
#include "base/task/single_thread_task_runner.h"
#include "base/test/run_until.h"
#include "build/build_config.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/ui/browser_window.h"
#include "chrome/browser/ui/tabs/tab_strip_model.h"
#include "chrome/browser/ui/views/content_test_utils.h"
#include "chrome/browser/ui/webui/chrome_web_contents_handler.h"
#include "chrome/common/url_constants.h"
#include "chrome/test/base/in_process_browser_test.h"
#include "chrome/test/base/interactive_test_utils.h"
#include "chrome/test/base/ui_test_utils.h"
#include "components/input/native_web_keyboard_event.h"
#include "content/public/browser/browser_context.h"
#include "content/public/browser/render_widget_host_view.h"
#include "content/public/browser/web_contents.h"
#include "content/public/test/browser_test.h"
#include "third_party/blink/public/common/input/web_input_event.h"
#include "ui/base/accelerators/accelerator.h"
#include "ui/base/accelerators/test_accelerator_target.h"
#include "ui/base/mojom/ui_base_types.mojom-shared.h"
#include "ui/events/keycodes/keyboard_codes.h"
#include "ui/views/controls/button/label_button.h"
#include "ui/views/focus/focus_manager.h"
#include "ui/views/view_tracker.h"
#include "ui/views/widget/widget.h"
#include "ui/views/widget/widget_observer.h"
#include "ui/web_dialogs/test/test_web_dialog_delegate.h"

#if BUILDFLAG(IS_MAC)
#include "base/mac/mac_util.h"
#endif

namespace {

// Initial size of WebDialog for SizeWindow test case. Note the height must be
// at least 59 on Windows.
const int kInitialWidth = 60;
const int kInitialHeight = 60;

class WidgetResizeWaiter : public views::WidgetObserver {
 public:
  explicit WidgetResizeWaiter(views::Widget* widget) {
    old_size_ = widget->GetWindowBoundsInScreen().size();
    observation_.Observe(widget);
  }

  void Wait() { run_loop_.Run(); }

  void OnWidgetBoundsChanged(views::Widget* widget,
                             const gfx::Rect& bounds) override {
    if (bounds.size() != old_size_) {
      run_loop_.Quit();
    }
  }

 private:
  base::ScopedObservation<views::Widget, views::WidgetObserver> observation_{
      this};
  gfx::Size old_size_;
  base::RunLoop run_loop_;
};

}  // namespace

class WebDialogBrowserTest : public InProcessBrowserTest {
 public:
  WebDialogBrowserTest() = default;

  WebDialogBrowserTest(const WebDialogBrowserTest&) = delete;
  WebDialogBrowserTest& operator=(const WebDialogBrowserTest&) = delete;

  // content::BrowserTestBase:
  void SetUpOnMainThread() override;

 protected:
  void SimulateEscapeKey();

  bool was_view_deleted() const { return !view_tracker_.view(); }

  raw_ptr<views::WebDialogView, DanglingUntriaged> view_ = nullptr;
  bool web_dialog_delegate_destroyed_ = false;
  raw_ptr<ui::test::TestWebDialogDelegate, DanglingUntriaged> delegate_ =
      nullptr;

 private:
  views::ViewTracker view_tracker_;
};

void WebDialogBrowserTest::SetUpOnMainThread() {
  ui::test::TestWebDialogDelegate* delegate =
      new ui::test::TestWebDialogDelegate(GURL(chrome::kChromeUIChromeURLsURL));
  delegate->set_size(kInitialWidth, kInitialHeight);
  delegate->SetDeleteOnClosedAndObserve(&web_dialog_delegate_destroyed_);

  // Store the delegate so that we can update ShouldCloseDialogOnEscape().
  delegate_ = delegate;

  auto view = std::make_unique<views::WebDialogView>(
      browser()->GetProfile(), delegate,
      std::make_unique<ChromeWebContentsHandler>());
  view->SetOwnedByWidget(views::WidgetDelegate::OwnedByWidgetPassKey());
  gfx::NativeView parent_view =
      browser()->GetTabStripModel()->GetActiveWebContents()->GetNativeView();
  view_ = view.get();
  view_tracker_.SetView(view_);

  auto* widget =
      views::Widget::CreateWindowWithParent(std::move(view), parent_view);
  widget->Show();
  ASSERT_TRUE(base::test::RunUntil([&]() { return widget->IsVisible(); }));
}

void WebDialogBrowserTest::SimulateEscapeKey() {
  ui::KeyEvent escape_event(ui::EventType::kKeyPressed, ui::VKEY_ESCAPE,
                            ui::EF_NONE);
  if (view_->GetFocusManager()->OnKeyEvent(escape_event)) {
    ASSERT_TRUE(ui_test_utils::SendKeyPressToWindowSync(
        view_->GetWidget()->GetNativeWindow(), ui::VKEY_ESCAPE, false, false,
        false, false));
  }
}

// Windows has some issues resizing windows. An off by one problem, and a
// minimum size that seems too big. See http://crbug.com/41198181.
#if BUILDFLAG(IS_WIN)
#define MAYBE_SizeWindow DISABLED_SizeWindow
#else
#define MAYBE_SizeWindow SizeWindow
#endif
IN_PROC_BROWSER_TEST_F(WebDialogBrowserTest, MAYBE_SizeWindow) {
#if BUILDFLAG(IS_MAC)
  // On macOS, the new mechanism for sheets, which are used for window modals
  // like this dialog, always centers them within the parent window regardless
  // of the requested origin. The size is still honored.
  bool centered_in_window = true;
#else
  bool centered_in_window = false;
#endif

  gfx::Rect set_bounds = view_->GetWidget()->GetClientAreaBoundsInScreen();
  gfx::Rect actual_bounds, rwhv_bounds;

  // Bigger than the default in both dimensions.
  set_bounds.set_width(400);
  set_bounds.set_height(300);

  auto check_bounds = [&](const gfx::Rect& set, const gfx::Rect& actual) {
    if (centered_in_window) {
      gfx::Rect expected = browser()->GetWindow()->GetBounds();
      expected.ClampToCenteredSize(set.size());
      EXPECT_EQ(expected, actual);
    } else {
      EXPECT_EQ(set, actual);
    }
  };

  // WebDialogView ignores the WebContents* |source| argument to
  // SetContentsBounds. We could pass view_->web_contents(), but it's not
  // relevant for the test.
  {
    WidgetResizeWaiter waiter(view_->GetWidget());
    view_->SetContentsBounds(nullptr, set_bounds);
    waiter.Wait();
  }
  actual_bounds = view_->GetWidget()->GetClientAreaBoundsInScreen();
  check_bounds(set_bounds, actual_bounds);

  rwhv_bounds =
      view_->web_contents()->GetRenderWidgetHostView()->GetViewBounds();
  EXPECT_LT(0, rwhv_bounds.width());
  EXPECT_LT(0, rwhv_bounds.height());
  EXPECT_GE(set_bounds.width(), rwhv_bounds.width());
  EXPECT_GE(set_bounds.height(), rwhv_bounds.height());

  // Larger in one dimension and smaller in the other.
  set_bounds.set_width(550);
  set_bounds.set_height(250);

  {
    WidgetResizeWaiter waiter(view_->GetWidget());
    view_->SetContentsBounds(nullptr, set_bounds);
    waiter.Wait();
  }

  actual_bounds = view_->GetWidget()->GetClientAreaBoundsInScreen();
  check_bounds(set_bounds, actual_bounds);

  rwhv_bounds =
      view_->web_contents()->GetRenderWidgetHostView()->GetViewBounds();
  EXPECT_LT(0, rwhv_bounds.width());
  EXPECT_LT(0, rwhv_bounds.height());
  EXPECT_GE(set_bounds.width(), rwhv_bounds.width());
  EXPECT_GE(set_bounds.height(), rwhv_bounds.height());

  // Get very small.
  const gfx::Size min_size = view_->GetWidget()->GetMinimumSize();
  EXPECT_LT(0, min_size.width());
  EXPECT_LT(0, min_size.height());

  set_bounds.set_size(min_size);

  {
    WidgetResizeWaiter waiter(view_->GetWidget());
    view_->SetContentsBounds(nullptr, set_bounds);
    waiter.Wait();
  }

  actual_bounds = view_->GetWidget()->GetClientAreaBoundsInScreen();
  check_bounds(set_bounds, actual_bounds);

  rwhv_bounds =
      view_->web_contents()->GetRenderWidgetHostView()->GetViewBounds();
  EXPECT_LT(0, rwhv_bounds.width());
  EXPECT_LT(0, rwhv_bounds.height());
  EXPECT_GE(set_bounds.width(), rwhv_bounds.width());
  EXPECT_GE(set_bounds.height(), rwhv_bounds.height());

  // Check to make sure we can't get to 0x0. First expand beyond the minimum
  // size that was set above so that TestWebDialogView has a change to pick up.
  set_bounds.set_height(250);

  {
    WidgetResizeWaiter waiter(view_->GetWidget());
    view_->SetContentsBounds(nullptr, set_bounds);
    waiter.Wait();
  }

  actual_bounds = view_->GetWidget()->GetClientAreaBoundsInScreen();
  check_bounds(set_bounds, actual_bounds);

  // Now verify that attempts to re-size to 0x0 enforces the minimum size.
  set_bounds.set_width(0);
  set_bounds.set_height(0);

  {
    WidgetResizeWaiter waiter(view_->GetWidget());
    view_->SetContentsBounds(nullptr, set_bounds);
    waiter.Wait();
  }

  actual_bounds = view_->GetWidget()->GetClientAreaBoundsInScreen();
  EXPECT_EQ(min_size, actual_bounds.size());

  // And that the render view is also non-zero.
  rwhv_bounds =
      view_->web_contents()->GetRenderWidgetHostView()->GetViewBounds();
  EXPECT_LT(0, rwhv_bounds.width());
  EXPECT_LT(0, rwhv_bounds.height());

  // WebDialogView::CanClose() returns true only after before-unload handlers
  // have run (or the dialog has none and gets fast-closed via
  // RenderViewHostImpl::ClosePageIgnoringUnloadEvents which is the case here).
  // Close via WebContents for more authentic coverage (vs Widget::CloseNow()).
  EXPECT_FALSE(web_dialog_delegate_destroyed_);
  view_->web_contents()->Close();
  EXPECT_TRUE(web_dialog_delegate_destroyed_);

  // The close of the actual widget should happen asynchronously.
  EXPECT_FALSE(was_view_deleted());
  base::RunLoop().RunUntilIdle();
  EXPECT_TRUE(was_view_deleted());
}

// Test that closing the parent of a window-modal web dialog properly destroys
// the dialog and delegate.
IN_PROC_BROWSER_TEST_F(WebDialogBrowserTest, CloseParentWindow) {
  // Open a second browser window so we don't trigger shutdown.
  ui_test_utils::NavigateToURLWithDisposition(
      browser(), GURL(url::kAboutBlankURL), WindowOpenDisposition::NEW_WINDOW,
      ui_test_utils::BROWSER_TEST_NO_WAIT);

  // TestWebDialogDelegate defaults to window-modal, so closing the browser
  // Window (as opposed to closing merely the tab) should close the dialog.
  EXPECT_EQ(ui::mojom::ModalType::kWindow,
            view_->GetWidget()->widget_delegate()->GetModalType());

  // Close the parent window. Tear down may happen asynchronously.
  EXPECT_FALSE(web_dialog_delegate_destroyed_);
  EXPECT_FALSE(was_view_deleted());
  browser()->GetWindow()->Close();
  base::RunLoop().RunUntilIdle();
  EXPECT_TRUE(web_dialog_delegate_destroyed_);
  EXPECT_TRUE(was_view_deleted());
}

// Tests the Escape key behavior when ShouldCloseDialogOnEscape() is enabled.
IN_PROC_BROWSER_TEST_F(WebDialogBrowserTest, CloseDialogOnEscapeEnabled) {
  // Open a second browser window so we don't trigger shutdown.
  ui_test_utils::NavigateToURLWithDisposition(
      browser(), GURL(url::kAboutBlankURL), WindowOpenDisposition::NEW_WINDOW,
      ui_test_utils::BROWSER_TEST_NO_WAIT);

  // If ShouldCloseDialogOnEscape() is true, pressing Escape should close the
  // dialog.
  delegate_->SetCloseOnEscape(true);
  SimulateEscapeKey();
  base::RunLoop().RunUntilIdle();
  EXPECT_TRUE(web_dialog_delegate_destroyed_);
  EXPECT_TRUE(was_view_deleted());
}

// Tests the Escape key behavior when ShouldCloseDialogOnEscape() is disabled.
IN_PROC_BROWSER_TEST_F(WebDialogBrowserTest, CloseDialogOnEscapeDisabled) {
  // Open a second browser window so we don't trigger shutdown.
  ui_test_utils::NavigateToURLWithDisposition(
      browser(), GURL(url::kAboutBlankURL), WindowOpenDisposition::NEW_WINDOW,
      ui_test_utils::BROWSER_TEST_NO_WAIT);

  // If ShouldCloseDialogOnEscape() is false, pressing Escape does nothing.
  delegate_->SetCloseOnEscape(false);
  SimulateEscapeKey();
  base::RunLoop().RunUntilIdle();
  EXPECT_FALSE(web_dialog_delegate_destroyed_);
  EXPECT_FALSE(was_view_deleted());
}

// Verifies that when focus has switched to a button that consumes key events
// (such as a Confirm button), an unhandled Enter key event bubbling up from
// WebContents does not trigger an Enter accelerator.
// This is the regression test for crbug.com/523277481.
IN_PROC_BROWSER_TEST_F(WebDialogBrowserTest,
                       UnhandledEnterRespectsButtonFocus) {
  views::FocusManager* focus_manager = view_->GetFocusManager();
  ASSERT_TRUE(focus_manager);

  ui::TestAcceleratorTarget target;
  focus_manager->RegisterAccelerator(
      ui::Accelerator(ui::VKEY_RETURN, ui::EF_NONE),
      ui::AcceleratorManager::kNormalPriority, &target);

  input::NativeWebKeyboardEvent event(
      blink::WebInputEvent::Type::kRawKeyDown,
      blink::WebInputEvent::kNoModifiers,
      blink::WebInputEvent::GetStaticTimeStampForTests());
  event.windows_key_code = ui::VKEY_RETURN;

  // When WebDialogView is focused, an unhandled Enter event fires the
  // accelerator.
  focus_manager->SetFocusedView(view_);
  EXPECT_EQ(view_, focus_manager->GetFocusedView());
  view_->HandleKeyboardEvent(view_->web_contents(), event);
  EXPECT_EQ(1, target.accelerator_count());

  // After switching focus to a default button, an unhandled Enter event does
  // not fire the accelerator.
  auto* button = view_->AddChildView(std::make_unique<views::LabelButton>(
      views::Button::PressedCallback(), u"Confirm"));
  button->SetIsDefault(true);
  focus_manager->SetFocusedView(button);
  EXPECT_EQ(button, focus_manager->GetFocusedView());
  view_->HandleKeyboardEvent(view_->web_contents(), event);
  EXPECT_EQ(1, target.accelerator_count());
}

// Test that key event is translated to a text input properly.
// TODO(crbug.com/500602996): Enable the test.
#if BUILDFLAG(IS_MAC) || BUILDFLAG(IS_LINUX)
#define MAYBE_TextInputViaKeyEvent DISABLED_TextInputViaKeyEvent
#else
#define MAYBE_TextInputViaKeyEvent TextInputViaKeyEvent
#endif
IN_PROC_BROWSER_TEST_F(WebDialogBrowserTest, MAYBE_TextInputViaKeyEvent) {
  TestTextInputViaKeyEvent(view_->web_contents());
}
