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

#include <stddef.h>
#include <stdint.h>

#include <string>
#include <utility>
#include <vector>

#include "base/command_line.h"
#include "base/files/file_path.h"
#include "base/files/file_util.h"
#include "base/files/scoped_temp_dir.h"
#include "base/functional/bind.h"
#include "base/functional/callback_helpers.h"
#include "base/memory/raw_ptr.h"
#include "base/path_service.h"
#include "base/run_loop.h"
#include "base/strings/string_util.h"
#include "base/strings/stringprintf.h"
#include "base/strings/utf_string_conversions.h"
#include "base/test/metrics/histogram_tester.h"
#include "base/test/repeating_test_future.h"
#include "base/test/scoped_feature_list.h"
#include "base/test/test_file_util.h"
#include "base/threading/thread_restrictions.h"
#include "build/build_config.h"
#include "chrome/app/chrome_command_ids.h"
#include "chrome/browser/download/chrome_download_manager_delegate.h"
#include "chrome/browser/download/download_core_service.h"
#include "chrome/browser/download/download_core_service_factory.h"
#include "chrome/browser/download/download_history.h"
#include "chrome/browser/download/download_prefs.h"
#include "chrome/browser/download/save_package_file_picker.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/ui/browser_commands.h"
#include "chrome/browser/ui/browser_window.h"
#include "chrome/browser/ui/browser_window/public/browser_window_interface.h"
#include "chrome/browser/ui/tabs/tab_strip_model.h"
#include "chrome/common/chrome_paths.h"
#include "chrome/common/chrome_switches.h"
#include "chrome/common/pref_names.h"
#include "chrome/common/url_constants.h"
#include "chrome/test/base/in_process_browser_test.h"
#include "chrome/test/base/ui_test_utils.h"
#include "components/download/public/common/download_features.h"
#include "components/download/public/common/download_item.h"
#include "components/history/core/browser/download_constants.h"
#include "components/history/core/browser/download_row.h"
#include "components/prefs/pref_member.h"
#include "components/prefs/pref_service.h"
#include "components/security_state/core/security_state.h"
#include "components/services/quarantine/test_support.h"
#include "content/public/browser/download_manager.h"
#include "content/public/browser/render_frame_host.h"
#include "content/public/browser/render_process_host.h"
#include "content/public/browser/web_contents.h"
#include "content/public/common/content_switches.h"
#include "content/public/common/url_constants.h"
#include "content/public/test/browser_test.h"
#include "content/public/test/browser_test_utils.h"
#include "content/public/test/download_test_observer.h"
#include "content/public/test/no_renderer_crashes_assertion.h"
#include "net/base/filename_util.h"
#include "net/dns/mock_host_resolver.h"
#include "net/test/embedded_test_server/embedded_test_server.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "ui/shell_dialogs/fake_select_file_dialog.h"

#if BUILDFLAG(IS_CHROMEOS)
#include "base/test/test_future.h"
#include "chromeos/dbus/dlp/dlp_client.h"
#endif  // BUILDFLAG(IS_CHROMEOS)

using content::BrowserContext;
using content::BrowserThread;
using content::DownloadManager;
using content::RenderFrameHost;
using content::RenderProcessHost;
using content::WebContents;
using download::DownloadItem;
using testing::_;
using testing::ContainsRegex;
using testing::HasSubstr;
using ui::FakeSelectFileDialog;

namespace {

// Returns file contents with each continuous run of whitespace replaced by a
// single space.
std::string ReadFileAndCollapseWhitespace(const base::FilePath& file_path) {
  std::string file_contents;
  if (!base::ReadFileToString(file_path, &file_contents)) {
    ADD_FAILURE() << "Failed to read \"" << file_path.value() << "\" file.";
    return std::string();
  }

  return base::CollapseWhitespaceASCII(file_contents, false);
}

// Prepends a Mark Of The Web to `file_contents`.
std::string PrependMotw(const std::string& file_contents, const GURL& url) {
  return base::StringPrintf("<!-- saved from url=(%04d)%s --> ",
                            url.spec().size(), url.spec().c_str()) +
         file_contents;
}

// Waits for an item record in the downloads database to match |filter|. See
// DownloadStoredProperly() below for an example filter.
class DownloadPersistedObserver : public DownloadHistory::Observer {
 public:
  typedef base::RepeatingCallback<bool(DownloadItem* item,
                                       const history::DownloadRow&)>
      PersistedFilter;

  DownloadPersistedObserver(Profile* profile, const PersistedFilter& filter)
      : profile_(profile),
        filter_(filter),
        persisted_(false) {
    DownloadCoreService* service =
        DownloadCoreServiceFactory::GetForBrowserContext(profile_);
    if (service) {
      service->InitializeHistory();
      if (service->GetDownloadHistory()) {
        service->GetDownloadHistory()->AddObserver(this);
      }
    }
  }

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

  ~DownloadPersistedObserver() override {
    DownloadCoreService* service =
        DownloadCoreServiceFactory::GetForBrowserContext(profile_);
    if (service && service->GetDownloadHistory())
      service->GetDownloadHistory()->RemoveObserver(this);
  }

  bool WaitForPersisted() {
    if (persisted_)
      return true;
    base::RunLoop run_loop;
    quit_waiting_callback_ = run_loop.QuitClosure();
    run_loop.Run();
    return persisted_;
  }

  void OnDownloadStored(DownloadItem* item,
                        const history::DownloadRow& info) override {
    persisted_ = persisted_ || filter_.Run(item, info);
    if (persisted_ && quit_waiting_callback_)
      std::move(quit_waiting_callback_).Run();
  }

 private:
  raw_ptr<Profile> profile_;
  PersistedFilter filter_;
  base::OnceClosure quit_waiting_callback_;
  bool persisted_;
};

// Waits for an item record to be removed from the downloads database.
class DownloadRemovedObserver : public DownloadPersistedObserver {
 public:
  DownloadRemovedObserver(Profile* profile, int32_t download_id)
      : DownloadPersistedObserver(profile, PersistedFilter()),
        removed_(false),
        download_id_(download_id) {}

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

  ~DownloadRemovedObserver() override = default;

  bool WaitForRemoved() {
    if (removed_)
      return true;
    base::RunLoop run_loop;
    quit_waiting_callback_ = run_loop.QuitClosure();
    run_loop.Run();
    return removed_;
  }

  void OnDownloadStored(DownloadItem* item,
                        const history::DownloadRow& info) override {}

  void OnDownloadsRemoved(const DownloadHistory::IdSet& ids) override {
    removed_ = ids.find(download_id_) != ids.end();
    if (removed_ && quit_waiting_callback_)
      std::move(quit_waiting_callback_).Run();
  }

 private:
  bool removed_;
  base::OnceClosure quit_waiting_callback_;
  int32_t download_id_;
};

bool DownloadStoredProperly(const GURL& expected_url,
                            const base::FilePath& expected_path,
                            int64_t num_files,
                            history::DownloadState expected_state,
                            DownloadItem* item,
                            const history::DownloadRow& info) {
  // This function may be called multiple times for a given test. Returning
  // false doesn't necessarily mean that the test has failed or will fail, it
  // might just mean that the test hasn't passed yet.
  if (!expected_path.empty() && info.target_path != expected_path) {
    DVLOG(20) << __FUNCTION__ << " " << info.target_path.value()
              << " != " << expected_path.value();
    return false;
  }
  if (info.url_chain.size() != 1u) {
    DVLOG(20) << __FUNCTION__ << " " << info.url_chain.size() << " != 1";
    return false;
  }
  if (info.url_chain[0] != expected_url) {
    DVLOG(20) << __FUNCTION__ << " " << info.url_chain[0].spec()
              << " != " << expected_url.spec();
    return false;
  }
  if ((num_files >= 0) && (info.received_bytes != num_files)) {
    DVLOG(20) << __FUNCTION__ << " " << num_files
              << " != " << info.received_bytes;
    return false;
  }
  if (info.state != expected_state) {
    DVLOG(20) << __FUNCTION__ << " " << info.state << " != " << expected_state;
    return false;
  }
  return true;
}

static const char kAppendedExtension[] = ".html";

// Loosely based on logic in DownloadTestObserver.
class DownloadItemCreatedObserver : public DownloadManager::Observer {
 public:
  explicit DownloadItemCreatedObserver(DownloadManager* manager)
      : manager_(manager) {
    manager->AddObserver(this);
  }

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

  ~DownloadItemCreatedObserver() override {
    if (manager_)
      manager_->RemoveObserver(this);
  }

  // Wait for the first download item created after object creation.
  // Note that this class provides no protection against the download
  // being destroyed between creation and return of WaitForNewDownloadItem();
  // the caller must guarantee that in some other fashion.
  void WaitForDownloadItem(
      std::vector<raw_ptr<DownloadItem, VectorExperimental>>* items_seen) {
    if (!manager_) {
      // The manager went away before we were asked to wait; return
      // what we have, even if it's null.
      *items_seen = items_seen_;
      return;
    }

    if (items_seen_.empty()) {
      base::RunLoop run_loop;
      quit_waiting_callback_ = run_loop.QuitClosure();
      run_loop.Run();
    }

    *items_seen = items_seen_;
    return;
  }

 private:
  // DownloadManager::Observer
  void OnDownloadCreated(DownloadManager* manager,
                         DownloadItem* item) override {
    DCHECK_EQ(manager, manager_);
    items_seen_.push_back(item);

    if (quit_waiting_callback_)
      std::move(quit_waiting_callback_).Run();
  }

  void ManagerGoingDown(DownloadManager* manager) override {
    manager_->RemoveObserver(this);
    manager_ = nullptr;
    if (quit_waiting_callback_)
      std::move(quit_waiting_callback_).Run();
  }

  base::OnceClosure quit_waiting_callback_;
  raw_ptr<DownloadManager> manager_;
  std::vector<raw_ptr<DownloadItem, VectorExperimental>> items_seen_;
};

class SavePageBrowserTest : public InProcessBrowserTest {
 public:
  SavePageBrowserTest() = default;

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

 protected:
  void SetUp() override {
    ASSERT_TRUE(embedded_test_server()->InitializeAndListen());
    content::SetupCrossSiteRedirector(embedded_test_server());
    embedded_test_server()->StartAcceptingConnections();

    ASSERT_TRUE(base::PathService::Get(chrome::DIR_TEST_DATA, &test_dir_));
    InProcessBrowserTest::SetUp();
  }

  GURL NavigateToMockURL(const std::string& prefix) {
    GURL url = embedded_test_server()->GetURL("/save_page/" + prefix + ".htm");
    EXPECT_TRUE(ui_test_utils::NavigateToURL(browser(), url));
    return url;
  }

  // Returns full paths of destination file and directory.
  void GetDestinationPaths(const std::string& prefix,
                           base::FilePath* full_file_name,
                           base::FilePath* dir,
                           content::SavePageType save_page_type =
                               content::SAVE_PAGE_TYPE_AS_COMPLETE_HTML) {
    std::string extension =
        (save_page_type == content::SAVE_PAGE_TYPE_AS_MHTML) ? ".mht" : ".htm";
    *full_file_name = GetSaveDir().AppendASCII(prefix + extension);
    *dir = GetSaveDir().AppendASCII(prefix + "_files");
  }

  WebContents* GetCurrentTab(BrowserWindowInterface* browser) const {
    WebContents* current_tab =
        browser->GetTabStripModel()->GetActiveWebContents();
    EXPECT_TRUE(current_tab);
    return current_tab;
  }

  // Returns true if and when there was a single download created, and its url
  // is |expected_url|.
  bool VerifySavePackageExpectations(BrowserWindowInterface* browser,
                                     const GURL& expected_url) const {
    // Generally, there should only be one download item created
    // in all of these tests.  If it's already here, grab it; if not,
    // wait for it to show up.
    std::vector<raw_ptr<DownloadItem, VectorExperimental>> items;
    DownloadManager* manager = browser->GetProfile()->GetDownloadManager();
    manager->GetAllDownloads(&items);
    if (items.empty())
      DownloadItemCreatedObserver(manager).WaitForDownloadItem(&items);

    if (items.size() != 1) {
      ADD_FAILURE();
      return false;
    }

    return items[0]->GetOriginalUrl() == expected_url;
  }

  void SaveCurrentTab(const GURL& url,
                      content::SavePageType save_page_type,
                      const std::string& prefix_for_output_files,
                      int expected_number_of_files,
                      base::FilePath* output_dir,
                      base::FilePath* main_file_name) {
    GetDestinationPaths(prefix_for_output_files, main_file_name, output_dir,
                        save_page_type);
    DownloadPersistedObserver persisted(
        browser()->GetProfile(),
        base::BindRepeating(&DownloadStoredProperly, url, *main_file_name,
                            expected_number_of_files,
                            history::DownloadState::COMPLETE));
    base::RunLoop run_loop;
    content::SavePackageFinishedObserver observer(
        browser()->GetProfile()->GetDownloadManager(), run_loop.QuitClosure());
    ASSERT_TRUE(GetCurrentTab(browser())
                    ->SavePage(*main_file_name, *output_dir, save_page_type));

    run_loop.Run();
    ASSERT_TRUE(VerifySavePackageExpectations(browser(), url));
    persisted.WaitForPersisted();
  }

  // Note on synchronization:
  //
  // For each Save Page As operation, we create a corresponding shell
  // DownloadItem to display progress to the user.  That DownloadItem goes
  // through its own state transitions, including being persisted out to the
  // history database, and the download shelf is not shown until after the
  // persistence occurs.  Save Package completion (and marking the DownloadItem
  // as completed) occurs asynchronously from persistence.  Thus if we want to
  // examine either UI state or DB state, we need to wait until both the save
  // package operation is complete and the relevant download item has been
  // persisted.

  DownloadManager* GetDownloadManager() const {
    auto* download_manager = browser()->GetProfile()->GetDownloadManager();
    EXPECT_TRUE(download_manager);
    return download_manager;
  }

  // Returns full path to a file in chrome/test/data/save_page directory.
  base::FilePath GetTestDirFile(const std::string& file_name) {
    const base::FilePath::CharType kTestDir[] = FILE_PATH_LITERAL("save_page");
    return test_dir_.Append(base::FilePath(kTestDir)).AppendASCII(file_name);
  }

  base::FilePath GetSaveDir() {
    return DownloadPrefs(browser()->GetProfile()).DownloadPath();
  }

  // Path to directory containing test data.
  base::FilePath test_dir_;
};

IN_PROC_BROWSER_TEST_F(SavePageBrowserTest, SaveHTMLOnly) {
  GURL url = NavigateToMockURL("a");

  base::FilePath full_file_name, dir;
  SaveCurrentTab(url, content::SAVE_PAGE_TYPE_AS_ONLY_HTML, "a", 1, &dir,
                 &full_file_name);
  ASSERT_FALSE(HasFailure());

  base::ScopedAllowBlockingForTesting allow_blocking;
  EXPECT_TRUE(base::PathExists(full_file_name));
  EXPECT_FALSE(base::PathExists(dir));
  EXPECT_TRUE(base::ContentsEqual(GetTestDirFile("a.htm"), full_file_name));
}

IN_PROC_BROWSER_TEST_F(SavePageBrowserTest, SaveFileURL) {
  GURL url = net::FilePathToFileURL(GetTestDirFile("text.txt"));
  ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), url));

  base::FilePath full_file_name, dir;
  SaveCurrentTab(url, content::SAVE_PAGE_TYPE_AS_ONLY_HTML, "test", 1, &dir,
                 &full_file_name);
  ASSERT_FALSE(HasFailure());

  base::ScopedAllowBlockingForTesting allow_blocking;
  EXPECT_TRUE(base::PathExists(full_file_name));
  EXPECT_FALSE(base::PathExists(dir));
  EXPECT_TRUE(base::ContentsEqual(GetTestDirFile("text.txt"), full_file_name));
#if BUILDFLAG(IS_WIN)
  // Local file URL will not be quarantined.
  EXPECT_FALSE(quarantine::IsFileQuarantined(full_file_name, GURL(), GURL()));
#endif
}

IN_PROC_BROWSER_TEST_F(SavePageBrowserTest,
                       SaveHTMLOnly_CrossOriginReadPolicy) {
  GURL url = embedded_test_server()->GetURL(
      "/downloads/cross-origin-resource-policy-resource.txt");
  ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), url));

  base::FilePath full_file_name, dir;
  SaveCurrentTab(url, content::SAVE_PAGE_TYPE_AS_ONLY_HTML, "a", 1, &dir,
                 &full_file_name);
  ASSERT_FALSE(HasFailure());

  base::ScopedAllowBlockingForTesting allow_blocking;
  EXPECT_TRUE(base::PathExists(full_file_name));
  EXPECT_FALSE(base::PathExists(dir));

  const base::FilePath::CharType kTestDir[] = FILE_PATH_LITERAL("downloads");
  const base::FilePath kTestFile =
      test_dir_.Append(base::FilePath(kTestDir))
          .AppendASCII("cross-origin-resource-policy-resource.txt");
  EXPECT_TRUE(base::ContentsEqual(kTestFile, full_file_name));
}

// TODO(crbug.com/40805571): Flaky on mac arm64.
#if BUILDFLAG(IS_MAC) && defined(ARCH_CPU_ARM64)
#define MAYBE_SaveHTMLOnlyCancel DISABLED_SaveHTMLOnlyCancel
#else
#define MAYBE_SaveHTMLOnlyCancel SaveHTMLOnlyCancel
#endif
IN_PROC_BROWSER_TEST_F(SavePageBrowserTest, MAYBE_SaveHTMLOnlyCancel) {
  GURL url = NavigateToMockURL("a");
  DownloadManager* manager = GetDownloadManager();
  std::vector<raw_ptr<DownloadItem, VectorExperimental>> downloads;
  manager->GetAllDownloads(&downloads);
  ASSERT_EQ(0u, downloads.size());

  base::FilePath full_file_name, dir;
  GetDestinationPaths("a", &full_file_name, &dir);
  DownloadItemCreatedObserver creation_observer(manager);
  DownloadPersistedObserver persisted(
      browser()->GetProfile(),
      base::BindRepeating(&DownloadStoredProperly, url, full_file_name, -1,
                          history::DownloadState::CANCELLED));
  // -1 to disable number of files check; we don't update after cancel, and
  // we don't know when the single file completed in relationship to
  // the cancel.

  ASSERT_TRUE(GetCurrentTab(browser())->SavePage(full_file_name, dir,
                                        content::SAVE_PAGE_TYPE_AS_ONLY_HTML));
  std::vector<raw_ptr<DownloadItem, VectorExperimental>> items;
  creation_observer.WaitForDownloadItem(&items);
  ASSERT_EQ(1UL, items.size());
  ASSERT_EQ(url.spec(), items[0]->GetOriginalUrl().spec());
  items[0]->Cancel(true);
  // TODO(rdsmith): Fix DII::Cancel() to actually cancel the save package.
  // Currently it's ignored.

  persisted.WaitForPersisted();

  // TODO(benjhayden): Figure out how to safely wait for SavePackage's finished
  // notification, then expect the contents of the downloaded file.
}

// Test that saving an HTML file with long (i.e. > 65536 bytes) text content
// does not crash the browser despite the renderer requiring more than one
// "pass" to serialize the HTML content (see crash from crbug.com/40693777).
IN_PROC_BROWSER_TEST_F(SavePageBrowserTest, SaveHTMLWithLongTextContent) {
  GURL url =
      embedded_test_server()->GetURL("/save_page/long-text-content.html");
  ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), url));

  base::FilePath full_file_name, dir;
  SaveCurrentTab(url, content::SAVE_PAGE_TYPE_AS_COMPLETE_HTML,
                 "long-text-content", 1, &dir, &full_file_name);

  ASSERT_FALSE(HasFailure());

  base::ScopedAllowBlockingForTesting allow_blocking;
  EXPECT_TRUE(base::PathExists(full_file_name));
  EXPECT_FALSE(base::PathExists(dir));

  // Besides checking that the renderer didn't crash, test also that the HTML
  // content saved is the expected one (i.e. the whole HTML, no truncation).
  EXPECT_EQ(ReadFileAndCollapseWhitespace(full_file_name),
            PrependMotw(ReadFileAndCollapseWhitespace(
                            GetTestDirFile("long-text-content.saved.html")),
                        url));
}

class DelayingDownloadManagerDelegate : public ChromeDownloadManagerDelegate {
 public:
  explicit DelayingDownloadManagerDelegate(Profile* profile)
    : ChromeDownloadManagerDelegate(profile) {
  }

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

  ~DelayingDownloadManagerDelegate() override = default;

  bool ShouldCompleteDownload(
      download::DownloadItem* item,
      base::OnceClosure user_complete_callback) override {
    return false;
  }
};

// Disabled on multiple platforms due to flakiness. crbug.com/41236339
IN_PROC_BROWSER_TEST_F(SavePageBrowserTest, DISABLED_SaveHTMLOnlyTabDestroy) {
  GURL url = NavigateToMockURL("a");
  auto delaying_delegate = std::make_unique<DelayingDownloadManagerDelegate>(
      browser()->GetProfile());
  delaying_delegate->GetDownloadIdReceiverCallback().Run(
      download::DownloadItem::kInvalidId + 1);
  DownloadCoreServiceFactory::GetForBrowserContext(browser()->GetProfile())
      ->SetDownloadManagerDelegateForTesting(std::move(delaying_delegate));
  DownloadManager* manager = GetDownloadManager();
  std::vector<raw_ptr<DownloadItem, VectorExperimental>> downloads;
  manager->GetAllDownloads(&downloads);
  ASSERT_EQ(0u, downloads.size());

  base::FilePath full_file_name, dir;
  GetDestinationPaths("a", &full_file_name, &dir);
  DownloadItemCreatedObserver creation_observer(manager);
  ASSERT_TRUE(GetCurrentTab(browser())->SavePage(full_file_name, dir,
                                        content::SAVE_PAGE_TYPE_AS_ONLY_HTML));
  std::vector<raw_ptr<DownloadItem, VectorExperimental>> items;
  creation_observer.WaitForDownloadItem(&items);
  ASSERT_EQ(1u, items.size());

  // Close the tab; does this cancel the download?
  GetCurrentTab(browser())->Close();
  EXPECT_EQ(DownloadItem::CANCELLED, items[0]->GetState());

  base::ScopedAllowBlockingForTesting allow_blocking;
  EXPECT_FALSE(base::PathExists(full_file_name));
  EXPECT_FALSE(base::PathExists(dir));
}

IN_PROC_BROWSER_TEST_F(SavePageBrowserTest, SaveViewSourceHTMLOnly) {
  // TODO(lukasza): https://crbug.com/40631088: Disallow renderer crashes once
  // the bug is fixed.
  content::ScopedAllowRendererCrashes scoped_allow_renderer_crashes;

  GURL mock_url = embedded_test_server()->GetURL("/save_page/a.htm");
  GURL view_source_url =
      GURL(content::kViewSourceScheme + std::string(":") + mock_url.spec());
  GURL actual_page_url = embedded_test_server()->GetURL("/save_page/a.htm");
  ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), view_source_url));

  base::FilePath full_file_name, dir;
  SaveCurrentTab(actual_page_url, content::SAVE_PAGE_TYPE_AS_ONLY_HTML, "a", 1,
                 &dir, &full_file_name);
  ASSERT_FALSE(HasFailure());

  base::ScopedAllowBlockingForTesting allow_blocking;
  EXPECT_TRUE(base::PathExists(full_file_name));
  EXPECT_FALSE(base::PathExists(dir));
  EXPECT_TRUE(base::ContentsEqual(GetTestDirFile("a.htm"), full_file_name));
}

// Regression test for https://crbug.com/40632177 (saving a page that was served
// with `Cross-Origin-Resource-Policy: same-origin` http response header).
IN_PROC_BROWSER_TEST_F(SavePageBrowserTest, SaveCompleteHTML) {
  GURL url = NavigateToMockURL("b");

  base::FilePath full_file_name, dir;
  SaveCurrentTab(url, content::SAVE_PAGE_TYPE_AS_COMPLETE_HTML, "b", 3, &dir,
                 &full_file_name);
  ASSERT_FALSE(HasFailure());

  base::ScopedAllowBlockingForTesting allow_blocking;
  EXPECT_TRUE(base::PathExists(full_file_name));
  EXPECT_TRUE(base::PathExists(dir));

  EXPECT_EQ(
      ReadFileAndCollapseWhitespace(full_file_name),
      PrependMotw(ReadFileAndCollapseWhitespace(GetTestDirFile("b.saved1.htm")),
                  url));
  EXPECT_TRUE(
      base::ContentsEqual(GetTestDirFile("1.png"), dir.AppendASCII("1.png")));
  EXPECT_EQ(ReadFileAndCollapseWhitespace(dir.AppendASCII("1.css")),
            ReadFileAndCollapseWhitespace(GetTestDirFile("1.css")));
}

IN_PROC_BROWSER_TEST_F(SavePageBrowserTest,
                       SaveDuringInitialNavigationIncognito) {
  // Open an Incognito window.
  BrowserWindowInterface* incognito = CreateIncognitoBrowser();  // Waits.
  ASSERT_TRUE(incognito);

  // Create a download item creation waiter on that window.
  DownloadItemCreatedObserver creation_observer(
      incognito->GetProfile()->GetDownloadManager());

  // Navigate, unblocking with new tab.
  GURL url = embedded_test_server()->GetURL("/save_page/b.htm");
  NavigateToURLWithDisposition(incognito, url,
                               WindowOpenDisposition::NEW_FOREGROUND_TAB,
                               ui_test_utils::BROWSER_TEST_WAIT_FOR_TAB);

  // Save the page before completion.
  base::FilePath full_file_name, dir;
  GetDestinationPaths("b", &full_file_name, &dir);

  base::RunLoop run_loop;
  content::SavePackageFinishedObserver observer(
      incognito->GetProfile()->GetDownloadManager(), run_loop.QuitClosure());
  ASSERT_TRUE(GetCurrentTab(incognito)->SavePage(
      full_file_name, dir, content::SAVE_PAGE_TYPE_AS_COMPLETE_HTML));

  run_loop.Run();
  ASSERT_TRUE(VerifySavePackageExpectations(incognito, url));

  // We can't check more than this because SavePackage is racing with
  // the page load.  If the page load won the race, then SavePackage
  // might have completed. If the page load lost the race, then
  // SavePackage will cancel because there aren't any resources to
  // save.
}

IN_PROC_BROWSER_TEST_F(SavePageBrowserTest, NoSave) {
  ASSERT_TRUE(
      ui_test_utils::NavigateToURL(browser(), GURL(url::kAboutBlankURL)));
  EXPECT_FALSE(chrome::CanSavePage(browser()));
}

IN_PROC_BROWSER_TEST_F(SavePageBrowserTest, FileNameFromPageTitle) {
  GURL url = NavigateToMockURL("b");

  base::FilePath full_file_name = GetSaveDir().AppendASCII(
      std::string("Test page for saving page feature") + kAppendedExtension);
  base::FilePath dir =
      GetSaveDir().AppendASCII("Test page for saving page feature_files");
  DownloadPersistedObserver persisted(
      browser()->GetProfile(),
      base::BindRepeating(&DownloadStoredProperly, url, full_file_name, 3,
                          history::DownloadState::COMPLETE));
  base::RunLoop run_loop;
  content::SavePackageFinishedObserver observer(
      browser()->GetProfile()->GetDownloadManager(), run_loop.QuitClosure());
  ASSERT_TRUE(GetCurrentTab(browser())->SavePage(
      full_file_name, dir, content::SAVE_PAGE_TYPE_AS_COMPLETE_HTML));

  run_loop.Run();
  ASSERT_TRUE(VerifySavePackageExpectations(browser(), url));
  persisted.WaitForPersisted();

  base::ScopedAllowBlockingForTesting allow_blocking;
  EXPECT_TRUE(base::PathExists(full_file_name));
  EXPECT_TRUE(base::PathExists(dir));

  EXPECT_EQ(
      ReadFileAndCollapseWhitespace(full_file_name),
      PrependMotw(ReadFileAndCollapseWhitespace(GetTestDirFile("b.saved2.htm")),
                  url));
  EXPECT_TRUE(
      base::ContentsEqual(GetTestDirFile("1.png"), dir.AppendASCII("1.png")));
  EXPECT_EQ(ReadFileAndCollapseWhitespace(dir.AppendASCII("1.css")),
            ReadFileAndCollapseWhitespace(GetTestDirFile("1.css")));
}

IN_PROC_BROWSER_TEST_F(SavePageBrowserTest, RemoveFromList) {
  GURL url = NavigateToMockURL("a");

  base::FilePath full_file_name, dir;
  SaveCurrentTab(url, content::SAVE_PAGE_TYPE_AS_ONLY_HTML, "a", 1, &dir,
                 &full_file_name);
  ASSERT_FALSE(HasFailure());

  DownloadManager* manager = GetDownloadManager();
  std::vector<raw_ptr<DownloadItem, VectorExperimental>> downloads;
  manager->GetAllDownloads(&downloads);
  ASSERT_EQ(1UL, downloads.size());

  DownloadRemovedObserver removed(browser()->GetProfile(),
                                  downloads[0]->GetId());
  downloads[0]->Remove();
  removed.WaitForRemoved();

  base::ScopedAllowBlockingForTesting allow_blocking;
  EXPECT_TRUE(base::PathExists(full_file_name));
  EXPECT_FALSE(base::PathExists(dir));
  EXPECT_TRUE(base::ContentsEqual(GetTestDirFile("a.htm"), full_file_name));
}

// This tests that a webpage with the title "test.exe" is saved as
// "test.exe.htm".
// We probably don't care to handle this on Linux or Mac.
#if BUILDFLAG(IS_WIN)
IN_PROC_BROWSER_TEST_F(SavePageBrowserTest, CleanFilenameFromPageTitle) {
  base::FilePath download_dir =
      DownloadPrefs::FromDownloadManager(GetDownloadManager())->
          DownloadPath();
  base::FilePath full_file_name =
      download_dir.AppendASCII(std::string("test.exe") + kAppendedExtension);
  base::FilePath dir = download_dir.AppendASCII("test.exe_files");

  base::ScopedAllowBlockingForTesting allow_blocking;
  EXPECT_FALSE(base::PathExists(full_file_name));
  GURL url = embedded_test_server()->GetURL("/save_page/c.htm");
  ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), url));

  SavePackageFilePicker::SetShouldPromptUser(false);
  base::RunLoop run_loop;
  content::SavePackageFinishedObserver observer(
      browser()->GetProfile()->GetDownloadManager(), run_loop.QuitClosure());
  chrome::SavePage(browser());
  run_loop.Run();

  EXPECT_TRUE(base::PathExists(full_file_name));

  EXPECT_TRUE(base::DieFileDie(full_file_name, false));
  EXPECT_TRUE(base::DieFileDie(dir, true));
}
#endif

// Tests that the SecurityLevel histograms are logged for save page downloads.
IN_PROC_BROWSER_TEST_F(SavePageBrowserTest, SecurityLevelHistogram) {
  base::HistogramTester histogram_tester;
  GURL url = NavigateToMockURL("a");
  base::FilePath full_file_name, dir;
  SaveCurrentTab(url, content::SAVE_PAGE_TYPE_AS_ONLY_HTML, "a", 1, &dir,
                 &full_file_name);
  histogram_tester.ExpectUniqueSample("Security.SecurityLevel.DownloadStarted",
                                      security_state::NONE, 1);
}

// Tests that a page can be saved as MHTML.
// Flaky on Windows, crbug.com/40671774
#if BUILDFLAG(IS_WIN)
#define MAYBE_SavePageAsMHTML DISABLED_SavePageAsMHTML
#else
#define MAYBE_SavePageAsMHTML SavePageAsMHTML
#endif
IN_PROC_BROWSER_TEST_F(SavePageBrowserTest, MAYBE_SavePageAsMHTML) {
  static const int64_t kFileSizeMin = 2758;
  GURL url = NavigateToMockURL("b");
  base::FilePath download_dir = DownloadPrefs::FromDownloadManager(
      GetDownloadManager())->DownloadPath();
  base::FilePath full_file_name =
      download_dir.AppendASCII("Test page for saving page feature.mhtml");

  SavePackageFilePicker::SetShouldPromptUser(true);
  DownloadPersistedObserver persisted(
      browser()->GetProfile(),
      base::BindRepeating(&DownloadStoredProperly, url, full_file_name, -1,
                          history::DownloadState::COMPLETE));

  FakeSelectFileDialog::Factory* select_file_dialog_factory =
      FakeSelectFileDialog::RegisterFactory();
  // Save page and run until the fake select file dialog opens.
  {
    base::RunLoop run_loop;
    select_file_dialog_factory->SetOpenCallback(run_loop.QuitClosure());
    chrome::SavePage(browser());
    run_loop.Run();
  }

// On ChromeOS, the default should be MHTML.
#if BUILDFLAG(IS_CHROMEOS)
  ASSERT_EQ("mhtml",
            select_file_dialog_factory->GetLastDialog()->default_extension());
#else
  ASSERT_EQ("html",
            select_file_dialog_factory->GetLastDialog()->default_extension());
#endif

  // Save the file as MHTML. Run until save completes.
  base::RunLoop run_loop;
  content::SavePackageFinishedObserver observer(
      browser()->GetProfile()->GetDownloadManager(), run_loop.QuitClosure());
  ASSERT_TRUE(select_file_dialog_factory->GetLastDialog()->CallFileSelected(
      full_file_name, "mhtml"));
  run_loop.Run();

  ASSERT_TRUE(VerifySavePackageExpectations(browser(), url));
  persisted.WaitForPersisted();

  base::ScopedAllowBlockingForTesting allow_blocking;
  std::optional<int64_t> actual_file_size = base::GetFileSize(full_file_name);
  ASSERT_TRUE(actual_file_size.has_value());
  EXPECT_LE(kFileSizeMin, actual_file_size.value());

  std::string contents;
  EXPECT_TRUE(base::ReadFileToString(full_file_name, &contents));
  // Test for a CSS encoded character.  This used to use HTML encoding.
  EXPECT_THAT(contents, HasSubstr("content: \"\\e003 \\e004 b\""));
}

// Tests that if we default our file picker to MHTML due to user preference we
// update the suggested file name to end with .mhtml.
IN_PROC_BROWSER_TEST_F(SavePageBrowserTest,
                       SavePageAsMHTMLByPrefUpdatesExtension) {
  SavePackageFilePicker::SetShouldPromptUser(false);
  DownloadPrefs* download_prefs =
      DownloadPrefs::FromDownloadManager(GetDownloadManager());
  base::FilePath download_dir = download_prefs->DownloadPath();
  base::FilePath full_file_name = download_dir.AppendASCII("test_page");
  download_prefs->SetSaveFileType(content::SAVE_PAGE_TYPE_AS_MHTML);

  content::SavePackagePathPickedParams received_params;
  content::SavePackagePathPickedCallback callback = base::BindOnce(
      [](content::SavePackagePathPickedParams* received_params,
         content::SavePackagePathPickedParams params,
         content::SavePackageDownloadCreatedCallback cb) {
        *received_params = params;
      },
      &received_params);

  // Deletes itself.
  new SavePackageFilePicker(
      /*web_contents=*/GetCurrentTab(browser()),
      /*suggested_path=*/full_file_name,
      /*default_extension=*/FILE_PATH_LITERAL(".html"),
      /*can_save_as_complete=*/true,
      /*download_prefs=*/download_prefs,
      /*callback=*/std::move(callback));

  EXPECT_TRUE(
      received_params.file_path.MatchesExtension(FILE_PATH_LITERAL(".mhtml")));
  EXPECT_EQ(received_params.save_type, content::SAVE_PAGE_TYPE_AS_MHTML);
}

// Flaky on Windows: https://crbug.com/40789916.
#if BUILDFLAG(IS_WIN)
#define MAYBE_SavePageBrowserTest_NonMHTML DISABLED_SavePageBrowserTest_NonMHTML
#else
#define MAYBE_SavePageBrowserTest_NonMHTML SavePageBrowserTest_NonMHTML
#endif
IN_PROC_BROWSER_TEST_F(SavePageBrowserTest,
                       MAYBE_SavePageBrowserTest_NonMHTML) {
  SavePackageFilePicker::SetShouldPromptUser(false);
  GURL url("data:text/plain,foo");
  ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), url));
  base::RunLoop run_loop;
  content::SavePackageFinishedObserver observer(
      browser()->GetProfile()->GetDownloadManager(), run_loop.QuitClosure());
  chrome::SavePage(browser());
  run_loop.Run();
  base::FilePath download_dir = DownloadPrefs::FromDownloadManager(
      GetDownloadManager())->DownloadPath();
  base::FilePath filename = download_dir.AppendASCII("dataurl.txt");
  base::ScopedAllowBlockingForTesting allow_blocking;
  ASSERT_TRUE(base::PathExists(filename));
  std::string contents;
  EXPECT_TRUE(base::ReadFileToString(filename, &contents));
  EXPECT_EQ("foo", contents);
}

// If a save-page-complete operation results in creating subresources that would
// otherwise be considered dangerous, such files should get a .download
// extension appended so that they won't be accidentally executed by the user.
IN_PROC_BROWSER_TEST_F(SavePageBrowserTest, DangerousSubresources) {
  GURL url =
      embedded_test_server()->GetURL("/save_page/dubious-subresources.html");

  ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), url));
  base::FilePath full_file_name, dir;
  SaveCurrentTab(url, content::SAVE_PAGE_TYPE_AS_COMPLETE_HTML,
                 "dubious-subresources", 2, &dir, &full_file_name);
  ASSERT_FALSE(HasFailure());

  base::ScopedAllowBlockingForTesting allow_blocking;
  EXPECT_TRUE(base::PathExists(full_file_name));
  EXPECT_TRUE(base::PathExists(dir.AppendASCII("not-a-crx.crx.download")));
}

// Test that we don't crash when the page contains an iframe that
// was handled as a download (http://crbug.com/41136499).
IN_PROC_BROWSER_TEST_F(SavePageBrowserTest, SaveDownloadableIFrame) {
  GURL url =
      embedded_test_server()->GetURL("/downloads/iframe-src-is-a-download.htm");

  // Wait for and then dismiss the non-save-page-as-related download item
  // (the one associated with downloading of "thisdayinhistory.xls" file).
  {
    GURL download_url =
        embedded_test_server()->GetURL("/downloads/thisdayinhistory.xls");
    DownloadPersistedObserver persisted(
        browser()->GetProfile(),
        base::BindRepeating(&DownloadStoredProperly, download_url,
                            base::FilePath(), -1,
                            history::DownloadState::COMPLETE));

    ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), url));

    ASSERT_TRUE(VerifySavePackageExpectations(browser(), download_url));
    persisted.WaitForPersisted();
    std::vector<raw_ptr<download::DownloadItem, VectorExperimental>> downloads;
    GetDownloadManager()->GetAllDownloads(&downloads);
    for (download::DownloadItem* download : downloads) {
      download->Remove();
    }
  }

  base::FilePath full_file_name, dir;
  SaveCurrentTab(url, content::SAVE_PAGE_TYPE_AS_COMPLETE_HTML,
                 "iframe-src-is-a-download", 3, &dir, &full_file_name);
  ASSERT_FALSE(HasFailure());

  base::ScopedAllowBlockingForTesting allow_blocking;
  EXPECT_TRUE(base::PathExists(full_file_name));
  EXPECT_TRUE(base::PathExists(dir.AppendASCII("thisdayinhistory.html")));
  EXPECT_TRUE(base::PathExists(dir.AppendASCII("no-such-file.html")));
}

// Test that file: URI won't be saved when referred to from an HTTP page.
// See also https://crbug.com/40084438.
IN_PROC_BROWSER_TEST_F(SavePageBrowserTest, SaveUnauthorizedResource) {
  GURL url = NavigateToMockURL("unauthorized-access");

  // Create a test file (that the web page should not have access to).
  base::ScopedAllowBlockingForTesting allow_blocking;
  base::ScopedTempDir temp_dir2;
  ASSERT_TRUE(temp_dir2.CreateUniqueTempDir());
  base::FilePath file_path =
      temp_dir2.GetPath().Append(FILE_PATH_LITERAL("should-not-save.jpg"));
  std::string file_content("fake-jpg");
  ASSERT_TRUE(base::WriteFile(file_path, file_content));

  // Refer to the test file from the test page.
  GURL file_url = net::FilePathToFileURL(file_path);
  ASSERT_TRUE(ExecJs(
      browser()->tab_strip_model()->GetWebContentsAt(0),
      base::StringPrintf("document.getElementById('resource1').src = '%s';",
                         file_url.spec().data())));

  // Save the current page.
  base::FilePath full_file_name, dir;
  SaveCurrentTab(url, content::SAVE_PAGE_TYPE_AS_COMPLETE_HTML,
                 "unauthorized-access", 2, &dir, &full_file_name);

  // We should not save resource that the web page didn't have access to.
  // (because executing a resource request can have side effects - for example
  // after https://crbug.com/40083783 a website from the internet should not be
  // able to issue a resource request to an intranet website and trigger
  // server-side actions in the internet;  this test uses a file: URI as a
  // canary for detecting whether a website can access restricted resources).
  EXPECT_FALSE(base::PathExists(dir.AppendASCII("should-not-save.jpg")));
}

#if BUILDFLAG(IS_WIN)
// Save a file and confirm that the file is correctly quarantined.
// TODO(https://crbug.com/502209268): Test has started failing on win11-arm64,
// likely after an OS update, because the file does not report as quarantined.
IN_PROC_BROWSER_TEST_F(SavePageBrowserTest, DISABLED_SaveURLQuarantine) {
  GURL url = embedded_test_server()->GetURL("/save_page/text.txt");
  ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), url));

  base::FilePath full_file_name, dir;
  SaveCurrentTab(url, content::SAVE_PAGE_TYPE_AS_ONLY_HTML, "test", 1, &dir,
                 &full_file_name);
  ASSERT_FALSE(HasFailure());

  base::ScopedAllowBlockingForTesting allow_blocking;
  EXPECT_TRUE(base::PathExists(full_file_name));
  EXPECT_FALSE(base::PathExists(dir));
  EXPECT_TRUE(base::ContentsEqual(GetTestDirFile("text.txt"), full_file_name));
  EXPECT_TRUE(quarantine::IsFileQuarantined(full_file_name, url, GURL()));
}
#endif

class SavePageDeferredDownloadHistoryBrowserTest : public SavePageBrowserTest {
 public:
  SavePageDeferredDownloadHistoryBrowserTest() {
    feature_list_.InitAndEnableFeature(
        download::features::kDeferredDownloadHistoryLoading);
  }

 private:
  base::test::ScopedFeatureList feature_list_;
};

IN_PROC_BROWSER_TEST_F(SavePageDeferredDownloadHistoryBrowserTest,
                       SaveHTMLOnly) {
  GURL url = NavigateToMockURL("a");

  base::FilePath full_file_name, dir;
  SaveCurrentTab(url, content::SAVE_PAGE_TYPE_AS_ONLY_HTML, "a", 1, &dir,
                 &full_file_name);
  ASSERT_FALSE(HasFailure());

  base::ScopedAllowBlockingForTesting allow_blocking;
  EXPECT_TRUE(base::PathExists(full_file_name));
  EXPECT_FALSE(base::PathExists(dir));
  EXPECT_TRUE(base::ContentsEqual(GetTestDirFile("a.htm"), full_file_name));
}

// Test suite that allows testing --site-per-process against cross-site frames.
// See http://dev.chromium.org/developers/design-documents/site-isolation.
class SavePageSitePerProcessBrowserTest : public SavePageBrowserTest {
 public:
  SavePageSitePerProcessBrowserTest() = default;

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

 protected:
  void SetUpCommandLine(base::CommandLine* command_line) override {
    SavePageBrowserTest::SetUpCommandLine(command_line);

    // Append --site-per-process flag.
    content::IsolateAllSitesForTesting(command_line);
  }

  void SetUpOnMainThread() override {
    SavePageBrowserTest::SetUpOnMainThread();

    // Used by the BrokenImage test which depends on *.no.such.host not
    // resolving to 127.0.0.1
    host_resolver()->AddRule("no.such.host", "128.0.0.1");
    host_resolver()->AddRule("*", "127.0.0.1");
  }
};

// Test for crbug.com/40433720.
IN_PROC_BROWSER_TEST_F(SavePageSitePerProcessBrowserTest, SaveAsCompleteHtml) {
  GURL url(
      embedded_test_server()->GetURL("a.com", "/save_page/frames-xsite.htm"));
  ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), url));

  base::FilePath full_file_name, dir;
  SaveCurrentTab(url, content::SAVE_PAGE_TYPE_AS_COMPLETE_HTML,
                 "frames-xsite-complete-html", 5, &dir, &full_file_name);
  ASSERT_FALSE(HasFailure());

  base::ScopedAllowBlockingForTesting allow_blocking;
  EXPECT_TRUE(base::DirectoryExists(dir));
  base::FilePath expected_files[] = {
      full_file_name,
      dir.AppendASCII("a.html"),  // From iframes.htm
      dir.AppendASCII("b.html"),  // From iframes.htm
      dir.AppendASCII("1.css"),   // From b.htm
      dir.AppendASCII("1.png"),   // Deduplicated from iframes.htm and b.htm.
  };
  for (auto file_path : expected_files) {
    EXPECT_TRUE(base::PathExists(file_path)) << "Does " << file_path.value()
                                             << " exist?";
    std::optional<int64_t> actual_file_size = base::GetFileSize(file_path);
    ASSERT_TRUE(actual_file_size.has_value());
    EXPECT_NE(0, actual_file_size.value())
        << "Is " << file_path.value() << " non-empty?";
  }

  // Verify that local links got correctly replaced with local paths
  // (most importantly for iframe elements, which are only exercised
  // by this particular test).
  std::string main_contents;
  ASSERT_TRUE(base::ReadFileToString(full_file_name, &main_contents));
  EXPECT_THAT(
      main_contents,
      HasSubstr("<iframe "
                "src=\"./frames-xsite-complete-html_files/a.html\"></iframe>"));
  EXPECT_THAT(
      main_contents,
      HasSubstr("<iframe "
                "src=\"./frames-xsite-complete-html_files/b.html\"></iframe>"));
  EXPECT_THAT(
      main_contents,
      HasSubstr("<img src=\"./frames-xsite-complete-html_files/1.png\">"));

  // Verification of html contents.
  EXPECT_THAT(
      main_contents,
      HasSubstr("frames-xsite.htm: 896fd88d-a77a-4f46-afd8-24db7d5af9c2"));
  std::string a_contents;
  ASSERT_TRUE(base::ReadFileToString(dir.AppendASCII("a.html"), &a_contents));
  EXPECT_THAT(a_contents,
              HasSubstr("a.htm: 1b8aae2b-e164-462f-bd5b-98aa366205f2"));
  std::string b_contents;
  ASSERT_TRUE(base::ReadFileToString(dir.AppendASCII("b.html"), &b_contents));
  EXPECT_THAT(b_contents,
              HasSubstr("b.htm: 3a35f7fa-96a9-4487-9f18-4470263907fa"));
}

// Test for crbug.com/40438557.
// Disabled on Mac due to excessive flakiness. https://crbug.com/40805746
#if BUILDFLAG(IS_MAC)
#define MAYBE_SaveAsMHTML DISABLED_SaveAsMHTML
#else
#define MAYBE_SaveAsMHTML SaveAsMHTML
#endif
IN_PROC_BROWSER_TEST_F(SavePageSitePerProcessBrowserTest,
                       MAYBE_SaveAsMHTML) {
  GURL url(
      embedded_test_server()->GetURL("a.com", "/save_page/frames-xsite.htm"));
  ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), url));

  base::FilePath full_file_name, dir;
  SaveCurrentTab(url, content::SAVE_PAGE_TYPE_AS_MHTML, "frames-xsite-mhtml",
                 -1, &dir, &full_file_name);
  ASSERT_FALSE(HasFailure());

  std::string mhtml;
  {
    base::ScopedAllowBlockingForTesting allow_blocking;
    ASSERT_TRUE(base::ReadFileToString(full_file_name, &mhtml));
  }

  // Verify content of main frame, subframes and some savable resources.
  EXPECT_THAT(
      mhtml,
      HasSubstr("frames-xsite.htm: 896fd88d-a77a-4f46-afd8-24db7d5af9c2"));
  EXPECT_THAT(mhtml, HasSubstr("a.htm: 1b8aae2b-e164-462f-bd5b-98aa366205f2"));
  EXPECT_THAT(mhtml, HasSubstr("b.htm: 3a35f7fa-96a9-4487-9f18-4470263907fa"));
  EXPECT_THAT(mhtml, HasSubstr("font-size: 20px;"))
      << "Verifying if content from 1.css is present";

  // Verify presence of URLs associated with main frame, subframes and some
  // savable resources.
  // (note that these are single-line regexes).
  EXPECT_THAT(mhtml,
              ContainsRegex("Content-Location.*/save_page/frames-xsite.htm"));
  EXPECT_THAT(mhtml, ContainsRegex("Content-Location.*/save_page/a.htm"));
  EXPECT_THAT(mhtml, ContainsRegex("Content-Location.*/save_page/b.htm"));
  EXPECT_THAT(mhtml, ContainsRegex("Content-Location.*/save_page/1.css"));
  EXPECT_THAT(mhtml, ContainsRegex("Content-Location.*/save_page/1.png"));

  // Verify that 1.png appears in the output only once (despite being referred
  // to twice - from iframes.htm and from b.htm).
  int count = 0;
  size_t pos = 0;
  for (;;) {
    pos = mhtml.find("Content-Type: image/png", pos);
    if (pos == std::string::npos)
      break;
    count++;
    pos++;
  }
  EXPECT_EQ(1, count) << "Verify number of image/png parts in the mhtml output";
}

// Test for crbug.com/40439413 - handling of dead renderer processes.
IN_PROC_BROWSER_TEST_F(SavePageSitePerProcessBrowserTest,
                       CompleteHtmlWhenRendererIsDead) {
  GURL url(
      embedded_test_server()->GetURL("a.com", "/save_page/frames-xsite.htm"));
  ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), url));

  // Kill one of renderer processes (this is the essence of this test).
  WebContents* web_contents = GetCurrentTab(browser());
  bool did_kill_a_process = false;
  web_contents->GetPrimaryMainFrame()
      ->ForEachRenderFrameHostWithAction(
          [web_contents, &did_kill_a_process](RenderFrameHost* frame) {
            if (frame->GetLastCommittedURL().GetHost() == "bar.com") {
              RenderProcessHost* process_to_kill = frame->GetProcess();
              EXPECT_NE(web_contents->GetPrimaryMainFrame()
                            ->GetProcess()
                            ->GetDeprecatedID(),
                        process_to_kill->GetDeprecatedID())
                  << "a.com and bar.com should be in different processes.";

              EXPECT_TRUE(process_to_kill->FastShutdownIfPossible());
              EXPECT_FALSE(process_to_kill->IsInitializedAndNotDead());
              did_kill_a_process = true;
              return content::RenderFrameHost::FrameIterationAction::kStop;
            }
            return content::RenderFrameHost::FrameIterationAction::kContinue;
          });
  EXPECT_TRUE(did_kill_a_process);

  // Main verification is that we don't hang and time out when saving.
  base::FilePath full_file_name, dir;
  SaveCurrentTab(url, content::SAVE_PAGE_TYPE_AS_COMPLETE_HTML,
                 "frames-xsite-complete-html", 5, &dir, &full_file_name);
  ASSERT_FALSE(HasFailure());
  base::ScopedAllowBlockingForTesting allow_blocking;
  EXPECT_TRUE(base::DirectoryExists(dir));
  EXPECT_TRUE(base::PathExists(full_file_name));
}

class SavePageSitePerProcessDeferredDownloadHistoryBrowserTest
    : public SavePageSitePerProcessBrowserTest {
 public:
  SavePageSitePerProcessDeferredDownloadHistoryBrowserTest() {
    feature_list_.InitAndEnableFeature(
        download::features::kDeferredDownloadHistoryLoading);
  }

 private:
  base::test::ScopedFeatureList feature_list_;
};

IN_PROC_BROWSER_TEST_F(SavePageSitePerProcessDeferredDownloadHistoryBrowserTest,
                       SaveAsCompleteHtml) {
  GURL url(
      embedded_test_server()->GetURL("a.com", "/save_page/frames-xsite.htm"));
  ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), url));

  base::FilePath full_file_name, dir;
  SaveCurrentTab(url, content::SAVE_PAGE_TYPE_AS_COMPLETE_HTML,
                 "frames-xsite-complete-html", 5, &dir, &full_file_name);
  ASSERT_FALSE(HasFailure());

  base::ScopedAllowBlockingForTesting allow_blocking;
  EXPECT_TRUE(base::DirectoryExists(dir));
  base::FilePath expected_files[] = {
      full_file_name,
      dir.AppendASCII("a.html"),
      dir.AppendASCII("b.html"),
      dir.AppendASCII("1.css"),
      dir.AppendASCII("1.png"),
  };
  for (auto file_path : expected_files) {
    EXPECT_TRUE(base::PathExists(file_path))
        << "Does " << file_path.value() << " exist?";
    std::optional<int64_t> actual_file_size = base::GetFileSize(file_path);
    ASSERT_TRUE(actual_file_size.has_value());
    EXPECT_NE(0, actual_file_size.value())
        << "Is " << file_path.value() << " non-empty?";
  }
}

// Test suite that verifies that the frame tree "looks" the same before
// and after a save-page-as.
class SavePageOriginalVsSavedComparisonTest
    : public SavePageSitePerProcessBrowserTest,
      public ::testing::WithParamInterface<content::SavePageType> {
 protected:
  void TestOriginalVsSavedPage(
      content::SavePageType save_page_type,
      const GURL& url,
      size_t expected_number_of_frames_in_original_page,
      size_t expected_number_of_frames_in_mhtml_page,
      const std::vector<std::string>& expected_substrings) {
    // Navigate to the test page and verify if test expectations
    // are met (this is mostly a sanity check - a failure to meet
    // expectations would probably mean that there is a test bug
    // (i.e. that we got called with wrong expected_foo argument).
    ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), url));
    DLOG(INFO) << "Verifying test expectations for original page... : "
               << GetCurrentTab(browser())->GetLastCommittedURL();
    AssertExpectationsAboutCurrentTab(
        expected_number_of_frames_in_original_page, expected_substrings,
        save_page_type);

    // Save the page.
    base::FilePath full_file_name, dir;
    SaveCurrentTab(url, save_page_type, "save_result", -1, &dir,
                   &full_file_name);
    ASSERT_FALSE(HasFailure());

    // Stop the test server (to make sure the locally saved page
    // is self-contained / won't try to open original resources).
    ASSERT_TRUE(embedded_test_server()->ShutdownAndWaitUntilComplete());

    // Open the saved page and verify if test expectations are
    // met (i.e. if the same expectations are met for "after"
    // [saved version of the page] as for the "before"
    // [the original version of the page].
    ASSERT_TRUE(ui_test_utils::NavigateToURL(
        browser(), GURL(net::FilePathToFileURL(full_file_name))));
    DLOG(INFO) << "Verifying test expectations for saved page... : "
               << GetCurrentTab(browser())->GetLastCommittedURL();
    // Hidden elements, i.e., hidden frames, will be removed only from MHTML
    // page. They're still kept in other types of serialization, like saving
    // as a complete html page.
    size_t expected_number_of_frames_in_saved_page =
        (save_page_type == content::SAVE_PAGE_TYPE_AS_MHTML)
            ? expected_number_of_frames_in_mhtml_page
            : expected_number_of_frames_in_original_page;
    AssertExpectationsAboutCurrentTab(expected_number_of_frames_in_saved_page,
                                      expected_substrings, save_page_type);

    if (GetParam() == content::SAVE_PAGE_TYPE_AS_MHTML) {
      std::set<url::Origin> origins;
      GetCurrentTab(browser())->GetPrimaryMainFrame()->ForEachRenderFrameHost(
          [&origins](content::RenderFrameHost* host) {
            CheckFrameForMHTML(host, origins);
          });
      EXPECT_EQ(expected_number_of_frames_in_saved_page, origins.size())
          << "All origins should be unique";
    }

    // Check that we're able to navigate away and come back, as well.
    // See https://crbug.com/41450468.
    ASSERT_TRUE(
        ui_test_utils::NavigateToURL(browser(), GURL("data:text/html,foo")));
    chrome::GoBack(browser(), WindowOpenDisposition::CURRENT_TAB);
    EXPECT_TRUE(content::WaitForLoadStop(GetCurrentTab(browser())));
    DLOG(INFO) << "Verifying test expectations after history navigation...";
    AssertExpectationsAboutCurrentTab(expected_number_of_frames_in_saved_page,
                                      expected_substrings, save_page_type);
  }

  // Helper method to deduplicate some code across 2 tests.
  void RunObjectElementsTest(GURL url) {
    content::SavePageType save_page_type = GetParam();

    // The `kExpectedNumberOfFrames` comes from:
    // - main frame (frames-objects.htm)
    // - object with frame-nested.htm + 2 subframes (frames-nested2.htm + b.htm)
    // - iframe with a.htm
    // - object with svg.svg
    // - object with text.txt
    // - object with pdf.pdf is responsible for presence of 2 extra frames
    //   (about:blank + one frame for the actual pdf.pdf).  These frames are an
    //   implementation detail and are not web-exposed (e.g. via window.frames).
    static constexpr size_t kExpectedNumberOfFrames = 9;

    std::vector<std::string> expected_substrings = {
        "frames-objects.htm: 8da13db4-a512-4d9b-b1c5-dc1c134234b9",
        "a.htm: 1b8aae2b-e164-462f-bd5b-98aa366205f2",
        "b.htm: 3a35f7fa-96a9-4487-9f18-4470263907fa",
        "frames-nested.htm: 4388232f-8d45-4d2e-9807-721b381be153",
        "frames-nested2.htm: 6d23dc47-f283-4977-96ec-66bcf72301a4",
        "text-object.txt: ae52dd09-9746-4b7e-86a6-6ada5e2680c2",
        "svg: 0875fd06-131d-4708-95e1-861853c6b8dc",

        // TODO(lukasza): Consider also verifying presence of "PDF test file"
        // from <object data="pdf.pdf">.  This requires ensuring that the PDF is
        // loaded before continuing with the test.
    };

    // TODO(lukasza): crbug.com/41216547: Enable <object> testing of MHTML.
    if (save_page_type == content::SAVE_PAGE_TYPE_AS_MHTML)
      return;

    TestOriginalVsSavedPage(save_page_type, url, kExpectedNumberOfFrames,
                            kExpectedNumberOfFrames, expected_substrings);
  }

 private:
  void AssertExpectationsAboutCurrentTab(
      size_t expected_number_of_frames,
      const std::vector<std::string>& expected_substrings,
      content::SavePageType save_page_type) {
    size_t actual_number_of_frames =
        CollectAllRenderFrameHosts(GetCurrentTab(browser())->GetPrimaryPage())
            .size();
    EXPECT_EQ(expected_number_of_frames, actual_number_of_frames);

    for (const auto& expected_substring : expected_substrings) {
      int actual_number_of_matches = ui_test_utils::FindInPage(
          GetCurrentTab(browser()), base::UTF8ToUTF16(expected_substring),
          true,   // |forward|
          false,  // |case_sensitive|
          nullptr, nullptr);

      EXPECT_EQ(1, actual_number_of_matches)
          << "Verifying that \"" << expected_substring << "\" appears "
          << "exactly once in the text of web contents";

      // TODO(lukasza): https://crbug.com/40684650 and
      // https://crbug.com/40684809: Remove the extra test assertions below (and
      // maybe also the |save_page_type| parameter) after we get a better
      // understanding of the root cause of test flakiness.
      if (expected_substring == "a.htm: 1b8aae2b-e164-462f-bd5b-98aa366205f2" &&
          save_page_type == content::SAVE_PAGE_TYPE_AS_COMPLETE_HTML) {
        DLOG(INFO) << "Verifying that a.htm frame has fully loaded...";
        std::vector<std::string> frame_names;
        GetCurrentTab(browser())->GetPrimaryMainFrame()->ForEachRenderFrameHost(
            [&frame_names](content::RenderFrameHost* frame) {
              frame_names.push_back(frame->GetFrameName());
            });

        EXPECT_THAT(frame_names, testing::Contains("Frame name of a.htm"));
      }
    }

    std::string forbidden_substrings[] = {
        "head",  // Html markup should not be visible.
        "err",   // "err" is a prefix of error messages + is strategically
                 // included in some tests in contents that should not render
                 // (i.e. inside of an object element and/or inside of a frame
                 // that should be hidden).
    };
    for (const auto& forbidden_substring : forbidden_substrings) {
      int actual_number_of_matches = ui_test_utils::FindInPage(
          GetCurrentTab(browser()), base::UTF8ToUTF16(forbidden_substring),
          true,   // |forward|
          false,  // |case_sensitive|
          nullptr, nullptr);
      EXPECT_EQ(0, actual_number_of_matches)
          << "Verifying that \"" << forbidden_substring << "\" doesn't "
          << "appear in the text of web contents";
    }
  }

  static void CheckFrameForMHTML(content::RenderFrameHost* host,
                                 std::set<url::Origin>& origins) {
    // See RFC n°2557, section-8.3: "Use of the Content-ID header and CID URLs".
    const char kContentIdScheme[] = "cid";
    origins.insert(host->GetLastCommittedOrigin());
    EXPECT_TRUE(host->GetLastCommittedOrigin().opaque());
    if (!host->GetParent())
      EXPECT_TRUE(host->GetLastCommittedURL().SchemeIsFile());
    else
      EXPECT_TRUE(host->GetLastCommittedURL().SchemeIs(kContentIdScheme));
  }
};

// Test coverage for:
// - crbug.com/40433720: OOPIFs support for CompleteHtml
// - crbug.com/40438557: OOPIFs support for MHTML
// - crbug.com/40438945: Subframe gets redirected.
// Test compares original-vs-saved for a page with cross-site frames
// (subframes get redirected to a different domain - see frames-xsite.htm).
IN_PROC_BROWSER_TEST_P(SavePageOriginalVsSavedComparisonTest, CrossSite) {
  content::SavePageType save_page_type = GetParam();

  std::vector<std::string> expected_substrings = {
      "frames-xsite.htm: 896fd88d-a77a-4f46-afd8-24db7d5af9c2",
      "a.htm: 1b8aae2b-e164-462f-bd5b-98aa366205f2",
      "b.htm: 3a35f7fa-96a9-4487-9f18-4470263907fa",
  };

  GURL url(
      embedded_test_server()->GetURL("a.com", "/save_page/frames-xsite.htm"));

  TestOriginalVsSavedPage(save_page_type, url, 3, 3, expected_substrings);
}

// Test compares original-vs-saved for a page with <object> elements.
// (see crbug.com/41216547).
// crbug.com/40684809: disabled because of flakiness.
IN_PROC_BROWSER_TEST_P(SavePageOriginalVsSavedComparisonTest,
                       DISABLED_ObjectElementsViaHttp) {
  GURL url(
      embedded_test_server()->GetURL("a.com", "/save_page/frames-objects.htm"));

  RunObjectElementsTest(url);
}

// Tests that saving a page from file: URI works.
// TODO(lukasza): https://crbug.com/40627967: Re-enable the test.
IN_PROC_BROWSER_TEST_P(SavePageOriginalVsSavedComparisonTest,
                       DISABLED_ObjectElementsViaFile) {
  base::FilePath test_data_dir;
  ASSERT_TRUE(base::PathService::Get(chrome::DIR_TEST_DATA, &test_data_dir));
  GURL url(net::FilePathToFileURL(
      test_data_dir.Append(FILE_PATH_LITERAL("save_page/frames-objects.htm"))));
  EXPECT_TRUE(url.SchemeIsFile());

  RunObjectElementsTest(url);
}

// Test compares original-vs-saved for a page with frames at about:blank uri.
// This tests handling of iframe elements without src attribute (only with
// srcdoc attribute) and how they get saved / cross-referenced.
#if BUILDFLAG(IS_MAC)
// TODO(crbug.com/40202613): Fails on dcheck-enabled builds on 11.0.
#define MAYBE_AboutBlank DISABLED_AboutBlank
#else
#define MAYBE_AboutBlank AboutBlank
#endif
IN_PROC_BROWSER_TEST_P(SavePageOriginalVsSavedComparisonTest,
                       MAYBE_AboutBlank) {
  content::SavePageType save_page_type = GetParam();

  std::vector<std::string> expected_substrings = {
      "main: acb0609d-eb10-4c26-83e2-ad8afb7b0ff3",
      "sub1: b124df3a-d39f-47a1-ae04-5bb5d0bf549e",
      "sub2: 07014068-604d-45ae-884f-a068cfe7bc0a",
      "sub3: 06cc8fcc-c692-4a1a-a10f-1645b746e8f4",
  };

  GURL url(embedded_test_server()->GetURL("a.com",
                                          "/save_page/frames-about-blank.htm"));

  TestOriginalVsSavedPage(save_page_type, url, 4, 4, expected_substrings);
}

// Test compares original-vs-saved for a page with nested frames.
// Two levels of nesting are especially good for verifying correct
// link rewriting for subframes-vs-main-frame (see crbug.com/40444391).
IN_PROC_BROWSER_TEST_P(SavePageOriginalVsSavedComparisonTest, NestedFrames) {
  content::SavePageType save_page_type = GetParam();

  std::vector<std::string> expected_substrings = {
      "frames-nested.htm: 4388232f-8d45-4d2e-9807-721b381be153",
      "frames-nested2.htm: 6d23dc47-f283-4977-96ec-66bcf72301a4",
      "b.htm: 3a35f7fa-96a9-4487-9f18-4470263907fa",
  };

  GURL url(
      embedded_test_server()->GetURL("a.com", "/save_page/frames-nested.htm"));

  TestOriginalVsSavedPage(save_page_type, url, 3, 3, expected_substrings);
}

// Test for crbug.com/40123384 and crbug.com/40438320.
// Test frames have the same uri ...
//   subframe1 and subframe2 - both have src=b.htm
//   subframe3 and subframe4 - about:blank (no src, only srcdoc attribute).
// ... but different content (generated by main frame's javascript).
#if BUILDFLAG(IS_MAC)
// TODO(crbug.com/40202613): Fails on dcheck-enabled builds on 11.0.
#define MAYBE_RuntimeChanges DISABLED_RuntimeChanges
#else
#define MAYBE_RuntimeChanges RuntimeChanges
#endif
IN_PROC_BROWSER_TEST_P(SavePageOriginalVsSavedComparisonTest,
                       MAYBE_RuntimeChanges) {
  content::SavePageType save_page_type = GetParam();

  std::vector<std::string> expected_substrings = {
      "frames-runtime-changes.htm: 4388232f-8d45-4d2e-9807-721b381be153",
      "subframe1: 21595339-61fc-4854-b6df-0668328ea263",
      "subframe2: adf55719-15e7-45be-9eda-d12fe782a1bd",
      "subframe3: 50e294bf-3a5b-499d-8772-651ead26952f",
      "subframe4: e0ea9289-7467-4d32-ba5c-c604e8d84cb7",
  };

  GURL url(embedded_test_server()->GetURL(
      "a.com", "/save_page/frames-runtime-changes.htm?do_runtime_changes=1"));

  TestOriginalVsSavedPage(save_page_type, url, 5, 5, expected_substrings);
}

// Test for saving frames with various encodings:
// - iso-8859-2: encoding declared via <meta> element
// - utf16-le-bom.htm, utf16-be-bom.htm: encoding detected via BOM
// - utf16-le-nobom.htm, utf16-le-nobom.htm - encoding declared via
//                                            mocked http headers
IN_PROC_BROWSER_TEST_P(SavePageOriginalVsSavedComparisonTest, Encoding) {
  content::SavePageType save_page_type = GetParam();

  std::vector<std::string> expected_substrings = {
      "frames-encodings.htm: f53295dd-a95b-4b32-85f5-b6e15377fb20",
      "iso-8859-2.htm: Za\xc5\xbc\xc3\xb3\xc5\x82\xc4\x87 "
      "g\xc4\x99\xc5\x9bl\xc4\x85 ja\xc5\xba\xc5\x84",
      "utf16-le-nobom.htm: Za\xc5\xbc\xc3\xb3\xc5\x82\xc4\x87 "
      "g\xc4\x99\xc5\x9bl\xc4\x85 ja\xc5\xba\xc5\x84",
      "utf16-le-bom.htm: Za\xc5\xbc\xc3\xb3\xc5\x82\xc4\x87 "
      "g\xc4\x99\xc5\x9bl\xc4\x85 ja\xc5\xba\xc5\x84",
      "utf16-be-nobom.htm: Za\xc5\xbc\xc3\xb3\xc5\x82\xc4\x87 "
      "g\xc4\x99\xc5\x9bl\xc4\x85 ja\xc5\xba\xc5\x84",
      "utf16-be-bom.htm: Za\xc5\xbc\xc3\xb3\xc5\x82\xc4\x87 "
      "g\xc4\x99\xc5\x9bl\xc4\x85 ja\xc5\xba\xc5\x84"};

  GURL url(embedded_test_server()->GetURL("a.com",
                                          "/save_page/frames-encodings.htm"));

  // TODO(lukasza): crbug.com/40439547: MHTML needs to handle multi-byte
  // encodings by either:
  // 1. Continuing to preserve the original encoding, but starting to round-trip
  //    the encoding declaration (in Content-Type MIME/MHTML header?)
  // 2. Saving html docs in UTF8.
  // 3. Saving the BOM (not sure if this will help for all cases though).
  if (save_page_type == content::SAVE_PAGE_TYPE_AS_MHTML)
    return;

  TestOriginalVsSavedPage(save_page_type, url, 6, 6, expected_substrings);
}

// Test for saving style element and attribute (see also crbug.com/40448527).
#if BUILDFLAG(IS_MAC)
// TODO(crbug.com/40202613): Fails on dcheck-enabled builds on 11.0.
#define MAYBE_Style DISABLED_Style
#else
#define MAYBE_Style Style
#endif
IN_PROC_BROWSER_TEST_P(SavePageOriginalVsSavedComparisonTest, MAYBE_Style) {
  content::SavePageType save_page_type = GetParam();

  std::vector<std::string> expected_substrings = {
      "style.htm: af84c3ca-0fc6-4b0d-bf7a-5ac18a4dab62",
      "frameF: c9539ccd-47b0-47cf-a03b-734614865872",
  };

  GURL url(embedded_test_server()->GetURL("a.com", "/save_page/style.htm"));

  // The original page has 7 iframes. One of them that contains hidden attribute
  // will be excluded from the saved page.
  TestOriginalVsSavedPage(save_page_type, url, 7, 6, expected_substrings);
}

// Test for saving a page with broken subresources:
// - Broken, undecodable image (see also https://crbug.com/40456883)
// - Broken link, to unresolvable host (see also https://crbug.com/41243783)
IN_PROC_BROWSER_TEST_P(SavePageOriginalVsSavedComparisonTest, BrokenImage) {
  content::SavePageType save_page_type = GetParam();

  std::vector<std::string> expected_substrings = {
      "broken-image.htm: 1e846775-b3ed-4d9c-a124-029554a1eb9d",
  };

  GURL url(embedded_test_server()->GetURL("127.0.0.1",
                                          "/save_page/broken-image.htm"));

  TestOriginalVsSavedPage(save_page_type, url, 1, 1, expected_substrings);
}

// Test for saving a page with a cross-site <object> element.
// Disabled on Windows due to flakiness. crbug.com/40684650.
#if BUILDFLAG(IS_WIN) || BUILDFLAG(IS_MAC)
#define MAYBE_CrossSiteObject DISABLED_CrossSiteObject
#else
#define MAYBE_CrossSiteObject CrossSiteObject
#endif
IN_PROC_BROWSER_TEST_P(SavePageOriginalVsSavedComparisonTest,
                       MAYBE_CrossSiteObject) {
  content::SavePageType save_page_type = GetParam();

  std::vector<std::string> expected_substrings = {
      "cross-site-object.htm: f727dd87-2048-44cf-beee-19fa9863f046",
      "a.htm: 1b8aae2b-e164-462f-bd5b-98aa366205f2",
      "svg: 0875fd06-131d-4708-95e1-861853c6b8dc",
  };

  GURL url(embedded_test_server()->GetURL("a.com",
                                          "/save_page/cross-site-object.htm"));

  TestOriginalVsSavedPage(save_page_type, url, 4, 4, expected_substrings);
}

INSTANTIATE_TEST_SUITE_P(
    SaveAsCompleteHtml,
    SavePageOriginalVsSavedComparisonTest,
    ::testing::Values(content::SAVE_PAGE_TYPE_AS_COMPLETE_HTML));
INSTANTIATE_TEST_SUITE_P(SaveAsMhtml,
                         SavePageOriginalVsSavedComparisonTest,
                         ::testing::Values(content::SAVE_PAGE_TYPE_AS_MHTML));

class BlockingDownloadManagerDelegate : public ChromeDownloadManagerDelegate {
 public:
  explicit BlockingDownloadManagerDelegate(Profile* profile)
      : ChromeDownloadManagerDelegate(profile) {}
  ~BlockingDownloadManagerDelegate() override = default;

  void CheckSavePackageAllowed(
      download::DownloadItem* download_item,
      base::flat_map<base::FilePath, base::FilePath> save_package_files,
      content::SavePackageAllowedCallback callback) override {
    {
      base::ScopedAllowBlockingForTesting allow_blocking;
      for (const auto& tmp_path_and_final_path : save_package_files) {
        // Every intermediate path in `save_package_files` should exist when
        // this function is called.
        EXPECT_TRUE(base::PathExists(tmp_path_and_final_path.first));

        // We don't know what exact temporary path the file has, but it
        // shouldn't be the same as its final one.
        EXPECT_NE(tmp_path_and_final_path.first,
                  tmp_path_and_final_path.second);

        save_package_final_paths_.insert(tmp_path_and_final_path.second);
      }
    }

    std::move(callback).Run(false);
  }

  void ValidateSavePackageFiles(
      const base::flat_set<base::FilePath>& expected_paths) {
    EXPECT_EQ(expected_paths.size(), save_package_final_paths_.size());
    for (const base::FilePath& expected_path : expected_paths) {
      EXPECT_TRUE(save_package_final_paths_.contains(expected_path));
    }
  }

 private:
  base::flat_set<base::FilePath> save_package_final_paths_;
};

IN_PROC_BROWSER_TEST_F(SavePageBrowserTest, SaveOnlyHTMLBlocked) {
  GURL url = NavigateToMockURL("a");

  auto blocking_delegate = std::make_unique<BlockingDownloadManagerDelegate>(
      browser()->GetProfile());
  blocking_delegate->GetDownloadIdReceiverCallback().Run(
      download::DownloadItem::kInvalidId + 1);
  DownloadCoreServiceFactory::GetForBrowserContext(browser()->GetProfile())
      ->SetDownloadManagerDelegateForTesting(std::move(blocking_delegate));
  auto* delegate = static_cast<BlockingDownloadManagerDelegate*>(
      DownloadCoreServiceFactory::GetForBrowserContext(browser()->GetProfile())
          ->GetDownloadManagerDelegate());

  base::FilePath full_file_name, dir;
  GetDestinationPaths("a", &full_file_name, &dir,
                      content::SAVE_PAGE_TYPE_AS_ONLY_HTML);
  base::RunLoop run_loop;
  content::SavePackageFinishedObserver observer(
      browser()->GetProfile()->GetDownloadManager(), run_loop.QuitClosure());
  ASSERT_TRUE(GetCurrentTab(browser())->SavePage(
      full_file_name, dir, content::SAVE_PAGE_TYPE_AS_ONLY_HTML));

  run_loop.Run();
  ASSERT_FALSE(HasFailure());

  base::ScopedAllowBlockingForTesting allow_blocking;
  EXPECT_FALSE(base::PathExists(full_file_name));
  EXPECT_FALSE(base::PathExists(dir));

  delegate->ValidateSavePackageFiles({full_file_name});
}

IN_PROC_BROWSER_TEST_F(SavePageBrowserTest, SaveCompleteHTMLBlocked) {
  GURL url = NavigateToMockURL("b");

  auto blocking_delegate = std::make_unique<BlockingDownloadManagerDelegate>(
      browser()->GetProfile());
  blocking_delegate->GetDownloadIdReceiverCallback().Run(
      download::DownloadItem::kInvalidId + 1);
  DownloadCoreServiceFactory::GetForBrowserContext(browser()->GetProfile())
      ->SetDownloadManagerDelegateForTesting(std::move(blocking_delegate));
  auto* delegate = static_cast<BlockingDownloadManagerDelegate*>(
      DownloadCoreServiceFactory::GetForBrowserContext(browser()->GetProfile())
          ->GetDownloadManagerDelegate());

  base::FilePath full_file_name, dir;
  GetDestinationPaths("b", &full_file_name, &dir,
                      content::SAVE_PAGE_TYPE_AS_COMPLETE_HTML);
  base::RunLoop run_loop;
  content::SavePackageFinishedObserver observer(
      browser()->GetProfile()->GetDownloadManager(), run_loop.QuitClosure());
  ASSERT_TRUE(GetCurrentTab(browser())->SavePage(
      full_file_name, dir, content::SAVE_PAGE_TYPE_AS_COMPLETE_HTML));

  run_loop.Run();
  ASSERT_FALSE(HasFailure());

  base::ScopedAllowBlockingForTesting allow_blocking;
  EXPECT_FALSE(base::PathExists(full_file_name));
  EXPECT_FALSE(base::PathExists(dir));

  delegate->ValidateSavePackageFiles({
      full_file_name,
      dir.AppendASCII("1.png"),
      dir.AppendASCII("1.css"),
  });
}

#if BUILDFLAG(IS_CHROMEOS)

IN_PROC_BROWSER_TEST_F(SavePageBrowserTest, SaveHTMLWithDlp) {
  base::FilePath full_file_name, dir;
  GURL url;

  chromeos::DlpClient::Shutdown();
  chromeos::DlpClient::InitializeFake();

  // Use page "b" which has subresources (1.png, 1.css). Total 3 files.
  url = NavigateToMockURL("b");

  SaveCurrentTab(url, content::SAVE_PAGE_TYPE_AS_COMPLETE_HTML, "b", 3, &dir,
                 &full_file_name);

  ASSERT_FALSE(HasFailure());

  // Asynchronously get the recorded requests from the fake client.
  base::test::TestFuture<const dlp::GetDatabaseEntriesResponse> future;
  chromeos::DlpClient::Get()->GetDatabaseEntries(future.GetCallback());
  const auto& response = future.Get();
  const auto& requests = response.files_entries();

  // There is a total of 6 requests, 3 for temporary files and 3 for final
  // destination.
  ASSERT_EQ(6, requests.size());

  // The order of subresource saving is not guaranteed, so we use a set
  // to verify the presence of each expected file path.
  std::set<std::string> expected_paths;
  expected_paths.insert(full_file_name.value());
  expected_paths.insert(dir.AppendASCII("1.png").value());
  expected_paths.insert(dir.AppendASCII("1.css").value());

  std::set<std::string> actual_paths;
  for (const auto& request : requests) {
    actual_paths.insert(request.path());
  }

  for (const auto& expected_path : expected_paths) {
    EXPECT_TRUE(actual_paths.contains(expected_path));
  }
}

IN_PROC_BROWSER_TEST_F(SavePageBrowserTest, SaveMHTMLWithDlp) {
  base::FilePath full_file_name, dir;
  GURL url;

  chromeos::DlpClient::Shutdown();
  chromeos::DlpClient::InitializeFake();
  base::test::RepeatingTestFuture<
      dlp::AddFilesRequest, base::OnceCallback<void(dlp::AddFilesResponse)>>
      add_file_cb;
  chromeos::DlpClient::Get()->GetTestInterface()->SetAddFilesMock(
      add_file_cb.GetCallback());

  url = NavigateToMockURL("a");

  SaveCurrentTab(url, content::SAVE_PAGE_TYPE_AS_MHTML, "a", -1, &dir,
                 &full_file_name);

  ASSERT_FALSE(HasFailure());

  auto request = std::get<0>(add_file_cb.Take());
  ASSERT_EQ(1, request.add_file_requests().size());
  EXPECT_EQ(full_file_name.value(), request.add_file_requests(0).file_path());
  EXPECT_EQ(request.add_file_requests(0).source_url(), url.spec());

  base::ScopedAllowBlockingForTesting allow_blocking;
  EXPECT_TRUE(base::PathExists(full_file_name));
  EXPECT_FALSE(base::PathExists(dir));
}

#endif  // BUILDFLAG(IS_CHROMEOS)

}  // namespace
