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

#include <algorithm>
#include <cstddef>
#include <optional>

#include "base/functional/bind.h"
#include "base/functional/callback_helpers.h"
#include "base/memory/raw_ptr.h"
#include "base/strings/string_util.h"
#include "base/strings/stringprintf.h"
#include "base/strings/utf_string_conversions.h"
#include "build/build_config.h"
#include "chrome/browser/content_settings/host_content_settings_map_factory.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/ui/blocked_content/framebust_block_tab_helper.h"
#include "chrome/browser/ui/browser_content_setting_bubble_model_delegate.h"
#include "chrome/browser/ui/browser_window/public/browser_window_features.h"
#include "chrome/browser/ui/browser_window/public/browser_window_interface.h"
#include "chrome/browser/ui/chrome_pages.h"
#include "chrome/browser/ui/content_settings/content_setting_bubble_model.h"
#include "chrome/browser/ui/content_settings/content_setting_image_model.h"
#include "chrome/browser/ui/content_settings/fake_owner.h"
#include "chrome/browser/ui/tabs/tab_strip_model.h"
#include "chrome/browser/ui/views/content_setting_bubble_contents.h"
#include "chrome/browser/ui/views/frame/browser_view.h"
#include "chrome/browser/ui/views/location_bar/content_setting_image_view.h"
#include "chrome/browser/ui/views/location_bar/location_bar_view.h"
#include "chrome/common/webui_url_constants.h"
#include "chrome/test/base/in_process_browser_test.h"
#include "chrome/test/base/ui_test_utils.h"
#include "components/blocked_content/url_list_manager.h"
#include "components/content_settings/core/browser/host_content_settings_map.h"
#include "components/content_settings/core/common/content_settings.h"
#include "components/content_settings/core/common/content_settings_types.h"
#include "components/tabs/public/tab_interface.h"
#include "content/public/common/content_features.h"
#include "content/public/common/isolated_world_ids.h"
#include "content/public/common/url_constants.h"
#include "content/public/test/browser_test.h"
#include "content/public/test/browser_test_utils.h"
#include "content/public/test/fenced_frame_test_util.h"
#include "content/public/test/prerender_test_util.h"
#include "content/public/test/test_navigation_observer.h"
#include "net/dns/mock_host_resolver.h"
#include "net/test/embedded_test_server/embedded_test_server.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "ui/events/base_event_utils.h"
#include "ui/events/event.h"
#include "ui/events/test/test_event.h"
#include "url/gurl.h"

#if BUILDFLAG(IS_CHROMEOS)
#include "chrome/browser/ash/system_web_apps/system_web_app_manager.h"
#endif

namespace {

const int kAllowRadioButtonIndex = 0;
const int kDisallowRadioButtonIndex = 1;

}  // namespace

class FramebustBlockBrowserTest
    : public InProcessBrowserTest,
      public blocked_content::UrlListManager::Observer {
 public:
  FramebustBlockBrowserTest() = default;

  // InProcessBrowserTest:
  void SetUpOnMainThread() override {
    host_resolver()->AddRule("*", "127.0.0.1");
    ASSERT_TRUE(embedded_test_server()->Start());
    current_browser_ = InProcessBrowserTest::browser();
    GetFramebustTabHelper()->manager()->AddObserver(this);
  }

  // UrlListManager::Observer:
  void BlockedUrlAdded(int32_t id, const GURL& blocked_url) override {
    if (!blocked_url_added_closure_.is_null()) {
      std::move(blocked_url_added_closure_).Run();
    }
  }

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

  FramebustBlockTabHelper* GetFramebustTabHelper() {
    return FramebustBlockTabHelper::From(
        tabs::TabInterface::GetFromContents(GetWebContents()));
  }

  void OnClick(const GURL& url, size_t index, size_t total_size) {
    clicked_url_ = url;
    clicked_index_ = index;
  }

  BrowserWindowInterface* browser() { return current_browser_; }

  void CreateAndSetBrowser() {
    current_browser_ = CreateBrowser(browser()->GetProfile());
  }

  bool NavigateIframeToUrlWithoutGesture(content::WebContents* contents,
                                         const std::string iframe_id,
                                         const GURL& url) {
    const char kScript[] = R"(
        var iframe = document.getElementById('%s');
        iframe.src='%s'
    )";
    content::TestNavigationObserver load_observer(contents);
    bool result = content::ExecJs(
        contents,
        base::StringPrintf(kScript, iframe_id.c_str(), url.spec().c_str()),
        content::EXECUTE_SCRIPT_NO_USER_GESTURE);
    load_observer.Wait();
    return result;
  }

  bool ExecuteAndCheckBlockedRedirection() {
    return ExecuteAndCheckBlockedRedirection(
        embedded_test_server()->GetURL("b.com", "/title1.html"));
  }

  // Attempts to framebust to `redirect_url` and ensures the navigation is
  // blocked. (The test fails if not.) Returns whether the blocked URL is added
  // to the tab helper, where the user can proceed to it if desired.
  bool ExecuteAndCheckBlockedRedirection(const GURL& redirect_url) {
    const GURL original_url = embedded_test_server()->GetURL("/iframe.html");
    EXPECT_TRUE(ui_test_utils::NavigateToURL(browser(), original_url));

    const GURL child_url =
        embedded_test_server()->GetURL("a.com", "/title1.html");
    NavigateIframeToUrlWithoutGesture(GetWebContents(), "test", child_url);

    content::RenderFrameHost* child =
        content::ChildFrameAt(GetWebContents()->GetPrimaryMainFrame(), 0);
    EXPECT_EQ(child_url, child->GetLastCommittedURL());

    base::RunLoop block_waiter;
    blocked_url_added_closure_ = block_waiter.QuitClosure();
    child->ExecuteJavaScriptForTests(
        base::ASCIIToUTF16(base::StringPrintf("window.top.location = '%s';",
                                              redirect_url.spec().c_str())),
        base::NullCallback(), content::ISOLATED_WORLD_ID_GLOBAL);
    block_waiter.Run();

    // Ensure we have not left the original page.
    EXPECT_EQ(original_url, GetWebContents()->GetLastCommittedURL());

    // Return whether the redirect URL itself ended up in the list of blocked
    // URLs, which only happens if the renderer had the ability to navigate to
    // the URL in the first place.
    return std::ranges::contains(GetFramebustTabHelper()->blocked_urls(),
                                 redirect_url);
  }

 protected:
  std::optional<GURL> clicked_url_;
  std::optional<size_t> clicked_index_;

  base::OnceClosure blocked_url_added_closure_;
  raw_ptr<BrowserWindowInterface, AcrossTasksDanglingUntriaged>
      current_browser_;
};

// Tests that clicking an item in the list of blocked URLs trigger a navigation
// to that URL.
IN_PROC_BROWSER_TEST_F(FramebustBlockBrowserTest, ModelAllowsRedirection) {
  const GURL blocked_urls[] = {
      embedded_test_server()->GetURL("b.com", "/title1.html"),
      embedded_test_server()->GetURL("c.com", "/title1.html"),
      embedded_test_server()->GetURL("d.com", "/title1.html"),
  };

  // Signal that a blocked redirection happened.
  auto* helper = GetFramebustTabHelper();
  for (const GURL& url : blocked_urls) {
    helper->AddBlockedUrl(url, url::Origin::Create(url),
                          base::BindOnce(&FramebustBlockBrowserTest::OnClick,
                                         base::Unretained(this)));
  }
  EXPECT_TRUE(helper->HasBlockedUrls());

  LocationBarView* location_bar_view =
      BrowserView::GetBrowserViewForBrowser(browser())->GetLocationBarView();
  ContentSettingImageView& image_view =
      **std::ranges::find(location_bar_view->GetContentSettingViewsForTest(),
                          ContentSettingImageModel::ImageType::kFramebust,
                          &ContentSettingImageView::GetType);

  EXPECT_TRUE(image_view.GetVisible());
  EXPECT_FALSE(image_view.IsBubbleShowing());

  image_view.ShowBubble(ui::test::TestEvent());

  EXPECT_FALSE(clicked_index_.has_value());
  EXPECT_FALSE(clicked_url_.has_value());

  // Simulate clicking on the second blocked URL.
  content::TestNavigationObserver observer(GetWebContents());
  ui::MouseEvent click_event(ui::EventType::kMousePressed, gfx::Point(),
                             gfx::Point(), ui::EventTimeForNow(),
                             ui::EF_LEFT_MOUSE_BUTTON,
                             ui::EF_LEFT_MOUSE_BUTTON);
  static_cast<ContentSettingBubbleContents*>(
      image_view.GetBubbleViewForTesting())
      ->LinkClicked(/*row=*/1, click_event);
  observer.Wait();

  EXPECT_TRUE(clicked_index_.has_value());
  EXPECT_TRUE(clicked_url_.has_value());
  EXPECT_EQ(1u, clicked_index_.value());
  EXPECT_EQ(embedded_test_server()->GetURL("c.com", "/title1.html"),
            clicked_url_.value());
  EXPECT_FALSE(helper->HasBlockedUrls());
  EXPECT_EQ(blocked_urls[1], GetWebContents()->GetLastCommittedURL());
}

IN_PROC_BROWSER_TEST_F(FramebustBlockBrowserTest,
                       EndToEndInitiatorVerification) {
  const GURL redirect_url =
      embedded_test_server()->GetURL("b.com", "/title1.html");
  EXPECT_TRUE(ExecuteAndCheckBlockedRedirection(redirect_url));

  auto* helper = GetFramebustTabHelper();
  EXPECT_TRUE(helper->HasBlockedUrls());
  EXPECT_EQ(1u, helper->blocked_urls().size());

  ContentSettingFramebustBlockBubbleModel framebust_block_bubble_model(
      browser()->GetFeatures().content_setting_bubble_model_delegate(),
      GetWebContents()->GetPrimaryPage());

  class InitiatorObserver : public content::WebContentsObserver {
   public:
    explicit InitiatorObserver(content::WebContents* web_contents)
        : content::WebContentsObserver(web_contents) {}

    void DidStartNavigation(
        content::NavigationHandle* navigation_handle) override {
      initiator_origin_ = navigation_handle->GetInitiatorOrigin();
    }

    std::optional<url::Origin> initiator_origin_;
  };

  InitiatorObserver init_observer(GetWebContents());
  content::TestNavigationObserver observer(GetWebContents());

  ui::MouseEvent click_event(ui::EventType::kMousePressed, gfx::Point(),
                             gfx::Point(), ui::EventTimeForNow(),
                             ui::EF_LEFT_MOUSE_BUTTON,
                             ui::EF_LEFT_MOUSE_BUTTON);
  framebust_block_bubble_model.OnListItemClicked(/* index = */ 0, click_event);
  observer.Wait();

  EXPECT_EQ(redirect_url, GetWebContents()->GetLastCommittedURL());
  EXPECT_TRUE(init_observer.initiator_origin_.has_value());
  EXPECT_EQ(url::Origin::Create(
                embedded_test_server()->GetURL("a.com", "/title1.html")),
            init_observer.initiator_origin_.value());
}

IN_PROC_BROWSER_TEST_F(FramebustBlockBrowserTest, AllowRadioButtonSelected) {
  const GURL url = embedded_test_server()->GetURL("/iframe.html");
  ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), url));

  // Signal that a blocked redirection happened.
  auto* helper = GetFramebustTabHelper();
  helper->AddBlockedUrl(url, url::Origin::Create(url),
                        base::BindOnce(&FramebustBlockBrowserTest::OnClick,
                                       base::Unretained(this)));
  EXPECT_TRUE(helper->HasBlockedUrls());

  HostContentSettingsMap* settings_map =
      HostContentSettingsMapFactory::GetForProfile(browser()->GetProfile());
  EXPECT_EQ(CONTENT_SETTING_BLOCK,
            settings_map->GetContentSetting(url, GURL(),
                                            ContentSettingsType::POPUPS));

  // Create a content bubble and simulate clicking on the first radio button
  // before closing it.
  ContentSettingFramebustBlockBubbleModel framebust_block_bubble_model(
      browser()->GetFeatures().content_setting_bubble_model_delegate(),
      GetWebContents()->GetPrimaryPage());
  std::unique_ptr<FakeOwner> owner = FakeOwner::Create(
      framebust_block_bubble_model, kDisallowRadioButtonIndex);

  owner->SetSelectedRadioOptionAndCommit(kAllowRadioButtonIndex);

  EXPECT_EQ(CONTENT_SETTING_ALLOW,
            settings_map->GetContentSetting(url, GURL(),
                                            ContentSettingsType::POPUPS));
}

IN_PROC_BROWSER_TEST_F(FramebustBlockBrowserTest, DisallowRadioButtonSelected) {
  const GURL url = embedded_test_server()->GetURL("/iframe.html");
  ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), url));

  // Signal that a blocked redirection happened.
  auto* helper = GetFramebustTabHelper();
  helper->AddBlockedUrl(url, url::Origin::Create(url),
                        base::BindOnce(&FramebustBlockBrowserTest::OnClick,
                                       base::Unretained(this)));
  EXPECT_TRUE(helper->HasBlockedUrls());

  HostContentSettingsMap* settings_map =
      HostContentSettingsMapFactory::GetForProfile(browser()->GetProfile());
  EXPECT_EQ(CONTENT_SETTING_BLOCK,
            settings_map->GetContentSetting(url, GURL(),
                                            ContentSettingsType::POPUPS));

  // Create a content bubble and simulate clicking on the second radio button
  // before closing it.
  ContentSettingFramebustBlockBubbleModel framebust_block_bubble_model(
      browser()->GetFeatures().content_setting_bubble_model_delegate(),
      GetWebContents()->GetPrimaryPage());

  std::unique_ptr<FakeOwner> owner =
      FakeOwner::Create(framebust_block_bubble_model, kAllowRadioButtonIndex);

  owner->SetSelectedRadioOptionAndCommit(kDisallowRadioButtonIndex);

  EXPECT_EQ(CONTENT_SETTING_BLOCK,
            settings_map->GetContentSetting(url, GURL(),
                                            ContentSettingsType::POPUPS));
}

#if BUILDFLAG(IS_CHROMEOS) || BUILDFLAG(IS_LINUX)
#define MAYBE_ManageButtonClicked DISABLED_ManageButtonClicked
#else
#define MAYBE_ManageButtonClicked ManageButtonClicked
#endif
IN_PROC_BROWSER_TEST_F(FramebustBlockBrowserTest, MAYBE_ManageButtonClicked) {
#if BUILDFLAG(IS_CHROMEOS)
  ash::SystemWebAppManager::GetForTest(browser()->GetProfile())
      ->InstallSystemAppsForTesting();
#endif

  const GURL url = embedded_test_server()->GetURL("/iframe.html");
  ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), url));

  // Signal that a blocked redirection happened.
  auto* helper = GetFramebustTabHelper();
  helper->AddBlockedUrl(url, url::Origin::Create(url),
                        base::BindOnce(&FramebustBlockBrowserTest::OnClick,
                                       base::Unretained(this)));
  EXPECT_TRUE(helper->HasBlockedUrls());

  // Create a content bubble and simulate clicking on the second radio button
  // before closing it.
  ContentSettingFramebustBlockBubbleModel framebust_block_bubble_model(
      browser()->GetFeatures().content_setting_bubble_model_delegate(),
      GetWebContents()->GetPrimaryPage());

  content::TestNavigationObserver navigation_observer(nullptr);
  navigation_observer.StartWatchingNewWebContents();
  framebust_block_bubble_model.OnManageButtonClicked();
  navigation_observer.Wait();

  EXPECT_TRUE(base::StartsWith(navigation_observer.last_navigation_url().spec(),
                               chrome::kChromeUISettingsURL,
                               base::CompareCase::SENSITIVE));
}

IN_PROC_BROWSER_TEST_F(FramebustBlockBrowserTest, SimpleFramebust_Blocked) {
  EXPECT_TRUE(ExecuteAndCheckBlockedRedirection());
}

// Attempts to navigate to chrome:// URLs should be blocked without allowing the
// user to proceed. Instead, the blocked URLs list includes content:kBlockedURL,
// which is about:blank#blocked, similar to other cases where a renderer
// attempts to navigate to an off-limits URL. See https://crbug.com/375550814.
IN_PROC_BROWSER_TEST_F(FramebustBlockBrowserTest,
                       Framebust_WebUI_Blocked_No_Bypass) {
  const GURL chrome_url(chrome::kChromeUISettingsURL);
  EXPECT_FALSE(ExecuteAndCheckBlockedRedirection(chrome_url));
  EXPECT_TRUE(std::ranges::contains(GetFramebustTabHelper()->blocked_urls(),
                                    GURL(content::kBlockedURL)));
}

// Attempts to navigate to file:// URLs should be blocked without allowing the
// user to proceed. Instead, the blocked URLs list includes content:kBlockedURL,
// which is about:blank#blocked, similar to other cases where a renderer
// attempts to navigate to an off-limits URL. See https://crbug.com/375550814.
IN_PROC_BROWSER_TEST_F(FramebustBlockBrowserTest,
                       Framebust_File_Blocked_No_Bypass) {
  const GURL file_url("file:///");
  EXPECT_FALSE(ExecuteAndCheckBlockedRedirection(file_url));
  EXPECT_TRUE(std::ranges::contains(GetFramebustTabHelper()->blocked_urls(),
                                    GURL(content::kBlockedURL)));
}

IN_PROC_BROWSER_TEST_F(FramebustBlockBrowserTest,
                       FramebustAllowedByGlobalSetting) {
  HostContentSettingsMap* settings_map =
      HostContentSettingsMapFactory::GetForProfile(browser()->GetProfile());
  settings_map->SetDefaultContentSetting(ContentSettingsType::POPUPS,
                                         CONTENT_SETTING_ALLOW);

  // Create a new browser to test in to ensure that the render process gets the
  // updated content settings.
  CreateAndSetBrowser();
  ASSERT_TRUE(ui_test_utils::NavigateToURL(
      browser(), embedded_test_server()->GetURL("/iframe.html")));
  NavigateIframeToUrlWithoutGesture(
      GetWebContents(), "test",
      embedded_test_server()->GetURL("a.com", "/title1.html"));

  content::RenderFrameHost* child =
      content::ChildFrameAt(GetWebContents()->GetPrimaryMainFrame(), 0);
  ASSERT_TRUE(child);

  GURL redirect_url = embedded_test_server()->GetURL("b.com", "/title1.html");

  content::TestNavigationObserver observer(GetWebContents());
  child->ExecuteJavaScriptForTests(
      base::ASCIIToUTF16(base::StringPrintf("window.top.location = '%s';",
                                            redirect_url.spec().c_str())),
      base::NullCallback(), content::ISOLATED_WORLD_ID_GLOBAL);
  observer.Wait();
  EXPECT_TRUE(GetFramebustTabHelper()->blocked_urls().empty());
}

IN_PROC_BROWSER_TEST_F(FramebustBlockBrowserTest,
                       FramebustAllowedBySiteSetting) {
  GURL top_level_url = embedded_test_server()->GetURL("/iframe.html");
  HostContentSettingsMap* settings_map =
      HostContentSettingsMapFactory::GetForProfile(browser()->GetProfile());
  settings_map->SetContentSettingDefaultScope(top_level_url, GURL(),
                                              ContentSettingsType::POPUPS,
                                              CONTENT_SETTING_ALLOW);

  // Create a new browser to test in to ensure that the render process gets the
  // updated content settings.
  CreateAndSetBrowser();
  ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), top_level_url));
  NavigateIframeToUrlWithoutGesture(
      GetWebContents(), "test",
      embedded_test_server()->GetURL("a.com", "/title1.html"));

  content::RenderFrameHost* child =
      content::ChildFrameAt(GetWebContents()->GetPrimaryMainFrame(), 0);
  ASSERT_TRUE(child);

  GURL redirect_url = embedded_test_server()->GetURL("b.com", "/title1.html");

  content::TestNavigationObserver observer(GetWebContents());
  child->ExecuteJavaScriptForTests(
      base::ASCIIToUTF16(base::StringPrintf("window.top.location = '%s';",
                                            redirect_url.spec().c_str())),
      base::NullCallback(), content::ISOLATED_WORLD_ID_GLOBAL);
  observer.Wait();
  EXPECT_TRUE(GetFramebustTabHelper()->blocked_urls().empty());
}

// Regression test for https://crbug.com/40597964, where the framebust UI would
// persist on subsequent navigations.
IN_PROC_BROWSER_TEST_F(FramebustBlockBrowserTest,
                       FramebustBlocked_SubsequentNavigation_NoUI) {
  EXPECT_TRUE(ExecuteAndCheckBlockedRedirection());

  // Now, navigate away and check that the UI went away.
  ASSERT_TRUE(ui_test_utils::NavigateToURL(
      browser(), embedded_test_server()->GetURL("/title2.html")));

  // TODO(csharrison): Ideally we could query the actual UI here. For now, just
  // look at the internal state of the framebust tab helper.
  EXPECT_FALSE(GetFramebustTabHelper()->HasBlockedUrls());
}

class FramebustBlockPrerenderTest : public FramebustBlockBrowserTest {
 public:
  FramebustBlockPrerenderTest()
      : prerender_helper_(
            base::BindRepeating(&FramebustBlockPrerenderTest::GetWebContents,
                                base::Unretained(this))) {}
  ~FramebustBlockPrerenderTest() override = default;

  void SetUpOnMainThread() override {
    prerender_helper_.RegisterServerRequestMonitor(embedded_test_server());
    FramebustBlockBrowserTest::SetUpOnMainThread();
  }

 protected:
  content::test::PrerenderTestHelper prerender_helper_;
};

IN_PROC_BROWSER_TEST_F(FramebustBlockPrerenderTest,
                       FramebustBlocked_PrerenderNavigation) {
  EXPECT_TRUE(ExecuteAndCheckBlockedRedirection());

  // Start a prerender and ensure that the framebust UI persists on the
  // prerender navigation.
  const GURL prerender_url =
      embedded_test_server()->GetURL("/title1.html?prerender");
  prerender_helper_.AddPrerender(prerender_url);
  EXPECT_TRUE(GetFramebustTabHelper()->HasBlockedUrls());

  // Activate a prerendered page.
  prerender_helper_.NavigatePrimaryPage(prerender_url);
  EXPECT_FALSE(GetFramebustTabHelper()->HasBlockedUrls());
}

class FramebustBlockFencedFrameTest : public FramebustBlockBrowserTest {
 public:
  FramebustBlockFencedFrameTest() = default;
  ~FramebustBlockFencedFrameTest() override = default;

  content::RenderFrameHost* primary_main_frame_host() {
    return GetWebContents()->GetPrimaryMainFrame();
  }

 protected:
  content::test::FencedFrameTestHelper fenced_frame_helper_;
};

IN_PROC_BROWSER_TEST_F(FramebustBlockFencedFrameTest,
                       FramebustBlocked_FencedFrameNavigation) {
  EXPECT_TRUE(ExecuteAndCheckBlockedRedirection());

  // Create a fenced frame in the primary main page and ensure that the
  // framebust UI persists on fenced frame navigation.
  const GURL fenced_frame_url =
      embedded_test_server()->GetURL("/fenced_frames/title1.html");
  content::RenderFrameHost* fenced_frame_rfh =
      fenced_frame_helper_.CreateFencedFrame(primary_main_frame_host(),
                                             fenced_frame_url);
  ASSERT_NE(nullptr, fenced_frame_rfh);

  EXPECT_TRUE(GetFramebustTabHelper()->HasBlockedUrls());
}
