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

#include "base/functional/bind.h"
#include "base/memory/ref_counted_memory.h"
#include "base/test/bind.h"
#include "base/test/scoped_feature_list.h"
#include "content/browser/renderer_host/navigation_controller_impl.h"
#include "content/browser/renderer_host/render_frame_host_impl.h"
#include "content/browser/web_contents/web_contents_impl.h"
#include "content/browser/webui/web_ui_data_source_impl.h"
#include "content/common/features.h"
#include "content/public/browser/webui_config.h"
#include "content/public/browser/webui_config_map.h"
#include "content/public/common/content_client.h"
#include "content/public/common/content_features.h"
#include "content/public/common/url_utils.h"
#include "content/public/test/browser_test.h"
#include "content/public/test/browser_test_utils.h"
#include "content/public/test/content_browser_test.h"
#include "content/public/test/content_browser_test_content_browser_client.h"
#include "content/public/test/content_browser_test_utils.h"
#include "content/public/test/test_navigation_observer.h"
#include "content/public/test/test_utils.h"
#include "content/public/test/web_ui_browsertest_util.h"
#include "content/shell/browser/shell.h"
#include "content/test/content_browser_test_utils_internal.h"
#include "url/url_constants.h"

namespace content {

#if !BUILDFLAG(IS_ANDROID)
namespace {

class TestWebUIV2EnabledConfig : public DefaultWebUIConfig<WebUIController> {
 public:
  explicit TestWebUIV2EnabledConfig(std::string_view host)
      : DefaultWebUIConfig("chrome", host) {}
  bool SupportsInProcessResourceLoadingV2() const override { return true; }
};

class TestWebUIV2DisabledConfig : public DefaultWebUIConfig<WebUIController> {
 public:
  explicit TestWebUIV2DisabledConfig(std::string_view host)
      : DefaultWebUIConfig("chrome", host) {}
  bool SupportsInProcessResourceLoadingV2() const override { return false; }
};

class InitialWebUIOverrideContentBrowserClient
    : public ContentBrowserTestContentBrowserClient {
 public:
  explicit InitialWebUIOverrideContentBrowserClient(
      const GURL& initial_webui_url)
      : initial_webui_url_(initial_webui_url) {}

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

  bool IsInitialWebUIURL(const GURL& url) override {
    return initial_webui_url_ == url;
  }
  bool IsTopChromeWebUIURL(const GURL& url) override {
    return url.SchemeIs("chrome") && url.DomainIs("top-chrome");
  }

 private:
  GURL initial_webui_url_;
};

class WebUITestWebUIControllerFactory : public WebUIControllerFactory {
 public:
  std::unique_ptr<WebUIController> CreateWebUIControllerForURL(
      WebUI* web_ui,
      const GURL& url) override {
    return HasWebUIScheme(url) ? std::make_unique<WebUIController>(web_ui)
                               : nullptr;
  }
  WebUI::TypeID GetWebUIType(BrowserContext* browser_context,
                             const GURL& url) override {
    return HasWebUIScheme(url) ? reinterpret_cast<WebUI::TypeID>(1) : nullptr;
  }
  bool UseWebUIForURL(BrowserContext* browser_context,
                      const GURL& url) override {
    return HasWebUIScheme(url);
  }
};

}  // namespace

class InitialWebUINavigationBrowserTest : public ContentBrowserTest {
 public:
  InitialWebUINavigationBrowserTest() {
    WebUIControllerFactory::RegisterFactory(&factory_);
    scoped_feature_list_.InitWithFeatures(
        {features::kWebUIInProcessResourceLoadingV2}, {});
  }

 protected:
  WebContentsImpl* contents() const {
    return static_cast<WebContentsImpl*>(shell()->web_contents());
  }

 private:
  WebUITestWebUIControllerFactory factory_;
  base::test::ScopedFeatureList scoped_feature_list_;
};

IN_PROC_BROWSER_TEST_F(InitialWebUINavigationBrowserTest, CommitInitialWebUI) {
  GURL url("chrome://foo.top-chrome");

  auto& config_map = WebUIConfigMap::GetInstance();
  config_map.AddWebUIConfig(
      std::make_unique<TestWebUIV2EnabledConfig>("foo.top-chrome"));
  base::ScopedClosureRunner cleanup_configs(base::BindLambdaForTesting([&]() {
    WebUIConfigMap::GetInstance().RemoveConfig(GURL("chrome://foo.top-chrome"));
  }));

  // Make `url` an initial WebUI URL.
  InitialWebUIOverrideContentBrowserClient content_browser_client(url);

  // Create a new WebContents, since initial WebUI navigations are only allowed
  // to happen as the first navigation in a new WebContents.
  WebContents::CreateParams new_contents_params(contents()->GetBrowserContext(),
                                                contents()->GetSiteInstance());
  // Make sure the initial SiteInstance for the WebContents uses `url` as the
  // site URL, rather than an empty URL. This is required to make sure that the
  // renderer process flags are clear.
  new_contents_params.site_instance =
      SiteInstance::CreateForURL(contents()->GetBrowserContext(), url);
  std::unique_ptr<WebContents> new_web_contents(
      WebContents::Create(new_contents_params));
  NavigationControllerImpl& controller =
      static_cast<NavigationControllerImpl&>(new_web_contents->GetController());

  // Setup a fake resource path for the body. Note that we're adding a direct
  // string response instead of adding a ResourceBundle resource ID. This is
  // different from what happens in production (which will use a resource ID),
  // but this is significantly easier to test.
  WebUIDataSource* source = WebUIDataSource::CreateAndAdd(
      controller.GetBrowserContext(), "foo.top-chrome");
  source->SetResourcePathToResponse("", "<!doctype html><body>bar</body>");

  // Ensure that there are no navigations yet on the new WebContents.
  FrameTreeNode* root = static_cast<WebContentsImpl*>(new_web_contents.get())
                            ->GetPrimaryFrameTree()
                            .root();
  EXPECT_FALSE(root->navigation_request());

  // Navigate to `url`.
  TestNavigationObserver navigation_observer(url);
  navigation_observer.WatchExistingWebContents();
  controller.LoadURLWithParams(NavigationController::LoadURLParams(url));

  // The navigation reached CommitNavigation synchronously, since it is now
  // owned by the primary main RenderFrameHost instead of the root
  // FrameTreeNode.
  EXPECT_FALSE(root->navigation_request());
  EXPECT_TRUE(
      root->current_frame_host()->HasPendingCommitForCrossDocumentNavigation());
  {
    std::vector<base::SafeRef<NavigationHandle>>
        committing_navigation_requests =
            root->current_frame_host()
                ->GetPendingCommitCrossDocumentNavigations();
    EXPECT_EQ(1u, committing_navigation_requests.size());
    EXPECT_EQ(url, committing_navigation_requests[0]->GetURL());
  }
  navigation_observer.Wait();

  // Ensure the navigation successfully commits and loads the body HTML.
  EXPECT_TRUE(navigation_observer.last_navigation_succeeded());
  EXPECT_EQ(navigation_observer.last_navigation_url(), url);

  RenderFrameHostImplWrapper rfh(root->current_frame_host());
  EXPECT_EQ("bar", EvalJs(rfh.get(), "document.body.innerHTML"));

  // Ensure that the process has the correct flag set.
  EXPECT_TRUE(rfh->GetProcess()->IsForTopChromeWebUI());

  // Check that CSP was set.
  {
    const std::vector<network::mojom::ContentSecurityPolicyPtr>& root_csp =
        rfh->policy_container_host()->policies().content_security_policies;
    EXPECT_EQ(1u, root_csp.size());
    EXPECT_EQ(
        "child-src 'none';"
        "object-src 'none';"
        "require-trusted-types-for 'script';"
        "script-src chrome://resources 'self';"
        "trusted-types;frame-ancestors 'none';",
        root_csp[0]->header->header_value);
  }
}
// Verifies that V2 enablement is based on WebUIConfig.
IN_PROC_BROWSER_TEST_F(InitialWebUINavigationBrowserTest,
                       V2EnabledBasedOnWebUIConfig) {
  GURL v2_enabled_url("chrome://v2-enabled");
  GURL v2_disabled_url("chrome://v2-disabled");

  auto& config_map = WebUIConfigMap::GetInstance();
  config_map.AddWebUIConfig(
      std::make_unique<TestWebUIV2EnabledConfig>("v2-enabled"));
  config_map.AddWebUIConfig(
      std::make_unique<TestWebUIV2DisabledConfig>("v2-disabled"));

  // Cleanup configs on teardown.
  base::ScopedClosureRunner cleanup_configs(base::BindLambdaForTesting([&]() {
    WebUIConfigMap::GetInstance().RemoveConfig(GURL("chrome://v2-enabled"));
    WebUIConfigMap::GetInstance().RemoveConfig(GURL("chrome://v2-disabled"));
  }));

  // Test v2_disabled_url (should NOT have V2 resources in config)
  {
    WebContents::CreateParams new_contents_params(
        contents()->GetBrowserContext());
    new_contents_params.site_instance = SiteInstance::CreateForURL(
        contents()->GetBrowserContext(), v2_disabled_url);
    std::unique_ptr<WebContents> new_web_contents(
        WebContents::Create(new_contents_params));
    NavigationControllerImpl& controller =
        static_cast<NavigationControllerImpl&>(
            new_web_contents->GetController());

    WebUIDataSource* source = WebUIDataSource::CreateAndAdd(
        controller.GetBrowserContext(), "v2-disabled");
    // Use SetRequestFilter to serve the main HTML so it loads successfully.
    source->SetRequestFilter(
        base::BindRepeating([](const std::string& path) { return path == ""; }),
        base::BindRepeating([](const std::string& path,
                               URLDataSource::GotDataCallback callback) {
          std::move(callback).Run(base::MakeRefCounted<base::RefCountedString>(
              "<!doctype html><body>disabled</body>"));
        }));
    // Enable strings.m.js generation.
    source->UseStringsJs();
    source->AddString("dummy", "value");

    TestNavigationObserver navigation_observer(v2_disabled_url);
    navigation_observer.WatchExistingWebContents();
    controller.LoadURLWithParams(
        NavigationController::LoadURLParams(v2_disabled_url));
    navigation_observer.Wait();
    EXPECT_TRUE(navigation_observer.last_navigation_succeeded());

    RenderFrameHostImpl* rfh = static_cast<RenderFrameHostImpl*>(
        new_web_contents->GetPrimaryMainFrame());
    auto& config = rfh->local_resource_loader_config_for_testing();
    EXPECT_FALSE(config.is_null());
    auto origin = url::Origin::Create(v2_disabled_url);
    EXPECT_TRUE(config->sources.contains(origin));
    // strings.m.js should NOT be present because V2 is disabled for this
    // origin.
    EXPECT_FALSE(
        config->sources[origin]->path_to_resource_map.contains("strings.m.js"));
  }

  // Test v2_enabled_url (should HAVE V2 resources in config)
  {
    WebContents::CreateParams new_contents_params(
        contents()->GetBrowserContext());
    new_contents_params.site_instance = SiteInstance::CreateForURL(
        contents()->GetBrowserContext(), v2_enabled_url);
    std::unique_ptr<WebContents> new_web_contents(
        WebContents::Create(new_contents_params));
    NavigationControllerImpl& controller =
        static_cast<NavigationControllerImpl&>(
            new_web_contents->GetController());

    WebUIDataSource* source = WebUIDataSource::CreateAndAdd(
        controller.GetBrowserContext(), "v2-enabled");
    // Use SetRequestFilter to serve the main HTML so it loads successfully.
    source->SetRequestFilter(
        base::BindRepeating([](const std::string& path) { return path == ""; }),
        base::BindRepeating([](const std::string& path,
                               URLDataSource::GotDataCallback callback) {
          std::move(callback).Run(base::MakeRefCounted<base::RefCountedString>(
              "<!doctype html><body>enabled</body>"));
        }));
    // Enable strings.m.js generation.
    source->UseStringsJs();
    source->AddString("dummy", "value");

    TestNavigationObserver navigation_observer(v2_enabled_url);
    navigation_observer.WatchExistingWebContents();
    controller.LoadURLWithParams(
        NavigationController::LoadURLParams(v2_enabled_url));
    navigation_observer.Wait();
    EXPECT_TRUE(navigation_observer.last_navigation_succeeded());

    RenderFrameHostImpl* rfh = static_cast<RenderFrameHostImpl*>(
        new_web_contents->GetPrimaryMainFrame());
    auto& config = rfh->local_resource_loader_config_for_testing();
    EXPECT_FALSE(config.is_null());
    auto origin = url::Origin::Create(v2_enabled_url);
    EXPECT_TRUE(config->sources.contains(origin));
    // Generated resources SHOULD be present because V2 is enabled for this
    // origin.
    EXPECT_TRUE(
        config->sources[origin]->path_to_resource_map.contains("strings.m.js"));
  }
}

class InitialWebUINavigationFeatureEnabledBrowserTest
    : public InitialWebUINavigationBrowserTest,
      public testing::WithParamInterface<bool> {
 public:
  InitialWebUINavigationFeatureEnabledBrowserTest() {
    if (ShouldAllowDebug()) {
      feature_list_.InitAndEnableFeature(features::kDebugTopChromeWebUI);
    } else {
      feature_list_.InitAndDisableFeature(features::kDebugTopChromeWebUI);
    }
  }

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

 private:
  base::test::ScopedFeatureList feature_list_;
};

INSTANTIATE_TEST_SUITE_P(All,
                         InitialWebUINavigationFeatureEnabledBrowserTest,
                         testing::Bool());

// Test that we can navigate away from the initial web UI if and only if
// debugging is enabled.
IN_PROC_BROWSER_TEST_P(InitialWebUINavigationFeatureEnabledBrowserTest,
                       NavigateAwayFromInitialWebUIWithDebugEnabled) {
  GURL initial_webui_url("chrome://foo.top-chrome");

  auto& config_map = WebUIConfigMap::GetInstance();
  config_map.AddWebUIConfig(
      std::make_unique<TestWebUIV2EnabledConfig>("foo.top-chrome"));
  base::ScopedClosureRunner cleanup_configs(base::BindLambdaForTesting([&]() {
    WebUIConfigMap::GetInstance().RemoveConfig(GURL("chrome://foo.top-chrome"));
  }));

  InitialWebUIOverrideContentBrowserClient content_browser_client(
      initial_webui_url);

  // Setup data source for the WebUI.
  WebUIDataSource* source = WebUIDataSource::CreateAndAdd(
      contents()->GetBrowserContext(), "foo.top-chrome");
  source->SetResourcePathToResponse("", "<!doctype html><body>bar</body>");

  // Create a new WebContents to navigate to initial WebUI.
  WebContents::CreateParams new_contents_params(
      contents()->GetBrowserContext());
  new_contents_params.site_instance = SiteInstance::CreateForURL(
      contents()->GetBrowserContext(), initial_webui_url);
  std::unique_ptr<WebContents> new_web_contents(
      WebContents::Create(new_contents_params));
  NavigationControllerImpl& controller =
      static_cast<NavigationControllerImpl&>(new_web_contents->GetController());

  // Navigate to initial WebUI.
  {
    TestNavigationObserver navigation_observer(new_web_contents.get(), 1);
    controller.LoadURLWithParams(
        NavigationController::LoadURLParams(initial_webui_url));
    navigation_observer.Wait();
    EXPECT_TRUE(navigation_observer.last_navigation_succeeded());
  }

  // Now try to navigate away to about:blank
  // With feature flag enabled, this should succeed.
  ASSERT_EQ(NavigateToURL(new_web_contents.get(), GURL(url::kAboutBlankURL)),
            ShouldAllowDebug());
}
#endif

}  // namespace content
