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

#import <XCTest/XCTest.h>

#import "base/ios/ios_util.h"
#import "base/logging.h"
#import "base/strings/sys_string_conversions.h"
#import "base/strings/utf_string_conversions.h"
#import "base/test/ios/wait_util.h"
#import "components/signin/internal/identity_manager/account_capabilities_constants.h"
#import "components/translate/core/browser/translate_pref_names.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/badges/ui_bundled/badge_constants.h"
#import "ios/chrome/browser/infobars/ui_bundled/banners/infobar_banner_constants.h"
#import "ios/chrome/browser/infobars/ui_bundled/infobar_earl_grey_ui_test_util.h"
#import "ios/chrome/browser/infobars/ui_bundled/modals/infobar_modal_constants.h"
#import "ios/chrome/browser/overlays/model/public/web_content_area/alert_constants.h"
#import "ios/chrome/browser/permissions/ui_bundled/permissions_app_interface.h"
#import "ios/chrome/browser/permissions/ui_bundled/permissions_constants.h"
#import "ios/chrome/browser/reader_mode/model/features.h"
#import "ios/chrome/browser/signin/model/fake_system_identity.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/scoped_eg_synchronization_disabler.h"
#import "ios/testing/earl_grey/app_launch_configuration.h"
#import "ios/testing/earl_grey/disabled_test_macros.h"
#import "ios/testing/earl_grey/earl_grey_test.h"
#import "ios/web/public/permissions/permissions.h"
#import "net/test/embedded_test_server/default_handlers.h"
#import "net/test/embedded_test_server/embedded_test_server.h"
#import "ui/base/l10n/l10n_util.h"

namespace {

using ::base::test::ios::kWaitForPageLoadTimeout;
using ::base::test::ios::kWaitForUIElementTimeout;
using ::base::test::ios::WaitUntilConditionOrTimeout;

// Matcher for banner shown when camera permission is enabled.
id<GREYMatcher> InfobarBannerCameraOnly() {
  return grey_allOf(
      grey_accessibilityID(kInfobarBannerLabelsStackViewIdentifier),
      grey_accessibilityLabel(l10n_util::GetNSString(
          IDS_IOS_PERMISSIONS_INFOBAR_BANNER_CAMERA_ACCESSIBLE)),
      nil);
}

// Matcher for banner shown when microphone permission is enabled.
id<GREYMatcher> InfobarBannerMicrophoneOnly() {
  return grey_allOf(
      grey_accessibilityID(kInfobarBannerLabelsStackViewIdentifier),
      grey_accessibilityLabel(l10n_util::GetNSString(
          IDS_IOS_PERMISSIONS_INFOBAR_BANNER_MICROPHONE_ACCESSIBLE)),
      nil);
}

// Matcher for banner shown when both camera and microphone permissions are
// enabled.
id<GREYMatcher> InfobarBannerCameraAndMicrophone() {
  return grey_allOf(
      grey_accessibilityID(kInfobarBannerLabelsStackViewIdentifier),
      grey_accessibilityLabel(l10n_util::GetNSString(
          IDS_IOS_PERMISSIONS_INFOBAR_BANNER_CAMERA_AND_MICROPHONE_ACCESSIBLE)),
      nil);
}

// Matcher for infobar banner "Edit" button.
id<GREYMatcher> InfobarBannerEditButton() {
  return grey_accessibilityID(kInfobarBannerAcceptButtonIdentifier);
}

// Matcher for camera infobar badge with acceptance state `accepted`;
id<GREYMatcher> CameraBadge(BOOL accepted) {
  NSString* axid =
      accepted ? kBadgeButtonPermissionsCameraAcceptedAccessibilityIdentifier
               : kBadgeButtonPermissionsCameraAccessibilityIdentifier;
  return grey_accessibilityID(axid);
}

// Matcher for microphone infobar badge with acceptance state `accepted`;
id<GREYMatcher> MicrophoneBadge(BOOL accepted) {
  NSString* axid =
      accepted
          ? kBadgeButtonPermissionsMicrophoneAcceptedAccessibilityIdentifier
          : kBadgeButtonPermissionsMicrophoneAccessibilityIdentifier;
  return grey_accessibilityID(axid);
}

// Matcher infobar modal camera permissions switch.
id<GREYMatcher> InfobarModalCameraPermissionsSwitch(BOOL isOn) {
  return chrome_test_util::TableViewSwitchCell(
      kInfobarModalCameraSwitchAccessibilityIdentifier, isOn);
}

// Matcher infobar modal microphone permissions switch.
id<GREYMatcher> InfobarModalMicrophonePermissionsSwitch(BOOL isOn) {
  return chrome_test_util::TableViewSwitchCell(
      kInfobarModalMicrophoneSwitchAccessibilityIdentifier, isOn);
}

// Taps "Done" button to dismiss modal when shown.
void TapDoneButtonOnInfobarModal() {
  [[EarlGrey
      selectElementWithMatcher:grey_accessibilityID(kInfobarModalCancelButton)]
      performAction:grey_tap()];
}

}  // namespace

// This test tests the infobar banner, modal and badge behaviors related to
// permissions.
@interface PermissionsTestCase : ChromeTestCase
@end

@implementation PermissionsTestCase

- (void)setUp {
  [super setUp];
  // Disable translate to avoid the translate badge showing automatically.
  [ChromeEarlGrey setBoolValue:NO
                   forUserPref:translate::prefs::kOfferTranslateEnabled];
}

- (void)tearDownHelper {
  // Reactivate translation.
  [ChromeEarlGrey setBoolValue:YES
                   forUserPref:translate::prefs::kOfferTranslateEnabled];
  [super tearDownHelper];
}

#pragma mark - Helper functions

// Checks that if the alert for site permissions pops up with
// `permissionsString` that shows permissions requested, and allow or deny it.
- (void)checkAndTapAlertContainingPermissions:(NSString*)permissionsString
                                  shouldAllow:(BOOL)allow {
  //  Allow system permission if shown.
  NSError* systemAlertFoundError = nil;
  [[EarlGrey selectElementWithMatcher:grey_systemAlertViewShown()]
      assertWithMatcher:grey_nil()
                  error:&systemAlertFoundError];
  if (systemAlertFoundError) {
    NSError* acceptAlertError = nil;
    [self grey_acceptSystemDialogWithError:&acceptAlertError];
    GREYAssertNil(acceptAlertError, @"Error accepting system alert.\n%@",
                  acceptAlertError);
  }
  // Click button on site permissions dialog.
  id<GREYMatcher> dialogMatcher =
      grey_accessibilityID(kPermissionsDialogAccessibilityIdentifier);
  ConditionBlock condition = ^{
    NSError* error = nil;
    [[EarlGrey selectElementWithMatcher:dialogMatcher]
        assertWithMatcher:grey_sufficientlyVisible()
                    error:&error];
    return !error;
  };
  GREYAssert(WaitUntilConditionOrTimeout(kWaitForUIElementTimeout, condition),
             @"Permissions dialog was not shown.");
  NSString* alertText = l10n_util::GetNSStringF(
      IDS_IOS_PERMISSIONS_ALERT_DIALOG_MESSAGE,
      base::UTF8ToUTF16(self.testServer->base_url().GetHost()),
      base::SysNSStringToUTF16(permissionsString));
  id<GREYMatcher> textMatcher = grey_allOf(
      grey_ancestor(dialogMatcher), grey_accessibilityLabel(alertText), nil);
  [[EarlGrey selectElementWithMatcher:textMatcher]
      assertWithMatcher:grey_sufficientlyVisible()];
  NSString* buttonText = l10n_util::GetNSString(
      allow ? IDS_IOS_PERMISSIONS_ALERT_DIALOG_BUTTON_TEXT_GRANT
            : IDS_IOS_PERMISSIONS_ALERT_DIALOG_BUTTON_TEXT_DENY);

  id<GREYMatcher> buttonMatcher = grey_allOf(
      grey_ancestor(dialogMatcher), grey_accessibilityLabel(buttonText),
      grey_accessibilityTrait(UIAccessibilityTraitStaticText), nil);

  [[[EarlGrey selectElementWithMatcher:buttonMatcher]
      assertWithMatcher:grey_sufficientlyVisible()] performAction:grey_tap()];
}

// Checks `expectedStatesForPermissions` matches the actual states for
// permissions of the active web state; checks will fail if there is no active
// web state.
- (void)checkStatesForPermissions:
    (NSDictionary<NSNumber*, NSNumber*>*)expectedStatesForPermissions {
  NSDictionary<NSNumber*, NSNumber*>* actualStatesForPermissions =
      [PermissionsAppInterface statesForAllPermissions];
  GREYAssertEqualObjects(
      expectedStatesForPermissions[@(web::PermissionCamera)],
      actualStatesForPermissions[@(web::PermissionCamera)],
      @"Camera state: %@ does not match expected: %@.",
      actualStatesForPermissions[@(web::PermissionCamera)],
      expectedStatesForPermissions[@(web::PermissionCamera)]);
  GREYAssertEqualObjects(
      expectedStatesForPermissions[@(web::PermissionMicrophone)],
      actualStatesForPermissions[@(web::PermissionMicrophone)],
      @"Microphone state: %@ does not match expected: %@.",
      actualStatesForPermissions[@(web::PermissionMicrophone)],
      expectedStatesForPermissions[@(web::PermissionMicrophone)]);
}

#pragma mark - Test cases

// Tests that when camera permission is granted, the user could see a banner
// notification and then toggle camera permission through the infobar modal
// through pressing the "Edit" button on the banner.
- (void)testAllowAndBlockCameraPermission {
  // TODO(crbug.com/342245057): Camera access is broken in the simulator on iOS
  // 17.5.
  if (@available(iOS 17.5, *)) {
    EARL_GREY_TEST_DISABLED(@"Test disabled on iOS 17.5.");
  }
  GREYAssertTrue(self.testServer->Start(), @"Test server failed to start.");
  [ChromeEarlGrey
      loadURL:self.testServer->GetURL("/permissions/camera_only.html")];
  {
    // It is suspected that the video element in the test page performs some
    // fast repetitive animations that, combined with the EarlGrey
    // synchronization, delays the invocation of the infobar appearance
    // animation completion block. As a workaround, we disables EarlGrey
    // synchronization whenever the test is showing the video element.
    ScopedSynchronizationDisabler disabler;
    [self checkAndTapAlertContainingPermissions:
              l10n_util::GetNSString(
                  IDS_IOS_PERMISSIONS_ALERT_DIALOG_PERMISSION_CAMERA)
                                    shouldAllow:YES];
    [InfobarEarlGreyUI waitUntilInfobarBannerVisibleOrTimeout:YES];
    [[EarlGrey selectElementWithMatcher:InfobarBannerCameraOnly()]
        assertWithMatcher:grey_sufficientlyVisible()];
    // Tap "Edit" to show infobar modal.
    [[[EarlGrey selectElementWithMatcher:InfobarBannerEditButton()]
        performAction:grey_tap()] assertWithMatcher:grey_notVisible()];
    [self checkStatesForPermissions:@{
      @(web::PermissionCamera) : @(web::PermissionStateAllowed),
      @(web::PermissionMicrophone) : @(web::PermissionStateNotAccessible)
    }];
    [[EarlGrey
        selectElementWithMatcher:InfobarModalCameraPermissionsSwitch(YES)]
        performAction:chrome_test_util::TurnTableViewSwitchOn(NO)];
    [self checkStatesForPermissions:@{
      @(web::PermissionCamera) : @(web::PermissionStateBlocked),
      @(web::PermissionMicrophone) : @(web::PermissionStateNotAccessible)
    }];
    TapDoneButtonOnInfobarModal();
    [[EarlGrey selectElementWithMatcher:CameraBadge(/*accepted=*/NO)]
        assertWithMatcher:grey_sufficientlyVisible()];
  }
}

// Tests that when microphone permission is granted, the user could see a banner
// notification and then toggle microphone permission through the infobar modal
// through the location bar badge.
// TODO(crbug.com/460744137): Test is flaky.
- (void)FLAKY_testAllowAndBlockMicrophonePermission {
  GREYAssertTrue(self.testServer->Start(), @"Test server failed to start.");
  [ChromeEarlGrey
      loadURL:self.testServer->GetURL("/permissions/microphone_only.html")];
  [self checkAndTapAlertContainingPermissions:
            l10n_util::GetNSString(
                IDS_IOS_PERMISSIONS_ALERT_DIALOG_PERMISSION_MICROPHONE)
                                  shouldAllow:YES];
  [InfobarEarlGreyUI waitUntilInfobarBannerVisibleOrTimeout:YES];
  [[EarlGrey selectElementWithMatcher:InfobarBannerMicrophoneOnly()]
      assertWithMatcher:grey_sufficientlyVisible()];
  // Swipe up to dismiss.
  [[[EarlGrey selectElementWithMatcher:InfobarBannerMicrophoneOnly()]
      performAction:grey_swipeFastInDirection(kGREYDirectionUp)]
      assertWithMatcher:grey_notVisible()];
  // Tap badges button to show modal.
  [[EarlGrey selectElementWithMatcher:MicrophoneBadge(/*accepted=*/YES)]
      performAction:grey_tap()];
  [self checkStatesForPermissions:@{
    @(web::PermissionCamera) : @(web::PermissionStateNotAccessible),
    @(web::PermissionMicrophone) : @(web::PermissionStateAllowed)
  }];
  [[EarlGrey
      selectElementWithMatcher:InfobarModalMicrophonePermissionsSwitch(YES)]
      performAction:chrome_test_util::TurnTableViewSwitchOn(NO)];
  [self checkStatesForPermissions:@{
    @(web::PermissionCamera) : @(web::PermissionStateNotAccessible),
    @(web::PermissionMicrophone) : @(web::PermissionStateBlocked)
  }];
  TapDoneButtonOnInfobarModal();
  [[EarlGrey selectElementWithMatcher:MicrophoneBadge(/*accepted=*/NO)]
      assertWithMatcher:grey_sufficientlyVisible()];
}

// Tests that when both camera and microphone permissions are granted, the user
// could see a banner notification and then toggle the permissions through both
// the infobar modal and the location bar badge.
- (void)testAllowAndBlockCameraAndMicrophonePermissions {
  // TODO(crbug.com/342245057): Camera access is broken in the simulator on iOS
  // 17.5.
  if (@available(iOS 17.5, *)) {
    EARL_GREY_TEST_DISABLED(@"Test disabled on iOS 17.5.");
  }
  GREYAssertTrue(self.testServer->Start(), @"Test server failed to start.");
  [ChromeEarlGrey loadURL:self.testServer->GetURL(
                              "/permissions/camera_and_microphone.html")];
  {
    // It is suspected that the video element in the test page performs some
    // fast repetitive animations that, combined with the EarlGrey
    // synchronization, delays the invocation of the infobar appearance
    // animation completion block. As a workaround, we disables EarlGrey
    // synchronization whenever the test is showing the video element.
    ScopedSynchronizationDisabler disabler;
    [self
        checkAndTapAlertContainingPermissions:
            l10n_util::GetNSString(
                IDS_IOS_PERMISSIONS_ALERT_DIALOG_PERMISSION_CAMERA_AND_MICROPHONE)
                                  shouldAllow:YES];
    [InfobarEarlGreyUI waitUntilInfobarBannerVisibleOrTimeout:YES];
    [[EarlGrey selectElementWithMatcher:InfobarBannerCameraAndMicrophone()]
        assertWithMatcher:grey_sufficientlyVisible()];
    // Tap "Edit" to show infobar modal.
    [[[EarlGrey selectElementWithMatcher:InfobarBannerEditButton()]
        performAction:grey_tap()] assertWithMatcher:grey_notVisible()];
    [self checkStatesForPermissions:@{
      @(web::PermissionCamera) : @(web::PermissionStateAllowed),
      @(web::PermissionMicrophone) : @(web::PermissionStateAllowed)
    }];
    // Block camera permission.
    [[EarlGrey
        selectElementWithMatcher:InfobarModalCameraPermissionsSwitch(YES)]
        performAction:chrome_test_util::TurnTableViewSwitchOn(NO)];
    // Taps "Done" button. Check infobar badge and tap it.
    TapDoneButtonOnInfobarModal();
    [[EarlGrey selectElementWithMatcher:MicrophoneBadge(/*accepted=*/YES)]
        performAction:grey_tap()];
    // Check current permission states and block microphone permission as
    // well.
    [self checkStatesForPermissions:@{
      @(web::PermissionCamera) : @(web::PermissionStateBlocked),
      @(web::PermissionMicrophone) : @(web::PermissionStateAllowed)
    }];
    [[EarlGrey
        selectElementWithMatcher:InfobarModalMicrophonePermissionsSwitch(YES)]
        performAction:chrome_test_util::TurnTableViewSwitchOn(NO)];
    // Taps "Done" button. Check infobar badge and tap it again.
    TapDoneButtonOnInfobarModal();
    [[EarlGrey selectElementWithMatcher:CameraBadge(/*accepted=*/NO)]
        performAction:grey_tap()];
    // Check current permission states and re-enable camera permission.
    [self checkStatesForPermissions:@{
      @(web::PermissionCamera) : @(web::PermissionStateBlocked),
      @(web::PermissionMicrophone) : @(web::PermissionStateBlocked)
    }];
    [[EarlGrey selectElementWithMatcher:InfobarModalCameraPermissionsSwitch(NO)]
        performAction:chrome_test_util::TurnTableViewSwitchOn(YES)];
    TapDoneButtonOnInfobarModal();
    [[EarlGrey selectElementWithMatcher:CameraBadge(/*accepted=*/YES)]
        assertWithMatcher:grey_sufficientlyVisible()];
    [self checkStatesForPermissions:@{
      @(web::PermissionCamera) : @(web::PermissionStateAllowed),
      @(web::PermissionMicrophone) : @(web::PermissionStateBlocked)
    }];
  }
}

// Tests that when permissions are denied, there will not be a banner or badge.
- (void)testDenyPermissions {
  GREYAssertTrue(self.testServer->Start(), @"Test server failed to start.");
  [ChromeEarlGrey loadURL:self.testServer->GetURL(
                              "/permissions/camera_and_microphone.html")];
  {
    // It is suspected that the video element in the test page performs some
    // fast repetitive animations that, combined with the EarlGrey
    // synchronization, delays the invocation of the infobar appearance
    // animation completion block. As a workaround, we disables EarlGrey
    // synchronization whenever the test is showing the video element.
    ScopedSynchronizationDisabler disabler;
    [self
        checkAndTapAlertContainingPermissions:
            l10n_util::GetNSString(
                IDS_IOS_PERMISSIONS_ALERT_DIALOG_PERMISSION_CAMERA_AND_MICROPHONE)
                                  shouldAllow:NO];
    [InfobarEarlGreyUI waitUntilInfobarBannerVisibleOrTimeout:NO];
    id<GREYMatcher> anyPermissionBadge =
        grey_anyOf(CameraBadge(/*accepted=*/YES), CameraBadge(NO),
                   MicrophoneBadge(YES), MicrophoneBadge(NO), nil);
    [[EarlGrey selectElementWithMatcher:anyPermissionBadge]
        assertWithMatcher:grey_nil()];
    [self checkStatesForPermissions:@{
      @(web::PermissionCamera) : @(web::PermissionStateNotAccessible),
      @(web::PermissionMicrophone) : @(web::PermissionStateNotAccessible)
    }];
  }
}

// Tests that permissions infobar banner, modal and badge works the same way in
// incognito by toggling camera access.
- (void)testTogglePermissionsInIncognito {
  // TODO(crbug.com/342245057): Camera access is broken in the simulator on iOS
  // 17.5.
  if (@available(iOS 17.5, *)) {
    EARL_GREY_TEST_DISABLED(@"Test disabled on iOS 17.5.");
  }
  GREYAssertTrue(self.testServer->Start(), @"Test server failed to start.");
  [ChromeEarlGrey openNewIncognitoTab];
  [ChromeEarlGrey
      loadURL:self.testServer->GetURL("/permissions/camera_only.html")];
  {
    // It is suspected that the video element in the test page performs some
    // fast repetitive animations that, combined with the EarlGrey
    // synchronization, delays the invocation of the infobar appearance
    // animation completion block. As a workaround, we disables EarlGrey
    // synchronization whenever the test is showing the video element.
    ScopedSynchronizationDisabler disabler;
    [self checkAndTapAlertContainingPermissions:
              l10n_util::GetNSString(
                  IDS_IOS_PERMISSIONS_ALERT_DIALOG_PERMISSION_CAMERA)
                                    shouldAllow:YES];
    [InfobarEarlGreyUI waitUntilInfobarBannerVisibleOrTimeout:YES];
    [[EarlGrey selectElementWithMatcher:InfobarBannerCameraOnly()]
        assertWithMatcher:grey_sufficientlyVisible()];
    // Tap "Edit" to show infobar modal.
    [[[EarlGrey selectElementWithMatcher:InfobarBannerEditButton()]
        performAction:grey_tap()] assertWithMatcher:grey_notVisible()];
    [self checkStatesForPermissions:@{
      @(web::PermissionCamera) : @(web::PermissionStateAllowed),
      @(web::PermissionMicrophone) : @(web::PermissionStateNotAccessible)
    }];
    [[EarlGrey
        selectElementWithMatcher:InfobarModalCameraPermissionsSwitch(YES)]
        performAction:chrome_test_util::TurnTableViewSwitchOn(NO)];
    [self checkStatesForPermissions:@{
      @(web::PermissionCamera) : @(web::PermissionStateBlocked),
      @(web::PermissionMicrophone) : @(web::PermissionStateNotAccessible)
    }];
    TapDoneButtonOnInfobarModal();
    [[EarlGrey selectElementWithMatcher:CameraBadge(/*accepted=*/NO)]
        assertWithMatcher:grey_sufficientlyVisible()];
  }
}

// Tests that permissions infobar banner is correctly dismissed when the
// incognito browser is killed.
- (void)testDismissPermissionsWhenIncognitoKilled {
  GREYAssertTrue(self.testServer->Start(), @"Test server failed to start.");
  [ChromeEarlGrey openNewIncognitoTab];
  [ChromeEarlGrey
      loadURL:self.testServer->GetURL("/permissions/camera_only.html")];
  {
    // It is suspected that the video element in the test page performs some
    // fast repetitive animations that, combined with the EarlGrey
    // synchronization, delays the invocation of the infobar appearance
    // animation completion block. As a workaround, we disables EarlGrey
    // synchronization whenever the test is showing the video element.
    ScopedSynchronizationDisabler disabler;
    [self checkAndTapAlertContainingPermissions:
              l10n_util::GetNSString(
                  IDS_IOS_PERMISSIONS_ALERT_DIALOG_PERMISSION_CAMERA)
                                    shouldAllow:YES];
    [InfobarEarlGreyUI waitUntilInfobarBannerVisibleOrTimeout:YES];

    // Kill the incognito browser while the banner is still presented.
    [ChromeEarlGreyUI openTabGrid];
  }
  [[EarlGrey selectElementWithMatcher:chrome_test_util::
                                          TabGridCloseButtonForCellAtIndex(0)]
      performAction:grey_tap()];
}

// Tests that permissions are reset after user navigation.
// TODO(crbug.com/40921852): Re-enable the test.
- (void)DISABLED_testPermissionsAfterNavigation {
  GREYAssertTrue(self.testServer->Start(), @"Test server failed to start.");
  [ChromeEarlGrey
      loadURL:self.testServer->GetURL("/permissions/microphone_only.html")];
  [self checkAndTapAlertContainingPermissions:
            l10n_util::GetNSString(
                IDS_IOS_PERMISSIONS_ALERT_DIALOG_PERMISSION_MICROPHONE)
                                  shouldAllow:YES];
  [InfobarEarlGreyUI waitUntilInfobarBannerVisibleOrTimeout:YES];
  [[EarlGrey selectElementWithMatcher:InfobarBannerEditButton()]
      performAction:grey_tap()];
  [[EarlGrey
      selectElementWithMatcher:InfobarModalMicrophonePermissionsSwitch(YES)]
      performAction:chrome_test_util::TurnTableViewSwitchOn(NO)];
  TapDoneButtonOnInfobarModal();
  [[EarlGrey selectElementWithMatcher:MicrophoneBadge(/*accepted=*/NO)]
      assertWithMatcher:grey_sufficientlyVisible()];

  // Navigate away and go back.
  [ChromeEarlGrey loadURL:self.testServer->GetURL("/pony.html")];
  [ChromeEarlGrey goBack];

  // Note: There's currently an existing WebKit bug that WKUIDelegate method
  // `requestMediaCapturePermissionForOrigin:` would not be invoked when the
  // user hits backward/forward; therefore, the alert and banner would not
  // show again, and the checks for the alert and the infobar banner are
  // commented out. Once this issue is fixed, these checks should be
  // uncommented.

  /*[self checkAndTapAlertContainingPermissions:
            l10n_util::GetNSString(
                IDS_IOS_PERMISSIONS_ALERT_DIALOG_PERMISSION_MICROPHONE)
                                  shouldAllow:YES];
  [InfobarEarlGreyUI waitUntilInfobarBannerVisibleOrTimeout:YES];*/
  [self checkStatesForPermissions:@{
    @(web::PermissionCamera) : @(web::PermissionStateNotAccessible),
    @(web::PermissionMicrophone) : @(web::PermissionStateNotAccessible)
  }];
}

// Tests that permissions stay the same after user switches to another tab then
// comes back.
- (void)testPermissionsAfterTabSwitch {
  // TODO(crbug.com/40921852): Failing on iOS17.
  if (@available(iOS 17.0, *)) {
    EARL_GREY_TEST_DISABLED(@"Failing on iOS17.");
  }

  GREYAssertTrue(self.testServer->Start(), @"Test server failed to start.");
  // Opens a page that requests both camera and microphone permissions, and
  // block microphone permission.
  [ChromeEarlGrey loadURL:self.testServer->GetURL(
                              "/permissions/camera_and_microphone.html")];
  {
    // It is suspected that the video element in the test page performs some
    // fast repetitive animations that, combined with the EarlGrey
    // synchronization, delays the invocation of the infobar appearance
    // animation completion block. As a workaround, we disables EarlGrey
    // synchronization whenever the test is showing the video element.
    ScopedSynchronizationDisabler disabler;
    [self
        checkAndTapAlertContainingPermissions:
            l10n_util::GetNSString(
                IDS_IOS_PERMISSIONS_ALERT_DIALOG_PERMISSION_CAMERA_AND_MICROPHONE)
                                  shouldAllow:YES];
    [InfobarEarlGreyUI waitUntilInfobarBannerVisibleOrTimeout:YES];
    [[EarlGrey selectElementWithMatcher:InfobarBannerEditButton()]
        performAction:grey_tap()];
    [[EarlGrey
        selectElementWithMatcher:InfobarModalCameraPermissionsSwitch(YES)]
        performAction:chrome_test_util::TurnTableViewSwitchOn(NO)];
  }
  TapDoneButtonOnInfobarModal();
  [[EarlGrey selectElementWithMatcher:MicrophoneBadge(/*accepted=*/YES)]
      assertWithMatcher:grey_sufficientlyVisible()];
  // Switches tab and go back.
  [ChromeEarlGrey openNewTab];
  [ChromeEarlGrey loadURL:self.testServer->GetURL("/pony.html")];
  [ChromeEarlGrey selectTabAtIndex:0];
  // Check if permission states stay the same. Wrap this in a GREYCondition
  // since it takes a small timeout for the web state to retrieve its state.
  GREYCondition* microphoneAcceptedBadgeShown = [GREYCondition
      conditionWithName:@"Microphone accepted badge visible"
                  block:^BOOL {
                    NSError* error;
                    [[EarlGrey selectElementWithMatcher:MicrophoneBadge(
                                                            /*accepted=*/YES)]
                        assertWithMatcher:grey_sufficientlyVisible()
                                    error:&error];
                    return error == nil;
                  }];
  // Wait for infobar to be shown or timeout after kWaitForUIElementTimeout.
  BOOL success = [microphoneAcceptedBadgeShown
      waitWithTimeout:base::test::ios::kWaitForUIElementTimeout.InSecondsF()];
  GREYAssertTrue(success, @"Did not find accepted microphone badge.");
  [self checkStatesForPermissions:@{
    @(web::PermissionCamera) : @(web::PermissionStateBlocked),
    @(web::PermissionMicrophone) : @(web::PermissionStateAllowed)
  }];
}

// Tests that permissions are reset after reload.
- (void)testPermissionsAfterReload {
  // TODO(crbug.com/342245057): Camera access is broken in the simulator on iOS
  // 17.5.
  if (@available(iOS 17.5, *)) {
    EARL_GREY_TEST_DISABLED(@"Test disabled on iOS 17.5.");
  }
  GREYAssertTrue(self.testServer->Start(), @"Test server failed to start.");
  // Opens a page that requests camera permission.
  [ChromeEarlGrey
      loadURL:self.testServer->GetURL("/permissions/camera_only.html")];

  {
    // It is suspected that the video element in the test page performs some
    // fast repetitive animations that, combined with the EarlGrey
    // synchronization, delays the invocation of the infobar appearance
    // animation completion block. As a workaround, we disables EarlGrey
    // synchronization whenever the test is showing the video element.
    ScopedSynchronizationDisabler disabler;
    [self checkAndTapAlertContainingPermissions:
              l10n_util::GetNSString(
                  IDS_IOS_PERMISSIONS_ALERT_DIALOG_PERMISSION_CAMERA)
                                    shouldAllow:YES];
    [InfobarEarlGreyUI waitUntilInfobarBannerVisibleOrTimeout:YES];
    [[EarlGrey selectElementWithMatcher:InfobarBannerCameraOnly()]
        assertWithMatcher:grey_sufficientlyVisible()];
  }

  // Reload and allow again and check if things work as expected. Wait until
  // permissions are reset to validate behaviors.
  [ChromeEarlGrey reload];
  GREYCondition* permissionReset = [GREYCondition
      conditionWithName:@"Permission is reset"
                  block:^BOOL {
                    NSDictionary<NSNumber*, NSNumber*>*
                        actualStatesForPermissions =
                            [PermissionsAppInterface statesForAllPermissions];
                    return [actualStatesForPermissions[@(web::PermissionCamera)]
                        isEqualToNumber:@(web::PermissionStateNotAccessible)];
                  }];
  BOOL success = [permissionReset
      waitWithTimeout:base::test::ios::kWaitForPageLoadTimeout.InSecondsF()];
  GREYAssertTrue(success,
                 @"Camera permission state is not reset after reload.");
  {
    // It is suspected that the video element in the test page performs some
    // fast repetitive animations that, combined with the EarlGrey
    // synchronization, delays the invocation of the infobar appearance
    // animation completion block. As a workaround, we disables EarlGrey
    // synchronization whenever the test is showing the video element.
    ScopedSynchronizationDisabler disabler;
    [self checkAndTapAlertContainingPermissions:
              l10n_util::GetNSString(
                  IDS_IOS_PERMISSIONS_ALERT_DIALOG_PERMISSION_CAMERA)
                                    shouldAllow:YES];
    [InfobarEarlGreyUI waitUntilInfobarBannerVisibleOrTimeout:YES];
    [[EarlGrey selectElementWithMatcher:InfobarBannerCameraOnly()]
        performAction:grey_swipeFastInDirection(kGREYDirectionUp)];
    [[EarlGrey selectElementWithMatcher:CameraBadge(/*accepted=*/YES)]
        performAction:grey_tap()];
    [self checkStatesForPermissions:@{
      @(web::PermissionCamera) : @(web::PermissionStateAllowed),
      @(web::PermissionMicrophone) : @(web::PermissionStateNotAccessible)
    }];
    [[EarlGrey
        selectElementWithMatcher:InfobarModalCameraPermissionsSwitch(YES)]
        performAction:chrome_test_util::TurnTableViewSwitchOn(NO)];
    TapDoneButtonOnInfobarModal();
    [self checkStatesForPermissions:@{
      @(web::PermissionCamera) : @(web::PermissionStateBlocked),
      @(web::PermissionMicrophone) : @(web::PermissionStateNotAccessible)
    }];
  }

  // Reload and deny to check if permissions are no longer accessible.
  [ChromeEarlGrey reload];
  [self checkAndTapAlertContainingPermissions:
            l10n_util::GetNSString(
                IDS_IOS_PERMISSIONS_ALERT_DIALOG_PERMISSION_CAMERA)
                                  shouldAllow:NO];
  [InfobarEarlGreyUI waitUntilInfobarBannerVisibleOrTimeout:NO];
  id<GREYMatcher> anyPermissionBadge =
      grey_anyOf(CameraBadge(/*accepted=*/YES), CameraBadge(NO),
                 MicrophoneBadge(YES), MicrophoneBadge(NO), nil);
  [[EarlGrey selectElementWithMatcher:anyPermissionBadge]
      assertWithMatcher:grey_nil()];
}

// Tests that a supervised user account with parental controls enabled does not
// have access to modify camera or microphone site permissions.
// TODO(crbug.com/40921852): Failing on iOS17 on physical devices.
#if TARGET_OS_SIMULATOR
#define MAYBE_testSupervisedUserPermissionsNoCameraOrMicAccess \
  testSupervisedUserPermissionsNoCameraOrMicAccess
#else
#define MAYBE_testSupervisedUserPermissionsNoCameraOrMicAccess \
  DISABLED_testSupervisedUserPermissionsNoCameraOrMicAccess
#endif
- (void)MAYBE_testSupervisedUserPermissionsNoCameraOrMicAccess {
  // These settings are controlled in Family Link and would be updated through
  // Sync content settings.
  [ChromeEarlGrey setContentSetting:ContentSetting::CONTENT_SETTING_BLOCK
             forContentSettingsType:ContentSettingsType::MEDIASTREAM_CAMERA];
  [ChromeEarlGrey setContentSetting:ContentSetting::CONTENT_SETTING_BLOCK
             forContentSettingsType:ContentSettingsType::MEDIASTREAM_MIC];

  FakeSystemIdentity* fakeIdentity = [FakeSystemIdentity fakeIdentity1];
  [SigninEarlGrey addFakeIdentity:fakeIdentity
                 withCapabilities:@{
                   @(kIsSubjectToParentalControlsCapabilityName) : @YES,
                 }];

  [SigninEarlGrey signinWithFakeIdentity:fakeIdentity];

  GREYAssertTrue(self.testServer->Start(), @"Test server failed to start.");
  [ChromeEarlGrey loadURL:self.testServer->GetURL(
                              "/permissions/camera_and_microphone.html")];

  [[EarlGrey selectElementWithMatcher:grey_systemAlertViewShown()]
      assertWithMatcher:grey_nil()];
  [InfobarEarlGreyUI waitUntilInfobarBannerVisibleOrTimeout:NO];
  [self checkStatesForPermissions:@{
    @(web::PermissionCamera) : @(web::PermissionStateNotAccessible),
    @(web::PermissionMicrophone) : @(web::PermissionStateNotAccessible)
  }];
}

// Tests that by enabling permissions, then triggering Reader mode, then
// disabling Reader mode, the permission badges are still visible at the end.
- (void)testPermissionsWithReaderMode {
  GREYAssertTrue(self.testServer->Start(), @"Test server failed to start.");
  [ChromeEarlGrey
      loadURL:self.testServer->GetURL("/permissions/camera_only.html")];

  {
    // It is suspected that the video element in the test page performs some
    // fast repetitive animations that, combined with the EarlGrey
    // synchronization, delays the invocation of the infobar appearance
    // animation completion block. As a workaround, we disables EarlGrey
    // synchronization whenever the test is showing the video element.
    ScopedSynchronizationDisabler disabler;
    [self checkAndTapAlertContainingPermissions:
              l10n_util::GetNSString(
                  IDS_IOS_PERMISSIONS_ALERT_DIALOG_PERMISSION_CAMERA)
                                    shouldAllow:YES];

    // Verify Camera Badge is visible (accepted state).
    [InfobarEarlGreyUI waitUntilInfobarBannerVisibleOrTimeout:YES];
    [[EarlGrey selectElementWithMatcher:InfobarBannerCameraOnly()]
        performAction:grey_swipeFastInDirection(kGREYDirectionUp)];
    [ChromeEarlGrey
        waitForSufficientlyVisibleElementWithMatcher:CameraBadge(
                                                         /*accepted=*/YES)];

    // Inject a lot of text to make the page distillable (eligible for Reader
    // Mode).
    NSString* loremIpsum =
        @"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do "
         "eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim "
         "ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut "
         "aliquip ex ea commodo consequat. Duis aute irure dolor in "
         "reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla "
         "pariatur. Excepteur sint occaecat cupidatat non proident, sunt in "
         "culpa qui officia deserunt mollit anim id est laborum.";
    NSString* script = [NSString
        stringWithFormat:
            @"(function() { for (var i = 0; i < 20; i++) { var p = "
            @"document.createElement('p'); "
            @"p.innerText = '%@'; document.body.appendChild(p); } })();",
            loremIpsum];
    [ChromeEarlGrey evaluateJavaScriptForSideEffect:script];

    // Enable Reader Mode.
    [ChromeEarlGrey showReaderModeAndWaitUntilReaderModeWebStateIsReady];

    // Disable Reader Mode.
    [ChromeEarlGrey hideReaderMode];

    // Verify Camera Badge is visible (accepted state) at the end.
    [ChromeEarlGrey
        waitForSufficientlyVisibleElementWithMatcher:CameraBadge(
                                                         /*accepted=*/YES)];
  }
}

// Tests that denying microphone permissions on a webpage that recursively
// re-requests permission upon denial does not cause a synchronous WebKit
// re-entrancy crash (regression test for issue 529634846).
- (void)testMicrophonePermissionRecursionCrashRegression {
  GREYAssertTrue(self.testServer->Start(), @"Test server failed to start.");
  [ChromeEarlGrey loadURL:self.testServer->GetURL(
                              "/permissions/microphone_recursion.html")];

  {
    ScopedSynchronizationDisabler disabler;
    // Deny the initial microphone permission alert.
    [self checkAndTapAlertContainingPermissions:
              l10n_util::GetNSString(
                  IDS_IOS_PERMISSIONS_ALERT_DIALOG_PERMISSION_MICROPHONE)
                                    shouldAllow:NO];

    // Verify that Chrome survives the recursion loop and updates the title.
    GREYAssert(WaitUntilConditionOrTimeout(
                   kWaitForPageLoadTimeout,
                   ^{
                     return [[ChromeEarlGrey currentTabTitle]
                         isEqualToString:@"Denied And Survived"];
                   }),
               @"Page title was not updated after recursion loop.");
  }
}

@end
