// Copyright 2016 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/main/ui_bundled/browser_lifecycle_manager.h"

#import <UIKit/UIKit.h>

#import "base/scoped_multi_source_observation.h"
#import "base/scoped_observation.h"
#import "base/test/scoped_feature_list.h"
#import "components/bookmarks/test/bookmark_test_helpers.h"
#import "components/sync/test/test_sync_service.h"
#import "ios/chrome/app/profile/profile_init_stage.h"
#import "ios/chrome/app/profile/profile_state.h"
#import "ios/chrome/app/profile/profile_state_test_utils.h"
#import "ios/chrome/browser/bookmarks/model/bookmark_model_factory.h"
#import "ios/chrome/browser/browser_view/ui_bundled/browser_view_controller.h"
#import "ios/chrome/browser/favicon/model/favicon_service_factory.h"
#import "ios/chrome/browser/favicon/model/ios_chrome_favicon_loader_factory.h"
#import "ios/chrome/browser/favicon/model/ios_chrome_large_icon_service_factory.h"
#import "ios/chrome/browser/history/model/history_service_factory.h"
#import "ios/chrome/browser/incognito_reauth/ui_bundled/incognito_reauth_scene_agent.h"
#import "ios/chrome/browser/intelligence/bwg/utils/gemini_constants.h"
#import "ios/chrome/browser/main/ui_bundled/wrangled_browser.h"
#import "ios/chrome/browser/saved_tab_groups/model/tab_group_sync_service_factory.h"
#import "ios/chrome/browser/search_engines/model/template_url_service_factory.h"
#import "ios/chrome/browser/sessions/model/session_restoration_observer.h"
#import "ios/chrome/browser/sessions/model/session_restoration_service.h"
#import "ios/chrome/browser/sessions/model/session_restoration_service_factory.h"
#import "ios/chrome/browser/sessions/model/test_session_restoration_observer.h"
#import "ios/chrome/browser/sessions/model/test_session_restoration_service.h"
#import "ios/chrome/browser/shared/coordinator/layout_guide/layout_guide_scene_agent.h"
#import "ios/chrome/browser/shared/coordinator/scene/scene_state.h"
#import "ios/chrome/browser/shared/model/browser/browser.h"
#import "ios/chrome/browser/shared/model/browser/browser_list.h"
#import "ios/chrome/browser/shared/model/browser/browser_list_factory.h"
#import "ios/chrome/browser/shared/model/browser/test/test_browser_list_observer.h"
#import "ios/chrome/browser/shared/model/profile/test/test_profile_ios.h"
#import "ios/chrome/browser/shared/public/commands/command_dispatcher.h"
#import "ios/chrome/browser/shared/public/commands/gemini_commands.h"
#import "ios/chrome/browser/shared/public/commands/scene_commands.h"
#import "ios/chrome/browser/shared/public/commands/settings_commands.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/sync/model/send_tab_to_self_sync_service_factory.h"
#import "ios/chrome/browser/sync/model/sync_service_factory.h"
#import "ios/chrome/browser/sync/model/test_sync_service_utils.h"
#import "ios/chrome/common/ui/reauthentication/reauthentication_module.h"
#import "ios/chrome/test/ios_chrome_scoped_testing_local_state.h"
#import "ios/web/public/test/web_task_environment.h"
#import "testing/platform_test.h"
#import "third_party/ocmock/OCMock/OCMock.h"
#import "ui/base/device_form_factor.h"

class BrowserLifecycleManagerTest : public PlatformTest {
 protected:
  BrowserLifecycleManagerTest() {
    TestProfileIOS::Builder test_profile_builder;
    test_profile_builder.AddTestingFactory(
        SendTabToSelfSyncServiceFactory::GetInstance(),
        SendTabToSelfSyncServiceFactory::GetDefaultFactory());
    test_profile_builder.AddTestingFactory(
        ios::TemplateURLServiceFactory::GetInstance(),
        ios::TemplateURLServiceFactory::GetDefaultFactory());
    test_profile_builder.AddTestingFactory(
        IOSChromeFaviconLoaderFactory::GetInstance(),
        IOSChromeFaviconLoaderFactory::GetDefaultFactory());
    test_profile_builder.AddTestingFactory(
        IOSChromeLargeIconServiceFactory::GetInstance(),
        IOSChromeLargeIconServiceFactory::GetDefaultFactory());
    test_profile_builder.AddTestingFactory(
        ios::FaviconServiceFactory::GetInstance(),
        ios::FaviconServiceFactory::GetDefaultFactory());
    test_profile_builder.AddTestingFactory(
        ios::HistoryServiceFactory::GetInstance(),
        ios::HistoryServiceFactory::GetDefaultFactory());
    test_profile_builder.AddTestingFactory(
        ios::BookmarkModelFactory::GetInstance(),
        ios::BookmarkModelFactory::GetDefaultFactory());
    test_profile_builder.AddTestingFactory(
        AuthenticationServiceFactory::GetInstance(),
        AuthenticationServiceFactory::GetFactoryWithDelegateForTesting(
            std::make_unique<FakeAuthenticationServiceDelegate>()));
    test_profile_builder.AddTestingFactory(
        SyncServiceFactory::GetInstance(),
        base::BindRepeating(&CreateTestSyncService));
    test_profile_builder.AddTestingFactory(
        SessionRestorationServiceFactory::GetInstance(),
        TestSessionRestorationService::GetTestingFactory());
    test_profile_builder.AddTestingFactory(
        tab_groups::TabGroupSyncServiceFactory::GetInstance(),
        tab_groups::TabGroupSyncServiceFactory::GetDefaultFactory());

    profile_ = std::move(test_profile_builder).Build();
    profile_->CreateOffTheRecordProfileWithTestingFactories(
        {TestProfileIOS::TestingFactory{
            SessionRestorationServiceFactory::GetInstance(),
            TestSessionRestorationService::GetTestingFactory(),
        }});

    profile_state_ = [[ProfileState alloc] initWithAppState:nil];
    SetProfileStateInitStage(profile_state_, ProfileInitStage::kFinal);
    profile_state_.profile = profile_.get();

    scene_state_ = [[SceneState alloc] init];
    scene_state_.profileState = profile_state_;
    scene_state_.sceneSessionID = "scene";

    LayoutGuideSceneAgent* layout_guide_scene_agent =
        [[LayoutGuideSceneAgent alloc] init];
    [scene_state_ addAgent:layout_guide_scene_agent];

    scoped_session_restoration_observation_.AddObservation(
        SessionRestorationServiceFactory::GetForProfile(profile_.get()));
    scoped_session_restoration_observation_.AddObservation(
        SessionRestorationServiceFactory::GetForProfile(
            profile_->GetOffTheRecordProfile()));

    scoped_browser_list_observation_.Observe(
        BrowserListFactory::GetForProfile(profile_.get()));
  }

  void TearDown() override {
    @autoreleasepool {
      scene_state_ = nil;
      profile_state_ = nil;
    }
  }

  void RecreateOffTheRecordProfile() {
    scoped_session_restoration_observation_.RemoveObservation(
        SessionRestorationServiceFactory::GetForProfile(
            profile_->GetOffTheRecordProfile()));

    profile_->DestroyOffTheRecordProfile();
    profile_->CreateOffTheRecordProfileWithTestingFactories(
        {TestProfileIOS::TestingFactory{
            SessionRestorationServiceFactory::GetInstance(),
            TestSessionRestorationService::GetTestingFactory(),
        }});

    scoped_session_restoration_observation_.AddObservation(
        SessionRestorationServiceFactory::GetForProfile(
            profile_->GetOffTheRecordProfile()));
  }

  ProfileIOS* profile() { return profile_.get(); }

  SceneState* scene_state() { return scene_state_; }

  TestSessionRestorationObserver& session_restoration_observer() {
    return session_restoration_observer_;
  }

  TestBrowserListObserver& browser_list_observer() {
    return browser_list_observer_;
  }

 private:
  web::WebTaskEnvironment task_environment_;
  IOSChromeScopedTestingLocalState scoped_testing_local_state_;
  std::unique_ptr<TestProfileIOS> profile_;
  ProfileState* profile_state_;
  SceneState* scene_state_;

  // SessionRestorationObserver and its scoped observation.
  TestSessionRestorationObserver session_restoration_observer_;
  base::ScopedMultiSourceObservation<SessionRestorationService,
                                     SessionRestorationObserver>
      scoped_session_restoration_observation_{&session_restoration_observer_};

  // TestBrowserListObserver and its scoped observation.
  TestBrowserListObserver browser_list_observer_;
  base::ScopedObservation<BrowserList, BrowserListObserver>
      scoped_browser_list_observation_{&browser_list_observer_};
};

TEST_F(BrowserLifecycleManagerTest, TestInitNilObserver) {
  // `task_environment_` must outlive all objects created by BVC, because those
  // objects may rely on threading API in dealloc.
  @autoreleasepool {
    id mock_scene_handler = OCMProtocolMock(@protocol(SceneCommands));
    id mock_settings_handler = OCMProtocolMock(@protocol(SettingsCommands));
    id mock_gemini_handler = OCMProtocolMock(@protocol(GeminiCommands));
    IncognitoReauthSceneAgent* reauth_agent = [[IncognitoReauthSceneAgent alloc]
        initWithReauthModule:[[ReauthenticationModule alloc] init]];
    [scene_state() addAgent:reauth_agent];
    BrowserLifecycleManager* wrangler =
        [[BrowserLifecycleManager alloc] initWithProfile:profile()
                                              sceneState:scene_state()
                                           sceneEndpoint:mock_scene_handler
                                        settingsEndpoint:mock_settings_handler
                                          geminiEndpoint:mock_gemini_handler];
    [wrangler createMainCoordinatorAndInterface];

    // Test that BVC is created on demand.
    UIViewController* bvc = wrangler.mainInterface.viewController;
    EXPECT_NE(bvc, nil);

    // Test that SceneState is associated with the browser.
    SceneState* main_browser_scene_state =
        wrangler.mainInterface.browser->GetSceneState();
    EXPECT_EQ(scene_state(), main_browser_scene_state);

    // Test that once created the BVC isn't re-created.
    EXPECT_EQ(bvc, wrangler.mainInterface.viewController);

    // Test that the OTR objects are (a) OTR and (b) not the same as the non-OTR
    // objects.
    EXPECT_NE(bvc, wrangler.incognitoInterface.viewController);
    EXPECT_TRUE(wrangler.incognitoInterface.profile->IsOffTheRecord());

    // Test that the OTR browser has SceneState associated with it.
    SceneState* otr_browser_scene_state =
        wrangler.incognitoInterface.browser->GetSceneState();
    EXPECT_EQ(scene_state(), otr_browser_scene_state);

    [wrangler shutdown];
  }
}

TEST_F(BrowserLifecycleManagerTest, TestBrowserList) {
  id mock_scene_handler = OCMProtocolMock(@protocol(SceneCommands));
  id mock_settings_handler = OCMProtocolMock(@protocol(SettingsCommands));
  id mock_gemini_handler = OCMProtocolMock(@protocol(GeminiCommands));
  IncognitoReauthSceneAgent* reauth_agent = [[IncognitoReauthSceneAgent alloc]
      initWithReauthModule:[[ReauthenticationModule alloc] init]];
  [scene_state() addAgent:reauth_agent];
  BrowserLifecycleManager* wrangler =
      [[BrowserLifecycleManager alloc] initWithProfile:profile()
                                            sceneState:scene_state()
                                         sceneEndpoint:mock_scene_handler
                                      settingsEndpoint:mock_settings_handler
                                        geminiEndpoint:mock_gemini_handler];

  BrowserList* browser_list = BrowserListFactory::GetForProfile(profile());

  // Create the coordinator and interface. This is required to get access
  // to the Browser via the -mainInterface/-incognitoInterface providers.
  [wrangler createMainCoordinatorAndInterface];

  // The BrowserLifecycleManager creates all browser in its initializer. The
  // first created CL is the main Browser, the second one the inactive
  // Browser, and then the OTR Browser.
  EXPECT_EQ(2UL,
            browser_list
                ->BrowsersOfType(BrowserList::BrowserType::kRegularAndInactive)
                .size());
  EXPECT_EQ(1UL,
            browser_list->BrowsersOfType(BrowserList::BrowserType::kIncognito)
                .size());
  EXPECT_EQ(wrangler.mainInterface.browser->GetInactiveBrowser(),
            browser_list_observer().GetLastAddedBrowser());
  EXPECT_EQ(wrangler.incognitoInterface.browser,
            browser_list_observer().GetLastAddedIncognitoBrowser());

  // Record the old Browser before it is destroyed. This will be dangling
  // after the call to -willDestroyIncognitoProfile.
  Browser* prior_otr_browser = wrangler.incognitoInterface.browser;
  [wrangler willDestroyIncognitoProfile];
  RecreateOffTheRecordProfile();
  [wrangler incognitoProfileCreated];

  // Expect that the prior OTR browser was removed, and a new one was added.
  EXPECT_EQ(prior_otr_browser,
            browser_list_observer().GetLastRemovedIncognitoBrowser());
  EXPECT_EQ(wrangler.incognitoInterface.browser,
            browser_list_observer().GetLastAddedIncognitoBrowser());
  // There still should be one OTR browser.
  EXPECT_EQ(1UL,
            browser_list->BrowsersOfType(BrowserList::BrowserType::kIncognito)
                .size());

  // Store unsafe pointers to the current browsers.
  Browser* pre_shutdown_main_browser = wrangler.mainInterface.browser;
  Browser* pre_shutdown_incognito_browser = wrangler.incognitoInterface.browser;

  // After shutdown all browsers are destroyed.
  [wrangler shutdown];

  // There should be no browsers in the BrowserList.
  EXPECT_EQ(0UL,
            browser_list
                ->BrowsersOfType(BrowserList::BrowserType::kRegularAndInactive)
                .size());
  EXPECT_EQ(0UL,
            browser_list->BrowsersOfType(BrowserList::BrowserType::kIncognito)
                .size());
  // Both browser removals should have been observed.
  EXPECT_EQ(pre_shutdown_main_browser,
            browser_list_observer().GetLastRemovedBrowser());
  EXPECT_EQ(pre_shutdown_incognito_browser,
            browser_list_observer().GetLastRemovedIncognitoBrowser());
}

TEST_F(BrowserLifecycleManagerTest, TestInactiveInterface) {
  id mock_scene_handler = OCMProtocolMock(@protocol(SceneCommands));
  id mock_settings_handler = OCMProtocolMock(@protocol(SettingsCommands));
  id mock_gemini_handler = OCMProtocolMock(@protocol(GeminiCommands));
  IncognitoReauthSceneAgent* reauth_agent = [[IncognitoReauthSceneAgent alloc]
      initWithReauthModule:[[ReauthenticationModule alloc] init]];
  [scene_state() addAgent:reauth_agent];
  BrowserLifecycleManager* wrangler =
      [[BrowserLifecycleManager alloc] initWithProfile:profile()
                                            sceneState:scene_state()
                                         sceneEndpoint:mock_scene_handler
                                      settingsEndpoint:mock_settings_handler
                                        geminiEndpoint:mock_gemini_handler];

  BrowserList* browser_list = BrowserListFactory::GetForProfile(profile());

  [wrangler createMainCoordinatorAndInterface];
  EXPECT_EQ(2UL,
            browser_list
                ->BrowsersOfType(BrowserList::BrowserType::kRegularAndInactive)
                .size());
  EXPECT_EQ(wrangler.mainInterface.browser->GetInactiveBrowser(),
            browser_list_observer().GetLastAddedBrowser());

  // After shutdown all browsers are destroyed.
  [wrangler shutdown];
  EXPECT_EQ(0UL,
            browser_list
                ->BrowsersOfType(BrowserList::BrowserType::kRegularAndInactive)
                .size());
}

// Tests the session restoration logic.
TEST_F(BrowserLifecycleManagerTest, TestSessionRestorationLogic) {
  id mock_scene_handler = OCMProtocolMock(@protocol(SceneCommands));
  id mock_settings_handler = OCMProtocolMock(@protocol(SettingsCommands));
  id mock_gemini_handler = OCMProtocolMock(@protocol(GeminiCommands));
  IncognitoReauthSceneAgent* reauth_agent = [[IncognitoReauthSceneAgent alloc]
      initWithReauthModule:[[ReauthenticationModule alloc] init]];
  [scene_state() addAgent:reauth_agent];
  BrowserLifecycleManager* wrangler =
      [[BrowserLifecycleManager alloc] initWithProfile:profile()
                                            sceneState:scene_state()
                                         sceneEndpoint:mock_scene_handler
                                      settingsEndpoint:mock_settings_handler
                                        geminiEndpoint:mock_gemini_handler];

  // Create the coordinator and interface. This is required to get access
  // to the Browser via the -mainInterface/-incognitoInterface providers.
  [wrangler createMainCoordinatorAndInterface];
  EXPECT_EQ(0, session_restoration_observer().session_restoration_call_count());

  // Load the session for all Browser. There should be one for the main
  // Browser, one for the inactive Browser and one for the OTR Browser.
  [wrangler loadSession];
  EXPECT_EQ(3, session_restoration_observer().session_restoration_call_count());

  // Destroing and rebuilding the incognito browser should not restore the
  // sessions.
  [wrangler willDestroyIncognitoProfile];
  RecreateOffTheRecordProfile();
  [wrangler incognitoProfileCreated];

  EXPECT_EQ(3, session_restoration_observer().session_restoration_call_count());
  [wrangler shutdown];
}

// Tests that calling -prepareForShutdown before unregistering a target from
// CommandDispatcher allows late invocations during teardown to fail silently
// instead of throwing unrecognized selector exceptions.
TEST_F(BrowserLifecycleManagerTest, TestPrepareForShutdown) {
  id mock_scene_handler = OCMProtocolMock(@protocol(SceneCommands));
  id mock_settings_handler = OCMProtocolMock(@protocol(SettingsCommands));
  id mock_gemini_handler = OCMProtocolMock(@protocol(GeminiCommands));
  IncognitoReauthSceneAgent* reauth_agent = [[IncognitoReauthSceneAgent alloc]
      initWithReauthModule:[[ReauthenticationModule alloc] init]];
  [scene_state() addAgent:reauth_agent];
  BrowserLifecycleManager* wrangler =
      [[BrowserLifecycleManager alloc] initWithProfile:profile()
                                            sceneState:scene_state()
                                         sceneEndpoint:mock_scene_handler
                                      settingsEndpoint:mock_settings_handler
                                        geminiEndpoint:mock_gemini_handler];

  [wrangler createMainCoordinatorAndInterface];
  CommandDispatcher* dispatcher =
      wrangler.mainInterface.browser->GetCommandDispatcher();

  // Verify GeminiCommands handler is registered.
  id<GeminiCommands> gemini_proxy =
      HandlerForProtocol(dispatcher, GeminiCommands);
  EXPECT_TRUE([gemini_proxy
      respondsToSelector:@selector(hideFloatyIfInvokedAnimated:fromSource:)]);

  // Call prepareForShutdown before stopping/unregistering the target.
  [wrangler prepareForShutdown];

  // Unregister the target (simulating SceneCoordinator::stop).
  [dispatcher stopDispatchingToTarget:mock_gemini_handler];

  // Because prepareForShutdown was called prior to unregistering, invoking a
  // GeminiCommands method on the proxy during teardown should fail silently and
  // not throw a doesNotRecognizeSelector exception.
  [gemini_proxy hideFloatyIfInvokedAnimated:NO
                                 fromSource:gemini::FloatyUpdateSource::Alert];

  [wrangler shutdown];
}
