// 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 <string>

#include "base/command_line.h"
#include "base/test/run_until.h"
#include "base/time/time.h"
#include "build/build_config.h"
#include "chrome/browser/ui/browser_window/public/browser_window_interface.h"
#include "chrome/browser/ui/exclusive_access/exclusive_access_manager.h"
#include "chrome/browser/ui/exclusive_access/fullscreen_controller.h"
#include "chrome/browser/ui/tabs/tab_strip_model.h"
#include "chrome/browser/ui/test/fullscreen_test_util.h"
#include "chrome/browser/ui/test/popup_test_base.h"
#include "chrome/browser/ui/views/frame/browser_view.h"
#include "content/public/test/browser_test.h"
#include "content/public/test/browser_test_utils.h"
#include "net/dns/mock_host_resolver.h"
#include "third_party/blink/public/common/features_generated.h"
#include "ui/base/base_window.h"
#include "ui/display/display.h"
#include "ui/display/screen.h"
#include "ui/display/test/virtual_display_util.h"
#include "ui/gfx/geometry/rect.h"
#include "url/gurl.h"

namespace {

// Tests popups with multi-screen features from the Window Management API.
// Tests are run with and without the requisite Window Management permission.
// Tests must run in series to manage virtual displays on supported platforms.
// Use 2+ physical displays to run locally with --gtest_also_run_disabled_tests.
// See: //docs/ui/display/multiscreen_testing.md
#if BUILDFLAG(IS_CHROMEOS) || BUILDFLAG(IS_MAC) || BUILDFLAG(IS_WIN)
#define MAYBE_PopupMultiScreenTest PopupMultiScreenTest
#else
#define MAYBE_PopupMultiScreenTest DISABLED_PopupMultiScreenTest
#endif  // BUILDFLAG(IS_CHROMEOS) || BUILDFLAG(IS_MAC) || BUILDFLAG(IS_WIN)
#if BUILDFLAG(IS_WIN)
// TODO(crbug.com/371121282): Re-enable the test.
// TODO(crbug.com/365126887): Re-enable the test.
#undef MAYBE_PopupMultiScreenTest
#define MAYBE_PopupMultiScreenTest DISABLED_PopupMultiScreenTest
#endif  // BUILDFLAG(IS_WIN)
class MAYBE_PopupMultiScreenTest : public PopupTestBase,
                                   public ::testing::WithParamInterface<bool> {
 public:
  void SetUpOnMainThread() override {
    if (!SetUpVirtualDisplays()) {
      GTEST_SKIP() << "Skipping test; unavailable multi-screen support.";
    }
    ASSERT_GE(display::Screen::Get()->GetNumDisplays(), 2);
    host_resolver()->AddRule("*", "127.0.0.1");
    ASSERT_TRUE(embedded_test_server()->Start());
    content::WebContents* web_contents =
        browser()->tab_strip_model()->GetActiveWebContents();
    ASSERT_TRUE(NavigateToURL(web_contents,
                              embedded_test_server()->GetURL("/simple.html")));
    EXPECT_TRUE(WaitForRenderFrameReady(web_contents->GetPrimaryMainFrame()));
    if (ShouldTestWindowManagement()) {
      SetUpWindowManagement(browser());
    }
  }

  void TearDownOnMainThread() override { virtual_display_util_.reset(); }

 protected:
  bool ShouldTestWindowManagement() { return GetParam(); }

  // Create virtual displays as needed, ensuring 2 displays are available for
  // testing multi-screen functionality. Not all platforms and OS versions are
  // supported. Returns false if virtual displays could not be created.
  bool SetUpVirtualDisplays() {
    if (display::Screen::Get()->GetNumDisplays() > 1) {
      return true;
    }
    if ((virtual_display_util_ = display::test::VirtualDisplayUtil::TryCreate(
             display::Screen::Get()))) {
      virtual_display_util_->AddDisplay(
          display::test::VirtualDisplayUtil::k1024x768);
      return true;
    }
    return false;
  }

 private:
  std::unique_ptr<display::test::VirtualDisplayUtil> virtual_display_util_;
};

INSTANTIATE_TEST_SUITE_P(, MAYBE_PopupMultiScreenTest, ::testing::Bool());

// Tests opening a popup without explicit bounds.
IN_PROC_BROWSER_TEST_P(MAYBE_PopupMultiScreenTest, Basic) {
  // Copy the display vector so references are not invalidated while looping.
  std::vector<display::Display> displays =
      display::Screen::Get()->GetAllDisplays();
  for (const display::Display& opener_display : displays) {
    browser()->GetWindow()->SetBounds(opener_display.work_area());
    ASSERT_EQ(opener_display.id(), GetDisplayNearestBrowser(browser()).id());
    for (const char* url : {"/simple.html", "about:blank"}) {
      const std::string open_script =
          content::JsReplace("open($1, '', 'popup');", url);
      BrowserWindowInterface* popup = OpenPopup(browser(), open_script);
      display::Display popup_display = GetDisplayNearestBrowser(popup);
      // The popup should open on the same screen as the opener.
      EXPECT_EQ(opener_display.id(), popup_display.id())
          << " expected: " << opener_display.work_area().ToString()
          << " actual: " << popup_display.work_area().ToString()
          << " popup: " << popup->GetWindow()->GetBounds().ToString()
          << " script: " << open_script;
      // The popup is constrained to the available bounds of its screen.
      const gfx::Rect popup_bounds = popup->GetWindow()->GetBounds();
      EXPECT_TRUE(popup_display.work_area().Contains(popup_bounds))
          << " work_area: " << popup_display.work_area().ToString()
          << " popup: " << popup_bounds.ToString();
    }
  }
}

// Tests opening a popup on another screen.
IN_PROC_BROWSER_TEST_P(MAYBE_PopupMultiScreenTest, OpenOnAnotherScreen) {
  // Copy the display vector so references are not invalidated while looping.
  std::vector<display::Display> displays =
      display::Screen::Get()->GetAllDisplays();
  for (const display::Display& opener_display : displays) {
    browser()->GetWindow()->SetBounds(opener_display.work_area());
    ASSERT_EQ(opener_display.id(), GetDisplayNearestBrowser(browser()).id());
    for (const display::Display& target_display : displays) {
      for (const char* url : {"/simple.html", "about:blank"}) {
        const std::string open_script = content::JsReplace(
            "open($1, '', 'left=$2,top=$3,width=200,height=200');", url,
            target_display.work_area().x(), target_display.work_area().y());
        BrowserWindowInterface* popup = OpenPopup(browser(), open_script);
        display::Display popup_display = GetDisplayNearestBrowser(popup);
        // The popup only opens on another screen with permission.
        const display::Display& expected_display =
            ShouldTestWindowManagement() ? target_display : opener_display;
        EXPECT_EQ(expected_display.id(), popup_display.id())
            << " expected: " << expected_display.work_area().ToString()
            << " actual: " << popup_display.work_area().ToString()
            << " opener: " << browser()->GetWindow()->GetBounds().ToString()
            << " popup: " << popup->GetWindow()->GetBounds().ToString()
            << " script: " << open_script;
        // The popup is constrained to the available bounds of its screen.
        const gfx::Rect popup_bounds = popup->GetWindow()->GetBounds();
        EXPECT_TRUE(popup_display.work_area().Contains(popup_bounds))
            << " work_area: " << popup_display.work_area().ToString()
            << " popup: " << popup_bounds.ToString();
      }
    }
  }
}

// Tests opening a popup on the same screen, then moving it to another screen.
// TODO(crbug.com/365057654): Test is failing on Mac bot.
IN_PROC_BROWSER_TEST_P(MAYBE_PopupMultiScreenTest,
                       DISABLED_MoveToAnotherScreen) {
  content::WebContents* opener_contents =
      browser()->tab_strip_model()->GetActiveWebContents();
  // Copy the display vector so references are not invalidated while looping.
  display::Screen* screen = display::Screen::Get();
  std::vector<display::Display> displays = screen->GetAllDisplays();
  for (const display::Display& opener_display : displays) {
    browser()->GetWindow()->SetBounds(opener_display.work_area());
    ASSERT_EQ(opener_display.id(), GetDisplayNearestBrowser(browser()).id());
    gfx::Point opener_display_center = opener_display.work_area().CenterPoint();
    for (const display::Display& target_display : displays) {
      for (const char* url : {"/simple.html", "about:blank"}) {
        const std::string open_script = content::JsReplace(
            "w = open($1, '', 'left=$2,top=$3,width=200,height=200');", url,
            opener_display_center.x() - 100, opener_display_center.y() - 100);
        BrowserWindowInterface* popup = OpenPopup(browser(), open_script);
        EXPECT_EQ(opener_display, GetDisplayNearestBrowser(popup));
        // Ensure the opener can access the popup window object.
        ASSERT_NE("", EvalJs(opener_contents, "w.location.href"));

        // Have the opener try to move the popup to the target screen.
        const std::string move_script = content::JsReplace(
            "w.moveTo($1, $2);", target_display.work_area().x(),
            target_display.work_area().y());
        {
          SCOPED_TRACE(
              testing::Message()
              << "\n"
              << "script: " << open_script << " " << move_script << "\n"
              << "opener: " << browser()->GetWindow()->GetBounds().ToString()
              << " popup: " << popup->GetWindow()->GetBounds().ToString());
          content::ExecuteScriptAsync(opener_contents, move_script);
          WaitForBoundsChange(popup, /*move_by=*/40, /*resize_by=*/0);
        }
        const display::Display popup_display = GetDisplayNearestBrowser(popup);

        // The popup only moves to another screen with permission.
        const display::Display& expected_display =
            ShouldTestWindowManagement() ? target_display : opener_display;
        EXPECT_EQ(expected_display.id(), popup_display.id())
            << " expected: " << expected_display.work_area().ToString()
            << " actual: " << popup_display.work_area().ToString()
            << " opener: " << browser()->GetWindow()->GetBounds().ToString()
            << " popup: " << popup->GetWindow()->GetBounds().ToString()
            << " script: " << open_script << " " << move_script;
        // The popup is constrained to the available bounds of its screen.
        const gfx::Rect popup_bounds = popup->GetWindow()->GetBounds();
        EXPECT_TRUE(popup_display.work_area().Contains(popup_bounds))
            << " work_area: " << popup_display.work_area().ToString()
            << " popup: " << popup_bounds.ToString();
      }
    }
  }
}

// Tests opening a popup on another screen from a cross-origin iframe.
IN_PROC_BROWSER_TEST_P(MAYBE_PopupMultiScreenTest, CrossOriginIFrame) {
  net::EmbeddedTestServer https_server(net::EmbeddedTestServer::TYPE_HTTPS);
  https_server.SetCertHostnames({"a.com", "b.com"});
  https_server.AddDefaultHandlers(GetChromeTestDataDir());
  content::SetupCrossSiteRedirector(&https_server);
  ASSERT_TRUE(https_server.Start());
  content::WebContents* web_contents =
      browser()->tab_strip_model()->GetActiveWebContents();
  ASSERT_TRUE(NavigateToURL(web_contents,
                            https_server.GetURL("a.com", "/simple.html")));
  EXPECT_TRUE(WaitForRenderFrameReady(web_contents->GetPrimaryMainFrame()));
  // Grant permission to the new origin after navigation.
  if (ShouldTestWindowManagement()) {
    SetUpWindowManagement(browser());
  }

  // Append cross-origin iframes with and without the permission policy.
  const GURL src = https_server.GetURL("b.com", "/simple.html");
  const std::string script = R"JS(
    new Promise(resolve => {
      let f = document.createElement('iframe');
      f.src = $1;
      f.allow = $2 ? 'window-management' : '';
      f.addEventListener('load', () => resolve(true));
      document.body.appendChild(f);
    });
  )JS";
  EXPECT_EQ(true, EvalJs(web_contents, content::JsReplace(script, src, false)));
  EXPECT_EQ(true, EvalJs(web_contents, content::JsReplace(script, src, true)));

  // Copy the display vector so references are not invalidated while looping.
  std::vector<display::Display> displays =
      display::Screen::Get()->GetAllDisplays();
  for (const display::Display& opener_display : displays) {
    browser()->GetWindow()->SetBounds(opener_display.work_area());
    ASSERT_EQ(opener_display.id(), GetDisplayNearestBrowser(browser()).id());
    for (const bool iframe_policy_granted : {true, false}) {
      content::RenderFrameHost* cross_origin_iframe =
          ChildFrameAt(web_contents, iframe_policy_granted ? 1 : 0);
      ASSERT_TRUE(cross_origin_iframe);
      ASSERT_NE(cross_origin_iframe->GetLastCommittedOrigin(),
                web_contents->GetPrimaryMainFrame()->GetLastCommittedOrigin());
      for (const display::Display& target_display : displays) {
        for (const char* url : {"/simple.html", "about:blank"}) {
          const std::string open_script = content::JsReplace(
              "w = open($1, '', 'left=$2,top=$3,width=200,height=200');", url,
              target_display.work_area().x(), target_display.work_area().y());
          BrowserWindowInterface* popup =
              OpenPopup(cross_origin_iframe, open_script);
          display::Display popup_display = GetDisplayNearestBrowser(popup);
          // The popup only opens on another screen with permission.
          const display::Display& expected_display =
              ShouldTestWindowManagement() && iframe_policy_granted
                  ? target_display
                  : opener_display;
          EXPECT_EQ(expected_display.id(), popup_display.id())
              << " expected: " << expected_display.work_area().ToString()
              << " actual: " << popup_display.work_area().ToString()
              << " opener: " << browser()->GetWindow()->GetBounds().ToString()
              << " popup: " << popup->GetWindow()->GetBounds().ToString()
              << " script: " << open_script;
        }
      }
    }
  }
}

}  // namespace
