// Copyright 2016 The Chromium Authors. All rights reserved.
// 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 "content/browser/frame_host/frame_tree_node.h"
#include "content/browser/web_contents/web_contents_impl.h"
#include "content/public/common/content_switches.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_frame_navigation_observer.h"
#include "content/public/test/test_navigation_observer.h"
#include "content/shell/browser/shell.h"
#include "content/test/content_browser_test_utils_internal.h"
#include "net/dns/mock_host_resolver.h"
#include "net/test/embedded_test_server/embedded_test_server.h"
#include "url/gurl.h"

namespace content {

class TopDocumentIsolationTest : public ContentBrowserTest {
 public:
  TopDocumentIsolationTest() {}

 protected:
  std::string DepictFrameTree(FrameTreeNode* node) {
    return visualizer_.DepictFrameTree(node);
  }

  void SetUpCommandLine(base::CommandLine* command_line) override {
    command_line->AppendSwitch(switches::kTopDocumentIsolation);
  }

  void SetUpOnMainThread() override {
    host_resolver()->AddRule("*", "127.0.0.1");
    SetupCrossSiteRedirector(embedded_test_server());
    ASSERT_TRUE(embedded_test_server()->Start());
  }

  FrameTreeNode* root() {
    return static_cast<WebContentsImpl*>(shell()->web_contents())
        ->GetFrameTree()
        ->root();
  }

  void GoBack() {
    TestNavigationObserver back_load_observer(shell()->web_contents());
    shell()->web_contents()->GetController().GoBack();
    back_load_observer.Wait();
  }

  Shell* OpenPopup(FrameTreeNode* opener, const std::string& url) {
    GURL gurl =
        opener->current_frame_host()->GetLastCommittedURL().Resolve(url);
    return content::OpenPopup(opener, gurl, "_blank");
  }

  void RendererInitiatedNavigateToURL(FrameTreeNode* node, const GURL& url) {
    TestFrameNavigationObserver nav_observer(node);
    ASSERT_TRUE(
        ExecuteScript(node, "window.location.href='" + url.spec() + "'"));
    nav_observer.Wait();
  }

 private:
  FrameTreeVisualizer visualizer_;
};

IN_PROC_BROWSER_TEST_F(TopDocumentIsolationTest, SameSiteDeeplyNested) {
  if (content::AreAllSitesIsolatedForTesting())
    return;  // Top Document Isolation is disabled in this mode.

  GURL main_url(embedded_test_server()->GetURL(
      "a.com", "/cross_site_iframe_factory.html?a(a,a(a,a(a)))"));

  NavigateToURL(shell(), main_url);

  EXPECT_EQ(
      " Site A\n"
      "   |--Site A\n"
      "   +--Site A\n"
      "        |--Site A\n"
      "        +--Site A\n"
      "             +--Site A\n"
      "Where A = http://a.com/",
      DepictFrameTree(root()));
}

IN_PROC_BROWSER_TEST_F(TopDocumentIsolationTest, CrossSiteDeeplyNested) {
  if (content::AreAllSitesIsolatedForTesting())
    return;  // Top Document Isolation is disabled in this mode.

  GURL main_url(embedded_test_server()->GetURL(
      "a.com", "/cross_site_iframe_factory.html?a(b(c(d(b))))"));

  NavigateToURL(shell(), main_url);

  EXPECT_EQ(
      " Site A ------------ proxies for B\n"
      "   +--Site B ------- proxies for A\n"
      "        +--Site B -- proxies for A\n"
      "             +--Site B -- proxies for A\n"
      "                  +--Site B -- proxies for A\n"
      "Where A = http://a.com/\n"
      "      B = default subframe process",
      DepictFrameTree(root()));
}

IN_PROC_BROWSER_TEST_F(TopDocumentIsolationTest, ReturnToTopSite) {
  if (content::AreAllSitesIsolatedForTesting())
    return;  // Top Document Isolation is disabled in this mode.

  GURL main_url(embedded_test_server()->GetURL(
      "a.com", "/cross_site_iframe_factory.html?a(b(a(c)))"));

  NavigateToURL(shell(), main_url);

  EXPECT_EQ(
      " Site A ------------ proxies for B\n"
      "   +--Site B ------- proxies for A\n"
      "        +--Site A -- proxies for B\n"
      "             +--Site B -- proxies for A\n"
      "Where A = http://a.com/\n"
      "      B = default subframe process",
      DepictFrameTree(root()));
}

IN_PROC_BROWSER_TEST_F(TopDocumentIsolationTest, NavigateSubframeToTopSite) {
  if (content::AreAllSitesIsolatedForTesting())
    return;  // Top Document Isolation is disabled in this mode.

  GURL main_url(embedded_test_server()->GetURL(
      "a.com", "/cross_site_iframe_factory.html?a(b(c(d)))"));

  NavigateToURL(shell(), main_url);

  EXPECT_EQ(
      " Site A ------------ proxies for B\n"
      "   +--Site B ------- proxies for A\n"
      "        +--Site B -- proxies for A\n"
      "             +--Site B -- proxies for A\n"
      "Where A = http://a.com/\n"
      "      B = default subframe process",
      DepictFrameTree(root()));

  GURL ada_url(embedded_test_server()->GetURL(
      "a.com", "/cross_site_iframe_factory.html?a(d(a))"));
  RendererInitiatedNavigateToURL(root()->child_at(0)->child_at(0), ada_url);

  EXPECT_EQ(
      " Site A ------------ proxies for B\n"
      "   +--Site B ------- proxies for A\n"
      "        +--Site A -- proxies for B\n"
      "             +--Site B -- proxies for A\n"
      "                  +--Site A -- proxies for B\n"
      "Where A = http://a.com/\n"
      "      B = default subframe process",
      DepictFrameTree(root()));
}

IN_PROC_BROWSER_TEST_F(TopDocumentIsolationTest, NavigateToSubframeSite) {
  if (content::AreAllSitesIsolatedForTesting())
    return;  // Top Document Isolation is disabled in this mode.

  GURL ab_url(embedded_test_server()->GetURL(
      "a.com", "/cross_site_iframe_factory.html?a(b)"));
  GURL ba_url(embedded_test_server()->GetURL(
      "b.com", "/cross_site_iframe_factory.html?b(a, c)"));

  NavigateToURL(shell(), ab_url);

  EXPECT_EQ(
      " Site A ------------ proxies for B\n"
      "   +--Site B ------- proxies for A\n"
      "Where A = http://a.com/\n"
      "      B = default subframe process",
      DepictFrameTree(root()));

  NavigateToURL(shell(), ba_url);

  EXPECT_EQ(
      " Site C ------------ proxies for B\n"
      "   |--Site B ------- proxies for C\n"
      "   +--Site B ------- proxies for C\n"
      "Where B = default subframe process\n"
      "      C = http://b.com/",
      DepictFrameTree(root()));
}

IN_PROC_BROWSER_TEST_F(TopDocumentIsolationTest,
                       NavigateToSubframeSiteWithPopup) {
  if (content::AreAllSitesIsolatedForTesting())
    return;  // Top Document Isolation is disabled in this mode.

  // A(B) -> B(A), but while a separate B(A) popup exists.
  GURL ab_url(embedded_test_server()->GetURL(
      "a.com", "/cross_site_iframe_factory.html?a(b)"));

  NavigateToURL(shell(), ab_url);

  EXPECT_EQ(
      " Site A ------------ proxies for B\n"
      "   +--Site B ------- proxies for A\n"
      "Where A = http://a.com/\n"
      "      B = default subframe process",
      DepictFrameTree(root()));

  Shell* popup =
      OpenPopup(root()->child_at(0), "/cross_site_iframe_factory.html?b(a)");
  FrameTreeNode* popup_root =
      static_cast<WebContentsImpl*>(popup->web_contents())
          ->GetFrameTree()
          ->root();

  // This popup's main frame must stay in the default subframe siteinstance,
  // since its opener (the b.com subframe) may synchronously script it. Note
  // that the popup's subframe is same-site with window.top.opener.top, the
  // a.com main frame of the tab. But --top-document-isolation does not
  // currently place the popup subframe in the a.com process in this case.
  EXPECT_EQ(
      " Site B\n"
      "   +--Site B\n"
      "Where B = default subframe process",
      DepictFrameTree(popup_root));

  GURL ba_url(embedded_test_server()->GetURL(
      "b.com", "/cross_site_iframe_factory.html?b(a, c)"));
  NavigateToURL(shell(), ba_url);

  // This navigation destroys the popup's opener, so we allow the main frame to
  // commit in a top level process for b.com, in spite of the b.com popup in the
  // default subframe process.
  EXPECT_EQ(
      " Site C ------------ proxies for B\n"
      "   |--Site B ------- proxies for C\n"
      "   +--Site B ------- proxies for C\n"
      "Where B = default subframe process\n"
      "      C = http://b.com/",
      DepictFrameTree(root()));
  EXPECT_EQ(
      " Site B\n"
      "   +--Site B\n"
      "Where B = default subframe process",
      DepictFrameTree(popup_root));

  // Navigate the popup to a new site.
  GURL c_url(embedded_test_server()->GetURL(
      "c.com", "/cross_site_iframe_factory.html?c(c, c, c, c)"));
  NavigateToURL(popup, c_url);
  EXPECT_EQ(
      " Site D ------------ proxies for B\n"
      "   |--Site D ------- proxies for B\n"
      "   |--Site D ------- proxies for B\n"
      "   |--Site D ------- proxies for B\n"
      "   +--Site D ------- proxies for B\n"
      "Where B = default subframe process\n"
      "      D = http://c.com/",
      DepictFrameTree(popup_root));
  NavigateToURL(shell(), c_url);
  EXPECT_EQ(
      " Site D\n"
      "   |--Site D\n"
      "   |--Site D\n"
      "   |--Site D\n"
      "   +--Site D\n"
      "Where D = http://c.com/",
      DepictFrameTree(popup_root));
  EXPECT_EQ(
      " Site D\n"
      "   |--Site D\n"
      "   |--Site D\n"
      "   |--Site D\n"
      "   +--Site D\n"
      "Where D = http://c.com/",
      DepictFrameTree(root()));
}

IN_PROC_BROWSER_TEST_F(TopDocumentIsolationTest,
                       NavigateToSubframeSiteWithPopup2) {
  if (content::AreAllSitesIsolatedForTesting())
    return;  // Top Document Isolation is disabled in this mode.

  // A(B, C) -> C(A, B), but while a separate C(A) popup exists.
  //
  // This test is constructed so that c.com is the second site to commit in the
  // default subframe SiteInstance, so the default subframe SiteInstance does
  // not have a "c.com" as the value of GetSiteURL().
  GURL abb_url(embedded_test_server()->GetURL(
      "a.com", "/cross_site_iframe_factory.html?a(b, b)"));

  NavigateToURL(shell(), abb_url);

  EXPECT_EQ(
      " Site A ------------ proxies for B\n"
      "   |--Site B ------- proxies for A\n"
      "   +--Site B ------- proxies for A\n"
      "Where A = http://a.com/\n"
      "      B = default subframe process",
      DepictFrameTree(root()));

  // A(B, B) -> A(B, C)
  GURL c_url(embedded_test_server()->GetURL(
      "c.com", "/cross_site_iframe_factory.html?c"));
  NavigateFrameToURL(root()->child_at(1), c_url);

  EXPECT_EQ(
      " Site A ------------ proxies for B\n"
      "   |--Site B ------- proxies for A\n"
      "   +--Site B ------- proxies for A\n"
      "Where A = http://a.com/\n"
      "      B = default subframe process",
      DepictFrameTree(root()));

  // This test exercises what happens when the SiteURL of the default subframe
  // siteinstance doesn't match the subframe site.
  EXPECT_NE("c.com", root()
                         ->child_at(1)
                         ->current_frame_host()
                         ->GetSiteInstance()
                         ->GetSiteURL()
                         .host());

  // Subframe C creates C(A) popup.
  Shell* popup =
      OpenPopup(root()->child_at(1), "/cross_site_iframe_factory.html?c(a)");

  FrameTreeNode* popup_root =
      static_cast<WebContentsImpl*>(popup->web_contents())
          ->GetFrameTree()
          ->root();

  // The popup must stay with its opener, in the default subframe process.
  EXPECT_EQ(
      " Site B\n"
      "   +--Site B\n"
      "Where B = default subframe process",
      DepictFrameTree(popup_root));

  GURL cab_url(embedded_test_server()->GetURL(
      "c.com", "/cross_site_iframe_factory.html?c(a, b)"));
  {
    RenderFrameDeletedObserver deleted_observer(root()->current_frame_host());
    NavigateToURL(shell(), cab_url);
    deleted_observer.WaitUntilDeleted();
  }

  // This c.com navigation currently breaks out of the default subframe process,
  // even though that process houses a c.com pop-up.
  EXPECT_EQ(
      " Site C ------------ proxies for B\n"
      "   |--Site B ------- proxies for C\n"
      "   +--Site B ------- proxies for C\n"
      "Where B = default subframe process\n"
      "      C = http://c.com/",
      DepictFrameTree(root()));

  // c.com popup should remain where it was, in the subframe process.
  EXPECT_EQ(
      " Site B\n"
      "   +--Site B\n"
      "Where B = default subframe process",
      DepictFrameTree(popup_root));
  EXPECT_EQ(nullptr, popup_root->opener());

  // If we navigate the popup to a new site, it ought to transfer processes.
  GURL d_url(embedded_test_server()->GetURL(
      "d.com", "/cross_site_iframe_factory.html?d"));
  {
    RenderFrameDeletedObserver deleted_observer(
        popup_root->current_frame_host());
    NavigateToURL(popup, d_url);
    deleted_observer.WaitUntilDeleted();
  }
  EXPECT_EQ(
      " Site D ------------ proxies for B\n"
      "Where B = default subframe process\n"
      "      D = http://d.com/",
      DepictFrameTree(popup_root));
  {
    RenderFrameDeletedObserver deleted_observer(root()->current_frame_host());
    NavigateToURL(shell(), d_url);
    deleted_observer.WaitUntilDeleted();
  }
  EXPECT_EQ(
      " Site D\n"
      "Where D = http://d.com/",
      DepictFrameTree(popup_root));
  EXPECT_EQ(
      " Site D\n"
      "Where D = http://d.com/",
      DepictFrameTree(root()));
}

IN_PROC_BROWSER_TEST_F(TopDocumentIsolationTest, FramesForSitesInHistory) {
  if (content::AreAllSitesIsolatedForTesting())
    return;  // Top Document Isolation is disabled in this mode.

  // First, do a series of navigations.
  GURL a_url = embedded_test_server()->GetURL(
      "a.com", "/cross_site_iframe_factory.html?a");
  GURL b_url = embedded_test_server()->GetURL(
      "b.com", "/cross_site_iframe_factory.html?b");
  GURL c_url = embedded_test_server()->GetURL(
      "c.com", "/cross_site_iframe_factory.html?c");

  // Browser-initiated navigation to a.com.
  NavigateToURL(shell(), a_url);
  EXPECT_EQ(
      " Site A\n"
      "Where A = http://a.com/",
      DepictFrameTree(root()));

  // Browser-initiated navigation to b.com.
  {
    // For any cross-process navigations, we must wait for the old RenderFrame
    // to be deleted before calling DepictFrameTree, or else there's a chance
    // the old SiteInstance could be listed while pending deletion.
    RenderFrameDeletedObserver deleted_observer(root()->current_frame_host());
    NavigateToURL(shell(), b_url);
    deleted_observer.WaitUntilDeleted();
  }
  EXPECT_EQ(
      " Site B\n"
      "Where B = http://b.com/",
      DepictFrameTree(root()));

  // Renderer-initiated navigation back to a.com. This shouldn't swap processes.
  RendererInitiatedNavigateToURL(root(), a_url);
  EXPECT_EQ(
      " Site B\n"
      "Where B = http://b.com/",
      DepictFrameTree(root()));

  // Browser-initiated navigation to c.com.
  {
    RenderFrameDeletedObserver deleted_observer(root()->current_frame_host());
    NavigateToURL(shell(), c_url);
    deleted_observer.WaitUntilDeleted();
  }
  EXPECT_EQ(
      " Site C\n"
      "Where C = http://c.com/",
      DepictFrameTree(root()));

  // Now, navigate to a fourth site with iframes to the sites in the history.
  {
    RenderFrameDeletedObserver deleted_observer(root()->current_frame_host());
    NavigateToURL(shell(),
                  embedded_test_server()->GetURL(
                      "d.com", "/cross_site_iframe_factory.html?d(a,b,c)"));
    deleted_observer.WaitUntilDeleted();
  }

  EXPECT_EQ(
      " Site D ------------ proxies for E\n"
      "   |--Site E ------- proxies for D\n"
      "   |--Site E ------- proxies for D\n"
      "   +--Site E ------- proxies for D\n"
      "Where D = http://d.com/\n"
      "      E = default subframe process",
      DepictFrameTree(root()));

  // Now try going back.
  {
    RenderFrameDeletedObserver deleted_observer(root()->current_frame_host());
    GoBack();
    deleted_observer.WaitUntilDeleted();
  }
  EXPECT_EQ(
      " Site C\n"
      "Where C = http://c.com/",
      DepictFrameTree(root()));
  {
    RenderFrameDeletedObserver deleted_observer(root()->current_frame_host());
    GoBack();
    deleted_observer.WaitUntilDeleted();
  }
  EXPECT_EQ(
      " Site B\n"
      "Where B = http://b.com/",
      DepictFrameTree(root()));
  GoBack();
  EXPECT_EQ(
      " Site B\n"
      "Where B = http://b.com/",
      DepictFrameTree(root()));
  {
    RenderFrameDeletedObserver deleted_observer(root()->current_frame_host());
    GoBack();
    deleted_observer.WaitUntilDeleted();
  }
  EXPECT_EQ(
      " Site A\n"
      "Where A = http://a.com/",
      DepictFrameTree(root()));
}

IN_PROC_BROWSER_TEST_F(TopDocumentIsolationTest, CrossSiteAtLevelTwo) {
  if (content::AreAllSitesIsolatedForTesting())
    return;  // Top Document Isolation is disabled in this mode.

  GURL main_url(embedded_test_server()->GetURL(
      "a.com", "/cross_site_iframe_factory.html?a(a(b, a))"));

  NavigateToURL(shell(), main_url);

  EXPECT_EQ(
      " Site A ------------ proxies for B\n"
      "   +--Site A ------- proxies for B\n"
      "        |--Site B -- proxies for A\n"
      "        +--Site A -- proxies for B\n"
      "Where A = http://a.com/\n"
      "      B = default subframe process",
      DepictFrameTree(root()));

  GURL c_url(embedded_test_server()->GetURL(
      "c.com", "/cross_site_iframe_factory.html?c"));
  NavigateFrameToURL(root()->child_at(0)->child_at(1), c_url);

  // This navigation should complete in the default subframe siteinstance.
  EXPECT_EQ(
      " Site A ------------ proxies for B\n"
      "   +--Site A ------- proxies for B\n"
      "        |--Site B -- proxies for A\n"
      "        +--Site B -- proxies for A\n"
      "Where A = http://a.com/\n"
      "      B = default subframe process",
      DepictFrameTree(root()));
}

IN_PROC_BROWSER_TEST_F(TopDocumentIsolationTest, PopupAndRedirection) {
  if (content::AreAllSitesIsolatedForTesting())
    return;  // Top Document Isolation is disabled in this mode.

  GURL main_url(embedded_test_server()->GetURL(
      "page.com", "/cross_site_iframe_factory.html?page(adnetwork)"));

  // User opens page on page.com which contains a subframe from adnetwork.com.
  NavigateToURL(shell(), main_url);

  EXPECT_EQ(
      " Site A ------------ proxies for B\n"
      "   +--Site B ------- proxies for A\n"
      "Where A = http://page.com/\n"
      "      B = default subframe process",
      DepictFrameTree(root()));

  GURL ad_url(embedded_test_server()->GetURL(
      "ad.com", "/cross_site_iframe_factory.html?ad"));

  // adnetwork.com retrieves an ad from advertiser (ad.com) and redirects the
  // subframe to ad.com.
  RendererInitiatedNavigateToURL(root()->child_at(0), ad_url);

  // The subframe still uses the default subframe SiteInstance after navigation.
  EXPECT_EQ(
      " Site A ------------ proxies for B\n"
      "   +--Site B ------- proxies for A\n"
      "Where A = http://page.com/\n"
      "      B = default subframe process",
      DepictFrameTree(root()));

  // User clicks the ad in the subframe, which opens a popup on the ad
  // network's domain.
  GURL popup_url(embedded_test_server()->GetURL(
      "adnetwork.com", "/cross_site_iframe_factory.html?adnetwork"));
  Shell* popup = OpenPopup(root()->child_at(0), popup_url.spec());

  FrameTreeNode* popup_root =
      static_cast<WebContentsImpl*>(popup->web_contents())
          ->GetFrameTree()
          ->root();

  // It's ok for the popup to break out of the subframe process because it's
  // currently cross-site from its opener frame.
  EXPECT_EQ(
      " Site C ------------ proxies for B\n"
      "Where B = default subframe process\n"
      "      C = http://adnetwork.com/",
      DepictFrameTree(popup_root));

  EXPECT_EQ(
      " Site A ------------ proxies for B C\n"
      "   +--Site B ------- proxies for A C\n"
      "Where A = http://page.com/\n"
      "      B = default subframe process\n"
      "      C = http://adnetwork.com/",
      DepictFrameTree(root()));

  // The popup redirects itself to the advertiser's website (ad.com).
  RendererInitiatedNavigateToURL(popup_root, ad_url);

  // This must join its same-site opener, in the default subframe SiteInstance.
  EXPECT_EQ(
      " Site A ------------ proxies for B C\n"
      "   +--Site B ------- proxies for A C\n"
      "Where A = http://page.com/\n"
      "      B = default subframe process\n"
      "      C = http://adnetwork.com/",
      DepictFrameTree(root()));
  EXPECT_EQ(
      " Site C ------------ proxies for B\n"
      "Where B = default subframe process\n"
      "      C = http://adnetwork.com/",
      DepictFrameTree(popup_root));
}

}  // namespace content
