// 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 "base/test/scoped_feature_list.h"
#include "content/browser/renderer_host/render_frame_host_manager_browsertest.h"
#include "content/browser/web_contents/web_contents_impl.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_utils.h"
#include "content/public/test/test_utils.h"
#include "content/shell/browser/shell.h"
#include "content/test/content_browser_test_utils_internal.h"
#include "content/test/render_document_feature.h"
#include "net/test/embedded_test_server/embedded_test_server.h"

namespace content {

// Fully enable RenderDocument for speculative RenderViewHost tests. This is
// done by parameterizing this test suite with a single test parameter.
using SpeculativeRenderViewHostTest = RenderFrameHostManagerTest;

// Check that FrameTree::speculative_render_view_host_ is used correctly.
IN_PROC_BROWSER_TEST_P(SpeculativeRenderViewHostTest,
                       SameSiteInstanceGroupCase) {
  // Disable BFCache because otherwise the BrowsingInstances will be proactively
  // swapped and the navigation will not be same-SiteInstanceGroup, which is
  // the only case a speculative RenderViewHost is used.
  DisableBackForwardCache(
      BackForwardCacheImpl::TEST_ASSUMES_NO_RENDER_FRAME_CHANGE);

  StartEmbeddedServer();

  // Open a page in SiteInstanceGroup A.
  GURL url1(embedded_test_server()->GetURL("a.com", "/title1.html"));
  EXPECT_TRUE(NavigateToURL(shell(), url1));

  // Open a different page in SiteInstanceGroup A. Stop the navigation when
  // there's a speculative RenderFrameHost for page 2.
  WebContentsImpl* web_contents =
      static_cast<WebContentsImpl*>(shell()->web_contents());
  GURL url2(embedded_test_server()->GetURL("a.com", "/title2.html"));
  TestNavigationManager navigation(web_contents, url2);
  shell()->LoadURL(url2);
  navigation.WaitForSpeculativeRenderFrameHostCreation();

  // Check that the speculative RenderViewHost exists, and that it matches the
  // RenderViewHost of the speculative RenderFrameHost.
  FrameTree& frame_tree = web_contents->GetPrimaryFrameTree();
  FrameTreeNode* root = frame_tree.root();
  RenderFrameHostImpl* speculative_rfh =
      root->render_manager()->speculative_frame_host();
  EXPECT_TRUE(speculative_rfh);
  EXPECT_TRUE(frame_tree.speculative_render_view_host());
  RenderViewHostImpl* speculative_rvh = speculative_rfh->render_view_host();
  EXPECT_EQ(speculative_rvh, frame_tree.speculative_render_view_host());
  EXPECT_EQ(speculative_rvh->rvh_map_id(),
            frame_tree.GetRenderViewHostMapId(
                speculative_rfh->GetSiteInstance()->group()));

  // The speculative RenderViewHost and the current main frame RenderViewHost
  // should have the same RenderViewHostMap ID, since they're in the same
  // SiteInstanceGroup, but are two different RenderViewHosts.
  EXPECT_NE(speculative_rvh, root->current_frame_host()->render_view_host());
  EXPECT_EQ(speculative_rvh->rvh_map_id(),
            root->current_frame_host()->render_view_host()->rvh_map_id());

  // Expect that the speculative RenderViewHost was swapped in correctly.
  EXPECT_TRUE(navigation.WaitForNavigationFinished());
  EXPECT_EQ(root->current_frame_host()->render_view_host(), speculative_rvh);
  EXPECT_FALSE(frame_tree.speculative_render_view_host());
}

// Test that cross-SiteInstanceGroup navigations do not yet use speculative
// RenderViewHosts.
IN_PROC_BROWSER_TEST_P(SpeculativeRenderViewHostTest,
                       CrossSiteInstanceGroupNavigation) {
  StartEmbeddedServer();

  // Open a page in SiteInstanceGroup A.
  GURL url1(embedded_test_server()->GetURL("a.com", "/title1.html"));
  EXPECT_TRUE(NavigateToURL(shell(), url1));

  // Open a different page in SiteInstanceGroup B. Stop the navigation when
  // there's a speculative RenderFrameHost for page 2.
  WebContentsImpl* web_contents =
      static_cast<WebContentsImpl*>(shell()->web_contents());
  GURL url2(embedded_test_server()->GetURL("b.com", "/title1.html"));
  TestNavigationManager navigation(web_contents, url2);
  shell()->LoadURL(url2);
  navigation.WaitForSpeculativeRenderFrameHostCreation();

  // There should be a speculative RenderFrameHost with a RenderViewHost, but
  // the RenderViewHost should not be speculative.
  FrameTree& frame_tree = web_contents->GetPrimaryFrameTree();
  RenderFrameHostImpl* speculative_rfh =
      frame_tree.root()->render_manager()->speculative_frame_host();
  EXPECT_TRUE(speculative_rfh);
  EXPECT_TRUE(speculative_rfh->render_view_host());
  EXPECT_FALSE(frame_tree.speculative_render_view_host());

  EXPECT_TRUE(navigation.WaitForNavigationFinished());
}

// Check that FrameTree::speculative_render_view_host_ is removed when the
// navigation is cancelled before completing.
IN_PROC_BROWSER_TEST_P(SpeculativeRenderViewHostTest,
                       SpeculativeRenderViewHostCreatedNotUsed) {
  // Disable BFCache because otherwise the BrowsingInstances will be proactively
  // swapped and the navigation will not be same-SiteInstanceGroup, which is
  // the only case a speculative RenderViewHost is used.
  DisableBackForwardCache(
      BackForwardCacheImpl::TEST_ASSUMES_NO_RENDER_FRAME_CHANGE);

  StartEmbeddedServer();

  // Open a page in SiteInstanceGroup A.
  GURL url1(embedded_test_server()->GetURL("a.com", "/title1.html"));
  EXPECT_TRUE(NavigateToURL(shell(), url1));

  // Open a different page in SiteInstanceGroup A. Stop the navigation when
  // there's a speculative RenderFrameHost for page2.
  WebContentsImpl* web_contents =
      static_cast<WebContentsImpl*>(shell()->web_contents());
  GURL url2(embedded_test_server()->GetURL("a.com", "/title2.html"));
  TestNavigationManager navigation(web_contents, url2);
  shell()->LoadURL(url2);
  navigation.WaitForSpeculativeRenderFrameHostCreation();
  FrameTree& frame_tree = web_contents->GetPrimaryFrameTree();
  EXPECT_TRUE(frame_tree.speculative_render_view_host());

  // Cancel the navigation while there's still a speculative RenderFrameHost and
  // RenderViewHost.
  FrameTreeNode* root = frame_tree.root();
  root->navigator().CancelNavigation(
      root, NavigationDiscardReason::kExplicitCancellation);

  // Expect that the navigation finishes but doesn't commit. There should no
  // longer be a speculative RenderViewHost or RenderFrameHost. The current
  // RenderFrameHost's RenderViewHost should still be around.
  EXPECT_TRUE(navigation.WaitForNavigationFinished());
  EXPECT_FALSE(navigation.was_committed());
  EXPECT_FALSE(frame_tree.speculative_render_view_host());
  EXPECT_FALSE(root->render_manager()->speculative_frame_host());
  EXPECT_TRUE(root->current_frame_host()->render_view_host());
}

// With RenderDocument, a same-SiteInstanceGroup history navigation page should
// not be reusing RenderViewHosts, so check that a new speculative
// RenderViewHost is being created for history navigations.
IN_PROC_BROWSER_TEST_P(SpeculativeRenderViewHostTest, HistoryNavigation) {
  // Disable BFCache because otherwise the BrowsingInstances will be proactively
  // swapped and the navigation will not be same-SiteInstanceGroup, which is
  // the only case a speculative RenderViewHost is used.
  DisableBackForwardCache(
      BackForwardCacheImpl::TEST_ASSUMES_NO_RENDER_FRAME_CHANGE);

  StartEmbeddedServer();

  // Open two pages in SiteInstanceGroup A, one after the other..
  GURL url1(embedded_test_server()->GetURL("a.com", "/title1.html"));
  EXPECT_TRUE(NavigateToURL(shell(), url1));
  GURL url2(embedded_test_server()->GetURL("a.com", "/title2.html"));
  EXPECT_TRUE(NavigateToURL(shell(), url2));

  // Go back to the first page, but pause before it commits.
  WebContentsImpl* web_contents =
      static_cast<WebContentsImpl*>(shell()->web_contents());
  NavigationControllerImpl& controller =
      static_cast<NavigationControllerImpl&>(web_contents->GetController());
  TestNavigationManager navigation(web_contents, url1);
  ASSERT_TRUE(controller.CanGoBack());
  controller.GoBack();
  navigation.WaitForSpeculativeRenderFrameHostCreation();

  // Check that the speculative RenderViewHost exists, and that it matches the
  // RenderViewHost of the speculative RenderFrameHost.
  RenderFrameHostImpl* speculative_rfh = web_contents->GetPrimaryFrameTree()
                                             .root()
                                             ->render_manager()
                                             ->speculative_frame_host();
  FrameTree& frame_tree = web_contents->GetPrimaryFrameTree();
  EXPECT_TRUE(speculative_rfh);
  RenderViewHostImpl* speculative_rvh = speculative_rfh->render_view_host();
  EXPECT_TRUE(frame_tree.speculative_render_view_host());
  EXPECT_EQ(speculative_rvh->rvh_map_id(),
            frame_tree.GetRenderViewHostMapId(
                speculative_rfh->GetSiteInstance()->group()));
  EXPECT_NE(frame_tree.root()->current_frame_host()->render_view_host(),
            speculative_rvh);

  // Expect that the former speculative RenderViewHost is now the RenderViewHost
  // of the current main frame RenderFrameHost.
  EXPECT_TRUE(navigation.WaitForNavigationFinished());
  EXPECT_EQ(frame_tree.root()->current_frame_host()->render_view_host(),
            speculative_rvh);
}

// Check that opener proxies are unaffected in a navigation that has a
// speculative RenderViewHost. When navigating a tab with an opener proxy, the
// opener proxy to the tab shouldn't change when the page that was opened
// navigates.
IN_PROC_BROWSER_TEST_P(SpeculativeRenderViewHostTest, OpenerProxies) {
  // Disable BFCache because otherwise the BrowsingInstances will be proactively
  // swapped and the navigation will not be same-SiteInstanceGroup, which is
  // the only case a speculative RenderViewHost is used.
  DisableBackForwardCache(
      BackForwardCacheImpl::TEST_ASSUMES_NO_RENDER_FRAME_CHANGE);

  StartEmbeddedServer();
  IsolateAllSitesForTesting(base::CommandLine::ForCurrentProcess());

  // Open page A in a tab. From there, window.open page B in another tab.
  GURL url1(embedded_test_server()->GetURL("a.com", "/title1.html"));
  EXPECT_TRUE(NavigateToURL(shell(), url1));
  WebContentsImpl* contents_a =
      static_cast<WebContentsImpl*>(shell()->web_contents());
  FrameTreeNode* root_a = contents_a->GetPrimaryFrameTree().root();
  RenderViewHost* rvh_a = root_a->current_frame_host()->render_view_host();

  GURL url2(embedded_test_server()->GetURL("b.com", "/title1.html"));
  ShellAddedObserver new_shell_observer;
  EXPECT_TRUE(ExecJs(root_a, JsReplace("window.open($1)", url2)));
  Shell* new_shell = new_shell_observer.GetShell();
  WebContentsImpl* contents_b =
      static_cast<WebContentsImpl*>(new_shell->web_contents());
  EXPECT_TRUE(WaitForLoadStop(contents_b));

  // Store the current proxies.
  FrameTreeNode* root_b = contents_b->GetPrimaryFrameTree().root();
  RenderViewHost* rvh_b = root_b->current_frame_host()->render_view_host();
  SiteInstanceGroup* site_instance_group_a =
      root_a->current_frame_host()->GetSiteInstance()->group();
  SiteInstanceGroup* site_instance_group_b =
      root_b->current_frame_host()->GetSiteInstance()->group();
  RenderFrameProxyHost* proxy_of_root_a_in_sig_b =
      root_a->current_frame_host()
          ->browsing_context_state()
          ->GetRenderFrameProxyHost(site_instance_group_b);
  RenderFrameProxyHost* proxy_of_root_b_in_sig_a =
      root_b->current_frame_host()
          ->browsing_context_state()
          ->GetRenderFrameProxyHost(site_instance_group_a);

  // The RenderViewHosts of the proxies and the pages should be different.
  EXPECT_NE(proxy_of_root_a_in_sig_b->GetRenderViewHost(), rvh_a);
  EXPECT_NE(proxy_of_root_b_in_sig_a->GetRenderViewHost(), rvh_b);

  // Do a same-SiteInstanceGroup navigation on B's tab. Check that a speculative
  // RenderViewHost is created. See SpeculativeRenderViewHost for more detailed
  // checks on the speculative RenderViewHost.
  GURL url3(embedded_test_server()->GetURL("b.com", "/title2.html"));
  TestNavigationManager navigation(contents_b, url3);
  new_shell->LoadURL(url3);
  navigation.WaitForSpeculativeRenderFrameHostCreation();
  EXPECT_TRUE(contents_b->GetPrimaryFrameTree().speculative_render_view_host());
  EXPECT_TRUE(navigation.WaitForNavigationFinished());

  // Check that the proxies are still the same.
  EXPECT_EQ(site_instance_group_b,
            root_b->current_frame_host()->GetSiteInstance()->group());
  EXPECT_EQ(proxy_of_root_a_in_sig_b,
            root_a->current_frame_host()
                ->browsing_context_state()
                ->GetRenderFrameProxyHost(site_instance_group_b));
  EXPECT_EQ(proxy_of_root_b_in_sig_a,
            root_b->current_frame_host()
                ->browsing_context_state()
                ->GetRenderFrameProxyHost(site_instance_group_a));

  // The RenderViewHosts of the proxies and the pages should still be different.
  EXPECT_NE(proxy_of_root_a_in_sig_b->GetRenderViewHost(),
            root_a->current_frame_host()->render_view_host());
  EXPECT_NE(proxy_of_root_b_in_sig_a->GetRenderViewHost(),
            root_b->current_frame_host()->render_view_host());

  // Since page B had a navigation, that should result in a new RenderViewHost
  // being created. Page A didn't change, so the RenderViewHost should be the
  // same as before B navigated.
  EXPECT_NE(rvh_b, root_b->current_frame_host()->render_view_host());
  EXPECT_EQ(rvh_a, root_a->current_frame_host()->render_view_host());
}

// Make sure any subframe proxies are deleted after a navigation that uses
// speculative RenderViewHosts.
IN_PROC_BROWSER_TEST_P(SpeculativeRenderViewHostTest, SubframeProxies) {
  // Disable BFCache because otherwise the BrowsingInstances will be proactively
  // swapped and the navigation will not be same-SiteInstanceGroup, which is
  // the only case a speculative RenderViewHost is used.
  DisableBackForwardCache(
      BackForwardCacheImpl::TEST_ASSUMES_NO_RENDER_FRAME_CHANGE);

  StartEmbeddedServer();

  IsolateAllSitesForTesting(base::CommandLine::ForCurrentProcess());
  // Open page A with a cross site subframe.
  GURL url1(embedded_test_server()->GetURL(
      "a.com", "/cross_site_iframe_factory.html?a(b)"));
  EXPECT_TRUE(NavigateToURL(shell(), url1));

  // Expect that there are proxies to the subframe.
  WebContentsImpl* web_contents =
      static_cast<WebContentsImpl*>(shell()->web_contents());
  FrameTreeNode* root_a = web_contents->GetPrimaryFrameTree().root();
  FrameTreeNode* subframe = root_a->current_frame_host()->child_at(0);
  SiteInstanceGroup* site_instance_group_b =
      subframe->current_frame_host()->GetSiteInstance()->group();
  EXPECT_TRUE(root_a->current_frame_host()
                  ->browsing_context_state()
                  ->GetRenderFrameProxyHost(site_instance_group_b));

  // Open a different page, without subframes, in SiteInstanceGroup A. Stop the
  // navigation when there's a speculative RenderFrameHost for page 2.
  GURL url2(embedded_test_server()->GetURL("a.com", "/title2.html"));
  TestNavigationManager navigation(web_contents, url2);
  shell()->LoadURL(url2);
  navigation.WaitForSpeculativeRenderFrameHostCreation();

  // Make sure a speculative RenderViewHost is involved.
  FrameTree& frame_tree = web_contents->GetPrimaryFrameTree();
  EXPECT_TRUE(frame_tree.speculative_render_view_host());

  // Complete the navigation. Expect that there is no longer a speculative
  // RenderViewHost, or any proxies.
  EXPECT_TRUE(navigation.WaitForNavigationFinished());
  EXPECT_FALSE(frame_tree.speculative_render_view_host());
  EXPECT_EQ(0u, root_a->current_frame_host()
                    ->browsing_context_state()
                    ->proxy_hosts()
                    .size());
}

// Crash a page, then navigate to a same-site URL. The new navigation should use
// a new RenderFrameHost and speculative RenderViewHost.
// TODO(crbug.com/40228869): This navigation is not using a speculative
// RenderViewHost, when it should be. Fix and enable this test.
IN_PROC_BROWSER_TEST_P(SpeculativeRenderViewHostTest,
                       DISABLED_CrashedRenderFrameHost) {
  // Disable BFCache because otherwise the BrowsingInstances will be proactively
  // swapped and the navigation will not be same-SiteInstanceGroup, which is
  // the only case a speculative RenderViewHost is used.
  DisableBackForwardCache(
      BackForwardCacheImpl::TEST_ASSUMES_NO_RENDER_FRAME_CHANGE);

  StartEmbeddedServer();

  // Open a page in SiteInstanceGroup A.
  GURL url(embedded_test_server()->GetURL("a.com", "/title1.html"));
  EXPECT_TRUE(NavigateToURL(shell(), url));

  WebContentsImpl* web_contents =
      static_cast<WebContentsImpl*>(shell()->web_contents());
  TestNavigationObserver observer(web_contents);

  // Crash the page.
  RenderProcessHost* process =
      web_contents->GetPrimaryMainFrame()->GetProcess();
  RenderProcessHostWatcher crash_observer(
      process, RenderProcessHostWatcher::WATCH_FOR_PROCESS_EXIT);
  process->Shutdown(0);
  crash_observer.Wait();

  // Navigate to a same SiteInstanceGroup URL.
  GURL url2(embedded_test_server()->GetURL("a.com", "/title2.html"));
  TestNavigationManager navigation(web_contents, url2);
  shell()->LoadURL(url);
  EXPECT_TRUE(navigation.WaitForRequestStart());

  // Check that a speculative RenderViewHost is used for the navigation.
  FrameTree& frame_tree = web_contents->GetPrimaryFrameTree();
  RenderFrameHostImpl* speculative_rfh =
      frame_tree.root()->render_manager()->speculative_frame_host();
  EXPECT_TRUE(speculative_rfh);
  EXPECT_TRUE(frame_tree.speculative_render_view_host());
  RenderViewHostImpl* speculative_rvh = speculative_rfh->render_view_host();
  EXPECT_EQ(speculative_rvh->rvh_map_id(),
            frame_tree.GetRenderViewHostMapId(
                speculative_rfh->GetSiteInstance()->group()));

  // Expect that the speculative RenderViewHost was swapped in correctly.
  EXPECT_TRUE(navigation.WaitForNavigationFinished());
  EXPECT_EQ(frame_tree.root()->current_frame_host()->render_view_host(),
            speculative_rvh);
  EXPECT_FALSE(frame_tree.speculative_render_view_host());
}

// Check that FrameTree::ForEachRenderViewHost includes the speculative
// RenderViewHost when one exists.
IN_PROC_BROWSER_TEST_P(SpeculativeRenderViewHostTest, ForEachRenderViewHost) {
  // Disable BFCache because otherwise the BrowsingInstances will be proactively
  // swapped and the navigation will not be same-SiteInstanceGroup, which is
  // the only case a speculative RenderViewHost is used.
  DisableBackForwardCache(
      BackForwardCacheImpl::TEST_ASSUMES_NO_RENDER_FRAME_CHANGE);

  StartEmbeddedServer();

  // Open a page in SiteInstanceGroup A.
  GURL url1(embedded_test_server()->GetURL("a.com", "/title1.html"));
  EXPECT_TRUE(NavigateToURL(shell(), url1));

  // Navigate to a different page in SiteInstanceGroup A. Stop the navigation
  // when there's a speculative RenderFrameHost for page 2.
  WebContentsImpl* web_contents =
      static_cast<WebContentsImpl*>(shell()->web_contents());
  GURL url2(embedded_test_server()->GetURL("a.com", "/title2.html"));
  TestNavigationManager navigation(web_contents, url2);
  shell()->LoadURL(url2);
  navigation.WaitForSpeculativeRenderFrameHostCreation();

  // Check that the speculative RenderViewHost exists, and that it matches the
  // RenderViewHost of the speculative RenderFrameHost.
  FrameTree& frame_tree = web_contents->GetPrimaryFrameTree();
  RenderFrameHostImpl* speculative_rfh =
      frame_tree.root()->render_manager()->speculative_frame_host();
  EXPECT_TRUE(speculative_rfh);
  RenderViewHostImpl* speculative_rvh =
      frame_tree.speculative_render_view_host();
  EXPECT_TRUE(speculative_rvh);

  // Iterate over all RenderViewHosts, which should include the speculative
  // RenderViewHost.
  bool is_speculative = false;
  frame_tree.ForEachRenderViewHost(
      [&is_speculative, speculative_rvh](RenderViewHostImpl* rvh) {
        if (rvh->is_speculative()) {
          EXPECT_EQ(rvh, speculative_rvh);
          is_speculative = true;
        }
      });
  EXPECT_TRUE(is_speculative);

  // Once the navigation finishes, there should no longer be a speculative
  // RenderViewHost to iterate over.
  EXPECT_TRUE(navigation.WaitForNavigationFinished());
  is_speculative = false;
  frame_tree.ForEachRenderViewHost([&is_speculative](RenderViewHostImpl* rvh) {
    if (rvh->is_speculative()) {
      is_speculative = true;
    }
  });
  EXPECT_FALSE(is_speculative);
}

INSTANTIATE_TEST_SUITE_P(
    All,
    SpeculativeRenderViewHostTest,
    testing::ValuesIn(RenderDocumentFeatureFullyEnabled()));

}  // namespace content
