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

#import <objc/runtime.h>

#import "base/base_paths.h"
#import "base/functional/bind.h"
#import "base/path_service.h"
#import "base/strings/sys_string_conversions.h"
#import "base/test/ios/wait_util.h"
#import "ios/chrome/browser/shared/public/features/features.h"
#import "ios/chrome/browser/start_surface/ui_bundled/start_surface_features.h"
#import "ios/chrome/test/earl_grey/chrome_earl_grey.h"
#import "ios/chrome/test/earl_grey/chrome_earl_grey_ui.h"
#import "ios/chrome/test/earl_grey/chrome_matchers.h"
#import "ios/chrome/test/earl_grey/chrome_test_case.h"
#import "ios/net/url_test_util.h"
#import "ios/testing/earl_grey/app_launch_manager.h"
#import "ios/testing/earl_grey/earl_grey_test.h"
#import "ios/web/common/features.h"
#import "net/test/embedded_test_server/default_handlers.h"
#import "net/test/embedded_test_server/http_request.h"
#import "net/test/embedded_test_server/http_response.h"

using chrome_test_util::BackButton;
using chrome_test_util::ForwardButton;
using chrome_test_util::NTPCollectionView;
using chrome_test_util::ShowTabsButton;
using chrome_test_util::ToolsMenuButton;

namespace {

// Path to two test pages, page1 and page2 with associated contents and titles.
const char kPageOnePath[] = "/page1.html";
const char kPageOneContent[] = "This is the first page.";
const char kPageOneTitle[] = "The first page title.";
const char kPageTwoPath[] = "/page2.html";
const char kPageTwoContent[] = "This is the second page.";
const char kPageTwoTitle[] = "The second page title.";

// Path to a test page used to count each page load.
const char kCountURL[] = "/countme.html";

// Response handler for page1 and page2 that supports 'airplane mode' by
// returning an empty RawHttpResponse when `responds_with_content` us false.
std::unique_ptr<net::test_server::HttpResponse> RestoreResponse(
    const bool& responds_with_content,
    const net::test_server::HttpRequest& request) {
  if (!responds_with_content) {
    return std::make_unique<net::test_server::RawHttpResponse>(
        /*headers=*/"", /*contents=*/"");
  }
  std::unique_ptr<net::test_server::BasicHttpResponse> http_response =
      std::make_unique<net::test_server::BasicHttpResponse>();
  http_response->set_code(net::HTTP_OK);
  std::string title;
  std::string body;
  if (request.relative_url == kPageOnePath) {
    title = kPageOneTitle;
    body = kPageOneContent;
  } else if (request.relative_url == kPageTwoPath) {
    title = kPageTwoTitle;
    body = kPageTwoContent;
  } else {
    return nullptr;
  }
  http_response->set_content("<html><head><title>" + title +
                             "</title></head>"
                             "<body>" +
                             body + "</body></html>");
  return std::move(http_response);
}

// Response handler for `kCountURL` that counts the number of page loads.
std::unique_ptr<net::test_server::HttpResponse> CountResponse(
    int* counter,
    const net::test_server::HttpRequest& request) {
  if (request.relative_url != kCountURL) {
    return nullptr;
  }
  std::unique_ptr<net::test_server::BasicHttpResponse> http_response =
      std::make_unique<net::test_server::BasicHttpResponse>();
  http_response->set_code(net::HTTP_OK);
  http_response->set_content("<html><head><title>Hello World</title></head>"
                             "<body>Hello World!</body></html>");
  (*counter)++;
  return std::move(http_response);
}

}  // namespace

// Integration tests for restoring session history.
@interface RestoreWithCacheTestCase : ChromeTestCase {
  // Use a second test server to ensure different origin navigations.
  std::unique_ptr<net::EmbeddedTestServer> _secondTestServer;
}

// The secondary EmbeddedTestServer instance.
@property(nonatomic, readonly)
    net::test_server::EmbeddedTestServer* secondTestServer;

@property(atomic) bool serverRespondsWithContent;

// Start the primary and secondary test server.  Separate servers are used to
// force cross domain tests (via different ports).
- (void)setUpRestoreServers;

// Trigger a session history restore.  In EG1 this is possible via the TabGrid
// CloseAll-Undo-Done method. In EG2, this is possible via
// Background-Terminate-Activate
- (void)triggerRestore;

// Navigate to a set of sites include cross-domains, chrome URLs, error pages
// and the NTP.
- (void)loadTestPages;

// Verify that each page visited in -loadTestPages is properly restored by
// navigating to each page and triggering a restore, confirming that pages are
// reloaded and back-forward history is preserved.  If `checkServerData` is YES,
// also check that the proper content is restored.
- (void)verifyRestoredTestPages:(BOOL)checkServerData;

@end

@implementation RestoreWithCacheTestCase

- (AppLaunchConfiguration)appConfigurationForTestCase {
  AppLaunchConfiguration config = [super appConfigurationForTestCase];
  return config;
}

- (net::EmbeddedTestServer*)secondTestServer {
  if (!_secondTestServer) {
    _secondTestServer = std::make_unique<net::EmbeddedTestServer>();
    _secondTestServer->ServeFilesFromDirectory(
        base::PathService::CheckedGet(base::DIR_ASSETS)
            .AppendASCII("ios/testing/data/http_server_files/"));
    net::test_server::RegisterDefaultHandlers(_secondTestServer.get());
  }
  return _secondTestServer.get();
}

// Navigates to a set of cross-domains, chrome URLs and error pages, and then
// tests that they are properly restored.
// TODO(crbug.com/435144099): Reenable test.
#if TARGET_OS_SIMULATOR
#define MAYBE_testRestoreHistory testRestoreHistory
#else
#define MAYBE_testRestoreHistory FLAKY_testRestoreHistory
#endif
- (void)MAYBE_testRestoreHistory {
  [self setUpRestoreServers];
  [self loadTestPages];
  [self verifyRestoredTestPages:YES];
}

// Navigates to a set of cross-domains, chrome URLs and error pages, and then
// tests that they are properly restored in airplane mode.
- (void)testRestoreNoNetwork {
  [self setUpRestoreServers];
  [self loadTestPages];
  self.serverRespondsWithContent = false;
  [self verifyRestoredTestPages:NO];
}

// Tests that only the selected web state is loaded on a session restore.
- (void)testRestoreOneWebstateOnly {
  // Visit the background page.
  int visitCounter = 0;
  self.testServer->RegisterRequestHandler(
      base::BindRepeating(&CountResponse, &visitCounter));
  GREYAssertTrue(self.testServer->Start(), @"Test server failed to start.");
  const GURL countPage = self.testServer->GetURL(kCountURL);
  [ChromeEarlGrey loadURL:countPage];
  GREYAssertEqual(1, visitCounter, @"The page should have been loaded once");

  // Visit the forground page.
  [ChromeEarlGrey openNewTab];
  const GURL echoPage = self.testServer->GetURL("/echo");
  [ChromeEarlGrey loadURL:echoPage];

  // Trigger a restore and confirm the background page is not reloaded.
  [self triggerRestore];
  [ChromeEarlGrey waitForWebStateVisibleURL:echoPage];
  [ChromeEarlGrey waitForWebStateContainingText:"Echo"];
  GREYAssertEqual(1, visitCounter, @"The page should not reload");
}

// Tests that only the selected web state is loaded Restore-after-Crash.  This
// is only possible in EG2.
- (void)testRestoreOneWebstateOnlyAfterCrash {
  // Visit the background page.
  int visitCounter = 0;
  self.testServer->RegisterRequestHandler(
      base::BindRepeating(&CountResponse, &visitCounter));
  GREYAssertTrue(self.testServer->Start(), @"Test server failed to start.");
  const GURL countPage = self.testServer->GetURL(kCountURL);
  [ChromeEarlGrey loadURL:countPage];
  GREYAssertEqual(1, visitCounter, @"The page should have been loaded once");

  // Visit the foreground page.
  [ChromeEarlGrey openNewTab];
  const GURL echoPage = self.testServer->GetURL("/echo");
  [ChromeEarlGrey loadURL:echoPage];
  [ChromeEarlGrey waitForWebStateContainingText:"Echo"];

  // Clear cache, save the session and trigger a crash/activate.
  [ChromeEarlGrey removeBrowsingCache];
  [ChromeEarlGrey saveSessionImmediately];
  [[AppLaunchManager sharedManager] ensureAppLaunchedWithFeaturesEnabled:{}
      disabled:{}
      relaunchPolicy:ForceRelaunchByKilling];
  // Restore after crash and confirm the background page is not reloaded.
  [ChromeEarlGrey waitForWebStateVisibleURL:echoPage];
  [ChromeEarlGrey waitForWebStateContainingText:"Echo"];
  GREYAssertEqual(1, visitCounter, @"The page should not reload");
}

#pragma mark Utility methods

- (void)setUpRestoreServers {
  self.testServer->RegisterRequestHandler(base::BindRepeating(
      &RestoreResponse, std::cref(_serverRespondsWithContent)));
  self.secondTestServer->RegisterRequestHandler(base::BindRepeating(
      &RestoreResponse, std::cref(_serverRespondsWithContent)));
  self.serverRespondsWithContent = true;
  GREYAssertTrue(self.testServer->Start(), @"Test server failed to start.");
  GREYAssertTrue(self.secondTestServer->Start(),
                 @"Second test server failed to start.");
}

- (void)triggerRestore {
  [[AppLaunchManager sharedManager]
      ensureAppLaunchedWithFeaturesEnabled:{}
                                  disabled:{}
                            relaunchPolicy:ForceRelaunchByCleanShutdown];
}

- (void)loadTestPages {
  // Load page1.
  const GURL pageOne = self.testServer->GetURL(kPageOnePath);
  [ChromeEarlGrey loadURL:pageOne];
  [ChromeEarlGrey waitForWebStateContainingText:kPageOneContent];

  // Load chrome url
  const GURL chromePage = GURL("chrome://chrome-urls");
  [ChromeEarlGrey loadURL:chromePage];

  // Load error page.
  const GURL errorPage = GURL("http://invalid.");
  [ChromeEarlGrey loadURL:errorPage];
  [ChromeEarlGrey waitForWebStateContainingText:"ERR_"];
  [ChromeEarlGreyUI waitForAppToIdle];

  // Load page2.
  const GURL pageTwo = self.secondTestServer->GetURL(kPageTwoPath);
  [ChromeEarlGrey loadURL:pageTwo];
  [ChromeEarlGrey waitForWebStateContainingText:kPageTwoContent];
}

- (void)verifyRestoredTestPages:(BOOL)checkServerData {
  const GURL pageOne = self.testServer->GetURL(kPageOnePath);
  const GURL pageTwo = self.secondTestServer->GetURL(kPageTwoPath);

  // Restore page2
  [self triggerRestore];
  [ChromeEarlGrey waitForWebStateVisibleURL:pageTwo];
  if (checkServerData) {
    [ChromeEarlGrey waitForWebStateContainingText:kPageTwoContent];
  }

  // Confirm page1 is still in the history.
  [[EarlGrey selectElementWithMatcher:BackButton()]
      performAction:grey_longPress()];
  [[EarlGrey
      selectElementWithMatcher:grey_allOf(grey_text(base::SysUTF8ToNSString(
                                              kPageOneTitle)),
                                          grey_sufficientlyVisible(), nil)]
      assertWithMatcher:grey_notNil()];

  [[EarlGrey selectElementWithMatcher:ShowTabsButton()]
      performAction:grey_tap()];

  // Go back to error page.
  [[EarlGrey selectElementWithMatcher:BackButton()] performAction:grey_tap()];
  [ChromeEarlGrey waitForWebStateVisibleURL:GURL("http://invalid.")];
  [ChromeEarlGrey waitForWebStateContainingText:"ERR_"];
  [ChromeEarlGreyUI waitForAppToIdle];
  [self triggerRestore];
  [ChromeEarlGrey waitForWebStateVisibleURL:GURL("http://invalid.")];
  [ChromeEarlGrey waitForWebStateContainingText:"ERR_"];
  [ChromeEarlGreyUI waitForAppToIdle];

  // Go back to chrome url.
  [[EarlGrey selectElementWithMatcher:BackButton()] performAction:grey_tap()];
  [ChromeEarlGrey waitForWebStateVisibleURL:GURL("chrome://chrome-urls/")];
  [ChromeEarlGrey waitForWebStateContainingText:"List of Chrome"];
  [self triggerRestore];
  [ChromeEarlGrey waitForWebStateVisibleURL:GURL("chrome://chrome-urls/")];
  [ChromeEarlGrey waitForWebStateContainingText:"List of Chrome"];

  // Go back to page1 and confirm page2 is still in the forward history.
  [[EarlGrey selectElementWithMatcher:BackButton()] performAction:grey_tap()];
  [ChromeEarlGrey waitForWebStateVisibleURL:pageOne];
  if (checkServerData) {
    [ChromeEarlGrey waitForWebStateContainingText:kPageOneContent];
    [[EarlGrey selectElementWithMatcher:ForwardButton()]
        performAction:grey_longPress()];
    [[EarlGrey
        selectElementWithMatcher:grey_allOf(grey_text(base::SysUTF8ToNSString(
                                                kPageTwoTitle)),
                                            grey_sufficientlyVisible(), nil)]
        assertWithMatcher:grey_notNil()];

    [[EarlGrey selectElementWithMatcher:ToolsMenuButton()]
        performAction:grey_tap()];
  }
  [self triggerRestore];
  [ChromeEarlGrey waitForWebStateVisibleURL:pageOne];
  if (checkServerData) {
    [ChromeEarlGrey waitForWebStateContainingText:kPageOneContent];
    [[EarlGrey selectElementWithMatcher:ForwardButton()]
        performAction:grey_longPress()];
    [[EarlGrey
        selectElementWithMatcher:grey_allOf(grey_text(base::SysUTF8ToNSString(
                                                kPageTwoTitle)),
                                            grey_sufficientlyVisible(), nil)]
        assertWithMatcher:grey_notNil()];

    [[EarlGrey selectElementWithMatcher:ToolsMenuButton()]
        performAction:grey_tap()];
  }
  // TODO(crbug.com/530841942): On iOS 27 beta 2 and 3 frequently failed without
  // loadURL waits.  Beta 4 seems to fixed most, but introduced this one.  This
  // is a temporary fix until we find a better solution. It's possible this is a
  // iOS 27 beta bug, or that we need a different wait, but so far this wait is
  // the best approach we've found.
  if (@available(iOS 27, *)) {
    base::test::ios::SpinRunLoopWithMinDelay(base::Seconds(1));
  }
  [[EarlGrey selectElementWithMatcher:BackButton()] performAction:grey_tap()];
  [ChromeEarlGrey waitForPageToFinishLoading];

  // Confirm the NTP is still at the start.
  [[EarlGrey selectElementWithMatcher:NTPCollectionView()]
      assertWithMatcher:grey_notNil()];
  [self triggerRestore];
  [[EarlGrey selectElementWithMatcher:NTPCollectionView()]
      assertWithMatcher:grey_notNil()];
}

@end

// Test using synthesize restore.
@interface RestoreWithSynthesizedTestCase : RestoreWithCacheTestCase
@end

@implementation RestoreWithSynthesizedTestCase

- (AppLaunchConfiguration)appConfigurationForTestCase {
  AppLaunchConfiguration config = [super appConfigurationForTestCase];
  config.features_enabled.push_back(
      web::features::kForceSynthesizedRestoreSession);
  return config;
}

// This is currently needed to prevent this test case from being ignored.
- (void)testEmpty {
}

@end
