// 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.

#import "base/strings/sys_string_conversions.h"
#import "base/test/ios/wait_util.h"
#import "components/commerce/core/commerce_feature_list.h"
#import "components/segmentation_platform/public/features.h"
#import "components/sync/base/features.h"
#import "components/url_formatter/elide_url.h"
#import "components/visited_url_ranking/public/features.h"
#import "ios/chrome/browser/authentication/test/signin_earl_grey.h"
#import "ios/chrome/browser/authentication/test/signin_earl_grey_ui_test_util.h"
#import "ios/chrome/browser/content_suggestions/magic_stack/public/magic_stack_constants.h"
#import "ios/chrome/browser/content_suggestions/public/content_suggestions_constants.h"
#import "ios/chrome/browser/content_suggestions/tab_resumption/public/tab_resumption_constants.h"
#import "ios/chrome/browser/content_suggestions/test/new_tab_page_app_interface.h"
#import "ios/chrome/browser/ntp_tiles/model/tab_resumption/tab_resumption_prefs.h"
#import "ios/chrome/browser/recent_tabs/public/recent_tabs_constants.h"
#import "ios/chrome/browser/shared/public/features/features.h"
#import "ios/chrome/browser/signin/model/fake_system_identity.h"
#import "ios/chrome/browser/start_surface/ui_bundled/home_surface_egtest_utils.h"
#import "ios/chrome/browser/start_surface/ui_bundled/start_surface_features.h"
#import "ios/chrome/browser/tabs/ui_bundled/tests/distant_tabs_app_interface.h"
#import "ios/chrome/browser/tabs/ui_bundled/tests/fake_distant_tab.h"
#import "ios/chrome/grit/ios_strings.h"
#import "ios/chrome/test/earl_grey/chrome_earl_grey.h"
#import "ios/chrome/test/earl_grey/chrome_matchers.h"
#import "ios/chrome/test/earl_grey/chrome_test_case.h"
#import "ios/testing/earl_grey/app_launch_manager.h"
#import "ios/testing/earl_grey/earl_grey_test.h"
#import "net/test/embedded_test_server/embedded_test_server.h"
#import "net/test/embedded_test_server/request_handler_util.h"
#import "ui/base/l10n/l10n_util.h"

namespace {

// Timeout in seconds to wait for asynchronous sync operations.
constexpr base::TimeDelta kSyncOperationTimeout = base::Seconds(10);

// Sign in and enable history/tab sync using a fake identity.
void SignInAndEnableHistorySync() {
  FakeSystemIdentity* fake_identity = [FakeSystemIdentity fakeIdentity1];
  [SigninEarlGrey addFakeIdentity:fake_identity];
  [SigninEarlGreyUI signinWithFakeIdentity:fake_identity enableHistorySync:YES];
  [ChromeEarlGrey
      waitForSyncTransportStateActiveWithTimeout:kSyncOperationTimeout];
}

// Checks that the visibility of the tab resumption tile matches `should_show`.
void WaitUntilTabResumptionTileVisibleOrTimeout(bool should_show) {
  id<GREYMatcher> matcher =
      should_show ? grey_sufficientlyVisible() : grey_notVisible();
  ConditionBlock condition = ^{
    NSError* error = nil;
    [[EarlGrey
        selectElementWithMatcher:
            grey_allOf(
                grey_accessibilityID(
                    kMagicStackContentSuggestionsModuleTabResumptionAccessibilityIdentifier),
                grey_sufficientlyVisible(), nil)] assertWithMatcher:matcher
                                                              error:&error];
    return error == nil;
  };

  NSString* failure_reason = @"Tile visible.";
  if (should_show) {
    failure_reason = @"Tile did not appear.";
  }
  GREYAssert(
      base::test::ios::WaitUntilConditionOrTimeout(base::Seconds(6), condition),
      failure_reason);
}

// Returns a GREYMatcher for the given label.
id<GREYMatcher> TabResumptionLabelMatcher(NSString* label) {
  return grey_allOf(
      grey_ancestor(grey_accessibilityID(kTabResumptionViewIdentifier)),
      grey_text(label), nil);
}

// Returns the hostname from the given `URL`.
NSString* HostnameFromGURL(GURL URL) {
  return base::SysUTF16ToNSString(
      url_formatter::
          FormatUrlForDisplayOmitSchemePathTrivialSubdomainsAndMobilePrefix(
              URL));
}

}  // namespace

// Test case for the tab reusmption tile.
@interface TabResumptionTestCase : ChromeTestCase

@end

@implementation TabResumptionTestCase

- (AppLaunchConfiguration)appConfigurationForTestCase {
  AppLaunchConfiguration config;
  config.additional_args.push_back(std::string("--") +
                                   kTabResumptionShowItemImmediately);
  config.additional_args.push_back("--test-ios-module-ranker=tab_resumption");
  config.additional_args.push_back("--mock-shopping-service=is-eligible,"
                                   "has-empty-price-tracked-bookmarks-results,"
                                   "has-empty-subscriptions-results");
  // kVisitedURLRankingHistoryVisibilityScoreFilter require the network, keep
  // it disabled for tests.
  config.features_disabled.push_back(
      visited_url_ranking::features::
          kVisitedURLRankingHistoryVisibilityScoreFilter);
  return config;
}

// Relaunches the app with start surface enabled.
- (void)relaunchAppWithStartSurfaceEnabled {
  AppLaunchConfiguration config = [self appConfigurationForTestCase];
  config.relaunch_policy = ForceRelaunchByCleanShutdown;
  // Relaunching the app undoes the mock setup for shopping_service in setUp
  // For the relaunch cases, explicitly disabling ShopCard so the tests can
  // continue without waiting for the async callback.
  config.features_disabled.push_back(commerce::kTabResumptionShopCard);
  [[AppLaunchManager sharedManager] ensureAppLaunchedWithConfiguration:config];
}

- (void)setUp {
  [super setUp];
  MakeHomeSurfaceOpenImmediately();
  if (![ChromeTestCase forceRestartAndWipe]) {
    [ChromeEarlGrey clearBrowsingHistory];
  }
  GREYAssertTrue(self.testServer->Start(), @"Test server failed to start.");
  SignInAndEnableHistorySync();
  [NewTabPageAppInterface disableTipsCards];
  [[self class] closeAllTabs];
  [ChromeEarlGrey openNewTab];
}

- (void)tearDownHelper {
  [SigninEarlGrey signOut];
  ResetMakeHomeSurfaceOpenImmediately();
  [ChromeEarlGrey clearFakeSyncServerData];
  [ChromeEarlGrey clearUserPrefWithName:tab_resumption_prefs::
                                            kTabResumptionLastOpenedTabURLPref];
  [super tearDownHelper];
}

// Tests that the tab resumption tile is correctly displayed for a distant tab.
- (void)testTabResumptionTileDisplayedForDistantTab {
  // Check that the tile is not displayed when there is no distant tab.
  WaitUntilTabResumptionTileVisibleOrTimeout(false);

  // Create a distant session with 4 tabs.
  [DistantTabsAppInterface
      addSessionToFakeSyncServer:@"Desktop"
               modifiedTimeDelta:base::Minutes(5)
                            tabs:[FakeDistantTab
                                     createFakeTabsForServerURL:self.testServer
                                                                    ->base_url()
                                                   numberOfTabs:4]];
  [ChromeEarlGrey triggerSyncCycleForType:syncer::SESSIONS];

  // Check that the tile is displayed when there is a distant tab.
  WaitUntilTabResumptionTileVisibleOrTimeout(true);
  // Tab resumption 2 displays Tab0 in that case.
  NSString* displayedTab = @"Tab 3";
  [[EarlGrey selectElementWithMatcher:TabResumptionLabelMatcher(displayedTab)]
      assertWithMatcher:grey_sufficientlyVisible()];
  NSString* footerLabel =
      [NSString stringWithFormat:@"%@ • %@",
                                 HostnameFromGURL(self.testServer->base_url()),
                                 @"Desktop"];
  [[EarlGrey selectElementWithMatcher:TabResumptionLabelMatcher(footerLabel)]
      assertWithMatcher:grey_sufficientlyVisible()];

  // Tap on the tab resumption item.
  [[EarlGrey selectElementWithMatcher:grey_accessibilityID(
                                          kTabResumptionViewIdentifier)]
      performAction:grey_tap()];

  // Verify that the location bar shows the distant tab URL in a short form.
  [[EarlGrey selectElementWithMatcher:chrome_test_util::DefocusedLocationView()]
      assertWithMatcher:chrome_test_util::LocationViewContainingText(
                            self.testServer->base_url().GetHost())];
}

// Tests that the tab resumption tile is correctly displayed for a local tab.
// TODO(crbug.com/443707327): Test disabled.
- (void)FLAKY_testTabResumptionTileDisplayedForLocalTab {
  // Check that the tile is not displayed when there is no local tab.
  WaitUntilTabResumptionTileVisibleOrTimeout(false);

  const GURL destinationUrl = self.testServer->GetURL("/pony.html");
  [ChromeEarlGrey loadURL:destinationUrl];
  [ChromeEarlGrey openNewTab];
  [ChromeEarlGrey loadURL:destinationUrl];

  // Relaunch the app.
  [self relaunchAppWithStartSurfaceEnabled];

  // Check that the tile is displayed when there is a local tab.
  WaitUntilTabResumptionTileVisibleOrTimeout(true);
  [[EarlGrey selectElementWithMatcher:TabResumptionLabelMatcher(@"ponies")]
      assertWithMatcher:grey_sufficientlyVisible()];
  NSString* footerLabel =
      [NSString stringWithFormat:@"%@ • %@",
                                 HostnameFromGURL(self.testServer->base_url()),
                                 @"just now"];
  [[EarlGrey selectElementWithMatcher:TabResumptionLabelMatcher(footerLabel)]
      assertWithMatcher:grey_sufficientlyVisible()];

  // Tap on the tab resumption item.
  [[EarlGrey selectElementWithMatcher:grey_accessibilityID(
                                          kTabResumptionViewIdentifier)]
      performAction:grey_tap()];

  // Verify that the location bar shows the local tab URL in a short form.
  [[EarlGrey selectElementWithMatcher:chrome_test_util::DefocusedLocationView()]
      assertWithMatcher:chrome_test_util::LocationViewContainingText(
                            destinationUrl.GetHost())];
  [ChromeEarlGrey
      waitForWebStateContainingText:"Anyone know any good pony jokes?"];
  // The most recent tab is the second one.
  GREYAssertEqual(1u, [ChromeEarlGrey indexOfActiveNormalTab],
                  @"Incorrect active tab.");
}

// Tests that interacting with the Magic Stack edit button works when the tab
// resumption tile is displayed.
- (void)testInteractWithAnotherTile {
  // Check that the tile is not displayed when there is no distant tab.
  WaitUntilTabResumptionTileVisibleOrTimeout(false);

  // Create a distant session with 4 tabs.
  [DistantTabsAppInterface
      addSessionToFakeSyncServer:@"Desktop"
               modifiedTimeDelta:base::Minutes(5)
                            tabs:[FakeDistantTab
                                     createFakeTabsForServerURL:self.testServer
                                                                    ->base_url()
                                                   numberOfTabs:4]];
  [ChromeEarlGrey triggerSyncCycleForType:syncer::SESSIONS];

  // Check that the tile is displayed when there is a distant tab.
  WaitUntilTabResumptionTileVisibleOrTimeout(true);

  [[[EarlGrey selectElementWithMatcher:
                  grey_allOf(grey_accessibilityID(
                                 kMagicStackEditButtonAccessibilityIdentifier),
                             grey_sufficientlyVisible(), nil)]
         usingSearchAction:grey_swipeFastInDirection(kGREYDirectionLeft)
      onElementWithMatcher:grey_accessibilityID(
                               kMagicStackScrollViewAccessibilityIdentifier)]
      assertWithMatcher:grey_sufficientlyVisible()];
}

// Tests that the context menu has the correct action and correctly hides the
// tile.
- (void)testTabResumptionTileLongPress {
  // Check that the tile is not displayed when there is no distant tab.
  WaitUntilTabResumptionTileVisibleOrTimeout(false);

  // Create a distant session with 4 tabs.
  [DistantTabsAppInterface
      addSessionToFakeSyncServer:@"Desktop"
               modifiedTimeDelta:base::Minutes(5)
                            tabs:[FakeDistantTab
                                     createFakeTabsForServerURL:self.testServer
                                                                    ->base_url()
                                                   numberOfTabs:4]];
  [ChromeEarlGrey triggerSyncCycleForType:syncer::SESSIONS];

  // Check that the tile is displayed when there is a distant tab.
  WaitUntilTabResumptionTileVisibleOrTimeout(true);

  // Long press the distant tab.
  [[EarlGrey selectElementWithMatcher:grey_accessibilityID(
                                          kTabResumptionViewIdentifier)]
      performAction:grey_longPress()];
  [[EarlGrey selectElementWithMatcher:
                 grey_text(l10n_util::GetNSString(
                     IDS_IOS_TAB_RESUMPTION_CONTEXT_MENU_DESCRIPTION))]
      assertWithMatcher:grey_notNil()];
  [[EarlGrey selectElementWithMatcher:
                 grey_text(l10n_util::GetNSString(
                     IDS_IOS_MAGIC_STACK_CONTEXT_MENU_CUSTOMIZE_CARDS_TITLE))]
      assertWithMatcher:grey_notNil()];
  [[EarlGrey selectElementWithMatcher:
                 grey_text(l10n_util::GetNSString(
                     IDS_IOS_TAB_RESUMPTION_CONTEXT_MENU_DESCRIPTION))]
      performAction:grey_tap()];

  // Check that the tile is hidden.
  WaitUntilTabResumptionTileVisibleOrTimeout(false);
}

// TODO(crbug.com/443707327): Test disabled on simulator.
#if TARGET_OS_SIMULATOR
#define MAYBE_testShowMoreVisible DISABLED_testShowMoreVisible
#else
#define MAYBE_testShowMoreVisible testShowMoreVisible
#endif
- (void)MAYBE_testShowMoreVisible {
  // Check that the tile is not displayed when there is no local tab.
  WaitUntilTabResumptionTileVisibleOrTimeout(false);

  const GURL destinationUrl = self.testServer->GetURL("/pony.html");
  [ChromeEarlGrey loadURL:destinationUrl];

  // Relaunch the app.
  [self relaunchAppWithStartSurfaceEnabled];

  // Check that the tile is displayed when there is a local tab.
  WaitUntilTabResumptionTileVisibleOrTimeout(true);
  [[EarlGrey
      selectElementWithMatcher:grey_allOf(grey_accessibilityID(@"See more"),
                                          grey_sufficientlyVisible(), nil)]
      performAction:grey_tap()];
  [[EarlGrey
      selectElementWithMatcher:
          grey_allOf(grey_accessibilityID(
                         kRecentTabsTableViewControllerAccessibilityIdentifier),
                     grey_sufficientlyVisible(), nil)]
      assertWithMatcher:grey_sufficientlyVisible()];
}

@end
