// Copyright 2019 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 "base/unguessable_token.h"
#include "content/browser/renderer_host/frame_tree.h"
#include "content/browser/web_contents/web_contents_impl.h"
#include "content/common/content_navigation_policy.h"
#include "content/public/test/back_forward_cache_util.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 "net/dns/mock_host_resolver.h"
#include "net/test/embedded_test_server/embedded_test_server.h"
#include "testing/gtest/include/gtest/gtest.h"

namespace content {

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

  void SetUpCommandLine(base::CommandLine* command_line) override {
    scoped_feature_list_.InitWithFeaturesAndParameters(
        GetDefaultEnabledBackForwardCacheFeaturesForTesting(
            /*ignore_outstanding_network_request=*/false),
        GetDefaultDisabledBackForwardCacheFeaturesForTesting());

    ContentBrowserTest::SetUpCommandLine(command_line);
    IsolateAllSitesForTesting(command_line);
  }

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

  WebContents* web_contents() { return shell()->web_contents(); }

  RenderFrameHostImpl* top_frame_host() {
    return static_cast<RenderFrameHostImpl*>(
        web_contents()->GetPrimaryMainFrame());
  }

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

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

IN_PROC_BROWSER_TEST_F(EmbeddingTokenBrowserTest, EmbeddingTokenOnMainFrame) {
  GURL a_url = embedded_test_server()->GetURL("a.com", "/site_isolation/");
  GURL b_url = embedded_test_server()->GetURL("b.com", "/site_isolation/");
  // Starts without an embedding token.
  EXPECT_FALSE(top_frame_host()->GetEmbeddingToken().has_value());

  // Embedding tokens should get added to the main frame.
  EXPECT_TRUE(NavigateToURL(shell(), a_url.Resolve("blank.html")));
  EXPECT_TRUE(top_frame_host()->GetEmbeddingToken().has_value());
  auto first_token = top_frame_host()->GetEmbeddingToken().value();

  EXPECT_TRUE(NavigateToURL(shell(), b_url.Resolve("blank.html")));
  EXPECT_TRUE(top_frame_host()->GetEmbeddingToken().has_value());
  EXPECT_NE(top_frame_host()->GetEmbeddingToken().value(), first_token);
}

IN_PROC_BROWSER_TEST_F(EmbeddingTokenBrowserTest,
                       EmbeddingTokensAddedToCrossDocumentIFrames) {
  EXPECT_TRUE(NavigateToURL(
      shell(), embedded_test_server()->GetURL(
                   "a.com", "/cross_site_iframe_factory.html?a(b(a),c,a)")));

  ASSERT_EQ(3U, top_frame_host()->child_count());
  EXPECT_TRUE(top_frame_host()->GetEmbeddingToken().has_value());
  auto top_token = top_frame_host()->GetEmbeddingToken().value();

  // Child 0 (b) should have an embedding token.
  auto child_0_token =
      top_frame_host()->child_at(0)->current_frame_host()->GetEmbeddingToken();
  ASSERT_TRUE(child_0_token.has_value());
  EXPECT_NE(base::UnguessableToken::Null(), child_0_token);
  EXPECT_NE(top_token, child_0_token);

  // Child 0 (a) of Child 0 (b) should have an embedding token.
  ASSERT_EQ(1U, top_frame_host()->child_at(0)->child_count());
  auto child_0_0_token = top_frame_host()
                             ->child_at(0)
                             ->child_at(0)
                             ->current_frame_host()
                             ->GetEmbeddingToken();
  ASSERT_TRUE(child_0_0_token.has_value());
  EXPECT_NE(base::UnguessableToken::Null(), child_0_0_token);
  EXPECT_NE(top_token, child_0_0_token);
  EXPECT_NE(child_0_token, child_0_0_token);

  // Child 1 (c) should have an embedding token.
  auto child_1_token =
      top_frame_host()->child_at(1)->current_frame_host()->GetEmbeddingToken();
  ASSERT_TRUE(child_1_token.has_value());
  EXPECT_NE(base::UnguessableToken::Null(), child_1_token);
  EXPECT_NE(top_token, child_1_token);
  EXPECT_NE(child_0_token, child_1_token);
  EXPECT_NE(child_0_0_token, child_1_token);

  // Child 2 (a) should have an embedding token.
  auto child_2_token =
      top_frame_host()->child_at(2)->current_frame_host()->GetEmbeddingToken();
  ASSERT_TRUE(child_2_token.has_value());
  EXPECT_NE(base::UnguessableToken::Null(), child_2_token);
  EXPECT_NE(top_token, child_2_token);
  EXPECT_NE(child_0_token, child_2_token);
  EXPECT_NE(child_0_0_token, child_2_token);

  // TODO(ckitagawa): Somehow assert that the parent and child have matching
  // embedding tokens in parent HTMLOwnerElement and child LocalFrame.
}

IN_PROC_BROWSER_TEST_F(EmbeddingTokenBrowserTest,
                       EmbeddingTokenSwapsOnCrossDocumentNavigation) {
  EXPECT_TRUE(NavigateToURL(
      shell(), embedded_test_server()->GetURL(
                   "a.com", "/cross_site_iframe_factory.html?a(b)")));

  ASSERT_EQ(1U, top_frame_host()->child_count());
  EXPECT_TRUE(top_frame_host()->GetEmbeddingToken().has_value());
  auto top_token = top_frame_host()->GetEmbeddingToken().value();

  // Child 0 (b) should have an embedding token.
  RenderFrameHost* target = top_frame_host()->child_at(0)->current_frame_host();
  auto child_0_token = target->GetEmbeddingToken();
  ASSERT_TRUE(child_0_token.has_value());
  EXPECT_NE(base::UnguessableToken::Null(), child_0_token);
  EXPECT_NE(top_token, child_0_token);

  // Navigate child 0 (b) to same-site the token should swap.
  NavigateIframeToURL(shell()->web_contents(), "child-0",
                      embedded_test_server()
                          ->GetURL("b.com", "/site_isolation/")
                          .Resolve("blank.html"));
  auto same_site_new_child_0_token =
      top_frame_host()->child_at(0)->current_frame_host()->GetEmbeddingToken();
  ASSERT_TRUE(same_site_new_child_0_token.has_value());
  EXPECT_NE(base::UnguessableToken::Null(), same_site_new_child_0_token);
  EXPECT_NE(top_token, same_site_new_child_0_token);
  EXPECT_NE(child_0_token, same_site_new_child_0_token);

  // Navigate child 0 (b) to another site (cross-process) the token should swap.
  {
    if (ShouldCreateNewHostForSameSiteSubframe()) {
      // The RenderFrameHost was been replaced when the frame navigated.
      target = top_frame_host()->child_at(0)->current_frame_host();
    }
    RenderFrameDeletedObserver deleted_observer(target);
    NavigateIframeToURL(shell()->web_contents(), "child-0",
                        embedded_test_server()
                            ->GetURL("c.com", "/site_isolation/")
                            .Resolve("blank.html"));
    deleted_observer.WaitUntilDeleted();
  }
  auto new_site_child_0_token =
      top_frame_host()->child_at(0)->current_frame_host()->GetEmbeddingToken();
  ASSERT_TRUE(same_site_new_child_0_token.has_value());
  EXPECT_NE(base::UnguessableToken::Null(), new_site_child_0_token);
  EXPECT_NE(top_token, new_site_child_0_token);
  EXPECT_NE(child_0_token, new_site_child_0_token);
  EXPECT_NE(same_site_new_child_0_token, new_site_child_0_token);

  // TODO(ckitagawa): Somehow assert that the parent and child have matching
  // embedding tokens in parent HTMLOwnerElement and child LocalFrame.
}

IN_PROC_BROWSER_TEST_F(
    EmbeddingTokenBrowserTest,
    EmbeddingTokenNotChangedOnSubframeSameDocumentNavigation) {
  EXPECT_TRUE(NavigateToURL(
      shell(), embedded_test_server()->GetURL(
                   "a.com", "/cross_site_iframe_factory.html?a(a)")));

  ASSERT_EQ(1U, top_frame_host()->child_count());
  EXPECT_TRUE(top_frame_host()->GetEmbeddingToken().has_value());
  auto top_token = top_frame_host()->GetEmbeddingToken().value();

  // Child 0 (a) should have an embedding token.
  RenderFrameHost* target = top_frame_host()->child_at(0)->current_frame_host();
  auto child_0_token = target->GetEmbeddingToken();
  ASSERT_TRUE(child_0_token.has_value());
  EXPECT_NE(base::UnguessableToken::Null(), child_0_token);
  EXPECT_NE(top_token, child_0_token);

  auto b_url = embedded_test_server()->GetURL("b.com", "/site_isolation/");
  // Navigate child 0 to another site (cross-process) a token should be created.
  {
    RenderFrameDeletedObserver deleted_observer(
        top_frame_host()->child_at(0)->current_frame_host());
    NavigateIframeToURL(web_contents(), "child-0", b_url.Resolve("blank.html"));
    deleted_observer.WaitUntilDeleted();
  }

  // Child 0 (b) should have a new embedding token.
  auto new_child_0_token =
      top_frame_host()->child_at(0)->current_frame_host()->GetEmbeddingToken();
  ASSERT_TRUE(child_0_token.has_value());
  EXPECT_NE(base::UnguessableToken::Null(), new_child_0_token);
  EXPECT_NE(top_token, new_child_0_token);
  EXPECT_NE(child_0_token, new_child_0_token);

  // Navigate child 0 (b) to same document the token should not swap.
  NavigateIframeToURL(web_contents(), "child-0",
                      b_url.Resolve("blank.html#foo"));
  auto same_document_new_child_0_token =
      top_frame_host()->child_at(0)->current_frame_host()->GetEmbeddingToken();
  ASSERT_TRUE(same_document_new_child_0_token.has_value());
  EXPECT_EQ(new_child_0_token, same_document_new_child_0_token);

  // TODO(ckitagawa): Somehow assert that the parent and child have matching
  // embedding tokens in parent HTMLOwnerElement and child LocalFrame.
}

IN_PROC_BROWSER_TEST_F(EmbeddingTokenBrowserTest,
                       EmbeddingTokenChangedOnSubframeNavigationToNewDocument) {
  auto a_url = embedded_test_server()->GetURL("a.com", "/");
  EXPECT_TRUE(NavigateToURL(
      shell(), a_url.Resolve("cross_site_iframe_factory.html?a(b)")));

  ASSERT_EQ(1U, top_frame_host()->child_count());
  EXPECT_TRUE(top_frame_host()->GetEmbeddingToken().has_value());
  auto top_token = top_frame_host()->GetEmbeddingToken().value();

  // Child 0 (b) should have an embedding token.
  RenderFrameHost* target = top_frame_host()->child_at(0)->current_frame_host();
  auto child_0_token = target->GetEmbeddingToken();
  ASSERT_TRUE(child_0_token.has_value());
  EXPECT_NE(base::UnguessableToken::Null(), child_0_token);
  EXPECT_NE(top_token, child_0_token);

  // Navigate child 0 (b) to the same site as the main frame. This should create
  // an embedding token.
  {
    RenderFrameDeletedObserver deleted_observer(target);
    NavigateIframeToURL(web_contents(), "child-0",
                        a_url.Resolve("site_isolation/").Resolve("blank.html"));
    deleted_observer.WaitUntilDeleted();
  }

  auto new_child_0_token =
      top_frame_host()->child_at(0)->current_frame_host()->GetEmbeddingToken();
  ASSERT_TRUE(new_child_0_token.has_value());
  EXPECT_NE(base::UnguessableToken::Null(), new_child_0_token);
  EXPECT_NE(top_token, new_child_0_token);
  EXPECT_NE(child_0_token, new_child_0_token);

  // TODO(ckitagawa): Somehow assert that the parent and child have matching
  // embedding tokens in parent HTMLOwnerElement and child LocalFrame.
}

IN_PROC_BROWSER_TEST_F(EmbeddingTokenBrowserTest,
                       BackForwardCacheCrossDocument) {
  auto a_url = embedded_test_server()->GetURL("a.com", "/site_isolation/");
  auto b_url = embedded_test_server()->GetURL("b.com", "/site_isolation/");
  EXPECT_TRUE(NavigateToURL(shell(), a_url.Resolve("blank.html")));

  EXPECT_TRUE(top_frame_host()->GetEmbeddingToken().has_value());
  auto top_token_a = top_frame_host()->GetEmbeddingToken().value();

  EXPECT_TRUE(NavigateToURL(shell(), b_url.Resolve("blank.html")));
  EXPECT_TRUE(top_frame_host()->GetEmbeddingToken().has_value());
  auto top_token_b = top_frame_host()->GetEmbeddingToken().value();
  EXPECT_NE(top_token_a, top_token_b);

  // Navigate back to the first origin. The back forward cache should keep
  // the embedding token.
  web_contents()->GetController().GoBack();
  EXPECT_TRUE(content::WaitForLoadStop(web_contents()));

  EXPECT_TRUE(top_frame_host()->GetEmbeddingToken().has_value());
  auto top_token_a_prime = top_frame_host()->GetEmbeddingToken().value();
  EXPECT_EQ(top_token_a, top_token_a_prime);
}

IN_PROC_BROWSER_TEST_F(EmbeddingTokenBrowserTest,
                       BackForwardCacheCrossDocumentAfterSameDocument) {
  auto a_url = embedded_test_server()->GetURL("a.com", "/site_isolation/");
  auto b_url = embedded_test_server()->GetURL("b.com", "/site_isolation/");
  EXPECT_TRUE(NavigateToURL(shell(), a_url.Resolve("blank.html")));

  EXPECT_TRUE(top_frame_host()->GetEmbeddingToken().has_value());
  auto top_token_a = top_frame_host()->GetEmbeddingToken().value();

  EXPECT_TRUE(NavigateToURL(shell(), a_url.Resolve("blank.html#foo")));
  EXPECT_TRUE(top_frame_host()->GetEmbeddingToken().has_value());
  EXPECT_EQ(top_frame_host()->GetEmbeddingToken().value(), top_token_a);

  EXPECT_TRUE(NavigateToURL(shell(), b_url.Resolve("blank.html")));
  EXPECT_TRUE(top_frame_host()->GetEmbeddingToken().has_value());
  auto top_token_b = top_frame_host()->GetEmbeddingToken().value();
  EXPECT_NE(top_token_a, top_token_b);

  // Navigate back to the first origin. The back forward cache should keep
  // the embedding token even when the embedding token is not present in the
  // most recent navigation.
  web_contents()->GetController().GoBack();
  EXPECT_TRUE(content::WaitForLoadStop(web_contents()));

  EXPECT_TRUE(top_frame_host()->GetEmbeddingToken().has_value());
  auto top_token_a_prime = top_frame_host()->GetEmbeddingToken().value();
  EXPECT_EQ(top_token_a, top_token_a_prime);
}

IN_PROC_BROWSER_TEST_F(EmbeddingTokenBrowserTest,
                       SameDocumentHistoryPreservesTokens) {
  auto a_url = embedded_test_server()->GetURL("a.com", "/site_isolation/");
  EXPECT_TRUE(NavigateToURL(shell(), a_url.Resolve("blank.html")));

  EXPECT_TRUE(top_frame_host()->GetEmbeddingToken().has_value());
  auto top_token_a = top_frame_host()->GetEmbeddingToken().value();

  EXPECT_TRUE(NavigateToURL(shell(), a_url.Resolve("blank.html#foo")));
  EXPECT_TRUE(top_frame_host()->GetEmbeddingToken().has_value());
  auto top_token_a_prime = top_frame_host()->GetEmbeddingToken().value();
  EXPECT_EQ(top_token_a, top_token_a_prime);

  // Navigate back to before the fragment was added. This should preserve the
  // embedding token.
  web_contents()->GetController().GoBack();
  EXPECT_TRUE(content::WaitForLoadStop(web_contents()));

  EXPECT_TRUE(top_frame_host()->GetEmbeddingToken().has_value());
  auto top_token_a_prime_prime = top_frame_host()->GetEmbeddingToken().value();
  EXPECT_EQ(top_token_a, top_token_a_prime_prime);
}

}  // namespace content