// 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 <UIKit/UIKit.h>
#import <XCTest/XCTest.h>

#import "base/functional/bind.h"
#import "base/memory/ref_counted.h"
#import "base/strings/sys_string_conversions.h"
#import "ios/chrome/browser/settings/ui_bundled/content_settings/block_popups_app_interface.h"
#import "ios/chrome/browser/shared/public/features/features.h"
#import "ios/chrome/grit/ios_strings.h"
#import "ios/chrome/test/earl_grey/chrome_actions.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/chrome/test/earl_grey/scoped_block_popups_pref.h"
#import "ios/chrome/test/earl_grey/scoped_disable_timer_tracking.h"
#import "ios/chrome/test/scoped_eg_synchronization_disabler.h"
#import "ios/testing/earl_grey/earl_grey_test.h"
#import "ios/testing/earl_grey/matchers.h"
#import "net/test/embedded_test_server/embedded_test_server.h"
#import "net/test/embedded_test_server/http_request.h"
#import "net/test/embedded_test_server/http_response.h"
#import "url/gurl.h"

using chrome_test_util::ContentSettingsButton;
using chrome_test_util::NavigationBarEditButton;
using chrome_test_util::SettingsDoneButton;
using testing::NavigationBarBackButton;

namespace {

// URLs used in the tests.
const char* kBlockPopupsUrl = "/blockpopups";
const char* kOpenedWindowUrl = "/openedwindow";

// Page with a button that opens a new window after a short delay.
NSString* const kBlockPopupsResponseTemplate =
    @"<input type=\"button\" onclick=\"setTimeout(function() {"
     "window.open('%@')}, 1)\" "
     "id=\"open-window\" "
     "value=\"openWindow\">";
// JavaScript that clicks that button.
NSString* const kOpenPopupScript =
    @"document.getElementById('open-window').click()";
const std::string kOpenedWindowResponse = "Opened window";

// Returns matcher for the block popups settings menu button.
id<GREYMatcher> BlockPopupsSettingsButton() {
  return chrome_test_util::ButtonWithAccessibilityLabelId(IDS_IOS_BLOCK_POPUPS);
}

// ScopedBlockPopupsException adds an exception to the block popups exception
// list for as long as this object is in scope.
class ScopedBlockPopupsException {
 public:
  ScopedBlockPopupsException(const std::string& pattern)
      : pattern_(base::SysUTF8ToNSString(pattern)) {
    [BlockPopupsAppInterface setPopupPolicy:CONTENT_SETTING_ALLOW
                                 forPattern:pattern_];
  }

  ScopedBlockPopupsException(const ScopedBlockPopupsException&) = delete;
  ScopedBlockPopupsException& operator=(const ScopedBlockPopupsException&) =
      delete;

  ~ScopedBlockPopupsException() {
    [BlockPopupsAppInterface setPopupPolicy:CONTENT_SETTING_DEFAULT
                                 forPattern:pattern_];
  }

 private:
  // The exception pattern that this object is managing.
  NSString* pattern_;
};
struct ServerState {
  std::string blockPopupsResponse;
  std::string openedWindowResponse;
};

std::unique_ptr<net::test_server::HttpResponse> CreateHttpResponse(
    const std::string& content) {
  auto response = std::make_unique<net::test_server::BasicHttpResponse>();
  response->set_code(net::HTTP_OK);
  response->set_content_type("text/html");
  response->set_content(content);
  return response;
}

std::unique_ptr<net::test_server::HttpResponse> NotFoundResponse() {
  auto response = std::make_unique<net::test_server::BasicHttpResponse>();
  response->set_code(net::HTTP_NOT_FOUND);
  return response;
}

std::unique_ptr<net::test_server::HttpResponse> HandleBlockPopupsRequest(
    scoped_refptr<base::RefCountedData<ServerState>> state,
    const net::test_server::HttpRequest& request) {
  if (request.relative_url == kBlockPopupsUrl) {
    return CreateHttpResponse(state->data.blockPopupsResponse);
  }
  if (request.relative_url == kOpenedWindowUrl) {
    return CreateHttpResponse(state->data.openedWindowResponse);
  }
  return NotFoundResponse();
}

}  // namespace

// Block Popups tests for Chrome.
@interface BlockPopupsTestCase : ChromeTestCase
@end

@implementation BlockPopupsTestCase

- (void)setUp {
  [super setUp];

  auto state = base::MakeRefCounted<base::RefCountedData<ServerState>>();
  state->data.openedWindowResponse = kOpenedWindowResponse;

  self.testServer->RegisterRequestHandler(
      base::BindRepeating(&HandleBlockPopupsRequest, state));

  GREYAssertTrue(self.testServer->Start(), @"Test server failed to start.");

  const GURL openedWindowURL = self.testServer->GetURL(kOpenedWindowUrl);
  NSString* openedWindowURLString =
      base::SysUTF8ToNSString(openedWindowURL.spec());
  state->data.blockPopupsResponse = base::SysNSStringToUTF8([NSString
      stringWithFormat:kBlockPopupsResponseTemplate, openedWindowURLString]);
}

- (AppLaunchConfiguration)appConfigurationForTestCase {
  AppLaunchConfiguration config;

  return config;
}

// Opens the block popups settings page and verifies that accessibility is set
// up properly.
- (void)testAccessibilityOfBlockPopupSettings {
  [ChromeEarlGreyUI openSettingsMenu];
  [ChromeEarlGreyUI tapSettingsMenuButton:ContentSettingsButton()];
  [[EarlGrey selectElementWithMatcher:BlockPopupsSettingsButton()]
      performAction:grey_tap()];
  [[EarlGrey
      selectElementWithMatcher:grey_accessibilityID(
                                   @"block_popups_settings_view_controller")]
      assertWithMatcher:grey_notNil()];
  [ChromeEarlGrey verifyAccessibilityForCurrentScreen];

  {
    // Disable EarlGrey synchronization to avoid infinite spinner loop.
    ScopedSynchronizationDisabler disabler;

    // Close the settings menu.
    [[EarlGrey selectElementWithMatcher:NavigationBarBackButton()]
        performAction:grey_tap()];
  }
  [[EarlGrey selectElementWithMatcher:SettingsDoneButton()]
      performAction:grey_tap()];
}

// Tests that popups are opened in new tabs when the preference is set to ALLOW.
- (void)testPopupsAllowed {
  const GURL blockPopupsURL = self.testServer->GetURL(kBlockPopupsUrl);

  ScopedBlockPopupsPref prefSetter(CONTENT_SETTING_ALLOW);
  [ChromeEarlGrey loadURL:blockPopupsURL];
  [ChromeEarlGrey waitForMainTabCount:1];

  // Request popup (execute script without using a user gesture) and make sure
  // the popup opened in a new tab.
  [ChromeEarlGrey evaluateJavaScriptForSideEffect:kOpenPopupScript];
  [ChromeEarlGrey waitForMainTabCount:2];

  // No infobar should be displayed.
  [[EarlGrey selectElementWithMatcher:chrome_test_util::
                                          StaticTextWithAccessibilityLabel(
                                              @"Pop-ups blocked (1)")]
      assertWithMatcher:grey_notVisible()];
}

// Tests that popups are prevented from opening and an infobar is displayed when
// the preference is set to BLOCK.
- (void)testPopupsBlocked {
  const GURL blockPopupsURL = self.testServer->GetURL(kBlockPopupsUrl);

  ScopedBlockPopupsPref prefSetter(CONTENT_SETTING_BLOCK);
  [ChromeEarlGrey loadURL:blockPopupsURL];
  [ChromeEarlGrey waitForMainTabCount:1];

  // Request popup (execute script without using a user gesture), then make sure
  // it was blocked and an infobar was displayed. The window.open() call is run
  // via async JS, so the infobar may not open immediately.
  [ChromeEarlGrey evaluateJavaScriptForSideEffect:kOpenPopupScript];

  BOOL infobarVisible = [[GREYCondition
      conditionWithName:@"Wait for blocked popups infobar to show"
                  block:^BOOL {
                    NSError* error = nil;
                    [[EarlGrey
                        selectElementWithMatcher:
                            chrome_test_util::StaticTextWithAccessibilityLabel(
                                @"Pop-ups blocked (1)")]
                        assertWithMatcher:grey_sufficientlyVisible()
                                    error:&error];
                    return error == nil;
                  }] waitWithTimeout:4.0];
  GREYAssertTrue(infobarVisible, @"Infobar did not appear");
  [ChromeEarlGrey waitForMainTabCount:1];
}

// Tests that the "exceptions" section on the settings page is hidden and
// revealed properly when the preference switch is toggled.
- (void)testSettingsPageWithExceptions {
  std::string allowedPattern = "[*.]example.com";
  ScopedBlockPopupsPref prefSetter(CONTENT_SETTING_BLOCK);
  ScopedBlockPopupsException exceptionSetter(allowedPattern);

  [ChromeEarlGreyUI openSettingsMenu];
  [ChromeEarlGreyUI tapSettingsMenuButton:ContentSettingsButton()];
  {
    ScopedDisableTimerTracking timerTrackingDisabler;
    [[EarlGrey selectElementWithMatcher:BlockPopupsSettingsButton()]
        performAction:grey_tap()];

    // Make sure that the "example.com" exception is listed.
    [[EarlGrey selectElementWithMatcher:grey_text(base::SysUTF8ToNSString(
                                            allowedPattern))]
        assertWithMatcher:grey_sufficientlyVisible()];

    // Toggle the switch off via the UI and make sure the exceptions are not
    // visible.
    [[EarlGrey
        selectElementWithMatcher:chrome_test_util::TableViewSwitchCell(
                                     @"blockPopupsContentView_switch", YES)]
        performAction:chrome_test_util::TurnTableViewSwitchOn(NO)];
    [[EarlGrey selectElementWithMatcher:grey_text(base::SysUTF8ToNSString(
                                            allowedPattern))]
        assertWithMatcher:grey_notVisible()];
    [[EarlGrey selectElementWithMatcher:NavigationBarEditButton()]
        assertWithMatcher:grey_notVisible()];
    [[EarlGrey selectElementWithMatcher:SettingsDoneButton()]
        assertWithMatcher:grey_sufficientlyVisible()];

    // Toggle the switch back on via the UI and make sure the exceptions are now
    // visible.
    [[EarlGrey
        selectElementWithMatcher:chrome_test_util::TableViewSwitchCell(
                                     @"blockPopupsContentView_switch", NO)]
        performAction:chrome_test_util::TurnTableViewSwitchOn(YES)];
    [[EarlGrey selectElementWithMatcher:grey_text(base::SysUTF8ToNSString(
                                            allowedPattern))]
        assertWithMatcher:grey_sufficientlyVisible()];
    [[EarlGrey selectElementWithMatcher:NavigationBarEditButton()]
        assertWithMatcher:grey_sufficientlyVisible()];

    // Disable EarlGrey synchronization to avoid infinite spinner loop.
    ScopedSynchronizationDisabler synchronizationDisabler;

    // Close the settings menu.
    [[EarlGrey selectElementWithMatcher:NavigationBarBackButton()]
        performAction:grey_tap()];
    [[EarlGrey selectElementWithMatcher:NavigationBarBackButton()]
        performAction:grey_tap()];
    [[EarlGrey selectElementWithMatcher:SettingsDoneButton()]
        performAction:grey_tap()];
  }
}

@end
