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

@testable import FirebaseFunctions

import XCTest

final class FunctionsErrorTests: XCTestCase {
  func testInitWithCode() {
    let error = FunctionsError(.permissionDenied)

    let nsError = error as NSError
    XCTAssertEqual(nsError.domain, "com.firebase.functions")
    XCTAssertEqual(nsError.code, 7)
    XCTAssertEqual(nsError.localizedDescription, "PERMISSION DENIED")
    XCTAssertEqual(nsError.userInfo.count, 1)
  }

  func testInitWithCodeAndUserInfo() {
    let error = FunctionsError(.unimplemented, userInfo: ["TEST_Key": "TEST_Value"])

    let nsError = error as NSError
    XCTAssertEqual(nsError.domain, "com.firebase.functions")
    XCTAssertEqual(nsError.code, 12)
    XCTAssertEqual(
      nsError.localizedDescription,
      "The operation couldn’t be completed. (com.firebase.functions error 12.)"
    )
    XCTAssertEqual(nsError.userInfo.count, 1)
    XCTAssertEqual(nsError.userInfo["TEST_Key"] as? String, "TEST_Value")
  }

  func testInitWithOKStatusCodeAndNoErrorBody() {
    // The error should be `nil`.
    let error = FunctionsError(
      httpStatusCode: 200,
      region: "my-region",
      url: URL(string: "https://example.com/fake_func")!,
      body: nil,
      serializer: FunctionsSerializer()
    )

    XCTAssertNil(error)
  }

  func testInitWithErrorStatusCodeAndNoErrorBody() {
    // The error should be inferred from the HTTP status code.
    let error = FunctionsError(
      httpStatusCode: 429,
      region: "my-region",
      url: URL(string: "https://example.com/fake_func")!,
      body: nil,
      serializer: FunctionsSerializer()
    )

    guard let error else { return XCTFail("Unexpected `nil` value") }

    let nsError = error as NSError
    XCTAssertEqual(nsError.domain, "com.firebase.functions")
    XCTAssertEqual(nsError.code, 8)
    XCTAssertEqual(nsError.localizedDescription, "RESOURCE EXHAUSTED")
    XCTAssertEqual(nsError.userInfo.count, 3)
  }

  func testInitWithOKStatusCodeAndIncompleteErrorBody() {
    // The status code in the error body takes precedence over the HTTP status code.
    let responseData = #"{ "error": { "status": "OUT_OF_RANGE" } }"#.data(using: .utf8)!

    let error = FunctionsError(
      httpStatusCode: 200,
      region: "my-region",
      url: URL(string: "https://example.com/fake_func")!,
      body: responseData,
      serializer: FunctionsSerializer()
    )

    guard let error else { return XCTFail("Unexpected `nil` value") }

    let nsError = error as NSError
    XCTAssertEqual(nsError.domain, "com.firebase.functions")
    XCTAssertEqual(nsError.code, 11)
    XCTAssertEqual(nsError.localizedDescription, "OUT OF RANGE")
    XCTAssertEqual(nsError.userInfo.count, 3)
  }

  func testInitWithErrorStatusCodeAndErrorBody() {
    // The status code in the error body takes precedence over the HTTP status code.
    let responseData =
      #"{ "error": { "status": "OUT_OF_RANGE", "message": "TEST_ErrorMessage", "details": 123 } }"#
        .data(using: .utf8)!

    let error = FunctionsError(
      httpStatusCode: 499,
      region: "my-region",
      url: URL(string: "https://example.com/fake_func")!,
      body: responseData,
      serializer: FunctionsSerializer()
    )

    guard let error else { return XCTFail("Unexpected `nil` value") }

    let nsError = error as NSError
    XCTAssertEqual(nsError.domain, "com.firebase.functions")
    XCTAssertEqual(nsError.code, 11)
    XCTAssertEqual(nsError.localizedDescription, "TEST_ErrorMessage")
    XCTAssertEqual(nsError.userInfo.count, 4)
    XCTAssertEqual(nsError.userInfo["details"] as? Int, 123)
  }

  func testInitWithErrorStatusCodeAndOKErrorBody() {
    // When the status code in the error body is `OK`, error should be `nil` regardless of the HTTP
    // status code.
    let responseData =
      #"{ "error": { "status": "OK", "message": "TEST_ErrorMessage", "details": 123 } }"#
        .data(using: .utf8)!

    let error = FunctionsError(
      httpStatusCode: 401,
      region: "my-region",
      url: URL(string: "https://example.com/fake_func")!,
      body: responseData,
      serializer: FunctionsSerializer()
    )

    XCTAssertNil(error)
  }

  func testInitWithErrorStatusCodeAndIncompleteErrorBody() {
    // The error name is not in the body; it should be inferred from the HTTP status code.
    let responseData = #"{ "error": { "message": "TEST_ErrorMessage", "details": null } }"#
      .data(using: .utf8)!

    let error = FunctionsError(
      httpStatusCode: 403,
      region: "my-region",
      url: URL(string: "https://example.com/fake_func")!,
      body: responseData,
      serializer: FunctionsSerializer()
    )

    guard let error else { return XCTFail("Unexpected `nil` value") }

    let nsError = error as NSError
    XCTAssertEqual(nsError.domain, "com.firebase.functions")
    XCTAssertEqual(nsError.code, 7) // `permissionDenied`, inferred from the HTTP status code
    XCTAssertEqual(nsError.localizedDescription, "TEST_ErrorMessage")
    XCTAssertEqual(nsError.userInfo.count, 4)
    XCTAssertEqual(nsError.userInfo["details"] as? NSNull, NSNull())
  }

  func testInitWithErrorStatusCodeAndInvalidErrorBody() {
    // An unsupported status code in the error body should result in the rest of the body ignored.
    let responseData =
      #"{ "error": { "status": "TEST_UNKNOWN_ERROR", "message": "TEST_ErrorMessage", "details": 123 } }"#
        .data(using: .utf8)!

    let error = FunctionsError(
      httpStatusCode: 503,
      region: "my-region",
      url: URL(string: "https://example.com/fake_func")!,
      body: responseData,
      serializer: FunctionsSerializer()
    )

    guard let error else { return XCTFail("Unexpected `nil` value") }

    let nsError = error as NSError
    XCTAssertEqual(nsError.domain, "com.firebase.functions")
    // Currently, `internal` is used as the fallback error code. Is this correct?
    // Seems like we could get more information from the HTTP status code in such cases.
    XCTAssertEqual(nsError.code, 13)
    XCTAssertEqual(nsError.localizedDescription, "INTERNAL")
    XCTAssertEqual(nsError.userInfo.count, 1)
  }
}
