///|
/// Live integration check for the S3 client against a real S3-compatible
/// server (MinIO). Run with:
///   moon run src/livecheck --    
async fn main {
  let args = @env.args()
  guard args.length() >= 5 else {
    println(
      "usage: moon run src/livecheck --    ",
    )
    return
  }
  let url = args[1]
  let access_key = args[2]
  let secret_key = args[3]
  let region = args[4]
  let client = @s3.Client(
    @s3.Config(
      region,
      @s3.Credentials(access_key, secret_key),
      endpoint=url,
      endpoint_style=@s3.Path,
    ),
  )
  let bucket = "kaashyapan-s3-livecheck"
  let key = "livecheck/hello.txt"
  let content = "Hello MinIO from MoonBit!"

  println("=== 1. list_buckets ===")
  let buckets = client.list_buckets()
  println("bucket count: \{buckets.buckets.length()}")
  for b in buckets.buckets {
    println("  - \{b.name} (created \{Repr(b.creation_date)})")
  }
  assert_true(buckets.owner_id is Some(_))

  println("=== 2. create_bucket ===")
  ignore(client.create_bucket(bucket)) catch {
    @s3.S3Error::ServiceError(response, body) =>
      println("create_bucket: \{response.code} \{body}")
    _ => fail("unexpected error in create_bucket")
  }

  println("=== 3. head_bucket ===")
  let head_bucket_response = client.head_bucket(bucket)
  println("head_bucket: \{head_bucket_response.code}")

  println("=== 4. put_object ===")
  let put_response = client.put_object(bucket, key, content)
  println("put_object: \{put_response.code}")

  println("=== 5. head_object ===")
  let head_response = client.head_object(bucket, key)
  println("head_object: \{head_response.code}")
  println(
    "  Content-Length: \{Repr(head_response.headers.get("Content-Length"))}",
  )
  println("  ETag: \{Repr(head_response.headers.get("ETag"))}")
  println("  Content-Type: \{Repr(head_response.headers.get("Content-Type"))}")

  println("=== 6. get_object ===")
  let object = client.get_object(bucket, key)
  println("get_object: \{object.response.code}")
  let body = @io.Reader::read_all(object.body).text()
  object.body.close()
  assert_eq(body, content)
  println("  body matches: \{body}")

  println("=== 7. list_objects_v2 ===")
  let listed = client.list_objects_v2(bucket, prefix="livecheck/")
  assert_eq(listed.contents.length(), 1)
  assert_eq(listed.contents[0].key, key)
  assert_eq(listed.contents[0].size, content.length().to_int64())
  println("  key: \{listed.contents[0].key}")
  println("  size: \{listed.contents[0].size}")
  println("  etag: \{Repr(listed.contents[0].etag)}")

  println("=== 8. presigned GET URL ===")
  let presigned = client.generate_presigned_get_url(bucket, key)
  println("  url: \{presigned}")
  let (origin, target) = split_url(presigned)
  let unsigned = @http.Client(origin)
  unsigned.request(@http.Get, target)
  let presigned_response = unsigned.end_request()
  let presigned_body = unsigned.read_all().text()
  unsigned.close()
  assert_eq(presigned_response.code, 200)
  assert_eq(presigned_body, content)
  println("  presigned GET: \{presigned_response.code}, body matches")

  println("=== 9. presigned PUT URL ===")
  let put_url = client.generate_presigned_put_url(
    bucket, "livecheck/presigned-put.txt",
  )
  let (put_origin, put_target) = split_url(put_url)
  let put_unsigned = @http.Client(put_origin)
  put_unsigned.request(@http.Put, put_target, extra_headers={
    "Content-Length": content.length().to_string(),
  })
  put_unsigned.write(content)
  let presigned_put_response = put_unsigned.end_request()
  let put_error_body = put_unsigned.read_all().text()
  put_unsigned.close()
  println("  presigned PUT: \{presigned_put_response.code} \{put_error_body}")
  assert_eq(presigned_put_response.code, 200)
  let verified = client.head_object(bucket, "livecheck/presigned-put.txt")
  println(
    "  uploaded via presigned PUT, Content-Length: \{Repr(verified.headers.get("Content-Length"))}",
  )

  println("=== 10. negative: unsigned request is rejected ===")
  let rejected = @http.Client(url)
  rejected.request(@http.Get, "/\{bucket}/\{key}")
  let rejected_response = rejected.end_request()
  rejected.close()
  assert_eq(rejected_response.code, 403)
  println("  unsigned GET: \{rejected_response.code} (403 expected)")

  println("=== 11. negative: head_object on missing key raises 404 ===")
  ignore(client.head_object(bucket, "livecheck/missing.txt")) catch {
    @s3.S3Error::ServiceError(response, _) => {
      assert_eq(response.code, 404)
      println("  missing key: \{response.code} (404 expected)")
    }
    _ => fail("unexpected error for missing key")
  }

  println("=== 12. cleanup: delete_object + delete_bucket ===")
  let delete_response = client.delete_object(bucket, key)
  assert_eq(delete_response.code, 204)
  let delete_presigned = client.delete_object(
    bucket, "livecheck/presigned-put.txt",
  )
  assert_eq(delete_presigned.code, 204)
  let delete_bucket_response = client.delete_bucket(bucket)
  println("  delete_object: \{delete_response.code}")
  println("  delete_object (presigned put): \{delete_presigned.code}")
  println("  delete_bucket: \{delete_bucket_response.code}")

  println("=== live check passed ===")
}

///|
/// Split a URL of the form `scheme://host[:port]/path?query` into
/// `(origin, path_and_query)`.
fn split_url(url : String) -> (String, String) raise {
  let separator = "://"
  guard url.find(separator) is Some(index) else { fail("not a url: \{url}") }
  let rest = url[index + separator.length():]
  let mut split : Int = rest.length()
  for i, c in rest.to_array() {
    if c == '/' {
      split = i
      break
    }
  }
  let origin = url[:index].to_owned() + "://" + rest[:split].to_owned()
  (origin, rest[split:].to_owned())
}