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

#include "chrome/browser/ash/login/signin_partition_manager.h"

#include <memory>

#include "base/functional/bind.h"
#include "base/functional/callback.h"
#include "base/memory/ptr_util.h"
#include "base/run_loop.h"
#include "chrome/browser/ash/login/signin_partition_manager_factory.h"
#include "chrome/test/base/chrome_render_view_host_test_harness.h"
#include "chrome/test/base/testing_profile.h"
#include "content/public/browser/browser_context.h"
#include "content/public/browser/browser_task_traits.h"
#include "content/public/browser/browser_thread.h"
#include "content/public/browser/network_service_instance.h"
#include "content/public/browser/site_instance.h"
#include "content/public/browser/storage_partition.h"
#include "content/public/browser/web_contents.h"
#include "content/public/test/test_utils.h"
#include "content/public/test/web_contents_tester.h"
#include "mojo/public/cpp/bindings/receiver_set.h"
#include "mojo/public/cpp/bindings/remote.h"
#include "net/base/network_anonymization_key.h"
#include "net/cookies/cookie_store.h"
#include "net/http/http_auth.h"
#include "net/http/http_auth_cache.h"
#include "net/http/http_network_session.h"
#include "net/http/http_transaction_factory.h"
#include "net/url_request/url_request_context.h"
#include "services/cert_verifier/public/mojom/cert_verifier_service_factory.mojom.h"
#include "services/network/network_context.h"
#include "services/network/network_service.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "url/gurl.h"
#include "url/scheme_host_port.h"

namespace ash {
namespace login {
namespace {

constexpr char kEmbedderUrl[] = "http://www.whatever.com/";

void StorePartitionNameAndQuitLoop(base::RunLoop* loop,
                                   std::string* out_partition_name,
                                   const std::string& partition_name) {
  *out_partition_name = partition_name;
  loop->Quit();
}

void AddEntryToHttpAuthCache(network::NetworkContext* network_context) {
  net::HttpAuthCache* http_auth_cache = network_context->url_request_context()
                                            ->http_transaction_factory()
                                            ->GetSession()
                                            ->http_auth_cache();
  http_auth_cache->Add(
      url::SchemeHostPort(GURL(kEmbedderUrl)), net::HttpAuth::AUTH_PROXY, "",
      net::HttpAuth::AUTH_SCHEME_BASIC, net::NetworkAnonymizationKey(), "",
      net::AuthCredentials(), "");
}

void IsEntryInHttpAuthCache(network::NetworkContext* network_context,
                            bool* out_entry_found) {
  net::HttpAuthCache* http_auth_cache = network_context->url_request_context()
                                            ->http_transaction_factory()
                                            ->GetSession()
                                            ->http_auth_cache();
  *out_entry_found =
      http_auth_cache->Lookup(url::SchemeHostPort(GURL(kEmbedderUrl)),
                              net::HttpAuth::AUTH_PROXY, "",
                              net::HttpAuth::AUTH_SCHEME_BASIC,
                              net::NetworkAnonymizationKey()) != nullptr;
}

}  // namespace

class SigninPartitionManagerTest : public ChromeRenderViewHostTestHarness {
 public:
  SigninPartitionManagerTest(const SigninPartitionManagerTest&) = delete;
  SigninPartitionManagerTest& operator=(const SigninPartitionManagerTest&) =
      delete;

 protected:
  SigninPartitionManagerTest() = default;
  ~SigninPartitionManagerTest() override = default;

  void SetUp() override {
    ChromeRenderViewHostTestHarness::SetUp();

    signin_browser_context_ = std::make_unique<TestingProfile>();
    // Wait for the Network Service to initialize on the IO thread.
    content::RunAllPendingInMessageLoop(content::BrowserThread::IO);

    TestingProfile::Builder().BuildIncognito(signin_browser_context_.get());

    signin_ui_web_contents_ = content::WebContentsTester::CreateTestWebContents(
        GetSigninProfile(), content::SiteInstance::Create(GetSigninProfile()));

    auto system_network_context_params =
        network::mojom::NetworkContextParams::New();
    system_network_context_params->cert_verifier_params =
        content::GetCertVerifierParams(
            cert_verifier::mojom::CertVerifierCreationParams::New());

    system_network_context_ = std::make_unique<network::NetworkContext>(
        network::NetworkService::GetNetworkServiceForTesting(),
        system_network_context_remote_.BindNewPipeAndPassReceiver(),
        std::move(system_network_context_params));

    GURL url(kEmbedderUrl);
    content::WebContentsTester::For(signin_ui_web_contents())
        ->NavigateAndCommit(url);

    GetSigninPartitionManager()->SetClearStoragePartitionTaskForTesting(
        base::BindRepeating(
            &SigninPartitionManagerTest::ClearStoragePartitionTask,
            base::Unretained(this)));
    GetSigninPartitionManager()->SetGetSystemNetworkContextForTesting(
        base::BindRepeating(
            &SigninPartitionManagerTest::GetSystemNetworkContext,
            base::Unretained(this)));
    GetSigninPartitionManager()->SetOnCreateNewStoragePartitionForTesting(
        base::BindRepeating(
            &SigninPartitionManagerTest::OnCreateNewStoragePartition,
            base::Unretained(this)));
  }

  void TearDown() override {
    system_network_context_.reset();

    signin_ui_web_contents_.reset();

    signin_browser_context_.reset();

    signin_network_context_.reset();

    ChromeRenderViewHostTestHarness::TearDown();
  }

  Profile* GetSigninProfile() {
    return signin_browser_context_->GetPrimaryOTRProfile(
        /*create_if_needed=*/true);
  }

  SigninPartitionManager* GetSigninPartitionManager() {
    return SigninPartitionManagerFactory::GetForBrowserContext(
        GetSigninProfile());
  }

  content::WebContents* signin_ui_web_contents() {
    return signin_ui_web_contents_.get();
  }

  void ExpectOneClearPartitionTask(
      content::StoragePartition* storage_partition) {
    EXPECT_EQ(1u, pending_clear_tasks_.size());
    if (pending_clear_tasks_.size() > 0) {
      EXPECT_EQ(storage_partition, pending_clear_tasks_[0].first);
    }
  }

  void FinishAllClearPartitionTasks() {
    for (auto& task : pending_clear_tasks_) {
      std::move(task.second).Run();
    }
    pending_clear_tasks_.clear();
  }

  std::string RunStartSigninSesssion(content::WebContents* webcontents) {
    std::string partition_name;
    base::RunLoop loop;
    GetSigninPartitionManager()->StartSigninSession(
        webcontents,
        base::BindOnce(&StorePartitionNameAndQuitLoop, &loop, &partition_name));
    loop.Run();

    return partition_name;
  }

  network::mojom::NetworkContext* GetSystemNetworkContext() {
    return system_network_context_.get();
  }

  network::NetworkContext* GetSystemNetworkContextImpl() {
    return system_network_context_.get();
  }

  network::NetworkContext* GetSigninNetworkContextImpl() {
    return signin_network_context_.get();
  }

 private:
  void ClearStoragePartitionTask(content::StoragePartition* partition,
                                 base::OnceClosure clear_done_closure) {
    pending_clear_tasks_.push_back({partition, std::move(clear_done_closure)});
  }

  void OnCreateNewStoragePartition(
      content::StoragePartition* storage_partition) {
    // Bind the NetworkContext for the new StoragePartition.
    mojo::PendingRemote<network::mojom::NetworkContext>
        signin_network_context_remote;

    auto signin_network_context_params =
        network::mojom::NetworkContextParams::New();
    signin_network_context_params->cert_verifier_params =
        content::GetCertVerifierParams(
            cert_verifier::mojom::CertVerifierCreationParams::New());

    signin_network_context_ = std::make_unique<network::NetworkContext>(
        network::NetworkService::GetNetworkServiceForTesting(),
        signin_network_context_remote.InitWithNewPipeAndPassReceiver(),
        std::move(signin_network_context_params));
    storage_partition->SetNetworkContextForTesting(
        std::move(signin_network_context_remote));
  }

  mojo::Remote<network::mojom::NetworkContext> system_network_context_remote_;
  std::unique_ptr<network::NetworkContext> system_network_context_;

  std::unique_ptr<TestingProfile> signin_browser_context_;
  std::unique_ptr<network::NetworkContext> signin_network_context_;

  // Web contents of the sign-in UI, embedder of the signin-frame webview.
  std::unique_ptr<content::WebContents> signin_ui_web_contents_;

  std::vector<std::pair<content::StoragePartition*, base::OnceClosure>>
      pending_clear_tasks_;
};

TEST_F(SigninPartitionManagerTest, TestSubsequentAttempts) {
  // First sign-in attempt
  std::string signin_partition_name_1 =
      RunStartSigninSesssion(signin_ui_web_contents());
  auto* signin_partition_1 =
      GetSigninPartitionManager()->GetCurrentStoragePartition();
  EXPECT_FALSE(signin_partition_name_1.empty());
  EXPECT_EQ(signin_partition_name_1,
            GetSigninPartitionManager()->GetCurrentStoragePartitionName());

  // Second sign-in attempt
  std::string signin_partition_name_2 =
      RunStartSigninSesssion(signin_ui_web_contents());
  auto* signin_partition_2 =
      GetSigninPartitionManager()->GetCurrentStoragePartition();
  EXPECT_FALSE(signin_partition_name_2.empty());
  EXPECT_EQ(signin_partition_name_2,
            GetSigninPartitionManager()->GetCurrentStoragePartitionName());

  // Make sure that the StoragePartition has not been re-used.
  EXPECT_NE(signin_partition_name_1, signin_partition_name_2);
  EXPECT_NE(signin_partition_1, signin_partition_2);

  // Make sure that the first StoragePartition has been cleared automatically.
  ExpectOneClearPartitionTask(signin_partition_1);
  FinishAllClearPartitionTasks();

  // Make sure that the second StoragePartition will be cleared when we
  // explicitly close the current sign-in session.
  bool closure_called = false;
  base::RepeatingClosure partition_cleared_closure = base::BindRepeating(
      [](bool* closure_called_ptr) { *closure_called_ptr = true; },
      &closure_called);

  GetSigninPartitionManager()->CloseCurrentSigninSession(
      partition_cleared_closure);
  EXPECT_FALSE(closure_called);

  ExpectOneClearPartitionTask(signin_partition_2);
  FinishAllClearPartitionTasks();

  EXPECT_TRUE(closure_called);
}

TEST_F(SigninPartitionManagerTest, HttpAuthCacheTransferred) {
  base::RunLoop loop_prepare;
  content::GetIOThreadTaskRunner({})->PostTaskAndReply(
      FROM_HERE,
      base::BindOnce(AddEntryToHttpAuthCache,
                     base::Unretained(GetSystemNetworkContextImpl())),
      loop_prepare.QuitClosure());
  loop_prepare.Run();

  RunStartSigninSesssion(signin_ui_web_contents());

  bool entry_found = false;
  base::RunLoop loop_check;
  content::GetIOThreadTaskRunner({})->PostTaskAndReply(
      FROM_HERE,
      base::BindOnce(IsEntryInHttpAuthCache,
                     base::Unretained(GetSigninNetworkContextImpl()),
                     &entry_found),
      loop_check.QuitClosure());
  loop_check.Run();
  EXPECT_TRUE(entry_found);
}

}  // namespace login
}  // namespace ash
