///|
/// The OAuth2 password-bearer flow end to end. A `/token` endpoint reads an
/// `OAuth2PasswordRequestForm` off the urlencoded body, checks the credentials,
/// and issues an HS256 JWT with `create_access_token`, replying through
/// `token_response`. A protected route hands its context to
/// `OAuth2PasswordBearer::authenticate`, which pulls the `Authorization: Bearer`
/// header, verifies the token at a fixed clock, and enforces scopes: `401`
/// unauthenticated, `403` when a scope is missing, the `AuthenticatedUser`
/// otherwise. Times are Unix seconds, passed in so the checks are deterministic.
///
///     moon run src/examples/10-oauth2-jwt
#coverage.skip
async fn main {
  let secret = "heke1228-signing-secret"
  let bearer = @moonapi.OAuth2PasswordBearer::new("/token", secret)
  let now = 1_700_000_000L
  let app = @moonapi.App::new()
  app.post("/token", ctx => {
    match ctx.oauth2_password_form() {
      Some(form) =>
        if form.username == "lfanke" && form.password == "qwq" {
          let tok = @moonapi.create_access_token(
            form.username,
            secret,
            now,
            scopes=form.scopes,
          )
          @moonapi.token_response(tok)
        } else {
          @moonapi.text(401, "bad credentials")
        }
      None => @moonapi.text(400, "not a password form")
    }
  })
  app.get("/items", ctx => {
    match bearer.authenticate(ctx, now, scopes=["items:read"]) {
      Ok(user) =>
        @moonapi.text(
          200,
          "items for \{user.subject}, scopes=[\{join_csv(user.scopes)}], read=\{user.has_scope("items:read")}",
        )
      Err(resp) => resp
    }
  })

  // 1. Exchange credentials at the token endpoint (urlencoded, `+` = space).
  let token_req : @moonasgi.Request = {
    http_method: "POST",
    path: "/token",
    query_string: b"",
    headers: [("content-type", "application/x-www-form-urlencoded")],
    body: @utf8.encode(
      "grant_type=password&username=lfanke&password=qwq&scope=items:read+profile",
    ),
  }
  let token_resp = app.handle(token_req)
  println(
    "POST /token -> \{token_resp.status} \{@utf8.decode_lossy(token_resp.body[:])}",
  )

  // A token carrying the required scope, and one without it.
  let good = @moonapi.create_access_token("lfanke", secret, now, scopes=[
    "items:read", "profile",
  ])
  let weak = @moonapi.create_access_token("lfanke", secret, now, scopes=[
    "profile",
  ])
  let get_items = (label : String, headers : Array[(String, String)]) => {
    let req : @moonasgi.Request = {
      http_method: "GET",
      path: "/items",
      query_string: b"",
      headers,
      body: b"",
    }
    let r = app.handle(req)
    println(
      "GET /items \{label} -> \{r.status} \{@utf8.decode_lossy(r.body[:])}",
    )
  }
  // 2. No Authorization header -> 401.
  get_items("(no token)", [])
  // 3. A token with the scope -> 200.
  get_items("(items:read)", [("authorization", "Bearer " + good)])
  // 4. A valid token missing the scope -> 403.
  get_items("(profile only)", [("authorization", "Bearer " + weak)])

  // bearer_token pulls the raw credential straight off the header.
  let probe : @moonapi.Context = @moonapi.Context::new(
    {
      http_method: "GET",
      path: "/items",
      query_string: b"",
      headers: [("authorization", "Bearer " + good)],
      body: b"",
    },
    params=Map([]),
  )
  println("bearer_token present: \{probe.bearer_token() is Some(_)}")
}

///|
/// Join strings with commas — collections render through `Debug`, not string
/// interpolation, so an example spells the formatting out.
fn join_csv(xs : Array[String]) -> String {
  let sb = StringBuilder()
  for i = 0; i < xs.length(); i = i + 1 {
    if i > 0 {
      sb.write_string(",")
    }
    sb.write_string(xs[i])
  }
  sb.to_string()
}