// Copyright 2023 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License")
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//      http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

#if os(iOS)

  import Foundation
  import XCTest

  @testable import FirebaseAuth
  import FirebaseCore
  import SafariServices

  @available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *)
  class PhoneAuthProviderTests: RPCBaseTests {
    static let kFakeAuthorizedDomain = "test.firebaseapp.com"
    static let kFakeAPIKey = "asdfghjkl"
    static let kFakeEmulatorHost = "emulatorhost"
    static let kFakeEmulatorPort = 12345
    static let kFakeClientID = "123456.apps.googleusercontent.com"
    static let kFakeFirebaseAppID = "1:123456789:ios:123abc456def"
    static let kFakeEncodedFirebaseAppID = "app-1-123456789-ios-123abc456def"
    static let kFakeTenantID = "tenantID"
    static let kFakeReverseClientID = "com.googleusercontent.apps.123456"

    private let kTestVerificationID = "verificationID"
    private let kTestVerificationCode = "verificationCode"
    private let kTestReceipt = "receipt"
    private let kTestTimeout = "1"
    private let kTestSecret = "secret"
    private let kVerificationIDKey = "sessionInfo"
    private let kFakeEncodedFirebaseAppID = "app-1-123456789-ios-123abc456def"
    private let kFakeReCAPTCHAToken = "fakeReCAPTCHAToken"
    private let kCaptchaResponse: String = "captchaResponse"
    private let kRecaptchaVersion: String = "RECAPTCHA_ENTERPRISE"

    static var auth: Auth?

    /** @fn testCredentialWithVerificationID
     @brief Tests the @c credentialWithToken method to make sure that it returns a valid AuthCredential instance.
     */
    func testCredentialWithVerificationID() throws {
      initApp(#function)
      let auth = try XCTUnwrap(PhoneAuthProviderTests.auth)
      let provider = PhoneAuthProvider.provider(auth: auth)
      let credential = provider.credential(withVerificationID: kTestVerificationID,
                                           verificationCode: kTestVerificationCode)
      switch credential.credentialKind {
      case .phoneNumber: XCTFail("Should be verification case")
      case let .verification(id, code):
        XCTAssertEqual(id, kTestVerificationID)
        XCTAssertEqual(code, kTestVerificationCode)
      }
    }

    /** @fn testVerifyEmptyPhoneNumber
     @brief Tests a failed invocation verifyPhoneNumber because an empty phone
     number was provided.
     */
    func testVerifyEmptyPhoneNumber() async throws {
      initApp(#function)
      let auth = try XCTUnwrap(PhoneAuthProviderTests.auth)
      let provider = PhoneAuthProvider.provider(auth: auth)

      do {
        _ = try await provider.verifyPhoneNumber("")
        XCTFail("Expected an error, but verification succeeded.")
      } catch {
        XCTAssertEqual((error as NSError).code, AuthErrorCode.missingPhoneNumber.rawValue)
      }
    }

    /** @fn testVerifyInvalidPhoneNumber
     @brief Tests a failed invocation @c verifyPhoneNumber:completion: because an invalid phone
     number was provided.
     */
    func testVerifyInvalidPhoneNumber() async throws {
      try await internalTestVerify(errorString: "INVALID_PHONE_NUMBER",
                                   errorCode: AuthErrorCode.invalidPhoneNumber.rawValue,
                                   function: #function)
    }

    /** @fn testVerifyPhoneNumber
     @brief Tests a successful invocation of @c verifyPhoneNumber:completion:.
     */
    func testVerifyPhoneNumber() async throws {
      try await internalTestVerify(function: #function)
    }

    /**
     @fn testVerifyPhoneNumberWithRceEnforce
     @brief Tests a successful invocation of @c verifyPhoneNumber with recaptcha enterprise enforced
     */
    func testVerifyPhoneNumberWithRceEnforceSuccess() async throws {
      initApp(#function)
      let auth = try XCTUnwrap(PhoneAuthProviderTests.auth)
      // TODO: Figure out how to mock objective C's FIRRecaptchaGetToken response
      let provider = PhoneAuthProvider.provider(auth: auth)
      let mockVerifier = FakeAuthRecaptchaVerifier(captchaResponse: kCaptchaResponse)
      AuthRecaptchaVerifier.setShared(mockVerifier, auth: auth)
      rpcIssuer.rceMode = "ENFORCE"
      let requestExpectation = expectation(description: "verifyRequester")
      rpcIssuer.verifyRequester = { request in
        XCTAssertEqual(request.phoneNumber, self.kTestPhoneNumber)
        XCTAssertEqual(request.captchaResponse, self.kCaptchaResponse)
        XCTAssertEqual(request.recaptchaVersion, "RECAPTCHA_ENTERPRISE")
        XCTAssertEqual(request.codeIdentity, CodeIdentity.empty)
        requestExpectation.fulfill()
        do {
          return try self.rpcIssuer
            .respond(withJSON: [self.kVerificationIDKey: self.kTestVerificationID])
        } catch {
          XCTFail("Failure sending response: \(error)")
          return (nil, nil)
        }
      }
      do {
        let result = try await provider.verifyClAndSendVerificationCodeWithRecaptcha(
          toPhoneNumber: kTestPhoneNumber,
          retryOnInvalidAppCredential: false,
          uiDelegate: nil,
          recaptchaVerifier: mockVerifier
        )
        XCTAssertEqual(result, kTestVerificationID)
      } catch {
        XCTFail("Unexpected error")
      }
      await fulfillment(of: [requestExpectation], timeout: 5.0)
    }

    /**
     @fn testVerifyPhoneNumberWithRceEnforceInvalidRecaptcha
     @brief Tests a successful invocation of @c verifyPhoneNumber with recaptcha enterprise enforced
     */
    func testVerifyPhoneNumberWithRceEnforceInvalidRecaptcha() async throws {
      initApp(#function)
      let auth = try XCTUnwrap(PhoneAuthProviderTests.auth)
      // TODO: Figure out how to mock objective C's FIRRecaptchaGetToken response
      let provider = PhoneAuthProvider.provider(auth: auth)
      let mockVerifier = FakeAuthRecaptchaVerifier()
      AuthRecaptchaVerifier.setShared(mockVerifier, auth: auth)
      rpcIssuer.rceMode = "ENFORCE"
      let requestExpectation = expectation(description: "verifyRequester")
      rpcIssuer?.verifyRequester = { request in
        XCTAssertEqual(request.phoneNumber, self.kTestPhoneNumber)
        XCTAssertEqual(request.captchaResponse, "NO_RECAPTCHA")
        XCTAssertEqual(request.recaptchaVersion, "RECAPTCHA_ENTERPRISE")
        XCTAssertEqual(request.codeIdentity, CodeIdentity.empty)
        requestExpectation.fulfill()
        do {
          return try self.rpcIssuer
            .respond(serverErrorMessage: "INVALID_RECAPTCHA_TOKEN",
                     error: AuthErrorUtils.invalidRecaptchaTokenError() as NSError)
        } catch {
          XCTFail("Failure sending response: \(error)")
          return (nil, nil)
        }
      }
      do {
        _ = try await provider.verifyClAndSendVerificationCodeWithRecaptcha(
          toPhoneNumber: kTestPhoneNumber,
          retryOnInvalidAppCredential: false,
          uiDelegate: nil,
          recaptchaVerifier: mockVerifier
        )
        // XCTAssertEqual(result, kTestVerificationID)
      } catch {
        // Traverse the nested error to find the root cause
        let underlyingError = (error as NSError).userInfo[NSUnderlyingErrorKey] as? NSError
        let rootError = underlyingError?.userInfo[NSUnderlyingErrorKey] as? NSError

        // Compare the root error code to the expected error code
        XCTAssertEqual(rootError?.code, AuthErrorCode.invalidRecaptchaToken.code.rawValue)
      }
      await fulfillment(of: [requestExpectation], timeout: 5.0)
    }

    /**
     @fn testVerifyPhoneNumberWithRceEnforceSDKNotLinked
     @brief Tests a successful invocation of @c verifyPhoneNumber with recaptcha enterprise enforced
     */
    func testVerifyPhoneNumberWithRceEnforceRecaptchaSDKNotLinked() async throws {
      return try await testRecaptchaFlowError(
        function: #function,
        rceError: AuthErrorUtils.recaptchaSDKNotLinkedError()
      )
    }

    /**
     @fn testVerifyPhoneNumberWithRceEnforceSDKNotLinked
     @brief Tests a successful invocation of @c verifyPhoneNumber with recaptcha enterprise enforced
     */
    func testVerifyPhoneNumberWithRceEnforceRecaptchaActionCreationFailed() async throws {
      return try await testRecaptchaFlowError(
        function: #function,
        rceError: AuthErrorUtils.recaptchaActionCreationFailed()
      )
    }

    /// @fn testVerifyPhoneNumberWithRceAudit
    /// @brief Tests a successful invocation of @c verifyPhoneNumber with recaptcha enterprise in
    /// audit mode
    func testVerifyPhoneNumberWithRceAuditSuccess() async throws {
      initApp(#function)
      let auth = try XCTUnwrap(PhoneAuthProviderTests.auth)
      let provider = PhoneAuthProvider.provider(auth: auth)
      let mockVerifier = FakeAuthRecaptchaVerifier(captchaResponse: kCaptchaResponse)
      AuthRecaptchaVerifier.setShared(mockVerifier, auth: auth)
      rpcIssuer.rceMode = "AUDIT"
      let requestExpectation = expectation(description: "verifyRequester")
      rpcIssuer.verifyRequester = { request in
        XCTAssertEqual(request.phoneNumber, self.kTestPhoneNumber)
        XCTAssertEqual(request.captchaResponse, self.kCaptchaResponse)
        XCTAssertEqual(request.recaptchaVersion, "RECAPTCHA_ENTERPRISE")
        XCTAssertEqual(request.codeIdentity, CodeIdentity.empty)
        requestExpectation.fulfill()
        do {
          return try self.rpcIssuer
            .respond(withJSON: [self.kVerificationIDKey: self.kTestVerificationID])
        } catch {
          XCTFail("Failure sending response: \(error)")
          return (nil, nil)
        }
      }
      do {
        let result = try await provider.verifyClAndSendVerificationCodeWithRecaptcha(
          toPhoneNumber: kTestPhoneNumber,
          retryOnInvalidAppCredential: false,
          uiDelegate: nil,
          recaptchaVerifier: mockVerifier
        )
        XCTAssertEqual(result, kTestVerificationID)
      } catch {
        XCTFail("Unexpected error")
      }
      await fulfillment(of: [requestExpectation], timeout: 5.0)
    }

    /// @fn testVerifyPhoneNumberWithRceAuditInvalidRecaptcha
    /// @brief Tests a successful invocation of @c verifyPhoneNumber with recaptcha enterprise in
    /// audit mode
    func testVerifyPhoneNumberWithRceAuditInvalidRecaptcha() async throws {
      initApp(#function)
      let auth = try XCTUnwrap(PhoneAuthProviderTests.auth)
      let provider = PhoneAuthProvider.provider(auth: auth)
      let mockVerifier = FakeAuthRecaptchaVerifier()
      AuthRecaptchaVerifier.setShared(mockVerifier, auth: auth)
      rpcIssuer.rceMode = "AUDIT"
      let requestExpectation = expectation(description: "verifyRequester")
      rpcIssuer?.verifyRequester = { request in
        XCTAssertEqual(request.phoneNumber, self.kTestPhoneNumber)
        XCTAssertEqual(request.captchaResponse, "NO_RECAPTCHA")
        XCTAssertEqual(request.recaptchaVersion, "RECAPTCHA_ENTERPRISE")
        XCTAssertEqual(request.codeIdentity, CodeIdentity.empty)
        requestExpectation.fulfill()
        do {
          return try self.rpcIssuer
            .respond(
              serverErrorMessage: "INVALID_RECAPTCHA_TOKEN",
              error: AuthErrorUtils.invalidRecaptchaTokenError() as NSError
            )
        } catch {
          XCTFail("Failure sending response: \(error)")
          return (nil, nil)
        }
      }
      do {
        _ = try await provider.verifyClAndSendVerificationCodeWithRecaptcha(
          toPhoneNumber: kTestPhoneNumber,
          retryOnInvalidAppCredential: false,
          uiDelegate: nil,
          recaptchaVerifier: mockVerifier
        )
      } catch {
        let underlyingError = (error as NSError).userInfo[NSUnderlyingErrorKey] as? NSError
        let rootError = underlyingError?.userInfo[NSUnderlyingErrorKey] as? NSError
        XCTAssertEqual(rootError?.code, AuthErrorCode.invalidRecaptchaToken.code.rawValue)
      }
      await fulfillment(of: [requestExpectation], timeout: 5.0)
    }

    /** @fn testVerifyPhoneNumberInTestMode
     @brief Tests a successful invocation of @c verifyPhoneNumber:completion: when app verification
     is disabled.
     */
    func testVerifyPhoneNumberInTestMode() async throws {
      try await internalTestVerify(function: #function, testMode: true)
    }

    /** @fn testVerifyPhoneNumberInTestModeFailure
     @brief Tests a failed invocation of @c verifyPhoneNumber:completion: when app verification
     is disabled.
     */
    func testVerifyPhoneNumberInTestModeFailure() async throws {
      try await internalTestVerify(errorString: "INVALID_PHONE_NUMBER",
                                   errorCode: AuthErrorCode.invalidPhoneNumber.rawValue,
                                   function: #function, testMode: true)
    }

    /** @fn testVerifyPhoneNumberUIDelegateFirebaseAppIdFlow
     @brief Tests a successful invocation of @c verifyPhoneNumber:UIDelegate:completion:.
     */
    func testVerifyPhoneNumberUIDelegateFirebaseAppIdFlow() async throws {
      try await internalTestVerify(function: #function, reCAPTCHAfallback: true)
    }

    /** @fn testVerifyPhoneNumberUIDelegateFirebaseAppIdWhileClientIdPresentFlow
     @brief Tests a successful invocation of @c verifyPhoneNumber:UIDelegate:completion: when the
     client ID is present in the plist file, but the encoded app ID is the registered custom URL scheme.
     */
    func testVerifyPhoneNumberUIDelegateFirebaseAppIdWhileClientIdPresentFlow() async throws {
      try await internalTestVerify(function: #function, useClientID: true,
                                   bothClientAndAppID: true, reCAPTCHAfallback: true)
    }

    /** @fn testVerifyPhoneNumberUIDelegateClientIdFlow
     @brief Tests a successful invocation of @c verifyPhoneNumber:UIDelegate:completion:.
     */
    func testVerifyPhoneNumberUIDelegateClientIdFlow() async throws {
      try await internalTestVerify(function: #function, useClientID: true, reCAPTCHAfallback: true)
    }

    /** @fn testVerifyPhoneNumberUIDelegateInvalidClientID
     @brief Tests a invocation of @c verifyPhoneNumber:UIDelegate:completion: which results in an
     invalid client ID error.
     */
    func testVerifyPhoneNumberUIDelegateInvalidClientID() async throws {
      try await internalTestVerify(
        errorURLString: PhoneAuthProviderTests.kFakeRedirectURLStringInvalidClientID,
        errorCode: AuthErrorCode.invalidClientID.rawValue,
        function: #function,
        useClientID: true,
        reCAPTCHAfallback: true
      )
    }

    /** @fn testVerifyPhoneNumberUIDelegateWebNetworkRequestFailed
     @brief Tests a invocation of @c verifyPhoneNumber:UIDelegate:completion: which results in a web
     network request failed error.
     */
    func testVerifyPhoneNumberUIDelegateWebNetworkRequestFailed() async throws {
      try await internalTestVerify(
        errorURLString: PhoneAuthProviderTests.kFakeRedirectURLStringWebNetworkRequestFailed,
        errorCode: AuthErrorCode.webNetworkRequestFailed.rawValue,
        function: #function,
        useClientID: true,
        reCAPTCHAfallback: true
      )
    }

    /** @fn testVerifyPhoneNumberUIDelegateWebInternalError
     @brief Tests a invocation of @c verifyPhoneNumber:UIDelegate:completion: which results in a web
     internal error.
     */
    func testVerifyPhoneNumberUIDelegateWebInternalError() async throws {
      try await internalTestVerify(
        errorURLString: PhoneAuthProviderTests.kFakeRedirectURLStringWebInternalError,
        errorCode: AuthErrorCode.webInternalError.rawValue,
        function: #function,
        useClientID: true,
        reCAPTCHAfallback: true
      )
    }

    /** @fn testVerifyPhoneNumberUIDelegateUnexpectedError
     @brief Tests a invocation of @c verifyPhoneNumber:UIDelegate:completion: which results in an
     invalid client ID.
     */
    func testVerifyPhoneNumberUIDelegateUnexpectedError() async throws {
      try await internalTestVerify(
        errorURLString: PhoneAuthProviderTests.kFakeRedirectURLStringUnknownError,
        errorCode: AuthErrorCode.webSignInUserInteractionFailure.rawValue,
        function: #function,
        useClientID: true,
        reCAPTCHAfallback: true
      )
    }

    /** @fn testVerifyPhoneNumberUIDelegateUnstructuredError
     @brief Tests a invocation of @c verifyPhoneNumber:UIDelegate:completion: which results in an
     error being surfaced with a default NSLocalizedFailureReasonErrorKey due to an unexpected
     structure of the error response.
     */
    func testVerifyPhoneNumberUIDelegateUnstructuredError() async throws {
      try await internalTestVerify(
        errorURLString: PhoneAuthProviderTests.kFakeRedirectURLStringUnstructuredError,
        errorCode: AuthErrorCode.appVerificationUserInteractionFailure.rawValue,
        function: #function,
        useClientID: true,
        reCAPTCHAfallback: true
      )
    }

    // TODO: This test is skipped. What was formerly an Objective-C exception is now a Swift fatal_error.
    // The test runs correctly, but it's not clear how to automate fatal_error testing. Switching to
    // Swift exceptions would break the API.
    /** @fn testVerifyPhoneNumberUIDelegateRaiseException
     @brief Tests a invocation of @c verifyPhoneNumber:UIDelegate:completion: which results in an
     exception.
     */
    func SKIPtestVerifyPhoneNumberUIDelegateRaiseException() async throws {
      initApp(#function)
      let auth = try XCTUnwrap(PhoneAuthProviderTests.auth)
      auth.mainBundleUrlTypes = [["CFBundleURLSchemes": ["fail"]]]
      let provider = PhoneAuthProvider.provider(auth: auth)
      provider.verifyPhoneNumber(kTestPhoneNumber, uiDelegate: nil) { verificationID, error in
        XCTFail("Should not call completion")
      }
    }

    /** @fn testNotForwardingNotification
     @brief Tests returning an error for the app failing to forward notification.
     */
    func testNotForwardingNotification() throws {
      func testVerifyPhoneNumberUIDelegateUnstructuredError() async throws {
        try await internalTestVerify(
          errorURLString: PhoneAuthProviderTests.kFakeRedirectURLStringUnstructuredError,
          errorCode: AuthErrorCode.appVerificationUserInteractionFailure.rawValue,
          function: #function,
          useClientID: true,
          reCAPTCHAfallback: true,
          forwardingNotification: false
        )
      }
    }

    /** @fn testMissingAPNSToken
     @brief Tests returning an error for the app failing to provide an APNS device token.
     */
    func testMissingAPNSToken() async throws {
      try await internalTestVerify(
        errorCode: AuthErrorCode.missingAppToken.rawValue,
        function: #function,
        useClientID: true,
        reCAPTCHAfallback: true,
        presenterError: NSError(
          domain: AuthErrors.domain,
          code: AuthErrorCode.missingAppToken.rawValue
        )
      )
    }

    /** @fn testVerifyPhoneNumberUIDelegateiOSSecretMissingFlow
        @brief Tests a successful invocation of @c verifyPhoneNumber:UIDelegate:completion: that falls
       back to the reCAPTCHA flow when the push notification is not received before the timeout.
     */
    func testVerifyPhoneNumberUIDelegateiOSSecretMissingFlow() throws {
      try internalFlow(function: #function, useClientID: false, reCAPTCHAfallback: true)
    }

    /** @fn testVerifyClient
     @brief Tests verifying client before sending verification code.
     */
    func testVerifyClient() throws {
      try internalFlow(function: #function, useClientID: true, reCAPTCHAfallback: false)
    }

    /** @fn testSendVerificationCodeFailedRetry
     @brief Tests failed retry after failing to send verification code.
     */
    func testSendVerificationCodeFailedRetry() throws {
      try internalFlowRetry(function: #function)
    }

    /** @fn testSendVerificationCodeSuccessfulRetry
     @brief Tests successful retry after failing to send verification code.
     */
    func testSendVerificationCodeSuccessfulRetry() throws {
      try internalFlowRetry(function: #function, goodRetry: true)
    }

    /** @fn testPhoneAuthCredentialCoding
     @brief Tests successful archiving and unarchiving of @c PhoneAuthCredential.
     */
    func testPhoneAuthCredentialCoding() throws {
      let kVerificationID = "My verificationID"
      let kVerificationCode = "1234"
      let credential = PhoneAuthCredential(withProviderID: PhoneAuthProvider.id,
                                           verificationID: kVerificationID,
                                           verificationCode: kVerificationCode)
      XCTAssertTrue(PhoneAuthCredential.supportsSecureCoding)
      let data = try NSKeyedArchiver.archivedData(
        withRootObject: credential,
        requiringSecureCoding: true
      )
      let unarchivedCredential = try XCTUnwrap(NSKeyedUnarchiver.unarchivedObject(
        ofClass: PhoneAuthCredential.self, from: data
      ))
      switch unarchivedCredential.credentialKind {
      case .phoneNumber: XCTFail("Should be verification case")
      case let .verification(id, code):
        XCTAssertEqual(id, kVerificationID)
        XCTAssertEqual(code, kVerificationCode)
      }
      XCTAssertEqual(unarchivedCredential.provider, PhoneAuthProvider.id)
    }

    /** @fn testPhoneAuthCredentialCodingPhone
     @brief Tests successful archiving and unarchiving of @c PhoneAuthCredential after other constructor.
     */
    func testPhoneAuthCredentialCodingPhone() throws {
      let kTemporaryProof = "Proof"
      let kPhoneNumber = "123457"
      let credential = PhoneAuthCredential(withTemporaryProof: kTemporaryProof,
                                           phoneNumber: kPhoneNumber,
                                           providerID: PhoneAuthProvider.id)
      XCTAssertTrue(PhoneAuthCredential.supportsSecureCoding)
      let data = try NSKeyedArchiver.archivedData(
        withRootObject: credential,
        requiringSecureCoding: true
      )
      let unarchivedCredential = try XCTUnwrap(NSKeyedUnarchiver.unarchivedObject(
        ofClasses: [PhoneAuthCredential.self, NSString.self], from: data
      ) as? PhoneAuthCredential)
      switch unarchivedCredential.credentialKind {
      case let .phoneNumber(phoneNumber, temporaryProof):
        XCTAssertEqual(temporaryProof, kTemporaryProof)
        XCTAssertEqual(phoneNumber, kPhoneNumber)
      case .verification: XCTFail("Should be phoneNumber case")
      }
      XCTAssertEqual(unarchivedCredential.provider, PhoneAuthProvider.id)
    }

    private func testRecaptchaFlowError(function: String, rceError: Error) async throws {
      initApp(function)
      let auth = try XCTUnwrap(PhoneAuthProviderTests.auth)
      // TODO: Figure out how to mock objective C's FIRRecaptchaGetToken response
      // Mocking the output of verify() method
      let provider = PhoneAuthProvider.provider(auth: auth)
      let mockVerifier = FakeAuthRecaptchaVerifier(error: rceError)
      AuthRecaptchaVerifier.setShared(mockVerifier, auth: auth)
      rpcIssuer.rceMode = "ENFORCE"
      do {
        let _ = try await provider.verifyClAndSendVerificationCodeWithRecaptcha(
          toPhoneNumber: kTestPhoneNumber,
          retryOnInvalidAppCredential: false,
          uiDelegate: nil,
          recaptchaVerifier: mockVerifier
        )
      } catch {
        XCTAssertEqual((error as NSError).code, (rceError as NSError).code)
      }
    }

    private func internalFlowRetry(function: String, goodRetry: Bool = false) throws {
      let function = function
      initApp(function, useClientID: true, fakeToken: true)
      let auth = try XCTUnwrap(PhoneAuthProviderTests.auth)
      let provider = PhoneAuthProvider.provider(auth: auth)
      let expectation = self.expectation(description: function)

      // Fake push notification.
      auth.appCredentialManager?.fakeCredential = AuthAppCredential(
        receipt: kTestReceipt,
        secret: kTestSecret
      )

      // 1. Intercept, handle, and test three RPC calls.

      let verifyClientRequestExpectation = self.expectation(description: "verifyClientRequest")
      verifyClientRequestExpectation.expectedFulfillmentCount = 2
      rpcIssuer?.verifyClientRequester = { request in
        XCTAssertEqual(request.appToken, "21402324255E")
        XCTAssertFalse(request.isSandbox)
        verifyClientRequestExpectation.fulfill()
        do {
          // Response for the underlying VerifyClientRequest RPC call.
          return try self.rpcIssuer.respond(withJSON: [
            "receipt": self.kTestReceipt,
            "suggestedTimeout": self.kTestTimeout,
          ])
        } catch {
          XCTFail("Failure sending response: \(error)")
          return (nil, nil)
        }
      }

      let verifyRequesterExpectation = self.expectation(description: "verifyRequester")
      verifyRequesterExpectation.expectedFulfillmentCount = 2
      var visited = false
      rpcIssuer?.verifyRequester = { request in
        XCTAssertEqual(request.phoneNumber, self.kTestPhoneNumber)
        switch request.codeIdentity {
        case let .credential(credential):
          XCTAssertEqual(credential.receipt, self.kTestReceipt)
          XCTAssertEqual(credential.secret, self.kTestSecret)
        default:
          XCTFail("Should be credential")
        }
        verifyRequesterExpectation.fulfill()
        do {
          if visited == false || goodRetry == false {
            // First Response for the underlying SendVerificationCode RPC call.
            let (data, error) = try self.rpcIssuer
              .respond(serverErrorMessage: "INVALID_APP_CREDENTIAL")
            visited = true
            return (data, error)
          } else {
            // Second Response for the underlying SendVerificationCode RPC call.
            return try self.rpcIssuer
              .respond(withJSON: [self.kVerificationIDKey: self.kTestVerificationID])
          }
        } catch {
          XCTFail("Failure sending response: \(error)")
          return (nil, nil)
        }
      }

      // Use fake authURLPresenter so we can test the parameters that get sent to it.
      PhoneAuthProviderTests.auth?.authURLPresenter =
        FakePresenter(
          urlString: PhoneAuthProviderTests.kFakeRedirectURLStringWithReCAPTCHAToken,
          clientID: PhoneAuthProviderTests.kFakeClientID,
          firebaseAppID: nil,
          errorTest: false,
          presenterError: nil
        )

      // 2. After setting up the fakes and parameters, call `verifyPhoneNumber`.
      provider
        .verifyPhoneNumber(kTestPhoneNumber, uiDelegate: nil) { verificationID, error in

          // 8. After the response triggers the callback in the FakePresenter, verify the callback.
          XCTAssertTrue(Thread.isMainThread)
          if goodRetry {
            XCTAssertNil(error)
            XCTAssertEqual(verificationID, self.kTestVerificationID)
          } else {
            XCTAssertNil(verificationID)
            XCTAssertEqual((error as? NSError)?.code, AuthErrorCode.internalError.rawValue)
          }
          expectation.fulfill()
        }
      waitForExpectations(timeout: 5)
    }

    private func internalFlow(function: String,
                              useClientID: Bool = false,
                              reCAPTCHAfallback: Bool = false) throws {
      let function = function
      initApp(function, useClientID: useClientID, fakeToken: true)
      let auth = try XCTUnwrap(PhoneAuthProviderTests.auth)
      let provider = PhoneAuthProvider.provider(auth: auth)
      let expectation = self.expectation(description: function)

      // Fake push notification.
      auth.appCredentialManager?.fakeCredential = AuthAppCredential(
        receipt: kTestReceipt,
        secret: reCAPTCHAfallback ? nil : kTestSecret
      )

      // 1. Intercept, handle, and test three RPC calls.

      let verifyClientRequestExpectation = self.expectation(description: "verifyClientRequest")
      rpcIssuer?.verifyClientRequester = { request in
        XCTAssertEqual(request.appToken, "21402324255E")
        XCTAssertFalse(request.isSandbox)
        verifyClientRequestExpectation.fulfill()
        do {
          // Response for the underlying VerifyClientRequest RPC call.
          return try self.rpcIssuer.respond(withJSON: [
            "receipt": self.kTestReceipt,
            "suggestedTimeout": self.kTestTimeout,
          ])
        } catch {
          XCTFail("Failure sending response: \(error)")
          return (nil, nil)
        }
      }
      if reCAPTCHAfallback {
        let projectConfigExpectation = self.expectation(description: "projectConfiguration")
        rpcIssuer.projectConfigRequester = { request in
          XCTAssertEqual(request.apiKey, PhoneAuthProviderTests.kFakeAPIKey)
          projectConfigExpectation.fulfill()
          do {
            // Response for the underlying VerifyClientRequest RPC call.
            return try self.rpcIssuer.respond(
              withJSON: ["projectId": "kFakeProjectID",
                         "authorizedDomains": [PhoneAuthProviderTests.kFakeAuthorizedDomain]]
            )
          } catch {
            XCTFail("Failure sending response: \(error)")
            return (nil, nil)
          }
        }
      }

      let verifyRequesterExpectation = self.expectation(description: "verifyRequester")
      rpcIssuer?.verifyRequester = { request in
        XCTAssertEqual(request.phoneNumber, self.kTestPhoneNumber)
        if reCAPTCHAfallback {
          switch request.codeIdentity {
          case let .recaptcha(token):
            XCTAssertEqual(token, self.kFakeReCAPTCHAToken)
          default:
            XCTFail("Should be recaptcha")
          }
        } else {
          switch request.codeIdentity {
          case let .credential(credential):
            XCTAssertEqual(credential.receipt, self.kTestReceipt)
            XCTAssertEqual(credential.secret, self.kTestSecret)
          default:
            XCTFail("Should be credential")
          }
        }
        verifyRequesterExpectation.fulfill()
        do {
          // Response for the underlying SendVerificationCode RPC call.
          return try self.rpcIssuer
            .respond(withJSON: [self.kVerificationIDKey: self.kTestVerificationID])
        } catch {
          XCTFail("Failure sending response: \(error)")
          return (nil, nil)
        }
      }

      // Use fake authURLPresenter so we can test the parameters that get sent to it.
      PhoneAuthProviderTests.auth?.authURLPresenter =
        FakePresenter(
          urlString: PhoneAuthProviderTests.kFakeRedirectURLStringWithReCAPTCHAToken,
          clientID: useClientID ? PhoneAuthProviderTests.kFakeClientID : nil,
          firebaseAppID: useClientID ? nil : PhoneAuthProviderTests.kFakeFirebaseAppID,
          errorTest: false,
          presenterError: nil
        )
      let uiDelegate = reCAPTCHAfallback ? FakeUIDelegate() : nil

      // 2. After setting up the fakes and parameters, call `verifyPhoneNumber`.
      provider
        .verifyPhoneNumber(kTestPhoneNumber, uiDelegate: uiDelegate) { verificationID, error in

          // 8. After the response triggers the callback in the FakePresenter, verify the callback.
          XCTAssertTrue(Thread.isMainThread)
          XCTAssertNil(error)
          XCTAssertEqual(verificationID, self.kTestVerificationID)
          expectation.fulfill()
        }

      waitForExpectations(timeout: 5)
    }

    /** @fn testVerifyClient
        @brief Tests verifying client before sending verification code.
     */
    private func internalTestVerify(errorString: String? = nil,
                                    errorURLString: String? = nil,
                                    errorCode: Int = 0,
                                    function: String,
                                    testMode: Bool = false,
                                    useClientID: Bool = false,
                                    bothClientAndAppID: Bool = false,
                                    reCAPTCHAfallback: Bool = false,
                                    forwardingNotification: Bool = true,
                                    presenterError: Error? = nil) async throws {
      initApp(function, useClientID: useClientID, bothClientAndAppID: bothClientAndAppID,
              testMode: testMode,
              forwardingNotification: forwardingNotification)
      let auth = try XCTUnwrap(PhoneAuthProviderTests.auth)
      let provider = PhoneAuthProvider.provider(auth: auth)
      var expectations: [XCTestExpectation] = []

      if !reCAPTCHAfallback {
        // Fake out appCredentialManager flow.
        auth.appCredentialManager?.credential = AuthAppCredential(receipt: kTestReceipt,
                                                                  secret: kTestSecret)
      } else {
        // 1. Intercept, handle, and test the projectConfiguration RPC calls.
        let projectConfigExpectation = expectation(description: "projectConfiguration")
        expectations.append(projectConfigExpectation)
        rpcIssuer.projectConfigRequester = { request in
          XCTAssertEqual(request.apiKey, PhoneAuthProviderTests.kFakeAPIKey)
          projectConfigExpectation.fulfill()
          do {
            // Response for the underlying VerifyClientRequest RPC call.
            return try self.rpcIssuer.respond(
              withJSON: ["projectId": "kFakeProjectID",
                         "authorizedDomains": [PhoneAuthProviderTests.kFakeAuthorizedDomain]]
            )
          } catch {
            XCTFail("Failure sending response: \(error)")
            return (nil, nil)
          }
        }
      }
      if reCAPTCHAfallback {
        // Use fake authURLPresenter so we can test the parameters that get sent to it.
        let urlString = errorURLString ??
          PhoneAuthProviderTests.kFakeRedirectURLStringWithReCAPTCHAToken
        let errorTest = errorURLString != nil
        PhoneAuthProviderTests.auth?.authURLPresenter =
          FakePresenter(
            urlString: urlString,
            clientID: useClientID ? PhoneAuthProviderTests.kFakeClientID : nil,
            firebaseAppID: useClientID ? nil : PhoneAuthProviderTests.kFakeFirebaseAppID,
            errorTest: errorTest,
            presenterError: presenterError
          )
      }
      if errorURLString == nil, presenterError == nil {
        let requestExpectation = expectation(description: "verifyRequester")
        expectations.append(requestExpectation)
        rpcIssuer.verifyRequester = { request in
          XCTAssertEqual(request.phoneNumber, self.kTestPhoneNumber)
          switch request.codeIdentity {
          case let .credential(credential):
            XCTAssertFalse(reCAPTCHAfallback)
            XCTAssertEqual(credential.receipt, self.kTestReceipt)
            XCTAssertEqual(credential.secret, self.kTestSecret)
          case let .recaptcha(token):
            XCTAssertTrue(reCAPTCHAfallback)
            XCTAssertEqual(token, self.kFakeReCAPTCHAToken)
          case .empty:
            XCTAssertTrue(testMode)
          }
          requestExpectation.fulfill()
          do {
            // Response for the underlying SendVerificationCode RPC call.
            if let errorString {
              return try self.rpcIssuer.respond(serverErrorMessage: errorString)
            } else {
              return try self.rpcIssuer
                .respond(withJSON: [self.kVerificationIDKey: self.kTestVerificationID])
            }
          } catch {
            XCTFail("Failure sending response: \(error)")
            return (nil, nil)
          }
        }
      }
      let uiDelegate = reCAPTCHAfallback ? FakeUIDelegate() : nil
      // 2. After setting up the parameters, call `verifyPhoneNumber`.
      do {
        // Call the async function to verify the phone number
        let verificationID = try await provider.verifyPhoneNumber(
          kTestPhoneNumber,
          uiDelegate: uiDelegate
        )
        // Assert that the verificationID matches the expected value
        XCTAssertEqual(verificationID, kTestVerificationID)
      } catch {
        // If an error occurs, assert that verificationID is nil and the error code matches the
        // expected value
        XCTAssertEqual((error as NSError).code, errorCode)
      }
      await fulfillment(of: expectations, timeout: 5.0)
    }

    private func initApp(_ functionName: String,
                         useClientID: Bool = false,
                         bothClientAndAppID: Bool = false,
                         testMode: Bool = false,
                         forwardingNotification: Bool = true,
                         fakeToken: Bool = false) {
      let options = FirebaseOptions(googleAppID: "0:0000000000000:ios:0000000000000000",
                                    gcmSenderID: "00000000000000000-00000000000-000000000")
      options.apiKey = PhoneAuthProviderTests.kFakeAPIKey
      options.projectID = "myProjectID"
      if useClientID {
        options.clientID = PhoneAuthProviderTests.kFakeClientID
      }
      if !useClientID || bothClientAndAppID {
        // Use the appID.
        options.googleAppID = PhoneAuthProviderTests.kFakeFirebaseAppID
      }
      let scheme = useClientID ? PhoneAuthProviderTests.kFakeReverseClientID :
        PhoneAuthProviderTests.kFakeEncodedFirebaseAppID

      let strippedName = functionName.replacingOccurrences(of: "(", with: "")
        .replacingOccurrences(of: ")", with: "")
      FirebaseApp.configure(name: strippedName, options: options)
      let auth = Auth(app: FirebaseApp.app(name: strippedName)!, backend: authBackend)

      kAuthGlobalWorkQueue.sync {
        // Wait for Auth protectedDataInitialization to finish.
        PhoneAuthProviderTests.auth = auth
        if testMode {
          // Disable app verification.
          let settings = AuthSettings()
          settings.appVerificationDisabledForTesting = true
          auth.settings = settings
        }
        auth.notificationManager?.immediateCallbackForTestFaking = { forwardingNotification }
        auth.mainBundleUrlTypes = [["CFBundleURLSchemes": [scheme]]]

        if fakeToken {
          guard let data = "!@#$%^".data(using: .utf8) else {
            XCTFail("Failed to encode data for fake token")
            return
          }
          auth.tokenManager?.tokenStore = AuthAPNSToken(withData: data, type: .prod)
        } else {
          // Skip APNS token fetching.
          auth.tokenManager = FakeTokenManager(withApplication: UIApplication.shared)
        }
      }
    }

    class FakeAuthRecaptchaVerifier: AuthRecaptchaVerifier, @unchecked Sendable {
      var captchaResponse: String
      var error: Error?
      init(captchaResponse: String? = nil, error: Error? = nil) {
        self.captchaResponse = captchaResponse ?? "NO_RECAPTCHA"
        self.error = error
        super.init()
      }

      override func verify(forceRefresh: Bool, action: AuthRecaptchaAction) async throws -> String {
        if let error = error {
          throw error
        }
        return captchaResponse
      }
    }

    class FakeTokenManager: AuthAPNSTokenManager, @unchecked Sendable {
      override func getTokenInternal(callback: @escaping (Result<AuthAPNSToken, Error>) -> Void) {
        let error = NSError(domain: "dummy domain", code: AuthErrorCode.missingAppToken.rawValue)
        callback(.failure(error))
      }
    }

    class FakePresenter: NSObject, AuthWebViewControllerDelegate {
      func webViewController(_ webViewController: AuthWebViewController,
                             canHandle URL: URL) -> Bool {
        XCTFail("Do not call")
        return false
      }

      func webViewControllerDidCancel(_ webViewController: AuthWebViewController) {
        XCTFail("Do not call")
      }

      func webViewController(_ webViewController: AuthWebViewController,
                             didFailWithError error: Error) {
        XCTFail("Do not call")
      }

      func present(_ presentURL: URL,
                   uiDelegate UIDelegate: AuthUIDelegate?,
                   callbackMatcher: @escaping (URL?) -> Bool,
                   completion: @escaping (URL?, Error?) -> Void) {
        // 5. Verify flow triggers present in the FakePresenter class with the right parameters.
        XCTAssertEqual(presentURL.scheme, "https")
        XCTAssertEqual(presentURL.host, kFakeAuthorizedDomain)
        XCTAssertEqual(presentURL.path, "/__/auth/handler")

        let actualURLComponents = URLComponents(url: presentURL, resolvingAgainstBaseURL: false)
        guard let _ = actualURLComponents?.queryItems else {
          XCTFail("Failed to get queryItems")
          return
        }
        let params = AuthWebUtils.dictionary(withHttpArgumentsString: presentURL.query)
        XCTAssertEqual(params["ibi"], Bundle.main.bundleIdentifier)
        XCTAssertEqual(params["apiKey"], PhoneAuthProviderTests.kFakeAPIKey)
        XCTAssertEqual(params["authType"], "verifyApp")
        XCTAssertNotNil(params["v"])
        if OAuthProviderTests.testTenantID {
          XCTAssertEqual(params["tid"], OAuthProviderTests.kFakeTenantID)
        } else {
          XCTAssertNil(params["tid"])
        }
        let appCheckToken = presentURL.fragment
        let verifyAppCheckToken = OAuthProviderTests.testAppCheck ? "fac=fakeAppCheckToken" : nil
        XCTAssertEqual(appCheckToken, verifyAppCheckToken)

        var redirectURL = ""
        if let clientID {
          XCTAssertEqual(params["clientId"], clientID)
          redirectURL = "\(kFakeReverseClientID)\(urlString)"
        }
        if let firebaseAppID {
          XCTAssertEqual(params["appId"], firebaseAppID)
          redirectURL = "\(kFakeEncodedFirebaseAppID)\(urlString)"
        }

        // 6. Test callbackMatcher
        // Verify that the URL is rejected by the callback matcher without the event ID.
        XCTAssertFalse(callbackMatcher(URL(string: "\(redirectURL)")))

        // Verify that the URL is accepted by the callback matcher with the matching event ID.
        guard let eventID = params["eventId"] else {
          XCTFail("Failed to get eventID")
          return
        }
        let redirectWithEventID = "\(redirectURL)%26eventId%3D\(eventID)"
        let originalComponents = URLComponents(string: redirectWithEventID)!
        XCTAssertEqual(callbackMatcher(originalComponents.url), !errorTest)

        var components = originalComponents
        components.query = "https"
        XCTAssertFalse(callbackMatcher(components.url))

        components = originalComponents
        components.host = "badhost"
        XCTAssertFalse(callbackMatcher(components.url))

        components = originalComponents
        components.path = "badpath"
        XCTAssertFalse(callbackMatcher(components.url))

        components = originalComponents
        components.query = "badquery"
        XCTAssertFalse(callbackMatcher(components.url))

        // 7. Do the callback to the original call.
        kAuthGlobalWorkQueue.async {
          if let presenterError = self.presenterError {
            completion(nil, presenterError)
          } else {
            completion(URL(string: "\(kFakeEncodedFirebaseAppID)\(self.urlString)") ?? nil, nil)
          }
        }
      }

      let urlString: String
      let clientID: String?
      let firebaseAppID: String?
      let errorTest: Bool
      let presenterError: Error?

      init(urlString: String, clientID: String?, firebaseAppID: String?, errorTest: Bool,
           presenterError: Error?) {
        self.urlString = urlString
        self.clientID = clientID
        self.firebaseAppID = firebaseAppID
        self.errorTest = errorTest
        self.presenterError = presenterError
      }
    }

    private class FakeUIDelegate: NSObject, AuthUIDelegate {
      func present(_ viewControllerToPresent: UIViewController, animated flag: Bool,
                   completion: (() -> Void)? = nil) {
        guard let safariController = viewControllerToPresent as? SFSafariViewController,
              let delegate = safariController.delegate as? AuthURLPresenter,
              let uiDelegate = delegate.uiDelegate as? FakeUIDelegate else {
          XCTFail("Failed to get presentURL from controller")
          return
        }
        XCTAssertEqual(self, uiDelegate)
      }

      func dismiss(animated flag: Bool, completion: (() -> Void)? = nil) {
        XCTFail("Implement me")
      }
    }

    private static let kFakeRedirectURLStringInvalidClientID =
      "//firebaseauth/" +
      "link?deep_link_id=https%3A%2F%2Fexample.firebaseapp.com%2F__%2Fauth%2Fcal" +
      "lback%3FfirebaseError%3D%257B%2522code%2522%253A%2522auth%252Finvalid-oauth-client-id%2522%252" +
      "C%2522message%2522%253A%2522The%2520OAuth%2520client%2520ID%2520provided%2520is%2520either%252" +
      "0invalid%2520or%2520does%2520not%2520match%2520the%2520specified%2520API%2520key.%2522%257D%26" +
      "authType%3DverifyApp"

    private static let kFakeRedirectURLStringWebNetworkRequestFailed =
      "//firebaseauth/link?deep_link_id=https%3A%2F%2Fexample.firebaseapp.com%2F__%2Fauth%2Fc" +
      "allback%3FfirebaseError%3D%257B%2522code%2522%253A%2522auth%252Fnetwork-request-failed%2522%" +
      "252C%2522message%2522%253A%2522The%2520network%2520request%2520failed%2520.%2522%257D%" +
      "26authType%3DverifyApp"

    private static let kFakeRedirectURLStringWebInternalError =
      "//firebaseauth/link?deep_link_id=https%3A%2F%2Fexample.firebaseapp.com%2F__%2Fauth%2Fcal" +
      "lback%3FfirebaseError%3D%257B%2522code%2522%253A%2522auth%252Finternal-error%2522%252C%" +
      "2522message%2522%253A%2522Internal%2520error%2520.%2522%257D%26authType%3DverifyApp"

    private static let kFakeRedirectURLStringUnknownError =
      "//firebaseauth/link?deep_link_id=https%3A%2F%2Fexample.firebaseapp.com%2F__%2Fauth%2Fcal" +
      "lback%3FfirebaseError%3D%257B%2522code%2522%253A%2522auth%252Funknown-error-id%2522%252" +
      "C%2522message%2522%253A%2522The%2520OAuth%2520client%2520ID%2520provided%2520is%2520either%252" +
      "0invalid%2520or%2520does%2520not%2520match%2520the%2520specified%2520API%2520key.%2522%257D%26" +
      "authType%3DverifyApp"

    private static let kFakeRedirectURLStringUnstructuredError =
      "//firebaseauth/link?deep_link_id=https%3A%2F%2Fexample.firebaseapp.com%2F__%2Fauth%2Fcal" +
      "lback%3FfirebaseError%3D%257B%2522unstructuredcode%2522%253A%2522auth%252Funknown-error-id%" +
      "2522%252" +
      "C%2522unstructuredmessage%2522%253A%2522The%2520OAuth%2520client%2520ID%2520provided%2520is%" +
      "2520either%252" +
      "0invalid%2520or%2520does%2520not%2520match%2520the%2520specified%2520API%2520key.%2522%257D%" +
      "26authType%3DverifyApp"

    private static let kFakeRedirectURLStringWithReCAPTCHAToken =
      "://firebaseauth/" +
      "link?deep_link_id=https%3A%2F%2Fexample.firebaseapp.com%2F__%2Fauth%2Fcallback%3FauthType%" +
      "3DverifyApp%26recaptchaToken%3DfakeReCAPTCHAToken"
  }
#endif
