///|
/// `Client` represents an authorized b2 client.
struct Client {
  /// The identifier for the account.
  account_id : String // `json:"accountId"`
  /// A data structure that groups the information you need by API suite.
  api_info : APIInfo // `json:"apiInfo"`
  /// An authorization token to use with all calls, other than b2_authorize_account, that need an Authorization header. This authorization token is valid for at most 24 hours.
  authorization_token : String // `json:"authorizationToken"`
  /// Expiration timestamp for the application key.
  application_key_expiration_timestamp : Int? // `json:"applicationKeyExpirationTimestamp,omitempty"`
  /// This client is used to interact with this bucket.
  bucket_id : String
  bucket_name : String
} derive(Debug)

///|
fn Client::from_json(
  body : String,
  bucket_id : String,
  bucket_name : String,
) -> Client raise ClientError {
  let json = @json.parse(body) catch { e => raise ClientError(e.to_string()) }
  //
  match json {
    {
      "accountId": String(account_id),
      "apiInfo": Object(api_info),
      "authorizationToken": String(authorization_token),
      "applicationKeyExpirationTimestamp": application_key_expiration_timestamp,
      ..
    } => {
      let application_key_expiration_timestamp = match
        application_key_expiration_timestamp {
        Number(v, ..) => Some(v.to_int())
        _ => None
      }
      let api_info = APIInfo::from_json(api_info)
      {
        account_id,
        api_info,
        authorization_token,
        application_key_expiration_timestamp,
        bucket_id,
        bucket_name,
      }
    }
    _ => raise ClientError("unable to parse Client: \{@debug.to_string(json)}")
  }
}

///|
test "Client::from_json - invalid apiInfo structure" {
  let invalid_apiInfo_json =
    #|{
    #|  "accountId": "12345",
    #|  "apiInfo": "invalidApiInfo",
    #|  "authorizationToken": "auth_token",
    #|  "applicationKeyExpirationTimestamp": 1672531200
    #|}
  try {
    let client = Client::from_json(
      invalid_apiInfo_json, "some-bucket-id", "some-bucket-name",
    )
    raise ClientError("expected failure, but got: \{@debug.to_string(client)}")
  } catch {
    ClientError(e) =>
      assert_eq(
        e,
        (
          #|unable to parse Client: Object(
          #|  {
          #|    "accountId": String("12345"),
          #|    "apiInfo": String("invalidApiInfo"),
          #|    "authorizationToken": String("auth_token"),
          #|    "applicationKeyExpirationTimestamp": Number(1672531200),
          #|  },
          #|)
        ),
      )
  }
}

///|
test "Client::from_json - partial valid JSON input" {
  let partial_valid_json =
    #|{
    #|  "accountId": "12345",
    #|  "apiInfo": {
    #|    "storageApi": {
    #|      "absoluteMinimumPartSize": 5000000,
    #|      "apiUrl": "https://apiNNN.backblazeb2.com"
    #|    }
    #|  },
    #|  "authorizationToken": "auth_token"
    #|}
  try {
    let client = Client::from_json(
      partial_valid_json, "some-bucket-id", "some-bucket-name",
    )
    raise ClientError("expected failure, but got: \{@debug.to_string(client)}")
  } catch {
    ClientError(e) =>
      assert_eq(
        e,
        (
          #|unable to parse Client: Object(
          #|  {
          #|    "accountId": String("12345"),
          #|    "apiInfo": Object(
          #|      {
          #|        "storageApi": Object(
          #|          {
          #|            "absoluteMinimumPartSize": Number(5000000),
          #|            "apiUrl": String("https://apiNNN.backblazeb2.com"),
          #|          },
          #|        ),
          #|      },
          #|    ),
          #|    "authorizationToken": String("auth_token"),
          #|  },
          #|)
        ),
      )
  }
}

///|
test "Client::from_json - random JSON input" {
  let random_json =
    #|{
    #|  "randomField": "randomValue",
    #|  "anotherRandomField": 12345
    #|}
  try {
    let client = Client::from_json(
      random_json, "some-bucket-id", "some-bucket-name",
    )
    raise ClientError("expected failure, but got: \{@debug.to_string(client)}")
  } catch {
    ClientError(e) =>
      assert_eq(
        e,
        (
          #|unable to parse Client: Object(
          #|  {
          #|    "randomField": String("randomValue"),
          #|    "anotherRandomField": Number(12345),
          #|  },
          #|)
        ),
      )
  }
}

///|
test "Client::from_json - empty JSON input" {
  let empty_json = "{}"
  try {
    let client = Client::from_json(
      empty_json, "some-bucket-id", "some-bucket-name",
    )
    raise ClientError("expected failure, but got: \{@debug.to_string(client)}")
  } catch {
    ClientError(e) =>
      assert_eq(
        e,
        (
          #|unable to parse Client: Object({})
        ),
      )
  }
}

///|
test "Client::from_json - missing required fields" {
  let missing_fields_json =
    #|{
    #|  "accountId": "12345",
    #|  "apiInfo": {},
    #|  "authorizationToken": "auth_token"
    #|}
  try {
    let client = Client::from_json(
      missing_fields_json, "some-bucket-id", "some-bucket-name",
    )
    raise ClientError("expected failure, but got: \{@debug.to_string(client)}")
  } catch {
    ClientError(e) =>
      assert_eq(
        e,
        (
          #|unable to parse Client: Object(
          #|  {
          #|    "accountId": String("12345"),
          #|    "apiInfo": Object({}),
          #|    "authorizationToken": String("auth_token"),
          #|  },
          #|)
        ),
      )
  }
}

///|
test "Client::from_json - invalid JSON input" {
  let invalid_json = "invalid JSON string"
  try {
    let client = Client::from_json(
      invalid_json, "some-bucket-id", "some-bucket-name",
    )
    raise ClientError("expected failure, but got: \{@debug.to_string(client)}")
  } catch {
    ClientError(e) => assert_eq(e, "Invalid character 'i' at line 1, column 0")
  }
}

///|
test "Client::from_json - valid JSON input with nulls" {
  let valid_json =
    #|{
    #|  "accountId": "12345",
    #|  "apiInfo": {
    #|    "storageApi": {
    #|      "absoluteMinimumPartSize": 5000000,
    #|      "apiUrl": "https://apiNNN.backblazeb2.com",
    #|      "bucketId": null,
    #|      "bucketName": null,
    #|      "capabilities": ["deleteFiles", "readFiles"],
    #|      "downloadUrl": "https://f001.backblazeb2.com",
    #|      "infoType": "storageApi",
    #|      "namePrefix": null,
    #|      "recommendedPartSize": 100000000,
    #|      "s3ApiUrl": "https://s3.us-west-NNN.backblazeb2.com"
    #|    }
    #|  },
    #|  "authorizationToken": "auth_token",
    #|  "applicationKeyExpirationTimestamp": null
    #|}
  @debug.debug_inspect(
    Client::from_json(valid_json, "some-bucket-id", "some-bucket-name"),
    content=(
      #|{
      #|  account_id: "12345",
      #|  api_info: {
      #|    storage_api: {
      #|      absolute_minimum_part_size: 5000000,
      #|      api_url: "https://apiNNN.backblazeb2.com",
      #|      bucket_id: None,
      #|      bucket_name: None,
      #|      capabilities: ["deleteFiles", "readFiles"],
      #|      download_url: "https://f001.backblazeb2.com",
      #|      info_type: "storageApi",
      #|      name_prefix: None,
      #|      recommended_part_size: 100000000,
      #|      s3apiurl: "https://s3.us-west-NNN.backblazeb2.com",
      #|    },
      #|  },
      #|  authorization_token: "auth_token",
      #|  application_key_expiration_timestamp: None,
      #|  bucket_id: "some-bucket-id",
      #|  bucket_name: "some-bucket-name",
      #|}
    ),
  )
}