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

#include <limits>
#include <utility>

#include "base/containers/flat_map.h"
#include "base/containers/span.h"
#include "base/functional/bind.h"
#include "base/functional/callback.h"
#include "base/memory/raw_ptr.h"
#include "base/memory/ref_counted_memory.h"
#include "base/run_loop.h"
#include "base/strings/string_util.h"
#include "base/threading/thread_restrictions.h"
#include "base/unguessable_token.h"
#include "build/build_config.h"
#include "content/browser/webui/web_ui_controller_factory_registry.h"
#include "content/browser/webui/web_ui_impl.h"
#include "content/public/browser/browser_context.h"
#include "content/public/browser/render_frame_host.h"
#include "content/public/browser/render_process_host.h"
#include "content/public/browser/render_view_host.h"
#include "content/public/browser/web_contents.h"
#include "content/public/browser/web_ui.h"
#include "content/public/browser/web_ui_controller.h"
#include "content/public/browser/web_ui_controller_interface_binder.h"
#include "content/public/browser/web_ui_data_source.h"
#include "content/public/common/bindings_policy.h"
#include "content/public/common/content_client.h"
#include "content/public/common/referrer.h"
#include "content/public/common/url_constants.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/no_renderer_crashes_assertion.h"
#include "content/public/test/scoped_web_ui_controller_factory_registration.h"
#include "content/public/test/test_navigation_observer.h"
#include "content/public/test/test_utils.h"
#include "content/shell/browser/shell.h"
#include "content/test/data/web_ui_ts_test.test-mojom.h"
#include "content/test/data/web_ui_ts_test_types.test-mojom.h"
#include "content/test/grit/web_ui_mojo_test_resources.h"
#include "content/test/grit/web_ui_mojo_test_resources_map.h"
#include "mojo/public/cpp/bindings/binder_map.h"
#include "mojo/public/cpp/bindings/pending_receiver.h"
#include "mojo/public/cpp/bindings/receiver.h"
#include "mojo/public/cpp/bindings/self_owned_receiver.h"
#include "mojo/public/mojom/base/time.mojom.h"
#include "third_party/blink/public/common/chrome_debug_urls.h"

namespace content {
namespace {

const char kMojoWebUiTsHost[] = "mojo-web-ui-ts";
const char kDummyWebUiHost[] = "dummy-web-ui";

class WebUITsMojoTestCacheImpl : public mojom::WebUITsMojoTestCache {
 public:
  explicit WebUITsMojoTestCacheImpl(
      mojo::PendingReceiver<mojom::WebUITsMojoTestCache> receiver)
      : receiver_(this, std::move(receiver)) {}

  ~WebUITsMojoTestCacheImpl() override = default;

  // mojom::WebUITsMojoTestCache overrides:
  void Put(const GURL& url, const std::string& contents) override {
    cache_[url] = contents;
  }

  void GetAll(GetAllCallback callback) override {
    std::vector<mojom::TsCacheItemPtr> items;
    for (const auto& entry : cache_)
      items.push_back(mojom::TsCacheItem::New(entry.first, entry.second));
    std::move(callback).Run(std::move(items));
  }

  void Echo(
      std::optional<bool> optional_bool,
      std::optional<uint8_t> optional_uint8,
      std::optional<mojom::TestEnum> optional_enum,
      mojom::OptionalNumericsStructPtr optional_numerics,
      const std::vector<std::optional<bool>>& optional_bools,
      const std::vector<std::optional<uint32_t>>& optional_ints,
      const std::vector<std::optional<mojom::TestEnum>>& optional_enums,
      const base::flat_map<int32_t, std::optional<bool>>& bool_map,
      const base::flat_map<int32_t, std::optional<int32_t>>& int_map,
      const base::flat_map<int32_t, std::optional<mojom::TestEnum>>& enum_map,
      mojom::SimpleMappedTypePtr simple_mapped,
      mojom::NestedMappedTypePtr nested_mapped,
      mojom::StringDictPtr dict_ptr,
      EchoCallback callback) override {
    std::move(callback).Run(
        optional_bool.has_value() ? std::make_optional(!optional_bool.value())
                                  : std::nullopt,
        optional_uint8.has_value() ? std::make_optional(~optional_uint8.value())
                                   : std::nullopt,
        optional_enum.has_value() ? std::make_optional(mojom::TestEnum::kTwo)
                                  : std::nullopt,
        mojom::OptionalNumericsStruct::New(
            optional_numerics->optional_bool.has_value()
                ? std::make_optional(!optional_numerics->optional_bool.value())
                : std::nullopt,
            optional_numerics->optional_uint8.has_value()
                ? std::make_optional(~optional_numerics->optional_uint8.value())
                : std::nullopt,
            optional_numerics->optional_enum.has_value()
                ? std::make_optional(mojom::TestEnum::kTwo)
                : std::nullopt),
        optional_bools, optional_ints, optional_enums, bool_map, int_map,
        enum_map, simple_mapped->Clone(), nested_mapped->Clone(),
        dict_ptr ? dict_ptr->Clone() : nullptr);
  }

  void EchoTypemaps(base::Time time,
                    const base::UnguessableToken& token,
                    EchoTypemapsCallback cb) override {
    std::move(cb).Run(time, token);
  }

  void EchoOptionalTypemaps(mojom::OptionalTypemapPtr container,
                            EchoOptionalTypemapsCallback cb) override {
    std::move(cb).Run(container->Clone());
  }

  void AddStringWrapper(
      mojo::PendingRemote<mojom::StringWrapper> string_wrapper) override {
    string_wrapper_list_.push_back(
        mojo::Remote<mojom::StringWrapper>(std::move(string_wrapper)));
  }

  void GetStringWrapperList(GetStringWrapperListCallback cb) override {
    std::vector<mojo::PendingRemote<mojom::StringWrapper>> string_wrapper_list;
    for (auto& string_wrapper : string_wrapper_list_) {
      mojo::PendingRemote<mojom::StringWrapper> cloned_string_wrapper;
      string_wrapper->Clone(
          cloned_string_wrapper.InitWithNewPipeAndPassReceiver());
      string_wrapper_list.emplace_back(std::move(cloned_string_wrapper));
    }

    std::move(cb).Run(std::move(string_wrapper_list));
  }

  void GetAssociatedReceiver(GetAssociatedReceiverCallback cb) override {
    CHECK(!test_client_.is_bound()) << "This method can only be called once";
    std::move(cb).Run(test_client_.BindNewEndpointAndPassReceiver());
    test_client_->BlockUntilBound();
  }

  void Ping(PingCallback cb) override { std::move(cb).Run("ping"); }

 private:
  mojo::Receiver<mojom::WebUITsMojoTestCache> receiver_;
  std::map<GURL, std::string> cache_;
  std::vector<mojo::Remote<mojom::StringWrapper>> string_wrapper_list_;
  mojo::AssociatedRemote<mojom::TestAssociatedClient> test_client_;
};

// WebUIController that sets up mojo bindings.
class TestWebUIController : public WebUIController {
 public:
  explicit TestWebUIController(WebUI* web_ui,
                               BindingsPolicySet bindings = BindingsPolicySet(
                                   {BindingsPolicyValue::kMojoWebUi}))
      : WebUIController(web_ui) {
    web_ui->SetBindings(bindings);
    {
      WebUIDataSource* data_source = WebUIDataSource::CreateAndAdd(
          web_ui->GetWebContents()->GetBrowserContext(), kMojoWebUiTsHost);
      data_source->OverrideContentSecurityPolicy(
          network::mojom::CSPDirectiveName::ScriptSrc,
          "script-src chrome://resources 'self' 'unsafe-eval';");
      data_source->DisableTrustedTypesCSP();
      data_source->AddResourcePaths(kWebUiMojoTestResources);
      data_source->SetDefaultResource(IDR_WEB_UI_MOJO_TS_HTML);
    }
    {
      WebUIDataSource* data_source = WebUIDataSource::CreateAndAdd(
          web_ui->GetWebContents()->GetBrowserContext(), kDummyWebUiHost);
      data_source->SetRequestFilter(
          base::BindRepeating([](const std::string& path) { return true; }),
          base::BindRepeating([](const std::string& id,
                                 WebUIDataSource::GotDataCallback callback) {
            std::move(callback).Run(new base::RefCountedString);
          }));
    }
  }

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

 protected:
  std::unique_ptr<WebUITsMojoTestCacheImpl> ts_cache_;
};

// TestWebUIController that can bind a WebUITsMojoTestCache interface when
// requested by the page.
class CacheTestWebUIController : public TestWebUIController {
 public:
  explicit CacheTestWebUIController(WebUI* web_ui)
      : TestWebUIController(web_ui) {}
  ~CacheTestWebUIController() override = default;

  void BindInterface(
      mojo::PendingReceiver<mojom::WebUITsMojoTestCache> receiver) {
    ts_cache_ = std::make_unique<WebUITsMojoTestCacheImpl>(std::move(receiver));
  }

  WEB_UI_CONTROLLER_TYPE_DECL();
};

WEB_UI_CONTROLLER_TYPE_IMPL(CacheTestWebUIController)

// WebUIControllerFactory that creates TestWebUIController.
class TestWebUIControllerFactory : public WebUIControllerFactory {
 public:
  TestWebUIControllerFactory()
      : registered_controllers_(
            {{"cache", base::BindRepeating(
                           &TestWebUIControllerFactory::CreateCacheController,
                           base::Unretained(this))},
             {"hybrid", base::BindRepeating(
                            &TestWebUIControllerFactory::CreateHybridController,
                            base::Unretained(this))},
             {"webui_bindings",
              base::BindRepeating(
                  &TestWebUIControllerFactory::CreateWebUIController,
                  base::Unretained(this))}}) {}

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

  std::unique_ptr<WebUIController> CreateWebUIControllerForURL(
      WebUI* web_ui,
      const GURL& url) override {
    if (!web_ui_enabled_ || !url.SchemeIs(kChromeUIScheme))
      return nullptr;

    auto it = registered_controllers_.find(url.GetQuery());
    if (it != registered_controllers_.end())
      return it->second.Run(web_ui);

    return std::make_unique<TestWebUIController>(web_ui);
  }

  WebUI::TypeID GetWebUIType(BrowserContext* browser_context,
                             const GURL& url) override {
    if (!web_ui_enabled_ || !url.SchemeIs(kChromeUIScheme))
      return WebUI::kNoWebUI;

    return reinterpret_cast<WebUI::TypeID>(1);
  }

  bool UseWebUIForURL(BrowserContext* browser_context,
                      const GURL& url) override {
    return GetWebUIType(browser_context, url) != WebUI::kNoWebUI;
  }

  void set_web_ui_enabled(bool enabled) { web_ui_enabled_ = enabled; }

 private:
  std::unique_ptr<WebUIController> CreateCacheController(WebUI* web_ui) {
    return std::make_unique<CacheTestWebUIController>(web_ui);
  }

  std::unique_ptr<WebUIController> CreateHybridController(WebUI* web_ui) {
    return std::make_unique<TestWebUIController>(web_ui,
                                                 kWebUIBindingsPolicySet);
  }

  std::unique_ptr<WebUIController> CreateWebUIController(WebUI* web_ui) {
    return std::make_unique<TestWebUIController>(
        web_ui, BindingsPolicySet({BindingsPolicyValue::kWebUi}));
  }

  bool web_ui_enabled_ = true;
  const base::flat_map<
      std::string,
      base::RepeatingCallback<std::unique_ptr<WebUIController>(WebUI*)>>
      registered_controllers_;
};

class StringWrapperImpl : public mojom::StringWrapper {
 public:
  StringWrapperImpl() = default;

  // mojom::StringWrapper
  void PutString(const std::string& item) override { item_ = item; }

  void GetString(GetStringCallback cb) override { std::move(cb).Run(item_); }

  void Clone(
      mojo::PendingReceiver<mojom::StringWrapper> clone_receiver) override {
    receivers_.Add(this, std::move(clone_receiver));
  }

  static void Create(mojo::PendingReceiver<mojom::StringWrapper> receiver) {
    mojo::MakeSelfOwnedReceiver(std::make_unique<StringWrapperImpl>(),
                                std::move(receiver));
  }

 private:
  std::string item_;
  mojo::ReceiverSet<mojom::StringWrapper> receivers_;
};

class TestWebUIContentBrowserClient
    : public ContentBrowserTestContentBrowserClient {
 public:
  TestWebUIContentBrowserClient() {}
  TestWebUIContentBrowserClient(const TestWebUIContentBrowserClient&) = delete;
  TestWebUIContentBrowserClient& operator=(
      const TestWebUIContentBrowserClient&) = delete;
  ~TestWebUIContentBrowserClient() override {}

  void RegisterBrowserInterfaceBindersForFrame(
      RenderFrameHost* render_frame_host,
      mojo::BinderMapWithContext<content::RenderFrameHost*>* map) override {
    RegisterWebUIControllerInterfaceBinder<mojom::WebUITsMojoTestCache,
                                           CacheTestWebUIController>(map);

    map->Add<content::mojom::StringWrapper>(
        &TestWebUIContentBrowserClient::BindStringWrapper);
  }

  static void BindStringWrapper(
      RenderFrameHost* render_frame_host,
      mojo::PendingReceiver<mojom::StringWrapper> receiver) {
    StringWrapperImpl::Create(std::move(receiver));
  }
};

class WebUIMojoTest : public ContentBrowserTest {
 public:
  WebUIMojoTest() = default;

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

  TestWebUIControllerFactory* factory() { return &factory_; }

  void NavigateWithNewWebUI(const std::string& path) {
    // Load a dummy WebUI URL first so that a new WebUI is set up when we load
    // the URL we're actually interested in.
    EXPECT_TRUE(NavigateToURL(shell(), GetWebUIURL(kDummyWebUiHost)));
    EXPECT_TRUE(NavigateToURL(
        shell(), GetWebUIURL(GetMojoWebUiHost() + std::string("/") + path)));
  }

  // Run |script| and return a boolean result.
  bool RunBoolFunction(const std::string& script) {
    return EvalJs(shell()->web_contents(), script).ExtractBool();
  }

 protected:
  std::string GetMojoWebUiHost() { return kMojoWebUiTsHost; }

  void SetUpOnMainThread() override {
    client_ = std::make_unique<TestWebUIContentBrowserClient>();
  }

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

 private:
  TestWebUIControllerFactory factory_;
  content::ScopedWebUIControllerFactoryRegistration factory_registration_{
      &factory_};
  std::unique_ptr<TestWebUIContentBrowserClient> client_;
};

// Loads a WebUI page that contains Mojo JS bindings and verifies a message
// round-trip between the page and the browser.
IN_PROC_BROWSER_TEST_F(WebUIMojoTest, EndToEndCommunication) {
  // Load a dummy page in the initial RenderFrameHost.  The initial
  // RenderFrameHost is created by the test harness prior to installing
  // TestWebUIContentBrowserClient in WebUIMojoTest::SetUpOnMainThread().  If we
  // were to navigate that initial RFH to WebUI directly, it would get reused,
  // but it wouldn't have the test's browser interface binders (registered via
  // TestWebUIContentBrowserClient::RegisterBrowserInterfaceBindersForFrame() at
  // RFH creation time).  Navigating the initial RFH to some other page forces
  // the subsequent WebUI navigation to create a new RenderFrameHost, and by
  // this time, TestWebUIContentBrowserClient will take effect on that new RFH.
  EXPECT_TRUE(NavigateToURL(shell(), GURL("data:,foo")));

  GURL kTestUrl(GetWebUIURL(GetMojoWebUiHost() + "/?cache"));
  const std::string kTestScript = "runTest();";
  EXPECT_TRUE(NavigateToURL(shell(), kTestUrl));
  EXPECT_EQ(true, EvalJs(shell()->web_contents(), kTestScript));

  // Check that a second shell works correctly.
  Shell* other_shell = CreateBrowser();
  EXPECT_TRUE(WaitForLoadStop(other_shell->web_contents()));
  EXPECT_TRUE(NavigateToURL(other_shell, kTestUrl));
  EXPECT_EQ(true, EvalJs(other_shell->web_contents(), kTestScript));

  // Close the second shell and wait until the second shell exits.
  RenderFrameHostWrapper wrapper(
      other_shell->web_contents()->GetPrimaryMainFrame());
  other_shell->Close();
  EXPECT_TRUE(wrapper.WaitUntilRenderFrameDeleted());

  // Check that a third shell works correctly, even if we force it to share a
  // process with the first shell, by forcing an artificially low process
  // limit.
  RenderProcessHost::SetMaxRendererProcessCount(1);

  // Subtle: provide an explicit initial SiteInstance, since otherwise the WebUI
  // will stay in the initial RFH's process and avoid process reuse needed for
  // this test.
  other_shell = Shell::CreateNewWindow(
      shell()->web_contents()->GetBrowserContext(), GURL(),
      SiteInstance::CreateForURL(shell()->web_contents()->GetBrowserContext(),
                                 kTestUrl),
      gfx::Size());
  EXPECT_TRUE(NavigateToURL(other_shell, kTestUrl));
  EXPECT_EQ(shell()->web_contents()->GetPrimaryMainFrame()->GetProcess(),
            other_shell->web_contents()->GetPrimaryMainFrame()->GetProcess());
  EXPECT_EQ(true, EvalJs(other_shell->web_contents(), kTestScript));
}

// Disabled due to flakiness: crbug.com/860385.
#if BUILDFLAG(IS_ANDROID)
#define MAYBE_NativeMojoAvailable DISABLED_NativeMojoAvailable
#else
#define MAYBE_NativeMojoAvailable NativeMojoAvailable
#endif
IN_PROC_BROWSER_TEST_F(WebUIMojoTest, MAYBE_NativeMojoAvailable) {
  // Mojo bindings should be enabled.
  NavigateWithNewWebUI("web_ui_mojo_native.html");
  EXPECT_TRUE(RunBoolFunction("isNativeMojoAvailable()"));

  // Now navigate again with normal WebUI bindings and ensure chrome.send is
  // available.
  NavigateWithNewWebUI("web_ui_mojo_native.html?webui_bindings");
  EXPECT_FALSE(RunBoolFunction("isNativeMojoAvailable()"));

  // Now navigate again both WebUI and Mojo bindings and ensure chrome.send is
  // available.
  NavigateWithNewWebUI("web_ui_mojo_native.html?hybrid");
  EXPECT_TRUE(RunBoolFunction("isNativeMojoAvailable()"));

  // Now navigate again with WebUI disabled and ensure the native bindings are
  // not available.
  factory()->set_web_ui_enabled(false);
  NavigateWithNewWebUI("web_ui_mojo_native.html?hybrid");
  EXPECT_FALSE(RunBoolFunction("isNativeMojoAvailable()"));
}

// Disabled due to flakiness: crbug.com/860385.
#if BUILDFLAG(IS_ANDROID)
#define MAYBE_ChromeSendAvailable DISABLED_ChromeSendAvailable
#else
#define MAYBE_ChromeSendAvailable ChromeSendAvailable
#endif
IN_PROC_BROWSER_TEST_F(WebUIMojoTest, MAYBE_ChromeSendAvailable) {
  // chrome.send is not available on mojo-only WebUIs.
  NavigateWithNewWebUI("web_ui_mojo_native.html");
  EXPECT_FALSE(RunBoolFunction("isChromeSendAvailable()"));

  // Now navigate again with normal WebUI bindings and ensure chrome.send is
  // available.
  NavigateWithNewWebUI("web_ui_mojo_native.html?webui_bindings");
  EXPECT_TRUE(RunBoolFunction("isChromeSendAvailable()"));

  // Now navigate again both WebUI and Mojo bindings and ensure chrome.send is
  // available.
  NavigateWithNewWebUI("web_ui_mojo_native.html?hybrid");
  EXPECT_TRUE(RunBoolFunction("isChromeSendAvailable()"));

  // Now navigate again with WebUI disabled and ensure that chrome.send is
  // not available.
  factory()->set_web_ui_enabled(false);
  NavigateWithNewWebUI("web_ui_mojo_native.html?hybrid");
  EXPECT_FALSE(RunBoolFunction("isChromeSendAvailable()"));
}

// TODO(crbug.com/440535492): Flaky on Win dbg. Re-enable this test.
#if BUILDFLAG(IS_WIN) && !defined(NDEBUG)
#define MAYBE_ChromeSendAvailable_AfterCrash \
  DISABLED_ChromeSendAvailable_AfterCrash
#else
#define MAYBE_ChromeSendAvailable_AfterCrash ChromeSendAvailable_AfterCrash
#endif
IN_PROC_BROWSER_TEST_F(WebUIMojoTest, MAYBE_ChromeSendAvailable_AfterCrash) {
  GURL test_url(GetWebUIURL(GetMojoWebUiHost() +
                            "/web_ui_mojo_native.html?webui_bindings"));

  // Navigate with normal WebUI bindings and ensure chrome.send is available.
  EXPECT_TRUE(NavigateToURL(shell(), test_url));
  EXPECT_TRUE(EvalJs(shell(), "isChromeSendAvailable()").ExtractBool());

  WebUIImpl* web_ui = static_cast<WebUIImpl*>(
      shell()->web_contents()->GetPrimaryMainFrame()->GetWebUI());

  // Simulate a crash on the page.
  content::ScopedAllowRendererCrashes allow_renderer_crashes(shell());
  RenderProcessHostWatcher crash_observer(
      shell()->web_contents(),
      RenderProcessHostWatcher::WATCH_FOR_PROCESS_EXIT);
  shell()->web_contents()->GetController().LoadURL(
      GURL(blink::kChromeUICrashURL), content::Referrer(),
      ui::PAGE_TRANSITION_TYPED, std::string());
  crash_observer.Wait();
  EXPECT_FALSE(web_ui->GetRemoteForTest().is_bound());

  // Now navigate again both WebUI and Mojo bindings and ensure chrome.send is
  // available.
  EXPECT_TRUE(NavigateToURL(shell(), test_url));
  EXPECT_TRUE(EvalJs(shell(), "isChromeSendAvailable()").ExtractBool());
  // The RenderFrameHost has been replaced after the crash, so get web_ui again.
  web_ui = static_cast<WebUIImpl*>(
      shell()->web_contents()->GetPrimaryMainFrame()->GetWebUI());
  EXPECT_TRUE(web_ui->GetRemoteForTest().is_bound());
}

}  // namespace
}  // namespace content
