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

#include <cstdint>
#include <memory>
#include <optional>
#include <string>
#include <string_view>
#include <tuple>
#include <type_traits>
#include <utility>
#include <vector>

#include "base/barrier_closure.h"
#include "base/check_deref.h"
#include "base/containers/flat_map.h"
#include "base/containers/to_vector.h"
#include "base/files/file_enumerator.h"
#include "base/files/file_path.h"
#include "base/files/file_util.h"
#include "base/functional/bind.h"
#include "base/functional/callback_helpers.h"
#include "base/location.h"
#include "base/logging.h"
#include "base/memory/ref_counted.h"
#include "base/run_loop.h"
#include "base/task/sequenced_task_runner.h"
#include "base/task/task_traits.h"
#include "base/task/thread_pool.h"
#include "base/task/thread_pool/thread_pool_instance.h"
#include "base/test/bind.h"
#include "base/test/mock_callback.h"
#include "base/test/scoped_path_override.h"
#include "base/test/task_environment.h"
#include "base/time/time.h"
#include "base/types/expected.h"
#include "base/uuid.h"
#include "base/values.h"
#include "base/version.h"
#include "build/build_config.h"
#include "components/crx_file/crx_verifier.h"
#include "components/prefs/testing_pref_service.h"
#include "components/services/unzip/in_process_unzipper.h"
#include "components/update_client/activity_data_service.h"
#include "components/update_client/crx_downloader_factory.h"
#include "components/update_client/crx_update_item.h"
#include "components/update_client/network.h"
#include "components/update_client/patcher.h"
#include "components/update_client/persisted_data.h"
#include "components/update_client/ping_manager.h"
#include "components/update_client/protocol_definition.h"
#include "components/update_client/protocol_handler.h"
#include "components/update_client/protocol_parser_json.h"
#include "components/update_client/test_configurator.h"
#include "components/update_client/test_installer.h"
#include "components/update_client/test_utils.h"
#include "components/update_client/unpacker.h"
#include "components/update_client/unzip/unzip_impl.h"
#include "components/update_client/unzipper.h"
#include "components/update_client/update_checker.h"
#include "components/update_client/update_client_errors.h"
#include "components/update_client/update_client_internal.h"
#include "components/update_client/update_engine.h"
#include "components/update_client/utils.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "url/gurl.h"

#if BUILDFLAG(IS_WIN)
#include <windows.h>

#include "base/win/windows_types.h"
#endif  // BUILDFLAG(IS_WIN)

namespace update_client {
namespace {

// Makes stateful mock instances for callbacks in tests. These are callback
// mocks that retain state between successive invocations of the callback.
template <typename Mock>
auto MakeMockCallback() {
  return base::BindRepeating(&Mock::Callback, base::MakeRefCounted<Mock>());
}

// Makes factories for creating update checker instances. `UpdateClient` uses
// the factory to make one update client checker for each update check. This
// factory of factories counts instances of update checkers made. The factory
// scope must enclose the scope of its clients since the `num_calls_` member
// is captured by the lambda.
template <typename MockUpdateChecker>
class MockUpdateCheckerFactory {
 public:
  typename MockUpdateChecker::Factory GetFactory() {
    return base::BindLambdaForTesting(
        [&](scoped_refptr<Configurator>) -> std::unique_ptr<UpdateChecker> {
          if constexpr (std::is_default_constructible_v<MockUpdateChecker>) {
            return std::make_unique<MockUpdateChecker>();
          } else {
            return std::make_unique<MockUpdateChecker>(++num_calls_);
          }
        });
  }

 private:
  int num_calls_ = 0;
};

// Makes a copy of the file specified by |from_path| in a temporary directory
// and returns the path of the copy. Returns true if successful. Cleans up if
// there was an error creating the copy.
bool MakeTestFile(const base::FilePath& from_path, base::FilePath* to_path) {
  base::FilePath temp_dir;
  bool result = base::CreateNewTempDirectory(FILE_PATH_LITERAL("update_client"),
                                             &temp_dir);
  if (!result) {
    return false;
  }

  base::FilePath temp_file;
  result = CreateTemporaryFileInDir(temp_dir, &temp_file);
  if (!result) {
    return false;
  }

  result = CopyFile(from_path, temp_file);
  if (!result) {
    base::DeleteFile(temp_file);
    return false;
  }

  *to_path = temp_file;
  return true;
}

class MockObserver : public UpdateClient::Observer {
 public:
  MockObserver(const MockObserver&) = delete;
  MockObserver operator=(const MockObserver&) = delete;

  explicit MockObserver(scoped_refptr<UpdateClient> update_client)
      : update_client_(update_client) {
    update_client_->AddObserver(this);
  }

  ~MockObserver() override { update_client_->RemoveObserver(this); }

  MOCK_METHOD(void, OnEvent, (const CrxUpdateItem& item), (override));

 private:
  const scoped_refptr<UpdateClient> update_client_;
};

class MockActionHandler : public ActionHandler {
 public:
  MockActionHandler() = default;
  MockActionHandler(const MockActionHandler&) = delete;
  MockActionHandler& operator=(const MockActionHandler&) = delete;

  MOCK_METHOD(void,
              Handle,
              (const base::FilePath&, const std::string&, Callback),
              (override));

 private:
  ~MockActionHandler() override = default;
};

class MockCrxStateChangeReceiver
    : public base::RefCountedThreadSafe<MockCrxStateChangeReceiver> {
 public:
  MOCK_METHOD(void, Receive, (const CrxUpdateItem&));

 private:
  friend class base::RefCountedThreadSafe<MockCrxStateChangeReceiver>;

  ~MockCrxStateChangeReceiver() = default;
};

class MockCrxDownloaderFactory : public CrxDownloaderFactory {
 public:
  explicit MockCrxDownloaderFactory(scoped_refptr<CrxDownloader> crx_downloader)
      : crx_downloader_(crx_downloader) {}

 private:
  ~MockCrxDownloaderFactory() override = default;

  // Overrides for CrxDownloaderFactory.
  scoped_refptr<CrxDownloader> MakeCrxDownloader(
      const std::string& /*prod_id*/,
      bool /*background_download_enabled*/) const override {
    return crx_downloader_;
  }

  scoped_refptr<CrxDownloader> crx_downloader_;
};

// Mocks the completion callback.
auto ExpectError(Error expected_error) {
  return base::BindLambdaForTesting(
      [=](Error actual_error) { EXPECT_EQ(expected_error, actual_error); });
}

auto ExpectErrorThenQuit(base::RunLoop& runloop, Error expected_error) {
  return ExpectError(expected_error)
      .Then(base::BindLambdaForTesting([&runloop]() { runloop.Quit(); }));
}

auto ExpectErrorThenQuit(auto quit, Error expected_error) {
  return ExpectError(expected_error).Then(std::move(quit));
}

struct UpdateCheckerOptionsOneCrxUpdate {
  static constexpr int64_t kAvailableSpace = 3000;
  static constexpr size_t kComponentCount = 1;
  static constexpr std::string_view kJson = R"()]}'
{
  "response": {
    "protocol": "4.0",
    "apps": [
      {
        "appid": "jebgalgnebhfojomionfpkfelancnnkf",
        "status": "ok",
        "updatecheck": {
          "status": "ok",
          "nextversion": "1.0",
          "pipelines": [
            {
              "operations": [
                {
                  "type": "download",
                  "urls": [
                    {
                      "url": "http://localhost/download/jebgalgnebhfojomionfpkfelancnnkf.crx"
                    }
                  ],
                  "out": {
                    "sha256": "7ab32f071cd9b5ef8e0d7913be161f532d98b3e9fa284a7cd8059c3409ce0498"
                  },
                  "size": 1015
                },
                {
                  "type": "crx3",
                  "in": {
                    "sha256": "7ab32f071cd9b5ef8e0d7913be161f532d98b3e9fa284a7cd8059c3409ce0498"
                  }
                }
              ]
            }
          ]
        }
      }
    ]
  }
})";
};

struct UpdateCheckerOptionsTwoCrxUpdate {
  static constexpr int64_t kAvailableSpace = 150000;
  static constexpr size_t kComponentCount = 2;
  static constexpr std::string_view kJson = R"()]}'
{
  "response": {
    "protocol": "4.0",
    "apps": [
      {
        "appid": "jebgalgnebhfojomionfpkfelancnnkf",
        "status": "ok",
        "updatecheck": {
          "status": "ok",
          "nextversion": "1.0",
          "pipelines": [
            {
              "operations": [
                {
                  "type": "download",
                  "urls": [
                    {
                      "url": "http://localhost/download/jebgalgnebhfojomionfpkfelancnnkf.crx"
                    }
                  ],
                  "out": {
                    "sha256": "7ab32f071cd9b5ef8e0d7913be161f532d98b3e9fa284a7cd8059c3409ce0498"
                  },
                  "size": 1015
                },
                {
                  "type": "crx3",
                  "in": {
                    "sha256": "7ab32f071cd9b5ef8e0d7913be161f532d98b3e9fa284a7cd8059c3409ce0498"
                  }
                }
              ]
            }
          ]
        }
      },
      {
        "appid": "ihfokbkgjpifnbbojhneepfflplebdkc",
        "status": "ok",
        "updatecheck": {
          "status": "ok",
          "nextversion": "1.0",
          "pipelines": [
            {
              "operations": [
                {
                  "type": "download",
                  "urls": [
                    {
                      "url": "http://localhost/download/ihfokbkgjpifnbbojhneepfflplebdkc_1.crx"
                    }
                  ],
                  "out": {
                    "sha256": "8f5aa190311237cae00675af87ff457f278cd1a05895470ac5d46647d4a3c2ea"
                  },
                  "size": 54014
                },
                {
                  "type": "crx3",
                  "in": {
                    "sha256": "8f5aa190311237cae00675af87ff457f278cd1a05895470ac5d46647d4a3c2ea"
                  }
                }
              ]
            }
          ]
        }
      }
    ]
  }
})";
};

struct UpdateCheckerOptionsOneCrxInstall : UpdateCheckerOptionsOneCrxUpdate {
  static constexpr std::string_view kJson = R"()]}'
{
 "response": {
   "protocol": "4.0",
   "apps": [
     {
       "appid": "jebgalgnebhfojomionfpkfelancnnkf",
       "status": "ok",
       "updatecheck": {
         "status": "ok",
         "nextversion": "1.0",
         "pipelines": [
           {
             "pipeline_id": "pipe1",
             "operations": [
               {
                 "type": "download",
                 "urls": [
                   {
                     "url": "http://localhost/download/jebgalgnebhfojomionfpkfelancnnkf.crx"
                   }
                 ],
                 "out": {
                   "sha256": "7ab32f071cd9b5ef8e0d7913be161f532d98b3e9fa284a7cd8059c3409ce0498"
                 },
                 "size": 1015
               },
               {
                 "type": "crx3",
                 "arguments": "--arg1 --arg2",
                 "path": "UpdaterSetup.exe",
                 "in": {
                   "sha256": "7ab32f071cd9b5ef8e0d7913be161f532d98b3e9fa284a7cd8059c3409ce0498"
                 }
               }
             ]
           }
         ]
       }
     }
   ]
 }
})";
};

struct UpdateCheckerOptionsTwoCrxUpdateServerIgnoresSecond
    : UpdateCheckerOptionsOneCrxInstall {
  static constexpr size_t kComponentCount = 2;
};

struct UpdateCheckerOptionsTwoCrxUpdateNoUpdate {
  static constexpr int64_t kAvailableSpace = 3000;
  static constexpr size_t kComponentCount = 2;
  static constexpr std::string_view kJson = R"()]}'
{
  "response": {
    "protocol": "4.0",
    "apps": [
      {
        "appid": "jebgalgnebhfojomionfpkfelancnnkf",
        "status": "ok",
        "updatecheck": {
          "status": "ok",
          "nextversion": "1.0",
          "pipelines": [
            {
              "operations": [
                {
                  "type": "download",
                  "urls": [
                    {
                      "url": "http://localhost/download/jebgalgnebhfojomionfpkfelancnnkf.crx"
                    }
                  ],
                  "out": {
                    "sha256": "7ab32f071cd9b5ef8e0d7913be161f532d98b3e9fa284a7cd8059c3409ce0498"
                  },
                  "size": 1015
                },
                {
                  "type": "crx3",
                  "in": {
                    "sha256": "7ab32f071cd9b5ef8e0d7913be161f532d98b3e9fa284a7cd8059c3409ce0498"
                  }
                }
              ]
            }
          ]
        }
      },
      {
        "appid": "abagagagagagagagagagagagagagagag",
        "status": "ok",
        "updatecheck": {
          "status": "noupdate"
        }
      }
    ]
  }
})";
};

struct UpdateCheckerOptionsActionRunNoUpdate {
  static constexpr int64_t kAvailableSpace = 0;
  static constexpr size_t kComponentCount = 1;
  static constexpr std::string_view kJson = R"()]}'
{
  "response": {
    "protocol": "4.0",
    "apps": [
      {
        "appid": "gjpmebpgbhcamgdgjcmnjfhggjpgcimm",
        "status": "ok",
        "updatecheck": {
          "status": "ok",
          "nextversion": "1.0",
          "pipelines": [
            {
              "operations": [
                {
                  "type": "run",
                  "path": "ChromeRecovery.crx3"
                }
              ]
            }
          ]
        }
      }
    ]
  }
})";
};

struct UpdateCheckerOptionsOneCrxInstallDiskFull
    : UpdateCheckerOptionsOneCrxInstall {
  static constexpr int64_t kAvailableSpace = 0;
};

struct UpdateCheckerOptionsUnsupportedOperationType
    : UpdateCheckerOptionsOneCrxUpdate {
  static constexpr std::string_view kJson = R"()]}'
{
  "response": {
    "protocol": "4.0",
    "apps": [
      {
        "appid": "jebgalgnebhfojomionfpkfelancnnkf",
        "status": "ok",
        "updatecheck": {
          "status": "ok",
          "nextversion": "1.0",
          "pipelines": [
            {
              "operations": [
                {
                  "type": "badoperation"
                },
                {
                  "type": "crx3",
                  "arguments": "--arg1 --arg2",
                  "path": "UpdaterSetup.exe",
                  "in": {
                    "sha256": "7ab32f071cd9b5ef8e0d7913be161f532d98b3e9fa284a7cd8059c3409ce0498"
                  }
                }
              ]
            }
          ]
        }
      }
    ]
  }
})";
};

template <typename T>
concept IsUpdateCheckerOptions = requires(T t) {
  T::kAvailableSpace;
  T::kComponentCount;
  T::kJson;
};

template <typename Options>
  requires IsUpdateCheckerOptions<Options>
class MockUpdateCheckerImpl : public UpdateChecker {
 public:
  void CheckForUpdates(
      scoped_refptr<UpdateContext> context,
      const base::flat_map<std::string, std::string>& additional_attributes,
      UpdateCheckCallback update_check_callback) override {
    context->get_available_space = base::BindRepeating(
        [](const base::FilePath&) { return Options::kAvailableSpace; });
    base::expected<ProtocolParser::Results, std::string> results =
        ProtocolParserJSON::ParseJSON(std::string(Options::kJson));
    EXPECT_TRUE(results.has_value()) << results.error();
    EXPECT_FALSE(context->session_id.empty());
    EXPECT_EQ(context->components_to_check_for_updates.size(),
              Options::kComponentCount);
    base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
        FROM_HERE, base::BindOnce(std::move(update_check_callback),
                                  results.value(), ErrorCategory::kNone, 0, 0));
  }
};

class MockUpdateCheckerAlwaysFails : public UpdateChecker {
 public:
  MockUpdateCheckerAlwaysFails() = default;

  void CheckForUpdates(
      scoped_refptr<UpdateContext> context,
      const base::flat_map<std::string, std::string>& additional_attributes,
      UpdateCheckCallback update_check_callback) override {
    ADD_FAILURE() << "Check for update failed successfully.";
  }
};

using ::testing::_;
using ::testing::AnyNumber;
using ::testing::AtLeast;
using ::testing::DoAll;
using ::testing::InSequence;
using ::testing::Mock;
using ::testing::Return;
using ::testing::Truly;
using ::testing::Unused;

}  // namespace

class MockPingManagerImpl : public PingManager {
 public:
  struct PingData {
    std::string id;
    base::Version previous_version;
    base::Version next_version;
    ErrorCategory error_category = ErrorCategory::kNone;
    int error_code = 0;
    int extra_code1 = 0;
    int event_type;
    int event_result;
    std::string pipeline_id;
  };

  explicit MockPingManagerImpl(scoped_refptr<Configurator> config);
  MockPingManagerImpl(const MockPingManagerImpl&) = delete;
  MockPingManagerImpl& operator=(const MockPingManagerImpl&) = delete;

  void SendPing(const std::string& session_id,
                const CrxComponent& component,
                std::vector<base::DictValue> events,
                base::OnceClosure callback) override;

  const std::vector<PingData>& ping_data() const;
  const std::vector<PingData>& terminal_ping_data() const;

  const std::vector<base::DictValue>& events() const;

 protected:
  ~MockPingManagerImpl() override;

 private:
  std::vector<PingData> ping_data_;
  std::vector<PingData> terminal_ping_data_;
  std::vector<base::DictValue> events_;
};

MockPingManagerImpl::MockPingManagerImpl(scoped_refptr<Configurator> config)
    : PingManager(config) {}

MockPingManagerImpl::~MockPingManagerImpl() = default;

void MockPingManagerImpl::SendPing(const std::string& session_id,
                                   const CrxComponent& component,
                                   std::vector<base::DictValue> events,
                                   base::OnceClosure callback) {
  auto event_to_ping_data = [&](const base::DictValue& event) {
    PingData ping_data;
    ping_data.id = component.app_id;
    int event_type = event.FindInt("eventtype").value_or(0);
    const std::string* previous_version = event.FindString("previousversion");
    if (previous_version) {
      ping_data.previous_version = base::Version(*previous_version);
    }
    ping_data.event_result = event.FindInt("eventresult").value_or(0);
    const std::string* next_version = event.FindString("nextversion");
    if (next_version) {
      ping_data.next_version = base::Version(*next_version);
    }
    std::optional<int> error_category = event.FindInt("errorcat");
    if (error_category) {
      ping_data.error_category = static_cast<ErrorCategory>(*error_category);
    }
    std::optional<int> error_code = event.FindInt("errorcode");
    if (error_code) {
      ping_data.error_code = *error_code;
    }
    std::optional<int> extra_code1 = event.FindInt("extracode1");
    if (extra_code1) {
      ping_data.extra_code1 = *extra_code1;
    }
    const std::string* pipeline_id = event.FindString("pipeline_id");
    if (pipeline_id) {
      ping_data.pipeline_id = *pipeline_id;
    }
    if (event_type != 0) {
      ping_data.event_type = event_type;
    }
    return ping_data;
  };

  ping_data_ = base::ToVector(events, event_to_ping_data);
  for (const auto& ping_data : ping_data_) {
    if (ping_data.event_type == protocol_request::kEventInstall ||
        ping_data.event_type == protocol_request::kEventUpdate ||
        ping_data.event_type == protocol_request::kEventUninstall) {
      terminal_ping_data_.push_back(ping_data);
    }
  }
  events_ = std::move(events);

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

const std::vector<MockPingManagerImpl::PingData>&
MockPingManagerImpl::ping_data() const {
  return ping_data_;
}

const std::vector<MockPingManagerImpl::PingData>&
MockPingManagerImpl::terminal_ping_data() const {
  return terminal_ping_data_;
}

const std::vector<base::DictValue>& MockPingManagerImpl::events() const {
  return events_;
}

class UpdateClientTest : public testing::Test {
 private:
  // Must be initialized before `runloop_`.
  base::test::TaskEnvironment task_environment_;

 protected:
  UpdateClientTest() {
    RegisterPersistedDataPrefs(pref_->registry());
    config_ = base::MakeRefCounted<TestConfigurator>(pref_.get());
  }

  scoped_refptr<update_client::TestConfigurator> config() { return config_; }

  // Injects the CrxDownloaderFactory in the test fixture.
  template <typename MockCrxDownloaderT>
  void SetMockCrxDownloader() {
    config()->SetCrxDownloaderFactory(
        base::MakeRefCounted<MockCrxDownloaderFactory>(
            base::MakeRefCounted<MockCrxDownloaderT>()));
  }

  base::RunLoop runloop_;

 private:
  std::unique_ptr<TestingPrefServiceSimple> pref_ =
      std::make_unique<TestingPrefServiceSimple>();
  scoped_refptr<update_client::TestConfigurator> config_;
};

// Tests the scenario where one update check is done for one CRX. The CRX
// has no update.
TEST_F(UpdateClientTest, OneCrxNoUpdate) {
  class DataCallbackMock {
   public:
    static void Callback(
        const std::vector<std::string>& ids,
        base::OnceCallback<
            void(const std::vector<std::optional<CrxComponent>>&)> callback) {
      CrxComponent crx;
      crx.app_id = "jebgalgnebhfojomionfpkfelancnnkf";
      crx.name = "test_jebg";
      crx.pk_hash = base::ToVector(jebg_hash);
      crx.version = base::Version("0.9");
      crx.installer = base::MakeRefCounted<TestInstaller>();
      crx.crx_format_requirement = crx_file::VerifierFormat::CRX3;
      std::vector<std::optional<CrxComponent>> component = {crx};
      std::move(callback).Run({component});
    }
  };

  class MockUpdateChecker : public UpdateChecker {
   public:
    MockUpdateChecker() = default;

    void CheckForUpdates(
        scoped_refptr<UpdateContext> context,
        const base::flat_map<std::string, std::string>& additional_attributes,
        UpdateCheckCallback update_check_callback) override {
      EXPECT_FALSE(context->session_id.empty());
      EXPECT_EQ(1u, context->components_to_check_for_updates.size());
      const std::string id = "jebgalgnebhfojomionfpkfelancnnkf";
      EXPECT_EQ(id, context->components_to_check_for_updates.front());
      EXPECT_EQ(1u, context->components.count(id));

      auto& component = context->components.at(id);

      EXPECT_TRUE(component->is_foreground());

      ProtocolParser::App result;
      result.app_id = id;
      result.status = "noupdate";

      ProtocolParser::Results results;
      results.apps.push_back(result);

      base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
          FROM_HERE, base::BindOnce(std::move(update_check_callback), results,
                                    ErrorCategory::kNone, 0, 0));
    }
  };
  MockUpdateCheckerFactory<MockUpdateChecker> mock_update_checker_factory;

  class MockCrxDownloader : public CrxDownloader {
   public:
    MockCrxDownloader() = default;

   private:
    ~MockCrxDownloader() override = default;

    base::OnceClosure DoStartDownload(const GURL& url) override {
      ADD_FAILURE();
      return base::DoNothing();
    }
  };

  class MockPingManager : public MockPingManagerImpl {
   public:
    explicit MockPingManager(scoped_refptr<Configurator> config)
        : MockPingManagerImpl(config) {}

   protected:
    ~MockPingManager() override { EXPECT_TRUE(ping_data().empty()); }
  };

  SetMockCrxDownloader<MockCrxDownloader>();
  scoped_refptr<UpdateClient> update_client =
      base::MakeRefCounted<UpdateClientImpl>(
          config(), base::MakeRefCounted<MockPingManager>(config()),
          mock_update_checker_factory.GetFactory());

  MockObserver observer(update_client);
  {
    InSequence seq;
    EXPECT_CALL(observer, OnEvent(Truly([](const CrxUpdateItem& item) {
                  return item.state == ComponentState::kChecking;
                })));
    EXPECT_CALL(observer, OnEvent(Truly([](const CrxUpdateItem& item) {
                  return item.state == ComponentState::kUpToDate;
                })));
  }

  std::vector<CrxUpdateItem> items;
  auto receiver = base::MakeRefCounted<MockCrxStateChangeReceiver>();
  EXPECT_CALL(*receiver, Receive(_))
      .WillRepeatedly(
          [&items](const CrxUpdateItem& item) { items.push_back(item); });

  update_client->Update(
      {"jebgalgnebhfojomionfpkfelancnnkf"},
      base::BindOnce(&DataCallbackMock::Callback),
      base::BindRepeating(&MockCrxStateChangeReceiver::Receive, receiver), true,
      ExpectErrorThenQuit(runloop_, Error::NONE));
  runloop_.Run();

  EXPECT_EQ(2u, items.size());
  EXPECT_EQ(ComponentState::kChecking, items[0].state);
  EXPECT_EQ("jebgalgnebhfojomionfpkfelancnnkf", items[0].id);
  EXPECT_EQ(ComponentState::kUpToDate, items[1].state);
  EXPECT_EQ("jebgalgnebhfojomionfpkfelancnnkf", items[1].id);
}

// Tests the scenario where two CRXs are checked for updates. On CRX has
// an update, the other CRX does not.
TEST_F(UpdateClientTest, TwoCrxUpdateNoUpdate) {
  class DataCallbackMock {
   public:
    static void Callback(
        const std::vector<std::string>& ids,
        base::OnceCallback<
            void(const std::vector<std::optional<CrxComponent>>&)> callback) {
      CrxComponent crx1;
      crx1.app_id = "jebgalgnebhfojomionfpkfelancnnkf";
      crx1.name = "test_jebg";
      crx1.pk_hash = base::ToVector(jebg_hash);
      crx1.version = base::Version("0.9");
      crx1.installer = base::MakeRefCounted<TestInstaller>();
      crx1.crx_format_requirement = crx_file::VerifierFormat::CRX3;

      CrxComponent crx2;
      crx2.app_id = "abagagagagagagagagagagagagagagag";
      crx2.name = "test_abag";
      crx2.pk_hash = base::ToVector(abag_hash);
      crx2.version = base::Version("2.2");
      crx2.installer = base::MakeRefCounted<TestInstaller>();
      crx2.crx_format_requirement = crx_file::VerifierFormat::CRX3;

      std::move(callback).Run({crx1, crx2});
    }
  };

  MockUpdateCheckerFactory<
      MockUpdateCheckerImpl<UpdateCheckerOptionsTwoCrxUpdateNoUpdate>>
      mock_update_checker_factory;

  class MockCrxDownloader : public CrxDownloader {
   public:
    MockCrxDownloader() = default;

   private:
    ~MockCrxDownloader() override = default;

    base::OnceClosure DoStartDownload(const GURL& url) override {
      DownloadMetrics download_metrics;
      download_metrics.url = url;
      download_metrics.downloader = DownloadMetrics::kNone;
      download_metrics.error = 0;
      download_metrics.downloaded_bytes = 1015;
      download_metrics.total_bytes = 1015;
      download_metrics.download_time_ms = 1000;

      base::FilePath path;
      EXPECT_TRUE(MakeTestFile(
          GetTestFilePath("jebgalgnebhfojomionfpkfelancnnkf.crx"), &path));

      Result result;
      result.error = 0;
      result.response = path;

      base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
          FROM_HERE, base::BindOnce(&MockCrxDownloader::OnDownloadProgress,
                                    base::Unretained(this),
                                    download_metrics.downloaded_bytes / 2,
                                    download_metrics.total_bytes));

      base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
          FROM_HERE, base::BindOnce(&MockCrxDownloader::OnDownloadProgress,
                                    base::Unretained(this),
                                    download_metrics.downloaded_bytes,
                                    download_metrics.total_bytes));

      base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
          FROM_HERE, base::BindOnce(&MockCrxDownloader::OnDownloadComplete,
                                    base::Unretained(this), true, result,
                                    download_metrics));
      return base::DoNothing();
    }
  };

  class MockPingManager : public MockPingManagerImpl {
   public:
    explicit MockPingManager(scoped_refptr<Configurator> config)
        : MockPingManagerImpl(config) {}

   protected:
    ~MockPingManager() override {
      const auto ping_data = MockPingManagerImpl::terminal_ping_data();
      EXPECT_EQ(1u, ping_data.size());
      EXPECT_EQ("jebgalgnebhfojomionfpkfelancnnkf", ping_data[0].id);
      EXPECT_EQ(base::Version("0.9"), ping_data[0].previous_version);
      EXPECT_EQ(base::Version("1.0"), ping_data[0].next_version);
      EXPECT_EQ(0, static_cast<int>(ping_data[0].error_category));
      EXPECT_EQ(0, ping_data[0].error_code);
    }
  };

  SetMockCrxDownloader<MockCrxDownloader>();
  scoped_refptr<UpdateClient> update_client =
      base::MakeRefCounted<UpdateClientImpl>(
          config(), base::MakeRefCounted<MockPingManager>(config()),
          mock_update_checker_factory.GetFactory());

  MockObserver observer(update_client);
  {
    InSequence seq;
    EXPECT_CALL(observer, OnEvent(Truly([](const CrxUpdateItem& item) {
                  return item.state == ComponentState::kChecking &&
                         item.id == "jebgalgnebhfojomionfpkfelancnnkf";
                })));
    EXPECT_CALL(observer, OnEvent(Truly([](const CrxUpdateItem& item) {
                  return item.state == ComponentState::kCanUpdate &&
                         item.id == "jebgalgnebhfojomionfpkfelancnnkf";
                })));
    EXPECT_CALL(observer, OnEvent(Truly([](const CrxUpdateItem& item) {
                  return item.state == ComponentState::kDownloading &&
                         item.id == "jebgalgnebhfojomionfpkfelancnnkf";
                })))
        .Times(AtLeast(1));
    EXPECT_CALL(observer, OnEvent(Truly([](const CrxUpdateItem& item) {
                  return item.state == ComponentState::kUpdating &&
                         item.id == "jebgalgnebhfojomionfpkfelancnnkf";
                })))
        .Times(AtLeast(1));
    EXPECT_CALL(observer, OnEvent(Truly([](const CrxUpdateItem& item) {
                  return item.state == ComponentState::kUpdated &&
                         item.id == "jebgalgnebhfojomionfpkfelancnnkf";
                })));
  }
  {
    InSequence seq;
    EXPECT_CALL(observer, OnEvent(Truly([](const CrxUpdateItem& item) {
                  return item.state == ComponentState::kChecking &&
                         item.id == "abagagagagagagagagagagagagagagag";
                })));
    EXPECT_CALL(observer, OnEvent(Truly([](const CrxUpdateItem& item) {
                  return item.state == ComponentState::kUpToDate &&
                         item.id == "abagagagagagagagagagagagagagagag";
                })));
  }

  std::vector<CrxUpdateItem> items;
  auto receiver = base::MakeRefCounted<MockCrxStateChangeReceiver>();
  EXPECT_CALL(*receiver, Receive(_))
      .WillRepeatedly(
          [&items](const CrxUpdateItem& item) { items.push_back(item); });

  const std::vector<std::string> ids = {"jebgalgnebhfojomionfpkfelancnnkf",
                                        "abagagagagagagagagagagagagagagag"};
  update_client->Update(
      ids, base::BindOnce(&DataCallbackMock::Callback),
      base::BindRepeating(&MockCrxStateChangeReceiver::Receive, receiver),
      false, ExpectErrorThenQuit(runloop_, Error::NONE));
  runloop_.Run();

  EXPECT_EQ(9u, items.size());
  EXPECT_EQ(ComponentState::kChecking, items[0].state);
  EXPECT_EQ("jebgalgnebhfojomionfpkfelancnnkf", items[0].id);
  EXPECT_EQ(ComponentState::kChecking, items[1].state);
  EXPECT_EQ("abagagagagagagagagagagagagagagag", items[1].id);
  EXPECT_EQ(ComponentState::kCanUpdate, items[2].state);
  EXPECT_EQ("jebgalgnebhfojomionfpkfelancnnkf", items[2].id);
  EXPECT_EQ(ComponentState::kDownloading, items[3].state);
  EXPECT_EQ("jebgalgnebhfojomionfpkfelancnnkf", items[3].id);
  EXPECT_EQ(ComponentState::kDownloading, items[4].state);
  EXPECT_EQ("jebgalgnebhfojomionfpkfelancnnkf", items[4].id);
  EXPECT_EQ(ComponentState::kDownloading, items[5].state);
  EXPECT_EQ("jebgalgnebhfojomionfpkfelancnnkf", items[5].id);
  EXPECT_EQ(ComponentState::kUpdating, items[6].state);
  EXPECT_EQ("jebgalgnebhfojomionfpkfelancnnkf", items[6].id);
  EXPECT_EQ(ComponentState::kUpdated, items[7].state);
  EXPECT_EQ("jebgalgnebhfojomionfpkfelancnnkf", items[7].id);
  EXPECT_EQ(ComponentState::kUpToDate, items[8].state);
  EXPECT_EQ("abagagagagagagagagagagagagagagag", items[8].id);

  std::vector<std::tuple<int64_t, int64_t>> progress_bytes = {
      {-1, -1},     {-1, -1},     {-1, -1},     {-1, -1}, {507, 1015},
      {1015, 1015}, {1015, 1015}, {1015, 1015}, {-1, -1}};
  EXPECT_EQ(items.size(), progress_bytes.size());
  for (size_t i{0}; i != items.size(); ++i) {
    EXPECT_EQ(items[i].downloaded_bytes, std::get<0>(progress_bytes[i]));
    EXPECT_EQ(items[i].total_bytes, std::get<1>(progress_bytes[i]));
  }
}

// Tests the scenario where two CRXs are checked for updates. One CRX has
// an update but the server ignores the second CRX and returns no response for
// it. The second component gets an |UPDATE_RESPONSE_NOT_FOUND| error and
// transitions to the error state.
TEST_F(UpdateClientTest, TwoCrxUpdateFirstServerIgnoresSecond) {
  class DataCallbackMock {
   public:
    static void Callback(
        const std::vector<std::string>& ids,
        base::OnceCallback<
            void(const std::vector<std::optional<CrxComponent>>&)> callback) {
      CrxComponent crx1;
      crx1.app_id = "jebgalgnebhfojomionfpkfelancnnkf";
      crx1.name = "test_jebg";
      crx1.pk_hash = base::ToVector(jebg_hash);
      crx1.version = base::Version("0.9");
      crx1.installer = base::MakeRefCounted<TestInstaller>();
      crx1.crx_format_requirement = crx_file::VerifierFormat::CRX3;

      CrxComponent crx2;
      crx2.name = "test_abag";
      crx2.pk_hash = base::ToVector(abag_hash);
      crx2.version = base::Version("2.2");
      crx2.installer = base::MakeRefCounted<TestInstaller>();
      crx2.crx_format_requirement = crx_file::VerifierFormat::CRX3;

      std::move(callback).Run({crx1, crx2});
    }
  };

  // Note: even though 2 appid's are requested, we are intentionally only
  // sending the first.
  MockUpdateCheckerFactory<MockUpdateCheckerImpl<
      UpdateCheckerOptionsTwoCrxUpdateServerIgnoresSecond>>
      mock_update_checker_factory;

  class MockCrxDownloader : public CrxDownloader {
   public:
    MockCrxDownloader() = default;

   private:
    ~MockCrxDownloader() override = default;

    base::OnceClosure DoStartDownload(const GURL& url) override {
      DownloadMetrics download_metrics;
      download_metrics.url = url;
      download_metrics.downloader = DownloadMetrics::kNone;
      download_metrics.error = 0;
      download_metrics.downloaded_bytes = 1015;
      download_metrics.total_bytes = 1015;
      download_metrics.download_time_ms = 1000;

      base::FilePath path;
      EXPECT_TRUE(MakeTestFile(
          GetTestFilePath("jebgalgnebhfojomionfpkfelancnnkf.crx"), &path));

      Result result;
      result.error = 0;
      result.response = path;

      base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
          FROM_HERE, base::BindOnce(&MockCrxDownloader::OnDownloadProgress,
                                    base::Unretained(this),
                                    download_metrics.downloaded_bytes,
                                    download_metrics.total_bytes));

      base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
          FROM_HERE, base::BindOnce(&MockCrxDownloader::OnDownloadComplete,
                                    base::Unretained(this), true, result,
                                    download_metrics));
      return base::DoNothing();
    }
  };

  class MockPingManager : public MockPingManagerImpl {
   public:
    explicit MockPingManager(scoped_refptr<Configurator> config)
        : MockPingManagerImpl(config) {}

   protected:
    ~MockPingManager() override {
      const auto ping_data = MockPingManagerImpl::terminal_ping_data();
      EXPECT_EQ(1u, ping_data.size());
      EXPECT_EQ("jebgalgnebhfojomionfpkfelancnnkf", ping_data[0].id);
      EXPECT_EQ(base::Version("0.9"), ping_data[0].previous_version);
      EXPECT_EQ(base::Version("1.0"), ping_data[0].next_version);
      EXPECT_EQ(0, static_cast<int>(ping_data[0].error_category));
      EXPECT_EQ(0, ping_data[0].error_code);
    }
  };

  SetMockCrxDownloader<MockCrxDownloader>();
  scoped_refptr<UpdateClient> update_client =
      base::MakeRefCounted<UpdateClientImpl>(
          config(), base::MakeRefCounted<MockPingManager>(config()),
          mock_update_checker_factory.GetFactory());

  MockObserver observer(update_client);
  {
    InSequence seq;
    EXPECT_CALL(observer, OnEvent(Truly([](const CrxUpdateItem& item) {
                  return item.state == ComponentState::kChecking &&
                         item.id == "jebgalgnebhfojomionfpkfelancnnkf";
                })));
    EXPECT_CALL(observer, OnEvent(Truly([](const CrxUpdateItem& item) {
                  return item.state == ComponentState::kCanUpdate &&
                         item.id == "jebgalgnebhfojomionfpkfelancnnkf";
                })));
    EXPECT_CALL(observer, OnEvent(Truly([](const CrxUpdateItem& item) {
                  return item.state == ComponentState::kDownloading &&
                         item.id == "jebgalgnebhfojomionfpkfelancnnkf";
                })))
        .Times(AtLeast(1));
    EXPECT_CALL(observer, OnEvent(Truly([](const CrxUpdateItem& item) {
                  return item.state == ComponentState::kUpdating &&
                         item.id == "jebgalgnebhfojomionfpkfelancnnkf";
                })))
        .Times(AtLeast(1));
    EXPECT_CALL(observer, OnEvent(Truly([](const CrxUpdateItem& item) {
                  return item.state == ComponentState::kUpdated &&
                         item.id == "jebgalgnebhfojomionfpkfelancnnkf";
                })));
  }
  {
    InSequence seq;
    EXPECT_CALL(observer, OnEvent(Truly([](const CrxUpdateItem& item) {
                  return item.state == ComponentState::kChecking &&
                         item.id == "abagagagagagagagagagagagagagagag";
                })));
    EXPECT_CALL(observer, OnEvent(Truly([](const CrxUpdateItem& item) {
                  return item.state == ComponentState::kUpdateError &&
                         item.id == "abagagagagagagagagagagagagagagag";
                })))
        .WillOnce([](const CrxUpdateItem& item) {
          EXPECT_EQ(ComponentState::kUpdateError, item.state);
          EXPECT_EQ(5, static_cast<int>(item.error_category));
          EXPECT_EQ(-10004, item.error_code);
          EXPECT_EQ(0, item.extra_code1);
        });
  }

  std::vector<CrxUpdateItem> items;
  auto receiver = base::MakeRefCounted<MockCrxStateChangeReceiver>();
  EXPECT_CALL(*receiver, Receive(_))
      .WillRepeatedly(
          [&items](const CrxUpdateItem& item) { items.push_back(item); });

  const std::vector<std::string> ids = {"jebgalgnebhfojomionfpkfelancnnkf",
                                        "abagagagagagagagagagagagagagagag"};
  update_client->Update(
      ids, base::BindOnce(&DataCallbackMock::Callback),
      base::BindRepeating(&MockCrxStateChangeReceiver::Receive, receiver),
      false, ExpectErrorThenQuit(runloop_, Error::NONE));
  runloop_.Run();

  EXPECT_EQ(8u, items.size());
  EXPECT_EQ(ComponentState::kChecking, items[0].state);
  EXPECT_EQ("jebgalgnebhfojomionfpkfelancnnkf", items[0].id);
  EXPECT_EQ(ComponentState::kChecking, items[1].state);
  EXPECT_EQ("abagagagagagagagagagagagagagagag", items[1].id);
  EXPECT_EQ(ComponentState::kCanUpdate, items[2].state);
  EXPECT_EQ("jebgalgnebhfojomionfpkfelancnnkf", items[2].id);
  EXPECT_EQ(ComponentState::kDownloading, items[3].state);
  EXPECT_EQ("jebgalgnebhfojomionfpkfelancnnkf", items[3].id);
  EXPECT_EQ(ComponentState::kDownloading, items[4].state);
  EXPECT_EQ("jebgalgnebhfojomionfpkfelancnnkf", items[4].id);
  EXPECT_EQ(ComponentState::kUpdating, items[5].state);
  EXPECT_EQ("jebgalgnebhfojomionfpkfelancnnkf", items[5].id);
  EXPECT_EQ(ComponentState::kUpdated, items[6].state);
  EXPECT_EQ("jebgalgnebhfojomionfpkfelancnnkf", items[6].id);
  EXPECT_EQ(ComponentState::kUpdateError, items[7].state);
  EXPECT_EQ("abagagagagagagagagagagagagagagag", items[7].id);
}

// Tests the update check for two CRXs scenario when the second CRX does not
// provide a CrxComponent instance. In this case, the update is handled as
// if only one component were provided as an argument to the |Update| call
// with the exception that the second component still fires an event such as
// |COMPONENT_UPDATE_ERROR|.
TEST_F(UpdateClientTest, TwoCrxUpdateNoCrxComponentData) {
  class DataCallbackMock {
   public:
    static void Callback(
        const std::vector<std::string>& ids,
        base::OnceCallback<
            void(const std::vector<std::optional<CrxComponent>>&)> callback) {
      CrxComponent crx;
      crx.app_id = "jebgalgnebhfojomionfpkfelancnnkf";
      crx.name = "test_jebg";
      crx.pk_hash = base::ToVector(jebg_hash);
      crx.version = base::Version("0.9");
      crx.installer = base::MakeRefCounted<TestInstaller>();
      crx.crx_format_requirement = crx_file::VerifierFormat::CRX3;
      std::move(callback).Run({crx, std::nullopt});
    }
  };

  // Note: even though 2 appid's are requested, since "ihfo..." has no component
  // data, the response only contains the first appid's update response.
  MockUpdateCheckerFactory<
      MockUpdateCheckerImpl<UpdateCheckerOptionsOneCrxUpdate>>
      mock_update_checker_factory;

  class MockCrxDownloader : public CrxDownloader {
   public:
    MockCrxDownloader() = default;

   private:
    ~MockCrxDownloader() override = default;

    base::OnceClosure DoStartDownload(const GURL& url) override {
      DownloadMetrics download_metrics;
      base::FilePath path;
      Result result;
      if (url.GetPath() == "/download/jebgalgnebhfojomionfpkfelancnnkf.crx") {
        download_metrics.url = url;
        download_metrics.downloader = DownloadMetrics::kNone;
        download_metrics.error = 0;
        download_metrics.downloaded_bytes = 1015;
        download_metrics.total_bytes = 1015;
        download_metrics.download_time_ms = 1000;

        EXPECT_TRUE(MakeTestFile(
            GetTestFilePath("jebgalgnebhfojomionfpkfelancnnkf.crx"), &path));

        result.error = 0;
        result.response = path;
      } else {
        ADD_FAILURE();
      }

      base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
          FROM_HERE, base::BindOnce(&MockCrxDownloader::OnDownloadProgress,
                                    base::Unretained(this),
                                    download_metrics.downloaded_bytes,
                                    download_metrics.total_bytes));

      base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
          FROM_HERE, base::BindOnce(&MockCrxDownloader::OnDownloadComplete,
                                    base::Unretained(this), true, result,
                                    download_metrics));
      return base::DoNothing();
    }
  };

  class MockPingManager : public MockPingManagerImpl {
   public:
    explicit MockPingManager(scoped_refptr<Configurator> config)
        : MockPingManagerImpl(config) {}

   protected:
    ~MockPingManager() override {
      const auto ping_data = MockPingManagerImpl::terminal_ping_data();
      EXPECT_EQ(1u, ping_data.size());
      EXPECT_EQ("jebgalgnebhfojomionfpkfelancnnkf", ping_data[0].id);
      EXPECT_EQ(base::Version("0.9"), ping_data[0].previous_version);
      EXPECT_EQ(base::Version("1.0"), ping_data[0].next_version);
      EXPECT_EQ(0, static_cast<int>(ping_data[0].error_category));
      EXPECT_EQ(0, ping_data[0].error_code);
    }
  };

  SetMockCrxDownloader<MockCrxDownloader>();
  scoped_refptr<UpdateClient> update_client =
      base::MakeRefCounted<UpdateClientImpl>(
          config(), base::MakeRefCounted<MockPingManager>(config()),
          mock_update_checker_factory.GetFactory());

  MockObserver observer(update_client);
  {
    InSequence seq;
    EXPECT_CALL(observer, OnEvent(Truly([](const CrxUpdateItem& item) {
                  return item.id == "jebgalgnebhfojomionfpkfelancnnkf" &&
                         item.state == ComponentState::kChecking;
                })));
    EXPECT_CALL(observer, OnEvent(Truly([](const CrxUpdateItem& item) {
                  return item.id == "jebgalgnebhfojomionfpkfelancnnkf" &&
                         item.state == ComponentState::kCanUpdate;
                })));
    EXPECT_CALL(observer, OnEvent(Truly([](const CrxUpdateItem& item) {
                  return item.id == "jebgalgnebhfojomionfpkfelancnnkf" &&
                         item.state == ComponentState::kDownloading;
                })))
        .Times(AtLeast(1));
    EXPECT_CALL(observer, OnEvent(Truly([](const CrxUpdateItem& item) {
                  return item.id == "jebgalgnebhfojomionfpkfelancnnkf" &&
                         item.state == ComponentState::kUpdating;
                })));
    EXPECT_CALL(observer, OnEvent(Truly([](const CrxUpdateItem& item) {
                  return item.id == "jebgalgnebhfojomionfpkfelancnnkf" &&
                         item.state == ComponentState::kUpdated;
                })));
  }
  {
    InSequence seq;
    EXPECT_CALL(observer, OnEvent(Truly([](const CrxUpdateItem& item) {
                  return item.id == "ihfokbkgjpifnbbojhneepfflplebdkc" &&
                         item.state == ComponentState::kUpdateError;
                })));
  }

  std::vector<CrxUpdateItem> items;
  auto receiver = base::MakeRefCounted<MockCrxStateChangeReceiver>();
  EXPECT_CALL(*receiver, Receive(_))
      .WillRepeatedly(
          [&items](const CrxUpdateItem& item) { items.push_back(item); });

  const std::vector<std::string> ids = {"jebgalgnebhfojomionfpkfelancnnkf",
                                        "ihfokbkgjpifnbbojhneepfflplebdkc"};
  update_client->Update(
      ids, base::BindOnce(&DataCallbackMock::Callback),
      base::BindRepeating(&MockCrxStateChangeReceiver::Receive, receiver),
      false, ExpectErrorThenQuit(runloop_, Error::NONE));
  runloop_.Run();

  EXPECT_EQ(7u, items.size());
  EXPECT_EQ(ComponentState::kChecking, items[0].state);
  EXPECT_EQ("jebgalgnebhfojomionfpkfelancnnkf", items[0].id);
  EXPECT_EQ(ComponentState::kUpdateError, items[1].state);
  EXPECT_EQ("ihfokbkgjpifnbbojhneepfflplebdkc", items[1].id);
  EXPECT_EQ(ComponentState::kCanUpdate, items[2].state);
  EXPECT_EQ("jebgalgnebhfojomionfpkfelancnnkf", items[2].id);
  EXPECT_EQ(ComponentState::kDownloading, items[3].state);
  EXPECT_EQ("jebgalgnebhfojomionfpkfelancnnkf", items[3].id);
  EXPECT_EQ(ComponentState::kDownloading, items[4].state);
  EXPECT_EQ("jebgalgnebhfojomionfpkfelancnnkf", items[4].id);
  EXPECT_EQ(ComponentState::kUpdating, items[5].state);
  EXPECT_EQ("jebgalgnebhfojomionfpkfelancnnkf", items[5].id);
  EXPECT_EQ(ComponentState::kUpdated, items[6].state);
  EXPECT_EQ("jebgalgnebhfojomionfpkfelancnnkf", items[6].id);
}

// Tests the update check for two CRXs scenario when no CrxComponent data is
// provided for either component. In this case, no update check occurs, and
// |COMPONENT_UPDATE_ERROR| event fires for both components.
TEST_F(UpdateClientTest, TwoCrxUpdateNoCrxComponentDataAtAll) {
  class DataCallbackMock {
   public:
    static void Callback(
        const std::vector<std::string>& ids,
        base::OnceCallback<
            void(const std::vector<std::optional<CrxComponent>>&)> callback) {
      std::move(callback).Run({std::nullopt, std::nullopt});
    }
  };

  MockUpdateCheckerFactory<MockUpdateCheckerAlwaysFails>
      mock_update_checker_factory;

  class MockCrxDownloader : public CrxDownloader {
   public:
    MockCrxDownloader() = default;

   private:
    ~MockCrxDownloader() override = default;

    base::OnceClosure DoStartDownload(const GURL& url) override {
      ADD_FAILURE();
      return base::DoNothing();
    }
  };

  class MockPingManager : public MockPingManagerImpl {
   public:
    explicit MockPingManager(scoped_refptr<Configurator> config)
        : MockPingManagerImpl(config) {}

   protected:
    ~MockPingManager() override {
      EXPECT_EQ(0u, MockPingManagerImpl::ping_data().size());
    }
  };

  SetMockCrxDownloader<MockCrxDownloader>();
  scoped_refptr<UpdateClient> update_client =
      base::MakeRefCounted<UpdateClientImpl>(
          config(), base::MakeRefCounted<MockPingManager>(config()),
          mock_update_checker_factory.GetFactory());

  MockObserver observer(update_client);
  {
    InSequence seq;
    EXPECT_CALL(observer, OnEvent(Truly([](const CrxUpdateItem& item) {
                  return item.id == "jebgalgnebhfojomionfpkfelancnnkf" &&
                         item.state == ComponentState::kUpdateError;
                })));
    EXPECT_CALL(observer, OnEvent(Truly([](const CrxUpdateItem& item) {
                  return item.id == "ihfokbkgjpifnbbojhneepfflplebdkc" &&
                         item.state == ComponentState::kUpdateError;
                })));
  }

  std::vector<CrxUpdateItem> items;
  auto receiver = base::MakeRefCounted<MockCrxStateChangeReceiver>();
  EXPECT_CALL(*receiver, Receive(_))
      .WillRepeatedly(
          [&items](const CrxUpdateItem& item) { items.push_back(item); });

  const std::vector<std::string> ids = {"jebgalgnebhfojomionfpkfelancnnkf",
                                        "ihfokbkgjpifnbbojhneepfflplebdkc"};
  update_client->Update(
      ids, base::BindOnce(&DataCallbackMock::Callback),
      base::BindRepeating(&MockCrxStateChangeReceiver::Receive, receiver),
      false, ExpectErrorThenQuit(runloop_, Error::NONE));
  runloop_.Run();

  EXPECT_EQ(2u, items.size());
  EXPECT_EQ(ComponentState::kUpdateError, items[0].state);
  EXPECT_EQ("jebgalgnebhfojomionfpkfelancnnkf", items[0].id);
  EXPECT_EQ(ComponentState::kUpdateError, items[1].state);
  EXPECT_EQ("ihfokbkgjpifnbbojhneepfflplebdkc", items[1].id);
}

// Tests the scenario where there is a download timeout for the first
// CRX. The update for the first CRX fails. The update client waits before
// attempting the update for the second CRX. This update succeeds.
TEST_F(UpdateClientTest, TwoCrxUpdateDownloadTimeout) {
  class DataCallbackMock {
   public:
    static void Callback(
        const std::vector<std::string>& ids,
        base::OnceCallback<
            void(const std::vector<std::optional<CrxComponent>>&)> callback) {
      CrxComponent crx1;
      crx1.app_id = "jebgalgnebhfojomionfpkfelancnnkf";
      crx1.name = "test_jebg";
      crx1.pk_hash = base::ToVector(jebg_hash);
      crx1.version = base::Version("0.9");
      crx1.installer = base::MakeRefCounted<TestInstaller>();
      crx1.crx_format_requirement = crx_file::VerifierFormat::CRX3;

      CrxComponent crx2;
      crx2.app_id = "ihfokbkgjpifnbbojhneepfflplebdkc";
      crx2.name = "test_ihfo";
      crx2.pk_hash = base::ToVector(ihfo_hash);
      crx2.version = base::Version("0.8");
      crx2.installer = base::MakeRefCounted<TestInstaller>();
      crx2.crx_format_requirement = crx_file::VerifierFormat::CRX3;

      std::move(callback).Run({crx1, crx2});
    }
  };

  MockUpdateCheckerFactory<
      MockUpdateCheckerImpl<UpdateCheckerOptionsTwoCrxUpdate>>
      mock_update_checker_factory;

  class MockCrxDownloader : public CrxDownloader {
   public:
    MockCrxDownloader() = default;

   private:
    ~MockCrxDownloader() override = default;

    base::OnceClosure DoStartDownload(const GURL& url) override {
      DownloadMetrics download_metrics;
      base::FilePath path;
      Result result;
      if (url.GetPath() == "/download/jebgalgnebhfojomionfpkfelancnnkf.crx") {
        download_metrics.url = url;
        download_metrics.downloader = DownloadMetrics::kNone;
        download_metrics.error = -118;
        download_metrics.downloaded_bytes = 0;
        download_metrics.total_bytes = 0;
        download_metrics.download_time_ms = 1000;

        // The result must not include a file path in the case of errors.
        result.error = -118;
      } else if (url.GetPath() ==
                 "/download/ihfokbkgjpifnbbojhneepfflplebdkc_1.crx") {
        download_metrics.url = url;
        download_metrics.downloader = DownloadMetrics::kNone;
        download_metrics.error = 0;
        download_metrics.downloaded_bytes = 54014;
        download_metrics.total_bytes = 54014;
        download_metrics.download_time_ms = 2000;

        EXPECT_TRUE(MakeTestFile(
            GetTestFilePath("ihfokbkgjpifnbbojhneepfflplebdkc_1.crx"), &path));

        result.error = 0;
        result.response = path;
      } else {
        ADD_FAILURE();
      }

      base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
          FROM_HERE, base::BindOnce(&MockCrxDownloader::OnDownloadProgress,
                                    base::Unretained(this),
                                    download_metrics.downloaded_bytes,
                                    download_metrics.total_bytes));

      base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
          FROM_HERE, base::BindOnce(&MockCrxDownloader::OnDownloadComplete,
                                    base::Unretained(this), true, result,
                                    download_metrics));
      return base::DoNothing();
    }
  };

  class MockPingManager : public MockPingManagerImpl {
   public:
    explicit MockPingManager(scoped_refptr<Configurator> config)
        : MockPingManagerImpl(config) {}

   protected:
    ~MockPingManager() override {
      const auto ping_data = MockPingManagerImpl::terminal_ping_data();
      EXPECT_EQ(2u, ping_data.size());
      EXPECT_EQ("jebgalgnebhfojomionfpkfelancnnkf", ping_data[0].id);
      EXPECT_EQ(base::Version("0.9"), ping_data[0].previous_version);
      EXPECT_EQ(base::Version("1.0"), ping_data[0].next_version);
      EXPECT_EQ(1, static_cast<int>(ping_data[0].error_category));
      EXPECT_EQ(-118, ping_data[0].error_code);
      EXPECT_EQ("ihfokbkgjpifnbbojhneepfflplebdkc", ping_data[1].id);
      EXPECT_EQ(base::Version("0.8"), ping_data[1].previous_version);
      EXPECT_EQ(base::Version("1.0"), ping_data[1].next_version);
      EXPECT_EQ(0, static_cast<int>(ping_data[1].error_category));
      EXPECT_EQ(0, ping_data[1].error_code);
    }
  };

  SetMockCrxDownloader<MockCrxDownloader>();
  scoped_refptr<UpdateClient> update_client =
      base::MakeRefCounted<UpdateClientImpl>(
          config(), base::MakeRefCounted<MockPingManager>(config()),
          mock_update_checker_factory.GetFactory());

  MockObserver observer(update_client);
  {
    InSequence seq;
    EXPECT_CALL(observer, OnEvent(Truly([](const CrxUpdateItem& item) {
                  return item.id == "jebgalgnebhfojomionfpkfelancnnkf" &&
                         item.state == ComponentState::kChecking;
                })));
    EXPECT_CALL(observer, OnEvent(Truly([](const CrxUpdateItem& item) {
                  return item.id == "jebgalgnebhfojomionfpkfelancnnkf" &&
                         item.state == ComponentState::kCanUpdate;
                })));
    EXPECT_CALL(observer, OnEvent(Truly([](const CrxUpdateItem& item) {
                  return item.id == "jebgalgnebhfojomionfpkfelancnnkf" &&
                         item.state == ComponentState::kDownloading;
                })))
        .Times(AtLeast(1));
    EXPECT_CALL(observer, OnEvent(Truly([](const CrxUpdateItem& item) {
                  return item.id == "jebgalgnebhfojomionfpkfelancnnkf" &&
                         item.state == ComponentState::kUpdateError;
                })))
        .WillOnce([](const CrxUpdateItem& item) {
          EXPECT_EQ(ComponentState::kUpdateError, item.state);
          EXPECT_EQ(1, static_cast<int>(item.error_category));
          EXPECT_EQ(-118, item.error_code);
          EXPECT_EQ(0, item.extra_code1);
        });
  }
  {
    InSequence seq;
    EXPECT_CALL(observer, OnEvent(Truly([](const CrxUpdateItem& item) {
                  return item.id == "ihfokbkgjpifnbbojhneepfflplebdkc" &&
                         item.state == ComponentState::kChecking;
                })));
    EXPECT_CALL(observer, OnEvent(Truly([](const CrxUpdateItem& item) {
                  return item.id == "ihfokbkgjpifnbbojhneepfflplebdkc" &&
                         item.state == ComponentState::kCanUpdate;
                })));
    EXPECT_CALL(observer, OnEvent(Truly([](const CrxUpdateItem& item) {
                  return item.id == "ihfokbkgjpifnbbojhneepfflplebdkc" &&
                         item.state == ComponentState::kDownloading;
                })))
        .Times(AtLeast(1));
    EXPECT_CALL(observer, OnEvent(Truly([](const CrxUpdateItem& item) {
                  return item.id == "ihfokbkgjpifnbbojhneepfflplebdkc" &&
                         item.state == ComponentState::kUpdating;
                })));
    EXPECT_CALL(observer, OnEvent(Truly([](const CrxUpdateItem& item) {
                  return item.id == "ihfokbkgjpifnbbojhneepfflplebdkc" &&
                         item.state == ComponentState::kUpdated;
                })));
  }

  std::vector<CrxUpdateItem> items;
  auto receiver = base::MakeRefCounted<MockCrxStateChangeReceiver>();
  EXPECT_CALL(*receiver, Receive(_))
      .WillRepeatedly(
          [&items](const CrxUpdateItem& item) { items.push_back(item); });

  const std::vector<std::string> ids = {"jebgalgnebhfojomionfpkfelancnnkf",
                                        "ihfokbkgjpifnbbojhneepfflplebdkc"};
  update_client->Update(
      ids, base::BindOnce(&DataCallbackMock::Callback),
      base::BindRepeating(&MockCrxStateChangeReceiver::Receive, receiver),
      false, ExpectErrorThenQuit(runloop_, Error::NONE));
  runloop_.Run();

  EXPECT_EQ(11u, items.size());
  EXPECT_EQ(ComponentState::kChecking, items[0].state);
  EXPECT_EQ("jebgalgnebhfojomionfpkfelancnnkf", items[0].id);
  EXPECT_EQ(ComponentState::kChecking, items[1].state);
  EXPECT_EQ("ihfokbkgjpifnbbojhneepfflplebdkc", items[1].id);
  EXPECT_EQ(ComponentState::kCanUpdate, items[2].state);
  EXPECT_EQ("jebgalgnebhfojomionfpkfelancnnkf", items[2].id);
  EXPECT_EQ(ComponentState::kDownloading, items[3].state);
  EXPECT_EQ("jebgalgnebhfojomionfpkfelancnnkf", items[3].id);
  EXPECT_EQ(ComponentState::kDownloading, items[4].state);
  EXPECT_EQ("jebgalgnebhfojomionfpkfelancnnkf", items[4].id);
  EXPECT_EQ(ComponentState::kUpdateError, items[5].state);
  EXPECT_EQ("jebgalgnebhfojomionfpkfelancnnkf", items[5].id);
  EXPECT_EQ(ComponentState::kCanUpdate, items[6].state);
  EXPECT_EQ("ihfokbkgjpifnbbojhneepfflplebdkc", items[6].id);
  EXPECT_EQ(ComponentState::kDownloading, items[7].state);
  EXPECT_EQ("ihfokbkgjpifnbbojhneepfflplebdkc", items[7].id);
  EXPECT_EQ(ComponentState::kDownloading, items[8].state);
  EXPECT_EQ("ihfokbkgjpifnbbojhneepfflplebdkc", items[8].id);
  EXPECT_EQ(ComponentState::kUpdating, items[9].state);
  EXPECT_EQ("ihfokbkgjpifnbbojhneepfflplebdkc", items[9].id);
  EXPECT_EQ(ComponentState::kUpdated, items[10].state);
  EXPECT_EQ("ihfokbkgjpifnbbojhneepfflplebdkc", items[10].id);
}

// Tests the differential update scenario for one CRX. Tests install progress
// for differential and full updates.
TEST_F(UpdateClientTest, OneCrxDiffUpdate) {
  class DataCallbackMock : public base::RefCountedThreadSafe<DataCallbackMock> {
   public:
    DataCallbackMock() {
      installer_->set_installer_progress_samples({-1, 50, 100});
    }

    void Callback(
        const std::vector<std::string>& ids,
        base::OnceCallback<
            void(const std::vector<std::optional<CrxComponent>>&)> callback) {
      ++num_calls_;

      CrxComponent crx;
      crx.app_id = "ihfokbkgjpifnbbojhneepfflplebdkc";
      crx.name = "test_ihfo";
      crx.pk_hash = base::ToVector(ihfo_hash);
      crx.installer = installer_;
      crx.crx_format_requirement = crx_file::VerifierFormat::CRX3;
      if (num_calls_ == 1) {
        crx.version = base::Version("0.8");
      } else if (num_calls_ == 2) {
        crx.version = base::Version("1.0");
      } else {
        ADD_FAILURE();
      }

      std::move(callback).Run({crx});
    }

   private:
    friend class base::RefCountedThreadSafe<DataCallbackMock>;
    ~DataCallbackMock() = default;

    int num_calls_ = 0;
    scoped_refptr<VersionedTestInstaller> installer_ =
        base::MakeRefCounted<VersionedTestInstaller>();
  };
  auto data_callback_mock = MakeMockCallback<DataCallbackMock>();

  class MockUpdateChecker : public UpdateChecker {
   public:
    explicit MockUpdateChecker(int num_calls) : num_calls_(num_calls) {}

    void CheckForUpdates(
        scoped_refptr<UpdateContext> context,
        const base::flat_map<std::string, std::string>& additional_attributes,
        UpdateCheckCallback update_check_callback) override {
      context->get_available_space = base::BindRepeating(
          [](const base::FilePath&) -> int64_t { return 200000; });
      base::expected<ProtocolParser::Results, std::string> results;
      if (num_calls_ == 1) {
        context->get_available_space = base::BindRepeating(
            [](const base::FilePath&) -> int64_t { return 150000; });
        results = ProtocolParserJSON::ParseJSON(R"()]}'
{
  "response": {
    "protocol": "4.0",
    "apps": [
      {
        "appid": "ihfokbkgjpifnbbojhneepfflplebdkc",
        "status": "ok",
        "updatecheck": {
          "status": "ok",
          "nextversion": "1.0",
          "pipelines": [
            {
              "operations": [
                {
                  "type": "download",
                  "urls": [
                    {
                      "url": "http://localhost/download/ihfokbkgjpifnbbojhneepfflplebdkc_1.crx"
                    }
                  ],
                  "out": {
                    "sha256": "8f5aa190311237cae00675af87ff457f278cd1a05895470ac5d46647d4a3c2ea"
                  },
                  "size": 54014
                },
                {
                  "type": "crx3",
                  "in": {
                    "sha256": "8f5aa190311237cae00675af87ff457f278cd1a05895470ac5d46647d4a3c2ea"
                  }
                }
              ]
            }
          ]
        }
      }
    ]
  }
})");
      } else if (num_calls_ == 2) {
        context->get_available_space = base::BindRepeating(
            [](const base::FilePath&) -> int64_t { return 4000; });
        results = ProtocolParserJSON::ParseJSON(R"()]}'
{
  "response": {
    "protocol": "4.0",
    "apps": [
      {
        "appid": "ihfokbkgjpifnbbojhneepfflplebdkc",
        "status": "ok",
        "updatecheck": {
          "status": "ok",
          "nextversion": "2.0",
          "pipelines": [
            {
              "operations": [
                {
                  "type": "download",
                  "urls": [
                    {
                      "url": "http://localhost/download/ihfokbkgjpifnbbojhneepfflplebdkc_1to2.puff"
                    }
                  ],
                  "out": {
                    "sha256": "f2254da51fa2478a8ba90e58e1c28e24033ec7841015eebf1c82e31b957c44b2"
                  },
                  "size": 1680
                },
                {
                  "type": "puff",
                  "previous": {
                    "sha256": "8f5aa190311237cae00675af87ff457f278cd1a05895470ac5d46647d4a3c2ea"
                  },
                  "out": {
                    "sha256": "c87d8742c3ff3d7a0cb6f3c91aa2fcf3dea63618086a7db1c5be5300e1d4d6b6"
                  }
                },
                {
                  "type": "crx3",
                  "in": {
                    "sha256": "c87d8742c3ff3d7a0cb6f3c91aa2fcf3dea63618086a7db1c5be5300e1d4d6b6"
                  }
                }
              ]
            }
          ]
        }
      }
    ]
  }
})");
      } else {
        ADD_FAILURE();
      }
      EXPECT_TRUE(results.has_value()) << results.error();
      EXPECT_FALSE(context->session_id.empty());
      EXPECT_EQ(context->components_to_check_for_updates.size(), 1u);
      base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
          FROM_HERE,
          base::BindOnce(std::move(update_check_callback), results.value(),
                         ErrorCategory::kNone, 0, 0));
    }

   private:
    const int num_calls_;
  };
  MockUpdateCheckerFactory<MockUpdateChecker> mock_update_checker_factory;

  class MockCrxDownloader : public CrxDownloader {
   public:
    MockCrxDownloader() = default;

   private:
    ~MockCrxDownloader() override = default;

    base::OnceClosure DoStartDownload(const GURL& url) override {
      DownloadMetrics download_metrics;
      base::FilePath path;
      Result result;
      if (url.GetPath() == "/download/ihfokbkgjpifnbbojhneepfflplebdkc_1.crx") {
        download_metrics.url = url;
        download_metrics.downloader = DownloadMetrics::kNone;
        download_metrics.error = 0;
        download_metrics.downloaded_bytes = 54014;
        download_metrics.total_bytes = 54014;
        download_metrics.download_time_ms = 2000;

        EXPECT_TRUE(MakeTestFile(
            GetTestFilePath("ihfokbkgjpifnbbojhneepfflplebdkc_1.crx"), &path));

        result.error = 0;
        result.response = path;
      } else if (url.GetPath() ==
                 "/download/ihfokbkgjpifnbbojhneepfflplebdkc_1to2.puff") {
        download_metrics.url = url;
        download_metrics.downloader = DownloadMetrics::kNone;
        download_metrics.error = 0;
        download_metrics.downloaded_bytes = 1680;
        download_metrics.total_bytes = 1680;
        download_metrics.download_time_ms = 1000;

        EXPECT_TRUE(MakeTestFile(
            GetTestFilePath("ihfokbkgjpifnbbojhneepfflplebdkc_1to2.puff"),
            &path));

        result.error = 0;
        result.response = path;
      } else {
        ADD_FAILURE() << url.GetPath();
      }

      base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
          FROM_HERE, base::BindOnce(&MockCrxDownloader::OnDownloadProgress,
                                    base::Unretained(this),
                                    download_metrics.downloaded_bytes / 2,
                                    download_metrics.total_bytes));

      base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
          FROM_HERE, base::BindOnce(&MockCrxDownloader::OnDownloadProgress,
                                    base::Unretained(this),
                                    download_metrics.downloaded_bytes,
                                    download_metrics.total_bytes));

      base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
          FROM_HERE, base::BindOnce(&MockCrxDownloader::OnDownloadComplete,
                                    base::Unretained(this), true, result,
                                    download_metrics));
      return base::DoNothing();
    }
  };

  class MockPingManager : public MockPingManagerImpl {
   public:
    explicit MockPingManager(scoped_refptr<Configurator> config)
        : MockPingManagerImpl(config) {}

   protected:
    ~MockPingManager() override {
      const auto ping_data = MockPingManagerImpl::terminal_ping_data();
      EXPECT_EQ(2u, ping_data.size());
      EXPECT_EQ("ihfokbkgjpifnbbojhneepfflplebdkc", ping_data[0].id);
      EXPECT_EQ(base::Version("0.8"), ping_data[0].previous_version);
      EXPECT_EQ(base::Version("1.0"), ping_data[0].next_version);
      EXPECT_EQ(0, static_cast<int>(ping_data[0].error_category));
      EXPECT_EQ(0, ping_data[0].error_code);
      EXPECT_EQ("ihfokbkgjpifnbbojhneepfflplebdkc", ping_data[1].id);
      EXPECT_EQ(base::Version("1.0"), ping_data[1].previous_version);
      EXPECT_EQ(base::Version("2.0"), ping_data[1].next_version);
      EXPECT_EQ(0, ping_data[1].error_code);
    }
  };

  SetMockCrxDownloader<MockCrxDownloader>();
  scoped_refptr<UpdateClient> update_client =
      base::MakeRefCounted<UpdateClientImpl>(
          config(), base::MakeRefCounted<MockPingManager>(config()),
          mock_update_checker_factory.GetFactory());

  MockObserver observer(update_client);
  {
    InSequence seq;
    EXPECT_CALL(observer, OnEvent(Truly([](const CrxUpdateItem& item) {
                  return item.id == "ihfokbkgjpifnbbojhneepfflplebdkc" &&
                         item.state == ComponentState::kChecking;
                })));
    EXPECT_CALL(observer, OnEvent(Truly([](const CrxUpdateItem& item) {
                  return item.id == "ihfokbkgjpifnbbojhneepfflplebdkc" &&
                         item.state == ComponentState::kCanUpdate;
                })));
    EXPECT_CALL(observer, OnEvent(Truly([](const CrxUpdateItem& item) {
                  return item.id == "ihfokbkgjpifnbbojhneepfflplebdkc" &&
                         item.state == ComponentState::kDownloading;
                })))
        .Times(AtLeast(1));
    EXPECT_CALL(observer, OnEvent(Truly([](const CrxUpdateItem& item) {
                  return item.id == "ihfokbkgjpifnbbojhneepfflplebdkc" &&
                         item.state == ComponentState::kUpdating;
                })));
    EXPECT_CALL(observer, OnEvent(Truly([](const CrxUpdateItem& item) {
                  return item.id == "ihfokbkgjpifnbbojhneepfflplebdkc" &&
                         item.state == ComponentState::kUpdating;
                })))
        .Times(3);
    EXPECT_CALL(observer, OnEvent(Truly([](const CrxUpdateItem& item) {
                  return item.id == "ihfokbkgjpifnbbojhneepfflplebdkc" &&
                         item.state == ComponentState::kUpdated;
                })));
    EXPECT_CALL(observer, OnEvent(Truly([](const CrxUpdateItem& item) {
                  return item.id == "ihfokbkgjpifnbbojhneepfflplebdkc" &&
                         item.state == ComponentState::kChecking;
                })));
    EXPECT_CALL(observer, OnEvent(Truly([](const CrxUpdateItem& item) {
                  return item.id == "ihfokbkgjpifnbbojhneepfflplebdkc" &&
                         item.state == ComponentState::kCanUpdate;
                })));
    EXPECT_CALL(observer, OnEvent(Truly([](const CrxUpdateItem& item) {
                  return item.id == "ihfokbkgjpifnbbojhneepfflplebdkc" &&
                         item.state == ComponentState::kDownloading;
                })))
        .Times(AtLeast(1));
    EXPECT_CALL(observer, OnEvent(Truly([](const CrxUpdateItem& item) {
                  return item.id == "ihfokbkgjpifnbbojhneepfflplebdkc" &&
                         item.state == ComponentState::kUpdating;
                })));
    EXPECT_CALL(observer, OnEvent(Truly([](const CrxUpdateItem& item) {
                  return item.id == "ihfokbkgjpifnbbojhneepfflplebdkc" &&
                         item.state == ComponentState::kUpdating;
                })))
        .Times(3);
    EXPECT_CALL(observer, OnEvent(Truly([](const CrxUpdateItem& item) {
                  return item.id == "ihfokbkgjpifnbbojhneepfflplebdkc" &&
                         item.state == ComponentState::kUpdated;
                })));
  }

  const std::vector<std::string> ids = {"ihfokbkgjpifnbbojhneepfflplebdkc"};
  {
    std::vector<CrxUpdateItem> items;
    auto receiver = base::MakeRefCounted<MockCrxStateChangeReceiver>();
    EXPECT_CALL(*receiver, Receive(_))
        .WillRepeatedly(
            [&items](const CrxUpdateItem& item) { items.push_back(item); });

    base::RunLoop runloop;
    update_client->Update(
        ids, data_callback_mock,
        base::BindRepeating(&MockCrxStateChangeReceiver::Receive, receiver),
        false, ExpectErrorThenQuit(runloop, Error::NONE));
    runloop.Run();
    EXPECT_EQ(10u, items.size());
    EXPECT_EQ(ComponentState::kChecking, items[0].state);
    EXPECT_EQ("ihfokbkgjpifnbbojhneepfflplebdkc", items[0].id);
    EXPECT_EQ(ComponentState::kCanUpdate, items[1].state);
    EXPECT_EQ("ihfokbkgjpifnbbojhneepfflplebdkc", items[1].id);
    EXPECT_EQ(ComponentState::kDownloading, items[2].state);
    EXPECT_EQ("ihfokbkgjpifnbbojhneepfflplebdkc", items[2].id);
    EXPECT_EQ(ComponentState::kDownloading, items[3].state);
    EXPECT_EQ("ihfokbkgjpifnbbojhneepfflplebdkc", items[3].id);
    EXPECT_EQ(ComponentState::kDownloading, items[4].state);
    EXPECT_EQ("ihfokbkgjpifnbbojhneepfflplebdkc", items[4].id);
    EXPECT_EQ(ComponentState::kUpdating, items[5].state);
    EXPECT_EQ("ihfokbkgjpifnbbojhneepfflplebdkc", items[5].id);
    EXPECT_EQ(ComponentState::kUpdating, items[6].state);
    EXPECT_EQ("ihfokbkgjpifnbbojhneepfflplebdkc", items[6].id);
    EXPECT_EQ(ComponentState::kUpdating, items[7].state);
    EXPECT_EQ("ihfokbkgjpifnbbojhneepfflplebdkc", items[7].id);
    EXPECT_EQ(ComponentState::kUpdating, items[8].state);
    EXPECT_EQ("ihfokbkgjpifnbbojhneepfflplebdkc", items[8].id);
    EXPECT_EQ(ComponentState::kUpdated, items[9].state);
    EXPECT_EQ("ihfokbkgjpifnbbojhneepfflplebdkc", items[9].id);

    std::vector samples = {-1, -1, -1, -1, -1, -1, -1, 50, 100, 100};
    EXPECT_EQ(items.size(), samples.size());
    for (size_t i = 0; i != items.size(); ++i) {
      EXPECT_EQ(items[i].install_progress, samples[i]);
    }
  }

  {
    std::vector<CrxUpdateItem> items;
    auto receiver = base::MakeRefCounted<MockCrxStateChangeReceiver>();
    EXPECT_CALL(*receiver, Receive(_))
        .WillRepeatedly(
            [&items](const CrxUpdateItem& item) { items.push_back(item); });

    base::RunLoop runloop;
    update_client->Update(
        ids, data_callback_mock,
        base::BindRepeating(&MockCrxStateChangeReceiver::Receive, receiver),
        false, ExpectErrorThenQuit(runloop, Error::NONE));
    runloop.Run();

    EXPECT_EQ(10u, items.size());
    EXPECT_EQ(ComponentState::kChecking, items[0].state);
    EXPECT_EQ("ihfokbkgjpifnbbojhneepfflplebdkc", items[0].id);
    EXPECT_EQ(ComponentState::kCanUpdate, items[1].state);
    EXPECT_EQ("ihfokbkgjpifnbbojhneepfflplebdkc", items[1].id);
    EXPECT_EQ(ComponentState::kDownloading, items[2].state);
    EXPECT_EQ("ihfokbkgjpifnbbojhneepfflplebdkc", items[2].id);
    EXPECT_EQ(ComponentState::kDownloading, items[3].state);
    EXPECT_EQ("ihfokbkgjpifnbbojhneepfflplebdkc", items[3].id);
    EXPECT_EQ(ComponentState::kDownloading, items[4].state);
    EXPECT_EQ("ihfokbkgjpifnbbojhneepfflplebdkc", items[4].id);
    EXPECT_EQ(ComponentState::kUpdating, items[5].state);
    EXPECT_EQ("ihfokbkgjpifnbbojhneepfflplebdkc", items[5].id);
    EXPECT_EQ(ComponentState::kUpdating, items[6].state);
    EXPECT_EQ("ihfokbkgjpifnbbojhneepfflplebdkc", items[6].id);
    EXPECT_EQ(ComponentState::kUpdating, items[7].state);
    EXPECT_EQ("ihfokbkgjpifnbbojhneepfflplebdkc", items[7].id);
    EXPECT_EQ(ComponentState::kUpdating, items[8].state);
    EXPECT_EQ("ihfokbkgjpifnbbojhneepfflplebdkc", items[8].id);
    EXPECT_EQ(ComponentState::kUpdated, items[9].state);
    EXPECT_EQ("ihfokbkgjpifnbbojhneepfflplebdkc", items[9].id);

    std::vector<int> samples = {-1, -1, -1, -1, -1, -1, -1, 50, 100, 100};
    EXPECT_EQ(items.size(), samples.size());
    for (size_t i = 0; i != items.size(); ++i) {
      EXPECT_EQ(items[i].install_progress, samples[i]);
    }
  }
}

// Tests the update scenario for one CRX where the CRX installer returns
// an error. Tests that the |unpack_path| argument refers to a valid path
// then |Install| is called, then tests that the |unpack| path is deleted
// by the |update_client| code before the test ends.
TEST_F(UpdateClientTest, OneCrxInstallError) {
  class MockInstaller : public CrxInstaller {
   public:
    MOCK_METHOD(void, DoInstall, (const base::FilePath& unpack_path));
    MOCK_METHOD(std::optional<base::FilePath>,
                GetInstalledFile,
                (const std::string& file),
                (override));
    MOCK_METHOD(bool, Uninstall, (), (override));

    void Install(const base::FilePath& unpack_path,
                 const std::string& public_key,
                 std::unique_ptr<InstallParams> /*install_params*/,
                 ProgressCallback progress_callback,
                 Callback callback) override {
      DoInstall(unpack_path);

      unpack_path_ = unpack_path;
      EXPECT_TRUE(base::DirectoryExists(unpack_path_));
      base::ThreadPool::PostTask(
          FROM_HERE, {base::MayBlock()},
          base::BindOnce(
              std::move(callback),
              CrxInstaller::Result(
                  {.category = ErrorCategory::kInstaller,
                   .code = static_cast<int>(InstallError::GENERIC_ERROR)})));
    }

   protected:
    ~MockInstaller() override {
      // The unpack path is deleted unconditionally by the component state code,
      // which is driving this installer. Therefore, the unpack path must not
      // exist when this object is destroyed.
      if (!unpack_path_.empty()) {
        EXPECT_FALSE(base::DirectoryExists(unpack_path_));
      }
    }

   private:
    // Contains the |unpack_path| argument of the Install call.
    base::FilePath unpack_path_;
  };

  class DataCallbackMock {
   public:
    static void Callback(
        const std::vector<std::string>& ids,
        base::OnceCallback<
            void(const std::vector<std::optional<CrxComponent>>&)> callback) {
      scoped_refptr<MockInstaller> installer =
          base::MakeRefCounted<MockInstaller>();

      EXPECT_CALL(*installer, DoInstall(_));
      EXPECT_CALL(*installer, GetInstalledFile(_)).Times(0);
      EXPECT_CALL(*installer, Uninstall()).Times(0);

      CrxComponent crx;
      crx.app_id = "jebgalgnebhfojomionfpkfelancnnkf";
      crx.name = "test_jebg";
      crx.pk_hash = base::ToVector(jebg_hash);
      crx.version = base::Version("0.9");
      crx.installer = installer;
      crx.crx_format_requirement = crx_file::VerifierFormat::CRX3;

      std::move(callback).Run({crx});
    }
  };

  MockUpdateCheckerFactory<
      MockUpdateCheckerImpl<UpdateCheckerOptionsOneCrxInstall>>
      mock_update_checker_factory;

  class MockCrxDownloader : public CrxDownloader {
   public:
    MockCrxDownloader() = default;

   private:
    ~MockCrxDownloader() override = default;

    base::OnceClosure DoStartDownload(const GURL& url) override {
      DownloadMetrics download_metrics;
      download_metrics.url = url;
      download_metrics.downloader = DownloadMetrics::kNone;
      download_metrics.error = 0;
      download_metrics.downloaded_bytes = 1015;
      download_metrics.total_bytes = 1015;
      download_metrics.download_time_ms = 1000;

      base::FilePath path;
      EXPECT_TRUE(MakeTestFile(
          GetTestFilePath("jebgalgnebhfojomionfpkfelancnnkf.crx"), &path));

      Result result;
      result.error = 0;
      result.response = path;

      base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
          FROM_HERE, base::BindOnce(&MockCrxDownloader::OnDownloadProgress,
                                    base::Unretained(this),
                                    download_metrics.downloaded_bytes,
                                    download_metrics.total_bytes));

      base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
          FROM_HERE, base::BindOnce(&MockCrxDownloader::OnDownloadComplete,
                                    base::Unretained(this), true, result,
                                    download_metrics));
      return base::DoNothing();
    }
  };

  class MockPingManager : public MockPingManagerImpl {
   public:
    explicit MockPingManager(scoped_refptr<Configurator> config)
        : MockPingManagerImpl(config) {}

   protected:
    ~MockPingManager() override {
      const auto ping_data = MockPingManagerImpl::ping_data();
      EXPECT_EQ(3u, ping_data.size());
      // Expect that the download ping carries the pipeline id.
      EXPECT_EQ(ping_data[0].pipeline_id, "pipe1");  // Download
      EXPECT_EQ(ping_data[0].error_category, ErrorCategory::kNone);
      EXPECT_EQ(static_cast<CrxDownloaderError>(ping_data[0].error_code),
                CrxDownloaderError::NONE);

      EXPECT_EQ(ping_data[1].pipeline_id, "pipe1");  // crx3
      EXPECT_EQ(ping_data[1].error_category, ErrorCategory::kInstaller);
      EXPECT_EQ(static_cast<InstallError>(ping_data[1].error_code),
                InstallError::GENERIC_ERROR);

      EXPECT_EQ(ping_data[2].id, "jebgalgnebhfojomionfpkfelancnnkf");
      EXPECT_EQ(ping_data[2].previous_version, base::Version("0.9"));
      EXPECT_EQ(ping_data[2].next_version, base::Version("1.0"));
      EXPECT_EQ(ping_data[2].error_category, ErrorCategory::kInstaller);
      EXPECT_EQ(static_cast<InstallError>(ping_data[2].error_code),
                InstallError::GENERIC_ERROR);
    }
  };

  SetMockCrxDownloader<MockCrxDownloader>();
  scoped_refptr<UpdateClient> update_client =
      base::MakeRefCounted<UpdateClientImpl>(
          config(), base::MakeRefCounted<MockPingManager>(config()),
          mock_update_checker_factory.GetFactory());

  MockObserver observer(update_client);
  {
    InSequence seq;
    EXPECT_CALL(observer, OnEvent(Truly([](const CrxUpdateItem& item) {
                  return item.id == "jebgalgnebhfojomionfpkfelancnnkf" &&
                         item.state == ComponentState::kChecking;
                })));
    EXPECT_CALL(observer, OnEvent(Truly([](const CrxUpdateItem& item) {
                  return item.id == "jebgalgnebhfojomionfpkfelancnnkf" &&
                         item.state == ComponentState::kCanUpdate;
                })));
    EXPECT_CALL(observer, OnEvent(Truly([](const CrxUpdateItem& item) {
                  return item.id == "jebgalgnebhfojomionfpkfelancnnkf" &&
                         item.state == ComponentState::kDownloading;
                })))
        .Times(AtLeast(1));
    EXPECT_CALL(observer, OnEvent(Truly([](const CrxUpdateItem& item) {
                  return item.id == "jebgalgnebhfojomionfpkfelancnnkf" &&
                         item.state == ComponentState::kUpdating;
                })));
    EXPECT_CALL(observer, OnEvent(Truly([](const CrxUpdateItem& item) {
                  return item.id == "jebgalgnebhfojomionfpkfelancnnkf" &&
                         item.state == ComponentState::kUpdateError;
                })));
  }

  std::vector<CrxUpdateItem> items;
  auto receiver = base::MakeRefCounted<MockCrxStateChangeReceiver>();
  EXPECT_CALL(*receiver, Receive(_))
      .WillRepeatedly(
          [&items](const CrxUpdateItem& item) { items.push_back(item); });

  update_client->Update(
      {"jebgalgnebhfojomionfpkfelancnnkf"},
      base::BindOnce(&DataCallbackMock::Callback),
      base::BindRepeating(&MockCrxStateChangeReceiver::Receive, receiver),
      false, ExpectErrorThenQuit(runloop_, Error::NONE));
  runloop_.Run();

  EXPECT_EQ(6u, items.size());
  EXPECT_EQ(ComponentState::kChecking, items[0].state);
  EXPECT_EQ("jebgalgnebhfojomionfpkfelancnnkf", items[0].id);
  EXPECT_EQ(ComponentState::kCanUpdate, items[1].state);
  EXPECT_EQ("jebgalgnebhfojomionfpkfelancnnkf", items[1].id);
  EXPECT_EQ(ComponentState::kDownloading, items[2].state);
  EXPECT_EQ("jebgalgnebhfojomionfpkfelancnnkf", items[2].id);
  EXPECT_EQ(ComponentState::kDownloading, items[3].state);
  EXPECT_EQ("jebgalgnebhfojomionfpkfelancnnkf", items[3].id);
  EXPECT_EQ(ComponentState::kUpdating, items[4].state);
  EXPECT_EQ("jebgalgnebhfojomionfpkfelancnnkf", items[4].id);
  EXPECT_EQ(ComponentState::kUpdateError, items[5].state);
  EXPECT_EQ("jebgalgnebhfojomionfpkfelancnnkf", items[5].id);
}

// Tests the fallback from differential to full update scenario for one CRX.
TEST_F(UpdateClientTest, OneCrxDiffUpdateFailsFullUpdateSucceeds) {
  class DataCallbackMock : public base::RefCountedThreadSafe<DataCallbackMock> {
   public:
    void Callback(
        const std::vector<std::string>& ids,
        base::OnceCallback<
            void(const std::vector<std::optional<CrxComponent>>&)> callback) {
      ++num_calls_;

      CrxComponent crx;
      crx.app_id = "ihfokbkgjpifnbbojhneepfflplebdkc";
      crx.name = "test_ihfo";
      crx.pk_hash = base::ToVector(ihfo_hash);
      crx.installer = installer_;
      crx.crx_format_requirement = crx_file::VerifierFormat::CRX3;
      if (num_calls_ == 1) {
        crx.version = base::Version("0.8");
      } else if (num_calls_ == 2) {
        crx.version = base::Version("1.0");
      } else {
        ADD_FAILURE();
      }

      std::move(callback).Run({crx});
    }

   private:
    friend class base::RefCountedThreadSafe<DataCallbackMock>;
    ~DataCallbackMock() = default;

    int num_calls_ = 0;
    scoped_refptr<VersionedTestInstaller> installer_ =
        base::MakeRefCounted<VersionedTestInstaller>();
  };
  auto data_callback_mock = MakeMockCallback<DataCallbackMock>();

  class MockUpdateChecker : public UpdateChecker {
   public:
    explicit MockUpdateChecker(int num_calls) : num_calls_(num_calls) {}

    void CheckForUpdates(
        scoped_refptr<UpdateContext> context,
        const base::flat_map<std::string, std::string>& additional_attributes,
        UpdateCheckCallback update_check_callback) override {
      base::expected<ProtocolParser::Results, std::string> results;
      context->get_available_space = base::BindRepeating(
          [](const base::FilePath&) -> int64_t { return 150000; });

      if (num_calls_ == 1) {
        results = ProtocolParserJSON::ParseJSON(R"()]}'
{
  "response": {
    "protocol": "4.0",
    "apps": [
      {
        "appid": "ihfokbkgjpifnbbojhneepfflplebdkc",
        "status": "ok",
        "updatecheck": {
          "status": "ok",
          "nextversion": "1.0",
          "pipelines": [
            {
              "operations": [
                {
                  "type": "download",
                  "urls": [
                    {
                      "url": "http://localhost/download/ihfokbkgjpifnbbojhneepfflplebdkc_1.crx"
                    }
                  ],
                  "out": {
                    "sha256": "8f5aa190311237cae00675af87ff457f278cd1a05895470ac5d46647d4a3c2ea"
                  },
                  "size": 54014
                },
                {
                  "type": "crx3",
                  "in": {
                    "sha256": "8f5aa190311237cae00675af87ff457f278cd1a05895470ac5d46647d4a3c2ea"
                  }
                }
              ]
            }
          ]
        }
      }
    ]
  }
})");
      } else if (num_calls_ == 2) {
        results = ProtocolParserJSON::ParseJSON(R"()]}'
{
  "response": {
    "protocol": "4.0",
    "apps": [
      {
        "appid": "ihfokbkgjpifnbbojhneepfflplebdkc",
        "status": "ok",
        "updatecheck": {
          "status": "ok",
          "nextversion": "2.0",
          "pipelines": [
            {
              "operations": [
                {
                  "type": "download",
                  "urls": [
                    {
                      "url": "http://localhost/download/ihfokbkgjpifnbbojhneepfflplebdkc_1to2.puff"
                    }
                  ],
                  "out": {
                    "sha256": "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
                  },
                  "size": 1680
                },
                {
                  "type": "puff",
                  "previous": {
                    "sha256": "8f5aa190311237cae00675af87ff457f278cd1a05895470ac5d46647d4a3c2ea"
                  },
                  "out": {
                    "sha256": "c87d8742c3ff3d7a0cb6f3c91aa2fcf3dea63618086a7db1c5be5300e1d4d6b6"
                  }
                },
                {
                  "type": "crx3",
                  "in": {
                    "sha256": "c87d8742c3ff3d7a0cb6f3c91aa2fcf3dea63618086a7db1c5be5300e1d4d6b6"
                  }
                }
              ]
            },
            {
              "operations": [
                {
                  "type": "download",
                  "urls": [
                    {
                      "url": "http://localhost/download/ihfokbkgjpifnbbojhneepfflplebdkc_2.crx"
                    }
                  ],
                  "out": {
                    "sha256": "c87d8742c3ff3d7a0cb6f3c91aa2fcf3dea63618086a7db1c5be5300e1d4d6b6"
                  },
                  "size": 54409
                },
                {
                  "type": "crx3",
                  "in": {
                    "sha256": "c87d8742c3ff3d7a0cb6f3c91aa2fcf3dea63618086a7db1c5be5300e1d4d6b6"
                  }
                }
              ]
            }
          ]
        }
      }
    ]
  }
})");
      } else {
        ADD_FAILURE();
      }
      EXPECT_TRUE(results.has_value()) << results.error();
      EXPECT_FALSE(context->session_id.empty());
      EXPECT_EQ(context->components_to_check_for_updates.size(), 1u);
      base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
          FROM_HERE,
          base::BindOnce(std::move(update_check_callback), results.value(),
                         ErrorCategory::kNone, 0, 0));
    }

   private:
    const int num_calls_;
  };
  MockUpdateCheckerFactory<MockUpdateChecker> mock_update_checker_factory;

  class MockCrxDownloader : public CrxDownloader {
   public:
    MockCrxDownloader() = default;

   private:
    ~MockCrxDownloader() override = default;

    base::OnceClosure DoStartDownload(const GURL& url) override {
      DownloadMetrics download_metrics;
      base::FilePath path;
      Result result;
      if (url.GetPath() == "/download/ihfokbkgjpifnbbojhneepfflplebdkc_1.crx") {
        download_metrics.url = url;
        download_metrics.downloader = DownloadMetrics::kNone;
        download_metrics.error = 0;
        download_metrics.downloaded_bytes = 54014;
        download_metrics.total_bytes = 54014;
        download_metrics.download_time_ms = 2000;

        EXPECT_TRUE(MakeTestFile(
            GetTestFilePath("ihfokbkgjpifnbbojhneepfflplebdkc_1.crx"), &path));

        result.error = 0;
        result.response = path;
      } else if (url.GetPath() ==
                 "/download/ihfokbkgjpifnbbojhneepfflplebdkc_1to2.puff") {
        // A download error is injected on this execution path.
        download_metrics.url = url;
        download_metrics.downloader = DownloadMetrics::kNone;
        download_metrics.error = -1;
        download_metrics.downloaded_bytes = 0;
        download_metrics.total_bytes = 1680;
        download_metrics.download_time_ms = 1000;

        // The response must not include a file path in the case of errors.
        result.error = -1;
      } else if (url.GetPath() ==
                 "/download/ihfokbkgjpifnbbojhneepfflplebdkc_2.crx") {
        download_metrics.url = url;
        download_metrics.downloader = DownloadMetrics::kNone;
        download_metrics.error = 0;
        download_metrics.downloaded_bytes = 54409;
        download_metrics.total_bytes = 54409;
        download_metrics.download_time_ms = 1000;

        EXPECT_TRUE(MakeTestFile(
            GetTestFilePath("ihfokbkgjpifnbbojhneepfflplebdkc_2.crx"), &path));

        result.error = 0;
        result.response = path;
      }

      base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
          FROM_HERE, base::BindOnce(&MockCrxDownloader::OnDownloadProgress,
                                    base::Unretained(this),
                                    download_metrics.downloaded_bytes,
                                    download_metrics.total_bytes));

      base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
          FROM_HERE, base::BindOnce(&MockCrxDownloader::OnDownloadComplete,
                                    base::Unretained(this), true, result,
                                    download_metrics));
      return base::DoNothing();
    }
  };

  class MockPingManager : public MockPingManagerImpl {
   public:
    explicit MockPingManager(scoped_refptr<Configurator> config)
        : MockPingManagerImpl(config) {}

   protected:
    ~MockPingManager() override {
      const auto ping_data = MockPingManagerImpl::terminal_ping_data();
      EXPECT_EQ(2u, ping_data.size());
      EXPECT_EQ("ihfokbkgjpifnbbojhneepfflplebdkc", ping_data[0].id);
      EXPECT_EQ(base::Version("0.8"), ping_data[0].previous_version);
      EXPECT_EQ(base::Version("1.0"), ping_data[0].next_version);
      EXPECT_EQ(0, static_cast<int>(ping_data[0].error_category));
      EXPECT_EQ(0, ping_data[0].error_code);
      EXPECT_EQ("ihfokbkgjpifnbbojhneepfflplebdkc", ping_data[1].id);
      EXPECT_EQ(base::Version("1.0"), ping_data[1].previous_version);
      EXPECT_EQ(base::Version("2.0"), ping_data[1].next_version);
      EXPECT_EQ(0, static_cast<int>(ping_data[1].error_category));
      EXPECT_EQ(0, ping_data[1].error_code);
    }
  };

  SetMockCrxDownloader<MockCrxDownloader>();
  scoped_refptr<UpdateClient> update_client =
      base::MakeRefCounted<UpdateClientImpl>(
          config(), base::MakeRefCounted<MockPingManager>(config()),
          mock_update_checker_factory.GetFactory());

  MockObserver observer(update_client);
  {
    InSequence seq;
    EXPECT_CALL(observer, OnEvent(Truly([](const CrxUpdateItem& item) {
                  return item.id == "ihfokbkgjpifnbbojhneepfflplebdkc" &&
                         item.state == ComponentState::kChecking;
                })));
    EXPECT_CALL(observer, OnEvent(Truly([](const CrxUpdateItem& item) {
                  return item.id == "ihfokbkgjpifnbbojhneepfflplebdkc" &&
                         item.state == ComponentState::kCanUpdate;
                })));
    EXPECT_CALL(observer, OnEvent(Truly([](const CrxUpdateItem& item) {
                  return item.id == "ihfokbkgjpifnbbojhneepfflplebdkc" &&
                         item.state == ComponentState::kDownloading;
                })))
        .Times(AtLeast(1));
    EXPECT_CALL(observer, OnEvent(Truly([](const CrxUpdateItem& item) {
                  return item.id == "ihfokbkgjpifnbbojhneepfflplebdkc" &&
                         item.state == ComponentState::kUpdating;
                })));
    EXPECT_CALL(observer, OnEvent(Truly([](const CrxUpdateItem& item) {
                  return item.id == "ihfokbkgjpifnbbojhneepfflplebdkc" &&
                         item.state == ComponentState::kUpdated;
                })));

    EXPECT_CALL(observer, OnEvent(Truly([](const CrxUpdateItem& item) {
                  return item.id == "ihfokbkgjpifnbbojhneepfflplebdkc" &&
                         item.state == ComponentState::kChecking;
                })));
    EXPECT_CALL(observer, OnEvent(Truly([](const CrxUpdateItem& item) {
                  return item.id == "ihfokbkgjpifnbbojhneepfflplebdkc" &&
                         item.state == ComponentState::kCanUpdate;
                })));
    EXPECT_CALL(observer, OnEvent(Truly([](const CrxUpdateItem& item) {
                  return item.id == "ihfokbkgjpifnbbojhneepfflplebdkc" &&
                         item.state == ComponentState::kDownloading;
                })))
        .Times(AtLeast(1));
    EXPECT_CALL(observer, OnEvent(Truly([](const CrxUpdateItem& item) {
                  return item.id == "ihfokbkgjpifnbbojhneepfflplebdkc" &&
                         item.state == ComponentState::kDownloading;
                })))
        .Times(AtLeast(1));
    EXPECT_CALL(observer, OnEvent(Truly([](const CrxUpdateItem& item) {
                  return item.id == "ihfokbkgjpifnbbojhneepfflplebdkc" &&
                         item.state == ComponentState::kUpdating;
                })))
        .Times(AtLeast(1));
    EXPECT_CALL(observer, OnEvent(Truly([](const CrxUpdateItem& item) {
                  return item.id == "ihfokbkgjpifnbbojhneepfflplebdkc" &&
                         item.state == ComponentState::kUpdated;
                })));
  }

  const std::vector<std::string> ids = {"ihfokbkgjpifnbbojhneepfflplebdkc"};

  {
    std::vector<CrxUpdateItem> items;
    auto receiver = base::MakeRefCounted<MockCrxStateChangeReceiver>();
    EXPECT_CALL(*receiver, Receive(_))
        .WillRepeatedly(
            [&items](const CrxUpdateItem& item) { items.push_back(item); });

    base::RunLoop runloop;
    update_client->Update(
        ids, data_callback_mock,
        base::BindRepeating(&MockCrxStateChangeReceiver::Receive, receiver),
        false, ExpectErrorThenQuit(runloop, Error::NONE));
    runloop.Run();
    EXPECT_EQ(6u, items.size());
    EXPECT_EQ(ComponentState::kChecking, items[0].state);
    EXPECT_EQ("ihfokbkgjpifnbbojhneepfflplebdkc", items[0].id);
    EXPECT_EQ(ComponentState::kCanUpdate, items[1].state);
    EXPECT_EQ("ihfokbkgjpifnbbojhneepfflplebdkc", items[1].id);
    EXPECT_EQ(ComponentState::kDownloading, items[2].state);
    EXPECT_EQ("ihfokbkgjpifnbbojhneepfflplebdkc", items[2].id);
    EXPECT_EQ(ComponentState::kDownloading, items[3].state);
    EXPECT_EQ("ihfokbkgjpifnbbojhneepfflplebdkc", items[3].id);
    EXPECT_EQ(ComponentState::kUpdating, items[4].state);
    EXPECT_EQ("ihfokbkgjpifnbbojhneepfflplebdkc", items[4].id);
    EXPECT_EQ(ComponentState::kUpdated, items[5].state);
    EXPECT_EQ("ihfokbkgjpifnbbojhneepfflplebdkc", items[5].id);
  }

  {
    std::vector<CrxUpdateItem> items;
    auto receiver = base::MakeRefCounted<MockCrxStateChangeReceiver>();
    EXPECT_CALL(*receiver, Receive(_))
        .WillRepeatedly(
            [&items](const CrxUpdateItem& item) { items.push_back(item); });

    base::RunLoop runloop;
    update_client->Update(
        ids, data_callback_mock,
        base::BindRepeating(&MockCrxStateChangeReceiver::Receive, receiver),
        false, ExpectErrorThenQuit(runloop, Error::NONE));
    runloop.Run();

    EXPECT_EQ(8u, items.size());
    EXPECT_EQ(ComponentState::kChecking, items[0].state);
    EXPECT_EQ("ihfokbkgjpifnbbojhneepfflplebdkc", items[0].id);
    EXPECT_EQ(ComponentState::kCanUpdate, items[1].state);
    EXPECT_EQ("ihfokbkgjpifnbbojhneepfflplebdkc", items[1].id);
    EXPECT_EQ(ComponentState::kDownloading, items[2].state);
    EXPECT_EQ("ihfokbkgjpifnbbojhneepfflplebdkc", items[2].id);
    EXPECT_EQ(ComponentState::kDownloading, items[3].state);
    EXPECT_EQ("ihfokbkgjpifnbbojhneepfflplebdkc", items[3].id);
    EXPECT_EQ(ComponentState::kDownloading, items[4].state);
    EXPECT_EQ("ihfokbkgjpifnbbojhneepfflplebdkc", items[4].id);
    EXPECT_EQ(ComponentState::kDownloading, items[5].state);
    EXPECT_EQ("ihfokbkgjpifnbbojhneepfflplebdkc", items[5].id);
    EXPECT_EQ(ComponentState::kUpdating, items[6].state);
    EXPECT_EQ("ihfokbkgjpifnbbojhneepfflplebdkc", items[6].id);
    EXPECT_EQ(ComponentState::kUpdated, items[7].state);
    EXPECT_EQ("ihfokbkgjpifnbbojhneepfflplebdkc", items[7].id);
  }
}

// Tests the queuing of update checks. In this scenario, two update checks are
// done for one CRX. The second update check call is queued up and will run
// after the first check has completed. The CRX has no updates.
TEST_F(UpdateClientTest, OneCrxNoUpdateQueuedCall) {
  class DataCallbackMock {
   public:
    static void Callback(
        const std::vector<std::string>& ids,
        base::OnceCallback<
            void(const std::vector<std::optional<CrxComponent>>&)> callback) {
      CrxComponent crx;
      crx.app_id = "jebgalgnebhfojomionfpkfelancnnkf";
      crx.name = "test_jebg";
      crx.pk_hash = base::ToVector(jebg_hash);
      crx.version = base::Version("0.9");
      crx.installer = base::MakeRefCounted<TestInstaller>();
      crx.crx_format_requirement = crx_file::VerifierFormat::CRX3;
      std::move(callback).Run({crx});
    }
  };

  class MockUpdateChecker : public UpdateChecker {
   public:
    MockUpdateChecker() = default;

    void CheckForUpdates(
        scoped_refptr<UpdateContext> context,
        const base::flat_map<std::string, std::string>& additional_attributes,
        UpdateCheckCallback update_check_callback) override {
      EXPECT_FALSE(context->session_id.empty());
      EXPECT_EQ(1u, context->components_to_check_for_updates.size());
      const std::string id = "jebgalgnebhfojomionfpkfelancnnkf";
      EXPECT_EQ(id, context->components_to_check_for_updates.front());
      EXPECT_EQ(1u, context->components.count(id));

      auto& component = context->components.at(id);

      EXPECT_FALSE(component->is_foreground());

      ProtocolParser::App result;
      result.app_id = id;
      result.status = "noupdate";
      ProtocolParser::Results results;
      results.apps.push_back(result);

      base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
          FROM_HERE, base::BindOnce(std::move(update_check_callback), results,
                                    ErrorCategory::kNone, 0, 0));
    }
  };
  MockUpdateCheckerFactory<MockUpdateChecker> mock_update_checker_factory;

  class MockCrxDownloader : public CrxDownloader {
   public:
    MockCrxDownloader() = default;

   private:
    ~MockCrxDownloader() override = default;

    base::OnceClosure DoStartDownload(const GURL& url) override {
      ADD_FAILURE();
      return base::DoNothing();
    }
  };

  class MockPingManager : public MockPingManagerImpl {
   public:
    explicit MockPingManager(scoped_refptr<Configurator> config)
        : MockPingManagerImpl(config) {}

   protected:
    ~MockPingManager() override { EXPECT_TRUE(ping_data().empty()); }
  };

  SetMockCrxDownloader<MockCrxDownloader>();
  scoped_refptr<UpdateClient> update_client =
      base::MakeRefCounted<UpdateClientImpl>(
          config(), base::MakeRefCounted<MockPingManager>(config()),
          mock_update_checker_factory.GetFactory());

  MockObserver observer(update_client);
  {
    InSequence seq;
    EXPECT_CALL(observer, OnEvent(Truly([](const CrxUpdateItem& item) {
                  return item.id == "jebgalgnebhfojomionfpkfelancnnkf" &&
                         item.state == ComponentState::kChecking;
                })));
    EXPECT_CALL(observer, OnEvent(Truly([](const CrxUpdateItem& item) {
                  return item.id == "jebgalgnebhfojomionfpkfelancnnkf" &&
                         item.state == ComponentState::kUpToDate;
                })));
    EXPECT_CALL(observer, OnEvent(Truly([](const CrxUpdateItem& item) {
                  return item.id == "jebgalgnebhfojomionfpkfelancnnkf" &&
                         item.state == ComponentState::kChecking;
                })));
    EXPECT_CALL(observer, OnEvent(Truly([](const CrxUpdateItem& item) {
                  return item.id == "jebgalgnebhfojomionfpkfelancnnkf" &&
                         item.state == ComponentState::kUpToDate;
                })));
  }

  std::vector<CrxUpdateItem> items1;
  auto receiver1 = base::MakeRefCounted<MockCrxStateChangeReceiver>();
  EXPECT_CALL(*receiver1, Receive(_))
      .WillRepeatedly(
          [&items1](const CrxUpdateItem& item) { items1.push_back(item); });

  std::vector<CrxUpdateItem> items2;
  auto receiver2 = base::MakeRefCounted<MockCrxStateChangeReceiver>();
  EXPECT_CALL(*receiver2, Receive(_))
      .WillRepeatedly(
          [&items2](const CrxUpdateItem& item) { items2.push_back(item); });

  const std::vector<std::string> ids = {"jebgalgnebhfojomionfpkfelancnnkf"};
  update_client->Update(
      ids, base::BindOnce(&DataCallbackMock::Callback),
      base::BindRepeating(&MockCrxStateChangeReceiver::Receive, receiver1),
      false, ExpectError(Error::NONE));
  update_client->Update(
      ids, base::BindOnce(&DataCallbackMock::Callback),
      base::BindRepeating(&MockCrxStateChangeReceiver::Receive, receiver2),
      false, ExpectErrorThenQuit(runloop_, Error::NONE));
  runloop_.Run();

  EXPECT_EQ(2u, items1.size());
  EXPECT_EQ(ComponentState::kChecking, items1[0].state);
  EXPECT_EQ("jebgalgnebhfojomionfpkfelancnnkf", items1[0].id);
  EXPECT_EQ(ComponentState::kUpToDate, items1[1].state);
  EXPECT_EQ("jebgalgnebhfojomionfpkfelancnnkf", items1[1].id);

  EXPECT_EQ(2u, items2.size());
  EXPECT_EQ(ComponentState::kChecking, items2[0].state);
  EXPECT_EQ("jebgalgnebhfojomionfpkfelancnnkf", items2[0].id);
  EXPECT_EQ(ComponentState::kUpToDate, items2[1].state);
  EXPECT_EQ("jebgalgnebhfojomionfpkfelancnnkf", items2[1].id);
}

// Tests the install of one CRX. Tests the installer is invoked with the
// run and arguments values of the manifest object. Tests that "pv" and "fp"
// are persisted.
TEST_F(UpdateClientTest, OneCrxInstall) {
  class DataCallbackMock {
   public:
    static void Callback(
        const std::vector<std::string>& ids,
        base::OnceCallback<
            void(const std::vector<std::optional<CrxComponent>>&)> callback) {
      CrxComponent crx;
      crx.app_id = "jebgalgnebhfojomionfpkfelancnnkf";
      crx.name = "test_jebg";
      crx.pk_hash = base::ToVector(jebg_hash);
      crx.version = base::Version("0.0");
      crx.installer = base::MakeRefCounted<TestInstaller>();
      crx.crx_format_requirement = crx_file::VerifierFormat::CRX3;
      std::move(callback).Run({crx});
    }
  };

  MockUpdateCheckerFactory<
      MockUpdateCheckerImpl<UpdateCheckerOptionsOneCrxInstall>>
      mock_update_checker_factory;

  class MockCrxDownloader : public CrxDownloader {
   public:
    MockCrxDownloader() = default;

   private:
    ~MockCrxDownloader() override = default;

    base::OnceClosure DoStartDownload(const GURL& url) override {
      DownloadMetrics download_metrics;
      base::FilePath path;
      Result result;
      if (url.GetPath() == "/download/jebgalgnebhfojomionfpkfelancnnkf.crx") {
        download_metrics.url = url;
        download_metrics.downloader = DownloadMetrics::kNone;
        download_metrics.error = 0;
        download_metrics.downloaded_bytes = 1015;
        download_metrics.total_bytes = 1015;
        download_metrics.download_time_ms = 1000;

        EXPECT_TRUE(MakeTestFile(
            GetTestFilePath("jebgalgnebhfojomionfpkfelancnnkf.crx"), &path));

        result.error = 0;
        result.response = path;
      } else {
        ADD_FAILURE();
      }

      base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
          FROM_HERE, base::BindOnce(&MockCrxDownloader::OnDownloadProgress,
                                    base::Unretained(this),
                                    download_metrics.downloaded_bytes,
                                    download_metrics.total_bytes));

      base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
          FROM_HERE, base::BindOnce(&MockCrxDownloader::OnDownloadComplete,
                                    base::Unretained(this), true, result,
                                    download_metrics));
      return base::DoNothing();
    }
  };

  class MockPingManager : public MockPingManagerImpl {
   public:
    explicit MockPingManager(scoped_refptr<Configurator> config)
        : MockPingManagerImpl(config) {}

   protected:
    ~MockPingManager() override {
      const auto ping_data = MockPingManagerImpl::terminal_ping_data();
      EXPECT_EQ(1u, ping_data.size());
      EXPECT_EQ("jebgalgnebhfojomionfpkfelancnnkf", ping_data[0].id);
      EXPECT_EQ(base::Version("0.0"), ping_data[0].previous_version);
      EXPECT_EQ(base::Version("1.0"), ping_data[0].next_version);
      EXPECT_EQ(0, static_cast<int>(ping_data[0].error_category));
      EXPECT_EQ(0, ping_data[0].error_code);
    }
  };

  SetMockCrxDownloader<MockCrxDownloader>();
  scoped_refptr<UpdateClient> update_client =
      base::MakeRefCounted<UpdateClientImpl>(
          config(), base::MakeRefCounted<MockPingManager>(config()),
          mock_update_checker_factory.GetFactory());
  {
    EXPECT_FALSE(config()->GetPrefService()->FindPreference(
        "updateclientdata.apps.jebgalgnebhfojomionfpkfelancnnkf.pv"));
    EXPECT_FALSE(config()->GetPrefService()->FindPreference(
        "updateclientdata.apps.jebgalgnebhfojomionfpkfelancnnkf.fp"));
  }

  MockObserver observer(update_client);
  {
    InSequence seq;
    EXPECT_CALL(observer, OnEvent(Truly([](const CrxUpdateItem& item) {
                  return item.id == "jebgalgnebhfojomionfpkfelancnnkf" &&
                         item.state == ComponentState::kChecking;
                })));
    EXPECT_CALL(observer, OnEvent(Truly([](const CrxUpdateItem& item) {
                  return item.id == "jebgalgnebhfojomionfpkfelancnnkf" &&
                         item.state == ComponentState::kCanUpdate;
                })));
    EXPECT_CALL(observer, OnEvent(Truly([](const CrxUpdateItem& item) {
                  return item.id == "jebgalgnebhfojomionfpkfelancnnkf" &&
                         item.state == ComponentState::kDownloading;
                })))
        .Times(AtLeast(1));
    EXPECT_CALL(observer, OnEvent(Truly([](const CrxUpdateItem& item) {
                  return item.id == "jebgalgnebhfojomionfpkfelancnnkf" &&
                         item.state == ComponentState::kUpdating;
                })));
    EXPECT_CALL(observer, OnEvent(Truly([](const CrxUpdateItem& item) {
                  return item.id == "jebgalgnebhfojomionfpkfelancnnkf" &&
                         item.state == ComponentState::kUpdated;
                })))

        .WillOnce([](const CrxUpdateItem& item) {
          ASSERT_TRUE(item.component);
          const auto* test_installer =
              static_cast<TestInstaller*>(item.component->installer.get());
          EXPECT_EQ("UpdaterSetup.exe", test_installer->install_params()->run);
          EXPECT_EQ("--arg1 --arg2",
                    test_installer->install_params()->arguments);
        });
  }

  std::vector<CrxUpdateItem> items;
  auto receiver = base::MakeRefCounted<MockCrxStateChangeReceiver>();
  EXPECT_CALL(*receiver, Receive(_))
      .WillRepeatedly(
          [&items](const CrxUpdateItem& item) { items.push_back(item); });

  update_client->Install(
      std::string("jebgalgnebhfojomionfpkfelancnnkf"),
      base::BindOnce(&DataCallbackMock::Callback),
      base::BindRepeating(&MockCrxStateChangeReceiver::Receive, receiver),
      ExpectErrorThenQuit(runloop_, Error::NONE));
  runloop_.Run();

  EXPECT_EQ(6u, items.size());
  EXPECT_EQ(ComponentState::kChecking, items[0].state);
  EXPECT_EQ("jebgalgnebhfojomionfpkfelancnnkf", items[0].id);
  EXPECT_EQ(ComponentState::kCanUpdate, items[1].state);
  EXPECT_EQ("jebgalgnebhfojomionfpkfelancnnkf", items[1].id);
  EXPECT_EQ(ComponentState::kDownloading, items[2].state);
  EXPECT_EQ("jebgalgnebhfojomionfpkfelancnnkf", items[2].id);
  EXPECT_EQ(ComponentState::kDownloading, items[3].state);
  EXPECT_EQ("jebgalgnebhfojomionfpkfelancnnkf", items[3].id);
  EXPECT_EQ(ComponentState::kUpdating, items[4].state);
  EXPECT_EQ("jebgalgnebhfojomionfpkfelancnnkf", items[4].id);
  EXPECT_EQ(ComponentState::kUpdated, items[5].state);
  EXPECT_EQ("jebgalgnebhfojomionfpkfelancnnkf", items[5].id);

  const base::DictValue& dict =
      config()->GetPrefService()->GetDict("updateclientdata");
  EXPECT_EQ("1.0", CHECK_DEREF(dict.FindStringByDottedPath(
                       "apps.jebgalgnebhfojomionfpkfelancnnkf.pv")));
}

// Tests the install of one CRX when no component data is provided. This
// results in an install error.
TEST_F(UpdateClientTest, OneCrxInstallNoCrxComponentData) {
  class DataCallbackMock {
   public:
    static void Callback(
        const std::vector<std::string>& ids,
        base::OnceCallback<
            void(const std::vector<std::optional<CrxComponent>>&)> callback) {
      std::move(callback).Run({std::nullopt});
    }
  };

  MockUpdateCheckerFactory<MockUpdateCheckerAlwaysFails>
      mock_update_checker_factory;

  class MockCrxDownloader : public CrxDownloader {
   public:
    MockCrxDownloader() = default;

   private:
    ~MockCrxDownloader() override = default;

    base::OnceClosure DoStartDownload(const GURL& url) override {
      ADD_FAILURE();
      return base::DoNothing();
    }
  };

  class MockPingManager : public MockPingManagerImpl {
   public:
    explicit MockPingManager(scoped_refptr<Configurator> config)
        : MockPingManagerImpl(config) {}

   protected:
    ~MockPingManager() override {
      EXPECT_EQ(0u, MockPingManagerImpl::ping_data().size());
    }
  };

  SetMockCrxDownloader<MockCrxDownloader>();
  scoped_refptr<UpdateClient> update_client =
      base::MakeRefCounted<UpdateClientImpl>(
          config(), base::MakeRefCounted<MockPingManager>(config()),
          mock_update_checker_factory.GetFactory());

  MockObserver observer(update_client);
  {
    InSequence seq;
    EXPECT_CALL(observer, OnEvent(Truly([](const CrxUpdateItem& item) {
                  return item.id == "jebgalgnebhfojomionfpkfelancnnkf" &&
                         item.state == ComponentState::kUpdateError;
                })))

        .WillOnce([](const CrxUpdateItem& item) {
          // Tests that the state of the component when the CrxComponent data
          // is not provided. In this case, the optional |item.component|
          // instance is not present.
          EXPECT_EQ(ComponentState::kUpdateError, item.state);
          EXPECT_EQ("jebgalgnebhfojomionfpkfelancnnkf", item.id);
          EXPECT_FALSE(item.component);
          EXPECT_EQ(ErrorCategory::kService, item.error_category);
          EXPECT_EQ(static_cast<int>(Error::CRX_NOT_FOUND), item.error_code);
          EXPECT_EQ(0, item.extra_code1);
        });
  }

  std::vector<CrxUpdateItem> items;
  auto receiver = base::MakeRefCounted<MockCrxStateChangeReceiver>();
  EXPECT_CALL(*receiver, Receive(_))
      .WillRepeatedly(
          [&items](const CrxUpdateItem& item) { items.push_back(item); });

  update_client->Install(
      std::string("jebgalgnebhfojomionfpkfelancnnkf"),
      base::BindOnce(&DataCallbackMock::Callback),
      base::BindRepeating(&MockCrxStateChangeReceiver::Receive, receiver),
      ExpectErrorThenQuit(runloop_, Error::NONE));
  runloop_.Run();

  EXPECT_EQ(1u, items.size());
  EXPECT_EQ(ComponentState::kUpdateError, items[0].state);
  EXPECT_EQ("jebgalgnebhfojomionfpkfelancnnkf", items[0].id);
}

// Tests that overlapping installs of the same CRX result in an error.
TEST_F(UpdateClientTest, ConcurrentInstallSameCRX) {
  class DataCallbackMock {
   public:
    static void Callback(
        const std::vector<std::string>& ids,
        base::OnceCallback<
            void(const std::vector<std::optional<CrxComponent>>&)> callback) {
      CrxComponent crx;
      crx.app_id = "jebgalgnebhfojomionfpkfelancnnkf";
      crx.name = "test_jebg";
      crx.pk_hash = base::ToVector(jebg_hash);
      crx.version = base::Version("0.0");
      crx.installer = base::MakeRefCounted<TestInstaller>();
      crx.crx_format_requirement = crx_file::VerifierFormat::CRX3;
      std::move(callback).Run({crx});
    }
  };

  class MockUpdateChecker : public UpdateChecker {
   public:
    MockUpdateChecker() = default;

    void CheckForUpdates(
        scoped_refptr<UpdateContext> context,
        const base::flat_map<std::string, std::string>& additional_attributes,
        UpdateCheckCallback update_check_callback) override {
      EXPECT_FALSE(context->session_id.empty());
      EXPECT_EQ(1u, context->components_to_check_for_updates.size());
      const std::string id = "jebgalgnebhfojomionfpkfelancnnkf";
      EXPECT_EQ(id, context->components_to_check_for_updates.front());
      EXPECT_EQ(1u, context->components.count(id));

      ProtocolParser::App result;
      result.app_id = id;
      result.status = "noupdate";

      ProtocolParser::Results results;
      results.apps.push_back(result);

      // Verify that calling Install sets |is_foreground| for the component.
      EXPECT_TRUE(context->components.at(id)->is_foreground());

      base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
          FROM_HERE, base::BindOnce(std::move(update_check_callback), results,
                                    ErrorCategory::kNone, 0, 0));
    }
  };
  MockUpdateCheckerFactory<MockUpdateChecker> mock_update_checker_factory;

  class MockCrxDownloader : public CrxDownloader {
   public:
    MockCrxDownloader() = default;

   private:
    ~MockCrxDownloader() override = default;

    base::OnceClosure DoStartDownload(const GURL& url) override {
      ADD_FAILURE();
      return base::DoNothing();
    }
  };

  class MockPingManager : public MockPingManagerImpl {
   public:
    explicit MockPingManager(scoped_refptr<Configurator> config)
        : MockPingManagerImpl(config) {}

   protected:
    ~MockPingManager() override { EXPECT_TRUE(ping_data().empty()); }
  };

  SetMockCrxDownloader<MockCrxDownloader>();
  scoped_refptr<UpdateClient> update_client =
      base::MakeRefCounted<UpdateClientImpl>(
          config(), base::MakeRefCounted<MockPingManager>(config()),
          mock_update_checker_factory.GetFactory());

  MockObserver observer(update_client);
  EXPECT_CALL(observer, OnEvent(Truly([](const CrxUpdateItem& item) {
                return item.id == "jebgalgnebhfojomionfpkfelancnnkf" &&
                       item.state == ComponentState::kChecking;
              })));
  EXPECT_CALL(observer, OnEvent(Truly([](const CrxUpdateItem& item) {
                return item.id == "jebgalgnebhfojomionfpkfelancnnkf" &&
                       item.state == ComponentState::kUpToDate;
              })));

  std::vector<CrxUpdateItem> items1;
  auto receiver1 = base::MakeRefCounted<MockCrxStateChangeReceiver>();
  EXPECT_CALL(*receiver1, Receive(_))
      .WillRepeatedly(
          [&items1](const CrxUpdateItem& item) { items1.push_back(item); });

  std::vector<CrxUpdateItem> items2;
  auto receiver2 = base::MakeRefCounted<MockCrxStateChangeReceiver>();
  EXPECT_CALL(*receiver2, Receive(_))
      .WillRepeatedly(
          [&items2](const CrxUpdateItem& item) { items2.push_back(item); });

  base::RepeatingClosure barrier_quit_closure =
      BarrierClosure(2, runloop_.QuitClosure());

  update_client->Install(
      std::string("jebgalgnebhfojomionfpkfelancnnkf"),
      base::BindOnce(&DataCallbackMock::Callback),
      base::BindRepeating(&MockCrxStateChangeReceiver::Receive, receiver1),
      ExpectErrorThenQuit(barrier_quit_closure, Error::NONE));
  update_client->Install(
      std::string("jebgalgnebhfojomionfpkfelancnnkf"),
      base::BindOnce(&DataCallbackMock::Callback),
      base::BindRepeating(&MockCrxStateChangeReceiver::Receive, receiver2),
      ExpectErrorThenQuit(barrier_quit_closure, Error::UPDATE_IN_PROGRESS));
  runloop_.Run();

  EXPECT_EQ(2u, items1.size());
  EXPECT_EQ(ComponentState::kChecking, items1[0].state);
  EXPECT_EQ("jebgalgnebhfojomionfpkfelancnnkf", items1[0].id);
  EXPECT_EQ(ComponentState::kUpToDate, items1[1].state);
  EXPECT_EQ("jebgalgnebhfojomionfpkfelancnnkf", items1[1].id);

  EXPECT_TRUE(items2.empty());
}

// Tests that UpdateClient::Update returns Error::INVALID_ARGUMENT when
// the |ids| parameter is empty.
TEST_F(UpdateClientTest, EmptyIdList) {
  class DataCallbackMock {
   public:
    static void Callback(
        const std::vector<std::string>& ids,
        base::OnceCallback<
            void(const std::vector<std::optional<CrxComponent>>&)> callback) {
      std::move(callback).Run({});
    }
  };

  class MockCrxDownloader : public CrxDownloader {
   public:
    MockCrxDownloader() = default;

   private:
    ~MockCrxDownloader() override = default;

    base::OnceClosure DoStartDownload(const GURL& url) override {
      ADD_FAILURE();
      return base::DoNothing();
    }
  };
  MockUpdateCheckerFactory<MockUpdateCheckerAlwaysFails>
      mock_update_checker_factory;

  class MockPingManager : public MockPingManagerImpl {
   public:
    explicit MockPingManager(scoped_refptr<Configurator> config)
        : MockPingManagerImpl(config) {}

   protected:
    ~MockPingManager() override { EXPECT_TRUE(ping_data().empty()); }
  };

  SetMockCrxDownloader<MockCrxDownloader>();
  scoped_refptr<UpdateClient> update_client =
      base::MakeRefCounted<UpdateClientImpl>(
          config(), base::MakeRefCounted<MockPingManager>(config()),
          mock_update_checker_factory.GetFactory());

  const std::vector<std::string> empty_id_list;
  update_client->Update(empty_id_list,
                        base::BindOnce(&DataCallbackMock::Callback), {}, false,
                        ExpectErrorThenQuit(runloop_, Error::INVALID_ARGUMENT));
  runloop_.Run();
}

TEST_F(UpdateClientTest, DiskFull) {
  class DataCallbackMock {
   public:
    static void Callback(
        const std::vector<std::string>& ids,
        base::OnceCallback<
            void(const std::vector<std::optional<CrxComponent>>&)> callback) {
      CrxComponent crx1;
      crx1.app_id = "jebgalgnebhfojomionfpkfelancnnkf";
      crx1.name = "test_jebg";
      crx1.pk_hash = base::ToVector(jebg_hash);
      crx1.version = base::Version("0.9");
      crx1.installer = base::MakeRefCounted<TestInstaller>();
      crx1.crx_format_requirement = crx_file::VerifierFormat::CRX3;

      std::move(callback).Run({crx1});
    }
  };

  MockUpdateCheckerFactory<
      MockUpdateCheckerImpl<UpdateCheckerOptionsOneCrxInstallDiskFull>>
      mock_update_checker_factory;

  class MockCrxDownloader : public CrxDownloader {
   public:
    MockCrxDownloader() = default;

   private:
    ~MockCrxDownloader() override = default;

    base::OnceClosure DoStartDownload(const GURL& url) override {
      ADD_FAILURE();
      return base::DoNothing();
    }
  };

  class MockPingManager : public MockPingManagerImpl {
   public:
    explicit MockPingManager(scoped_refptr<Configurator> config)
        : MockPingManagerImpl(config) {}

   protected:
    ~MockPingManager() override {
      const auto ping_data = MockPingManagerImpl::terminal_ping_data();
      EXPECT_EQ(1u, ping_data.size());
      EXPECT_EQ("jebgalgnebhfojomionfpkfelancnnkf", ping_data[0].id);
      EXPECT_EQ(base::Version("0.9"), ping_data[0].previous_version);
      EXPECT_EQ(base::Version("1.0"), ping_data[0].next_version);
      EXPECT_EQ(1, static_cast<int>(ping_data[0].error_category));
      EXPECT_EQ(static_cast<int>(CrxDownloaderError::DISK_FULL),
                ping_data[0].error_code);
    }
  };

  SetMockCrxDownloader<MockCrxDownloader>();
  scoped_refptr<UpdateClient> update_client =
      base::MakeRefCounted<UpdateClientImpl>(
          config(), base::MakeRefCounted<MockPingManager>(config()),
          mock_update_checker_factory.GetFactory());

  MockObserver observer(update_client);
  {
    InSequence seq;
    EXPECT_CALL(observer, OnEvent(Truly([](const CrxUpdateItem& item) {
                  return item.id == "jebgalgnebhfojomionfpkfelancnnkf" &&
                         item.state == ComponentState::kChecking;
                })));
    EXPECT_CALL(observer, OnEvent(Truly([](const CrxUpdateItem& item) {
                  return item.id == "jebgalgnebhfojomionfpkfelancnnkf" &&
                         item.state == ComponentState::kCanUpdate;
                })));
    EXPECT_CALL(observer, OnEvent(Truly([](const CrxUpdateItem& item) {
                  return item.id == "jebgalgnebhfojomionfpkfelancnnkf" &&
                         item.state == ComponentState::kUpdateError;
                })));
  }

  std::vector<CrxUpdateItem> items;
  auto receiver = base::MakeRefCounted<MockCrxStateChangeReceiver>();
  EXPECT_CALL(*receiver, Receive(_))
      .WillRepeatedly(
          [&items](const CrxUpdateItem& item) { items.push_back(item); });

  const std::vector<std::string> ids = {"jebgalgnebhfojomionfpkfelancnnkf"};
  update_client->Update(
      ids, base::BindOnce(&DataCallbackMock::Callback),
      base::BindRepeating(&MockCrxStateChangeReceiver::Receive, receiver),
      false, ExpectErrorThenQuit(runloop_, Error::NONE));
  runloop_.Run();

  EXPECT_EQ(3u, items.size());
  EXPECT_EQ(ComponentState::kChecking, items[0].state);
  EXPECT_EQ("jebgalgnebhfojomionfpkfelancnnkf", items[0].id);
  EXPECT_EQ(ComponentState::kCanUpdate, items[1].state);
  EXPECT_EQ("jebgalgnebhfojomionfpkfelancnnkf", items[1].id);
  EXPECT_EQ(ComponentState::kUpdateError, items[2].state);
  EXPECT_EQ("jebgalgnebhfojomionfpkfelancnnkf", items[2].id);
}

TEST_F(UpdateClientTest, DiskFullDiff) {
  class DataCallbackMock : public base::RefCountedThreadSafe<DataCallbackMock> {
   public:
    DataCallbackMock() {
      installer_->set_installer_progress_samples({-1, 50, 100});
    }

    void Callback(
        const std::vector<std::string>& ids,
        base::OnceCallback<
            void(const std::vector<std::optional<CrxComponent>>&)> callback) {
      ++num_calls_;

      CrxComponent crx;
      crx.app_id = "ihfokbkgjpifnbbojhneepfflplebdkc";
      crx.name = "test_ihfo";
      crx.pk_hash = base::ToVector(ihfo_hash);
      crx.installer = installer_;
      crx.crx_format_requirement = crx_file::VerifierFormat::CRX3;
      if (num_calls_ == 1) {
        crx.version = base::Version("0.8");
      } else if (num_calls_ == 2) {
        crx.version = base::Version("1.0");
      } else {
        ADD_FAILURE();
      }

      std::move(callback).Run({crx});
    }

   private:
    friend class base::RefCountedThreadSafe<DataCallbackMock>;
    ~DataCallbackMock() = default;

    int num_calls_ = 0;
    scoped_refptr<VersionedTestInstaller> installer_ =
        base::MakeRefCounted<VersionedTestInstaller>();
  };
  auto data_callback_mock = MakeMockCallback<DataCallbackMock>();

  class MockUpdateChecker : public UpdateChecker {
   public:
    explicit MockUpdateChecker(int num_calls) : num_calls_(num_calls) {}

    void CheckForUpdates(
        scoped_refptr<UpdateContext> context,
        const base::flat_map<std::string, std::string>& additional_attributes,
        UpdateCheckCallback update_check_callback) override {
      base::expected<ProtocolParser::Results, std::string> results;
      if (num_calls_ == 1) {
        results = ProtocolParserJSON::ParseJSON(R"()]}'
{
  "response": {
    "protocol": "4.0",
    "apps": [
      {
        "appid": "ihfokbkgjpifnbbojhneepfflplebdkc",
        "status": "ok",
        "updatecheck": {
          "status": "ok",
          "nextversion": "1.0",
          "pipelines": [
            {
              "operations": [
                {
                  "type": "download",
                  "urls": [
                    {
                      "url": "http://localhost/download/ihfokbkgjpifnbbojhneepfflplebdkc_1.crx"
                    }
                  ],
                  "out": {
                    "sha256": "8f5aa190311237cae00675af87ff457f278cd1a05895470ac5d46647d4a3c2ea"
                  },
                  "size": 54014
                },
                {
                  "type": "crx3",
                  "in": {
                    "sha256": "8f5aa190311237cae00675af87ff457f278cd1a05895470ac5d46647d4a3c2ea"
                  }
                }
              ]
            }
          ]
        }
      }
    ]
  }
})");
      } else if (num_calls_ == 2) {
        context->get_available_space = base::BindRepeating(
            [](const base::FilePath&) -> int64_t { return 0; });

        results = ProtocolParserJSON::ParseJSON(R"()]}'
{
  "response": {
    "protocol": "4.0",
    "apps": [
      {
        "appid": "ihfokbkgjpifnbbojhneepfflplebdkc",
        "status": "ok",
        "updatecheck": {
          "status": "ok",
          "nextversion": "2.0",
          "pipelines": [
            {
              "operations": [
                {
                  "type": "download",
                  "urls": [
                    {
                      "url": "http://localhost/download/ihfokbkgjpifnbbojhneepfflplebdkc_1to2.puff"
                    }
                  ],
                  "out": {
                    "sha256": "f2254da51fa2478a8ba90e58e1c28e24033ec7841015eebf1c82e31b957c44b2"
                  },
                  "size": 1680
                },
                {
                  "type": "puff",
                  "previous": {
                    "sha256": "8f5aa190311237cae00675af87ff457f278cd1a05895470ac5d46647d4a3c2ea"
                  },
                  "out": {
                    "sha256": "c87d8742c3ff3d7a0cb6f3c91aa2fcf3dea63618086a7db1c5be5300e1d4d6b6"
                  }
                },
                {
                  "type": "crx3",
                  "in": {
                    "sha256": "c87d8742c3ff3d7a0cb6f3c91aa2fcf3dea63618086a7db1c5be5300e1d4d6b6"
                  }
                }
              ]
            },
            {
              "operations": [
                {
                  "type": "download",
                  "urls": [
                    {
                      "url": "http://localhost/download/ihfokbkgjpifnbbojhneepfflplebdkc_2.crx"
                    }
                  ],
                  "out": {
                    "sha256": "c87d8742c3ff3d7a0cb6f3c91aa2fcf3dea63618086a7db1c5be5300e1d4d6b6"
                  },
                  "size": 54409
                },
                {
                  "type": "crx3",
                  "in": {
                    "sha256": "c87d8742c3ff3d7a0cb6f3c91aa2fcf3dea63618086a7db1c5be5300e1d4d6b6"
                  }
                }
              ]
            }
          ]
        }
      }
    ]
  }
})");
      } else {
        ADD_FAILURE();
      }
      EXPECT_TRUE(results.has_value()) << results.error();
      EXPECT_FALSE(context->session_id.empty());
      EXPECT_EQ(context->components_to_check_for_updates.size(), 1u);
      base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
          FROM_HERE,
          base::BindOnce(std::move(update_check_callback), results.value(),
                         ErrorCategory::kNone, 0, 0));
    }

   private:
    const int num_calls_;
  };
  MockUpdateCheckerFactory<MockUpdateChecker> mock_update_checker_factory;

  class MockCrxDownloader : public CrxDownloader {
   public:
    MockCrxDownloader() = default;

   private:
    ~MockCrxDownloader() override = default;

    base::OnceClosure DoStartDownload(const GURL& url) override {
      DownloadMetrics download_metrics;
      base::FilePath path;
      Result result;
      if (url.GetPath() == "/download/ihfokbkgjpifnbbojhneepfflplebdkc_1.crx") {
        download_metrics.url = url;
        download_metrics.downloader = DownloadMetrics::kNone;
        download_metrics.error = 0;
        download_metrics.downloaded_bytes = 54014;
        download_metrics.total_bytes = 54014;
        download_metrics.download_time_ms = 2000;

        EXPECT_TRUE(MakeTestFile(
            GetTestFilePath("ihfokbkgjpifnbbojhneepfflplebdkc_1.crx"), &path));

        result.error = 0;
        result.response = path;
      } else {
        ADD_FAILURE();
      }

      base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
          FROM_HERE, base::BindOnce(&MockCrxDownloader::OnDownloadProgress,
                                    base::Unretained(this),
                                    download_metrics.downloaded_bytes / 2,
                                    download_metrics.total_bytes));

      base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
          FROM_HERE, base::BindOnce(&MockCrxDownloader::OnDownloadProgress,
                                    base::Unretained(this),
                                    download_metrics.downloaded_bytes,
                                    download_metrics.total_bytes));

      base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
          FROM_HERE, base::BindOnce(&MockCrxDownloader::OnDownloadComplete,
                                    base::Unretained(this), true, result,
                                    download_metrics));
      return base::DoNothing();
    }
  };

  class MockPingManager : public MockPingManagerImpl {
   public:
    explicit MockPingManager(scoped_refptr<Configurator> config)
        : MockPingManagerImpl(config) {}

   protected:
    ~MockPingManager() override {
      const auto ping_data = MockPingManagerImpl::terminal_ping_data();
      EXPECT_EQ(2u, ping_data.size());
      EXPECT_EQ("ihfokbkgjpifnbbojhneepfflplebdkc", ping_data[0].id);
      EXPECT_EQ(base::Version("0.8"), ping_data[0].previous_version);
      EXPECT_EQ(base::Version("1.0"), ping_data[0].next_version);
      EXPECT_EQ(0, static_cast<int>(ping_data[0].error_category));
      EXPECT_EQ(0, ping_data[0].error_code);
      EXPECT_EQ("ihfokbkgjpifnbbojhneepfflplebdkc", ping_data[1].id);
      EXPECT_EQ(base::Version("1.0"), ping_data[1].previous_version);
      EXPECT_EQ(base::Version("2.0"), ping_data[1].next_version);
      EXPECT_EQ(1, static_cast<int>(ping_data[1].error_category));
      EXPECT_EQ(static_cast<int>(CrxDownloaderError::DISK_FULL),
                ping_data[1].error_code);
    }
  };

  SetMockCrxDownloader<MockCrxDownloader>();
  scoped_refptr<UpdateClient> update_client =
      base::MakeRefCounted<UpdateClientImpl>(
          config(), base::MakeRefCounted<MockPingManager>(config()),
          mock_update_checker_factory.GetFactory());

  MockObserver observer(update_client);
  {
    InSequence seq;
    EXPECT_CALL(observer, OnEvent(Truly([](const CrxUpdateItem& item) {
                  return item.id == "ihfokbkgjpifnbbojhneepfflplebdkc" &&
                         item.state == ComponentState::kChecking;
                })));
    EXPECT_CALL(observer, OnEvent(Truly([](const CrxUpdateItem& item) {
                  return item.id == "ihfokbkgjpifnbbojhneepfflplebdkc" &&
                         item.state == ComponentState::kCanUpdate;
                })));
    EXPECT_CALL(observer, OnEvent(Truly([](const CrxUpdateItem& item) {
                  return item.id == "ihfokbkgjpifnbbojhneepfflplebdkc" &&
                         item.state == ComponentState::kDownloading;
                })))
        .Times(AtLeast(1));
    EXPECT_CALL(observer, OnEvent(Truly([](const CrxUpdateItem& item) {
                  return item.id == "ihfokbkgjpifnbbojhneepfflplebdkc" &&
                         item.state == ComponentState::kUpdating;
                })))
        .Times(AtLeast(1));
    EXPECT_CALL(observer, OnEvent(Truly([](const CrxUpdateItem& item) {
                  return item.id == "ihfokbkgjpifnbbojhneepfflplebdkc" &&
                         item.state == ComponentState::kUpdated;
                })));
    EXPECT_CALL(observer, OnEvent(Truly([](const CrxUpdateItem& item) {
                  return item.id == "ihfokbkgjpifnbbojhneepfflplebdkc" &&
                         item.state == ComponentState::kChecking;
                })));
    EXPECT_CALL(observer, OnEvent(Truly([](const CrxUpdateItem& item) {
                  return item.id == "ihfokbkgjpifnbbojhneepfflplebdkc" &&
                         item.state == ComponentState::kCanUpdate;
                })));
    EXPECT_CALL(observer, OnEvent(Truly([](const CrxUpdateItem& item) {
                  return item.id == "ihfokbkgjpifnbbojhneepfflplebdkc" &&
                         item.state == ComponentState::kUpdateError;
                })));
  }

  const std::vector<std::string> ids = {"ihfokbkgjpifnbbojhneepfflplebdkc"};
  {
    std::vector<CrxUpdateItem> items;
    auto receiver = base::MakeRefCounted<MockCrxStateChangeReceiver>();
    EXPECT_CALL(*receiver, Receive(_))
        .WillRepeatedly(
            [&items](const CrxUpdateItem& item) { items.push_back(item); });

    base::RunLoop runloop;
    update_client->Update(
        ids, data_callback_mock,
        base::BindRepeating(&MockCrxStateChangeReceiver::Receive, receiver),
        false, ExpectErrorThenQuit(runloop, Error::NONE));
    runloop.Run();

    EXPECT_EQ(10u, items.size());
    EXPECT_EQ(ComponentState::kChecking, items[0].state);
    EXPECT_EQ("ihfokbkgjpifnbbojhneepfflplebdkc", items[0].id);
    EXPECT_EQ(ComponentState::kCanUpdate, items[1].state);
    EXPECT_EQ("ihfokbkgjpifnbbojhneepfflplebdkc", items[1].id);
    EXPECT_EQ(ComponentState::kDownloading, items[2].state);
    EXPECT_EQ("ihfokbkgjpifnbbojhneepfflplebdkc", items[2].id);
    EXPECT_EQ(ComponentState::kDownloading, items[3].state);
    EXPECT_EQ("ihfokbkgjpifnbbojhneepfflplebdkc", items[3].id);
    EXPECT_EQ(ComponentState::kDownloading, items[4].state);
    EXPECT_EQ("ihfokbkgjpifnbbojhneepfflplebdkc", items[4].id);
    EXPECT_EQ(ComponentState::kUpdating, items[5].state);
    EXPECT_EQ("ihfokbkgjpifnbbojhneepfflplebdkc", items[5].id);
    EXPECT_EQ(ComponentState::kUpdating, items[6].state);
    EXPECT_EQ("ihfokbkgjpifnbbojhneepfflplebdkc", items[6].id);
    EXPECT_EQ(ComponentState::kUpdating, items[7].state);
    EXPECT_EQ("ihfokbkgjpifnbbojhneepfflplebdkc", items[7].id);
    EXPECT_EQ(ComponentState::kUpdating, items[8].state);
    EXPECT_EQ("ihfokbkgjpifnbbojhneepfflplebdkc", items[8].id);
    EXPECT_EQ(ComponentState::kUpdated, items[9].state);
    EXPECT_EQ("ihfokbkgjpifnbbojhneepfflplebdkc", items[9].id);

    std::vector samples = {-1, -1, -1, -1, -1, -1, -1, 50, 100, 100};
    EXPECT_EQ(items.size(), samples.size());
    for (size_t i = 0; i != items.size(); ++i) {
      EXPECT_EQ(items[i].install_progress, samples[i]);
    }
  }

  {
    std::vector<CrxUpdateItem> items;
    auto receiver = base::MakeRefCounted<MockCrxStateChangeReceiver>();
    EXPECT_CALL(*receiver, Receive(_))
        .WillRepeatedly(
            [&items](const CrxUpdateItem& item) { items.push_back(item); });

    base::RunLoop runloop;
    update_client->Update(
        ids, data_callback_mock,
        base::BindRepeating(&MockCrxStateChangeReceiver::Receive, receiver),
        false, ExpectErrorThenQuit(runloop, Error::NONE));
    runloop.Run();

    EXPECT_EQ(3u, items.size());
    EXPECT_EQ(ComponentState::kChecking, items[0].state);
    EXPECT_EQ("ihfokbkgjpifnbbojhneepfflplebdkc", items[0].id);
    EXPECT_EQ(ComponentState::kCanUpdate, items[1].state);
    EXPECT_EQ("ihfokbkgjpifnbbojhneepfflplebdkc", items[1].id);
    EXPECT_EQ(ComponentState::kUpdateError, items[2].state);
    EXPECT_EQ("ihfokbkgjpifnbbojhneepfflplebdkc", items[2].id);
  }
}

struct SendPingTestCase {
  const int event_type;
  const int result;
  const std::optional<int> error_code;
  const int extra_code1;
  const std::optional<base::Version> previous_version;
  const std::optional<base::Version> next_version;
};

class SendPingTest : public ::testing::WithParamInterface<SendPingTestCase>,
                     public UpdateClientTest {};

INSTANTIATE_TEST_SUITE_P(SendPingTestCases,
                         SendPingTest,
                         ::testing::ValuesIn(std::vector<SendPingTestCase>{
                             // Install ping.
                             {protocol_request::kEventInstall, 1, 2, 3},

                             // Uninstall ping.
                             {protocol_request::kEventUninstall,
                              1,
                              {},
                              10,
                              base::Version("1.2.3.4")},
                         }));

TEST_P(SendPingTest, SendPingTestCases) {
  MockUpdateCheckerFactory<MockUpdateCheckerAlwaysFails>
      mock_update_checker_factory;

  class MockCrxDownloader : public CrxDownloader {
   public:
    static scoped_refptr<CrxDownloader> Create(
        bool is_background_download,
        scoped_refptr<NetworkFetcherFactory> network_fetcher_factory) {
      return nullptr;
    }

    MockCrxDownloader() = default;

   private:
    ~MockCrxDownloader() override = default;

    base::OnceClosure DoStartDownload(const GURL& url) override {
      return base::DoNothing();
    }
  };

  class MockPingManager : public MockPingManagerImpl {
   public:
    explicit MockPingManager(scoped_refptr<Configurator> config)
        : MockPingManagerImpl(config) {}

   protected:
    ~MockPingManager() override {
      EXPECT_EQ(ping_data().size(), 1u);
      EXPECT_EQ(ping_data()[0].id, "jebgalgnebhfojomionfpkfelancnnkf");
      EXPECT_EQ(ping_data()[0].event_type, GetParam().event_type);
      EXPECT_EQ(ping_data()[0].event_result, GetParam().result);
      if (GetParam().error_code) {
        EXPECT_EQ(ping_data()[0].error_code, *GetParam().error_code);
      }
      EXPECT_EQ(ping_data()[0].extra_code1, GetParam().extra_code1);
      if (GetParam().previous_version) {
        EXPECT_EQ(ping_data()[0].previous_version,
                  *GetParam().previous_version);
      }
      if (GetParam().next_version) {
        EXPECT_EQ(ping_data()[0].next_version, *GetParam().next_version);
      }
    }
  };

  SetMockCrxDownloader<MockCrxDownloader>();
  scoped_refptr<UpdateClient> update_client =
      base::MakeRefCounted<UpdateClientImpl>(
          config(), base::MakeRefCounted<MockPingManager>(config()),
          mock_update_checker_factory.GetFactory());

  CrxComponent crx;
  crx.app_id = "jebgalgnebhfojomionfpkfelancnnkf";
  crx.name = "test_jebg";
  crx.version = GetParam().previous_version.value_or(base::Version("1.2.3.4"));
  update_client->SendPing(crx,
                          {.event_type = GetParam().event_type,
                           .result = GetParam().result,
                           .error_code = GetParam().error_code.value_or(0),
                           .extra_code1 = GetParam().extra_code1},
                          ExpectErrorThenQuit(runloop_, Error::NONE));
  runloop_.Run();
}

TEST_F(UpdateClientTest, RetryAfter) {
  class DataCallbackMock {
   public:
    static void Callback(
        const std::vector<std::string>& ids,
        base::OnceCallback<
            void(const std::vector<std::optional<CrxComponent>>&)> callback) {
      CrxComponent crx;
      crx.app_id = "jebgalgnebhfojomionfpkfelancnnkf";
      crx.name = "test_jebg";
      crx.pk_hash = base::ToVector(jebg_hash);
      crx.version = base::Version("0.9");
      crx.installer = base::MakeRefCounted<TestInstaller>();
      crx.crx_format_requirement = crx_file::VerifierFormat::CRX3;
      std::move(callback).Run({crx});
    }
  };

  class MockUpdateChecker : public UpdateChecker {
   public:
    explicit MockUpdateChecker(int num_calls) : num_calls_(num_calls) {}

   private:
    void CheckForUpdates(
        scoped_refptr<UpdateContext> context,
        const base::flat_map<std::string, std::string>& additional_attributes,
        UpdateCheckCallback update_check_callback) override {
      EXPECT_FALSE(context->session_id.empty());

      EXPECT_LE(num_calls_, 3);

      int retry_after_sec(0);
      if (num_calls_ == 1) {
        // Throttle the next call.
        retry_after_sec = 60 * 60;  // 1 hour.
      }

      EXPECT_EQ(1u, context->components_to_check_for_updates.size());
      const std::string id = "jebgalgnebhfojomionfpkfelancnnkf";
      EXPECT_EQ(id, context->components_to_check_for_updates.front());
      EXPECT_EQ(1u, context->components.count(id));

      ProtocolParser::App result;
      result.app_id = id;
      result.status = "noupdate";

      ProtocolParser::Results results;
      results.apps.push_back(result);

      base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
          FROM_HERE, base::BindOnce(std::move(update_check_callback), results,
                                    ErrorCategory::kNone, 0, retry_after_sec));
    }

    const int num_calls_;
  };
  MockUpdateCheckerFactory<MockUpdateChecker> mock_update_checker_factory;

  class MockCrxDownloader : public CrxDownloader {
   public:
    MockCrxDownloader() = default;

   private:
    ~MockCrxDownloader() override = default;

    base::OnceClosure DoStartDownload(const GURL& url) override {
      ADD_FAILURE();
      return base::DoNothing();
    }
  };

  class MockPingManager : public MockPingManagerImpl {
   public:
    explicit MockPingManager(scoped_refptr<Configurator> config)
        : MockPingManagerImpl(config) {}

   protected:
    ~MockPingManager() override { EXPECT_TRUE(ping_data().empty()); }
  };

  SetMockCrxDownloader<MockCrxDownloader>();
  scoped_refptr<UpdateClient> update_client =
      base::MakeRefCounted<UpdateClientImpl>(
          config(), base::MakeRefCounted<MockPingManager>(config()),
          mock_update_checker_factory.GetFactory());

  MockObserver observer(update_client);
  {
    InSequence seq;
    EXPECT_CALL(observer, OnEvent(Truly([](const CrxUpdateItem& item) {
                  return item.id == "jebgalgnebhfojomionfpkfelancnnkf" &&
                         item.state == ComponentState::kChecking;
                })));
    EXPECT_CALL(observer, OnEvent(Truly([](const CrxUpdateItem& item) {
                  return item.id == "jebgalgnebhfojomionfpkfelancnnkf" &&
                         item.state == ComponentState::kUpToDate;
                })));
    EXPECT_CALL(observer, OnEvent(Truly([](const CrxUpdateItem& item) {
                  return item.id == "jebgalgnebhfojomionfpkfelancnnkf" &&
                         item.state == ComponentState::kChecking;
                })));
    EXPECT_CALL(observer, OnEvent(Truly([](const CrxUpdateItem& item) {
                  return item.id == "jebgalgnebhfojomionfpkfelancnnkf" &&
                         item.state == ComponentState::kUpToDate;
                })));
    EXPECT_CALL(observer, OnEvent(Truly([](const CrxUpdateItem& item) {
                  return item.id == "jebgalgnebhfojomionfpkfelancnnkf" &&
                         item.state == ComponentState::kChecking;
                })));
    EXPECT_CALL(observer, OnEvent(Truly([](const CrxUpdateItem& item) {
                  return item.id == "jebgalgnebhfojomionfpkfelancnnkf" &&
                         item.state == ComponentState::kUpToDate;
                })));
  }

  const std::vector<std::string> ids = {"jebgalgnebhfojomionfpkfelancnnkf"};
  {
    // The engine handles this Update call but responds with a valid
    // |retry_after_sec|, which causes subsequent calls to fail.
    base::RunLoop runloop;
    update_client->Update(ids, base::BindOnce(&DataCallbackMock::Callback), {},
                          false, ExpectErrorThenQuit(runloop, Error::NONE));
    runloop.Run();
  }
  {
    // This call will result in a completion callback invoked with
    // Error::ERROR_UPDATE_RETRY_LATER.
    base::RunLoop runloop;
    update_client->Update(ids, base::BindOnce(&DataCallbackMock::Callback), {},
                          false,
                          ExpectErrorThenQuit(runloop, Error::RETRY_LATER));
    runloop.Run();
  }
  {
    // The Install call is handled, and the throttling is reset due to
    // the value of |retry_after_sec| in the completion callback.
    base::RunLoop runloop;
    update_client->Install(std::string("jebgalgnebhfojomionfpkfelancnnkf"),
                           base::BindOnce(&DataCallbackMock::Callback), {},
                           ExpectErrorThenQuit(runloop, Error::NONE));
    runloop.Run();
  }
  {
    // This call succeeds.
    base::RunLoop runloop;
    update_client->Update(ids, base::BindOnce(&DataCallbackMock::Callback), {},
                          false, ExpectErrorThenQuit(runloop, Error::NONE));
    runloop.Run();
  }
}

// Tests the update check for two CRXs scenario. The first component supports
// the group policy to enable updates, and has its updates disabled. The second
// component has an update. The server does not honor the "updatedisabled"
// attribute and returns updates for both components. However, the update for
// the first component is not applied and the client responds with a
// (SERVICE_ERROR, UPDATE_DISABLED)
TEST_F(UpdateClientTest, TwoCrxUpdateOneUpdateDisabled) {
  class DataCallbackMock {
   public:
    static void Callback(
        const std::vector<std::string>& ids,
        base::OnceCallback<
            void(const std::vector<std::optional<CrxComponent>>&)> callback) {
      CrxComponent crx1;
      crx1.app_id = "jebgalgnebhfojomionfpkfelancnnkf";
      crx1.name = "test_jebg";
      crx1.pk_hash = base::ToVector(jebg_hash);
      crx1.version = base::Version("0.9");
      crx1.installer = base::MakeRefCounted<TestInstaller>();
      crx1.crx_format_requirement = crx_file::VerifierFormat::CRX3;
      crx1.updates_enabled = false;

      CrxComponent crx2;
      crx2.app_id = "ihfokbkgjpifnbbojhneepfflplebdkc";
      crx2.name = "test_ihfo";
      crx2.pk_hash = base::ToVector(ihfo_hash);
      crx2.version = base::Version("0.8");
      crx2.installer = base::MakeRefCounted<TestInstaller>();
      crx2.crx_format_requirement = crx_file::VerifierFormat::CRX3;

      std::move(callback).Run({crx1, crx2});
    }
  };

  MockUpdateCheckerFactory<
      MockUpdateCheckerImpl<UpdateCheckerOptionsTwoCrxUpdate>>
      mock_update_checker_factory;

  class MockCrxDownloader : public CrxDownloader {
   public:
    MockCrxDownloader() = default;

   private:
    ~MockCrxDownloader() override = default;

    base::OnceClosure DoStartDownload(const GURL& url) override {
      DownloadMetrics download_metrics;
      base::FilePath path;
      Result result;
      if (url.GetPath() == "/download/ihfokbkgjpifnbbojhneepfflplebdkc_1.crx") {
        download_metrics.url = url;
        download_metrics.downloader = DownloadMetrics::kNone;
        download_metrics.error = 0;
        download_metrics.downloaded_bytes = 54014;
        download_metrics.total_bytes = 54014;
        download_metrics.download_time_ms = 2000;

        EXPECT_TRUE(MakeTestFile(
            GetTestFilePath("ihfokbkgjpifnbbojhneepfflplebdkc_1.crx"), &path));

        result.error = 0;
        result.response = path;
      } else {
        ADD_FAILURE();
      }

      base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
          FROM_HERE, base::BindOnce(&MockCrxDownloader::OnDownloadProgress,
                                    base::Unretained(this),
                                    download_metrics.downloaded_bytes,
                                    download_metrics.total_bytes));

      base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
          FROM_HERE, base::BindOnce(&MockCrxDownloader::OnDownloadComplete,
                                    base::Unretained(this), true, result,
                                    download_metrics));
      return base::DoNothing();
    }
  };

  class MockPingManager : public MockPingManagerImpl {
   public:
    explicit MockPingManager(scoped_refptr<Configurator> config)
        : MockPingManagerImpl(config) {}

   protected:
    ~MockPingManager() override {
      const auto ping_data = MockPingManagerImpl::terminal_ping_data();
      EXPECT_EQ(2u, ping_data.size());
      EXPECT_EQ("jebgalgnebhfojomionfpkfelancnnkf", ping_data[0].id);
      EXPECT_EQ(base::Version("0.9"), ping_data[0].previous_version);
      EXPECT_EQ(base::Version("1.0"), ping_data[0].next_version);
      EXPECT_EQ(4, static_cast<int>(ping_data[0].error_category));
      EXPECT_EQ(2, ping_data[0].error_code);
      EXPECT_EQ("ihfokbkgjpifnbbojhneepfflplebdkc", ping_data[1].id);
      EXPECT_EQ(base::Version("0.8"), ping_data[1].previous_version);
      EXPECT_EQ(base::Version("1.0"), ping_data[1].next_version);
      EXPECT_EQ(0, static_cast<int>(ping_data[1].error_category));
      EXPECT_EQ(0, ping_data[1].error_code);
    }
  };

  // Disables updates for the components declaring support for the group policy.
  SetMockCrxDownloader<MockCrxDownloader>();
  scoped_refptr<UpdateClient> update_client =
      base::MakeRefCounted<UpdateClientImpl>(
          config(), base::MakeRefCounted<MockPingManager>(config()),
          mock_update_checker_factory.GetFactory());

  MockObserver observer(update_client);
  {
    InSequence seq;
    EXPECT_CALL(observer, OnEvent(Truly([](const CrxUpdateItem& item) {
                  return item.id == "jebgalgnebhfojomionfpkfelancnnkf" &&
                         item.state == ComponentState::kChecking;
                })));
    EXPECT_CALL(observer, OnEvent(Truly([](const CrxUpdateItem& item) {
                  return item.id == "jebgalgnebhfojomionfpkfelancnnkf" &&
                         item.state == ComponentState::kCanUpdate;
                })));
    EXPECT_CALL(observer, OnEvent(Truly([](const CrxUpdateItem& item) {
                  return item.id == "jebgalgnebhfojomionfpkfelancnnkf" &&
                         item.state == ComponentState::kUpdateError;
                })));
  }
  {
    InSequence seq;
    EXPECT_CALL(observer, OnEvent(Truly([](const CrxUpdateItem& item) {
                  return item.id == "ihfokbkgjpifnbbojhneepfflplebdkc" &&
                         item.state == ComponentState::kChecking;
                })));
    EXPECT_CALL(observer, OnEvent(Truly([](const CrxUpdateItem& item) {
                  return item.id == "ihfokbkgjpifnbbojhneepfflplebdkc" &&
                         item.state == ComponentState::kCanUpdate;
                })));
    EXPECT_CALL(observer, OnEvent(Truly([](const CrxUpdateItem& item) {
                  return item.id == "ihfokbkgjpifnbbojhneepfflplebdkc" &&
                         item.state == ComponentState::kDownloading;
                })))
        .Times(AtLeast(1));
    EXPECT_CALL(observer, OnEvent(Truly([](const CrxUpdateItem& item) {
                  return item.id == "ihfokbkgjpifnbbojhneepfflplebdkc" &&
                         item.state == ComponentState::kUpdating;
                })));
    EXPECT_CALL(observer, OnEvent(Truly([](const CrxUpdateItem& item) {
                  return item.id == "ihfokbkgjpifnbbojhneepfflplebdkc" &&
                         item.state == ComponentState::kUpdated;
                })));
  }

  std::vector<CrxUpdateItem> items;
  auto receiver = base::MakeRefCounted<MockCrxStateChangeReceiver>();
  EXPECT_CALL(*receiver, Receive(_))
      .WillRepeatedly(
          [&items](const CrxUpdateItem& item) { items.push_back(item); });

  const std::vector<std::string> ids = {"jebgalgnebhfojomionfpkfelancnnkf",
                                        "ihfokbkgjpifnbbojhneepfflplebdkc"};
  update_client->Update(
      ids, base::BindOnce(&DataCallbackMock::Callback),
      base::BindRepeating(&MockCrxStateChangeReceiver::Receive, receiver),
      false, ExpectErrorThenQuit(runloop_, Error::NONE));
  runloop_.Run();

  EXPECT_EQ(9u, items.size());
  EXPECT_EQ(ComponentState::kChecking, items[0].state);
  EXPECT_EQ("jebgalgnebhfojomionfpkfelancnnkf", items[0].id);
  EXPECT_EQ(ComponentState::kChecking, items[1].state);
  EXPECT_EQ("ihfokbkgjpifnbbojhneepfflplebdkc", items[1].id);
  EXPECT_EQ(ComponentState::kCanUpdate, items[2].state);
  EXPECT_EQ("jebgalgnebhfojomionfpkfelancnnkf", items[2].id);
  EXPECT_EQ(ComponentState::kUpdateError, items[3].state);
  EXPECT_EQ("jebgalgnebhfojomionfpkfelancnnkf", items[3].id);
  EXPECT_EQ(ComponentState::kCanUpdate, items[4].state);
  EXPECT_EQ("ihfokbkgjpifnbbojhneepfflplebdkc", items[4].id);
  EXPECT_EQ(ComponentState::kDownloading, items[5].state);
  EXPECT_EQ("ihfokbkgjpifnbbojhneepfflplebdkc", items[5].id);
  EXPECT_EQ(ComponentState::kDownloading, items[6].state);
  EXPECT_EQ("ihfokbkgjpifnbbojhneepfflplebdkc", items[6].id);
  EXPECT_EQ(ComponentState::kUpdating, items[7].state);
  EXPECT_EQ("ihfokbkgjpifnbbojhneepfflplebdkc", items[7].id);
  EXPECT_EQ(ComponentState::kUpdated, items[8].state);
  EXPECT_EQ("ihfokbkgjpifnbbojhneepfflplebdkc", items[8].id);
}

// Tests all ping back events have the correct errorcode and extracode1 set in
// the case of a failed download with a valid http status code
TEST_F(UpdateClientTest, OneCrxUpdateDownloadTimeout) {
  class DataCallbackMock {
   public:
    static void Callback(
        const std::vector<std::string>& ids,
        base::OnceCallback<
            void(const std::vector<std::optional<CrxComponent>>&)> callback) {
      CrxComponent crx;
      crx.app_id = "jebgalgnebhfojomionfpkfelancnnkf";
      crx.name = "test_jebg";
      crx.pk_hash = base::ToVector(jebg_hash);
      crx.version = base::Version("0.9");
      crx.installer = base::MakeRefCounted<TestInstaller>();
      crx.crx_format_requirement = crx_file::VerifierFormat::CRX3;
      std::vector<std::optional<CrxComponent>> component = {crx};
      std::move(callback).Run({component});
    }
  };

  MockUpdateCheckerFactory<
      MockUpdateCheckerImpl<UpdateCheckerOptionsOneCrxUpdate>>
      mock_update_checker_factory;

  class MockCrxDownloader : public CrxDownloader {
   public:
    MockCrxDownloader() = default;

   private:
    ~MockCrxDownloader() override = default;

    base::OnceClosure DoStartDownload(const GURL& url) override {
      DownloadMetrics download_metrics;
      download_metrics.url = url;
      download_metrics.downloader = DownloadMetrics::kNone;
      download_metrics.error = 200;
      download_metrics.extra_code1 = -2147012894;
      download_metrics.downloaded_bytes = 1015 / 2;
      download_metrics.total_bytes = 1015;
      download_metrics.download_time_ms = 1000;

      base::FilePath path;
      EXPECT_TRUE(MakeTestFile(
          GetTestFilePath("jebgalgnebhfojomionfpkfelancnnkf.crx"), &path));

      Result result;
      result.error = 200;
      result.extra_code1 = -2147012894;

      base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
          FROM_HERE, base::BindOnce(&MockCrxDownloader::OnDownloadProgress,
                                    base::Unretained(this),
                                    download_metrics.downloaded_bytes / 2,
                                    download_metrics.total_bytes));

      base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
          FROM_HERE, base::BindOnce(&MockCrxDownloader::OnDownloadComplete,
                                    base::Unretained(this), true, result,
                                    download_metrics));
      return base::DoNothing();
    }
  };

  class MockPingManager : public MockPingManagerImpl {
   public:
    explicit MockPingManager(scoped_refptr<Configurator> config)
        : MockPingManagerImpl(config) {}

   protected:
    ~MockPingManager() override {
      const auto ping_data = MockPingManagerImpl::ping_data();
      EXPECT_EQ(ping_data.size(), 2u);

      auto download_ping = ping_data[0];
      EXPECT_EQ(download_ping.event_type, 14);
      EXPECT_EQ(download_ping.event_result, 0);
      EXPECT_EQ(download_ping.id, "jebgalgnebhfojomionfpkfelancnnkf");
      EXPECT_EQ(download_ping.previous_version, base::Version("0.9"));
      EXPECT_EQ(download_ping.next_version, base::Version("1.0"));
      EXPECT_EQ(download_ping.error_category, ErrorCategory::kNone);
      EXPECT_EQ(download_ping.error_code, 200);
      EXPECT_EQ(download_ping.extra_code1, -2147012894);

      auto update_ping = ping_data[1];
      EXPECT_EQ(update_ping.event_type, 3);
      EXPECT_EQ(update_ping.event_result, 0);
      EXPECT_EQ(update_ping.id, "jebgalgnebhfojomionfpkfelancnnkf");
      EXPECT_EQ(update_ping.previous_version, base::Version("0.9"));
      EXPECT_EQ(update_ping.next_version, base::Version("1.0"));
      EXPECT_EQ(update_ping.error_category, ErrorCategory::kDownload);
      EXPECT_EQ(update_ping.error_code, 200);
      EXPECT_EQ(update_ping.extra_code1, -2147012894);
    }
  };

  SetMockCrxDownloader<MockCrxDownloader>();
  scoped_refptr<UpdateClient> update_client =
      base::MakeRefCounted<UpdateClientImpl>(
          config(), base::MakeRefCounted<MockPingManager>(config()),
          mock_update_checker_factory.GetFactory());

  MockObserver observer(update_client);
  {
    InSequence seq;
    EXPECT_CALL(observer, OnEvent(Truly([](const CrxUpdateItem& item) {
                  return item.id == "jebgalgnebhfojomionfpkfelancnnkf" &&
                         item.state == ComponentState::kChecking;
                })));
    EXPECT_CALL(observer, OnEvent(Truly([](const CrxUpdateItem& item) {
                  return item.id == "jebgalgnebhfojomionfpkfelancnnkf" &&
                         item.state == ComponentState::kCanUpdate;
                })));
    EXPECT_CALL(observer, OnEvent(Truly([](const CrxUpdateItem& item) {
                  return item.id == "jebgalgnebhfojomionfpkfelancnnkf" &&
                         item.state == ComponentState::kDownloading;
                })))
        .Times(AtLeast(2));
    EXPECT_CALL(observer, OnEvent(Truly([](const CrxUpdateItem& item) {
                  return item.id == "jebgalgnebhfojomionfpkfelancnnkf" &&
                         item.state == ComponentState::kUpdateError;
                })))
        .WillOnce([](const CrxUpdateItem& item) {
          EXPECT_EQ(ComponentState::kUpdateError, item.state);
          EXPECT_EQ(1, static_cast<int>(item.error_category));
          EXPECT_EQ(200, item.error_code);
          EXPECT_EQ(-2147012894, item.extra_code1);
        });
  }

  std::vector<CrxUpdateItem> items;
  auto receiver = base::MakeRefCounted<MockCrxStateChangeReceiver>();
  EXPECT_CALL(*receiver, Receive(_))
      .WillRepeatedly(
          [&items](const CrxUpdateItem& item) { items.push_back(item); });

  const std::vector<std::string> ids = {"jebgalgnebhfojomionfpkfelancnnkf"};
  update_client->Update(
      ids, base::BindOnce(&DataCallbackMock::Callback),
      base::BindRepeating(&MockCrxStateChangeReceiver::Receive, receiver),
      false, ExpectErrorThenQuit(runloop_, Error::NONE));
  runloop_.Run();

  EXPECT_EQ(5u, items.size());
  EXPECT_EQ(ComponentState::kChecking, items[0].state);
  EXPECT_EQ("jebgalgnebhfojomionfpkfelancnnkf", items[0].id);
  EXPECT_EQ(ComponentState::kCanUpdate, items[1].state);
  EXPECT_EQ("jebgalgnebhfojomionfpkfelancnnkf", items[1].id);
  EXPECT_EQ(ComponentState::kDownloading, items[2].state);
  EXPECT_EQ("jebgalgnebhfojomionfpkfelancnnkf", items[2].id);
  EXPECT_EQ(ComponentState::kDownloading, items[3].state);
  EXPECT_EQ("jebgalgnebhfojomionfpkfelancnnkf", items[3].id);
  EXPECT_EQ(ComponentState::kUpdateError, items[4].state);
  EXPECT_EQ("jebgalgnebhfojomionfpkfelancnnkf", items[4].id);
}

// Tests the scenario where the update check fails.
TEST_F(UpdateClientTest, OneCrxUpdateCheckFails) {
  class DataCallbackMock {
   public:
    static void Callback(
        const std::vector<std::string>& ids,
        base::OnceCallback<
            void(const std::vector<std::optional<CrxComponent>>&)> callback) {
      CrxComponent crx;
      crx.name = "test_jebg";
      crx.pk_hash = base::ToVector(jebg_hash);
      crx.version = base::Version("0.9");
      crx.installer = base::MakeRefCounted<TestInstaller>();
      crx.crx_format_requirement = crx_file::VerifierFormat::CRX3;
      std::move(callback).Run({crx});
    }
  };

  class MockUpdateChecker : public UpdateChecker {
   public:
    MockUpdateChecker() = default;

    void CheckForUpdates(
        scoped_refptr<UpdateContext> context,
        const base::flat_map<std::string, std::string>& additional_attributes,
        UpdateCheckCallback update_check_callback) override {
      EXPECT_FALSE(context->session_id.empty());
      EXPECT_EQ(1u, context->components_to_check_for_updates.size());
      const std::string id = "jebgalgnebhfojomionfpkfelancnnkf";
      EXPECT_EQ(id, context->components_to_check_for_updates.front());
      EXPECT_EQ(1u, context->components.count(id));
      base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
          FROM_HERE,
          base::BindOnce(std::move(update_check_callback), std::nullopt,
                         ErrorCategory::kUpdateCheck, -1, 0));
    }
  };
  MockUpdateCheckerFactory<MockUpdateChecker> mock_update_checker_factory;

  class MockCrxDownloader : public CrxDownloader {
   public:
    MockCrxDownloader() = default;

   private:
    ~MockCrxDownloader() override = default;

    base::OnceClosure DoStartDownload(const GURL& url) override {
      ADD_FAILURE();
      return base::DoNothing();
    }
  };

  class MockPingManager : public MockPingManagerImpl {
   public:
    explicit MockPingManager(scoped_refptr<Configurator> config)
        : MockPingManagerImpl(config) {}

   protected:
    ~MockPingManager() override { EXPECT_TRUE(ping_data().empty()); }
  };

  SetMockCrxDownloader<MockCrxDownloader>();
  scoped_refptr<UpdateClient> update_client =
      base::MakeRefCounted<UpdateClientImpl>(
          config(), base::MakeRefCounted<MockPingManager>(config()),
          mock_update_checker_factory.GetFactory());

  MockObserver observer(update_client);
  {
    InSequence seq;
    EXPECT_CALL(observer, OnEvent(Truly([](const CrxUpdateItem& item) {
                  return item.id == "jebgalgnebhfojomionfpkfelancnnkf" &&
                         item.state == ComponentState::kChecking;
                })));
    EXPECT_CALL(observer, OnEvent(Truly([](const CrxUpdateItem& item) {
                  return item.id == "jebgalgnebhfojomionfpkfelancnnkf" &&
                         item.state == ComponentState::kUpdateError;
                })))
        .WillOnce([](const CrxUpdateItem& item) {
          EXPECT_EQ(ComponentState::kUpdateError, item.state);
          EXPECT_EQ(5, static_cast<int>(item.error_category));
          EXPECT_EQ(-1, item.error_code);
          EXPECT_EQ(0, item.extra_code1);
        });
  }

  std::vector<CrxUpdateItem> items;
  auto receiver = base::MakeRefCounted<MockCrxStateChangeReceiver>();
  EXPECT_CALL(*receiver, Receive(_))
      .WillRepeatedly(
          [&items](const CrxUpdateItem& item) { items.push_back(item); });

  const std::vector<std::string> ids = {"jebgalgnebhfojomionfpkfelancnnkf"};
  update_client->Update(
      ids, base::BindOnce(&DataCallbackMock::Callback),
      base::BindRepeating(&MockCrxStateChangeReceiver::Receive, receiver),
      false, ExpectErrorThenQuit(runloop_, Error::UPDATE_CHECK_ERROR));
  runloop_.Run();

  EXPECT_EQ(2u, items.size());
  EXPECT_EQ(ComponentState::kChecking, items[0].state);
  EXPECT_EQ("jebgalgnebhfojomionfpkfelancnnkf", items[0].id);
  EXPECT_EQ(ComponentState::kUpdateError, items[1].state);
  EXPECT_EQ("jebgalgnebhfojomionfpkfelancnnkf", items[1].id);
}

// Tests the scenario where the server responds with different values for
// application status.
TEST_F(UpdateClientTest, OneCrxErrorUnknownApp) {
  class DataCallbackMock {
   public:
    static void Callback(
        const std::vector<std::string>& ids,
        base::OnceCallback<
            void(const std::vector<std::optional<CrxComponent>>&)> callback) {
      std::vector<std::optional<CrxComponent>> component;
      {
        CrxComponent crx;
        crx.app_id = "jebgalgnebhfojomionfpkfelancnnkf";
        crx.name = "test_jebg";
        crx.pk_hash = base::ToVector(jebg_hash);
        crx.version = base::Version("0.9");
        crx.installer = base::MakeRefCounted<TestInstaller>();
        crx.crx_format_requirement = crx_file::VerifierFormat::CRX3;
        component.push_back(crx);
      }
      {
        CrxComponent crx;
        crx.app_id = "abagagagagagagagagagagagagagagag";
        crx.name = "test_abag";
        crx.pk_hash = base::ToVector(abag_hash);
        crx.version = base::Version("0.1");
        crx.installer = base::MakeRefCounted<TestInstaller>();
        crx.crx_format_requirement = crx_file::VerifierFormat::CRX3;
        component.push_back(crx);
      }
      {
        CrxComponent crx;
        crx.app_id = "ihfokbkgjpifnbbojhneepfflplebdkc";
        crx.name = "test_ihfo";
        crx.pk_hash = base::ToVector(ihfo_hash);
        crx.version = base::Version("0.2");
        crx.installer = base::MakeRefCounted<TestInstaller>();
        crx.crx_format_requirement = crx_file::VerifierFormat::CRX3;
        component.push_back(crx);
      }
      {
        CrxComponent crx;
        crx.app_id = "gjpmebpgbhcamgdgjcmnjfhggjpgcimm";
        crx.name = "test_gjpm";
        crx.pk_hash = base::ToVector(gjpm_hash);
        crx.version = base::Version("0.3");
        crx.installer = base::MakeRefCounted<TestInstaller>();
        crx.crx_format_requirement = crx_file::VerifierFormat::CRX3;
        component.push_back(crx);
      }
      std::move(callback).Run(component);
    }
  };

  class MockUpdateChecker : public UpdateChecker {
   public:
    MockUpdateChecker() = default;

    void CheckForUpdates(
        scoped_refptr<UpdateContext> context,
        const base::flat_map<std::string, std::string>& additional_attributes,
        UpdateCheckCallback update_check_callback) override {
      EXPECT_FALSE(context->session_id.empty());
      EXPECT_EQ(4u, context->components_to_check_for_updates.size());

      const std::string update_response = R"()]}'
{
  "response": {
    "protocol": "4.0",
    "apps": [
      {
        "appid": "jebgalgnebhfojomionfpkfelancnnkf",
        "status": "error-unknownApplication"
      },
      {
        "appid": "abagagagagagagagagagagagagagagag",
        "status": "restricted"
      },
      {
        "appid": "ihfokbkgjpifnbbojhneepfflplebdkc",
        "status": "error-invalidAppId"
      },
      {
        "appid": "gjpmebpgbhcamgdgjcmnjfhggjpgcimm",
        "status": "error-foobarApp"
      }
    ]
  }
})";

      const auto parser = ProtocolHandlerFactoryJSON().CreateParser();
      EXPECT_TRUE(parser->Parse(update_response));

      base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
          FROM_HERE,
          base::BindOnce(std::move(update_check_callback), parser->results(),
                         ErrorCategory::kNone, 0, 0));
    }
  };
  MockUpdateCheckerFactory<MockUpdateChecker> mock_update_checker_factory;

  class MockCrxDownloader : public CrxDownloader {
   public:
    MockCrxDownloader() = default;

   private:
    ~MockCrxDownloader() override = default;

    base::OnceClosure DoStartDownload(const GURL& url) override {
      ADD_FAILURE();
      return base::DoNothing();
    }
  };

  class MockPingManager : public MockPingManagerImpl {
   public:
    explicit MockPingManager(scoped_refptr<Configurator> config)
        : MockPingManagerImpl(config) {}

   protected:
    ~MockPingManager() override { EXPECT_TRUE(ping_data().empty()); }
  };

  SetMockCrxDownloader<MockCrxDownloader>();
  scoped_refptr<UpdateClient> update_client =
      base::MakeRefCounted<UpdateClientImpl>(
          config(), base::MakeRefCounted<MockPingManager>(config()),
          mock_update_checker_factory.GetFactory());

  MockObserver observer(update_client);
  {
    InSequence seq;
    EXPECT_CALL(observer, OnEvent(Truly([](const CrxUpdateItem& item) {
                  return item.id == "jebgalgnebhfojomionfpkfelancnnkf" &&
                         item.state == ComponentState::kChecking;
                })));
    EXPECT_CALL(observer, OnEvent(Truly([](const CrxUpdateItem& item) {
                  return item.id == "jebgalgnebhfojomionfpkfelancnnkf" &&
                         item.state == ComponentState::kUpdateError;
                })))
        .WillOnce([](const CrxUpdateItem& item) {
          EXPECT_EQ(ComponentState::kUpdateError, item.state);
          EXPECT_EQ(5, static_cast<int>(item.error_category));
          EXPECT_EQ(-10006, item.error_code);  // UNKNOWN_APPPLICATION.
          EXPECT_EQ(0, item.extra_code1);
        });
  }
  {
    InSequence seq;
    EXPECT_CALL(observer, OnEvent(Truly([](const CrxUpdateItem& item) {
                  return item.id == "abagagagagagagagagagagagagagagag" &&
                         item.state == ComponentState::kChecking;
                })));
    EXPECT_CALL(observer, OnEvent(Truly([](const CrxUpdateItem& item) {
                  return item.id == "abagagagagagagagagagagagagagagag" &&
                         item.state == ComponentState::kUpdateError;
                })))
        .WillOnce([](const CrxUpdateItem& item) {
          EXPECT_EQ(ComponentState::kUpdateError, item.state);
          EXPECT_EQ(5, static_cast<int>(item.error_category));
          EXPECT_EQ(-10007, item.error_code);  // RESTRICTED_APPLICATION.
          EXPECT_EQ(0, item.extra_code1);
        });
  }
  {
    InSequence seq;
    EXPECT_CALL(observer, OnEvent(Truly([](const CrxUpdateItem& item) {
                  return item.id == "ihfokbkgjpifnbbojhneepfflplebdkc" &&
                         item.state == ComponentState::kChecking;
                })));
    EXPECT_CALL(observer, OnEvent(Truly([](const CrxUpdateItem& item) {
                  return item.id == "ihfokbkgjpifnbbojhneepfflplebdkc" &&
                         item.state == ComponentState::kUpdateError;
                })))
        .WillOnce([](const CrxUpdateItem& item) {
          EXPECT_EQ(ComponentState::kUpdateError, item.state);
          EXPECT_EQ(5, static_cast<int>(item.error_category));
          EXPECT_EQ(-10008, item.error_code);  // INVALID_APPID.
          EXPECT_EQ(0, item.extra_code1);
        });
  }
  {
    InSequence seq;
    EXPECT_CALL(observer, OnEvent(Truly([](const CrxUpdateItem& item) {
                  return item.id == "gjpmebpgbhcamgdgjcmnjfhggjpgcimm" &&
                         item.state == ComponentState::kChecking;
                })));
    EXPECT_CALL(observer, OnEvent(Truly([](const CrxUpdateItem& item) {
                  return item.id == "gjpmebpgbhcamgdgjcmnjfhggjpgcimm" &&
                         item.state == ComponentState::kUpdateError;
                })))
        .WillOnce([](const CrxUpdateItem& item) {
          EXPECT_EQ(ComponentState::kUpdateError, item.state);
          EXPECT_EQ(5, static_cast<int>(item.error_category));
          EXPECT_EQ(-10016, item.error_code);  // UNKNOWN_ERROR.
          EXPECT_EQ(0, item.extra_code1);
        });
  }

  const std::vector<std::string> ids = {
      "jebgalgnebhfojomionfpkfelancnnkf", "abagagagagagagagagagagagagagagag",
      "ihfokbkgjpifnbbojhneepfflplebdkc", "gjpmebpgbhcamgdgjcmnjfhggjpgcimm"};
  update_client->Update(ids, base::BindOnce(&DataCallbackMock::Callback), {},
                        true, ExpectErrorThenQuit(runloop_, Error::NONE));
  runloop_.Run();
}

// Tests that a run action in invoked in the CRX install scenario.
TEST_F(UpdateClientTest, ActionRun_Install) {
  class MockUpdateChecker : public UpdateChecker {
   public:
    MockUpdateChecker() = default;

    void CheckForUpdates(
        scoped_refptr<UpdateContext> context,
        const base::flat_map<std::string, std::string>& additional_attributes,
        UpdateCheckCallback update_check_callback) override {
      context->get_available_space = base::BindRepeating(
          [](const base::FilePath&) -> int64_t { return 200000; });
      base::expected<ProtocolParser::Results, std::string> results =
          ProtocolParserJSON::ParseJSON(R"()]}'
{
  "response": {
    "protocol": "4.0",
    "apps": [
      {
        "appid": "gjpmebpgbhcamgdgjcmnjfhggjpgcimm",
        "status": "ok",
        "updatecheck": {
          "status": "ok",
          "nextversion": "1.0",
          "pipelines": [
            {
              "operations": [
                {
                  "type": "download",
                  "urls": [
                    {
                      "url": "http://localhost/download/runaction_test_win.crx3"
                    }
                  ],
                  "out": {
                    "sha256": "89290a0d2ff21ca5b45e109c6cc859ab5fe294e19c102d54acd321429c372cea"
                  },
                  "size": 48141
                },
                {
                  "type": "crx3",
                  "in": {
                    "sha256": "89290a0d2ff21ca5b45e109c6cc859ab5fe294e19c102d54acd321429c372cea"
                  }
                },
                {
                  "type": "run",
                  "path": "ChromeRecovery.crx3"
                }
              ]
            }
          ]
        }
      }
    ]
  }
})");
      EXPECT_TRUE(results.has_value()) << results.error();
      EXPECT_FALSE(context->session_id.empty());
      EXPECT_EQ(context->components_to_check_for_updates.size(), 1u);
      base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
          FROM_HERE,
          base::BindOnce(std::move(update_check_callback), results.value(),
                         ErrorCategory::kNone, 0, 0));
    }
  };
  MockUpdateCheckerFactory<MockUpdateChecker> mock_update_checker_factory;

  class MockCrxDownloader : public CrxDownloader {
   public:
    MockCrxDownloader() = default;

   private:
    ~MockCrxDownloader() override = default;

    base::OnceClosure DoStartDownload(const GURL& url) override {
      DownloadMetrics download_metrics;
      base::FilePath path;
      Result result;
      if (url.GetPath() == "/download/runaction_test_win.crx3") {
        download_metrics.url = url;
        download_metrics.downloader = DownloadMetrics::kNone;
        download_metrics.error = 0;
        download_metrics.downloaded_bytes = 48141;
        download_metrics.total_bytes = 48141;
        download_metrics.download_time_ms = 1000;

        EXPECT_TRUE(
            MakeTestFile(GetTestFilePath("runaction_test_win.crx3"), &path));

        result.error = 0;
        result.response = path;
      } else {
        ADD_FAILURE();
      }

      base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
          FROM_HERE, base::BindOnce(&MockCrxDownloader::OnDownloadComplete,
                                    base::Unretained(this), true, result,
                                    download_metrics));
      return base::DoNothing();
    }
  };

  class MockPingManager : public MockPingManagerImpl {
   public:
    explicit MockPingManager(scoped_refptr<Configurator> config)
        : MockPingManagerImpl(config) {}

   protected:
    ~MockPingManager() override {
      EXPECT_EQ(4u, events().size());

      /*
      "<event eventtype="14" eventresult="1" downloader="unknown"
      url="http://localhost/download/runaction_test_win.crx3"
      downloaded=48141 total=48141 download_time_ms="1000"
      previousversion="0.0" nextversion="1.0"/>
      */
      const base::DictValue& event0 = events()[0];
      EXPECT_EQ(14, event0.FindInt("eventtype"));
      EXPECT_EQ(1, event0.FindInt("eventresult"));
      EXPECT_EQ("unknown", CHECK_DEREF(event0.FindString("downloader")));
      EXPECT_EQ("http://localhost/download/runaction_test_win.crx3",
                CHECK_DEREF(event0.FindString("url")));
      EXPECT_EQ(48141, event0.FindDouble("downloaded"));
      EXPECT_EQ(48141, event0.FindDouble("total"));
      EXPECT_EQ(1000, event0.FindDouble("download_time_ms"));
      EXPECT_EQ("0.0", CHECK_DEREF(event0.FindString("previousversion")));
      EXPECT_EQ("1.0", CHECK_DEREF(event0.FindString("nextversion")));

      // <event eventtype="63" eventresult="1" previousversion="0.0"
      // nextversion="1.0"/>
      const base::DictValue& event1 = events()[1];
      EXPECT_EQ(63, event1.FindInt("eventtype"));
      EXPECT_EQ(1, event1.FindInt("eventresult"));
      EXPECT_EQ("0.0", CHECK_DEREF(event1.FindString("previousversion")));
      EXPECT_EQ("1.0", CHECK_DEREF(event1.FindString("nextversion")));

      // <event eventtype="42" eventresult="1" errorcode="1877345072"/>
      const base::DictValue& event2 = events()[2];
      EXPECT_EQ(42, event2.FindInt("eventtype"));
      EXPECT_EQ(1, event2.FindInt("eventresult"));
      EXPECT_EQ(1877345072, event2.FindInt("errorcode"));

      // <event eventtype="2" eventresult="1" previousversion="0.0"
      // nextversion="1.0"/>
      const base::DictValue& event3 = events()[3];
      EXPECT_EQ(2, event3.FindInt("eventtype"));
      EXPECT_EQ(1, event3.FindInt("eventresult"));
      EXPECT_EQ("0.0", CHECK_DEREF(event3.FindString("previousversion")));
      EXPECT_EQ("1.0", CHECK_DEREF(event3.FindString("nextversion")));
    }
  };

  SetMockCrxDownloader<MockCrxDownloader>();
  scoped_refptr<UpdateClient> update_client =
      base::MakeRefCounted<UpdateClientImpl>(
          config(), base::MakeRefCounted<MockPingManager>(config()),
          mock_update_checker_factory.GetFactory());

  update_client->Install(
      std::string("gjpmebpgbhcamgdgjcmnjfhggjpgcimm"),
      base::BindOnce(
          [](const std::vector<std::string>& ids,
             base::OnceCallback<void(
                 const std::vector<std::optional<CrxComponent>>&)> callback) {
            auto action_handler = base::MakeRefCounted<MockActionHandler>();
            EXPECT_CALL(*action_handler, Handle(_, _, _))
                .WillOnce([](const base::FilePath& action,
                             const std::string& session_id,
                             ActionHandler::Callback callback) {
                  EXPECT_EQ("ChromeRecovery.crx3",
                            action.BaseName().AsUTF8Unsafe());
                  EXPECT_TRUE(!session_id.empty());
                  std::move(callback).Run(true, 1877345072, 0);
                });

            CrxComponent crx;
            crx.app_id = "gjpmebpgbhcamgdgjcmnjfhggjpgcimm";
            crx.name = "test_gjpm";
            crx.pk_hash = base::ToVector(gjpm_hash);
            crx.version = base::Version("0.0");
            crx.installer = base::MakeRefCounted<VersionedTestInstaller>();
            crx.action_handler = action_handler;
            crx.crx_format_requirement = crx_file::VerifierFormat::CRX3;
            std::move(callback).Run({crx});
          }),
      {}, ExpectErrorThenQuit(runloop_, Error::NONE));
  runloop_.Run();
}

// Tests that a run action is invoked in an update scenario when there was
// no update.
TEST_F(UpdateClientTest, ActionRun_NoUpdate) {
  MockUpdateCheckerFactory<
      MockUpdateCheckerImpl<UpdateCheckerOptionsActionRunNoUpdate>>
      mock_update_checker_factory;

  class MockCrxDownloader : public CrxDownloader {
   public:
    MockCrxDownloader() = default;

   private:
    ~MockCrxDownloader() override = default;

    base::OnceClosure DoStartDownload(const GURL& url) override {
      ADD_FAILURE();
      return base::DoNothing();
    }
  };

  class MockPingManager : public MockPingManagerImpl {
   public:
    explicit MockPingManager(scoped_refptr<Configurator> config)
        : MockPingManagerImpl(config) {}

   protected:
    ~MockPingManager() override {
      auto ping_data = MockPingManagerImpl::ping_data();
      EXPECT_EQ(ping_data.size(), 2u);

      // "<event eventtype="42" eventresult="1" errorcode="1877345072"/>"
      auto download = ping_data[0];
      EXPECT_EQ(download.event_type, protocol_request::kEventAction);
      EXPECT_EQ(download.event_result, protocol_request::kEventResultSuccess);
      EXPECT_EQ(download.error_code, 1877345072);

      // "<event eventtype="3" eventresult="1"/>"
      auto install = ping_data[1];
      EXPECT_EQ(install.event_type, protocol_request::kEventUpdate);
      EXPECT_EQ(install.event_result, protocol_request::kEventResultSuccess);
    }
  };

  // Unpack the CRX to mock an existing install to be updated. The action to
  // run is going to be resolved relative to this directory.
  base::FilePath unpack_path;
  {
    base::RunLoop runloop;
    base::OnceClosure quit_closure = runloop.QuitClosure();

    Unpacker::Unpack(
        "gjpmebpgbhcamgdgjcmnjfhggjpgcimm", "UpdateClientTest",
        base::ToVector(gjpm_hash), GetTestFilePath("runaction_test_win.crx3"),
        base::MakeRefCounted<UnzipChromiumFactory>(
            base::BindRepeating(&unzip::LaunchInProcessUnzipper))
            ->Create(),
        crx_file::VerifierFormat::CRX3,
        /*is_foreground=*/true,
        base::BindOnce(
            [](base::FilePath* unpack_path, base::OnceClosure quit_closure,
               const Unpacker::Result& result) {
              EXPECT_EQ(UnpackerError::kNone, result.error);
              EXPECT_EQ(0, result.extended_error);
              *unpack_path = result.unpack_path;
              std::move(quit_closure).Run();
            },
            &unpack_path, runloop.QuitClosure()));

    runloop.Run();
  }

  EXPECT_FALSE(unpack_path.empty());
  EXPECT_TRUE(base::DirectoryExists(unpack_path));
  std::optional<int64_t> file_size = base::GetFileSize(
      unpack_path.Append(FILE_PATH_LITERAL("ChromeRecovery.crx3")));
  EXPECT_TRUE(file_size.has_value());
  EXPECT_EQ(44582, file_size.value());

  base::ScopedTempDir unpack_path_owner;
  EXPECT_TRUE(unpack_path_owner.Set(unpack_path));

  SetMockCrxDownloader<MockCrxDownloader>();
  scoped_refptr<UpdateClient> update_client =
      base::MakeRefCounted<UpdateClientImpl>(
          config(), base::MakeRefCounted<MockPingManager>(config()),
          mock_update_checker_factory.GetFactory());

  const std::vector<std::string> ids = {"gjpmebpgbhcamgdgjcmnjfhggjpgcimm"};
  update_client->Update(
      ids,
      base::BindOnce(
          [](const base::FilePath& unpack_path,
             const std::vector<std::string>& ids,
             base::OnceCallback<void(
                 const std::vector<std::optional<CrxComponent>>&)> callback) {
            auto action_handler = base::MakeRefCounted<MockActionHandler>();
            EXPECT_CALL(*action_handler, Handle(_, _, _))
                .WillOnce([](const base::FilePath& action,
                             const std::string& session_id,
                             ActionHandler::Callback callback) {
                  EXPECT_EQ("ChromeRecovery.crx3",
                            action.BaseName().AsUTF8Unsafe());
                  EXPECT_TRUE(!session_id.empty());
                  std::move(callback).Run(true, 1877345072, 0);
                });

            CrxComponent crx;
            crx.app_id = "gjpmebpgbhcamgdgjcmnjfhggjpgcimm";
            crx.name = "test_gjpm";
            crx.pk_hash = base::ToVector(gjpm_hash);
            crx.version = base::Version("1.0");
            crx.installer =
                base::MakeRefCounted<ReadOnlyTestInstaller>(unpack_path);
            crx.action_handler = action_handler;
            crx.crx_format_requirement = crx_file::VerifierFormat::CRX3;
            std::move(callback).Run({crx});
          },
          unpack_path),
      {}, false, ExpectErrorThenQuit(runloop_, Error::NONE));

  runloop_.Run();
}

// Tests that custom response attributes are visible to observers.
TEST_F(UpdateClientTest, CustomAttributeNoUpdate) {
  class DataCallbackMock {
   public:
    static void Callback(
        const std::vector<std::string>& ids,
        base::OnceCallback<
            void(const std::vector<std::optional<CrxComponent>>&)> callback) {
      CrxComponent crx;
      crx.app_id = "jebgalgnebhfojomionfpkfelancnnkf";
      crx.name = "test_jebg";
      crx.pk_hash = base::ToVector(jebg_hash);
      crx.version = base::Version("0.9");
      crx.installer = base::MakeRefCounted<TestInstaller>();
      crx.crx_format_requirement = crx_file::VerifierFormat::CRX3;
      std::vector<std::optional<CrxComponent>> component = {crx};
      std::move(callback).Run(component);
    }
  };

  class MockUpdateChecker : public UpdateChecker {
   public:
    MockUpdateChecker() = default;

    void CheckForUpdates(
        scoped_refptr<UpdateContext> context,
        const base::flat_map<std::string, std::string>& additional_attributes,
        UpdateCheckCallback update_check_callback) override {
      EXPECT_FALSE(context->session_id.empty());
      EXPECT_EQ(1u, context->components_to_check_for_updates.size());
      const std::string id = "jebgalgnebhfojomionfpkfelancnnkf";
      EXPECT_EQ(id, context->components_to_check_for_updates.front());
      EXPECT_EQ(1u, context->components.count(id));

      auto& component = context->components.at(id);

      EXPECT_TRUE(component->is_foreground());

      ProtocolParser::App result;
      result.app_id = id;
      result.status = "noupdate";
      result.custom_attributes["_example"] = "example_value";

      ProtocolParser::Results results;
      results.apps.push_back(result);

      base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
          FROM_HERE, base::BindOnce(std::move(update_check_callback), results,
                                    ErrorCategory::kNone, 0, 0));
    }
  };
  MockUpdateCheckerFactory<MockUpdateChecker> mock_update_checker_factory;

  class MockCrxDownloader : public CrxDownloader {
   public:
    MockCrxDownloader() = default;

   private:
    ~MockCrxDownloader() override = default;

    base::OnceClosure DoStartDownload(const GURL& url) override {
      ADD_FAILURE();
      return base::DoNothing();
    }
  };

  class MockPingManager : public MockPingManagerImpl {
   public:
    explicit MockPingManager(scoped_refptr<Configurator> config)
        : MockPingManagerImpl(config) {}

   protected:
    ~MockPingManager() override { EXPECT_TRUE(ping_data().empty()); }
  };

  SetMockCrxDownloader<MockCrxDownloader>();
  scoped_refptr<UpdateClient> update_client =
      base::MakeRefCounted<UpdateClientImpl>(
          config(), base::MakeRefCounted<MockPingManager>(config()),
          mock_update_checker_factory.GetFactory());

  MockObserver observer(update_client);
  EXPECT_CALL(observer, OnEvent(_))
      .WillRepeatedly([](const CrxUpdateItem& item) {
        if (item.state == ComponentState::kUpToDate) {
          ASSERT_TRUE(item.custom_updatecheck_data.contains("_example"));
          EXPECT_EQ("example_value",
                    item.custom_updatecheck_data.at("_example"));
        }
      });

  update_client->Update({"jebgalgnebhfojomionfpkfelancnnkf"},
                        base::BindOnce(&DataCallbackMock::Callback), {}, true,
                        ExpectErrorThenQuit(runloop_, Error::NONE));
  runloop_.Run();
}

// Tests the scenario where `CrxDataCallback` returns a vector whose elements
// don't include a value for one of the component ids specified by the `ids`
// parameter of the `UpdateClient::Update` function. Expects the completion
// callback to include a specific error, and no other events and pings be
// generated, since the update engine rejects the UpdateClient::Update call.
TEST_F(UpdateClientTest, BadCrxDataCallback) {
  class MockPingManager : public MockPingManagerImpl {
   public:
    explicit MockPingManager(scoped_refptr<Configurator> config)
        : MockPingManagerImpl(config) {}

   protected:
    ~MockPingManager() override { EXPECT_TRUE(ping_data().empty()); }
  };

  scoped_refptr<UpdateClient> update_client =
      base::MakeRefCounted<UpdateClientImpl>(
          config(), base::MakeRefCounted<MockPingManager>(config()),
          UpdateChecker::Factory{});

  MockObserver observer(update_client);

  std::vector<CrxUpdateItem> items;
  auto receiver = base::MakeRefCounted<MockCrxStateChangeReceiver>();
  EXPECT_CALL(*receiver, Receive(_))
      .WillRepeatedly(
          [&items](const CrxUpdateItem& item) { items.push_back(item); });

  const std::vector<std::string> ids = {"jebgalgnebhfojomionfpkfelancnnkf",
                                        "gjpmebpgbhcamgdgjcmnjfhggjpgcimm"};
  // The `CrxDataCallback` argument only returns a value for the first
  // component id. This means that its result is ill formed, and the `Update`
  // call completes with an error.
  update_client->Update(
      ids,
      base::BindOnce(
          [](const std::vector<std::string>& ids,
             base::OnceCallback<void(
                 const std::vector<std::optional<CrxComponent>>&)> callback) {
            EXPECT_EQ(ids.size(), size_t{2});
            std::move(callback).Run({std::nullopt});
          }),
      base::BindRepeating(&MockCrxStateChangeReceiver::Receive, receiver), true,
      ExpectErrorThenQuit(runloop_, Error::BAD_CRX_DATA_CALLBACK));
  runloop_.Run();

  EXPECT_TRUE(items.empty());
}

// Tests cancellation of an install before the task is run.
TEST_F(UpdateClientTest, CancelInstallBeforeTaskStart) {
  class DataCallbackMock {
   public:
    static void Callback(
        const std::vector<std::string>& ids,
        base::OnceCallback<
            void(const std::vector<std::optional<CrxComponent>>&)> callback) {
      CrxComponent crx;
      crx.app_id = "jebgalgnebhfojomionfpkfelancnnkf";
      crx.name = "test_jebg";
      crx.pk_hash = base::ToVector(jebg_hash);
      crx.version = base::Version("0.0");
      crx.installer = base::MakeRefCounted<TestInstaller>();
      crx.crx_format_requirement = crx_file::VerifierFormat::CRX3;
      std::move(callback).Run({crx});
    }
  };

  MockUpdateCheckerFactory<
      MockUpdateCheckerImpl<UpdateCheckerOptionsOneCrxInstall>>
      mock_update_checker_factory;

  class MockCrxDownloader : public CrxDownloader {
   public:
    MockCrxDownloader() = default;

   private:
    ~MockCrxDownloader() override = default;

    base::OnceClosure DoStartDownload(const GURL& url) override {
      DownloadMetrics download_metrics;
      base::FilePath path;
      Result result;
      if (url.GetPath() == "/download/jebgalgnebhfojomionfpkfelancnnkf.crx") {
        download_metrics.url = url;
        download_metrics.downloader = DownloadMetrics::kNone;
        download_metrics.error = 0;
        download_metrics.downloaded_bytes = 1015;
        download_metrics.total_bytes = 1015;
        download_metrics.download_time_ms = 1000;

        EXPECT_TRUE(MakeTestFile(
            GetTestFilePath("jebgalgnebhfojomionfpkfelancnnkf.crx"), &path));

        result.error = 0;
        result.response = path;
      } else {
        ADD_FAILURE();
      }

      base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
          FROM_HERE, base::BindOnce(&MockCrxDownloader::OnDownloadProgress,
                                    base::Unretained(this),
                                    download_metrics.downloaded_bytes,
                                    download_metrics.total_bytes));

      base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
          FROM_HERE, base::BindOnce(&MockCrxDownloader::OnDownloadComplete,
                                    base::Unretained(this), true, result,
                                    download_metrics));
      return base::DoNothing();
    }
  };

  class MockPingManager : public MockPingManagerImpl {
   public:
    explicit MockPingManager(scoped_refptr<Configurator> config)
        : MockPingManagerImpl(config) {}

   protected:
    ~MockPingManager() override {
      const auto ping_data = MockPingManagerImpl::ping_data();
      EXPECT_EQ(0u, ping_data.size());
    }
  };

  SetMockCrxDownloader<MockCrxDownloader>();
  scoped_refptr<UpdateClient> update_client =
      base::MakeRefCounted<UpdateClientImpl>(
          config(), base::MakeRefCounted<MockPingManager>(config()),
          mock_update_checker_factory.GetFactory());

  std::vector<CrxUpdateItem> items;
  auto receiver = base::MakeRefCounted<MockCrxStateChangeReceiver>();
  EXPECT_CALL(*receiver, Receive(_))
      .WillRepeatedly(
          [&items](const CrxUpdateItem& item) { items.push_back(item); });

  update_client
      ->Install(
          std::string("jebgalgnebhfojomionfpkfelancnnkf"),
          base::BindOnce(&DataCallbackMock::Callback),
          base::BindRepeating(&MockCrxStateChangeReceiver::Receive, receiver),
          ExpectErrorThenQuit(runloop_, Error::UPDATE_CANCELED))
      .Run();
  runloop_.Run();
  EXPECT_EQ(0u, items.size());
}

// Tests cancellation of an install before the component installer runs.
TEST_F(UpdateClientTest, CancelInstallBeforeInstall) {
  class DataCallbackMock {
   public:
    static void Callback(
        const std::vector<std::string>& ids,
        base::OnceCallback<
            void(const std::vector<std::optional<CrxComponent>>&)> callback) {
      CrxComponent crx;
      crx.app_id = "jebgalgnebhfojomionfpkfelancnnkf";
      crx.name = "test_jebg";
      crx.pk_hash = base::ToVector(jebg_hash);
      crx.version = base::Version("0.0");
      crx.installer = base::MakeRefCounted<TestInstaller>();
      crx.crx_format_requirement = crx_file::VerifierFormat::CRX3;
      std::move(callback).Run({crx});
    }
  };

  MockUpdateCheckerFactory<
      MockUpdateCheckerImpl<UpdateCheckerOptionsOneCrxInstall>>
      mock_update_checker_factory;

  class MockCrxDownloader : public CrxDownloader {
   public:
    MockCrxDownloader() = default;

   private:
    ~MockCrxDownloader() override = default;

    base::OnceClosure DoStartDownload(const GURL& url) override {
      DownloadMetrics download_metrics;
      base::FilePath path;
      Result result;
      if (url.GetPath() == "/download/jebgalgnebhfojomionfpkfelancnnkf.crx") {
        download_metrics.url = url;
        download_metrics.downloader = DownloadMetrics::kNone;
        download_metrics.error = 0;
        download_metrics.downloaded_bytes = 1015;
        download_metrics.total_bytes = 1015;
        download_metrics.download_time_ms = 1000;

        EXPECT_TRUE(MakeTestFile(
            GetTestFilePath("jebgalgnebhfojomionfpkfelancnnkf.crx"), &path));

        result.error = 0;
        result.response = path;
      } else {
        ADD_FAILURE();
      }

      base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
          FROM_HERE, base::BindOnce(&MockCrxDownloader::OnDownloadProgress,
                                    base::Unretained(this),
                                    download_metrics.downloaded_bytes,
                                    download_metrics.total_bytes));

      base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
          FROM_HERE, base::BindOnce(&MockCrxDownloader::OnDownloadComplete,
                                    base::Unretained(this), true, result,
                                    download_metrics));
      return base::DoNothing();
    }
  };

  class MockPingManager : public MockPingManagerImpl {
   public:
    explicit MockPingManager(scoped_refptr<Configurator> config)
        : MockPingManagerImpl(config) {}

   protected:
    ~MockPingManager() override {
      const auto ping_data = MockPingManagerImpl::terminal_ping_data();
      EXPECT_EQ(1u, ping_data.size());
      EXPECT_EQ("jebgalgnebhfojomionfpkfelancnnkf", ping_data[0].id);
      EXPECT_EQ(base::Version("0.0"), ping_data[0].previous_version);
      EXPECT_EQ(base::Version("1.0"), ping_data[0].next_version);
      EXPECT_EQ(ErrorCategory::kService, ping_data[0].error_category);
      EXPECT_EQ(std::to_underlying(ServiceError::CANCELLED),
                ping_data[0].error_code);
    }
  };

  SetMockCrxDownloader<MockCrxDownloader>();
  scoped_refptr<UpdateClient> update_client =
      base::MakeRefCounted<UpdateClientImpl>(
          config(), base::MakeRefCounted<MockPingManager>(config()),
          mock_update_checker_factory.GetFactory());

  base::RepeatingClosure cancel;

  MockObserver observer(update_client);
  {
    InSequence seq;
    EXPECT_CALL(observer, OnEvent(Truly([](const CrxUpdateItem& item) {
                  return item.id == "jebgalgnebhfojomionfpkfelancnnkf" &&
                         item.state == ComponentState::kChecking;
                })));
    EXPECT_CALL(observer, OnEvent(Truly([](const CrxUpdateItem& item) {
                  return item.id == "jebgalgnebhfojomionfpkfelancnnkf" &&
                         item.state == ComponentState::kCanUpdate;
                })));
    EXPECT_CALL(observer, OnEvent(Truly([](const CrxUpdateItem& item) {
                  return item.id == "jebgalgnebhfojomionfpkfelancnnkf" &&
                         item.state == ComponentState::kDownloading;
                })))
        .Times(AtLeast(1))
        .WillRepeatedly([&cancel] { cancel.Run(); });
    EXPECT_CALL(observer, OnEvent(Truly([](const CrxUpdateItem& item) {
                  return item.id == "jebgalgnebhfojomionfpkfelancnnkf" &&
                         item.state == ComponentState::kUpdateError;
                })));
  }

  std::vector<CrxUpdateItem> items;
  auto receiver = base::MakeRefCounted<MockCrxStateChangeReceiver>();
  EXPECT_CALL(*receiver, Receive(_))
      .WillRepeatedly(
          [&items](const CrxUpdateItem& item) { items.push_back(item); });

  cancel = update_client->Install(
      std::string("jebgalgnebhfojomionfpkfelancnnkf"),
      base::BindOnce(&DataCallbackMock::Callback),
      base::BindRepeating(&MockCrxStateChangeReceiver::Receive, receiver),
      ExpectErrorThenQuit(runloop_, Error::NONE));
  runloop_.Run();

  EXPECT_EQ(5u, items.size());
  EXPECT_EQ(ComponentState::kChecking, items[0].state);
  EXPECT_EQ("jebgalgnebhfojomionfpkfelancnnkf", items[0].id);
  EXPECT_EQ(ComponentState::kCanUpdate, items[1].state);
  EXPECT_EQ("jebgalgnebhfojomionfpkfelancnnkf", items[1].id);
  EXPECT_EQ(ComponentState::kDownloading, items[2].state);
  EXPECT_EQ("jebgalgnebhfojomionfpkfelancnnkf", items[2].id);
  EXPECT_EQ(ComponentState::kDownloading, items[3].state);
  EXPECT_EQ("jebgalgnebhfojomionfpkfelancnnkf", items[3].id);
  EXPECT_EQ(ComponentState::kUpdateError, items[4].state);
  EXPECT_EQ("jebgalgnebhfojomionfpkfelancnnkf", items[4].id);
}

// Tests cancellation of an install before the download.
TEST_F(UpdateClientTest, CancelInstallBeforeDownload) {
  class DataCallbackMock {
   public:
    static void Callback(
        const std::vector<std::string>& ids,
        base::OnceCallback<
            void(const std::vector<std::optional<CrxComponent>>&)> callback) {
      CrxComponent crx;
      crx.name = "test_jebg";
      crx.pk_hash = base::ToVector(jebg_hash);
      crx.app_id = "jebgalgnebhfojomionfpkfelancnnkf";
      crx.version = base::Version("0.0");
      crx.installer = base::MakeRefCounted<TestInstaller>();
      crx.crx_format_requirement = crx_file::VerifierFormat::CRX3;
      std::move(callback).Run({crx});
    }
  };

  MockUpdateCheckerFactory<
      MockUpdateCheckerImpl<UpdateCheckerOptionsOneCrxInstall>>
      mock_update_checker_factory;

  class MockCrxDownloader : public CrxDownloader {
   public:
    MockCrxDownloader() = default;

   private:
    ~MockCrxDownloader() override = default;

    base::OnceClosure DoStartDownload(const GURL& url) override {
      DownloadMetrics download_metrics;
      base::FilePath path;
      Result result;
      if (url.GetPath() == "/download/jebgalgnebhfojomionfpkfelancnnkf.crx") {
        download_metrics.url = url;
        download_metrics.downloader = DownloadMetrics::kNone;
        download_metrics.error = 0;
        download_metrics.downloaded_bytes = 1015;
        download_metrics.total_bytes = 1015;
        download_metrics.download_time_ms = 1000;

        EXPECT_TRUE(MakeTestFile(
            GetTestFilePath("jebgalgnebhfojomionfpkfelancnnkf.crx"), &path));

        result.error = 0;
        result.response = path;
      } else {
        ADD_FAILURE();
      }

      base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
          FROM_HERE, base::BindOnce(&MockCrxDownloader::OnDownloadProgress,
                                    base::Unretained(this),
                                    download_metrics.downloaded_bytes,
                                    download_metrics.total_bytes));

      base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
          FROM_HERE, base::BindOnce(&MockCrxDownloader::OnDownloadComplete,
                                    base::Unretained(this), true, result,
                                    download_metrics));
      return base::DoNothing();
    }
  };

  class MockPingManager : public MockPingManagerImpl {
   public:
    explicit MockPingManager(scoped_refptr<Configurator> config)
        : MockPingManagerImpl(config) {}

   protected:
    ~MockPingManager() override {
      const auto ping_data = MockPingManagerImpl::terminal_ping_data();
      EXPECT_EQ(1u, ping_data.size());
      EXPECT_EQ("jebgalgnebhfojomionfpkfelancnnkf", ping_data[0].id);
      EXPECT_EQ(base::Version("0.0"), ping_data[0].previous_version);
      EXPECT_EQ(base::Version("1.0"), ping_data[0].next_version);
      EXPECT_EQ(ErrorCategory::kService, ping_data[0].error_category);
      EXPECT_EQ(std::to_underlying(ServiceError::CANCELLED),
                ping_data[0].error_code);
    }
  };

  SetMockCrxDownloader<MockCrxDownloader>();
  scoped_refptr<UpdateClient> update_client =
      base::MakeRefCounted<UpdateClientImpl>(
          config(), base::MakeRefCounted<MockPingManager>(config()),
          mock_update_checker_factory.GetFactory());

  base::RepeatingClosure cancel;

  MockObserver observer(update_client);
  {
    InSequence seq;
    EXPECT_CALL(observer, OnEvent(Truly([](const CrxUpdateItem& item) {
                  return item.id == "jebgalgnebhfojomionfpkfelancnnkf" &&
                         item.state == ComponentState::kChecking;
                })));
    EXPECT_CALL(observer, OnEvent(Truly([](const CrxUpdateItem& item) {
                  return item.id == "jebgalgnebhfojomionfpkfelancnnkf" &&
                         item.state == ComponentState::kCanUpdate;
                })))
        .WillOnce([&cancel] { cancel.Run(); });
    EXPECT_CALL(observer, OnEvent(Truly([](const CrxUpdateItem& item) {
                  return item.id == "jebgalgnebhfojomionfpkfelancnnkf" &&
                         item.state == ComponentState::kUpdateError;
                })));
  }

  std::vector<CrxUpdateItem> items;
  auto receiver = base::MakeRefCounted<MockCrxStateChangeReceiver>();
  EXPECT_CALL(*receiver, Receive(_))
      .WillRepeatedly(
          [&items](const CrxUpdateItem& item) { items.push_back(item); });

  cancel = update_client->Install(
      std::string("jebgalgnebhfojomionfpkfelancnnkf"),
      base::BindOnce(&DataCallbackMock::Callback),
      base::BindRepeating(&MockCrxStateChangeReceiver::Receive, receiver),
      ExpectErrorThenQuit(runloop_, Error::NONE));
  runloop_.Run();

  EXPECT_EQ(3u, items.size());
  EXPECT_EQ(ComponentState::kChecking, items[0].state);
  EXPECT_EQ("jebgalgnebhfojomionfpkfelancnnkf", items[0].id);
  EXPECT_EQ(ComponentState::kCanUpdate, items[1].state);
  EXPECT_EQ("jebgalgnebhfojomionfpkfelancnnkf", items[1].id);
  EXPECT_EQ(ComponentState::kUpdateError, items[2].state);
  EXPECT_EQ("jebgalgnebhfojomionfpkfelancnnkf", items[2].id);
}

TEST_F(UpdateClientTest, CheckForUpdate_NoUpdate) {
  class DataCallbackMock {
   public:
    static void Callback(
        const std::vector<std::string>& ids,
        base::OnceCallback<
            void(const std::vector<std::optional<CrxComponent>>&)> callback) {
      CrxComponent crx;
      crx.app_id = "jebgalgnebhfojomionfpkfelancnnkf";
      crx.name = "test_jebg";
      crx.pk_hash = base::ToVector(jebg_hash);
      crx.version = base::Version("0.9");
      crx.installer = base::MakeRefCounted<TestInstaller>();
      crx.crx_format_requirement = crx_file::VerifierFormat::CRX3;
      std::move(callback).Run({crx});
    }
  };

  class MockUpdateChecker : public UpdateChecker {
   public:
    MockUpdateChecker() = default;

    void CheckForUpdates(
        scoped_refptr<UpdateContext> context,
        const base::flat_map<std::string, std::string>& additional_attributes,
        UpdateCheckCallback update_check_callback) override {
      EXPECT_FALSE(context->session_id.empty());
      EXPECT_EQ(1u, context->components_to_check_for_updates.size());
      const std::string id = "jebgalgnebhfojomionfpkfelancnnkf";
      EXPECT_EQ(id, context->components_to_check_for_updates.front());
      EXPECT_EQ(1u, context->components.count(id));

      auto& component = context->components.at(id);

      EXPECT_TRUE(component->is_foreground());

      ProtocolParser::App result;
      result.app_id = id;
      result.status = "noupdate";

      ProtocolParser::Results results;
      results.apps.push_back(result);

      base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
          FROM_HERE, base::BindOnce(std::move(update_check_callback), results,
                                    ErrorCategory::kNone, 0, 0));
    }
  };
  MockUpdateCheckerFactory<MockUpdateChecker> mock_update_checker_factory;

  class MockCrxDownloader : public CrxDownloader {
   public:
    MockCrxDownloader() = default;

   private:
    ~MockCrxDownloader() override = default;

    base::OnceClosure DoStartDownload(const GURL& url) override {
      ADD_FAILURE();
      return base::DoNothing();
    }
  };

  class MockPingManager : public MockPingManagerImpl {
   public:
    explicit MockPingManager(scoped_refptr<Configurator> config)
        : MockPingManagerImpl(config) {}

   protected:
    ~MockPingManager() override { EXPECT_TRUE(ping_data().empty()); }
  };

  SetMockCrxDownloader<MockCrxDownloader>();
  scoped_refptr<UpdateClient> update_client =
      base::MakeRefCounted<UpdateClientImpl>(
          config(), base::MakeRefCounted<MockPingManager>(config()),
          mock_update_checker_factory.GetFactory());

  MockObserver observer(update_client);
  {
    InSequence seq;
    EXPECT_CALL(observer, OnEvent(Truly([](const CrxUpdateItem& item) {
                  return item.id == "jebgalgnebhfojomionfpkfelancnnkf" &&
                         item.state == ComponentState::kChecking;
                })));
    EXPECT_CALL(observer, OnEvent(Truly([](const CrxUpdateItem& item) {
                  return item.id == "jebgalgnebhfojomionfpkfelancnnkf" &&
                         item.state == ComponentState::kUpToDate;
                })));
  }
  std::vector<CrxUpdateItem> items;
  auto receiver = base::MakeRefCounted<MockCrxStateChangeReceiver>();
  EXPECT_CALL(*receiver, Receive(_))
      .WillRepeatedly(
          [&items](const CrxUpdateItem& item) { items.push_back(item); });

  const std::string id = "jebgalgnebhfojomionfpkfelancnnkf";
  update_client->CheckForUpdate(
      id, base::BindOnce(&DataCallbackMock::Callback),
      base::BindRepeating(&MockCrxStateChangeReceiver::Receive, receiver),
      /*is_foreground=*/true, ExpectErrorThenQuit(runloop_, Error::NONE));
  runloop_.Run();
  EXPECT_EQ(items.size(), 2u);
  EXPECT_EQ(items[0].state, ComponentState::kChecking);
  EXPECT_EQ(items[0].id, "jebgalgnebhfojomionfpkfelancnnkf");
  EXPECT_EQ(items[1].state, ComponentState::kUpToDate);
  EXPECT_EQ(items[1].id, "jebgalgnebhfojomionfpkfelancnnkf");
}

TEST_F(UpdateClientTest, CheckForUpdate_UpdateAvailable) {
  class DataCallbackMock {
   public:
    static void Callback(
        const std::vector<std::string>& ids,
        base::OnceCallback<
            void(const std::vector<std::optional<CrxComponent>>&)> callback) {
      CrxComponent crx;
      crx.app_id = "jebgalgnebhfojomionfpkfelancnnkf";
      crx.name = "test_jebg";
      crx.pk_hash = base::ToVector(jebg_hash);
      crx.version = base::Version("0.9");
      crx.installer = base::MakeRefCounted<TestInstaller>();
      crx.crx_format_requirement = crx_file::VerifierFormat::CRX3;
      std::move(callback).Run({crx});
    }
  };

  MockUpdateCheckerFactory<
      MockUpdateCheckerImpl<UpdateCheckerOptionsOneCrxUpdate>>
      mock_update_checker_factory;

  class MockCrxDownloader : public CrxDownloader {
   public:
    MockCrxDownloader() = default;

   private:
    ~MockCrxDownloader() override = default;

    base::OnceClosure DoStartDownload(const GURL& url) override {
      ADD_FAILURE();
      return base::DoNothing();
    }
  };

  class MockPingManager : public MockPingManagerImpl {
   public:
    explicit MockPingManager(scoped_refptr<Configurator> config)
        : MockPingManagerImpl(config) {}

   protected:
    ~MockPingManager() override {
      const std::vector<PingData> ping_data =
          MockPingManagerImpl::terminal_ping_data();
      EXPECT_EQ(ping_data.size(), 1u);
      EXPECT_EQ(ping_data[0].id, "jebgalgnebhfojomionfpkfelancnnkf");
      EXPECT_EQ(ping_data[0].previous_version, base::Version("0.9"));
      EXPECT_EQ(ping_data[0].next_version, base::Version("1.0"));
      EXPECT_EQ(ping_data[0].error_category, ErrorCategory::kService);
      EXPECT_EQ(ping_data[0].error_code,
                std::to_underlying(ServiceError::CHECK_FOR_UPDATE_ONLY));
      EXPECT_EQ(ping_data[0].extra_code1, 0);
    }
  };

  SetMockCrxDownloader<MockCrxDownloader>();
  scoped_refptr<UpdateClient> update_client =
      base::MakeRefCounted<UpdateClientImpl>(
          config(), base::MakeRefCounted<MockPingManager>(config()),
          mock_update_checker_factory.GetFactory());

  MockObserver observer(update_client);
  {
    InSequence seq;
    EXPECT_CALL(observer, OnEvent(Truly([](const CrxUpdateItem& item) {
                  return item.id == "jebgalgnebhfojomionfpkfelancnnkf" &&
                         item.state == ComponentState::kChecking;
                })));
    EXPECT_CALL(observer, OnEvent(Truly([](const CrxUpdateItem& item) {
                  return item.id == "jebgalgnebhfojomionfpkfelancnnkf" &&
                         item.state == ComponentState::kCanUpdate;
                })));
  }
  std::vector<CrxUpdateItem> items;
  auto receiver = base::MakeRefCounted<MockCrxStateChangeReceiver>();
  EXPECT_CALL(*receiver, Receive(_))
      .WillRepeatedly(
          [&items](const CrxUpdateItem& item) { items.push_back(item); });

  const std::string id = "jebgalgnebhfojomionfpkfelancnnkf";
  update_client->CheckForUpdate(
      id, base::BindOnce(&DataCallbackMock::Callback),
      base::BindRepeating(&MockCrxStateChangeReceiver::Receive, receiver),
      /*is_foreground=*/false, ExpectErrorThenQuit(runloop_, Error::NONE));
  runloop_.Run();
  EXPECT_EQ(items.size(), 2u);
  EXPECT_EQ(items[0].state, ComponentState::kChecking);
  EXPECT_EQ(items[0].id, "jebgalgnebhfojomionfpkfelancnnkf");
  EXPECT_EQ(items[1].state, ComponentState::kCanUpdate);
  EXPECT_EQ(items[1].id, "jebgalgnebhfojomionfpkfelancnnkf");
}

TEST_F(UpdateClientTest, CheckForUpdate_QueueChecks) {
  class DataCallbackMock {
   public:
    static void Callback(
        const std::vector<std::string>& ids,
        base::OnceCallback<
            void(const std::vector<std::optional<CrxComponent>>&)> callback) {
      CrxComponent crx;
      crx.app_id = "jebgalgnebhfojomionfpkfelancnnkf";
      crx.name = "test_jebg";
      crx.pk_hash = base::ToVector(jebg_hash);
      crx.version = base::Version("0.9");
      crx.installer = base::MakeRefCounted<TestInstaller>();
      crx.crx_format_requirement = crx_file::VerifierFormat::CRX3;
      std::move(callback).Run({crx});
    }
  };

  class MockUpdateChecker : public UpdateChecker {
   public:
    MockUpdateChecker() = default;

    void CheckForUpdates(
        scoped_refptr<UpdateContext> context,
        const base::flat_map<std::string, std::string>& additional_attributes,
        UpdateCheckCallback update_check_callback) override {
      EXPECT_FALSE(context->session_id.empty());
      EXPECT_EQ(1u, context->components_to_check_for_updates.size());
      const std::string id = "jebgalgnebhfojomionfpkfelancnnkf";
      EXPECT_EQ(id, context->components_to_check_for_updates.front());
      EXPECT_EQ(1u, context->components.count(id));

      auto& component = context->components.at(id);

      EXPECT_TRUE(component->is_foreground());

      ProtocolParser::App result;
      result.app_id = id;
      result.status = "noupdate";

      ProtocolParser::Results results;
      results.apps.push_back(result);

      base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
          FROM_HERE, base::BindOnce(std::move(update_check_callback), results,
                                    ErrorCategory::kNone, 0, 0));
    }
  };
  MockUpdateCheckerFactory<MockUpdateChecker> mock_update_checker_factory;

  class MockCrxDownloader : public CrxDownloader {
   public:
    MockCrxDownloader() = default;

   private:
    ~MockCrxDownloader() override = default;

    base::OnceClosure DoStartDownload(const GURL& url) override {
      ADD_FAILURE();
      return base::DoNothing();
    }
  };

  class MockPingManager : public MockPingManagerImpl {
   public:
    explicit MockPingManager(scoped_refptr<Configurator> config)
        : MockPingManagerImpl(config) {}

   protected:
    ~MockPingManager() override { EXPECT_TRUE(ping_data().empty()); }
  };

  SetMockCrxDownloader<MockCrxDownloader>();
  scoped_refptr<UpdateClient> update_client =
      base::MakeRefCounted<UpdateClientImpl>(
          config(), base::MakeRefCounted<MockPingManager>(config()),
          mock_update_checker_factory.GetFactory());

  MockObserver observer(update_client);
  {
    InSequence seq;
    EXPECT_CALL(observer, OnEvent(Truly([](const CrxUpdateItem& item) {
                  return item.id == "jebgalgnebhfojomionfpkfelancnnkf" &&
                         item.state == ComponentState::kChecking;
                })));
    EXPECT_CALL(observer, OnEvent(Truly([](const CrxUpdateItem& item) {
                  return item.id == "jebgalgnebhfojomionfpkfelancnnkf" &&
                         item.state == ComponentState::kUpToDate;
                })));
    EXPECT_CALL(observer, OnEvent(Truly([](const CrxUpdateItem& item) {
                  return item.id == "jebgalgnebhfojomionfpkfelancnnkf" &&
                         item.state == ComponentState::kChecking;
                })));
    EXPECT_CALL(observer, OnEvent(Truly([](const CrxUpdateItem& item) {
                  return item.id == "jebgalgnebhfojomionfpkfelancnnkf" &&
                         item.state == ComponentState::kUpToDate;
                })));
  }
  std::vector<CrxUpdateItem> items;
  auto receiver = base::MakeRefCounted<MockCrxStateChangeReceiver>();
  EXPECT_CALL(*receiver, Receive(_))
      .WillRepeatedly(
          [&items](const CrxUpdateItem& item) { items.push_back(item); });

  // Do two `CheckForUpdate` calls, expect the calls to be done in sequence.
  base::RepeatingClosure barrier_quit_closure =
      BarrierClosure(2, runloop_.QuitClosure());
  const std::string id = "jebgalgnebhfojomionfpkfelancnnkf";
  update_client->CheckForUpdate(
      id, base::BindOnce(&DataCallbackMock::Callback),
      base::BindRepeating(&MockCrxStateChangeReceiver::Receive, receiver),
      /*is_foreground=*/true,
      ExpectErrorThenQuit(barrier_quit_closure, Error::NONE));
  update_client->CheckForUpdate(
      id, base::BindOnce(&DataCallbackMock::Callback),
      base::BindRepeating(&MockCrxStateChangeReceiver::Receive, receiver),
      /*is_foreground=*/true,
      ExpectErrorThenQuit(barrier_quit_closure, Error::NONE));
  EXPECT_TRUE(update_client->IsUpdating(id));
  runloop_.Run();
  EXPECT_EQ(items.size(), 4u);
  EXPECT_EQ(items[0].state, ComponentState::kChecking);
  EXPECT_EQ(items[0].id, "jebgalgnebhfojomionfpkfelancnnkf");
  EXPECT_EQ(items[1].state, ComponentState::kUpToDate);
  EXPECT_EQ(items[1].id, "jebgalgnebhfojomionfpkfelancnnkf");
  EXPECT_EQ(items[2].state, ComponentState::kChecking);
  EXPECT_EQ(items[2].id, "jebgalgnebhfojomionfpkfelancnnkf");
  EXPECT_EQ(items[3].state, ComponentState::kUpToDate);
  EXPECT_EQ(items[3].id, "jebgalgnebhfojomionfpkfelancnnkf");
}

TEST_F(UpdateClientTest, CheckForUpdate_Stop) {
  class DataCallbackMock {
   public:
    static void Callback(
        const std::vector<std::string>& ids,
        base::OnceCallback<
            void(const std::vector<std::optional<CrxComponent>>&)> callback) {
      CrxComponent crx;
      crx.app_id = "jebgalgnebhfojomionfpkfelancnnkf";
      crx.name = "test_jebg";
      crx.pk_hash = base::ToVector(jebg_hash);
      crx.version = base::Version("0.9");
      crx.installer = base::MakeRefCounted<TestInstaller>();
      crx.crx_format_requirement = crx_file::VerifierFormat::CRX3;
      std::move(callback).Run({crx});
    }
  };

  class MockUpdateChecker : public UpdateChecker {
   public:
    MockUpdateChecker() = default;

    void CheckForUpdates(
        scoped_refptr<UpdateContext> context,
        const base::flat_map<std::string, std::string>& additional_attributes,
        UpdateCheckCallback update_check_callback) override {
      EXPECT_FALSE(context->session_id.empty());
      EXPECT_EQ(1u, context->components_to_check_for_updates.size());
      const std::string id = "jebgalgnebhfojomionfpkfelancnnkf";
      EXPECT_EQ(id, context->components_to_check_for_updates.front());
      EXPECT_EQ(1u, context->components.count(id));

      auto& component = context->components.at(id);

      EXPECT_TRUE(component->is_foreground());

      ProtocolParser::App result;
      result.app_id = id;
      result.status = "noupdate";

      ProtocolParser::Results results;
      results.apps.push_back(result);

      base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
          FROM_HERE, base::BindOnce(std::move(update_check_callback), results,
                                    ErrorCategory::kNone, 0, 0));
    }
  };
  MockUpdateCheckerFactory<MockUpdateChecker> mock_update_checker_factory;

  class MockCrxDownloader : public CrxDownloader {
   public:
    MockCrxDownloader() = default;

   private:
    ~MockCrxDownloader() override = default;

    base::OnceClosure DoStartDownload(const GURL& url) override {
      ADD_FAILURE();
      return base::DoNothing();
    }
  };

  class MockPingManager : public MockPingManagerImpl {
   public:
    explicit MockPingManager(scoped_refptr<Configurator> config)
        : MockPingManagerImpl(config) {}

   protected:
    ~MockPingManager() override { EXPECT_TRUE(ping_data().empty()); }
  };

  SetMockCrxDownloader<MockCrxDownloader>();
  scoped_refptr<UpdateClient> update_client =
      base::MakeRefCounted<UpdateClientImpl>(
          config(), base::MakeRefCounted<MockPingManager>(config()),
          mock_update_checker_factory.GetFactory());

  MockObserver observer(update_client);
  {
  }

  std::vector<CrxUpdateItem> items;
  auto receiver = base::MakeRefCounted<MockCrxStateChangeReceiver>();
  EXPECT_CALL(*receiver, Receive(_))
      .WillRepeatedly(
          [&items](const CrxUpdateItem& item) { items.push_back(item); });

  // Do two `CheckForUpdate` calls, expect both calls to be cancelled.
  base::RepeatingClosure barrier_quit_closure =
      BarrierClosure(2, runloop_.QuitClosure());
  const std::string id = "jebgalgnebhfojomionfpkfelancnnkf";
  update_client->CheckForUpdate(
      id, base::BindOnce(&DataCallbackMock::Callback),
      base::BindRepeating(&MockCrxStateChangeReceiver::Receive, receiver),
      /*is_foreground=*/true,
      ExpectErrorThenQuit(barrier_quit_closure, Error::UPDATE_CANCELED));
  update_client->CheckForUpdate(
      id, base::BindOnce(&DataCallbackMock::Callback),
      base::BindRepeating(&MockCrxStateChangeReceiver::Receive, receiver),
      /*is_foreground=*/true,
      ExpectErrorThenQuit(barrier_quit_closure, Error::UPDATE_CANCELED));
  EXPECT_TRUE(update_client->IsUpdating(id));
  update_client->Stop();
  runloop_.Run();
  EXPECT_TRUE(items.empty());
}

TEST_F(UpdateClientTest, CheckForUpdate_Errors) {
  class MockUpdateChecker : public UpdateChecker {
   public:
    MockUpdateChecker() = default;

    void CheckForUpdates(
        scoped_refptr<UpdateContext> context,
        const base::flat_map<std::string, std::string>& additional_attributes,
        UpdateCheckCallback update_check_callback) override {}
  };
  MockUpdateCheckerFactory<MockUpdateChecker> mock_update_checker_factory;

  class MockCrxDownloader : public CrxDownloader {
   public:
    MockCrxDownloader() = default;

   private:
    ~MockCrxDownloader() override = default;

    base::OnceClosure DoStartDownload(const GURL& url) override {
      ADD_FAILURE();
      return base::DoNothing();
    }
  };

  class MockPingManager : public MockPingManagerImpl {
   public:
    explicit MockPingManager(scoped_refptr<Configurator> config)
        : MockPingManagerImpl(config) {}

   protected:
    ~MockPingManager() override { EXPECT_TRUE(ping_data().empty()); }
  };

  SetMockCrxDownloader<MockCrxDownloader>();
  scoped_refptr<UpdateClient> update_client =
      base::MakeRefCounted<UpdateClientImpl>(
          config(), base::MakeRefCounted<MockPingManager>(config()),
          mock_update_checker_factory.GetFactory());

  MockObserver observer(update_client);
  {
    InSequence seq;
    EXPECT_CALL(observer, OnEvent(Truly([](const CrxUpdateItem& item) {
                  return item.id == "jebgalgnebhfojomionfpkfelancnnkf" &&
                         item.state == ComponentState::kUpdateError;
                })));
  }
  std::vector<CrxUpdateItem> items;
  auto receiver = base::MakeRefCounted<MockCrxStateChangeReceiver>();
  EXPECT_CALL(*receiver, Receive(_))
      .WillRepeatedly(
          [&items](const CrxUpdateItem& item) { items.push_back(item); });

  // Tests some error cases when arguments are incorrect.
  base::RepeatingClosure barrier_quit_closure =
      BarrierClosure(2, runloop_.QuitClosure());
  const std::string id = "jebgalgnebhfojomionfpkfelancnnkf";
  update_client->CheckForUpdate(
      id,
      base::BindOnce(
          [](const std::vector<std::string>& /*ids*/,
             base::OnceCallback<void(
                 const std::vector<std::optional<CrxComponent>>&)> callback) {
            std::move(callback).Run({});
          }),
      base::BindRepeating(&MockCrxStateChangeReceiver::Receive, receiver),
      /*is_foreground=*/true,
      ExpectErrorThenQuit(barrier_quit_closure, Error::BAD_CRX_DATA_CALLBACK));
  update_client->CheckForUpdate(
      id,
      base::BindLambdaForTesting(
          [&id](
              const std::vector<std::string>& ids,
              base::OnceCallback<void(
                  const std::vector<std::optional<CrxComponent>>&)> callback) {
            EXPECT_EQ(ids.size(), 1u);
            EXPECT_EQ(id, ids[0]);
            std::move(callback).Run({std::nullopt});
          }),
      base::BindRepeating(&MockCrxStateChangeReceiver::Receive, receiver),
      /*is_foreground=*/true,
      ExpectErrorThenQuit(barrier_quit_closure, Error::NONE));
  EXPECT_TRUE(update_client->IsUpdating(id));
  runloop_.Run();
  EXPECT_EQ(items.size(), 1u);
  EXPECT_EQ(items[0].state, ComponentState::kUpdateError);
  EXPECT_EQ(items[0].id, "jebgalgnebhfojomionfpkfelancnnkf");
  EXPECT_EQ(items[0].error_code, std::to_underlying(Error::CRX_NOT_FOUND));
}

// Tests `CheckForUpdate` when the updates are disabled but the server ignores
// "updatedisabled" attribute and returns on update. In this case, the client
// reports an error (SERVICE_ERROR, UPDATE_DISABLED) and pings.
TEST_F(UpdateClientTest, UpdateCheck_UpdateDisabled) {
  class DataCallbackMock {
   public:
    static void Callback(
        const std::vector<std::string>& ids,
        base::OnceCallback<
            void(const std::vector<std::optional<CrxComponent>>&)> callback) {
      CrxComponent crx;
      crx.app_id = "jebgalgnebhfojomionfpkfelancnnkf";
      crx.name = "test_jebg";
      crx.pk_hash = base::ToVector(jebg_hash);
      crx.version = base::Version("0.9");
      crx.installer = base::MakeRefCounted<TestInstaller>();
      crx.crx_format_requirement = crx_file::VerifierFormat::CRX3;
      crx.updates_enabled = false;
      std::move(callback).Run({crx});
    }
  };

  MockUpdateCheckerFactory<
      MockUpdateCheckerImpl<UpdateCheckerOptionsOneCrxUpdate>>
      mock_update_checker_factory;

  class MockCrxDownloader : public CrxDownloader {
   public:
    MockCrxDownloader() = default;

   private:
    ~MockCrxDownloader() override = default;

    base::OnceClosure DoStartDownload(const GURL& url) override {
      ADD_FAILURE();
      return base::DoNothing();
    }
  };

  class MockPingManager : public MockPingManagerImpl {
   public:
    explicit MockPingManager(scoped_refptr<Configurator> config)
        : MockPingManagerImpl(config) {}

   protected:
    ~MockPingManager() override {
      const std::vector<PingData>& ping_data =
          MockPingManagerImpl::terminal_ping_data();
      EXPECT_EQ(ping_data.size(), 1u);
      EXPECT_EQ(ping_data[0].id, "jebgalgnebhfojomionfpkfelancnnkf");
      EXPECT_EQ(ping_data[0].previous_version, base::Version("0.9"));
      EXPECT_EQ(ping_data[0].next_version, base::Version("1.0"));
      EXPECT_EQ(ping_data[0].error_category, ErrorCategory::kService);
      EXPECT_EQ(ping_data[0].error_code,
                std::to_underlying(ServiceError::UPDATE_DISABLED));
    }
  };

  SetMockCrxDownloader<MockCrxDownloader>();
  scoped_refptr<UpdateClient> update_client =
      base::MakeRefCounted<UpdateClientImpl>(
          config(), base::MakeRefCounted<MockPingManager>(config()),
          mock_update_checker_factory.GetFactory());

  MockObserver observer(update_client);
  {
    InSequence seq;
    EXPECT_CALL(observer, OnEvent(Truly([](const CrxUpdateItem& item) {
                  return item.id == "jebgalgnebhfojomionfpkfelancnnkf" &&
                         item.state == ComponentState::kChecking;
                })));
    EXPECT_CALL(observer, OnEvent(Truly([](const CrxUpdateItem& item) {
                  return item.id == "jebgalgnebhfojomionfpkfelancnnkf" &&
                         item.state == ComponentState::kCanUpdate;
                })));
    EXPECT_CALL(observer, OnEvent(Truly([](const CrxUpdateItem& item) {
                  return item.id == "jebgalgnebhfojomionfpkfelancnnkf" &&
                         item.state == ComponentState::kUpdateError;
                })));
  }
  std::vector<CrxUpdateItem> items;
  auto receiver = base::MakeRefCounted<MockCrxStateChangeReceiver>();
  EXPECT_CALL(*receiver, Receive(_))
      .WillRepeatedly(
          [&items](const CrxUpdateItem& item) { items.push_back(item); });

  update_client->CheckForUpdate(
      "jebgalgnebhfojomionfpkfelancnnkf",
      base::BindOnce(&DataCallbackMock::Callback),
      base::BindRepeating(&MockCrxStateChangeReceiver::Receive, receiver),
      false, ExpectErrorThenQuit(runloop_, Error::NONE));
  runloop_.Run();
  EXPECT_EQ(items.size(), 3u);
  EXPECT_EQ(items[0].state, ComponentState::kChecking);
  EXPECT_EQ(items[0].id, "jebgalgnebhfojomionfpkfelancnnkf");
  EXPECT_EQ(items[1].state, ComponentState::kCanUpdate);
  EXPECT_EQ(items[1].id, "jebgalgnebhfojomionfpkfelancnnkf");
  EXPECT_EQ(items[2].state, ComponentState::kUpdateError);
  EXPECT_EQ(items[2].id, "jebgalgnebhfojomionfpkfelancnnkf");
}

// Tests the cached update scenario for one CRX to validate that the file is
// cached if an install error occurs and re-used when the update is retried.
TEST_F(UpdateClientTest, OneCrxCachedUpdate) {
  class DataCallbackMock : public base::RefCountedThreadSafe<DataCallbackMock> {
   public:
    void Callback(
        const std::vector<std::string>& ids,
        base::OnceCallback<
            void(const std::vector<std::optional<CrxComponent>>&)> callback) {
      ++num_calls_;

      CrxComponent crx;
      crx.app_id = "jebgalgnebhfojomionfpkfelancnnkf";
      crx.name = "test_jebg";
      crx.pk_hash = base::ToVector(jebg_hash);
      crx.version = base::Version("0.9");
      crx.crx_format_requirement = crx_file::VerifierFormat::CRX3;

      scoped_refptr<TestInstaller> installer =
          base::MakeRefCounted<TestInstaller>();
      if (num_calls_ == 1) {
        installer->set_install_error(InstallError::GENERIC_ERROR);
        installer->set_installer_progress_samples({-1, 25});
      } else if (num_calls_ == 2) {
        installer->set_installer_progress_samples({-1, 50, 100});
      } else {
        ADD_FAILURE();
      }
      crx.installer = installer;

      std::move(callback).Run({crx});
    }

   private:
    friend class base::RefCountedThreadSafe<DataCallbackMock>;
    ~DataCallbackMock() = default;

    int num_calls_ = 0;
  };
  auto data_callback_mock = MakeMockCallback<DataCallbackMock>();

  MockUpdateCheckerFactory<
      MockUpdateCheckerImpl<UpdateCheckerOptionsOneCrxUpdate>>
      mock_update_checker_factory;

  class MockCrxDownloader : public CrxDownloader {
   public:
    MockCrxDownloader() = default;

   private:
    ~MockCrxDownloader() override = default;

    base::OnceClosure DoStartDownload(const GURL& url) override {
      DownloadMetrics download_metrics;
      download_metrics.url = url;
      download_metrics.downloader = DownloadMetrics::kNone;
      download_metrics.error = 0;
      download_metrics.downloaded_bytes = 54014;
      download_metrics.total_bytes = 54014;
      download_metrics.download_time_ms = 2000;

      base::FilePath path;
      EXPECT_TRUE(MakeTestFile(
          GetTestFilePath("jebgalgnebhfojomionfpkfelancnnkf.crx"), &path));

      Result result;
      result.error = 0;
      result.response = path;

      base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
          FROM_HERE, base::BindOnce(&MockCrxDownloader::OnDownloadProgress,
                                    base::Unretained(this),
                                    download_metrics.downloaded_bytes,
                                    download_metrics.total_bytes));

      base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
          FROM_HERE, base::BindOnce(&MockCrxDownloader::OnDownloadComplete,
                                    base::Unretained(this), true, result,
                                    download_metrics));
      return base::DoNothing();
    }
  };

  class MockPingManager : public MockPingManagerImpl {
   public:
    explicit MockPingManager(scoped_refptr<Configurator> config)
        : MockPingManagerImpl(config) {}

   protected:
    ~MockPingManager() override {
      const auto ping_data = MockPingManagerImpl::terminal_ping_data();
      EXPECT_EQ(2u, ping_data.size());
      EXPECT_EQ("jebgalgnebhfojomionfpkfelancnnkf", ping_data[0].id);
      EXPECT_EQ(base::Version("0.9"), ping_data[0].previous_version);
      EXPECT_EQ(base::Version("1.0"), ping_data[0].next_version);
      EXPECT_EQ(ping_data[0].error_category, ErrorCategory::kInstall);
      EXPECT_EQ(9, ping_data[0].error_code);  // GENERIC_ERROR.
      EXPECT_EQ("jebgalgnebhfojomionfpkfelancnnkf", ping_data[1].id);
      EXPECT_EQ(base::Version("0.9"), ping_data[1].previous_version);
      EXPECT_EQ(base::Version("1.0"), ping_data[1].next_version);
      EXPECT_EQ(ping_data[1].error_category, ErrorCategory::kNone);
      EXPECT_EQ(0, ping_data[1].error_code);
    }
  };

  SetMockCrxDownloader<MockCrxDownloader>();
  scoped_refptr<UpdateClient> update_client =
      base::MakeRefCounted<UpdateClientImpl>(
          config(), base::MakeRefCounted<MockPingManager>(config()),
          mock_update_checker_factory.GetFactory());

  MockObserver observer(update_client);
  {
    InSequence seq;
    EXPECT_CALL(observer, OnEvent(Truly([](const CrxUpdateItem& item) {
                  return item.id == "jebgalgnebhfojomionfpkfelancnnkf" &&
                         item.state == ComponentState::kChecking;
                })));
    EXPECT_CALL(observer, OnEvent(Truly([](const CrxUpdateItem& item) {
                  return item.id == "jebgalgnebhfojomionfpkfelancnnkf" &&
                         item.state == ComponentState::kCanUpdate;
                })));
    EXPECT_CALL(observer, OnEvent(Truly([](const CrxUpdateItem& item) {
                  return item.id == "jebgalgnebhfojomionfpkfelancnnkf" &&
                         item.state == ComponentState::kDownloading;
                })))
        .Times(2);
    EXPECT_CALL(observer, OnEvent(Truly([](const CrxUpdateItem& item) {
                  return item.id == "jebgalgnebhfojomionfpkfelancnnkf" &&
                         item.state == ComponentState::kUpdating;
                })));
    EXPECT_CALL(observer, OnEvent(Truly([](const CrxUpdateItem& item) {
                  return item.id == "jebgalgnebhfojomionfpkfelancnnkf" &&
                         item.state == ComponentState::kUpdating;
                })))
        .Times(2);
    EXPECT_CALL(observer, OnEvent(Truly([](const CrxUpdateItem& item) {
                  return item.id == "jebgalgnebhfojomionfpkfelancnnkf" &&
                         item.state == ComponentState::kUpdateError;
                })));

    EXPECT_CALL(observer, OnEvent(Truly([](const CrxUpdateItem& item) {
                  return item.id == "jebgalgnebhfojomionfpkfelancnnkf" &&
                         item.state == ComponentState::kChecking;
                })));
    EXPECT_CALL(observer, OnEvent(Truly([](const CrxUpdateItem& item) {
                  return item.id == "jebgalgnebhfojomionfpkfelancnnkf" &&
                         item.state == ComponentState::kCanUpdate;
                })));
    EXPECT_CALL(observer, OnEvent(Truly([](const CrxUpdateItem& item) {
                  return item.id == "jebgalgnebhfojomionfpkfelancnnkf" &&
                         item.state == ComponentState::kUpdating;
                })));
    EXPECT_CALL(observer, OnEvent(Truly([](const CrxUpdateItem& item) {
                  return item.id == "jebgalgnebhfojomionfpkfelancnnkf" &&
                         item.state == ComponentState::kUpdating;
                })))
        .Times(3);
    EXPECT_CALL(observer, OnEvent(Truly([](const CrxUpdateItem& item) {
                  return item.id == "jebgalgnebhfojomionfpkfelancnnkf" &&
                         item.state == ComponentState::kUpdated;
                })));
  }

  const std::vector<std::string> ids = {"jebgalgnebhfojomionfpkfelancnnkf"};
  {
    std::vector<CrxUpdateItem> items;
    auto receiver = base::MakeRefCounted<MockCrxStateChangeReceiver>();
    EXPECT_CALL(*receiver, Receive(_))
        .WillRepeatedly(
            [&items](const CrxUpdateItem& item) { items.push_back(item); });

    base::RunLoop runloop;
    update_client->Update(
        ids, data_callback_mock,
        base::BindRepeating(&MockCrxStateChangeReceiver::Receive, receiver),
        false, ExpectErrorThenQuit(runloop, Error::NONE));
    runloop.Run();

    EXPECT_EQ(8u, items.size());
    EXPECT_EQ(ComponentState::kChecking, items[0].state);
    EXPECT_EQ("jebgalgnebhfojomionfpkfelancnnkf", items[0].id);
    EXPECT_EQ(ComponentState::kCanUpdate, items[1].state);
    EXPECT_EQ("jebgalgnebhfojomionfpkfelancnnkf", items[1].id);
    EXPECT_EQ(ComponentState::kDownloading, items[2].state);
    EXPECT_EQ("jebgalgnebhfojomionfpkfelancnnkf", items[2].id);
    EXPECT_EQ(ComponentState::kDownloading, items[3].state);
    EXPECT_EQ("jebgalgnebhfojomionfpkfelancnnkf", items[3].id);
    EXPECT_EQ(ComponentState::kUpdating, items[4].state);
    EXPECT_EQ("jebgalgnebhfojomionfpkfelancnnkf", items[4].id);
    EXPECT_EQ(ComponentState::kUpdating, items[5].state);
    EXPECT_EQ("jebgalgnebhfojomionfpkfelancnnkf", items[5].id);
    EXPECT_EQ(ComponentState::kUpdating, items[6].state);
    EXPECT_EQ("jebgalgnebhfojomionfpkfelancnnkf", items[6].id);
    EXPECT_EQ(ComponentState::kUpdateError, items[7].state);
    EXPECT_EQ("jebgalgnebhfojomionfpkfelancnnkf", items[7].id);

    std::vector samples = {-1, -1, -1, -1, -1, -1, 25, 25};
    EXPECT_EQ(items.size(), samples.size());
    for (size_t i = 0; i != items.size(); ++i) {
      EXPECT_EQ(items[i].install_progress, samples[i]);
    }
  }

  {
    std::vector<CrxUpdateItem> items;
    auto receiver = base::MakeRefCounted<MockCrxStateChangeReceiver>();
    EXPECT_CALL(*receiver, Receive(_))
        .WillRepeatedly(
            [&items](const CrxUpdateItem& item) { items.push_back(item); });

    base::RunLoop runloop;
    update_client->Update(
        ids, data_callback_mock,
        base::BindRepeating(&MockCrxStateChangeReceiver::Receive, receiver),
        false, ExpectErrorThenQuit(runloop, Error::NONE));
    runloop.Run();

    EXPECT_EQ(7u, items.size());
    EXPECT_EQ(ComponentState::kChecking, items[0].state);
    EXPECT_EQ("jebgalgnebhfojomionfpkfelancnnkf", items[0].id);
    EXPECT_EQ(ComponentState::kCanUpdate, items[1].state);
    EXPECT_EQ("jebgalgnebhfojomionfpkfelancnnkf", items[1].id);
    EXPECT_EQ(ComponentState::kUpdating, items[2].state);
    EXPECT_EQ("jebgalgnebhfojomionfpkfelancnnkf", items[2].id);
    EXPECT_EQ(ComponentState::kUpdating, items[3].state);
    EXPECT_EQ("jebgalgnebhfojomionfpkfelancnnkf", items[3].id);
    EXPECT_EQ(ComponentState::kUpdating, items[4].state);
    EXPECT_EQ("jebgalgnebhfojomionfpkfelancnnkf", items[4].id);
    EXPECT_EQ(ComponentState::kUpdating, items[5].state);
    EXPECT_EQ("jebgalgnebhfojomionfpkfelancnnkf", items[5].id);
    EXPECT_EQ(ComponentState::kUpdated, items[6].state);
    EXPECT_EQ("jebgalgnebhfojomionfpkfelancnnkf", items[6].id);

    std::vector samples = {-1, -1, -1, -1, 50, 100, 100};
    EXPECT_EQ(items.size(), samples.size());
    for (size_t i = 0; i != items.size(); ++i) {
      EXPECT_EQ(items[i].install_progress, samples[i]);
    }
  }
}

TEST_F(UpdateClientTest, UnsupportedOperationType) {
  class DataCallbackMock {
   public:
    static void Callback(
        const std::vector<std::string>& ids,
        base::OnceCallback<
            void(const std::vector<std::optional<CrxComponent>>&)> callback) {
      CrxComponent crx1;
      crx1.app_id = "jebgalgnebhfojomionfpkfelancnnkf";
      crx1.name = "test_jebg";
      crx1.pk_hash = base::ToVector(jebg_hash);
      crx1.version = base::Version("0.9");
      crx1.installer = base::MakeRefCounted<TestInstaller>();
      crx1.crx_format_requirement = crx_file::VerifierFormat::CRX3;

      std::move(callback).Run({crx1});
    }
  };

  MockUpdateCheckerFactory<
      MockUpdateCheckerImpl<UpdateCheckerOptionsUnsupportedOperationType>>
      mock_update_checker_factory;

  class MockCrxDownloader : public CrxDownloader {
   public:
    MockCrxDownloader() = default;

   private:
    ~MockCrxDownloader() override = default;

    base::OnceClosure DoStartDownload(const GURL& url) override {
      ADD_FAILURE() << "Intentionally forcing download to fail here.";
      return base::DoNothing();
    }
  };

  class MockPingManager : public MockPingManagerImpl {
   public:
    explicit MockPingManager(scoped_refptr<Configurator> config)
        : MockPingManagerImpl(config) {}

   protected:
    ~MockPingManager() override {
      const auto ping_data = MockPingManagerImpl::ping_data();
      EXPECT_EQ(2u, ping_data.size());

      // unsupported operation event
      EXPECT_EQ(ping_data[0].id, "jebgalgnebhfojomionfpkfelancnnkf");
      EXPECT_EQ(ping_data[0].previous_version, base::Version("0.9"));
      EXPECT_EQ(ping_data[0].next_version, base::Version("1.0"));
      EXPECT_EQ(ping_data[0].error_category, ErrorCategory::kUpdateCheck);
      EXPECT_EQ(ping_data[0].event_type, protocol_request::kEventUnknown);

      // update check
      EXPECT_EQ("jebgalgnebhfojomionfpkfelancnnkf", ping_data[1].id);
      EXPECT_EQ(base::Version("0.9"), ping_data[1].previous_version);
      EXPECT_EQ(base::Version("1.0"), ping_data[1].next_version);
      EXPECT_EQ(ping_data[1].error_category, ErrorCategory::kUpdateCheck);
      EXPECT_EQ(ping_data[1].event_type, protocol_request::kEventUpdate);
    }
  };

  SetMockCrxDownloader<MockCrxDownloader>();
  scoped_refptr<UpdateClient> update_client =
      base::MakeRefCounted<UpdateClientImpl>(
          config(), base::MakeRefCounted<MockPingManager>(config()),
          mock_update_checker_factory.GetFactory());

  MockObserver observer(update_client);
  {
    InSequence seq;
    EXPECT_CALL(observer, OnEvent(Truly([](const CrxUpdateItem& item) {
                  return item.id == "jebgalgnebhfojomionfpkfelancnnkf" &&
                         item.state == ComponentState::kChecking;
                })));
    EXPECT_CALL(observer, OnEvent(Truly([](const CrxUpdateItem& item) {
                  return item.id == "jebgalgnebhfojomionfpkfelancnnkf" &&
                         item.state == ComponentState::kCanUpdate;
                })));
    EXPECT_CALL(observer, OnEvent(Truly([](const CrxUpdateItem& item) {
                  return item.id == "jebgalgnebhfojomionfpkfelancnnkf" &&
                         item.state == ComponentState::kUpdateError;
                })));
  }

  std::vector<CrxUpdateItem> items;
  auto receiver = base::MakeRefCounted<MockCrxStateChangeReceiver>();
  EXPECT_CALL(*receiver, Receive(_))
      .WillRepeatedly(
          [&items](const CrxUpdateItem& item) { items.push_back(item); });

  const std::vector<std::string> ids = {"jebgalgnebhfojomionfpkfelancnnkf"};
  update_client->Update(
      ids, base::BindOnce(&DataCallbackMock::Callback),
      base::BindRepeating(&MockCrxStateChangeReceiver::Receive, receiver),
      false, ExpectErrorThenQuit(runloop_, Error::NONE));
  runloop_.Run();

  EXPECT_EQ(3u, items.size());
  EXPECT_EQ(ComponentState::kChecking, items[0].state);
  EXPECT_EQ("jebgalgnebhfojomionfpkfelancnnkf", items[0].id);
  EXPECT_EQ(ComponentState::kCanUpdate, items[1].state);
  EXPECT_EQ("jebgalgnebhfojomionfpkfelancnnkf", items[1].id);
  EXPECT_EQ(ComponentState::kUpdateError, items[2].state);
  EXPECT_EQ("jebgalgnebhfojomionfpkfelancnnkf", items[2].id);
}

class UpdateClientCleanupTest : public UpdateClientTest {
 protected:
  base::FilePath temp_dir_;

  void SetUp() override {
#if BUILDFLAG(IS_WIN)
    ASSERT_TRUE(base::GetSecureTempDirectory(&temp_dir_));
#else   // BUILDFLAG(IS_WIN)
    ASSERT_TRUE(base::GetTempDir(&temp_dir_));
#endif  // BUILDFLAG(IS_WIN)

    config()->SetProdId(
        base::Uuid::GenerateRandomV4().AsLowercaseString().substr(0, 8));
  }

  base::FilePath CreateFakeDownloadDir(const std::string& prod_id,
                                       const std::string& suffix,
                                       base::TimeDelta age) {
    // Pattern matching `UpdateClient::CreateDownloadDir`.
    const base::FilePath path = temp_dir_.Append(
        base::StrCat({UTF8ToStringType(prod_id),
                      FILE_PATH_LITERAL("_chrome_url_fetcher_")}) +
        UTF8ToStringType(suffix));
    EXPECT_TRUE(base::CreateDirectory(path));

    if (age.is_zero()) {
      return path;
    }

#if BUILDFLAG(IS_WIN)
    // Manually set the directory's creation/access time to simulate age.
    FILETIME creation_filetime =
        (base::Time::NowFromSystemTime() + age).ToFileTime();
    base::File download_dir(path, base::File::FLAG_OPEN |
                                      base::File::FLAG_WIN_BACKUP_SEMANTICS |
                                      base::File::FLAG_WRITE_ATTRIBUTES);
    EXPECT_TRUE(download_dir.IsValid());
    EXPECT_TRUE(::SetFileTime(download_dir.GetPlatformFile(),
                              &creation_filetime, NULL, NULL));
    download_dir.Close();
#endif  // BUILDFLAG(IS_WIN)

    return path;
  }
};

TEST_F(UpdateClientCleanupTest, CleansStaleDirectoriesOnConstruction) {
  constexpr base::TimeDelta kStaleTime = base::Seconds(0);
  constexpr base::TimeDelta kFreshTime = base::Minutes(10);

  const base::FilePath stale_dir =
      CreateFakeDownloadDir(config()->GetProdId(), "stale", kStaleTime);
  const base::FilePath fresh_dir =
      CreateFakeDownloadDir(config()->GetProdId(), "fresh", kFreshTime);

  // Create a stale directory for a different prod_id. It should not be deleted.
  const base::FilePath other_prod_dir = CreateFakeDownloadDir(
      base::StrCat({"other_prod", config()->GetProdId()}), "stale", kStaleTime);

  // Instantiate `UpdateClient` and call `CleanupStaleDownloads`, which should
  // trigger the asynchronous cleanup.
  base::MakeRefCounted<UpdateClientImpl>(
      config(), base::MakeRefCounted<MockPingManagerImpl>(config()),
      MockUpdateCheckerFactory<
          MockUpdateCheckerImpl<UpdateCheckerOptionsUnsupportedOperationType>>()
          .GetFactory())
      ->CleanupStaleDownloads(base::Time::Now(), base::DoNothing());

  // Wait to allow the cleanup task to execute.
  base::ThreadPoolInstance::Get()->FlushForTesting();

  EXPECT_FALSE(base::PathExists(stale_dir))
      << stale_dir << " should be deleted.";

#if BUILDFLAG(IS_WIN)
  EXPECT_TRUE(base::PathExists(fresh_dir))
      << fresh_dir << " should be preserved.";
#else   // BUILDFLAG(IS_WIN)
  EXPECT_FALSE(base::PathExists(fresh_dir))
      << fresh_dir << " should be deleted.";
#endif  // BUILDFLAG(IS_WIN)

  EXPECT_TRUE(base::PathExists(other_prod_dir))
      << other_prod_dir << " should be preserved.";

#if BUILDFLAG(IS_WIN)
  ASSERT_TRUE(RetryFileOperation(&base::DeletePathRecursively, fresh_dir));
#endif  // BUILDFLAG(IS_WIN)

  ASSERT_TRUE(RetryFileOperation(&base::DeletePathRecursively, other_prod_dir));
}

TEST_F(UpdateClientTest,
       AllPipelinesContainingOperationsWithInvalidAttributesNoUpdate) {
  class DataCallbackMock {
   public:
    static void Callback(
        const std::vector<std::string>& ids,
        base::OnceCallback<
            void(const std::vector<std::optional<CrxComponent>>&)> callback) {
      CrxComponent crx1;
      crx1.app_id = "jebgalgnebhfojomionfpkfelancnnkf";
      crx1.name = "test_jebg";
      crx1.pk_hash = base::ToVector(jebg_hash);
      crx1.version = base::Version("0.9");
      crx1.installer = base::MakeRefCounted<TestInstaller>();
      crx1.crx_format_requirement = crx_file::VerifierFormat::CRX3;

      std::move(callback).Run({crx1});
    }
  };

  class MockUpdateChecker : public UpdateChecker {
   public:
    MockUpdateChecker() = default;

    void CheckForUpdates(
        scoped_refptr<UpdateContext> context,
        const base::flat_map<std::string, std::string>& additional_attributes,
        UpdateCheckCallback update_check_callback) override {
      context->get_available_space = base::BindRepeating(
          [](const base::FilePath&) -> int64_t { return 0; });
      // The puff operation is missing previous hash, so it fails.
      base::expected<ProtocolParser::Results, std::string> results =
          ProtocolParserJSON::ParseJSON(R"()]}'
{
  "response": {
    "protocol": "4.0",
    "apps": [
      {
        "appid": "jebgalgnebhfojomionfpkfelancnnkf",
        "status": "ok",
        "updatecheck": {
          "status": "ok",
          "nextversion": "1.0",
          "pipelines": [
            {
              "pipeline_id": "download_missing_urls",
              "operations": [
                {
                  "type": "download",
                  "size": 10,
                  "out": {
                    "sha256": "7ab32f071cd9b5ef8e0d7913be161f532d98b3e9fa284a7cd8059c3409ce0498"
                  }
                }
              ]
            },
            {
              "pipeline_id": "download_missing_out",
              "operations": [
                {
                  "type": "download",
                  "size": 10,
                  "urls": {
                    "url": "http://does.not.matter.com/file.crx"
                  }
                }
              ]
            },
            {
              "pipeline_id": "download_invalid_size",
              "operations": [
                {
                  "type": "download",
                  "size": -10,
                  "urls": {
                    "url": "http://does.not.matter.com/file.crx"
                  },
                  "out": {
                    "sha256": "7ab32f071cd9b5ef8e0d7913be161f532d98b3e9fa284a7cd8059c3409ce0498"
                  }
                }
              ]
            },
            {
              "pipeline_id": "puff_missing_prev",
              "operations": [
                {
                  "type": "puff",
                  "out": {
                    "sha256": "7ab32f071cd9b5ef8e0d7913be161f532d98b3e9fa284a7cd8059c3409ce0498"
                  }
                }
              ]
            },
            {
              "pipeline_id": "zucc_missing_prev",
              "operations": [
                {
                  "type": "zucc",
                  "out": {
                    "sha256": "7ab32f071cd9b5ef8e0d7913be161f532d98b3e9fa284a7cd8059c3409ce0498"
                  }
                }
              ]
            },
            {
              "pipeline_id": "crx3_missing_in",
              "operations": [
                {
                  "type": "crx3"
                }
              ]
            }
          ]
        }
      }
    ]
  }
})");
      EXPECT_TRUE(results.has_value()) << results.error();
      EXPECT_FALSE(context->session_id.empty());
      EXPECT_EQ(context->components_to_check_for_updates.size(), 1u);
      base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
          FROM_HERE,
          base::BindOnce(std::move(update_check_callback), results.value(),
                         ErrorCategory::kNone, 0, 0));
    }
  };
  MockUpdateCheckerFactory<MockUpdateChecker> mock_update_checker_factory;

  class MockCrxDownloader : public CrxDownloader {
   public:
    MockCrxDownloader() = default;

   private:
    ~MockCrxDownloader() override = default;

    base::OnceClosure DoStartDownload(const GURL& url) override {
      ADD_FAILURE();
      return base::DoNothing();
    }
  };

  class MockPingManager : public MockPingManagerImpl {
   public:
    explicit MockPingManager(scoped_refptr<Configurator> config)
        : MockPingManagerImpl(config) {}

   protected:
    ~MockPingManager() override {
      const auto ping_data = MockPingManagerImpl::ping_data();
      EXPECT_EQ(ping_data.size(), 7u);

      // Download failures:
      auto noUrls = ping_data[0];
      EXPECT_EQ(noUrls.id, "jebgalgnebhfojomionfpkfelancnnkf");
      EXPECT_EQ(noUrls.pipeline_id, "download_missing_urls");
      EXPECT_EQ(noUrls.error_category, ErrorCategory::kUpdateCheck);
      EXPECT_EQ(noUrls.event_type, protocol_request::kEventDownload);

      auto noOutHash = ping_data[1];
      EXPECT_EQ(noOutHash.id, "jebgalgnebhfojomionfpkfelancnnkf");
      EXPECT_EQ(noOutHash.pipeline_id, "download_missing_out");
      EXPECT_EQ(noOutHash.error_category, ErrorCategory::kUpdateCheck);
      EXPECT_EQ(noOutHash.event_type, protocol_request::kEventDownload);

      auto badSize = ping_data[2];
      EXPECT_EQ(badSize.id, "jebgalgnebhfojomionfpkfelancnnkf");
      EXPECT_EQ(badSize.pipeline_id, "download_invalid_size");
      EXPECT_EQ(badSize.error_category, ErrorCategory::kUpdateCheck);
      EXPECT_EQ(badSize.event_type, protocol_request::kEventDownload);

      auto puff = ping_data[3];
      EXPECT_EQ(puff.id, "jebgalgnebhfojomionfpkfelancnnkf");
      EXPECT_EQ(puff.pipeline_id, "puff_missing_prev");
      EXPECT_EQ(puff.error_category, ErrorCategory::kUpdateCheck);
      EXPECT_EQ(puff.event_type, protocol_request::kEventPuff);

      auto zucc = ping_data[4];
      EXPECT_EQ(zucc.id, "jebgalgnebhfojomionfpkfelancnnkf");
      EXPECT_EQ(zucc.pipeline_id, "zucc_missing_prev");
      EXPECT_EQ(zucc.error_category, ErrorCategory::kUpdateCheck);
      EXPECT_EQ(zucc.event_type, protocol_request::kEventZucchini);

      auto crx3 = ping_data[5];
      EXPECT_EQ(crx3.id, "jebgalgnebhfojomionfpkfelancnnkf");
      EXPECT_EQ(crx3.pipeline_id, "crx3_missing_in");
      EXPECT_EQ(crx3.error_category, ErrorCategory::kUpdateCheck);
      EXPECT_EQ(crx3.event_type, protocol_request::kEventCrx3);

      auto update = ping_data[6];
      EXPECT_EQ(update.id, "jebgalgnebhfojomionfpkfelancnnkf");
      EXPECT_TRUE(update.pipeline_id.empty());
      EXPECT_EQ(update.error_category, ErrorCategory::kUpdateCheck);
      EXPECT_EQ(update.event_type, protocol_request::kEventUpdate);
    }
  };

  SetMockCrxDownloader<MockCrxDownloader>();
  scoped_refptr<UpdateClient> update_client =
      base::MakeRefCounted<UpdateClientImpl>(
          config(), base::MakeRefCounted<MockPingManager>(config()),
          mock_update_checker_factory.GetFactory());

  MockObserver observer(update_client);
  {
    InSequence seq;
    EXPECT_CALL(observer, OnEvent(Truly([](const CrxUpdateItem& item) {
                  return item.id == "jebgalgnebhfojomionfpkfelancnnkf" &&
                         item.state == ComponentState::kChecking;
                })));
    EXPECT_CALL(observer, OnEvent(Truly([](const CrxUpdateItem& item) {
                  return item.id == "jebgalgnebhfojomionfpkfelancnnkf" &&
                         item.state == ComponentState::kCanUpdate;
                })));
    EXPECT_CALL(observer, OnEvent(Truly([](const CrxUpdateItem& item) {
                  return item.id == "jebgalgnebhfojomionfpkfelancnnkf" &&
                         item.state == ComponentState::kUpdateError;
                })));
  }

  std::vector<CrxUpdateItem> items;
  auto receiver = base::MakeRefCounted<MockCrxStateChangeReceiver>();
  EXPECT_CALL(*receiver, Receive(_))
      .WillRepeatedly(
          [&items](const CrxUpdateItem& item) { items.push_back(item); });

  const std::vector<std::string> ids = {"jebgalgnebhfojomionfpkfelancnnkf"};
  update_client->Update(
      ids, base::BindOnce(&DataCallbackMock::Callback),
      base::BindRepeating(&MockCrxStateChangeReceiver::Receive, receiver),
      false, ExpectErrorThenQuit(runloop_, Error::NONE));
  runloop_.Run();

  EXPECT_EQ(3u, items.size());
  EXPECT_EQ(ComponentState::kChecking, items[0].state);
  EXPECT_EQ("jebgalgnebhfojomionfpkfelancnnkf", items[0].id);
  EXPECT_EQ(ComponentState::kCanUpdate, items[1].state);
  EXPECT_EQ("jebgalgnebhfojomionfpkfelancnnkf", items[1].id);
  EXPECT_EQ(ComponentState::kUpdateError, items[2].state);
  EXPECT_EQ("jebgalgnebhfojomionfpkfelancnnkf", items[2].id);
}

// Tests cancellation of an active download when `UpdateClient::Stop` is called.
TEST_F(UpdateClientTest, Install_StopCancelsActiveDownload) {
  base::FilePath temp_dir;
#if BUILDFLAG(IS_WIN)
  ASSERT_TRUE(base::GetSecureTempDirectory(&temp_dir));
#else   // BUILDFLAG(IS_WIN)
  ASSERT_TRUE(base::GetTempDir(&temp_dir));
#endif  // BUILDFLAG(IS_WIN)

  base::FileEnumerator(
      temp_dir, /*recursive=*/false, base::FileEnumerator::DIRECTORIES,
      base::StrCat({UTF8ToStringType(config()->GetProdId()),
                    FILE_PATH_LITERAL("_chrome_url_fetcher_")}))
      .ForEach([](const base::FilePath& path) {
        ASSERT_TRUE(RetryFileOperation(&base::DeletePathRecursively, path));
      });

  class DataCallbackMock {
   public:
    static void Callback(
        const std::vector<std::string>& ids,
        base::OnceCallback<
            void(const std::vector<std::optional<CrxComponent>>&)> callback) {
      CrxComponent crx;
      crx.app_id = "jebgalgnebhfojomionfpkfelancnnkf";
      crx.name = "test_jebg";
      crx.pk_hash = base::ToVector(jebg_hash);
      crx.version = base::Version("0.0");
      crx.installer = base::MakeRefCounted<TestInstaller>();
      crx.crx_format_requirement = crx_file::VerifierFormat::CRX3;
      std::move(callback).Run({crx});
    }
  };

  MockUpdateCheckerFactory<
      MockUpdateCheckerImpl<UpdateCheckerOptionsOneCrxInstall>>
      mock_update_checker_factory;

  class MockCrxDownloader : public CrxDownloader {
   public:
    MockCrxDownloader() = default;

   private:
    ~MockCrxDownloader() override = default;

    base::OnceClosure DoStartDownload(const GURL& url) override {
      DownloadMetrics download_metrics;
      base::FilePath path;
      Result result;
      if (url.GetPath() == "/download/jebgalgnebhfojomionfpkfelancnnkf.crx") {
        download_metrics.url = url;
        download_metrics.downloader = DownloadMetrics::kNone;
        download_metrics.error = 0;
        download_metrics.downloaded_bytes = 1015;
        download_metrics.total_bytes = 1015;
        download_metrics.download_time_ms = 1000;

        EXPECT_TRUE(MakeTestFile(
            GetTestFilePath("jebgalgnebhfojomionfpkfelancnnkf.crx"), &path));

        result.error = 0;
        result.response = path;
      } else {
        ADD_FAILURE();
      }

      base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
          FROM_HERE, base::BindOnce(&MockCrxDownloader::OnDownloadProgress,
                                    base::Unretained(this),
                                    download_metrics.downloaded_bytes,
                                    download_metrics.total_bytes));

      base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
          FROM_HERE, base::BindOnce(&MockCrxDownloader::OnDownloadComplete,
                                    base::Unretained(this), true, result,
                                    download_metrics));

      EXPECT_CALL(cancel_callback_, Run()).Times(testing::AtLeast(1));
      return cancel_callback_.Get();
    }

    base::MockCallback<base::OnceClosure> cancel_callback_;
  };

  class MockPingManager : public MockPingManagerImpl {
   public:
    explicit MockPingManager(scoped_refptr<Configurator> config)
        : MockPingManagerImpl(config) {}

   protected:
    ~MockPingManager() override {
      // Verify the ping data shows that the active task was cancelled.
      const auto ping_data = MockPingManagerImpl::terminal_ping_data();
      EXPECT_EQ(1u, ping_data.size());
      EXPECT_EQ("jebgalgnebhfojomionfpkfelancnnkf", ping_data[0].id);
      EXPECT_EQ(base::Version("0.0"), ping_data[0].previous_version);
      EXPECT_EQ(base::Version("1.0"), ping_data[0].next_version);
      EXPECT_EQ(ErrorCategory::kService, ping_data[0].error_category);
      EXPECT_EQ(static_cast<int>(ServiceError::CANCELLED),
                ping_data[0].error_code);
    }
  };

  SetMockCrxDownloader<MockCrxDownloader>();
  scoped_refptr<UpdateClient> update_client =
      base::MakeRefCounted<UpdateClientImpl>(
          config(), base::MakeRefCounted<MockPingManager>(config()),
          mock_update_checker_factory.GetFactory());

  MockObserver observer(update_client);
  {
    InSequence seq;
    EXPECT_CALL(observer, OnEvent(Truly([](const CrxUpdateItem& item) {
                  return item.id == "jebgalgnebhfojomionfpkfelancnnkf" &&
                         item.state == ComponentState::kChecking;
                })));
    EXPECT_CALL(observer, OnEvent(Truly([](const CrxUpdateItem& item) {
                  return item.id == "jebgalgnebhfojomionfpkfelancnnkf" &&
                         item.state == ComponentState::kCanUpdate;
                })));
    EXPECT_CALL(observer, OnEvent(Truly([](const CrxUpdateItem& item) {
                  return item.id == "jebgalgnebhfojomionfpkfelancnnkf" &&
                         item.state == ComponentState::kDownloading;
                })))
        .Times(AtLeast(1))
        .WillRepeatedly([&update_client] {
          // Call `Stop` during the download.
          update_client->Stop();
        });
    EXPECT_CALL(observer, OnEvent(Truly([](const CrxUpdateItem& item) {
                  return item.id == "jebgalgnebhfojomionfpkfelancnnkf" &&
                         item.state == ComponentState::kUpdateError;
                })));
  }

  std::vector<CrxUpdateItem> items;
  auto receiver = base::MakeRefCounted<MockCrxStateChangeReceiver>();
  EXPECT_CALL(*receiver, Receive(_))
      .WillRepeatedly(
          [&items](const CrxUpdateItem& item) { items.push_back(item); });

  update_client->Install(
      std::string("jebgalgnebhfojomionfpkfelancnnkf"),
      base::BindOnce(&DataCallbackMock::Callback),
      base::BindRepeating(&MockCrxStateChangeReceiver::Receive, receiver),
      ExpectErrorThenQuit(runloop_, Error::NONE));
  runloop_.Run();

  EXPECT_EQ(5u, items.size());
  EXPECT_EQ(ComponentState::kChecking, items[0].state);
  EXPECT_EQ("jebgalgnebhfojomionfpkfelancnnkf", items[0].id);
  EXPECT_EQ(ComponentState::kCanUpdate, items[1].state);
  EXPECT_EQ("jebgalgnebhfojomionfpkfelancnnkf", items[1].id);
  EXPECT_EQ(ComponentState::kDownloading, items[2].state);
  EXPECT_EQ("jebgalgnebhfojomionfpkfelancnnkf", items[2].id);
  EXPECT_EQ(ComponentState::kDownloading, items[3].state);
  EXPECT_EQ("jebgalgnebhfojomionfpkfelancnnkf", items[3].id);
  EXPECT_EQ(ComponentState::kUpdateError, items[4].state);
  EXPECT_EQ("jebgalgnebhfojomionfpkfelancnnkf", items[4].id);

  base::FileEnumerator(
      temp_dir, /*recursive=*/false, base::FileEnumerator::DIRECTORIES,
      base::StrCat({UTF8ToStringType(config()->GetProdId()),
                    FILE_PATH_LITERAL("_chrome_url_fetcher_")}))
      .ForEach([](const base::FilePath& path) {
        ADD_FAILURE() << "Unexpected left over directory found: " << path;
      });
}

}  // namespace update_client
