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

import Foundation
import XCTest

@testable import FirebaseAuth

@available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *)
class GetOOBConfirmationCodeTests: RPCBaseTests {
  private let kRequestTypeKey = "requestType"
  private let kPasswordResetRequestTypeValue = "PASSWORD_RESET"
  private let kVerifyEmailRequestTypeValue = "VERIFY_EMAIL"
  private let kEmailLinkSignInTypeValue = "EMAIL_SIGNIN"
  private let kEmailKey = "email"
  private let kTestEmail = "testgmail.com"
  private let kAccessTokenKey = "idToken"
  private let kTestAccessToken = "ACCESS_TOKEN"
  private let kContinueURLKey = "continueUrl"
  private let kIosBundleIDKey = "iOSBundleId"
  private let kAndroidPackageNameKey = "androidPackageName"
  private let kAndroidInstallAppKey = "androidInstallApp"
  private let kAndroidMinimumVersionKey = "androidMinimumVersion"
  private let kCanHandleCodeInAppKey = "canHandleCodeInApp"
  private let kLinkDomainKey = "linkDomain"
  private let kExpectedAPIURL =
    "https://www.googleapis.com/identitytoolkit/v3/relyingparty/getOobConfirmationCode?key=APIKey"
  private let kOOBCodeKey = "oobCode"
  private let kTestOOBCode = "OOBCode"

  func testOobRequests() async throws {
    for (request, requestType) in [
      (getPasswordResetRequest, kPasswordResetRequestTypeValue),
      (getSignInWithEmailRequest, kEmailLinkSignInTypeValue),
      (getEmailVerificationRequest, kVerifyEmailRequestTypeValue),
    ] {
      let request = try request()
      try await checkRequest(
        request: request,
        expected: kExpectedAPIURL,
        key: "should_be_empty_dictionary",
        value: nil
      )
      let decodedRequest = try XCTUnwrap(rpcIssuer.decodedRequest)
      XCTAssertEqual(decodedRequest[kRequestTypeKey] as? String, requestType)
      if requestType == kVerifyEmailRequestTypeValue {
        XCTAssertEqual(decodedRequest[kAccessTokenKey] as? String, kTestAccessToken)
      } else {
        XCTAssertEqual(decodedRequest[kEmailKey] as? String, kTestEmail)
      }
      XCTAssertEqual(decodedRequest[kContinueURLKey] as? String, kContinueURL)
      XCTAssertEqual(decodedRequest[kIosBundleIDKey] as? String, kIosBundleID)
      XCTAssertEqual(decodedRequest[kAndroidPackageNameKey] as? String, kAndroidPackageName)
      XCTAssertEqual(decodedRequest[kAndroidMinimumVersionKey] as? String, kAndroidMinimumVersion)
      XCTAssertEqual(decodedRequest[kAndroidInstallAppKey] as? Bool, true)
      XCTAssertEqual(decodedRequest[kCanHandleCodeInAppKey] as? Bool, true)
      XCTAssertEqual(decodedRequest[kLinkDomainKey] as? String, kLinkDomain)
    }
  }

  /** @fn testPasswordResetRequestOptionalFields
      @brief Tests the encoding of a password reset request with optional fields.
   */
  func testPasswordResetRequestOptionalFields() async throws {
    let kCaptchaResponseKey = "captchaResp"
    let kTestCaptchaResponse = "testCaptchaResponse"
    let kClientTypeKey = "clientType"
    let kTestClientType = "testClientType"
    let kRecaptchaVersionKey = "recaptchaVersion"
    let kTestRecaptchaVersion = "testRecaptchaVersion"

    for (request, requestType) in [
      (getPasswordResetRequest, kPasswordResetRequestTypeValue),
      (getSignInWithEmailRequest, kEmailLinkSignInTypeValue),
      (getEmailVerificationRequest, kVerifyEmailRequestTypeValue),
    ] {
      let request = try request()
      request.captchaResponse = kTestCaptchaResponse
      request.clientType = kTestClientType
      request.recaptchaVersion = kTestRecaptchaVersion

      try await checkRequest(
        request: request,
        expected: kExpectedAPIURL,
        key: "should_be_empty_dictionary",
        value: nil
      )
      let decodedRequest = try XCTUnwrap(rpcIssuer.decodedRequest)
      XCTAssertEqual(decodedRequest[kRequestTypeKey] as? String, requestType)
      if requestType == kVerifyEmailRequestTypeValue {
        XCTAssertEqual(decodedRequest[kAccessTokenKey] as? String, kTestAccessToken)
      } else {
        XCTAssertEqual(decodedRequest[kEmailKey] as? String, kTestEmail)
      }
      XCTAssertEqual(decodedRequest[kContinueURLKey] as? String, kContinueURL)
      XCTAssertEqual(decodedRequest[kIosBundleIDKey] as? String, kIosBundleID)
      XCTAssertEqual(decodedRequest[kAndroidPackageNameKey] as? String, kAndroidPackageName)
      XCTAssertEqual(decodedRequest[kAndroidMinimumVersionKey] as? String, kAndroidMinimumVersion)
      XCTAssertEqual(decodedRequest[kAndroidInstallAppKey] as? Bool, true)
      XCTAssertEqual(decodedRequest[kCanHandleCodeInAppKey] as? Bool, true)
      XCTAssertEqual(decodedRequest[kLinkDomainKey] as? String, kLinkDomain)
      XCTAssertEqual(decodedRequest[kCaptchaResponseKey] as? String, kTestCaptchaResponse)
      XCTAssertEqual(decodedRequest[kClientTypeKey] as? String, kTestClientType)
      XCTAssertEqual(decodedRequest[kRecaptchaVersionKey] as? String, kTestRecaptchaVersion)
    }
  }

  func testGetOOBConfirmationCodeErrors() async throws {
    let kEmailNotFoundMessage = "EMAIL_NOT_FOUND: fake custom message"
    let kMissingEmailErrorMessage = "MISSING_EMAIL"
    let kInvalidEmailErrorMessage = "INVALID_EMAIL:"
    let kInvalidMessagePayloadErrorMessage = "INVALID_MESSAGE_PAYLOAD"
    let kInvalidSenderErrorMessage = "INVALID_SENDER"
    let kMissingIosBundleIDErrorMessage = "MISSING_IOS_BUNDLE_ID"
    let kMissingAndroidPackageNameErrorMessage = "MISSING_ANDROID_PACKAGE_NAME"
    let kUnauthorizedDomainErrorMessage = "UNAUTHORIZED_DOMAIN"
    let kInvalidRecipientEmailErrorMessage = "INVALID_RECIPIENT_EMAIL"
    let kInvalidContinueURIErrorMessage = "INVALID_CONTINUE_URI"
    let kMissingContinueURIErrorMessage = "MISSING_CONTINUE_URI"

    try await checkBackendError(
      request: getPasswordResetRequest(),
      message: kEmailNotFoundMessage,
      errorCode: AuthErrorCode.userNotFound
    )
    try await checkBackendError(
      request: getEmailVerificationRequest(),
      message: kMissingEmailErrorMessage,
      errorCode: AuthErrorCode.missingEmail
    )
    try await checkBackendError(
      request: getPasswordResetRequest(),
      message: kInvalidEmailErrorMessage,
      errorCode: AuthErrorCode.invalidEmail
    )
    try await checkBackendError(
      request: getPasswordResetRequest(),
      message: kInvalidMessagePayloadErrorMessage,
      errorCode: AuthErrorCode.invalidMessagePayload
    )
    try await checkBackendError(
      request: getPasswordResetRequest(),
      message: kInvalidSenderErrorMessage,
      errorCode: AuthErrorCode.invalidSender
    )
    try await checkBackendError(
      request: getPasswordResetRequest(),
      message: kMissingIosBundleIDErrorMessage,
      errorCode: AuthErrorCode.missingIosBundleID
    )
    try await checkBackendError(
      request: getPasswordResetRequest(),
      message: kMissingAndroidPackageNameErrorMessage,
      errorCode: AuthErrorCode.missingAndroidPackageName
    )
    try await checkBackendError(
      request: getPasswordResetRequest(),
      message: kUnauthorizedDomainErrorMessage,
      errorCode: AuthErrorCode.unauthorizedDomain
    )
    try await checkBackendError(
      request: getPasswordResetRequest(),
      message: kInvalidRecipientEmailErrorMessage,
      errorCode: AuthErrorCode.invalidRecipientEmail
    )
    try await checkBackendError(
      request: getPasswordResetRequest(),
      message: kInvalidContinueURIErrorMessage,
      errorCode: AuthErrorCode.invalidContinueURI
    )
    try await checkBackendError(
      request: getPasswordResetRequest(),
      message: kMissingContinueURIErrorMessage,
      errorCode: AuthErrorCode.missingContinueURI
    )
  }

  /** @fn testSuccessfulPasswordResetResponse
      @brief This test simulates a complete password reset response (with OOB Code) and makes sure
          it succeeds, and we get the OOB Code decoded correctly.
   */
  func testSuccessfulOOBResponse() async throws {
    let rpcIssuer = try XCTUnwrap(self.rpcIssuer)
    for request in [
      getPasswordResetRequest,
      getSignInWithEmailRequest,
      getEmailVerificationRequest,
    ] {
      rpcIssuer.respondBlock = {
        try self.rpcIssuer.respond(withJSON: [self.kOOBCodeKey: self.kTestOOBCode])
      }
      let response = try await authBackend.call(with: request())
      XCTAssertEqual(response.OOBCode, kTestOOBCode)
    }
  }

  /** @fn testSuccessfulPasswordResetResponseWithoutOOBCode
      @brief This test simulates a password reset request where we don't receive the optional OOBCode
          response value. It should still succeed.
   */
  func testSuccessfulOOBResponseWithoutOOBCode() async throws {
    let rpcIssuer = try XCTUnwrap(self.rpcIssuer)
    for request in [
      getPasswordResetRequest,
      getSignInWithEmailRequest,
      getEmailVerificationRequest,
    ] {
      rpcIssuer.respondBlock = {
        try self.rpcIssuer.respond(withJSON: [:])
      }
      let response = try await authBackend.call(with: request())
      XCTAssertNil(response.OOBCode)
    }
  }

  private func getPasswordResetRequest() throws -> GetOOBConfirmationCodeRequest {
    return try XCTUnwrap(GetOOBConfirmationCodeRequest.passwordResetRequest(
      email: kTestEmail,
      actionCodeSettings: fakeActionCodeSettings(),
      requestConfiguration: makeRequestConfiguration()
    ))
  }

  private func getSignInWithEmailRequest() throws -> GetOOBConfirmationCodeRequest {
    return try XCTUnwrap(GetOOBConfirmationCodeRequest.signInWithEmailLinkRequest(
      kTestEmail,
      actionCodeSettings: fakeActionCodeSettings(),
      requestConfiguration: makeRequestConfiguration()
    ))
  }

  private func getEmailVerificationRequest() throws -> GetOOBConfirmationCodeRequest {
    return try XCTUnwrap(GetOOBConfirmationCodeRequest
      .verifyEmailRequest(accessToken: kTestAccessToken,
                          actionCodeSettings: fakeActionCodeSettings(),
                          requestConfiguration: makeRequestConfiguration()))
  }
}
