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

#include "chrome/browser/download/bubble/download_bubble_update_service.h"

#include <memory>

#include "base/strings/utf_string_conversions.h"
#include "base/time/time.h"
#include "build/build_config.h"
#include "chrome/browser/download/bubble/download_bubble_display_info.h"
#include "chrome/browser/download/bubble/download_bubble_update_service_factory.h"
#include "chrome/browser/download/chrome_download_manager_delegate.h"
#include "chrome/browser/download/download_core_service.h"
#include "chrome/browser/download/download_core_service_factory.h"
#include "chrome/browser/download/download_core_service_impl.h"
#include "chrome/browser/download/download_item_model.h"
#include "chrome/browser/download/download_item_web_app_data.h"
#include "chrome/browser/download/download_ui_model.h"
#include "chrome/browser/history/history_service_factory.h"
#include "chrome/browser/offline_items_collection/offline_content_aggregator_factory.h"
#include "chrome/browser/profiles/profile_key.h"
#include "chrome/test/base/testing_browser_process.h"
#include "chrome/test/base/testing_profile.h"
#include "chrome/test/base/testing_profile_manager.h"
#include "components/download/public/common/download_item.h"
#include "components/download/public/common/mock_download_item.h"
#include "components/keyed_service/core/service_access_type.h"
#include "components/offline_items_collection/core/offline_item.h"
#include "components/offline_items_collection/core/offline_item_state.h"
#include "components/offline_items_collection/core/test_support/mock_offline_content_provider.h"
#include "components/webapps/common/web_app_id.h"
#include "content/public/browser/download_item_utils.h"
#include "content/public/test/browser_task_environment.h"
#include "content/public/test/mock_download_manager.h"
#include "content/public/test/test_utils.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"

#if BUILDFLAG(IS_MAC)
#include "base/mac/mac_util.h"
#endif

namespace {

using ::download::DownloadDangerType;
using ::offline_items_collection::OfflineItem;
using ::offline_items_collection::OfflineItemState;
using ::testing::_;
using ::testing::AllOf;
using ::testing::IsEmpty;
using ::testing::NiceMock;
using ::testing::Return;
using ::testing::ReturnRefOfCopy;
using ::testing::UnorderedElementsAre;
using ::testing::WithArg;
using Alert = DownloadBubbleAccessibleAlertsMap::Alert;
using DownloadState = download::DownloadItem::DownloadState;
using DownloadUIModelPtrVector =
    std::vector<DownloadUIModel::DownloadUIModelPtr>;
using NiceMockDownloadItem = NiceMock<download::MockDownloadItem>;

constexpr char kProfileName[] = "testing_profile";
constexpr char kProviderNamespace[] = "mock_namespace";

// Helper for MockDownloadManager to properly mimic GetAllDownloads(), such that
// it appends all items in |items| to the argument of the action, which should
// be a std::vector<DownloadItem*>.
ACTION_P(AddDownloadItems, items) {
  for (download::DownloadItem* item : items) {
    arg0->push_back(item);
  }
}

// Matcher that works with substrings of u16string.
MATCHER_P(HasSubstr16, substring, "") {
  return arg.find(substring) != std::u16string::npos;
}

void RemoveDownloadItemsObserver(
    std::vector<std::unique_ptr<NiceMockDownloadItem>>& download_items,
    download::DownloadItem::Observer& observer) {
  for (auto& item : download_items) {
    item->RemoveObserver(&observer);
  }
}

std::unique_ptr<KeyedService> SetUpDownloadCoreService(
    content::DownloadManager* download_manager,
    content::BrowserContext* context) {
  Profile* profile = static_cast<Profile*>(context);
  auto dcs = std::make_unique<DownloadCoreServiceImpl>(profile);
  auto cdmd = std::make_unique<ChromeDownloadManagerDelegate>(profile);
  cdmd->SetDownloadManager(download_manager);
  dcs->SetDownloadManagerDelegateForTesting(std::move(cdmd));
  auto download_history = std::make_unique<DownloadHistory>(
      download_manager, std::make_unique<DownloadHistory::HistoryAdapter>(
                            HistoryServiceFactory::GetInstance()->GetForProfile(
                                profile, ServiceAccessType::EXPLICIT_ACCESS)));
  dcs->SetDownloadHistoryForTesting(std::move(download_history));
  return dcs;
}

class DownloadBubbleUpdateServiceTest : public testing::Test {
 public:
  DownloadBubbleUpdateServiceTest()
      : testing_profile_manager_(TestingBrowserProcess::GetGlobal()) {}
  DownloadBubbleUpdateServiceTest(const DownloadBubbleUpdateServiceTest&) =
      delete;
  DownloadBubbleUpdateServiceTest& operator=(
      const DownloadBubbleUpdateServiceTest&) = delete;
  ~DownloadBubbleUpdateServiceTest() override = default;

  std::unique_ptr<NiceMock<content::MockDownloadManager>>
  SetUpDownloadManager() {
    auto download_manager =
        std::make_unique<NiceMock<content::MockDownloadManager>>();
    EXPECT_CALL(*download_manager, RemoveObserver(_)).WillRepeatedly(Return());
    // Default case for when no download exists with the requested guid.
    EXPECT_CALL(*download_manager, GetDownloadByGuid(_))
        .WillRepeatedly(Return(nullptr));
    return download_manager;
  }

  std::unique_ptr<KeyedService> CreateUpdateService(
      DownloadBubbleUpdateService* preexisting_update_service,
      content::BrowserContext* context) {
    // Unregister the observer from the previous instance of the update service.
    // See note below in TearDown() for why this is necessary.
    if (preexisting_update_service) {
      RemoveDownloadItemsObserver(
          download_items_,
          preexisting_update_service->download_item_notifier_for_testing());
    }
    auto service = std::make_unique<DownloadBubbleUpdateService>(
        Profile::FromBrowserContext(context));
    return service;
  }

  void InitializeUpdateService() {
    update_service_->Initialize(download_manager_);
    // Run the offline content provider initialization.
    task_environment_.RunUntilIdle();
  }

  void SetUp() override {
    ASSERT_TRUE(testing_profile_manager_.SetUp());
    // The order of these initialization steps is important. The DownloadManager
    // must be created and set on the DownloadCoreService before setting up the
    // history service and calling DownloadBubbleUpdateService::Initialize.
    // Otherwise GetDownloadHistory will cause Initialize() to be called a
    // second time, violating invariants assumed by the Update Service.
    std::unique_ptr<NiceMock<content::MockDownloadManager>> download_manager =
        SetUpDownloadManager();
    download_manager_ = download_manager.get();
    TestingProfile::TestingFactories testing_factories;
    testing_factories.emplace_back(HistoryServiceFactory::GetInstance(),
                                   HistoryServiceFactory::GetDefaultFactory());
    testing_factories.emplace_back(
        DownloadCoreServiceFactory::GetInstance(),
        base::BindRepeating(&SetUpDownloadCoreService, download_manager_));
    profile_ = testing_profile_manager_.CreateTestingProfile(
        kProfileName, std::move(testing_factories));
    profile_->SetDownloadManagerForTesting(std::move(download_manager));
    EXPECT_CALL(*download_manager_, GetBrowserContext())
        .WillRepeatedly(Return(profile_));

    download_core_service_ =
        DownloadCoreServiceFactory::GetInstance()->GetForBrowserContext(
            profile_);

    offline_content_provider_ = std::make_unique<
        NiceMock<offline_items_collection::MockOfflineContentProvider>>();
    OfflineContentAggregatorFactory::GetForKey(profile_->GetProfileKey())
        ->RegisterProvider(kProviderNamespace, offline_content_provider_.get());

    update_service_ = static_cast<DownloadBubbleUpdateService*>(
        DownloadBubbleUpdateServiceFactory::GetInstance()
            ->SetTestingFactoryAndUse(
                profile_,
                base::BindRepeating(
                    &DownloadBubbleUpdateServiceTest::CreateUpdateService,
                    base::Unretained(this), update_service_)));
    InitializeUpdateService();
  }

  void TearDown() override {
    // This would normally be done in the Notifier's destructor, but this
    // notifier doesn't know what items it's observing because we have manually
    // added it as an observer on each item, since the MockDownloadManager does
    // not actually call OnDownloadCreated on it.
    if (update_service_) {
      RemoveDownloadItemsObserver(
          download_items_,
          update_service_->download_item_notifier_for_testing());
    }
    // This must be shut down before the DCS is destroyed.
    download_core_service_->GetDownloadManagerDelegate()->Shutdown();
    offline_content_provider_.reset();
    update_service_ = nullptr;
    download_manager_ = nullptr;
    download_core_service_ = nullptr;
    profile_ = nullptr;
    testing_profile_manager_.DeleteTestingProfile(kProfileName);
  }

 protected:
  // TODO(chlily): Factor these out into a test utils file.
  download::MockDownloadItem& GetDownloadItem(size_t index) {
    return *download_items_[index];
  }

  // Forwards to the below version.
  void InitDownloadItem(
      DownloadState state,
      const std::string& guid,
      bool is_paused,
      base::Time start_time = base::Time::Now(),
      const webapps::AppId* web_app_id = nullptr,
      bool is_crx = false,
      bool observe = true,
      download::DownloadItem::DownloadCreationType creation_type =
          download::DownloadItem::TYPE_ACTIVE_DOWNLOAD) {
    InitDownloadItem(*download_manager_, *update_service_, download_items_,
                     profile_, state, guid, is_paused, start_time, web_app_id,
                     is_crx, observe, creation_type);
  }

  void InitDownloadItem(
      NiceMock<content::MockDownloadManager>& download_manager,
      DownloadBubbleUpdateService& update_service,
      std::vector<std::unique_ptr<NiceMockDownloadItem>>& download_items,
      Profile* profile,
      DownloadState state,
      const std::string& guid,
      bool is_paused,
      base::Time start_time = base::Time::Now(),
      const webapps::AppId* web_app_id = nullptr,
      bool is_crx = false,
      bool observe = true,
      download::DownloadItem::DownloadCreationType creation_type =
          download::DownloadItem::TYPE_ACTIVE_DOWNLOAD) {
    size_t index = download_items.size();
    download_items.push_back(std::make_unique<NiceMockDownloadItem>());
    auto& item = *download_items[index];
    EXPECT_CALL(item, GetId())
        .WillRepeatedly(
            Return(static_cast<uint32_t>(download_items.size() + 1)));
    EXPECT_CALL(item, GetGuid()).WillRepeatedly(ReturnRefOfCopy(guid));
    EXPECT_CALL(item, GetState()).WillRepeatedly(Return(state));
    EXPECT_CALL(item, GetStartTime()).WillRepeatedly(Return(start_time));
    if (state == DownloadState::COMPLETE) {
      EXPECT_CALL(item, GetEndTime())
          .WillRepeatedly(Return(start_time + base::Minutes(1)));
    } else {
      EXPECT_CALL(item, GetEndTime()).WillRepeatedly(Return(base::Time()));
    }
    base::FilePath::StringType filename;
#if BUILDFLAG(IS_WIN)
    filename = base::UTF8ToWide(guid);
#else
    filename = guid;
#endif
    EXPECT_CALL(item, GetTargetFilePath())
        .WillRepeatedly(ReturnRefOfCopy(base::FilePath(filename)));
    EXPECT_CALL(item, GetFileNameToReportUser())
        .WillRepeatedly(Return(base::FilePath(filename)));
    int received_bytes =
        state == download::DownloadItem::IN_PROGRESS ? 50 : 100;
    EXPECT_CALL(item, GetReceivedBytes())
        .WillRepeatedly(Return(received_bytes));
    EXPECT_CALL(item, GetTotalBytes()).WillRepeatedly(Return(100));
    EXPECT_CALL(item, IsDone()).WillRepeatedly(Return(false));
    EXPECT_CALL(item, IsTransient()).WillRepeatedly(Return(false));
    EXPECT_CALL(item, IsPaused()).WillRepeatedly(Return(is_paused));
    EXPECT_CALL(item, GetDownloadCreationType())
        .WillRepeatedly(Return(creation_type));
    EXPECT_CALL(item, GetTargetDisposition())
        .WillRepeatedly(
            Return(is_crx ? download::DownloadItem::TARGET_DISPOSITION_OVERWRITE
                          : download::DownloadItem::TARGET_DISPOSITION_PROMPT));
    EXPECT_CALL(item, GetMimeType())
        .WillRepeatedly(Return(is_crx ? "application/x-chrome-extension" : ""));
    std::vector<download::DownloadItem*> items;
    for (auto& download_item : download_items) {
      items.push_back(download_item.get());
    }
    EXPECT_CALL(download_manager, GetAllDownloads(_))
        .WillRepeatedly(WithArg<0>(AddDownloadItems(items)));
    EXPECT_CALL(download_manager, GetDownloadByGuid(guid))
        .WillRepeatedly(Return(&item));
    content::DownloadItemUtils::AttachInfoForTesting(&item, profile, nullptr);
    if (web_app_id != nullptr) {
      DownloadItemWebAppData::CreateAndAttachToItem(&item, *web_app_id);
    }
    if (observe) {
      item.AddObserver(&update_service.download_item_notifier_for_testing());
      item.NotifyObserversDownloadUpdated();
    }
  }

  void UpdateDownloadItem(
      download::MockDownloadItem& item,
      DownloadState state,
      bool is_paused = false,
      DownloadDangerType danger_type =
          DownloadDangerType::DOWNLOAD_DANGER_TYPE_NOT_DANGEROUS) {
    EXPECT_CALL(item, GetState()).WillRepeatedly(Return(state));
    EXPECT_CALL(item, IsDone())
        .WillRepeatedly(Return(state == DownloadState::COMPLETE));
    EXPECT_CALL(item, IsDangerous())
        .WillRepeatedly(
            Return(danger_type !=
                   DownloadDangerType::DOWNLOAD_DANGER_TYPE_NOT_DANGEROUS));
    EXPECT_CALL(item, GetDangerType()).WillRepeatedly(Return(danger_type));
    EXPECT_CALL(item, IsPaused()).WillRepeatedly(Return(is_paused));
    item.NotifyObserversDownloadUpdated();
  }

  // Overload of the above that identifies the item by index.
  void UpdateDownloadItem(
      int item_index,
      DownloadState state,
      bool is_paused = false,
      DownloadDangerType danger_type =
          DownloadDangerType::DOWNLOAD_DANGER_TYPE_NOT_DANGEROUS) {
    DCHECK_GT(download_items_.size(), static_cast<size_t>(item_index));
    auto& item = GetDownloadItem(item_index);
    UpdateDownloadItem(item, state, is_paused, danger_type);
  }

  void RemoveDownloadItem(size_t item_index) {
    download_items_[item_index]->NotifyObserversDownloadRemoved();
    download_items_.erase(download_items_.begin() + item_index);

    std::vector<download::DownloadItem*> items;
    for (size_t i = 0; i < download_items_.size(); ++i) {
      items.push_back(&GetDownloadItem(i));
    }
    EXPECT_CALL(*download_manager_, GetAllDownloads(_))
        .WillRepeatedly(WithArg<0>(AddDownloadItems(items)));
  }

  void InitOfflineItems(const std::vector<OfflineItemState>& states,
                        const std::vector<std::string>& ids,
                        const std::vector<base::Time>& start_times) {
    ASSERT_EQ(states.size(), ids.size());
    ASSERT_EQ(states.size(), start_times.size());
    std::vector<OfflineItem> new_items;
    for (size_t i = 0; i < states.size(); ++i) {
      OfflineItem item;
      item.state = states[i];
      item.id.id = ids[i];
      item.title = ids[i];
      item.creation_time = start_times[i];
      if (states[i] != OfflineItemState::COMPLETE) {
        item.time_remaining_ms = 1000 * 60;
        item.progress.value = 50;
        item.progress.max = 100;
        item.progress.unit =
            offline_items_collection::OfflineItemProgressUnit::PERCENTAGE;
      }
      new_items.push_back(item);
      offline_items_.push_back(std::move(item));
    }
    offline_content_provider_->SetItems(new_items);
    offline_content_provider_->NotifyOnItemsAdded(new_items);
  }

  void UpdateOfflineItem(int item_index, OfflineItemState state) {
    offline_items_[item_index].state = state;
    UpdateDelta delta;
    delta.state_changed = true;
    offline_content_provider_->NotifyOnItemUpdated(offline_items_[item_index],
                                                   delta);
  }

  content::BrowserTaskEnvironment task_environment_{
      base::test::TaskEnvironment::TimeSource::MOCK_TIME};
  raw_ptr<NiceMock<content::MockDownloadManager>, DanglingUntriaged>
      download_manager_ = nullptr;
  std::vector<std::unique_ptr<NiceMockDownloadItem>> download_items_;
  std::vector<offline_items_collection::OfflineItem> offline_items_;
  TestingProfileManager testing_profile_manager_;
  raw_ptr<TestingProfile, DanglingUntriaged> profile_ = nullptr;
  std::unique_ptr<
      NiceMock<offline_items_collection::MockOfflineContentProvider>>
      offline_content_provider_;
  raw_ptr<DownloadBubbleUpdateService, DanglingUntriaged> update_service_ =
      nullptr;
  raw_ptr<DownloadCoreService> download_core_service_ = nullptr;
};

TEST_F(DownloadBubbleUpdateServiceTest, PopulatesCaches) {
  base::Time now = base::Time::Now();
  base::Time older_time = now - base::Hours(1);
  InitDownloadItem(DownloadState::IN_PROGRESS, "in_progress_active_download",
                   /*is_paused=*/false, older_time);
  InitDownloadItem(DownloadState::IN_PROGRESS, "in_progress_paused_download",
                   /*is_paused=*/true, now);
  InitDownloadItem(DownloadState::COMPLETE, "completed_download",
                   /*is_paused=*/false, now);

  DownloadUIModelPtrVector models;
  EXPECT_TRUE(
      update_service_->GetAllModelsToDisplay(models, /*web_app_id=*/nullptr));
  ASSERT_EQ(models.size(), 3u);
  EXPECT_EQ(models[0]->GetContentId().id, "in_progress_active_download");
  EXPECT_EQ(models[1]->GetContentId().id, "in_progress_paused_download");
  EXPECT_EQ(models[2]->GetContentId().id, "completed_download");
  EXPECT_THAT(update_service_->TakeAccessibleAlertsForAnnouncement(nullptr),
              UnorderedElementsAre(HasSubstr16(u"in_progress_active_download"),
                                   HasSubstr16(u"in_progress_paused_download"),
                                   HasSubstr16(u"completed_download")));

  // Recreate the update service to check that it pulls in the existing
  // download items upon initialization.
  auto service = CreateUpdateService(update_service_, profile_);
  update_service_ = static_cast<DownloadBubbleUpdateService*>(service.get());
  InitializeUpdateService();

  EXPECT_TRUE(
      update_service_->GetAllModelsToDisplay(models, /*web_app_id=*/nullptr));
  ASSERT_EQ(models.size(), 3u);
  EXPECT_EQ(models[0]->GetContentId().id, "in_progress_active_download");
  EXPECT_EQ(models[1]->GetContentId().id, "in_progress_paused_download");
  EXPECT_EQ(models[2]->GetContentId().id, "completed_download");

  // Add some more (offline) items.
  InitOfflineItems(
      {OfflineItemState::IN_PROGRESS, OfflineItemState::PAUSED,
       OfflineItemState::COMPLETE},
      {"in_progress_active_offline_item", "in_progress_paused_offline_item",
       "completed_offline_item"},
      {now, older_time, older_time});

  // All items are returned in sorted order.
  EXPECT_TRUE(
      update_service_->GetAllModelsToDisplay(models, /*web_app_id=*/nullptr));
  ASSERT_EQ(models.size(), 6u);
  EXPECT_EQ(models[0]->GetContentId().id, "in_progress_active_offline_item");
  EXPECT_EQ(models[1]->GetContentId().id, "in_progress_active_download");
  EXPECT_EQ(models[2]->GetContentId().id, "in_progress_paused_download");
  EXPECT_EQ(models[3]->GetContentId().id, "in_progress_paused_offline_item");
  EXPECT_EQ(models[4]->GetContentId().id, "completed_download");
  EXPECT_EQ(models[5]->GetContentId().id, "completed_offline_item");
  // Only have alerts for the new offline items, not the download items that
  // were preexisting when this service instance was initialized.
  EXPECT_THAT(
      update_service_->TakeAccessibleAlertsForAnnouncement(nullptr),
      UnorderedElementsAre(HasSubstr16(u"in_progress_active_offline_item"),
                           HasSubstr16(u"in_progress_paused_offline_item"),
                           HasSubstr16(u"completed_offline_item")));

  // Manually clean up the second service instance to avoid UAF.
  RemoveDownloadItemsObserver(
      download_items_, update_service_->download_item_notifier_for_testing());
  update_service_ = nullptr;
}

TEST_F(DownloadBubbleUpdateServiceTest, AddsNonCrxDownloadItems) {
  InitDownloadItem(DownloadState::IN_PROGRESS, "new_download",
                   /*is_paused=*/false, base::Time::Now(),
                   /*web_app_id=*/nullptr, /*is_crx=*/false,
                   /*observe=*/false);
  // Manually notify the service of the new download rather than going through
  // the observer update notification in InitDownloadItem().
  update_service_->OnDownloadCreated(download_manager_.get(),
                                     &GetDownloadItem(0));
  DownloadUIModelPtrVector models;
  EXPECT_TRUE(
      update_service_->GetAllModelsToDisplay(models, /*web_app_id=*/nullptr));
  ASSERT_EQ(models.size(), 1u);
  EXPECT_EQ(models[0]->GetContentId().id, "new_download");
  EXPECT_THAT(update_service_->TakeAccessibleAlertsForAnnouncement(nullptr),
              UnorderedElementsAre(HasSubstr16(u"new_download")));
}

TEST_F(DownloadBubbleUpdateServiceTest, AlertsBasedOnCreationType) {
  // 1. History-imported downloads should not trigger alerts.
  InitDownloadItem(*download_manager_, *update_service_, download_items_,
                   profile_, DownloadState::IN_PROGRESS, "history_import",
                   /*is_paused=*/false, base::Time::Now(),
                   /*web_app_id=*/nullptr,
                   /*is_crx=*/false, /*observe=*/false,
                   download::DownloadItem::TYPE_HISTORY_IMPORT);
  update_service_->OnDownloadCreated(download_manager_.get(),
                                     &GetDownloadItem(0));
  EXPECT_THAT(update_service_->TakeAccessibleAlertsForAnnouncement(nullptr),
              IsEmpty());

  // 2. Active downloads should trigger alerts.
  InitDownloadItem(*download_manager_, *update_service_, download_items_,
                   profile_, DownloadState::IN_PROGRESS, "active_download",
                   /*is_paused=*/false, base::Time::Now(),
                   /*web_app_id=*/nullptr,
                   /*is_crx=*/false, /*observe=*/false,
                   download::DownloadItem::TYPE_ACTIVE_DOWNLOAD);
  update_service_->OnDownloadCreated(download_manager_.get(),
                                     &GetDownloadItem(1));
  EXPECT_THAT(update_service_->TakeAccessibleAlertsForAnnouncement(nullptr),
              UnorderedElementsAre(HasSubstr16(u"active_download")));
}

TEST_F(DownloadBubbleUpdateServiceTest, DelaysCrx) {
  InitDownloadItem(DownloadState::IN_PROGRESS, "in_progress_crx",
                   /*is_paused=*/false, base::Time::Now(),
                   /*web_app_id=*/nullptr, /*is_crx=*/true,
                   /*observe=*/false);
  // Manually notify the service of the new download rather than going through
  // the observer update notification in InitDownloadItem().
  update_service_->OnDownloadCreated(download_manager_, &GetDownloadItem(0));

  DownloadUIModelPtrVector models;
  EXPECT_TRUE(
      update_service_->GetAllModelsToDisplay(models, /*web_app_id=*/nullptr));
  // The crx download does not show up immediately.
  EXPECT_EQ(models.size(), 0u);
  EXPECT_THAT(update_service_->TakeAccessibleAlertsForAnnouncement(nullptr),
              IsEmpty());

  // Updates are also withheld.
  UpdateDownloadItem(0, DownloadState::IN_PROGRESS, /*is_paused=*/true);
  EXPECT_TRUE(
      update_service_->GetAllModelsToDisplay(models, /*web_app_id=*/nullptr));
  EXPECT_EQ(models.size(), 0u);
  EXPECT_THAT(update_service_->TakeAccessibleAlertsForAnnouncement(nullptr),
              IsEmpty());

  task_environment_.FastForwardBy(base::Seconds(2));

  // After the delay, the crx is added.
  EXPECT_TRUE(
      update_service_->GetAllModelsToDisplay(models, /*web_app_id=*/nullptr));
  ASSERT_EQ(models.size(), 1u);
  EXPECT_EQ(models[0]->GetContentId().id, "in_progress_crx");
  EXPECT_THAT(update_service_->TakeAccessibleAlertsForAnnouncement(nullptr),
              UnorderedElementsAre(HasSubstr16(u"in_progress_crx")));
}

TEST_F(DownloadBubbleUpdateServiceTest, EvictsExcessItemsAndBackfills) {
  update_service_->set_max_num_items_to_show_for_testing(3);
  update_service_->set_extra_items_to_cache_for_testing(0);
  base::Time now = base::Time::Now();
  base::Time older_time = now - base::Hours(1);
  InitDownloadItem(DownloadState::IN_PROGRESS, "in_progress_active_download",
                   /*is_paused=*/false, older_time);
  InitDownloadItem(DownloadState::IN_PROGRESS, "in_progress_paused_download",
                   /*is_paused=*/true, now);
  InitDownloadItem(DownloadState::COMPLETE, "completed_download_older",
                   /*is_paused=*/false, older_time);

  DownloadUIModelPtrVector models;
  EXPECT_TRUE(
      update_service_->GetAllModelsToDisplay(models, /*web_app_id=*/nullptr));
  ASSERT_EQ(models.size(), 3u);
  EXPECT_EQ(models[0]->GetContentId().id, "in_progress_active_download");
  EXPECT_EQ(models[1]->GetContentId().id, "in_progress_paused_download");
  EXPECT_EQ(models[2]->GetContentId().id, "completed_download_older");

  // Add another item. This sorts before the last item and should cause it to be
  // evicted.
  InitDownloadItem(DownloadState::COMPLETE, "completed_download_newer",
                   /*is_paused=*/false, now);

  EXPECT_TRUE(
      update_service_->GetAllModelsToDisplay(models, /*web_app_id=*/nullptr));
  ASSERT_EQ(models.size(), 3u);
  EXPECT_EQ(models[0]->GetContentId().id, "in_progress_active_download");
  EXPECT_EQ(models[1]->GetContentId().id, "in_progress_paused_download");
  EXPECT_EQ(models[2]->GetContentId().id, "completed_download_newer");
  // Alerts for all the downloads are included.
  EXPECT_THAT(update_service_->TakeAccessibleAlertsForAnnouncement(nullptr),
              UnorderedElementsAre(HasSubstr16(u"in_progress_active_download"),
                                   HasSubstr16(u"in_progress_paused_download"),
                                   HasSubstr16(u"completed_download_older"),
                                   HasSubstr16(u"completed_download_newer")));

  // Remove an item. The previously evicted item should come back via backfill.
  RemoveDownloadItem(1);
  task_environment_.RunUntilIdle();

  EXPECT_TRUE(
      update_service_->GetAllModelsToDisplay(models, /*web_app_id=*/nullptr));
  ASSERT_EQ(models.size(), 3u);
  EXPECT_EQ(models[0]->GetContentId().id, "in_progress_active_download");
  EXPECT_EQ(models[1]->GetContentId().id, "completed_download_newer");
  EXPECT_EQ(models[2]->GetContentId().id, "completed_download_older");
  EXPECT_THAT(update_service_->TakeAccessibleAlertsForAnnouncement(nullptr),
              IsEmpty());
}

TEST_F(DownloadBubbleUpdateServiceTest, BackfillsOnUpdate) {
  update_service_->set_max_num_items_to_show_for_testing(3);
  update_service_->set_extra_items_to_cache_for_testing(0);
  base::Time now = base::Time::Now();
  base::Time older_time = now - base::Hours(1);
  base::Time even_older_time = older_time - base::Hours(1);
  base::Time oldest_time = even_older_time - base::Hours(1);
  InitDownloadItem(DownloadState::IN_PROGRESS, "now",
                   /*is_paused=*/false, now);
  InitDownloadItem(DownloadState::IN_PROGRESS, "older",
                   /*is_paused=*/false, older_time);
  InitDownloadItem(DownloadState::IN_PROGRESS, "even_older",
                   /*is_paused=*/false, even_older_time);
  InitDownloadItem(DownloadState::IN_PROGRESS, "oldest",
                   /*is_paused=*/false, oldest_time);

  // Only the 3 newest downloads are shown at first.
  DownloadUIModelPtrVector models;
  EXPECT_TRUE(
      update_service_->GetAllModelsToDisplay(models, /*web_app_id=*/nullptr));
  ASSERT_EQ(models.size(), 3u);
  EXPECT_EQ(models[0]->GetContentId().id, "now");
  EXPECT_EQ(models[1]->GetContentId().id, "older");
  EXPECT_EQ(models[2]->GetContentId().id, "even_older");
  // Alerts for all the downloads are included.
  EXPECT_THAT(
      update_service_->TakeAccessibleAlertsForAnnouncement(nullptr),
      UnorderedElementsAre(HasSubstr16(u"now"), HasSubstr16(u"older"),
                           HasSubstr16(u"even_older"), HasSubstr16(u"oldest")));

  // Update the newest item to be paused, so it should sort behind all the other
  // items.
  UpdateDownloadItem(0, DownloadState::IN_PROGRESS, /*is_paused=*/true);
  task_environment_.RunUntilIdle();

  // The oldest download, previously too low in sort order to display, is
  // retrieved after backfilling.
  EXPECT_TRUE(
      update_service_->GetAllModelsToDisplay(models, /*web_app_id=*/nullptr));
  ASSERT_EQ(models.size(), 3u);
  EXPECT_EQ(models[0]->GetContentId().id, "older");
  EXPECT_EQ(models[1]->GetContentId().id, "even_older");
  EXPECT_EQ(models[2]->GetContentId().id, "oldest");
  // Only the item that was updated has an alert.
  EXPECT_THAT(update_service_->TakeAccessibleAlertsForAnnouncement(nullptr),
              UnorderedElementsAre(HasSubstr16(u"now")));
}

TEST_F(DownloadBubbleUpdateServiceTest, UpdatesOfflineItems) {
  base::Time now = base::Time::Now();
  InitOfflineItems({OfflineItemState::IN_PROGRESS},
                   {"in_progress_active_offline_item"}, {now});

  DownloadUIModelPtrVector models;
  EXPECT_TRUE(
      update_service_->GetAllModelsToDisplay(models, /*web_app_id=*/nullptr));
  ASSERT_EQ(models.size(), 1u);
  EXPECT_EQ(models[0]->GetContentId().id, "in_progress_active_offline_item");
  EXPECT_EQ(models[0]->GetState(), DownloadState::IN_PROGRESS);
  EXPECT_THAT(
      update_service_->TakeAccessibleAlertsForAnnouncement(nullptr),
      UnorderedElementsAre(HasSubstr16(u"in_progress_active_offline_item")));

  UpdateOfflineItem(0, OfflineItemState::COMPLETE);
  EXPECT_TRUE(
      update_service_->GetAllModelsToDisplay(models, /*web_app_id=*/nullptr));
  ASSERT_EQ(models.size(), 1u);
  EXPECT_EQ(models[0]->GetContentId().id, "in_progress_active_offline_item");
  EXPECT_EQ(models[0]->GetState(), DownloadState::COMPLETE);
  EXPECT_THAT(update_service_->TakeAccessibleAlertsForAnnouncement(nullptr),
              UnorderedElementsAre(
                  AllOf(HasSubstr16(u"in_progress_active_offline_item"),
                        HasSubstr16(u"complete"))));
}

TEST_F(DownloadBubbleUpdateServiceTest, RemovesOfflineItems) {
  base::Time now = base::Time::Now();
  InitOfflineItems(
      {OfflineItemState::IN_PROGRESS, OfflineItemState::PAUSED,
       OfflineItemState::COMPLETE},
      {"in_progress_active_offline_item", "in_progress_paused_offline_item",
       "completed_offline_item"},
      {now, now, now});

  DownloadUIModelPtrVector models;
  EXPECT_TRUE(
      update_service_->GetAllModelsToDisplay(models, /*web_app_id=*/nullptr));
  ASSERT_EQ(models.size(), 3u);
  EXPECT_EQ(models[0]->GetContentId().id, "in_progress_active_offline_item");
  EXPECT_EQ(models[1]->GetContentId().id, "in_progress_paused_offline_item");
  EXPECT_EQ(models[2]->GetContentId().id, "completed_offline_item");
  EXPECT_THAT(
      update_service_->TakeAccessibleAlertsForAnnouncement(nullptr),
      UnorderedElementsAre(HasSubstr16(u"in_progress_active_offline_item"),
                           HasSubstr16(u"in_progress_paused_offline_item"),
                           HasSubstr16(u"completed_offline_item")));

  offline_content_provider_->NotifyOnItemRemoved(models[0]->GetContentId());
  EXPECT_TRUE(
      update_service_->GetAllModelsToDisplay(models, /*web_app_id=*/nullptr));
  ASSERT_EQ(models.size(), 2u);
  EXPECT_EQ(models[0]->GetContentId().id, "in_progress_paused_offline_item");
  EXPECT_EQ(models[1]->GetContentId().id, "completed_offline_item");
  // No alert generated for removal.
  EXPECT_THAT(update_service_->TakeAccessibleAlertsForAnnouncement(nullptr),
              IsEmpty());
}

TEST_F(DownloadBubbleUpdateServiceTest, DoesNotAddExpiredItems) {
  base::Time too_old_time = base::Time::Now() - base::Hours(25);
  InitDownloadItem(DownloadState::IN_PROGRESS, "old",
                   /*is_paused=*/false, too_old_time);
  DownloadUIModelPtrVector models;
  EXPECT_TRUE(
      update_service_->GetAllModelsToDisplay(models, /*web_app_id=*/nullptr));
  EXPECT_EQ(models.size(), 0u);
  EXPECT_THAT(update_service_->TakeAccessibleAlertsForAnnouncement(nullptr),
              IsEmpty());
}

TEST_F(DownloadBubbleUpdateServiceTest, PrunesExpiredItems) {
#if BUILDFLAG(IS_MAC)
  // TODO(crbug.com/434660312): Re-enable on macOS 26 once issues with
  // unexpected test timeout failures are resolved.
  if (base::mac::MacOSMajorVersion() == 26) {
    GTEST_SKIP() << "Disabled on macOS Tahoe.";
  }
#endif
  base::Time now = base::Time::Now();
  base::Time two_hours_ago = now - base::Hours(2);
  // Add some items that are currently recent enough to add.
  InitDownloadItem(DownloadState::IN_PROGRESS, "now_download",
                   /*is_paused=*/false, now);
  InitDownloadItem(DownloadState::IN_PROGRESS, "two_hours_ago_download",
                   /*is_paused=*/false, two_hours_ago);
  InitOfflineItems({OfflineItemState::PAUSED, OfflineItemState::PAUSED},
                   {"now_offline_item", "two_hours_ago_offline_item"},
                   {now, two_hours_ago});

  DownloadUIModelPtrVector models;
  EXPECT_TRUE(
      update_service_->GetAllModelsToDisplay(models, /*web_app_id=*/nullptr));
  ASSERT_EQ(models.size(), 4u);
  EXPECT_EQ(models[0]->GetContentId().id, "now_download");
  EXPECT_EQ(models[1]->GetContentId().id, "two_hours_ago_download");
  EXPECT_EQ(models[2]->GetContentId().id, "now_offline_item");
  EXPECT_EQ(models[3]->GetContentId().id, "two_hours_ago_offline_item");
  EXPECT_THAT(update_service_->TakeAccessibleAlertsForAnnouncement(nullptr),
              UnorderedElementsAre(HasSubstr16(u"now_download"),
                                   HasSubstr16(u"two_hours_ago_download"),
                                   HasSubstr16(u"now_offline_item"),
                                   HasSubstr16(u"two_hours_ago_offline_item")));

  // Fast forward so that the older items become too old.
  task_environment_.FastForwardBy(base::Hours(23));

  // Only the newer items should remain.
  EXPECT_TRUE(
      update_service_->GetAllModelsToDisplay(models, /*web_app_id=*/nullptr));
  ASSERT_EQ(models.size(), 2u);
  EXPECT_EQ(models[0]->GetContentId().id, "now_download");
  EXPECT_EQ(models[1]->GetContentId().id, "now_offline_item");
  EXPECT_THAT(update_service_->TakeAccessibleAlertsForAnnouncement(nullptr),
              IsEmpty());
}

TEST_F(DownloadBubbleUpdateServiceTest, DoesNotBackfillIfNotForced) {
#if BUILDFLAG(IS_MAC)
  // TODO(crbug.com/434660312): Re-enable on macOS 26 once issues with
  // unexpected test timeout failures are resolved.
  if (base::mac::MacOSMajorVersion() == 26) {
    GTEST_SKIP() << "Disabled on macOS Tahoe.";
  }
#endif
  update_service_->set_max_num_items_to_show_for_testing(3);
  update_service_->set_extra_items_to_cache_for_testing(0);
  base::Time now = base::Time::Now();
  base::Time recent = now - base::Minutes(1);
  base::Time two_hours_ago = now - base::Hours(2);

  InitDownloadItem(DownloadState::IN_PROGRESS, "now",
                   /*is_paused=*/false, now);
  InitDownloadItem(DownloadState::IN_PROGRESS, "recent",
                   /*is_paused=*/false, recent);
  InitDownloadItem(DownloadState::IN_PROGRESS, "two_hours_ago",
                   /*is_paused=*/false, two_hours_ago);
  InitDownloadItem(DownloadState::IN_PROGRESS, "now_paused",
                   /*is_paused=*/true, now);

  DownloadUIModelPtrVector models;
  EXPECT_TRUE(
      update_service_->GetAllModelsToDisplay(models, /*web_app_id=*/nullptr));
  ASSERT_EQ(models.size(), 3u);
  EXPECT_EQ(models[0]->GetContentId().id, "now");
  EXPECT_EQ(models[1]->GetContentId().id, "recent");
  EXPECT_EQ(models[2]->GetContentId().id, "two_hours_ago");

  task_environment_.FastForwardBy(base::Hours(23));

  // Since items are pruned, return the unpruned ones immediately and indicate
  // that results are not complete.
  EXPECT_FALSE(
      update_service_->GetAllModelsToDisplay(models, /*web_app_id=*/nullptr));
  ASSERT_EQ(models.size(), 2u);
  EXPECT_EQ(models[0]->GetContentId().id, "now");
  EXPECT_EQ(models[1]->GetContentId().id, "recent");

  // Sometime later, once the backfilling is complete, we will start to return
  // all the non-expired items.
  task_environment_.RunUntilIdle();
  EXPECT_TRUE(
      update_service_->GetAllModelsToDisplay(models, /*web_app_id=*/nullptr));
  ASSERT_EQ(models.size(), 3u);
  EXPECT_EQ(models[0]->GetContentId().id, "now");
  EXPECT_EQ(models[1]->GetContentId().id, "recent");
  EXPECT_EQ(models[2]->GetContentId().id, "now_paused");
}

TEST_F(DownloadBubbleUpdateServiceTest, BackfillsSynchronouslyIfForced) {
#if BUILDFLAG(IS_MAC)
  // TODO(crbug.com/434660312): Re-enable on macOS 26 once issues with
  // unexpected test timeout failures are resolved.
  if (base::mac::MacOSMajorVersion() == 26) {
    GTEST_SKIP() << "Disabled on macOS Tahoe.";
  }
#endif
  update_service_->set_max_num_items_to_show_for_testing(3);
  update_service_->set_extra_items_to_cache_for_testing(0);
  base::Time now = base::Time::Now();
  base::Time recent = now - base::Minutes(1);
  base::Time two_hours_ago = now - base::Hours(2);

  InitDownloadItem(DownloadState::IN_PROGRESS, "now",
                   /*is_paused=*/false, now);
  InitDownloadItem(DownloadState::IN_PROGRESS, "recent",
                   /*is_paused=*/false, recent);
  InitDownloadItem(DownloadState::IN_PROGRESS, "two_hours_ago",
                   /*is_paused=*/false, two_hours_ago);
  InitDownloadItem(DownloadState::IN_PROGRESS, "now_paused",
                   /*is_paused=*/true, now);

  DownloadUIModelPtrVector models;
  EXPECT_TRUE(
      update_service_->GetAllModelsToDisplay(models, /*web_app_id=*/nullptr));
  ASSERT_EQ(models.size(), 3u);
  EXPECT_EQ(models[0]->GetContentId().id, "now");
  EXPECT_EQ(models[1]->GetContentId().id, "recent");
  EXPECT_EQ(models[2]->GetContentId().id, "two_hours_ago");

  task_environment_.FastForwardBy(base::Hours(23));

  EXPECT_TRUE(update_service_->GetAllModelsToDisplay(
      models, /*web_app_id=*/nullptr, /*force_backfill_download_items=*/true));
  ASSERT_EQ(models.size(), 3u);
  EXPECT_EQ(models[0]->GetContentId().id, "now");
  EXPECT_EQ(models[1]->GetContentId().id, "recent");
  EXPECT_EQ(models[2]->GetContentId().id, "now_paused");
}

TEST_F(DownloadBubbleUpdateServiceTest, CachesExtraItems) {
#if BUILDFLAG(IS_MAC)
  // TODO(crbug.com/434660312): Re-enable on macOS 26 once issues with
  // unexpected test timeout failures are resolved.
  if (base::mac::MacOSMajorVersion() == 26) {
    GTEST_SKIP() << "Disabled on macOS Tahoe.";
  }
#endif
  update_service_->set_max_num_items_to_show_for_testing(3);
  // Cache an extra item.
  update_service_->set_extra_items_to_cache_for_testing(1);
  base::Time now = base::Time::Now();
  base::Time recent = now - base::Minutes(2);
  base::Time two_hours_ago = now - base::Hours(2);

  InitDownloadItem(DownloadState::IN_PROGRESS, "now",
                   /*is_paused=*/false, now);
  InitDownloadItem(DownloadState::IN_PROGRESS, "recent",
                   /*is_paused=*/false, recent);
  InitDownloadItem(DownloadState::IN_PROGRESS, "two_hours_ago",
                   /*is_paused=*/false, two_hours_ago);
  InitDownloadItem(DownloadState::IN_PROGRESS, "now_paused",
                   /*is_paused=*/true, now);

  DownloadUIModelPtrVector models;
  EXPECT_TRUE(
      update_service_->GetAllModelsToDisplay(models, /*web_app_id=*/nullptr));
  ASSERT_EQ(models.size(), 3u);
  EXPECT_EQ(models[0]->GetContentId().id, "now");
  EXPECT_EQ(models[1]->GetContentId().id, "recent");
  EXPECT_EQ(models[2]->GetContentId().id, "two_hours_ago");

  task_environment_.FastForwardBy(base::Hours(23));

  // This returns true despite not forcing a backfill, because the extra item
  // in the cache is available to be returned.
  EXPECT_TRUE(
      update_service_->GetAllModelsToDisplay(models, /*web_app_id=*/nullptr));
  ASSERT_EQ(models.size(), 3u);
  EXPECT_EQ(models[0]->GetContentId().id, "now");
  EXPECT_EQ(models[1]->GetContentId().id, "recent");
  EXPECT_EQ(models[2]->GetContentId().id, "now_paused");
}

// Test that downloads from web apps are only displayed when queried for the
// specific web app.
TEST_F(DownloadBubbleUpdateServiceTest, GetAllModelsToDisplayForWebApp) {
  base::Time now = base::Time::Now();
  base::Time before = now - base::Hours(1);
  webapps::AppId app_a_id = "app_a";
  webapps::AppId app_b_id = "app_b";
  InitDownloadItem(DownloadState::IN_PROGRESS, "app_a_download",
                   /*is_paused=*/false, now, &app_a_id);
  InitDownloadItem(DownloadState::IN_PROGRESS, "app_b_download",
                   /*is_paused=*/false, now, &app_b_id);
  InitDownloadItem(DownloadState::IN_PROGRESS, "non_app_download",
                   /*is_paused=*/false, now);

  // Offline items should only be returned for non-web-app queries.
  InitOfflineItems({OfflineItemState::IN_PROGRESS}, {"offline_item"}, {before});

  DownloadUIModelPtrVector models;
  EXPECT_TRUE(
      update_service_->GetAllModelsToDisplay(models, /*web_app_id=*/nullptr));
  ASSERT_EQ(models.size(), 2u);
  EXPECT_EQ(models[0]->GetContentId().id, "non_app_download");
  EXPECT_EQ(models[1]->GetContentId().id, "offline_item");
  EXPECT_THAT(update_service_->TakeAccessibleAlertsForAnnouncement(nullptr),
              UnorderedElementsAre(HasSubstr16(u"non_app_download"),
                                   HasSubstr16(u"offline_item")));

  EXPECT_TRUE(update_service_->GetAllModelsToDisplay(models, &app_a_id));
  ASSERT_EQ(models.size(), 1u);
  EXPECT_EQ(models[0]->GetContentId().id, "app_a_download");
  EXPECT_THAT(update_service_->TakeAccessibleAlertsForAnnouncement(&app_a_id),
              UnorderedElementsAre(HasSubstr16(u"app_a_download")));

  EXPECT_TRUE(update_service_->GetAllModelsToDisplay(models, &app_b_id));
  ASSERT_EQ(models.size(), 1u);
  EXPECT_EQ(models[0]->GetContentId().id, "app_b_download");
  EXPECT_THAT(update_service_->TakeAccessibleAlertsForAnnouncement(&app_b_id),
              UnorderedElementsAre(HasSubstr16(u"app_b_download")));
}

TEST_F(DownloadBubbleUpdateServiceTest, GetProgressInfo) {
  InitDownloadItem(DownloadState::IN_PROGRESS, "in_progress_active",
                   /*is_paused=*/false);
  InitDownloadItem(DownloadState::IN_PROGRESS, "in_progress_paused",
                   /*is_paused=*/true);
  InitDownloadItem(DownloadState::COMPLETE, "completed",
                   /*is_paused=*/false);

  DownloadDisplay::ProgressInfo progress_info =
      update_service_->GetProgressInfo(/*web_app_id=*/nullptr);
  EXPECT_EQ(progress_info.download_count, 2);
  EXPECT_TRUE(progress_info.progress_certain);
  EXPECT_EQ(progress_info.progress_percentage, 50);

  InitOfflineItems({OfflineItemState::IN_PROGRESS}, {"offline_item"},
                   {base::Time::Now()});

  progress_info = update_service_->GetProgressInfo(/*web_app_id=*/nullptr);
  EXPECT_EQ(progress_info.download_count, 3);
  EXPECT_FALSE(progress_info.progress_certain);
  EXPECT_EQ(progress_info.progress_percentage, 50);
}

TEST_F(DownloadBubbleUpdateServiceTest, GetProgressInfoForWebApp) {
  base::Time now = base::Time::Now();
  webapps::AppId app_a_id = "app_a";
  webapps::AppId app_b_id = "app_b";
  InitDownloadItem(DownloadState::IN_PROGRESS, "app_a_download1",
                   /*is_paused=*/false, now, &app_a_id);
  InitDownloadItem(DownloadState::IN_PROGRESS, "app_a_download2",
                   /*is_paused=*/false, now, &app_a_id);
  InitDownloadItem(DownloadState::IN_PROGRESS, "app_b_download1",
                   /*is_paused=*/false, now, &app_b_id);
  InitDownloadItem(DownloadState::IN_PROGRESS, "app_b_download2",
                   /*is_paused=*/false, now, &app_b_id);
  InitDownloadItem(DownloadState::IN_PROGRESS, "app_b_download3",
                   /*is_paused=*/false, now, &app_b_id);
  InitDownloadItem(DownloadState::IN_PROGRESS, "non_app_download",
                   /*is_paused=*/false, now);

  DownloadDisplay::ProgressInfo non_app_progress_info =
      update_service_->GetProgressInfo(/*web_app_id=*/nullptr);
  EXPECT_EQ(non_app_progress_info.download_count, 1);
  EXPECT_TRUE(non_app_progress_info.progress_certain);
  EXPECT_EQ(non_app_progress_info.progress_percentage, 50);

  DownloadDisplay::ProgressInfo app_a_progress_info =
      update_service_->GetProgressInfo(&app_a_id);
  EXPECT_EQ(app_a_progress_info.download_count, 2);
  EXPECT_TRUE(app_a_progress_info.progress_certain);
  EXPECT_EQ(app_a_progress_info.progress_percentage, 50);

  DownloadDisplay::ProgressInfo app_b_progress_info =
      update_service_->GetProgressInfo(&app_b_id);
  EXPECT_EQ(app_b_progress_info.download_count, 3);
  EXPECT_TRUE(app_b_progress_info.progress_certain);
  EXPECT_EQ(app_b_progress_info.progress_percentage, 50);
}

TEST_F(DownloadBubbleUpdateServiceTest, GetDisplayInfo_InProgress) {
  base::Time now = base::Time::Now();
  base::Time two_hours_ago = now - base::Hours(2);
  InitDownloadItem(DownloadState::IN_PROGRESS, "now_download",
                   /*is_paused=*/false, now);
  InitDownloadItem(DownloadState::IN_PROGRESS, "two_hours_ago_download",
                   /*is_paused=*/false, two_hours_ago);
  InitOfflineItems({OfflineItemState::PAUSED, OfflineItemState::PAUSED},
                   {"now_offline_item", "two_hours_ago_offline_item"},
                   {now, two_hours_ago});

  DownloadBubbleDisplayInfo info =
      update_service_->GetDisplayInfo(/*web_app_id=*/nullptr);
  EXPECT_EQ(info.all_models_size, 4u);
  // The last_completed_time is null, because no downloads have finished.
  EXPECT_EQ(info.last_completed_time, base::Time());
  EXPECT_EQ(info.in_progress_count, 4);
  EXPECT_EQ(info.paused_count, 2);
  EXPECT_TRUE(info.has_unactioned);
  EXPECT_FALSE(info.has_deep_scanning);
  EXPECT_FALSE(info.has_content_check);
}

TEST_F(DownloadBubbleUpdateServiceTest,
       GetDisplayInfo_UpdateForCompleteDownload) {
  base::Time now = base::Time::Now();
  base::Time two_hours_ago = now - base::Hours(2);
  InitDownloadItem(DownloadState::IN_PROGRESS, "now_download",
                   /*is_paused=*/false, now);
  InitDownloadItem(DownloadState::IN_PROGRESS, "two_hours_ago_download",
                   /*is_paused=*/false, two_hours_ago);
  InitDownloadItem(DownloadState::COMPLETE, "completed_download",
                   /*is_paused=*/false, two_hours_ago);
  InitOfflineItems({OfflineItemState::PAUSED, OfflineItemState::PAUSED},
                   {"now_offline_item", "two_hours_ago_offline_item"},
                   {now, two_hours_ago});

  // Make the download completed to update the last_completed_time.
  UpdateDownloadItem(0, DownloadState::COMPLETE);

  DownloadBubbleDisplayInfo info =
      update_service_->GetDisplayInfo(/*web_app_id=*/nullptr);
  EXPECT_EQ(info.all_models_size, 5u);
  EXPECT_EQ(info.last_completed_time, now);
  EXPECT_EQ(info.in_progress_count, 3);
  EXPECT_EQ(info.paused_count, 2);
  EXPECT_TRUE(info.has_unactioned);
  EXPECT_FALSE(info.has_deep_scanning);
  EXPECT_FALSE(info.has_content_check);
}

TEST_F(DownloadBubbleUpdateServiceTest,
       GetDisplayInfo_ExistingCompleteDownload) {
  base::Time now = base::Time::Now();
  base::Time two_hours_ago = now - base::Hours(2);
  InitDownloadItem(DownloadState::IN_PROGRESS, "now_download",
                   /*is_paused=*/false, now);
  InitDownloadItem(DownloadState::IN_PROGRESS, "two_hours_ago_download",
                   /*is_paused=*/false, two_hours_ago);
  InitDownloadItem(DownloadState::COMPLETE, "completed_download",
                   /*is_paused=*/false, two_hours_ago);
  InitOfflineItems({OfflineItemState::PAUSED, OfflineItemState::PAUSED},
                   {"now_offline_item", "two_hours_ago_offline_item"},
                   {now, two_hours_ago});

  DownloadBubbleDisplayInfo info =
      update_service_->GetDisplayInfo(/*web_app_id=*/nullptr);
  EXPECT_EQ(info.all_models_size, 5u);
  // The last_completed_time should be set to the end time of the completed
  // download, which is 1 minute past its start time (set in InitDownloadItem).
  EXPECT_EQ(info.last_completed_time, two_hours_ago + base::Minutes(1));
  EXPECT_EQ(info.in_progress_count, 4);
  EXPECT_EQ(info.paused_count, 2);
  EXPECT_TRUE(info.has_unactioned);
  EXPECT_FALSE(info.has_deep_scanning);
  EXPECT_FALSE(info.has_content_check);
}

TEST_F(DownloadBubbleUpdateServiceTest, GetDisplayInfo_UpdateForDangerous) {
  base::Time now = base::Time::Now();
  base::Time two_hours_ago = now - base::Hours(2);
  InitDownloadItem(DownloadState::IN_PROGRESS, "now_download",
                   /*is_paused=*/false, now);
  InitDownloadItem(DownloadState::IN_PROGRESS, "two_hours_ago_download",
                   /*is_paused=*/false, two_hours_ago);
  InitDownloadItem(DownloadState::COMPLETE, "completed_download",
                   /*is_paused=*/false, two_hours_ago);
  InitOfflineItems({OfflineItemState::PAUSED, OfflineItemState::PAUSED},
                   {"now_offline_item", "two_hours_ago_offline_item"},
                   {now, two_hours_ago});

  // Make the download dangerous to update the last_completed_time.
  UpdateDownloadItem(
      0, DownloadState::IN_PROGRESS, /*is_paused=*/false,
      DownloadDangerType::DOWNLOAD_DANGER_TYPE_DANGEROUS_CONTENT);

  DownloadBubbleDisplayInfo info =
      update_service_->GetDisplayInfo(/*web_app_id=*/nullptr);
  EXPECT_EQ(info.all_models_size, 5u);
  EXPECT_EQ(info.last_completed_time, now);
  EXPECT_EQ(info.in_progress_count, 3);
  EXPECT_EQ(info.paused_count, 2);
  EXPECT_TRUE(info.has_unactioned);
  EXPECT_FALSE(info.has_deep_scanning);
  EXPECT_FALSE(info.has_content_check);
}

TEST_F(DownloadBubbleUpdateServiceTest, GetDisplayInfoForWebApp) {
  base::Time now = base::Time::Now();
  base::Time two_hours_ago = now - base::Hours(2);
  webapps::AppId app_a_id = "app_a";
  webapps::AppId app_b_id = "app_b";
  InitDownloadItem(DownloadState::IN_PROGRESS, "non_app_download",
                   /*is_paused=*/false, now);
  InitOfflineItems({OfflineItemState::PAUSED, OfflineItemState::PAUSED},
                   {"now_offline_item", "two_hours_ago_offline_item"},
                   {now, two_hours_ago});
  InitDownloadItem(DownloadState::IN_PROGRESS, "app_a_download1",
                   /*is_paused=*/false, now, &app_a_id);
  InitDownloadItem(DownloadState::IN_PROGRESS, "app_a_download2",
                   /*is_paused=*/false, now, &app_a_id);
  InitDownloadItem(DownloadState::IN_PROGRESS, "app_b_download",
                   /*is_paused=*/false, now, &app_b_id);

  DownloadBubbleDisplayInfo non_app_info =
      update_service_->GetDisplayInfo(/*web_app_id=*/nullptr);
  EXPECT_EQ(non_app_info.all_models_size, 3u);
  EXPECT_EQ(non_app_info.paused_count, 2);
  DownloadBubbleDisplayInfo app_a_info =
      update_service_->GetDisplayInfo(&app_a_id);
  EXPECT_EQ(app_a_info.all_models_size, 2u);
  DownloadBubbleDisplayInfo app_b_info =
      update_service_->GetDisplayInfo(&app_b_id);
  EXPECT_EQ(app_b_info.all_models_size, 1u);
}

TEST_F(DownloadBubbleUpdateServiceTest,
       DownloadUpdatedWithWebAppDataAfterCreation) {
  base::Time now = base::Time::Now();
  webapps::AppId app_id = "app";
  // This simulates the restoration of a web app download from the history
  // database, during which the item is created first without the
  // DownloadItemWebAppData, and then subsequently tagged with the data.
  InitDownloadItem(DownloadState::IN_PROGRESS, "app_download",
                   /*is_paused=*/false, now);
  DownloadItemWebAppData::CreateAndAttachToItem(&GetDownloadItem(0), app_id);
  GetDownloadItem(0).NotifyObserversDownloadUpdated();

  DownloadUIModelPtrVector models;
  EXPECT_TRUE(
      update_service_->GetAllModelsToDisplay(models, /*web_app_id=*/nullptr));
  EXPECT_TRUE(models.empty());

  EXPECT_TRUE(update_service_->GetAllModelsToDisplay(models, &app_id));
  ASSERT_EQ(models.size(), 1u);
  EXPECT_EQ(models[0]->GetContentId().id, "app_download");

  DownloadDisplay::ProgressInfo non_app_progress_info =
      update_service_->GetProgressInfo(/*web_app_id=*/nullptr);
  EXPECT_EQ(non_app_progress_info.download_count, 0);

  DownloadDisplay::ProgressInfo app_progress_info =
      update_service_->GetProgressInfo(&app_id);
  EXPECT_EQ(app_progress_info.download_count, 1);

  DownloadBubbleDisplayInfo non_app_info =
      update_service_->GetDisplayInfo(/*web_app_id=*/nullptr);
  EXPECT_EQ(non_app_info.all_models_size, 0u);
  DownloadBubbleDisplayInfo app_info = update_service_->GetDisplayInfo(&app_id);
  EXPECT_EQ(app_info.all_models_size, 1u);
}

class DownloadBubbleUpdateServiceIncognitoTest
    : public DownloadBubbleUpdateServiceTest {
 public:
  DownloadBubbleUpdateServiceIncognitoTest() = default;
  DownloadBubbleUpdateServiceIncognitoTest(
      const DownloadBubbleUpdateServiceIncognitoTest&) = delete;
  DownloadBubbleUpdateServiceIncognitoTest& operator=(
      const DownloadBubbleUpdateServiceIncognitoTest&) = delete;
  ~DownloadBubbleUpdateServiceIncognitoTest() override = default;

  void SetUp() override {
    DownloadBubbleUpdateServiceTest::SetUp();
    TestingProfile::Builder builder;
    TestingProfile::TestingFactories testing_factories;
    testing_factories.emplace_back(HistoryServiceFactory::GetInstance(),
                                   HistoryServiceFactory::GetDefaultFactory());
    testing_factories.emplace_back(
        DownloadCoreServiceFactory::GetInstance(),
        base::BindRepeating(&SetUpDownloadCoreService, download_manager_));
    builder.AddTestingFactories(std::move(testing_factories));
    incognito_profile_ = builder.BuildIncognito(profile_);
    std::unique_ptr<NiceMock<content::MockDownloadManager>>
        incognito_download_manager = SetUpDownloadManager();
    incognito_download_manager_ = incognito_download_manager.get();
    EXPECT_CALL(*incognito_download_manager_, GetBrowserContext())
        .WillRepeatedly(Return(incognito_profile_));
    incognito_profile_->SetDownloadManagerForTesting(
        std::move(incognito_download_manager));
    incognito_update_service_ = static_cast<DownloadBubbleUpdateService*>(
        DownloadBubbleUpdateServiceFactory::GetInstance()
            ->SetTestingFactoryAndUse(
                incognito_profile_,
                base::BindRepeating(
                    &DownloadBubbleUpdateServiceTest::CreateUpdateService,
                    base::Unretained(this), incognito_update_service_)));
    incognito_download_core_service_ =
        DownloadCoreServiceFactory::GetInstance()->GetForBrowserContext(
            incognito_profile_);
    // Do not call Initialize here to delay registering the download manager for
    // the test.
  }

  void TearDown() override {
    RemoveDownloadItemsObserver(
        incognito_download_items_,
        incognito_update_service_->download_item_notifier_for_testing());
    RemoveDownloadItemsObserver(
        download_items_, incognito_update_service_
                             ->original_download_item_notifier_for_testing());
    incognito_download_core_service_->GetDownloadManagerDelegate()->Shutdown();
    incognito_update_service_ = nullptr;
    incognito_download_manager_ = nullptr;
    incognito_download_core_service_ = nullptr;
    incognito_profile_ = nullptr;
    DownloadBubbleUpdateServiceTest::TearDown();
  }

 protected:
  raw_ptr<Profile, DanglingUntriaged> incognito_profile_ = nullptr;
  raw_ptr<NiceMock<content::MockDownloadManager>, DanglingUntriaged>
      incognito_download_manager_ = nullptr;
  std::vector<std::unique_ptr<NiceMockDownloadItem>> incognito_download_items_;
  raw_ptr<DownloadBubbleUpdateService, DanglingUntriaged>
      incognito_update_service_ = nullptr;
  raw_ptr<DownloadCoreService> incognito_download_core_service_ = nullptr;
};

// Tests that initializing an update service for an incognito profile sets both
// the download manager and the original download manager.
TEST_F(DownloadBubbleUpdateServiceIncognitoTest, InitIncognito) {
  base::Time now = base::Time::Now();
  // |observe| is false because this only tests initialization.
  InitDownloadItem(DownloadState::COMPLETE, "regular_profile_download",
                   /*is_paused=*/false, now - base::Hours(1),
                   /*web_app_id=*/nullptr, /*is_crx=*/false,
                   /*observe=*/false);
  InitDownloadItem(*incognito_download_manager_, *incognito_update_service_,
                   incognito_download_items_, incognito_profile_,
                   DownloadState::COMPLETE, "incognito_profile_download",
                   /*is_paused=*/false, now, /*web_app_id=*/nullptr,
                   /*is_crx=*/false,
                   /*observe=*/false);

  // Initial state: Regular profile's update service has a manager, incognito's
  // does not.
  EXPECT_NE(update_service_->GetDownloadManager(), nullptr);
  EXPECT_EQ(update_service_->GetDownloadManager(), download_manager_);
  EXPECT_EQ(incognito_update_service_->GetDownloadManager(), nullptr);

  incognito_update_service_->Initialize(incognito_download_manager_);
  // Run the offline content provider initialization.
  task_environment_.RunUntilIdle();
  // Regular profile's update service's manager hasn't changed.
  EXPECT_EQ(update_service_->GetDownloadManager(), download_manager_);
  // Incognito profile's update service's manager now set correctly.
  EXPECT_EQ(incognito_update_service_->GetDownloadManager(),
            incognito_download_manager_);

  // Both download items should be present after initialization.
  DownloadUIModelPtrVector models;
  EXPECT_TRUE(incognito_update_service_->GetAllModelsToDisplay(
      models, /*web_app_id=*/nullptr));
  ASSERT_EQ(models.size(), 2u);
  EXPECT_EQ(models[0]->GetContentId().id, "incognito_profile_download");
  EXPECT_EQ(models[1]->GetContentId().id, "regular_profile_download");
}

// Tests that the DownloadBubbleDisplayInfo is updated when a download with an
// ephemeral warning expires.
TEST_F(DownloadBubbleUpdateServiceTest, OnEphemeralWarningExpired) {
  base::Time now = base::Time::Now();
  InitDownloadItem(DownloadState::IN_PROGRESS, "normal_download",
                   /*is_paused=*/false, now);
  InitDownloadItem(DownloadState::IN_PROGRESS, "ephemeral_warning_download",
                   /*is_paused=*/false, now);

  DownloadUIModelPtrVector models;
  EXPECT_TRUE(
      update_service_->GetAllModelsToDisplay(models, /*web_app_id=*/nullptr));
  EXPECT_EQ(models.size(), 2u);
  DownloadBubbleDisplayInfo info =
      update_service_->GetDisplayInfo(/*web_app_id=*/nullptr);
  EXPECT_EQ(info.all_models_size, 2u);

  // Mark the download with an ephemeral warning.
  UpdateDownloadItem(1, DownloadState::IN_PROGRESS, /*is_paused=*/false,
                     DownloadDangerType::DOWNLOAD_DANGER_TYPE_DANGEROUS_FILE);

  // Simulate showing the warning in the UI and waiting past the expiry time.
  DownloadItemModel(&GetDownloadItem(1)).SetEphemeralWarningUiShownTime(now);
  task_environment_.FastForwardBy(
      DownloadItemModel::kEphemeralWarningLifetimeOnBubble * 2);

  update_service_->OnEphemeralWarningExpired(GetDownloadItem(1).GetGuid());

  // The ephemeral warning download should no longer be observable.
  // Check GetDisplayInfo first, because GetAllModelsToDisplay will prune it.
  info = update_service_->GetDisplayInfo(/*web_app_id=*/nullptr);
  EXPECT_EQ(info.all_models_size, 1u);
  EXPECT_TRUE(
      update_service_->GetAllModelsToDisplay(models, /*web_app_id=*/nullptr));
  ASSERT_EQ(models.size(), 1u);
  EXPECT_EQ(models[0]->GetContentId().id, "normal_download");
}

// Tests that a download with an ephemeral warning from the original profile is
// properly handled when it expires.
TEST_F(DownloadBubbleUpdateServiceIncognitoTest,
       OnEphemeralWarningExpiredFromOriginalProfile) {
  base::Time now = base::Time::Now();
  InitDownloadItem(DownloadState::COMPLETE, "regular_profile_normal_download",
                   /*is_paused=*/false, now - base::Hours(1),
                   /*web_app_id=*/nullptr, /*is_crx=*/false,
                   /*observe=*/false);
  InitDownloadItem(DownloadState::COMPLETE,
                   "regular_profile_ephemeral_warning_download",
                   /*is_paused=*/false, now - base::Hours(1),
                   /*web_app_id=*/nullptr, /*is_crx=*/false,
                   /*observe=*/false);
  InitDownloadItem(*incognito_download_manager_, *incognito_update_service_,
                   incognito_download_items_, incognito_profile_,
                   DownloadState::COMPLETE,
                   "incognito_profile_ephemeral_warning_download",
                   /*is_paused=*/false, now, /*web_app_id=*/nullptr,
                   /*is_crx=*/false,
                   /*observe=*/false);
  incognito_update_service_->Initialize(incognito_download_manager_);
  // Run the offline content provider initialization.
  task_environment_.RunUntilIdle();

  DownloadUIModelPtrVector models;
  EXPECT_TRUE(incognito_update_service_->GetAllModelsToDisplay(
      models, /*web_app_id=*/nullptr));
  EXPECT_EQ(models.size(), 3u);
  DownloadBubbleDisplayInfo info =
      incognito_update_service_->GetDisplayInfo(/*web_app_id=*/nullptr);
  EXPECT_EQ(info.all_models_size, 3u);

  // Mark the regular profile ephemeral download with an ephemeral warning.
  UpdateDownloadItem(1, DownloadState::IN_PROGRESS, /*is_paused=*/false,
                     DownloadDangerType::DOWNLOAD_DANGER_TYPE_DANGEROUS_FILE);
  // Mark the incognito profile ephemeral download with an ephemeral warning.
  UpdateDownloadItem(*incognito_download_items_[0], DownloadState::IN_PROGRESS,
                     /*is_paused=*/false,
                     DownloadDangerType::DOWNLOAD_DANGER_TYPE_DANGEROUS_FILE);

  // Simulate showing the warning in the UI and waiting past the expiry time.
  DownloadItemModel(&GetDownloadItem(1)).SetEphemeralWarningUiShownTime(now);
  DownloadItemModel(incognito_download_items_[0].get())
      .SetEphemeralWarningUiShownTime(now);
  task_environment_.FastForwardBy(
      DownloadItemModel::kEphemeralWarningLifetimeOnBubble * 2);

  incognito_update_service_->OnEphemeralWarningExpired(
      GetDownloadItem(1).GetGuid());
  incognito_update_service_->OnEphemeralWarningExpired(
      incognito_download_items_[0]->GetGuid());

  // The ephemeral warning downloads should no longer be observable.
  // Check GetDisplayInfo first, because GetAllModelsToDisplay will prune
  // them.
  info = incognito_update_service_->GetDisplayInfo(/*web_app_id=*/nullptr);
  EXPECT_EQ(info.all_models_size, 1u);
  EXPECT_TRUE(incognito_update_service_->GetAllModelsToDisplay(
      models, /*web_app_id=*/nullptr));
  ASSERT_EQ(models.size(), 1u);
  EXPECT_EQ(models[0]->GetContentId().id, "regular_profile_normal_download");
}

}  // namespace
