// Copyright 2023 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/test/popup_test_base.h"

#include <string>

#include "base/memory/raw_ptr.h"
#include "chrome/browser/ui/browser_window/public/browser_window_interface.h"
#include "chrome/browser/ui/tabs/tab_strip_model.h"
#include "chrome/test/base/ui_test_utils.h"
#include "components/embedder_support/switches.h"
#include "components/permissions/permission_request_manager.h"
#include "content/public/test/browser_test_utils.h"
#include "ui/base/base_window.h"
#include "ui/display/display.h"
#include "ui/display/screen.h"
#include "ui/gfx/geometry/rect.h"
#include "ui/gfx/native_ui_types.h"
#include "ui/views/widget/widget.h"
#include "ui/views/widget/widget_observer.h"
#include "url/gurl.h"

namespace {

// A helper to wait for Browser window bounds changes beyond given thresholds.
class BoundsChangeWaiter final : public views::WidgetObserver {
 public:
  BoundsChangeWaiter(BrowserWindowInterface* browser,
                     int move_by,
                     int resize_by)
      : widget_(views::Widget::GetWidgetForNativeWindow(
            browser->GetWindow()->GetNativeWindow())),
        move_by_(move_by),
        resize_by_(resize_by),
        initial_bounds_(widget_->GetWindowBoundsInScreen()) {}

  BoundsChangeWaiter(const BoundsChangeWaiter&) = delete;
  BoundsChangeWaiter& operator=(const BoundsChangeWaiter&) = delete;
  ~BoundsChangeWaiter() final { widget_->RemoveObserver(this); }

  // views::WidgetObserver:
  void OnWidgetBoundsChanged(views::Widget* widget,
                             const gfx::Rect& rect) final {
    if (BoundsChangeMeetsThreshold(widget_->GetWindowBoundsInScreen())) {
      widget_->RemoveObserver(this);
      run_loop_.Quit();
    }
  }

  // Wait for changes to occur, or return immediately if they already have.
  void Wait() {
    if (!BoundsChangeMeetsThreshold(widget_->GetWindowBoundsInScreen())) {
      widget_->AddObserver(this);
      run_loop_.Run();
    }
  }

 private:
  bool BoundsChangeMeetsThreshold(const gfx::Rect& rect) const {
    return (std::abs(rect.x() - initial_bounds_.x()) >= move_by_ ||
            std::abs(rect.y() - initial_bounds_.y()) >= move_by_) &&
           (std::abs(rect.width() - initial_bounds_.width()) >= resize_by_ ||
            std::abs(rect.height() - initial_bounds_.height()) >= resize_by_);
  }

  const raw_ptr<views::Widget> widget_;
  const int move_by_, resize_by_;
  const gfx::Rect initial_bounds_;
  base::RunLoop run_loop_;
};

}  // namespace

void PopupTestBase::SetUpCommandLine(base::CommandLine* command_line) {
  command_line->AppendSwitch(embedder_support::kDisablePopupBlocking);
}

// static
BrowserWindowInterface* PopupTestBase::OpenPopup(
    BrowserWindowInterface* browser,
    const std::string& script,
    bool user_gesture) {
  return OpenPopup(browser->tab_strip_model()->GetActiveWebContents(), script,
                   user_gesture);
}

// static
BrowserWindowInterface* PopupTestBase::OpenPopup(
    const content::ToRenderFrameHost& adapter,
    const std::string& script,
    bool user_gesture) {
  if (user_gesture) {
    content::ExecuteScriptAsync(adapter, script);
  } else {
    content::ExecuteScriptAsyncWithoutUserGesture(adapter, script);
  }
  BrowserWindowInterface* popup = ui_test_utils::WaitForBrowserToOpen();
  content::WebContents* popup_contents =
      popup->tab_strip_model()->GetActiveWebContents();
  // The popup's bounds are initialized after the synchronous window.open().
  // Ideally, this might wait for browser->renderer window bounds init via:
  // blink::mojom::Widget.UpdateVisualProperties, but it seems sufficient to
  // wait for WebContents to load the URL after the initial about:blank doc,
  // and then for that Document's readyState to be 'complete'. Anecdotally,
  // initial bounds seem settled once outerWidth and outerHeight are non-zero.
  EXPECT_TRUE(WaitForLoadStop(popup_contents));
  EXPECT_TRUE(WaitForRenderFrameReady(popup_contents->GetPrimaryMainFrame()));
  EXPECT_NE("0x0", EvalJs(popup_contents, "outerWidth + 'x' + outerHeight"));
  return popup;
}

// static
void PopupTestBase::WaitForBoundsChange(BrowserWindowInterface* browser,
                                        int move_by,
                                        int resize_by) {
  BoundsChangeWaiter(browser, move_by, resize_by).Wait();
}

// static
void PopupTestBase::SetUpWindowManagement(BrowserWindowInterface* browser) {
  content::WebContents* web_contents =
      browser->tab_strip_model()->GetActiveWebContents();
  // Request and auto-accept the permission request.
  permissions::PermissionRequestManager* permission_request_manager =
      permissions::PermissionRequestManager::FromWebContents(web_contents);
  permission_request_manager->set_auto_response_for_test(
      permissions::PermissionRequestManager::ACCEPT_ALL);
  content::RenderFrameHost* rfh = web_contents->GetPrimaryMainFrame();
  ASSERT_TRUE(content::WaitForLoadStop(web_contents));
  ASSERT_TRUE(content::WaitForRenderFrameReady(rfh));
  ASSERT_EQ("complete", EvalJs(web_contents, "document.readyState"));
  ASSERT_EQ("visible", EvalJs(web_contents, "document.visibilityState"));
  ASSERT_GT(EvalJs(web_contents,
                   R"JS(getScreenDetails().then(s => {
                          window.screenDetails = s;
                          return s.screens.length; }))JS"),
            0);
  // Do not auto-accept any other permission requests.
  permission_request_manager->set_auto_response_for_test(
      permissions::PermissionRequestManager::NONE);
}

// static
display::Display PopupTestBase::GetDisplayNearestBrowser(
    const BrowserWindowInterface* browser) {
  return display::Screen::Get()->GetDisplayNearestWindow(
      browser->GetWindow()->GetNativeWindow());
}

// static
void PopupTestBase::WaitForUserActivationExpiry(
    BrowserWindowInterface* browser) {
  const std::string await_activation_expiry_script = R"(
    (async () => {
      while (navigator.userActivation.isActive)
        await new Promise(resolve => setTimeout(resolve, 1000));
      return navigator.userActivation.isActive;
    })();
  )";
  auto* tab = browser->tab_strip_model()->GetActiveWebContents();
  EXPECT_EQ(false, EvalJs(tab, await_activation_expiry_script,
                          content::EXECUTE_SCRIPT_NO_USER_GESTURE));
  EXPECT_FALSE(tab->HasRecentInteraction());
}
