chromium/ios/chrome/browser/ui/content_suggestions/magic_stack/magic_stack_ranking_model_unittest.mm

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

#import "ios/chrome/browser/ui/content_suggestions/magic_stack/magic_stack_ranking_model.h"

#import "base/test/ios/wait_util.h"
#import "base/test/metrics/histogram_tester.h"
#import "base/test/scoped_command_line.h"
#import "base/test/scoped_feature_list.h"
#import "base/test/test_timeouts.h"
#import "base/threading/thread_restrictions.h"
#import "components/commerce/core/commerce_feature_list.h"
#import "components/commerce/core/mock_shopping_service.h"
#import "components/feature_engagement/test/mock_tracker.h"
#import "components/ntp_tiles/icon_cacher.h"
#import "components/ntp_tiles/most_visited_sites.h"
#import "components/segmentation_platform/embedder/home_modules/constants.h"
#import "components/segmentation_platform/public/constants.h"
#import "components/segmentation_platform/public/features.h"
#import "components/segmentation_platform/public/segmentation_platform_service.h"
#import "components/signin/public/base/signin_pref_names.h"
#import "components/sync_preferences/testing_pref_service_syncable.h"
#import "ios/chrome/app/application_delegate/app_state.h"
#import "ios/chrome/browser/commerce/model/shopping_service_factory.h"
#import "ios/chrome/browser/default_browser/model/utils_test_support.h"
#import "ios/chrome/browser/favicon/model/ios_chrome_large_icon_cache_factory.h"
#import "ios/chrome/browser/favicon/model/ios_chrome_large_icon_service_factory.h"
#import "ios/chrome/browser/feature_engagement/model/tracker_factory.h"
#import "ios/chrome/browser/first_run/model/first_run.h"
#import "ios/chrome/browser/first_run/ui_bundled/first_run_util.h"
#import "ios/chrome/browser/ntp/model/set_up_list_prefs.h"
#import "ios/chrome/browser/reading_list/model/reading_list_model_factory.h"
#import "ios/chrome/browser/reading_list/model/reading_list_test_utils.h"
#import "ios/chrome/browser/safety_check/model/ios_chrome_safety_check_manager_factory.h"
#import "ios/chrome/browser/segmentation_platform/model/segmentation_platform_service_factory.h"
#import "ios/chrome/browser/shared/coordinator/scene/test/fake_scene_state.h"
#import "ios/chrome/browser/shared/model/application_context/application_context.h"
#import "ios/chrome/browser/shared/model/browser/test/test_browser.h"
#import "ios/chrome/browser/shared/model/profile/test/test_profile_ios.h"
#import "ios/chrome/browser/shared/model/profile/test/test_profile_manager_ios.h"
#import "ios/chrome/browser/shared/public/features/features.h"
#import "ios/chrome/browser/signin/model/authentication_service.h"
#import "ios/chrome/browser/signin/model/authentication_service_factory.h"
#import "ios/chrome/browser/signin/model/fake_authentication_service_delegate.h"
#import "ios/chrome/browser/signin/model/identity_manager_factory.h"
#import "ios/chrome/browser/start_surface/ui_bundled/start_surface_recent_tab_browser_agent.h"
#import "ios/chrome/browser/sync/model/mock_sync_service_utils.h"
#import "ios/chrome/browser/sync/model/sync_service_factory.h"
#import "ios/chrome/browser/ui/content_suggestions/cells/content_suggestions_most_visited_action_item.h"
#import "ios/chrome/browser/ui/content_suggestions/cells/content_suggestions_most_visited_item.h"
#import "ios/chrome/browser/ui/content_suggestions/cells/content_suggestions_shortcut_tile_view.h"
#import "ios/chrome/browser/ui/content_suggestions/cells/most_visited_tiles_config.h"
#import "ios/chrome/browser/ui/content_suggestions/cells/most_visited_tiles_mediator.h"
#import "ios/chrome/browser/ui/content_suggestions/cells/shortcuts_mediator.h"
#import "ios/chrome/browser/ui/content_suggestions/content_suggestions_constants.h"
#import "ios/chrome/browser/ui/content_suggestions/content_suggestions_consumer.h"
#import "ios/chrome/browser/ui/content_suggestions/content_suggestions_metrics_constants.h"
#import "ios/chrome/browser/ui/content_suggestions/content_suggestions_metrics_recorder.h"
#import "ios/chrome/browser/ui/content_suggestions/magic_stack/magic_stack_ranking_model_delegate.h"
#import "ios/chrome/browser/ui/content_suggestions/parcel_tracking/parcel_tracking_item.h"
#import "ios/chrome/browser/ui/content_suggestions/parcel_tracking/parcel_tracking_mediator.h"
#import "ios/chrome/browser/ui/content_suggestions/price_tracking_promo/price_tracking_promo_mediator.h"
#import "ios/chrome/browser/ui/content_suggestions/safety_check/safety_check_magic_stack_mediator.h"
#import "ios/chrome/browser/ui/content_suggestions/set_up_list/set_up_list_mediator.h"
#import "ios/chrome/browser/ui/content_suggestions/set_up_list/utils.h"
#import "ios/chrome/browser/ui/content_suggestions/tab_resumption/tab_resumption_helper_delegate.h"
#import "ios/chrome/browser/ui/content_suggestions/tab_resumption/tab_resumption_item.h"
#import "ios/chrome/browser/ui/content_suggestions/tab_resumption/tab_resumption_mediator.h"
#import "ios/chrome/browser/url_loading/model/fake_url_loading_browser_agent.h"
#import "ios/chrome/browser/url_loading/model/url_loading_notifier_browser_agent.h"
#import "ios/chrome/test/ios_chrome_scoped_testing_local_state.h"
#import "ios/chrome/test/ios_chrome_scoped_testing_variations_service.h"
#import "ios/web/public/test/web_task_environment.h"
#import "testing/platform_test.h"
#import "third_party/ocmock/OCMock/OCMock.h"
#import "third_party/ocmock/gtest_support.h"

using set_up_list_prefs::SetUpListItemState;
using startup_metric_utils::FirstRunSentinelCreationResult;

namespace {
std::unique_ptr<KeyedService> BuildFeatureEngagementMockTracker(
    web::BrowserState* browser_state) {
  return std::make_unique<feature_engagement::test::MockTracker>();
}
}  // namespace

// Expose -addConsumer: to validate consumer calls.
@interface SetUpListMediator (Testing)
- (void)addConsumer:(id<SetUpListConsumer>)consumer;
@end

// Fake subclass of SetUpListMediator to easily allow for Set Up List to be
// shown.
@interface FakeSetUpListMediator : SetUpListMediator

// Allows enabling or disabling the SetUpList.
@property(nonatomic, assign) BOOL shouldShowSetUpList;

@end

@implementation FakeSetUpListMediator
@end

// Fake subclass of ParcelTrackingMediator to override item config construction.
@interface FakeParcelTrackingMediator : ParcelTrackingMediator
@end

@implementation FakeParcelTrackingMediator {
  ParcelTrackingItem* _item;
}
- (ParcelTrackingItem*)parcelTrackingItemToShow {
  if (!_item) {
    _item = [[ParcelTrackingItem alloc] init];
  }
  return _item;
}
@end

// Fake subclass of TabResumptionMediator to override item config construction.
@interface FakeTabResumptionMediator : TabResumptionMediator
@end

@implementation FakeTabResumptionMediator {
  TabResumptionItem* _item;
}

- (TabResumptionItem*)itemConfig {
  if (!_item) {
    _item = [[TabResumptionItem alloc] initWithItemType:kMostRecentTab];
    _item.tabURL = GURL("http://test.com");
  }
  return _item;
}

- (void)fetchLastTabResumptionItem {
}

@end

// Fake subclass of MostVisitedTilesMediator to override item config
// construction.
@interface FakeMostVisitedTilesMediator : MostVisitedTilesMediator
@end

@implementation FakeMostVisitedTilesMediator {
  MostVisitedTilesConfig* _config;
}

- (MostVisitedTilesConfig*)mostVisitedConfig {
  if (!_config) {
    _config = [[MostVisitedTilesConfig alloc] init];
    _config.mostVisitedItems =
        @[ [[ContentSuggestionsMostVisitedItem alloc] init] ];
  }
  return _config;
}

@end

// Fake MagicStackRankingModelDelegate receiver to validate results sent by
// MagicStackRankingModel.
@interface FakeMagicStackRankingModelDelegate
    : NSObject <MagicStackRankingModelDelegate>
@property(strong, nonatomic) NSArray<MagicStackModule*>* rank;
@property(strong, nonatomic) MagicStackModule* lastInsertedItem;
@property(nonatomic, assign) NSUInteger lastInsertionIndex;
@property(strong, nonatomic) MagicStackModule* lastReplacedItem;
@property(strong, nonatomic) MagicStackModule* lastReplacingItem;
@end

@implementation FakeMagicStackRankingModelDelegate

- (void)magicStackRankingModel:(MagicStackRankingModel*)model
      didGetLatestRankingOrder:(NSArray<MagicStackModule*>*)rank {
  _rank = rank;
}
- (void)magicStackRankingModel:(MagicStackRankingModel*)model
                 didInsertItem:(MagicStackModule*)item
                       atIndex:(NSUInteger)index {
  _lastInsertedItem = item;
  _lastInsertionIndex = index;
}
- (void)magicStackRankingModel:(MagicStackRankingModel*)model
                didReplaceItem:(MagicStackModule*)oldItem
                      withItem:(MagicStackModule*)item {
  _lastReplacedItem = oldItem;
  _lastReplacingItem = item;
}

- (void)magicStackRankingModel:(MagicStackRankingModel*)model
                 didRemoveItem:(MagicStackModule*)item {
}

- (void)magicStackRankingModel:(MagicStackRankingModel*)model
            didReconfigureItem:(MagicStackModule*)item {
}

@end

// Expose -hasReceivedMagicStackResponse for waiting for ranking to return.
@interface MagicStackRankingModel (Testing) <
    MostVisitedTilesMediatorDelegate,
    ParcelTrackingMediatorDelegate,
    SafetyCheckMagicStackMediatorDelegate,
    TabResumptionHelperDelegate>
@property(nonatomic, assign, readonly) BOOL hasReceivedMagicStackResponse;
@property(nonatomic, assign, readonly) BOOL hasReceivedEphemericalCardResponse;
@end

// Testing Suite for MagicStackRankingModel
class MagicStackRankingModelTest : public PlatformTest {
 public:
  void SetUp() override {
    scoped_command_line_.GetProcessCommandLine()->AppendSwitchASCII(
        segmentation_platform::kEphemeralModuleBackendRankerTestOverride,
        "price_tracking_notification_promo");
    scoped_feature_list_.InitWithFeaturesAndParameters(
        {{kMagicStack, {{kMagicStackMostVisitedModuleParam, "true"}}}}, {});

    TestChromeBrowserState::Builder test_cbs_builder;
    test_cbs_builder.AddTestingFactory(
        AuthenticationServiceFactory::GetInstance(),
        AuthenticationServiceFactory::GetDefaultFactory());
    test_cbs_builder.AddTestingFactory(
        SyncServiceFactory::GetInstance(),
        base::BindRepeating(&CreateMockSyncService));
    test_cbs_builder.AddTestingFactory(
        segmentation_platform::SegmentationPlatformServiceFactory::
            GetInstance(),
        segmentation_platform::SegmentationPlatformServiceFactory::
            GetDefaultFactory());
    test_cbs_builder.AddTestingFactory(
        ReadingListModelFactory::GetInstance(),
        base::BindRepeating(&BuildReadingListModelWithFakeStorage,
                            std::vector<scoped_refptr<ReadingListEntry>>()));
    test_cbs_builder.AddTestingFactory(
        feature_engagement::TrackerFactory::GetInstance(),
        base::BindRepeating(&BuildFeatureEngagementMockTracker));
    test_cbs_builder.AddTestingFactory(
        IOSChromeLargeIconServiceFactory::GetInstance(),
        IOSChromeLargeIconServiceFactory::GetDefaultFactory());
    test_cbs_builder.AddTestingFactory(
        commerce::ShoppingServiceFactory::GetInstance(),
        base::BindRepeating(
            [](web::BrowserState*) -> std::unique_ptr<KeyedService> {
              return commerce::MockShoppingService::Build();
            }));

    browser_state_ =
        profile_manager_.AddProfileWithBuilder(std::move(test_cbs_builder));

    browser_ = std::make_unique<TestBrowser>(GetBrowserState());

    // Necessary set up for kIOSSetUpList.
    GetLocalState()->ClearPref(set_up_list_prefs::kDisabled);
    ClearDefaultBrowserPromoData();
    WriteFirstRunSentinel();

    // Necessary set up for parcel tracking.
    scoped_variations_service_ =
        std::make_unique<IOSChromeScopedTestingVariationsService>();
    scoped_variations_service_->Get()->OverrideStoredPermanentCountry("us");

    AuthenticationServiceFactory::CreateAndInitializeForBrowserState(
        GetBrowserState(),
        std::make_unique<FakeAuthenticationServiceDelegate>());
    syncer::SyncService* syncService =
        SyncServiceFactory::GetForBrowserState(GetBrowserState());
    AuthenticationService* authenticationService =
        AuthenticationServiceFactory::GetForBrowserState(GetBrowserState());
    signin::IdentityManager* identityManager =
        IdentityManagerFactory::GetForBrowserState(GetBrowserState());

    UrlLoadingNotifierBrowserAgent::CreateForBrowser(browser_.get());
    FakeUrlLoadingBrowserAgent::InjectForBrowser(browser_.get());
    url_loader_ = FakeUrlLoadingBrowserAgent::FromUrlLoadingBrowserAgent(
        UrlLoadingBrowserAgent::FromBrowser(browser_.get()));
    StartSurfaceRecentTabBrowserAgent::CreateForBrowser(browser_.get());

    ReadingListModel* readingListModel =
        ReadingListModelFactory::GetForBrowserState(GetBrowserState());
    feature_engagement::Tracker* tracker =
        feature_engagement::TrackerFactory::GetForBrowserState(
            GetBrowserState());
    AuthenticationService* authentication_service =
        AuthenticationServiceFactory::GetForBrowserState(GetBrowserState());
    _shortcutsMediator = [[ShortcutsMediator alloc]
        initWithReadingListModel:readingListModel
        featureEngagementTracker:(feature_engagement::Tracker*)tracker
                     authService:authentication_service];
    _setUpListMediator = [[FakeSetUpListMediator alloc]
                   initWithPrefService:GetBrowserState()->GetPrefs()
                           syncService:syncService
                       identityManager:identityManager
                 authenticationService:authenticationService
                            sceneState:scene_state_
                 isDefaultSearchEngine:NO
                   segmentationService:nullptr
        deviceSwitcherResultDispatcher:nullptr];
    _setUpListMediator.shouldShowSetUpList = YES;
    _parcelTrackingMediator = [[FakeParcelTrackingMediator alloc]
        initWithShoppingService:commerce::ShoppingServiceFactory::
                                    GetForBrowserState(GetBrowserState())
         URLLoadingBrowserAgent:url_loader_
                    prefService:GetLocalState()];
    _tabResumptionMediator = [[FakeTabResumptionMediator alloc]
        initWithLocalState:GetLocalState()
               prefService:GetBrowserState()->GetPrefs()
           identityManager:identityManager
                   browser:browser_.get()];
    favicon::LargeIconService* large_icon_service =
        IOSChromeLargeIconServiceFactory::GetForBrowserState(GetBrowserState());
    LargeIconCache* cache =
        IOSChromeLargeIconCacheFactory::GetForBrowserState(GetBrowserState());
    std::unique_ptr<ntp_tiles::MostVisitedSites> most_visited_sites =
        std::make_unique<ntp_tiles::MostVisitedSites>(
            &pref_service_, /*identity_manager*/ nullptr,
            /*supervised_user_service*/ nullptr, /*top_sites*/ nullptr,
            /*popular_sites*/ nullptr,
            /*custom_links*/ nullptr, /*icon_cacher*/ nullptr, true);
    _mostVisitedTilesMediator = [[FakeMostVisitedTilesMediator alloc]
        initWithMostVisitedSite:std::move(most_visited_sites)
                    prefService:GetBrowserState()->GetPrefs()
               largeIconService:large_icon_service
                 largeIconCache:cache
         URLLoadingBrowserAgent:url_loader_];

    id mockAppState = OCMClassMock([AppState class]);

    _safetyCheckMediator = [[SafetyCheckMagicStackMediator alloc]
        initWithSafetyCheckManager:IOSChromeSafetyCheckManagerFactory::
                                       GetForBrowserState(GetBrowserState())
                        localState:GetLocalState()
                         userState:GetBrowserState()->GetPrefs()
                          appState:mockAppState];

    _priceTrackingPromoMediator = [[PriceTrackingPromoMediator alloc]
        initWithShoppingService:commerce::ShoppingServiceFactory::
                                    GetForBrowserState(GetBrowserState())];

    _magicStackRankingModel = [[MagicStackRankingModel alloc]
        initWithSegmentationService:
            segmentation_platform::SegmentationPlatformServiceFactory::
                GetForBrowserState(GetBrowserState())
                    shoppingService:commerce::ShoppingServiceFactory::
                                        GetForBrowserState(GetBrowserState())
                        prefService:GetBrowserState()->GetPrefs()
                         localState:GetLocalState()
                    moduleMediators:@[
                      _shortcutsMediator,
                      _setUpListMediator,
                      _parcelTrackingMediator,
                      _tabResumptionMediator,
                      _mostVisitedTilesMediator,
                      _safetyCheckMediator,
                      _priceTrackingPromoMediator,
                    ]];

    metrics_recorder_ = [[ContentSuggestionsMetricsRecorder alloc]
        initWithLocalState:GetLocalState()];
    _magicStackRankingModel.contentSuggestionsMetricsRecorder =
        metrics_recorder_;
    _setUpListMediator.contentSuggestionsMetricsRecorder = metrics_recorder_;

    histogram_tester_ = std::make_unique<base::HistogramTester>();
  }

  ChromeBrowserState* GetBrowserState() { return browser_state_.get(); }

  PrefService* GetLocalState() {
    return GetApplicationContext()->GetLocalState();
  }

  ~MagicStackRankingModelTest() override {
    [_setUpListMediator disconnect];
    [_tabResumptionMediator disconnect];
    [_parcelTrackingMediator disconnect];
    [_shortcutsMediator disconnect];
    [_mostVisitedTilesMediator disconnect];
    [_safetyCheckMediator disconnect];
    [_priceTrackingPromoMediator disconnect];
  }

 protected:
  // Clears and re-writes the FirstRun sentinel file, in order to allow Set Up
  // List to display.
  void WriteFirstRunSentinel() {
    base::ScopedAllowBlockingForTesting allow_blocking;
    FirstRun::RemoveSentinel();
    base::File::Error file_error = base::File::FILE_OK;
    FirstRunSentinelCreationResult sentinel_created =
        FirstRun::CreateSentinel(&file_error);
    ASSERT_EQ(sentinel_created, FirstRunSentinelCreationResult::kSuccess)
        << "Error creating FirstRun sentinel: "
        << base::File::ErrorToString(file_error);
    FirstRun::LoadSentinelInfo();
    FirstRun::ClearStateForTesting();
    EXPECT_FALSE(set_up_list_prefs::IsSetUpListDisabled(GetLocalState()));
    EXPECT_FALSE(FirstRun::IsChromeFirstRun());
    EXPECT_TRUE(set_up_list_utils::IsSetUpListActive(GetLocalState()));
  }

  web::WebTaskEnvironment task_environment_;
  base::test::ScopedCommandLine scoped_command_line_;
  base::test::ScopedFeatureList scoped_feature_list_;
  IOSChromeScopedTestingLocalState scoped_testing_local_state_;
  TestProfileManagerIOS profile_manager_;
  raw_ptr<ChromeBrowserState> browser_state_;
  sync_preferences::TestingPrefServiceSyncable pref_service_;
  FakeSceneState* scene_state_;
  std::unique_ptr<Browser> browser_;
  FakeUrlLoadingBrowserAgent* url_loader_;
  std::unique_ptr<IOSChromeScopedTestingVariationsService>
      scoped_variations_service_;
  FakeSetUpListMediator* _setUpListMediator;
  FakeParcelTrackingMediator* _parcelTrackingMediator;
  FakeTabResumptionMediator* _tabResumptionMediator;
  ShortcutsMediator* _shortcutsMediator;
  SafetyCheckMagicStackMediator* _safetyCheckMediator;
  MostVisitedTilesMediator* _mostVisitedTilesMediator;
  PriceTrackingPromoMediator* _priceTrackingPromoMediator;
  MagicStackRankingModel* _magicStackRankingModel;
  id setUpListConsumer_;
  ContentSuggestionsMetricsRecorder* metrics_recorder_;
  std::unique_ptr<base::HistogramTester> histogram_tester_;
};

TEST_F(MagicStackRankingModelTest, TestSetUpListConsumerCall) {
  setUpListConsumer_ = OCMStrictProtocolMock(@protocol(SetUpListConsumer));
  [_setUpListMediator addConsumer:setUpListConsumer_];
  [_magicStackRankingModel fetchLatestMagicStackRanking];

  OCMExpect([setUpListConsumer_ setUpListItemDidComplete:[OCMArg any]
                                       allItemsCompleted:NO
                                              completion:[OCMArg any]]);
  set_up_list_prefs::MarkItemComplete(GetLocalState(),
                                      SetUpListItemType::kSignInSync);
  OCMExpect([setUpListConsumer_ setUpListItemDidComplete:[OCMArg any]
                                       allItemsCompleted:NO
                                              completion:[OCMArg any]]);
  set_up_list_prefs::MarkItemComplete(GetLocalState(),
                                      SetUpListItemType::kDefaultBrowser);
  OCMExpect([setUpListConsumer_ setUpListItemDidComplete:[OCMArg any]
                                       allItemsCompleted:YES
                                              completion:[OCMArg any]]);
  set_up_list_prefs::MarkItemComplete(GetLocalState(),
                                      SetUpListItemType::kAutofill);
  EXPECT_OCMOCK_VERIFY(setUpListConsumer_);
}

// Tests that SetUpList metrics are recorded when it is in the MagicStack.
TEST_F(MagicStackRankingModelTest, TestMetricsWithSetUpList) {
  [_magicStackRankingModel fetchLatestMagicStackRanking];
  EXPECT_TRUE(base::test::ios::WaitUntilConditionOrTimeout(
      TestTimeouts::action_timeout(), true, ^bool() {
        base::RunLoop().RunUntilIdle();
        return _magicStackRankingModel.hasReceivedMagicStackResponse;
      }));
  histogram_tester_->ExpectUniqueSample("IOS.SetUpList.Displayed", true, 1);
  histogram_tester_->ExpectTotalCount("IOS.SetUpList.ItemDisplayed", 2);
  histogram_tester_->ExpectBucketCount("IOS.SetUpList.ItemDisplayed",
                                       SetUpListItemType::kDefaultBrowser, 1);
  histogram_tester_->ExpectBucketCount("IOS.SetUpList.ItemDisplayed",
                                       SetUpListItemType::kAutofill, 1);
}

// Tests that SetUpList metrics are not recorded when it is not in the
// MagicStack.
TEST_F(MagicStackRankingModelTest, TestMetricsWithoutSetUpList) {
  _setUpListMediator.shouldShowSetUpList = NO;
  [_magicStackRankingModel fetchLatestMagicStackRanking];
  EXPECT_TRUE(base::test::ios::WaitUntilConditionOrTimeout(
      TestTimeouts::action_timeout(), true, ^bool() {
        base::RunLoop().RunUntilIdle();
        return _magicStackRankingModel.hasReceivedMagicStackResponse;
      }));
  histogram_tester_->ExpectTotalCount("IOS.SetUpList.Displayed", 0);
  histogram_tester_->ExpectTotalCount("IOS.SetUpList.ItemDisplayed", 0);
}

// Tests that when the user changes the setting to disable signin, the
// SetUpList signin item is marked complete.
TEST_F(MagicStackRankingModelTest, TestOnServiceStatusChanged) {
  // Verify the initial state.
  SetUpListItemState item_state = set_up_list_prefs::GetItemState(
      GetLocalState(), SetUpListItemType::kSignInSync);
  EXPECT_EQ(item_state, SetUpListItemState::kNotComplete);

  // Simulate the user disabling signin.
  GetBrowserState()->GetPrefs()->SetBoolean(prefs::kSigninAllowed, false);
  // Verify that the signin item is complete.
  item_state = set_up_list_prefs::GetItemState(GetLocalState(),
                                               SetUpListItemType::kSignInSync);
  EXPECT_EQ(item_state, SetUpListItemState::kCompleteInList);
}

// Tests that logging for IOS.MagicStack.Module.Click.[ModuleName] works
// correctly.
TEST_F(MagicStackRankingModelTest, TestModuleClickIndexMetric) {
  [_magicStackRankingModel fetchLatestMagicStackRanking];

  EXPECT_TRUE(base::test::ios::WaitUntilConditionOrTimeout(
      TestTimeouts::action_timeout(), true, ^bool() {
        base::RunLoop().RunUntilIdle();
        return _magicStackRankingModel.hasReceivedMagicStackResponse;
      }));

  [_magicStackRankingModel logMagicStackEngagementForType:
                               ContentSuggestionsModuleType::kSetUpListSync];
  histogram_tester_->ExpectUniqueSample("IOS.MagicStack.Module.Click.SetUpList",
                                        0, 1);

  [_magicStackRankingModel logMagicStackEngagementForType:
                               ContentSuggestionsModuleType::kMostVisited];
  histogram_tester_->ExpectUniqueSample(
      "IOS.MagicStack.Module.Click.MostVisited", 1, 1);
}

// Test that the ranking model passed an expected list of module configs in
// -didGetLatestRankingOrder:
TEST_F(MagicStackRankingModelTest, TestModelDidGetLatestRankingOrder) {
  FakeMagicStackRankingModelDelegate* delegate_ =
      [[FakeMagicStackRankingModelDelegate alloc] init];
  _magicStackRankingModel.delegate = delegate_;
  [_magicStackRankingModel fetchLatestMagicStackRanking];

  EXPECT_TRUE(base::test::ios::WaitUntilConditionOrTimeout(
      TestTimeouts::action_timeout(), true, ^bool() {
        base::RunLoop().RunUntilIdle();
        return [delegate_.rank count] > 0;
      }));
  NSArray* expectedModuleRank = @[ @(5), @(0), @(1), @(10), @(11) ];
  EXPECT_EQ([delegate_.rank count], [expectedModuleRank count]);
  for (NSUInteger i = 0; i < [expectedModuleRank count]; i++) {
    MagicStackModule* config = delegate_.rank[i];
    EXPECT_EQ(@(int(config.type)), expectedModuleRank[i])
        << "For Magic Stack order index " << i;
  }
}

// Tests that the ranking model sends insertion signals to its delgate in
// response to feature delegate signals.
TEST_F(MagicStackRankingModelTest, TestFeatureInsertCalls) {
  FakeMagicStackRankingModelDelegate* delegate_ =
      [[FakeMagicStackRankingModelDelegate alloc] init];
  _magicStackRankingModel.delegate = delegate_;
  [_magicStackRankingModel fetchLatestMagicStackRanking];
  EXPECT_TRUE(base::test::ios::WaitUntilConditionOrTimeout(
      TestTimeouts::action_timeout(), true, ^bool() {
        base::RunLoop().RunUntilIdle();
        return [delegate_.rank count] > 0;
      }));

  [_magicStackRankingModel newParcelsAvailable];
  EXPECT_EQ(delegate_.lastInsertionIndex, 4u);
  EXPECT_EQ(delegate_.lastInsertedItem,
            _parcelTrackingMediator.parcelTrackingItemToShow);

  [_magicStackRankingModel tabResumptionHelperDidReceiveItem];
  EXPECT_EQ(delegate_.lastInsertionIndex, 3u);
  EXPECT_EQ(delegate_.lastInsertedItem, _tabResumptionMediator.itemConfig);
}

// Test the TestMostVisitedTilesMediatorDelegate API implementations in
// MagicStackRankingModel.
TEST_F(MagicStackRankingModelTest, TestMostVisitedTilesMediatorDelegate) {
  scoped_feature_list_.Reset();
  scoped_feature_list_.InitWithFeaturesAndParameters(
      {{kMagicStack, {{kMagicStackMostVisitedModuleParam, "true"}}}}, {});

  // Assert that delegate API isn't called if rank has not been received yet.
  id mockDelegate =
      OCMStrictProtocolMock(@protocol(MagicStackRankingModelDelegate));
  _magicStackRankingModel.delegate = mockDelegate;
  [_magicStackRankingModel didReceiveInitialMostVistedTiles];
  [_magicStackRankingModel removeMostVisitedTilesModule];
  EXPECT_OCMOCK_VERIFY(mockDelegate);

  FakeMagicStackRankingModelDelegate* fakeDelegate =
      [[FakeMagicStackRankingModelDelegate alloc] init];
  _magicStackRankingModel.delegate = fakeDelegate;
  [_magicStackRankingModel fetchLatestMagicStackRanking];
  EXPECT_TRUE(base::test::ios::WaitUntilConditionOrTimeout(
      TestTimeouts::action_timeout(), true, ^bool() {
        base::RunLoop().RunUntilIdle();
        return [fakeDelegate.rank count] > 0;
      }));

  _magicStackRankingModel.delegate = mockDelegate;
  OCMExpect([mockDelegate magicStackRankingModel:[OCMArg any]
                                   didInsertItem:[OCMArg any]
                                         atIndex:1]);
  [_magicStackRankingModel didReceiveInitialMostVistedTiles];
  OCMExpect([mockDelegate magicStackRankingModel:[OCMArg any]
                                   didRemoveItem:[OCMArg any]]);
  [_magicStackRankingModel removeMostVisitedTilesModule];
  EXPECT_OCMOCK_VERIFY(mockDelegate);
}

// Verifies that the ranking model correctly emits removal signals to its
// delegate in response to feature delegate signals.
TEST_F(MagicStackRankingModelTest,
       TestSafetyCheckMediatorDelegateCallsRemoval) {
  // Assert that delegate API isn't called if rank has not been received yet.
  id mockDelegate =
      OCMStrictProtocolMock(@protocol(MagicStackRankingModelDelegate));
  _magicStackRankingModel.delegate = mockDelegate;
  [_magicStackRankingModel removeSafetyCheckModule];
  EXPECT_OCMOCK_VERIFY(mockDelegate);

  FakeMagicStackRankingModelDelegate* fakeDelegate =
      [[FakeMagicStackRankingModelDelegate alloc] init];
  _magicStackRankingModel.delegate = fakeDelegate;
  [_magicStackRankingModel fetchLatestMagicStackRanking];
  EXPECT_TRUE(base::test::ios::WaitUntilConditionOrTimeout(
      TestTimeouts::action_timeout(), true, ^bool() {
        base::RunLoop().RunUntilIdle();
        return [fakeDelegate.rank count] > 0;
      }));

  _magicStackRankingModel.delegate = mockDelegate;
  OCMExpect([mockDelegate magicStackRankingModel:[OCMArg any]
                                   didRemoveItem:[OCMArg any]]);
  [_magicStackRankingModel removeSafetyCheckModule];
  EXPECT_OCMOCK_VERIFY(mockDelegate);
}

// Test that disabling the Magic Stack ranking model doesn't crash and doesn't
// perform a valid fetch.
TEST_F(MagicStackRankingModelTest, TestDisabledSegmentationRanking) {
  scoped_feature_list_.Reset();
  scoped_feature_list_.InitWithFeaturesAndParameters(
      {}, {{segmentation_platform::features::
                kSegmentationPlatformIosModuleRanker}});
  id mockDelegate =
      OCMStrictProtocolMock(@protocol(MagicStackRankingModelDelegate));
  _magicStackRankingModel.delegate = mockDelegate;
  OCMReject([mockDelegate magicStackRankingModel:[OCMArg any]
                        didGetLatestRankingOrder:[OCMArg any]]);
  [_magicStackRankingModel fetchLatestMagicStackRanking];
  base::RunLoop().RunUntilIdle();
  EXPECT_OCMOCK_VERIFY(mockDelegate);
}

// Test that fetching the ephemeral card to show in the Magic Stack returns the
// correct card.
TEST_F(MagicStackRankingModelTest, TestEphemeralModelDidGetCardToShow) {
  scoped_feature_list_.Reset();
  scoped_feature_list_.InitWithFeatures(
      {commerce::kPriceTrackingPromo,
       segmentation_platform::features::
           kSegmentationPlatformEphemeralCardRanker},
      {});
  commerce::MockShoppingService* shopping_service =
      static_cast<commerce::MockShoppingService*>(
          commerce::ShoppingServiceFactory::GetForBrowserState(
              GetBrowserState()));
  shopping_service->SetIsShoppingListEligible(true);

  FakeMagicStackRankingModelDelegate* delegate_ =
      [[FakeMagicStackRankingModelDelegate alloc] init];
  _magicStackRankingModel.delegate = delegate_;
  [_magicStackRankingModel fetchLatestMagicStackRanking];

  EXPECT_TRUE(base::test::ios::WaitUntilConditionOrTimeout(
      TestTimeouts::action_timeout(), true, ^bool() {
        base::RunLoop().RunUntilIdle();
        return [delegate_.rank count] > 0 &&
               _magicStackRankingModel.hasReceivedMagicStackResponse &&
               _magicStackRankingModel.hasReceivedEphemericalCardResponse;
      }));
  NSArray* expectedModuleRank = @[ @(5), @(15), @(1), @(10), @(11) ];
  EXPECT_EQ([delegate_.rank count], [expectedModuleRank count]);
  for (NSUInteger i = 0; i < [expectedModuleRank count]; i++) {
    MagicStackModule* config = delegate_.rank[i];
    EXPECT_EQ(@(int(config.type)), expectedModuleRank[i])
        << "For Magic Stack order index " << i;
  }
}