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

#include <memory>
#include <optional>

#include "base/base64.h"
#include "base/containers/span.h"
#include "base/files/file_util.h"
#include "base/memory/raw_ptr.h"
#include "base/path_service.h"
#include "base/test/bind.h"
#include "base/test/metrics/histogram_tester.h"
#include "base/test/scoped_feature_list.h"
#include "base/threading/thread_restrictions.h"
#include "base/values.h"
#include "build/build_config.h"
#include "chrome/browser/browser_process.h"
#include "chrome/browser/download/chrome_download_manager_delegate.h"
#include "chrome/browser/download/download_commands.h"
#include "chrome/browser/download/download_core_service.h"
#include "chrome/browser/download/download_core_service_factory.h"
#include "chrome/browser/download/download_item_model.h"
#include "chrome/browser/download/download_prefs.h"
#include "chrome/browser/enterprise/connectors/connectors_service.h"
#include "chrome/browser/enterprise/connectors/reporting/realtime_reporting_client.h"
#include "chrome/browser/enterprise/connectors/reporting/realtime_reporting_client_factory.h"
#include "chrome/browser/enterprise/connectors/test/deep_scanning_browsertest_base.h"
#include "chrome/browser/enterprise/connectors/test/deep_scanning_test_utils.h"
#include "chrome/browser/enterprise/identifiers/profile_id_service_factory.h"
#include "chrome/browser/policy/dm_token_utils.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/safe_browsing/cloud_content_scanning/cloud_binary_upload_service.h"
#include "chrome/browser/safe_browsing/cloud_content_scanning/cloud_binary_upload_service_factory.h"
#include "chrome/browser/safe_browsing/download_protection/deep_scanning_request.h"
#include "chrome/browser/safe_browsing/download_protection/download_protection_service.h"
#include "chrome/browser/safe_browsing/test_safe_browsing_service.h"
#include "chrome/browser/ui/browser_window/public/browser_window_interface.h"
#include "chrome/browser/ui/omnibox/omnibox_next_features.h"
#include "chrome/browser/ui/views/file_system_access/file_system_access_test_utils.h"
#include "chrome/common/chrome_paths.h"
#include "chrome/common/pref_names.h"
#include "chrome/test/base/in_process_browser_test.h"
#include "chrome/test/base/ui_test_utils.h"
#include "components/autofill/core/common/mojom/autofill_types.mojom-shared.h"
#include "components/download/public/common/download_danger_type.h"
#include "components/download/public/common/download_features.h"
#include "components/enterprise/browser/identifiers/profile_id_service.h"
#include "components/enterprise/common/proto/connectors.pb.h"
#include "components/enterprise/connectors/core/cloud_content_scanning/binary_upload_service.h"
#include "components/enterprise/connectors/core/cloud_content_scanning/common.h"
#include "components/enterprise/connectors/core/reporting_constants.h"
#include "components/enterprise/obfuscation/core/utils.h"
#include "components/policy/core/common/cloud/mock_cloud_policy_client.h"
#include "components/policy/core/common/policy_pref_names.h"
#include "components/prefs/pref_service.h"
#include "components/safe_browsing/content/common/file_type_policies_test_util.h"
#include "components/safe_browsing/core/browser/db/test_database_manager.h"
#include "components/safe_browsing/core/common/features.h"
#include "components/safe_browsing/core/common/proto/csd.pb.h"
#include "components/safe_browsing/core/common/safe_browsing_prefs.h"
#include "components/signin/public/identity_manager/identity_test_environment.h"
#include "components/web_modal/web_contents_modal_dialog_manager.h"
#include "content/public/browser/browser_context.h"
#include "content/public/browser/download_manager.h"
#include "content/public/browser/save_page_type.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/test_utils.h"
#include "mojo/public/cpp/bindings/remote.h"
#include "services/network/public/cpp/data_element.h"
#include "services/network/public/mojom/data_pipe_getter.mojom.h"
#include "services/network/test/test_utils.h"

namespace safe_browsing {

namespace {

constexpr char kUserName[] = "test@chromium.org";

constexpr char kResumableUploadUrl[] =
    "http://uploads.google.com?upload_id=ABC&upload_protocol=resumable";

constexpr int64_t kSingleChunkObfuscationOverhead =
    enterprise_obfuscation::kKeySize + enterprise_obfuscation::kNonceSize +
    enterprise_obfuscation::kAuthTagSize;

constexpr char kTestContent[] = "test content";

// Extract the metadata proto from the raw request string based on multipart
// upload protocol. Returns true on success.
bool GetMultipartUploadMetadata(
    const std::string& upload_request,
    enterprise_connectors::ContentAnalysisRequest* out_proto) {
  // The request is of the following format, see multipart_uploader.h for
  // details:
  // ---MultipartBoundary---
  // <Headers for metadata>
  //
  // <Base64-encoded metadata>
  // ---MultipartBoundary---
  // <Headers for uploaded data>
  //
  // <Uploaded data>
  // ---MultipartBoundary---
  size_t boundary_end = upload_request.find("\r\n");
  std::string multipart_boundary = upload_request.substr(0, boundary_end);

  size_t headers_end = upload_request.find("\r\n\r\n");
  size_t metadata_end =
      upload_request.find("\r\n" + multipart_boundary, headers_end);
  std::string encoded_metadata =
      upload_request.substr(headers_end + 4, metadata_end - headers_end - 4);

  std::string serialized_metadata;
  base::Base64Decode(encoded_metadata, &serialized_metadata);
  return out_proto->ParseFromString(serialized_metadata);
}

// Extract the metadata proto from the raw request string based on resumable
// upload protocol. Returns true on success.
bool GetResumableUploadMetadata(
    const std::string& upload_request,
    enterprise_connectors::ContentAnalysisRequest* out_proto) {
  // The request content is of the following format, see resumable_uploader.h
  // for details: "metadata\r\n"
  size_t boundary_end = upload_request.find("\r\n");
  std::string encoded_metadata = upload_request.substr(0, boundary_end);

  std::string serialized_metadata;
  base::Base64Decode(encoded_metadata, &serialized_metadata);

  return out_proto->ParseFromString(serialized_metadata);
}
}  // namespace

// Integration tests for download deep scanning behavior, only mocking network
// traffic.
class DownloadDeepScanningBrowserTestBase
    : public enterprise_connectors::test::DeepScanningBrowserTestBase,
      public content::DownloadManager::Observer,
      public download::DownloadItem::Observer {
 public:
  // |connectors_machine_scope| indicates whether the Connector prefs such as
  // OnFileDownloadedEnterpriseConnector and OnSecurityEventEnterpriseConnector
  // should be set at the machine or user scope.
  // |is_consumer| indicates whether the content scan is a consumer or an
  // enterprise scan.
  // |is_obfuscated| indicates whether the downloaded file has been obfuscated
  // to prevent user access. Currently, this is done while waiting for an
  // enterprise deep scan verdict.
  explicit DownloadDeepScanningBrowserTestBase(bool connectors_machine_scope,
                                               bool is_consumer,
                                               bool is_obfuscated)
      : is_consumer_(is_consumer),
        is_obfuscated_(is_obfuscated),
        connectors_machine_scope_(connectors_machine_scope) {
    is_obfuscated_ ? enabled_features_.push_back(
                         enterprise_obfuscation::kEnterpriseFileObfuscation)
                   : disabled_features_.push_back(
                         enterprise_obfuscation::kEnterpriseFileObfuscation);
    // TODO(crbug.com/452061489): Fix tests that fail when the WebUI Omnibox is
    // is enabled and then remove these two Features.
    disabled_features_.push_back(omnibox::internal::kWebUIOmniboxPopup);
    disabled_features_.push_back(omnibox::internal::kWebUIOmniboxAimPopup);
  }

  void OnDownloadCreated(content::DownloadManager* manager,
                         download::DownloadItem* item) override {
    item->AddObserver(this);
    download_items_.insert(item);
  }

  void OnDownloadDestroyed(download::DownloadItem* item) override {
    download_items_.erase(item);
  }

  void SetUpReporting() {
    enterprise_connectors::test::SetOnSecurityEventReporting(
        browser()->GetProfile()->GetPrefs(),
        /*enabled*/ true, /*enabled_event_names*/ {},
        /*enabled_opt_in_events*/ {}, connectors_machine_scope());
    client_ = std::make_unique<policy::MockCloudPolicyClient>();
    client_->SetDMToken("dm_token");

#if BUILDFLAG(IS_CHROMEOS)
    enterprise_connectors::RealtimeReportingClientFactory::GetForProfile(
        browser()->GetProfile())
        ->SetBrowserCloudPolicyClientForTesting(client_.get());
#else
    if (connectors_machine_scope()) {
      enterprise_connectors::RealtimeReportingClientFactory::GetForProfile(
          browser()->GetProfile())
          ->SetBrowserCloudPolicyClientForTesting(client_.get());
    } else {
      enterprise_connectors::RealtimeReportingClientFactory::GetForProfile(
          browser()->GetProfile())
          ->SetProfileCloudPolicyClientForTesting(client_.get());
    }
#endif
    identity_test_environment_ =
        std::make_unique<signin::IdentityTestEnvironment>();
    identity_test_environment_->MakePrimaryAccountAvailable(
        kUserName, signin::ConsentLevel::kSignin);
    enterprise_connectors::RealtimeReportingClientFactory::GetForProfile(
        browser()->GetProfile())
        ->SetIdentityManagerForTesting(
            identity_test_environment_->identity_manager());
  }

  policy::MockCloudPolicyClient* client() { return client_.get(); }

 protected:
  void SetUp() override {
    scoped_feature_list_.InitWithFeatures(std::move(enabled_features_),
                                          std::move(disabled_features_));
    test_sb_factory_ = std::make_unique<TestSafeBrowsingServiceFactory>();
    test_sb_factory_->UseSBLocalDatabaseManager();
    SafeBrowsingService::RegisterFactory(test_sb_factory_.get());

    InProcessBrowserTest::SetUp();
  }

  void TearDown() override {
    InProcessBrowserTest::TearDown();

    SafeBrowsingService::RegisterFactory(nullptr);
  }

  void SetUpOnMainThread() override {
    embedded_test_server()->ServeFilesFromDirectory(GetTestDataDirectory());
    ASSERT_TRUE(embedded_test_server()->Start());

    SetBinaryUploadServiceTestFactory();
    SetUrlLoaderInterceptor();
    ObserveDownloadManager();

    if (!is_consumer_) {
      AuthorizeForDeepScanning();

#if BUILDFLAG(IS_CHROMEOS)
      SetDMTokenForTesting(policy::DMToken::CreateValidToken("dm_token"));
#else
      if (connectors_machine_scope()) {
        SetDMTokenForTesting(policy::DMToken::CreateValidToken("dm_token"));
      } else {
        enterprise_connectors::test::SetProfileDMToken(browser()->GetProfile(),
                                                       "dm_token");
      }
#endif
      enterprise_connectors::test::SetAnalysisConnector(
          browser()->GetProfile()->GetPrefs(),
          enterprise_connectors::FILE_DOWNLOADED,
          R"({
                              "service_provider": "google",
                              "enable": [
                                {
                                  "url_list": ["*"],
                                  "tags": ["dlp", "malware"]
                                }
                              ],
                              "block_until_verdict": 1,
                              "block_password_protected": true
                            })",
          connectors_machine_scope());
    }
  }

  void WaitForDownloadToFinish() {
    content::DownloadManager* download_manager =
        browser()->GetProfile()->GetDownloadManager();
    content::DownloadTestObserverTerminal observer(
        download_manager, 1,
        content::DownloadTestObserver::ON_DANGEROUS_DOWNLOAD_QUIT);
    observer.WaitForFinished();
  }

  void WaitForDeepScanRequest() {
    base::RunLoop run_loop(base::RunLoop::Type::kNestableTasksAllowed);
    waiting_for_upload_closure_ = run_loop.QuitClosure();
    run_loop.Run();
  }

  void WaitForMetadataCheck() {
    base::RunLoop run_loop(base::RunLoop::Type::kNestableTasksAllowed);
    waiting_for_metadata_closure_ = run_loop.QuitClosure();
    run_loop.Run();
  }

  void ExpectMetadataResponse(const ClientDownloadResponse& response) {
    test_sb_factory_->test_safe_browsing_service()
        ->GetTestUrlLoaderFactory()
        ->AddResponse(test_sb_factory_->test_safe_browsing_service()
                          ->download_protection_service()
                          ->GetDownloadRequestUrl()
                          .spec(),
                      response.SerializeAsString());
  }

  void PrepareConnectorUrl(const std::vector<std::string>& tags) {
    if (is_consumer_) {
      connector_url_ =
          "https://safebrowsing.google.com/safebrowsing/uploads/consumer";
    } else {
      connector_url_ =
          "https://safebrowsing.google.com/safebrowsing/uploads/"
          "scan?device_token=dm_token&connector=OnFileDownloaded";
      for (const std::string& tag : tags) {
        connector_url_ += ("&tag=" + tag);
      }
    }
  }

  void ExpectContentAnalysisResumableMetadataResponse(
      const std::vector<std::string>& tags) {
    PrepareConnectorUrl(tags);

    auto metadata_response_head = network::CreateURLResponseHead(net::HTTP_OK);
    metadata_response_head->headers->AddHeader("X-Goog-Upload-Status",
                                               "active");
    metadata_response_head->headers->AddHeader("X-Goog-Upload-URL",
                                               kResumableUploadUrl);
    test_sb_factory_->test_safe_browsing_service()
        ->GetTestUrlLoaderFactory()
        ->AddResponse(GURL(connector_url_), std::move(metadata_response_head),
                      "metadata_response",
                      network::URLLoaderCompletionStatus(net::OK));
  }

  void ExpectContentAnalysisResumableContentResponse(
      const enterprise_connectors::ContentAnalysisResponse& response) {
    auto content_response_head = network::CreateURLResponseHead(net::HTTP_OK);
    content_response_head->headers->AddHeader("X-Goog-Upload-Status", "final");

    test_sb_factory_->test_safe_browsing_service()
        ->GetTestUrlLoaderFactory()
        ->AddResponse(GURL(kResumableUploadUrl),
                      std::move(content_response_head),
                      response.SerializeAsString(),
                      network::URLLoaderCompletionStatus(net::OK));
  }

  void ExpectContentAnalysisMultipartResponse(
      const enterprise_connectors::ContentAnalysisResponse& response,
      const std::vector<std::string>& tags) {
    PrepareConnectorUrl(tags);

    test_sb_factory_->test_safe_browsing_service()
        ->GetTestUrlLoaderFactory()
        ->AddResponse(connector_url_, response.SerializeAsString());
  }

  void ExpectContentAnalysisUploadFailure(
      net::HttpStatusCode status_code,
      const std::vector<std::string>& tags) {
    PrepareConnectorUrl(tags);

    test_sb_factory_->test_safe_browsing_service()
        ->GetTestUrlLoaderFactory()
        ->AddResponse(connector_url_, "", status_code);
  }

  base::FilePath GetTestDataDirectory() {
    base::FilePath test_file_directory;
    base::PathService::Get(chrome::DIR_TEST_DATA, &test_file_directory);
    return test_file_directory;
  }

  TestSafeBrowsingServiceFactory* test_sb_factory() {
    return test_sb_factory_.get();
  }

  const enterprise_connectors::ContentAnalysisRequest& last_request() const {
    return last_request_;
  }

  const base::flat_set<raw_ptr<download::DownloadItem, CtnExperimental>>&
  download_items() {
    return download_items_;
  }

  void SetBinaryUploadServiceTestFactory() {
    CloudBinaryUploadServiceFactory::GetInstance()->SetTestingFactory(
        browser()->GetProfile(),
        base::BindRepeating(
            &DownloadDeepScanningBrowserTestBase::CreateBinaryUploadService,
            base::Unretained(this)));
  }

  void ObserveDownloadManager() {
    content::DownloadManager* download_manager =
        browser()->GetProfile()->GetDownloadManager();
    download_manager->AddObserver(this);
  }

  void SetUrlLoaderInterceptor() {
    test_sb_factory()->test_safe_browsing_service()->SetUseTestUrlLoaderFactory(
        true);
    test_sb_factory()
        ->test_safe_browsing_service()
        ->GetTestUrlLoaderFactory()
        ->SetInterceptor(base::BindRepeating(
            &DownloadDeepScanningBrowserTestBase::InterceptRequest,
            base::Unretained(this)));
  }

  void AuthorizeForDeepScanning() {
    static_cast<enterprise_connectors::CloudBinaryUploadServiceBase*>(
        CloudBinaryUploadServiceFactory::GetForProfile(browser()->GetProfile()))
        ->SetAuthForTesting("dm_token",
                            /*auth_check_result=*/enterprise_connectors::
                                ScanRequestUploadResult::kSuccess);
  }

  bool connectors_machine_scope() const { return connectors_machine_scope_; }

  bool is_obfuscated() const { return is_obfuscated_; }

  std::string GetProfileIdentifier() const {
#if BUILDFLAG(IS_CHROMEOS)
    return browser()->GetProfile()->GetPath().AsUTF8Unsafe();
#else
    if (connectors_machine_scope_) {
      return browser()->GetProfile()->GetPath().AsUTF8Unsafe();
    }
    auto* profile_id_service =
        enterprise::ProfileIdServiceFactory::GetForProfile(
            browser()->GetProfile());
    if (profile_id_service && profile_id_service->GetProfileId().has_value()) {
      return profile_id_service->GetProfileId().value();
    }
    return std::string();
#endif
  }

  std::vector<base::test::FeatureRef> enabled_features_;
  std::vector<base::test::FeatureRef> disabled_features_;

 private:
  std::unique_ptr<KeyedService> CreateBinaryUploadService(
      content::BrowserContext* browser_context) {
    Profile* profile = Profile::FromBrowserContext(browser_context);
    return std::make_unique<
        enterprise_connectors::CloudBinaryUploadServiceBase>(
        g_browser_process->safe_browsing_service()->GetURLLoaderFactory(
            profile),
        std::make_unique<safe_browsing::CloudBinaryUploadService>(profile));
  }

  std::string GetDataPipeUploadData(const network::ResourceRequest& request) {
    EXPECT_TRUE(request.request_body);
    EXPECT_EQ(1u, request.request_body->elements()->size());
    network::DataElement& data_pipe_element =
        (*request.request_body->elements_mutable())[0];

    data_pipe_getter_.reset();
    data_pipe_getter_.Bind(data_pipe_element.As<network::DataElementDataPipe>()
                               .ReleaseDataPipeGetter());
    EXPECT_TRUE(data_pipe_getter_);

    mojo::ScopedDataPipeProducerHandle data_pipe_producer;
    mojo::ScopedDataPipeConsumerHandle data_pipe_consumer;
    base::RunLoop run_loop;
    EXPECT_EQ(MOJO_RESULT_OK, mojo::CreateDataPipe(nullptr, data_pipe_producer,
                                                   data_pipe_consumer));
    data_pipe_getter_->Read(
        std::move(data_pipe_producer),
        base::BindLambdaForTesting([&run_loop](int32_t status, uint64_t size) {
          EXPECT_EQ(net::OK, status);
          run_loop.Quit();
        }));
    data_pipe_getter_.FlushForTesting();
    run_loop.Run();

    EXPECT_TRUE(data_pipe_consumer.is_valid());
    std::string body;
    while (true) {
      std::string buffer(1024, '\0');
      size_t actually_read_bytes = 0;
      MojoResult result = data_pipe_consumer->ReadData(
          MOJO_READ_DATA_FLAG_NONE, base::as_writable_byte_span(buffer),
          actually_read_bytes);
      if (result == MOJO_RESULT_SHOULD_WAIT) {
        base::RunLoop().RunUntilIdle();
        continue;
      }
      if (result != MOJO_RESULT_OK) {
        break;
      }
      body.append(std::string_view(buffer).substr(0, actually_read_bytes));
    }

    return body;
  }

  bool MaybeHandleDownloadRequest(const network::ResourceRequest& request) {
    if (request.url != test_sb_factory_->test_safe_browsing_service()
                           ->download_protection_service()
                           ->GetDownloadRequestUrl()) {
      return false;
    }
    if (waiting_for_metadata_closure_) {
      std::move(waiting_for_metadata_closure_).Run();
    }
    return true;
  }

  void HandleConsumerRequest(const network::ResourceRequest& request) {
    if (request.url ==
        enterprise_connectors::CloudBinaryUploadServiceBase::GetUploadUrl(
            /*is_consumer_scan_eligible=*/true)) {
      ASSERT_TRUE(GetMultipartUploadMetadata(GetDataPipeUploadData(request),
                                             &last_request_));
      if (waiting_for_upload_closure_) {
        std::move(waiting_for_upload_closure_).Run();
      }
    }
  }

  void HandleEnterpriseRequest(const network::ResourceRequest& request) {
    if (request.url == GURL(kResumableUploadUrl)) {
      GetDataPipeUploadData(request);
      if (waiting_for_upload_closure_) {
        std::move(waiting_for_upload_closure_).Run();
      }
      return;
    }

    if (request.url ==
        enterprise_connectors::CloudBinaryUploadServiceBase::GetUploadUrl(
            /*is_consumer_scan_eligible=*/false)) {
      ASSERT_TRUE(GetMultipartUploadMetadata(GetDataPipeUploadData(request),
                                             &last_request_));
      if (waiting_for_upload_closure_) {
        std::move(waiting_for_upload_closure_).Run();
      }
      return;
    }

    if (request.url == connector_url_) {
      ASSERT_TRUE(GetResumableUploadMetadata(network::GetUploadData(request),
                                             &last_request_));
    }
  }

  void InterceptRequest(const network::ResourceRequest& request) {
    if (MaybeHandleDownloadRequest(request)) {
      return;
    }
    if (is_consumer_) {
      HandleConsumerRequest(request);
      return;
    }
    HandleEnterpriseRequest(request);
  }

  base::test::ScopedFeatureList scoped_feature_list_;
  bool is_consumer_;
  bool is_obfuscated_;

  std::unique_ptr<TestSafeBrowsingServiceFactory> test_sb_factory_;

  enterprise_connectors::ContentAnalysisRequest last_request_;

  std::string connector_url_;

  base::OnceClosure waiting_for_upload_closure_;
  base::OnceClosure waiting_for_metadata_closure_;

  base::flat_set<raw_ptr<download::DownloadItem, CtnExperimental>>
      download_items_;

  std::unique_ptr<policy::MockCloudPolicyClient> client_;
  std::unique_ptr<signin::IdentityTestEnvironment> identity_test_environment_;

  bool connectors_machine_scope_;

  mojo::Remote<network::mojom::DataPipeGetter> data_pipe_getter_;
};

class ConsumerDeepScanningBrowserTest
    : public DownloadDeepScanningBrowserTestBase {
 public:
  ConsumerDeepScanningBrowserTest()
      : DownloadDeepScanningBrowserTestBase(/*connectors_machine_scope=*/true,
                                            /*is_consumer=*/true,
                                            /*is_obfuscated=*/false) {}
};

IN_PROC_BROWSER_TEST_F(ConsumerDeepScanningBrowserTest, ErrorIndicatesFailure) {
  SetSafeBrowsingState(browser()->GetProfile()->GetPrefs(),
                       SafeBrowsingState::ENHANCED_PROTECTION);

  ClientDownloadResponse metadata_response;
  metadata_response.set_request_deep_scan(true);
  ExpectMetadataResponse(metadata_response);

  GURL url = embedded_test_server()->GetURL(
      "/safe_browsing/download_protection/zipfile_two_archives.zip");
  ui_test_utils::NavigateToURLWithDisposition(
      browser(), url, WindowOpenDisposition::CURRENT_TAB,
      ui_test_utils::BROWSER_TEST_WAIT_FOR_LOAD_STOP);

  ExpectContentAnalysisUploadFailure(net::HTTP_INTERNAL_SERVER_ERROR, {});

  WaitForDownloadToFinish();

  ASSERT_EQ(download_items().size(), 1u);
  download::DownloadItem* download = *download_items().begin();
  EXPECT_EQ(download->GetState(), download::DownloadItem::COMPLETE);
  EXPECT_EQ(
      download->GetDangerType(),
      download::DownloadDangerType::DOWNLOAD_DANGER_TYPE_DEEP_SCANNED_FAILED);
}

class DownloadDeepScanningBrowserTest
    : public DownloadDeepScanningBrowserTestBase,
      public testing::WithParamInterface<std::tuple<bool, bool>> {
 public:
  DownloadDeepScanningBrowserTest()
      : DownloadDeepScanningBrowserTestBase(
            /*connectors_machine_scope=*/std::get<0>(GetParam()),
            /*is_consumer=*/false,
            /*is_obfuscated=*/std::get<1>(GetParam())) {}
};

INSTANTIATE_TEST_SUITE_P(,
                         DownloadDeepScanningBrowserTest,
                         testing::Combine(
                             /*connectors_machine_scope=*/testing::Bool(),
                             /*is_obfuscated=*/testing::Bool()));

IN_PROC_BROWSER_TEST_P(DownloadDeepScanningBrowserTest,
                       SafeDownloadHasCorrectDangerType) {
  // This allows the blocking DM token reads happening on profile-Connector
  // triggers.
  base::ScopedAllowBlockingForTesting allow_blocking;

  // The file is SAFE according to the metadata check
  ClientDownloadResponse metadata_response;
  metadata_response.set_verdict(ClientDownloadResponse::SAFE);
  ExpectMetadataResponse(metadata_response);

  // The DLP scan runs synchronously, but doesn't find anything.
  enterprise_connectors::ContentAnalysisResponse sync_response;
  auto* dlp_result = sync_response.add_results();
  dlp_result->set_tag("dlp");
  dlp_result->set_status(
      enterprise_connectors::ContentAnalysisResponse::Result::SUCCESS);

  // The malware scan finishes synchronously, and doesn't find anything.
  auto* malware_result = sync_response.add_results();
  malware_result->set_tag("malware");
  malware_result->set_status(
      enterprise_connectors::ContentAnalysisResponse::Result::SUCCESS);

  ExpectContentAnalysisResumableMetadataResponse({"dlp", "malware"});
  ExpectContentAnalysisResumableContentResponse(sync_response);

  GURL url = embedded_test_server()->GetURL(
      "/safe_browsing/download_protection/zipfile_two_archives.zip");
  ui_test_utils::NavigateToURLWithDisposition(
      browser(), url, WindowOpenDisposition::CURRENT_TAB,
      ui_test_utils::BROWSER_TEST_WAIT_FOR_LOAD_STOP);

  WaitForDeepScanRequest();

  EXPECT_EQ(last_request().reason(),
            enterprise_connectors::ContentAnalysisRequest::NORMAL_DOWNLOAD);

  WaitForDownloadToFinish();

  // The file should be deep scanned, and safe.
  ASSERT_EQ(download_items().size(), 1u);
  download::DownloadItem* item = *download_items().begin();
  EXPECT_EQ(
      item->GetDangerType(),
      download::DownloadDangerType::DOWNLOAD_DANGER_TYPE_DEEP_SCANNED_SAFE);
  EXPECT_EQ(item->GetState(), download::DownloadItem::COMPLETE);
}

IN_PROC_BROWSER_TEST_P(DownloadDeepScanningBrowserTest, DataURLDownloadIsTruncated) {
  base::ScopedAllowBlockingForTesting allow_blocking;

  ClientDownloadResponse metadata_response;
  metadata_response.set_verdict(ClientDownloadResponse::SAFE);
  ExpectMetadataResponse(metadata_response);

  enterprise_connectors::ContentAnalysisResponse sync_response;
  auto* dlp_result = sync_response.add_results();
  dlp_result->set_tag("dlp");
  dlp_result->set_status(
      enterprise_connectors::ContentAnalysisResponse::Result::SUCCESS);

  auto* malware_result = sync_response.add_results();
  malware_result->set_tag("malware");
  malware_result->set_status(
      enterprise_connectors::ContentAnalysisResponse::Result::SUCCESS);

  ExpectContentAnalysisResumableMetadataResponse({"dlp", "malware"});
  ExpectContentAnalysisResumableContentResponse(sync_response);

  // Needs to be an octet-stream to trigger download
  std::string data_url_string = "data:application/octet-stream;base64,";
  data_url_string.append(10000, 'A');
  GURL url(data_url_string);

  ui_test_utils::NavigateToURLWithDisposition(
      browser(), url, WindowOpenDisposition::CURRENT_TAB,
      ui_test_utils::BROWSER_TEST_WAIT_FOR_LOAD_STOP);

  WaitForDeepScanRequest();

  EXPECT_EQ(last_request().reason(),
            enterprise_connectors::ContentAnalysisRequest::NORMAL_DOWNLOAD);

  // The truncate limit is 8192 characters, but may be less to ensure a valid
  // base64 encoding.
  EXPECT_LE(last_request().request_data().url().size(), 8192u);
  EXPECT_GT(last_request().request_data().referrer_chain_size(), 0);
  EXPECT_LE(last_request().request_data().referrer_chain(0).url().size(),
            8192u);

  ASSERT_TRUE(last_request().request_data().has_csd());
  ASSERT_GT(last_request().request_data().csd().referrer_chain_size(), 0);
  ASSERT_GT(last_request().request_data().csd().resources_size(), 0);
  EXPECT_LE(last_request().request_data().csd().resources(0).url().size(),
            8192u);
  EXPECT_LE(last_request().request_data().csd().url().size(), 8192u);

  WaitForDownloadToFinish();

  ASSERT_EQ(download_items().size(), 1u);
  download::DownloadItem* item = *download_items().begin();
  EXPECT_EQ(
      item->GetDangerType(),
      download::DownloadDangerType::DOWNLOAD_DANGER_TYPE_DEEP_SCANNED_SAFE);
  EXPECT_EQ(item->GetState(), download::DownloadItem::COMPLETE);
}

IN_PROC_BROWSER_TEST_P(DownloadDeepScanningBrowserTest, FailedScanFailsOpen) {
  // This allows the blocking DM token reads happening on profile-Connector
  // triggers.
  base::ScopedAllowBlockingForTesting allow_blocking;

  // The file is SAFE according to the metadata check
  ClientDownloadResponse metadata_response;
  metadata_response.set_verdict(ClientDownloadResponse::SAFE);
  ExpectMetadataResponse(metadata_response);

  // The DLP scan runs synchronously, but doesn't find anything.
  enterprise_connectors::ContentAnalysisResponse sync_response;
  auto* dlp_result = sync_response.add_results();
  dlp_result->set_tag("dlp");
  dlp_result->set_status(
      enterprise_connectors::ContentAnalysisResponse::Result::SUCCESS);

  // The malware scan finishes synchronously, and fails.
  auto* malware_result = sync_response.add_results();
  malware_result->set_tag("malware");
  malware_result->set_status(
      enterprise_connectors::ContentAnalysisResponse::Result::FAILURE);

  ExpectContentAnalysisResumableMetadataResponse({"dlp", "malware"});
  ExpectContentAnalysisResumableContentResponse(sync_response);

  GURL url = embedded_test_server()->GetURL(
      "/safe_browsing/download_protection/zipfile_two_archives.zip");
  ui_test_utils::NavigateToURLWithDisposition(
      browser(), url, WindowOpenDisposition::CURRENT_TAB,
      ui_test_utils::BROWSER_TEST_WAIT_FOR_LOAD_STOP);

  WaitForDeepScanRequest();

  EXPECT_EQ(last_request().reason(),
            enterprise_connectors::ContentAnalysisRequest::NORMAL_DOWNLOAD);

  WaitForDownloadToFinish();

  // The file should be safe, but not deep scanned.
  ASSERT_EQ(download_items().size(), 1u);
  download::DownloadItem* item = *download_items().begin();
  EXPECT_EQ(item->GetDangerType(),
            download::DownloadDangerType::DOWNLOAD_DANGER_TYPE_NOT_DANGEROUS);
  EXPECT_EQ(item->GetState(), download::DownloadItem::COMPLETE);
}

IN_PROC_BROWSER_TEST_P(DownloadDeepScanningBrowserTest,
                       PartialFailureShowsMalwareWarning) {
  // This allows the blocking DM token reads happening on profile-Connector
  // triggers.
  base::ScopedAllowBlockingForTesting allow_blocking;

  // The file is SAFE according to the metadata check
  ClientDownloadResponse metadata_response;
  metadata_response.set_verdict(ClientDownloadResponse::SAFE);
  ExpectMetadataResponse(metadata_response);

  // The DLP scan runs synchronously, and fails.
  enterprise_connectors::ContentAnalysisResponse sync_response;
  auto* dlp_result = sync_response.add_results();
  dlp_result->set_tag("dlp");
  dlp_result->set_status(
      enterprise_connectors::ContentAnalysisResponse::Result::FAILURE);

  // The malware scan finishes synchronously, and finds malware.
  auto* malware_result = sync_response.add_results();
  malware_result->set_tag("malware");
  malware_result->set_status(
      enterprise_connectors::ContentAnalysisResponse::Result::SUCCESS);
  auto* malware_rule = malware_result->add_triggered_rules();
  malware_rule->set_action(enterprise_connectors::TriggeredRule::BLOCK);
  malware_rule->set_rule_name("malware");

  ExpectContentAnalysisResumableMetadataResponse({"dlp", "malware"});
  ExpectContentAnalysisResumableContentResponse(sync_response);

  GURL url = embedded_test_server()->GetURL(
      "/safe_browsing/download_protection/zipfile_two_archives.zip");
  ui_test_utils::NavigateToURLWithDisposition(
      browser(), url, WindowOpenDisposition::CURRENT_TAB,
      ui_test_utils::BROWSER_TEST_WAIT_FOR_LOAD_STOP);

  WaitForDeepScanRequest();

  EXPECT_EQ(last_request().reason(),
            enterprise_connectors::ContentAnalysisRequest::NORMAL_DOWNLOAD);

  WaitForDownloadToFinish();

  // The file should be dangerous.
  ASSERT_EQ(download_items().size(), 1u);
  download::DownloadItem* item = *download_items().begin();
  EXPECT_EQ(
      item->GetDangerType(),
      download::DownloadDangerType::DOWNLOAD_DANGER_TYPE_DANGEROUS_CONTENT);
  EXPECT_EQ(item->GetState(), download::DownloadItem::IN_PROGRESS);
}

IN_PROC_BROWSER_TEST_P(DownloadDeepScanningBrowserTest,
                       PartialFailureShowsDlpWarning) {
  // This allows the blocking DM token reads happening on profile-Connector
  // triggers.
  base::ScopedAllowBlockingForTesting allow_blocking;

  // The file is SAFE according to the metadata check
  ClientDownloadResponse metadata_response;
  metadata_response.set_verdict(ClientDownloadResponse::SAFE);
  ExpectMetadataResponse(metadata_response);

  // The DLP scan runs synchronously, and finds a violation.
  enterprise_connectors::ContentAnalysisResponse sync_response;
  auto* dlp_result = sync_response.add_results();
  dlp_result->set_tag("dlp");
  dlp_result->set_status(
      enterprise_connectors::ContentAnalysisResponse::Result::SUCCESS);
  auto* dlp_rule = dlp_result->add_triggered_rules();
  dlp_rule->set_action(enterprise_connectors::TriggeredRule::BLOCK);

  // The malware scan finishes synchronously, and fails.
  auto* malware_result = sync_response.add_results();
  malware_result->set_tag("malware");
  malware_result->set_status(
      enterprise_connectors::ContentAnalysisResponse::Result::FAILURE);

  ExpectContentAnalysisResumableMetadataResponse({"dlp", "malware"});
  ExpectContentAnalysisResumableContentResponse(sync_response);

  GURL url = embedded_test_server()->GetURL(
      "/safe_browsing/download_protection/zipfile_two_archives.zip");
  ui_test_utils::NavigateToURLWithDisposition(
      browser(), url, WindowOpenDisposition::CURRENT_TAB,
      ui_test_utils::BROWSER_TEST_WAIT_FOR_LOAD_STOP);

  WaitForDeepScanRequest();

  EXPECT_EQ(last_request().reason(),
            enterprise_connectors::ContentAnalysisRequest::NORMAL_DOWNLOAD);

  WaitForDownloadToFinish();

  // The file should be blocked for sensitive content.
  ASSERT_EQ(download_items().size(), 1u);
  download::DownloadItem* item = *download_items().begin();
  EXPECT_EQ(item->GetDangerType(),
            download::DownloadDangerType::
                DOWNLOAD_DANGER_TYPE_SENSITIVE_CONTENT_BLOCK);
  EXPECT_EQ(item->GetState(), download::DownloadItem::INTERRUPTED);
}

// Regardless of resumable or multipart upload protocol, when a file is password
// protected and the `block_password_protected` setting is on, the file should
// be blocked.
IN_PROC_BROWSER_TEST_P(DownloadDeepScanningBrowserTest,
                       PasswordProtectedTxtFilesAreBlocked) {
  // TODO(crbug.com/378490429): Add support for obfuscated password protected
  // files.
  if (is_obfuscated()) {
    GTEST_SKIP() << "Encryption status cannot be detected in obfuscated files.";
  }
  // This allows the blocking DM token reads happening on profile-Connector
  // triggers.
  base::ScopedAllowBlockingForTesting allow_blocking;

  // The file is SAFE according to the metadata check
  ClientDownloadResponse metadata_response;
  metadata_response.set_verdict(ClientDownloadResponse::SAFE);
  ExpectMetadataResponse(metadata_response);

  GURL url = embedded_test_server()->GetURL(
      "/safe_browsing/download_protection/encrypted_txt.zip");
  ui_test_utils::NavigateToURLWithDisposition(
      browser(), url, WindowOpenDisposition::CURRENT_TAB,
      ui_test_utils::BROWSER_TEST_WAIT_FOR_LOAD_STOP);

  WaitForDownloadToFinish();

  // The file should be blocked for containing a password protected file.
  ASSERT_EQ(download_items().size(), 1u);
  download::DownloadItem* item = *download_items().begin();
  EXPECT_EQ(item->GetDangerType(),
            download::DownloadDangerType::
                DOWNLOAD_DANGER_TYPE_BLOCKED_PASSWORD_PROTECTED);
  EXPECT_EQ(item->GetState(), download::DownloadItem::INTERRUPTED);
}

// TODO(https://crbug.com/414822762): Reenable the test once the flakiness is
// fixed.
IN_PROC_BROWSER_TEST_P(DownloadDeepScanningBrowserTest,
                       DISABLED_DlpAndMalwareViolations) {
  // This allows the blocking DM token reads happening on profile-Connector
  // triggers.
  base::ScopedAllowBlockingForTesting allow_blocking;

  SetUpReporting();
  base::HistogramTester histograms;

  GURL url = embedded_test_server()->GetURL(
      "/safe_browsing/download_protection/zipfile_two_archives.zip");
  ui_test_utils::NavigateToURLWithDisposition(
      browser(), url, WindowOpenDisposition::CURRENT_TAB,
      ui_test_utils::BROWSER_TEST_WAIT_FOR_LOAD_STOP);

  // The DLP scan finishes synchronously and find a violation. Malware scanning
  // finds violation.
  enterprise_connectors::ContentAnalysisResponse sync_response;
  auto* dlp_result = sync_response.add_results();
  dlp_result->set_tag("dlp");
  dlp_result->set_status(
      enterprise_connectors::ContentAnalysisResponse::Result::SUCCESS);
  auto* dlp_rule = dlp_result->add_triggered_rules();
  dlp_rule->set_action(enterprise_connectors::TriggeredRule::WARN);
  dlp_rule->set_rule_name("dlp_rule_name");
  auto* malware_result = sync_response.add_results();
  malware_result->set_tag("malware");
  malware_result->set_status(
      enterprise_connectors::ContentAnalysisResponse::Result::SUCCESS);
  auto* malware_rule = malware_result->add_triggered_rules();
  malware_rule->set_action(enterprise_connectors::TriggeredRule::WARN);
  malware_rule->set_rule_name("uws");

  ExpectContentAnalysisResumableMetadataResponse({"dlp", "malware"});
  ExpectContentAnalysisResumableContentResponse(sync_response);

  WaitForDeepScanRequest();

  EXPECT_EQ(last_request().reason(),
            enterprise_connectors::ContentAnalysisRequest::NORMAL_DOWNLOAD);

  // Both the DLP and malware violations generate an event.
  base::RunLoop run_loop;
  std::set<std::string> zip_types = {"application/zip",
                                     "application/x-zip-compressed"};
  enterprise_connectors::test::EventReportValidator validator(client());
  validator.SetDoneClosure(run_loop.QuitClosure());
  chrome::cros::reporting::proto::SafeBrowsingDangerousDownloadEvent
      dangerous_event;
  dangerous_event.set_url(url.spec());
  dangerous_event.set_tab_url(url.spec());
  dangerous_event.set_file_name(
      connectors_machine_scope()
          ? (*download_items().begin())->GetTargetFilePath().AsUTF8Unsafe()
          : "zipfile_two_archives.zip");
  dangerous_event.set_download_digest_sha256(
      "339C8FFDAE735C4F1846D0E6FF07FBD85CAEE6D96045AAEF5B30F3220836643C");
  dangerous_event.set_threat_type(
      chrome::cros::reporting::proto::SafeBrowsingDangerousDownloadEvent::
          POTENTIALLY_UNWANTED);
  dangerous_event.set_trigger(chrome::cros::reporting::proto::FILE_DOWNLOAD);
  dangerous_event.set_content_size(
      is_obfuscated() ? 276 + kSingleChunkObfuscationOverhead : 276);
  dangerous_event.set_event_result(
      chrome::cros::reporting::proto::EventResult::EVENT_RESULT_WARNED);
  dangerous_event.set_profile_user_name(kUserName);
  dangerous_event.set_profile_identifier(GetProfileIdentifier());
  dangerous_event.set_scan_id(last_request().request_token());

  chrome::cros::reporting::proto::DlpSensitiveDataEvent dlp_event;
  dlp_event.set_url(url.spec());
  dlp_event.set_tab_url(url.spec());
  dlp_event.set_file_name(
      connectors_machine_scope()
          ? (*download_items().begin())->GetTargetFilePath().AsUTF8Unsafe()
          : "zipfile_two_archives.zip");
  dlp_event.set_download_digest_sha_256(
      "339C8FFDAE735C4F1846D0E6FF07FBD85CAEE6D96045AAEF5B30F3220836643C");
  dlp_event.set_trigger(chrome::cros::reporting::proto::FILE_DOWNLOAD);
  dlp_event.set_content_size(
      is_obfuscated() ? 276 + kSingleChunkObfuscationOverhead : 276);
  dlp_event.set_event_result(
      chrome::cros::reporting::proto::EventResult::EVENT_RESULT_WARNED);
  dlp_event.set_profile_user_name(kUserName);
  dlp_event.set_profile_identifier(GetProfileIdentifier());
  dlp_event.set_scan_id(last_request().request_token());

  validator.ExpectDangerousDeepScanningResultAndSensitiveDataEvent(
      std::move(dangerous_event), std::move(dlp_event), &zip_types);
  WaitForDownloadToFinish();

  // The file should be blocked.
  ASSERT_EQ(download_items().size(), 1u);
  download::DownloadItem* item = *download_items().begin();
  EXPECT_EQ(
      item->GetDangerType(),
      download::DownloadDangerType::DOWNLOAD_DANGER_TYPE_POTENTIALLY_UNWANTED);
  EXPECT_EQ(item->GetState(), download::DownloadItem::IN_PROGRESS);

  // UMAs for this request should only be recorded once.
  histograms.ExpectUniqueSample(
      "SafeBrowsingBinaryUploadRequest.Result",
      enterprise_connectors::ScanRequestUploadResult::kSuccess, 1);
  histograms.ExpectUniqueSample("SafeBrowsingBinaryUploadRequest.DlpResult",
                                true, 1);
  histograms.ExpectUniqueSample("SafeBrowsingBinaryUploadRequest.MalwareResult",
                                true, 1);
  run_loop.Run();
}

class DownloadRestrictionsDeepScanningBrowserTest
    : public DownloadDeepScanningBrowserTestBase,
      public testing::WithParamInterface<bool> {
 public:
  DownloadRestrictionsDeepScanningBrowserTest()
      : DownloadDeepScanningBrowserTestBase(
            /*connectors_machine_scope=*/GetParam(),
            /*is_consumer=*/false,
            /*is_obfuscated=*/false) {}
  ~DownloadRestrictionsDeepScanningBrowserTest() override = default;

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

    browser()->GetProfile()->GetPrefs()->SetInteger(
        policy::policy_prefs::kDownloadRestrictions,
        static_cast<int>(policy::DownloadRestriction::DANGEROUS_FILES));
    enterprise_connectors::test::SetAnalysisConnector(
        browser()->GetProfile()->GetPrefs(),
        enterprise_connectors::FILE_DOWNLOADED,
        R"({
                              "service_provider": "google",
                              "enable": [
                                {
                                  "url_list": ["*"],
                                  "tags": ["malware"]
                                }
                              ],
                              "block_until_verdict": 1
                            })",
        connectors_machine_scope());
  }

 private:
  base::test::ScopedFeatureList feature_list;
};

INSTANTIATE_TEST_SUITE_P(,
                         DownloadRestrictionsDeepScanningBrowserTest,
                         testing::Bool());

IN_PROC_BROWSER_TEST_P(DownloadRestrictionsDeepScanningBrowserTest,
                       ReportsDownloadsBlockedByDownloadRestrictions) {
  safe_browsing::FileTypePoliciesTestOverlay scoped_all_file_types_dangerous =
      safe_browsing::ScopedMarkAllFilesDangerousForTesting();

  // This allows the blocking DM token reads happening on profile-Connector
  // triggers.
  base::ScopedAllowBlockingForTesting allow_blocking;

  SetUpReporting();

  GURL url = embedded_test_server()->GetURL(
      "/safe_browsing/download_protection/zipfile_two_archives.zip");
  ui_test_utils::NavigateToURLWithDisposition(
      browser(), url, WindowOpenDisposition::CURRENT_TAB,
      ui_test_utils::BROWSER_TEST_NO_WAIT);

  base::FilePath main_file = DownloadPrefs(browser()->GetProfile())
                                 .DownloadPath()
                                 .AppendASCII("zipfile_two_archives.zip");
  base::RunLoop run_loop;
  enterprise_connectors::test::EventReportValidator validator(client());
  validator.SetDoneClosure(run_loop.QuitClosure());
  std::set<std::string> zip_types = {"application/zip",
                                     "application/x-zip-compressed"};

  chrome::cros::reporting::proto::SafeBrowsingDangerousDownloadEvent
      expected_event;
  expected_event.set_url(url.spec());
  expected_event.set_tab_url(url.spec());
  expected_event.set_source("");
  expected_event.set_destination("");
#if BUILDFLAG(IS_CHROMEOS)
  expected_event.set_file_name("zipfile_two_archives.zip");
#else
  connectors_machine_scope()
      ? expected_event.set_file_name(main_file.AsUTF8Unsafe())
      : expected_event.set_file_name("zipfile_two_archives.zip");
#endif
  expected_event.set_content_size(276);
  expected_event.set_download_digest_sha256("");
  expected_event.set_threat_type(
      chrome::cros::reporting::proto::SafeBrowsingDangerousDownloadEvent::
          DANGEROUS_FILE_TYPE);

  expected_event.set_trigger(
      chrome::cros::reporting::proto::DataTransferEventTrigger::FILE_DOWNLOAD);

  expected_event.set_event_result(
      chrome::cros::reporting::proto::EventResult::EVENT_RESULT_BLOCKED);
  expected_event.set_clicked_through(false);

  ::chrome::cros::reporting::proto::UrlInfo referrers;
  referrers.set_url(url.spec());
  referrers.set_ip(embedded_test_server()->base_url().host());
  *expected_event.add_referrers() = referrers;

  expected_event.set_profile_identifier(GetProfileIdentifier());
  expected_event.set_profile_user_name(kUserName);

  validator.ExpectDangerousDownloadEvent(std::move(expected_event), &zip_types);

  WaitForDownloadToFinish();

  run_loop.Run();

  ASSERT_EQ(download_items().size(), 1u);
  download::DownloadItem* item = *download_items().begin();
  EXPECT_EQ(item->GetDangerType(),
            download::DownloadDangerType::DOWNLOAD_DANGER_TYPE_NOT_DANGEROUS);
  EXPECT_EQ(item->GetState(), download::DownloadItem::INTERRUPTED);
}

class AllowlistedUrlDeepScanningBrowserTest
    : public DownloadDeepScanningBrowserTestBase,
      public testing::WithParamInterface<std::tuple<bool, bool>> {
 public:
  AllowlistedUrlDeepScanningBrowserTest()
      : DownloadDeepScanningBrowserTestBase(
            /*connectors_machine_scope=*/std::get<0>(GetParam()),
            /*is_consumer=*/false,
            /*is_obfuscated=*/std::get<1>(GetParam())) {}
  ~AllowlistedUrlDeepScanningBrowserTest() override = default;

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

    base::ListValue domain_list;
    domain_list.Append(embedded_test_server()->base_url().host());
    browser()->GetProfile()->GetPrefs()->SetList(
        prefs::kSafeBrowsingAllowlistDomains, std::move(domain_list));
  }
};

INSTANTIATE_TEST_SUITE_P(,
                         AllowlistedUrlDeepScanningBrowserTest,
                         testing::Combine(
                             /*connectors_machine_scope=*/testing::Bool(),
                             /*is_obfuscated=*/testing::Bool()));

IN_PROC_BROWSER_TEST_P(AllowlistedUrlDeepScanningBrowserTest,
                       AllowlistedUrlStillDoesDlpAndMalware) {
  // This allows the blocking DM token reads happening on profile-Connector
  // triggers.
  base::ScopedAllowBlockingForTesting allow_blocking;

  // The file is SAFE according to the metadata check
  ClientDownloadResponse metadata_response;
  metadata_response.set_verdict(ClientDownloadResponse::SAFE);
  ExpectMetadataResponse(metadata_response);

  // The DLP scan runs synchronously, and finds a violation.
  enterprise_connectors::ContentAnalysisResponse sync_response;
  auto* result = sync_response.add_results();
  result->set_tag("dlp");
  result->set_status(
      enterprise_connectors::ContentAnalysisResponse::Result::SUCCESS);
  auto* dlp_rule = result->add_triggered_rules();
  dlp_rule->set_action(enterprise_connectors::TriggeredRule::BLOCK);

  // The malware scan also runs synchronously, but finds no violation
  result = sync_response.add_results();
  result->set_tag("malware");
  result->set_status(
      enterprise_connectors::ContentAnalysisResponse::Result::SUCCESS);

  ExpectContentAnalysisResumableMetadataResponse({"dlp", "malware"});
  ExpectContentAnalysisResumableContentResponse(sync_response);

  GURL url = embedded_test_server()->GetURL(
      "/safe_browsing/download_protection/zipfile_two_archives.zip");
  ui_test_utils::NavigateToURLWithDisposition(
      browser(), url, WindowOpenDisposition::CURRENT_TAB,
      ui_test_utils::BROWSER_TEST_NO_WAIT);

  WaitForDeepScanRequest();

  EXPECT_EQ(last_request().reason(),
            enterprise_connectors::ContentAnalysisRequest::NORMAL_DOWNLOAD);

  WaitForDownloadToFinish();

  // The file should be blocked for sensitive content.
  ASSERT_EQ(download_items().size(), 1u);
  download::DownloadItem* item = *download_items().begin();
  EXPECT_EQ(item->GetDangerType(),
            download::DownloadDangerType::
                DOWNLOAD_DANGER_TYPE_SENSITIVE_CONTENT_BLOCK);
  EXPECT_EQ(item->GetState(), download::DownloadItem::INTERRUPTED);
}

class WaitForModalObserver : public DeepScanningRequest::Observer {
 public:
  explicit WaitForModalObserver(DeepScanningRequest* request)
      : request_(request),
        run_loop_(base::RunLoop::Type::kNestableTasksAllowed) {
    request_->AddObserver(this);
  }

  ~WaitForModalObserver() override {
    if (request_) {
      request_->RemoveObserver(this);
    }
  }

  void Wait() { run_loop_.Run(); }

  void OnFinish(DeepScanningRequest* request) override {
    request_->RemoveObserver(this);
    request_ = nullptr;
  }

 private:
  raw_ptr<DeepScanningRequest> request_;
  base::RunLoop run_loop_;
};

class WaitForFinishObserver : public DeepScanningRequest::Observer {
 public:
  explicit WaitForFinishObserver(DeepScanningRequest* request)
      : request_(request),
        run_loop_(base::RunLoop::Type::kNestableTasksAllowed) {
    request_->AddObserver(this);
  }

  ~WaitForFinishObserver() override {
    if (request_) {
      request_->RemoveObserver(this);
    }
  }

  void Wait() { run_loop_.Run(); }

  void OnFinish(DeepScanningRequest* request) override {
    run_loop_.Quit();
    request_->RemoveObserver(this);
    request_ = nullptr;
  }

 private:
  raw_ptr<DeepScanningRequest> request_;
  base::RunLoop run_loop_;
};

class SavePackageDeepScanningBrowserTest
    : public DownloadDeepScanningBrowserTestBase {
 public:
  SavePackageDeepScanningBrowserTest()
      : DownloadDeepScanningBrowserTestBase(/*connectors_machine_scope=*/true,
                                            /*is_consumer=*/false,
                                            /*is_obfuscated=*/false) {}

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

  base::FilePath GetTestFilePath() {
    return GetTestDataDirectory().AppendASCII("save_page/text.txt");
  }
};

IN_PROC_BROWSER_TEST_F(SavePackageDeepScanningBrowserTest, Allowed) {
  SetUpReporting();

  EXPECT_TRUE(ui_test_utils::NavigateToURL(
      browser(), embedded_test_server()->GetURL("/save_page/text.txt")));

  enterprise_connectors::ContentAnalysisResponse response;
  auto* result = response.add_results();
  result->set_tag("dlp");
  result->set_status(
      enterprise_connectors::ContentAnalysisResponse::Result::SUCCESS);

  ExpectContentAnalysisResumableMetadataResponse({"dlp"});
  ExpectContentAnalysisResumableContentResponse(response);

  base::RunLoop run_loop;
  content::SavePackageFinishedObserver observer(
      browser()->GetProfile()->GetDownloadManager(), run_loop.QuitClosure());
  base::FilePath main_file = GetSaveDir().AppendASCII("text.htm");
  base::FilePath extra_files_dir = GetSaveDir().AppendASCII("text_files");
  ASSERT_TRUE(browser()->GetTabStripModel()->GetActiveWebContents()->SavePage(
      main_file, extra_files_dir, content::SAVE_PAGE_TYPE_AS_ONLY_HTML));
  WaitForDeepScanRequest();

  EXPECT_EQ(last_request().reason(),
            enterprise_connectors::ContentAnalysisRequest::SAVE_AS_DOWNLOAD);

  // The response should not trigger a security event.
  enterprise_connectors::test::EventReportValidator validator(client());
  validator.ExpectNoReport();

  run_loop.Run();

  ASSERT_EQ(download_items().size(), 1u);
  download::DownloadItem* item = *download_items().begin();
  EXPECT_EQ(
      item->GetDangerType(),
      download::DownloadDangerType::DOWNLOAD_DANGER_TYPE_DEEP_SCANNED_SAFE);
  EXPECT_EQ(item->GetState(), download::DownloadItem::COMPLETE);

  base::ScopedAllowBlockingForTesting allow_blocking;
  EXPECT_TRUE(base::PathExists(main_file));
  EXPECT_TRUE(base::ContentsEqual(GetTestFilePath(), main_file));
  EXPECT_FALSE(base::PathExists(extra_files_dir));
}

IN_PROC_BROWSER_TEST_F(SavePackageDeepScanningBrowserTest, Blocked) {
  SetUpReporting();

  GURL url = embedded_test_server()->GetURL("/save_page/text.txt");
  EXPECT_TRUE(ui_test_utils::NavigateToURL(browser(), url));

  // Prepares a scanning response that indicates the file should be blocked.
  enterprise_connectors::ContentAnalysisResponse response;
  auto* result = response.add_results();
  result->set_tag("dlp");
  result->set_status(
      enterprise_connectors::ContentAnalysisResponse::Result::SUCCESS);
  auto* dlp_verdict = result->add_triggered_rules();
  dlp_verdict->set_action(enterprise_connectors::TriggeredRule::BLOCK);

  ExpectContentAnalysisResumableMetadataResponse({"dlp"});
  ExpectContentAnalysisResumableContentResponse(response);

  base::RunLoop run_loop;
  content::SavePackageFinishedObserver observer(
      browser()->GetProfile()->GetDownloadManager(), run_loop.QuitClosure(),
      {download::DownloadItem::INTERRUPTED});
  base::FilePath main_file = GetSaveDir().AppendASCII("text.htm");
  base::FilePath extra_files_dir = GetSaveDir().AppendASCII("text_files");
  ASSERT_TRUE(browser()->GetTabStripModel()->GetActiveWebContents()->SavePage(
      main_file, extra_files_dir, content::SAVE_PAGE_TYPE_AS_ONLY_HTML));
  WaitForDeepScanRequest();

  EXPECT_EQ(last_request().reason(),
            enterprise_connectors::ContentAnalysisRequest::SAVE_AS_DOWNLOAD);

  // The blocking response should trigger a security event.
  base::RunLoop validator_run_loop;
  enterprise_connectors::test::EventReportValidator validator(client());
  validator.SetDoneClosure(validator_run_loop.QuitClosure());
  std::set<std::string> mimetypes = {"text/plain"};

  chrome::cros::reporting::proto::DlpSensitiveDataEvent expected_event;
  expected_event.set_url(url.spec());
  expected_event.set_tab_url(url.spec());
  expected_event.set_source("");
  expected_event.set_destination("");
  expected_event.set_download_digest_sha_256(
      "9789A2E12D50EFA4B891D4EF95C5189FA4C98E34C84E1F8017CD8F574CA035DD");
#if BUILDFLAG(IS_CHROMEOS)
  expected_event.set_file_name("text.htm");
#else
  expected_event.set_file_name(main_file.AsUTF8Unsafe());
#endif
  expected_event.set_content_type("text/plain");
  expected_event.set_content_size(54);
  expected_event.set_scan_id(last_request().request_token());
  expected_event.set_trigger(
      chrome::cros::reporting::proto::DataTransferEventTrigger::FILE_DOWNLOAD);
  expected_event.set_event_result(
      chrome::cros::reporting::proto::EventResult::EVENT_RESULT_BLOCKED);
  expected_event.set_clicked_through(false);

  chrome::cros::reporting::proto::TriggeredRuleInfo triggered_rule;
  triggered_rule.set_action(
      chrome::cros::reporting::proto::TriggeredRuleInfo::BLOCK);
  *expected_event.add_triggered_rule_info() = triggered_rule;

  ::chrome::cros::reporting::proto::UrlInfo referrers;
  referrers.set_url(url.spec());
  referrers.set_ip(embedded_test_server()->base_url().host());
  *expected_event.add_referrers() = referrers;

  expected_event.set_profile_identifier(GetProfileIdentifier());
  expected_event.set_profile_user_name(kUserName);

  validator.ExpectSensitiveDataEvent(std::move(expected_event));

  run_loop.Run();

  ASSERT_EQ(download_items().size(), 1u);
  download::DownloadItem* item = *download_items().begin();
  EXPECT_EQ(item->GetDangerType(),
            download::DownloadDangerType::
                DOWNLOAD_DANGER_TYPE_SENSITIVE_CONTENT_BLOCK);
  EXPECT_EQ(item->GetState(), download::DownloadItem::INTERRUPTED);

  base::ScopedAllowBlockingForTesting allow_blocking;
  EXPECT_FALSE(base::PathExists(main_file));
  EXPECT_FALSE(base::PathExists(extra_files_dir));
  validator_run_loop.Run();
}

IN_PROC_BROWSER_TEST_F(SavePackageDeepScanningBrowserTest, KeepAfterWarning) {
  SetUpReporting();

  GURL url = embedded_test_server()->GetURL("/save_page/text.txt");
  EXPECT_TRUE(ui_test_utils::NavigateToURL(browser(), url));

  // Prepares a scanning response that indicates the file should warn the user.
  enterprise_connectors::ContentAnalysisResponse response;
  auto* result = response.add_results();
  result->set_tag("dlp");
  result->set_status(
      enterprise_connectors::ContentAnalysisResponse::Result::SUCCESS);
  auto* dlp_verdict = result->add_triggered_rules();
  dlp_verdict->set_action(enterprise_connectors::TriggeredRule::WARN);

  ExpectContentAnalysisResumableMetadataResponse({"dlp"});
  ExpectContentAnalysisResumableContentResponse(response);

  base::RunLoop save_package_run_loop;
  content::SavePackageFinishedObserver observer(
      browser()->GetProfile()->GetDownloadManager(),
      save_package_run_loop.QuitClosure(), {download::DownloadItem::COMPLETE});
  base::FilePath main_file = GetSaveDir().AppendASCII("text.htm");
  base::FilePath extra_files_dir = GetSaveDir().AppendASCII("text_files");
  ASSERT_TRUE(browser()->GetTabStripModel()->GetActiveWebContents()->SavePage(
      main_file, extra_files_dir, content::SAVE_PAGE_TYPE_AS_ONLY_HTML));
  WaitForDeepScanRequest();

  EXPECT_EQ(last_request().reason(),
            enterprise_connectors::ContentAnalysisRequest::SAVE_AS_DOWNLOAD);

  // The warning response should trigger a security event.
  base::RunLoop validator_run_loop;
  enterprise_connectors::test::EventReportValidator validator(client());
  validator.SetDoneClosure(validator_run_loop.QuitClosure());
  std::set<std::string> mimetypes = {"text/plain"};

  chrome::cros::reporting::proto::DlpSensitiveDataEvent expected_event;
  expected_event.set_url(url.spec());
  expected_event.set_tab_url(url.spec());
  expected_event.set_source("");
  expected_event.set_destination("");
  expected_event.set_download_digest_sha_256(
      "9789A2E12D50EFA4B891D4EF95C5189FA4C98E34C84E1F8017CD8F574CA035DD");
#if BUILDFLAG(IS_CHROMEOS)
  expected_event.set_file_name("text.htm");
#else
  expected_event.set_file_name(main_file.AsUTF8Unsafe());
#endif
  expected_event.set_content_type("text/plain");
  expected_event.set_content_size(54);
  expected_event.set_scan_id(last_request().request_token());
  expected_event.set_trigger(
      chrome::cros::reporting::proto::DataTransferEventTrigger::FILE_DOWNLOAD);
  expected_event.set_event_result(
      chrome::cros::reporting::proto::EventResult::EVENT_RESULT_WARNED);
  expected_event.set_clicked_through(false);

  chrome::cros::reporting::proto::TriggeredRuleInfo triggered_rule;
  triggered_rule.set_action(
      chrome::cros::reporting::proto::TriggeredRuleInfo::WARN);
  *expected_event.add_triggered_rule_info() = triggered_rule;

  ::chrome::cros::reporting::proto::UrlInfo referrers;
  referrers.set_url(url.spec());
  referrers.set_ip(embedded_test_server()->base_url().host());
  *expected_event.add_referrers() = referrers;

  expected_event.set_profile_identifier(GetProfileIdentifier());
  expected_event.set_profile_user_name(kUserName);

  validator.ExpectSensitiveDataEvent(std::move(expected_event));
  validator_run_loop.Run();

  // The warning has been received but neither "keep" or "discard" has been
  // chosen at this point, so the download isn't complete or interrupted and the
  // file on disk is still not it its final location.
  ASSERT_EQ(download_items().size(), 1u);
  download::DownloadItem* item = *download_items().begin();
  EXPECT_EQ(item->GetDangerType(),
            download::DownloadDangerType::
                DOWNLOAD_DANGER_TYPE_SENSITIVE_CONTENT_WARNING);
  EXPECT_EQ(item->GetState(), download::DownloadItem::IN_PROGRESS);

  base::ScopedAllowBlockingForTesting allow_blocking;
  EXPECT_FALSE(base::PathExists(main_file));
  EXPECT_FALSE(base::PathExists(extra_files_dir));

  // Keeping the save package will generate a second warning event, complete the
  // download and move the file to its final destination.

  base::RunLoop validator_warn_run_loop;
  enterprise_connectors::test::EventReportValidator validator_warn(client());
  validator_warn.SetDoneClosure(validator_warn_run_loop.QuitClosure());

  chrome::cros::reporting::proto::DlpSensitiveDataEvent expected_event2;
  expected_event2.set_url(url.spec());
  expected_event2.set_tab_url(url.spec());
  expected_event2.set_source("");
  expected_event2.set_destination("");
  expected_event2.set_download_digest_sha_256(
      "9789A2E12D50EFA4B891D4EF95C5189FA4C98E34C84E1F8017CD8F574CA035DD");
#if BUILDFLAG(IS_CHROMEOS)
  expected_event2.set_file_name("text.htm");
#else
  expected_event2.set_file_name(main_file.AsUTF8Unsafe());
#endif
  expected_event2.set_content_type("text/plain");
  expected_event2.set_content_size(54);
  expected_event2.set_scan_id(last_request().request_token());
  expected_event2.set_trigger(
      chrome::cros::reporting::proto::DataTransferEventTrigger::FILE_DOWNLOAD);
  expected_event2.set_event_result(
      chrome::cros::reporting::proto::EventResult::EVENT_RESULT_BYPASSED);
  expected_event2.set_clicked_through(true);

  chrome::cros::reporting::proto::TriggeredRuleInfo triggered_rule2;
  triggered_rule2.set_action(
      chrome::cros::reporting::proto::TriggeredRuleInfo::WARN);
  *expected_event2.add_triggered_rule_info() = triggered_rule2;

  ::chrome::cros::reporting::proto::UrlInfo referrers2;
  referrers2.set_url(url.spec());
  referrers2.set_ip(embedded_test_server()->base_url().host());
  *expected_event2.add_referrers() = referrers2;

  expected_event2.set_profile_identifier(GetProfileIdentifier());
  expected_event2.set_profile_user_name(kUserName);

  validator_warn.ExpectSensitiveDataEvent(std::move(expected_event2));

  DownloadItemModel model(item);
  DownloadCommands(model.GetWeakPtr()).ExecuteCommand(DownloadCommands::KEEP);
  save_package_run_loop.Run();

  ASSERT_EQ(download_items().size(), 1u);
  EXPECT_EQ(item->GetDangerType(),
            download::DownloadDangerType::DOWNLOAD_DANGER_TYPE_USER_VALIDATED);
  EXPECT_EQ(item->GetState(), download::DownloadItem::COMPLETE);

  EXPECT_TRUE(base::PathExists(main_file));
  EXPECT_TRUE(base::ContentsEqual(GetTestFilePath(), main_file));
  EXPECT_FALSE(base::PathExists(extra_files_dir));
  validator_warn_run_loop.Run();
}

IN_PROC_BROWSER_TEST_F(SavePackageDeepScanningBrowserTest,
                       DiscardAfterWarning) {
  SetUpReporting();

  GURL url = embedded_test_server()->GetURL("/save_page/text.txt");
  EXPECT_TRUE(ui_test_utils::NavigateToURL(browser(), url));

  // Prepares a scanning response that indicates the file should warn the user.
  enterprise_connectors::ContentAnalysisResponse response;
  auto* result = response.add_results();
  result->set_tag("dlp");
  result->set_status(
      enterprise_connectors::ContentAnalysisResponse::Result::SUCCESS);
  auto* dlp_verdict = result->add_triggered_rules();
  dlp_verdict->set_action(enterprise_connectors::TriggeredRule::WARN);

  ExpectContentAnalysisResumableMetadataResponse({"dlp"});
  ExpectContentAnalysisResumableContentResponse(response);

  base::RunLoop save_package_run_loop;
  content::SavePackageFinishedObserver observer(
      browser()->GetProfile()->GetDownloadManager(),
      save_package_run_loop.QuitClosure(), {download::DownloadItem::CANCELLED});
  base::FilePath main_file = GetSaveDir().AppendASCII("text.htm");
  base::FilePath extra_files_dir = GetSaveDir().AppendASCII("text_files");
  ASSERT_TRUE(browser()->GetTabStripModel()->GetActiveWebContents()->SavePage(
      main_file, extra_files_dir, content::SAVE_PAGE_TYPE_AS_ONLY_HTML));
  WaitForDeepScanRequest();

  EXPECT_EQ(last_request().reason(),
            enterprise_connectors::ContentAnalysisRequest::SAVE_AS_DOWNLOAD);

  // That warning response should trigger a security event.
  base::RunLoop validator_run_loop;
  enterprise_connectors::test::EventReportValidator validator(client());
  validator.SetDoneClosure(validator_run_loop.QuitClosure());
  std::set<std::string> mimetypes = {"text/plain"};

  chrome::cros::reporting::proto::DlpSensitiveDataEvent expected_event;
  expected_event.set_url(url.spec());
  expected_event.set_tab_url(url.spec());
  expected_event.set_source("");
  expected_event.set_destination("");
  expected_event.set_download_digest_sha_256(
      "9789A2E12D50EFA4B891D4EF95C5189FA4C98E34C84E1F8017CD8F574CA035DD");
#if BUILDFLAG(IS_CHROMEOS)
  expected_event.set_file_name("text.htm");
#else
  expected_event.set_file_name(main_file.AsUTF8Unsafe());
#endif
  expected_event.set_content_type("text/plain");
  expected_event.set_content_size(54);
  expected_event.set_scan_id(last_request().request_token());
  expected_event.set_trigger(
      chrome::cros::reporting::proto::DataTransferEventTrigger::FILE_DOWNLOAD);
  expected_event.set_event_result(
      chrome::cros::reporting::proto::EventResult::EVENT_RESULT_WARNED);
  expected_event.set_clicked_through(false);

  chrome::cros::reporting::proto::TriggeredRuleInfo triggered_rule;
  triggered_rule.set_action(
      chrome::cros::reporting::proto::TriggeredRuleInfo::WARN);
  *expected_event.add_triggered_rule_info() = triggered_rule;

  ::chrome::cros::reporting::proto::UrlInfo referrers;
  referrers.set_url(url.spec());
  referrers.set_ip(embedded_test_server()->base_url().host());
  *expected_event.add_referrers() = referrers;

  expected_event.set_profile_identifier(GetProfileIdentifier());
  expected_event.set_profile_user_name(kUserName);

  validator.ExpectSensitiveDataEvent(std::move(expected_event));

  validator_run_loop.Run();

  // The warning has been received but neither "keep" or "discard" has been
  // chosen at this point, so the download isn't complete or interrupted and the
  // file on disk is still not it its final location.
  ASSERT_EQ(download_items().size(), 1u);
  download::DownloadItem* item = *download_items().begin();
  EXPECT_EQ(item->GetDangerType(),
            download::DownloadDangerType::
                DOWNLOAD_DANGER_TYPE_SENSITIVE_CONTENT_WARNING);
  EXPECT_EQ(item->GetState(), download::DownloadItem::IN_PROGRESS);

  base::ScopedAllowBlockingForTesting allow_blocking;
  EXPECT_FALSE(base::PathExists(main_file));
  EXPECT_FALSE(base::PathExists(extra_files_dir));

  // Discarding the download will remove it from disk and from download_items().
  DownloadItemModel model(item);
  DownloadCommands(model.GetWeakPtr())
      .ExecuteCommand(DownloadCommands::DISCARD);
  save_package_run_loop.Run();

  ASSERT_TRUE(download_items().empty());
  EXPECT_FALSE(base::PathExists(main_file));
  EXPECT_FALSE(base::PathExists(extra_files_dir));
}

IN_PROC_BROWSER_TEST_F(SavePackageDeepScanningBrowserTest, OpenNow) {
  SetUpReporting();

  GURL url = embedded_test_server()->GetURL("/save_page/text.txt");
  EXPECT_TRUE(ui_test_utils::NavigateToURL(browser(), url));

  enterprise_connectors::ContentAnalysisResponse response;
  auto* result = response.add_results();
  result->set_tag("dlp");
  result->set_status(
      enterprise_connectors::ContentAnalysisResponse::Result::SUCCESS);
  auto* dlp_verdict = result->add_triggered_rules();
  dlp_verdict->set_action(enterprise_connectors::TriggeredRule::BLOCK);

  base::RunLoop validator_run_loop;
  enterprise_connectors::test::EventReportValidator validator(client());
  validator.SetDoneClosure(validator_run_loop.QuitClosure());
  std::set<std::string> mimetypes = {"text/plain"};

  ExpectContentAnalysisResumableMetadataResponse({"dlp"});
  ExpectContentAnalysisResumableContentResponse(response);

  base::RunLoop save_package_run_loop;
  content::SavePackageFinishedObserver observer(
      browser()->GetProfile()->GetDownloadManager(),
      save_package_run_loop.QuitClosure(), {download::DownloadItem::COMPLETE});
  base::FilePath main_file = GetSaveDir().AppendASCII("text.htm");
  base::FilePath extra_files_dir = GetSaveDir().AppendASCII("text_files");
  ASSERT_TRUE(browser()->GetTabStripModel()->GetActiveWebContents()->SavePage(
      main_file, extra_files_dir, content::SAVE_PAGE_TYPE_AS_ONLY_HTML));
  WaitForDeepScanRequest();

  chrome::cros::reporting::proto::DlpSensitiveDataEvent expected_event;
  expected_event.set_url(url.spec());
  expected_event.set_tab_url(url.spec());
  expected_event.set_source("");
  expected_event.set_destination("");
  expected_event.set_download_digest_sha_256(
      "9789A2E12D50EFA4B891D4EF95C5189FA4C98E34C84E1F8017CD8F574CA035DD");
#if BUILDFLAG(IS_CHROMEOS)
  expected_event.set_file_name("text.htm");
#else
  expected_event.set_file_name(main_file.AsUTF8Unsafe());
#endif
  expected_event.set_content_type("text/plain");
  expected_event.set_content_size(54);
  expected_event.set_scan_id(last_request().request_token());
  expected_event.set_trigger(
      chrome::cros::reporting::proto::DataTransferEventTrigger::FILE_DOWNLOAD);
  expected_event.set_event_result(
      chrome::cros::reporting::proto::EventResult::EVENT_RESULT_BLOCKED);
  expected_event.set_clicked_through(false);

  chrome::cros::reporting::proto::TriggeredRuleInfo triggered_rule;
  triggered_rule.set_action(
      chrome::cros::reporting::proto::TriggeredRuleInfo::BLOCK);
  *expected_event.add_triggered_rule_info() = triggered_rule;

  ::chrome::cros::reporting::proto::UrlInfo referrers;
  referrers.set_url(url.spec());
  referrers.set_ip(embedded_test_server()->base_url().host());
  *expected_event.add_referrers() = referrers;

  expected_event.set_profile_identifier(GetProfileIdentifier());
  expected_event.set_profile_user_name(kUserName);

  validator.ExpectSensitiveDataEvent(std::move(expected_event));

  EXPECT_EQ(last_request().reason(),
            enterprise_connectors::ContentAnalysisRequest::SAVE_AS_DOWNLOAD);

  // Opening the save package before the async response is obtained will
  // generate a warning event once it does come back, complete the
  // download and move the file to its final destination.
  ASSERT_EQ(download_items().size(), 1u);
  download::DownloadItem* item = *download_items().begin();
  DownloadItemModel model(item);
  model.CompleteSafeBrowsingScan();
  save_package_run_loop.Run();

  validator_run_loop.Run();

  EXPECT_EQ(item->GetDangerType(),
            download::DownloadDangerType::DOWNLOAD_DANGER_TYPE_USER_VALIDATED);
  EXPECT_EQ(item->GetState(), download::DownloadItem::COMPLETE);

  base::ScopedAllowBlockingForTesting allow_blocking;
  EXPECT_TRUE(base::PathExists(main_file));
  EXPECT_TRUE(base::ContentsEqual(GetTestFilePath(), main_file));
  EXPECT_FALSE(base::PathExists(extra_files_dir));
}

class FileSystemAccessDeepScanningBrowserTest
    : public DownloadDeepScanningBrowserTestBase {
 public:
  FileSystemAccessDeepScanningBrowserTest()
      : DownloadDeepScanningBrowserTestBase(/*connectors_machine_scope=*/true,
                                            /*is_consumer=*/false,
                                            /*is_obfuscated=*/false) {
    enabled_features_.push_back(
        safe_browsing::kEnterpriseFileSystemAccessDeepScan);
  }

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

    // Setup a temporary directory for the file save path.
    ASSERT_TRUE(temp_dir_.CreateUniqueTempDir());

    // Have file save path automatically selected by picker.
    ui::SelectFileDialog::SetFactory(
        std::make_unique<SelectPredeterminedFileDialogFactory>(
            std::vector<base::FilePath>{GetTestFilePath()}));

    // Navigate to a simple page for FS API calls to run from.
    ASSERT_TRUE(ui_test_utils::NavigateToURL(
        browser(), embedded_test_server()->GetURL("/title1.html")));
  }

  void TearDownOnMainThread() override {
    ui::SelectFileDialog::SetFactory(nullptr);
    DownloadDeepScanningBrowserTestBase::TearDownOnMainThread();
  }

  base::FilePath GetTestFilePath() {
    return temp_dir_.GetPath().AppendASCII("test_fsa_file.txt");
  }

  void InitiateWriteViaFSA(content::WebContents* web_contents,
                           const std::string& content) {
    // Script that triggers FSA API write. Executes async and creates promise.
    constexpr char js_script[] = R"(
      window.fsaPromise = (async (content) => {
        const handle = await window.showSaveFilePicker();
        const writable = await handle.createWritable();
        await writable.write(content);
        await writable.close();
      })($1);
    )";

    content::ExecuteScriptAsync(web_contents,
                                content::JsReplace(js_script, content));
  }

 protected:
  base::ScopedTempDir temp_dir_;
};

// For FSA writes that trigger a block deep scan verdict, write should be
// blocked and the destination file empty.
IN_PROC_BROWSER_TEST_F(FileSystemAccessDeepScanningBrowserTest, BlockedWrite) {
  SetUpReporting();

  // Prepare the scan response with DLP block and successful malware scan.
  enterprise_connectors::ContentAnalysisResponse response;
  auto* dlp_result = response.add_results();
  dlp_result->set_tag("dlp");
  dlp_result->set_status(
      enterprise_connectors::ContentAnalysisResponse::Result::SUCCESS);
  auto* dlp_rule = dlp_result->add_triggered_rules();
  dlp_rule->set_action(enterprise_connectors::TriggeredRule::BLOCK);

  auto* malware_result = response.add_results();
  malware_result->set_tag("malware");
  malware_result->set_status(
      enterprise_connectors::ContentAnalysisResponse::Result::SUCCESS);

  ExpectContentAnalysisResumableMetadataResponse({"dlp", "malware"});
  ExpectContentAnalysisResumableContentResponse(response);

  // Setup message queue for JS responses.
  content::WebContents* web_contents =
      browser()->GetTabStripModel()->GetActiveWebContents();
  ASSERT_TRUE(web_contents);
  content::DOMMessageQueue message_queue(web_contents);

  base::RunLoop validator_run_loop;
  enterprise_connectors::test::EventReportValidator validator(client());
  validator.SetDoneClosure(validator_run_loop.QuitClosure());

  InitiateWriteViaFSA(web_contents, kTestContent);

  WaitForDeepScanRequest();

  GURL url = embedded_test_server()->GetURL("/title1.html");
  std::set<std::string> mimetypes = {"text/plain"};

  chrome::cros::reporting::proto::DlpSensitiveDataEvent expected_event;
  expected_event.set_url(url.spec());
  expected_event.set_tab_url(url.spec());
  expected_event.set_source("");
  expected_event.set_destination("");
  expected_event.set_download_digest_sha_256(
      "6AE8A75555209FD6C44157C0AED8016E763FF435A19CF186F76863140143FF72");
#if BUILDFLAG(IS_CHROMEOS)
  expected_event.set_file_name("test_fsa_file.txt");
#else
  expected_event.set_file_name(GetTestFilePath().AsUTF8Unsafe());
#endif
  expected_event.set_content_type("text/plain");
  expected_event.set_content_size(sizeof(kTestContent) - 1);
  expected_event.set_scan_id(last_request().request_token());
  expected_event.set_trigger(
      chrome::cros::reporting::proto::DataTransferEventTrigger::FILE_DOWNLOAD);
  expected_event.set_event_result(
      chrome::cros::reporting::proto::EventResult::EVENT_RESULT_BLOCKED);
  expected_event.set_clicked_through(false);

  chrome::cros::reporting::proto::TriggeredRuleInfo triggered_rule;
  triggered_rule.set_action(
      chrome::cros::reporting::proto::TriggeredRuleInfo::BLOCK);
  *expected_event.add_triggered_rule_info() = triggered_rule;

  ::chrome::cros::reporting::proto::UrlInfo referrers;
  referrers.set_url(url.spec());
  referrers.set_ip(embedded_test_server()->base_url().host());
  *expected_event.add_referrers() = referrers;

  expected_event.set_profile_identifier(GetProfileIdentifier());
  expected_event.set_profile_user_name(kUserName);

  validator.ExpectSensitiveDataEvent(std::move(expected_event));

  EXPECT_EQ(last_request().reason(),
            enterprise_connectors::ContentAnalysisRequest::NORMAL_DOWNLOAD);

  validator_run_loop.Run();

  content::EvalJsResult result =
      content::EvalJs(web_contents, "window.fsaPromise");

  // TODO(crbug.com/407065784): Improve error message for SB checks.
  ASSERT_THAT(
      result,
      content::EvalJsResult::ErrorIs(
          "a JavaScript error: \"AbortError: Blocked by Safe Browsing.\"\n"));

  // File is created but remains empty due to block.
  base::ScopedAllowBlockingForTesting allow_blocking;
  EXPECT_TRUE(base::PathExists(GetTestFilePath()));
  EXPECT_EQ(0, base::GetFileSize(GetTestFilePath()));
}

// For FSA writes that trigger a safe deep scan verdict, write should be allowed
// and the destination file populated appropriately.
IN_PROC_BROWSER_TEST_F(FileSystemAccessDeepScanningBrowserTest, AllowedWrite) {
  SetUpReporting();

  // Prepare the scan response with no DLP or malware rules triggered.
  enterprise_connectors::ContentAnalysisResponse response;
  auto* dlp_result = response.add_results();
  dlp_result->set_tag("dlp");
  dlp_result->set_status(
      enterprise_connectors::ContentAnalysisResponse::Result::SUCCESS);

  auto* malware_result = response.add_results();
  malware_result->set_tag("malware");
  malware_result->set_status(
      enterprise_connectors::ContentAnalysisResponse::Result::SUCCESS);

  ExpectContentAnalysisResumableMetadataResponse({"dlp", "malware"});
  ExpectContentAnalysisResumableContentResponse(response);

  // Setup message queue for JS responses.
  content::WebContents* web_contents =
      browser()->GetTabStripModel()->GetActiveWebContents();
  ASSERT_TRUE(web_contents);
  content::DOMMessageQueue message_queue(web_contents);

  InitiateWriteViaFSA(web_contents, kTestContent);

  WaitForDeepScanRequest();

  EXPECT_EQ(last_request().reason(),
            enterprise_connectors::ContentAnalysisRequest::NORMAL_DOWNLOAD);

  ASSERT_TRUE(content::ExecJs(web_contents, "window.fsaPromise"));

  // Checks that file is written successfully.
  base::ScopedAllowBlockingForTesting allow_blocking;
  EXPECT_TRUE(base::PathExists(GetTestFilePath()));

  std::string file_content;
  EXPECT_TRUE(base::ReadFileToString(GetTestFilePath(), &file_content));
  EXPECT_EQ(file_content, kTestContent);
}

// For FSA writes that trigger a warn deep scan verdict, write should be allowed
// and the destination file populated appropriately.
IN_PROC_BROWSER_TEST_F(FileSystemAccessDeepScanningBrowserTest, WarnedWrite) {
  SetUpReporting();

  // Prepare the scan response with warn DLP rule triggered.
  enterprise_connectors::ContentAnalysisResponse response;
  auto* dlp_result = response.add_results();
  dlp_result->set_tag("dlp");
  dlp_result->set_status(
      enterprise_connectors::ContentAnalysisResponse::Result::SUCCESS);
  auto* dlp_rule = dlp_result->add_triggered_rules();
  dlp_rule->set_action(enterprise_connectors::TriggeredRule::WARN);

  auto* malware_result = response.add_results();
  malware_result->set_tag("malware");
  malware_result->set_status(
      enterprise_connectors::ContentAnalysisResponse::Result::SUCCESS);

  ExpectContentAnalysisResumableMetadataResponse({"dlp", "malware"});
  ExpectContentAnalysisResumableContentResponse(response);

  // Setup message queue for JS responses.
  content::WebContents* web_contents =
      browser()->GetTabStripModel()->GetActiveWebContents();
  ASSERT_TRUE(web_contents);
  content::DOMMessageQueue message_queue(web_contents);

  base::RunLoop validator_run_loop;
  enterprise_connectors::test::EventReportValidator validator(client());
  validator.SetDoneClosure(validator_run_loop.QuitClosure());

  InitiateWriteViaFSA(web_contents, kTestContent);

  WaitForDeepScanRequest();

  GURL url = embedded_test_server()->GetURL("/title1.html");
  std::set<std::string> mimetypes = {"text/plain"};

  chrome::cros::reporting::proto::DlpSensitiveDataEvent expected_event;
  expected_event.set_url(url.spec());
  expected_event.set_tab_url(url.spec());
  expected_event.set_source("");
  expected_event.set_destination("");
  expected_event.set_download_digest_sha_256(
      "6AE8A75555209FD6C44157C0AED8016E763FF435A19CF186F76863140143FF72");
#if BUILDFLAG(IS_CHROMEOS)
  expected_event.set_file_name("test_fsa_file.txt");
#else
  expected_event.set_file_name(GetTestFilePath().AsUTF8Unsafe());
#endif
  expected_event.set_content_type("text/plain");
  expected_event.set_content_size(sizeof(kTestContent) - 1);
  expected_event.set_scan_id(last_request().request_token());
  expected_event.set_trigger(
      chrome::cros::reporting::proto::DataTransferEventTrigger::FILE_DOWNLOAD);
  expected_event.set_event_result(
      chrome::cros::reporting::proto::EventResult::EVENT_RESULT_WARNED);
  expected_event.set_clicked_through(false);

  chrome::cros::reporting::proto::TriggeredRuleInfo triggered_rule;
  triggered_rule.set_action(
      chrome::cros::reporting::proto::TriggeredRuleInfo::WARN);
  *expected_event.add_triggered_rule_info() = triggered_rule;

  ::chrome::cros::reporting::proto::UrlInfo referrers;
  referrers.set_url(url.spec());
  referrers.set_ip(embedded_test_server()->base_url().host());
  *expected_event.add_referrers() = referrers;

  expected_event.set_profile_identifier(GetProfileIdentifier());
  expected_event.set_profile_user_name(kUserName);

  validator.ExpectSensitiveDataEvent(std::move(expected_event));

  validator_run_loop.Run();

  // For warn verdicts, we allow the write to happen as there is currently no
  // dialog that allows the user to bypass warnings.
  ASSERT_TRUE(content::ExecJs(web_contents, "window.fsaPromise"));

  // Checks that file is written successfully.
  base::ScopedAllowBlockingForTesting allow_blocking;
  EXPECT_TRUE(base::PathExists(GetTestFilePath()));

  std::string file_content;
  EXPECT_TRUE(base::ReadFileToString(GetTestFilePath(), &file_content));
  EXPECT_EQ(file_content, kTestContent);
}

// Fail-open behavior for failed deep scan requests on FSA writes.
IN_PROC_BROWSER_TEST_F(FileSystemAccessDeepScanningBrowserTest,
                       DeepScanFailure) {
  SetUpReporting();

  // Configure scan response with failed status.
  enterprise_connectors::ContentAnalysisResponse response;
  auto* dlp_result = response.add_results();
  dlp_result->set_tag("dlp");
  dlp_result->set_status(
      enterprise_connectors::ContentAnalysisResponse::Result::FAILURE);

  auto* malware_result = response.add_results();
  malware_result->set_tag("malware");
  malware_result->set_status(
      enterprise_connectors::ContentAnalysisResponse::Result::FAILURE);

  ExpectContentAnalysisResumableMetadataResponse({"dlp", "malware"});
  ExpectContentAnalysisResumableContentResponse(response);

  // Setup message queue for JS responses.
  content::WebContents* web_contents =
      browser()->GetTabStripModel()->GetActiveWebContents();
  ASSERT_TRUE(web_contents);
  content::DOMMessageQueue message_queue(web_contents);

  InitiateWriteViaFSA(web_contents, kTestContent);

  WaitForDeepScanRequest();

  // When scan fails, write should still succeed (fail-open behavior).
  ASSERT_TRUE(content::ExecJs(web_contents, "window.fsaPromise"));

  base::ScopedAllowBlockingForTesting allow_blocking;
  EXPECT_TRUE(base::PathExists(GetTestFilePath()));

  std::string file_content;
  EXPECT_TRUE(base::ReadFileToString(GetTestFilePath(), &file_content));
  EXPECT_EQ(file_content, kTestContent);
}

}  // namespace safe_browsing
