///|
/// The primitives behind JWT auth — SHA-256, HMAC-SHA256 and the constant-time
/// comparison, all `mooncrypt`'s — then sign and verify a compact HS256 token,
/// including the ways verification is meant to fail: a tampered signature, an
/// expired token, and the `alg: none` downgrade.
///
///     moon run src/examples/05-jwt-crypto
#coverage.skip
fn main {
  // SHA-256 of "abc" — the FIPS 180-4 vector mooncrypt is checked against.
  println("sha256(\"abc\") = " + @base16.encode(@sha2.hash(b"abc"[:])))
  println(
    "hmac_sha256 = " +
    @base16.encode(
      @hmac.mac(b"key"[:], b"heke1228"[:], () => @sha2.Hasher::new()),
    ),
  )

  // Constant-time equality: same bytes match, different bytes do not.
  let a = @sha2.hash(b"qwe"[:])
  println(
    "constant_time_eq(a,a)=" +
    @spec.eq(a[:], a[:]).to_string() +
    " (a,b)=" +
    @spec.eq(a[:], @sha2.hash(b"qwq"[:])[:]).to_string(),
  )
  let secret = "topsecret"
  let claims : Map[String, Json] = Map([
    ("sub", Json::string("lfanke")),
    ("exp", Json::number(2000.0)),
  ])
  let token = @moonzero.jwt_sign(claims, secret)
  println("jwt three segments: " + count_dots(token).to_string())

  // A valid token verifies and hands back its claims.
  try {
    let got = @moonzero.jwt_verify(token, secret, 1000L)
    println("verify ok, sub=" + sub_of(got))
  } catch {
    e => println("unexpected verify failure: " + jwt_reason(e))
  }

  // Each rejection path reports its own reason.
  println("wrong secret -> " + reject(token, "wrong", 1000L))
  println("expired (t=3000 >= exp=2000) -> " + reject(token, secret, 3000L))
  println(
    "alg:none downgrade -> " +
    reject("eyJhbGciOiJub25lIn0.eyJzdWIiOiJtYWxsb3J5In0.QUFB", secret, 1000L),
  )

  // The claim checks are a record: the same expired token passes with `exp`
  // switched off.
  try {
    let lenient = @moonzero.jwt_verify(
      token,
      secret,
      3000L,
      policy=@jwt.Policy::new(expiry=false),
    )
    println("expired but expiry=false -> sub=" + sub_of(lenient))
  } catch {
    e => println("unexpected: " + jwt_reason(e))
  }

  // The middleware's accept/reject decision, as a pure predicate.
  println(
    "jwt_authorized(valid) = " +
    @moonzero.jwt_authorized(Some(token), secret, 1000L).to_string(),
  )
  println(
    "jwt_authorized(none)  = " +
    @moonzero.jwt_authorized(None, secret, 1000L).to_string(),
  )
}

///|
/// Verify a token expected to fail, returning the rejection's name.
fn reject(token : String, secret : String, now : Int64) -> String {
  try {
    let _ = @moonzero.jwt_verify(token, secret, now)
    "unexpectedly accepted"
  } catch {
    e => jwt_reason(e)
  }
}

///|
fn jwt_reason(e : @jwt.Refused) -> String {
  match e {
    Shape(m) => "malformed: " + m
    Algorithm(want~, got~) => "unsupported-alg: " + got + " (want " + want + ")"
    Signature => "bad-signature"
    Expired(..) => "expired"
    TooEarly(..) => "not-yet-valid"
    Issuer(..) => "wrong-issuer"
    Audience(..) => "wrong-audience"
    Subject(..) => "wrong-subject"
    IssuedAt(..) => "issued-in-the-future"
    Type(..) => "wrong-typ"
    Missing(claim~) => "missing claim: " + claim
  }
}

///|
fn sub_of(claims : Map[String, Json]) -> String {
  match claims.get("sub") {
    Some(String(s)) => s
    _ => "?"
  }
}

///|
fn count_dots(s : String) -> Int32 {
  let mut n = 1
  for i = 0; i < s.length(); i = i + 1 {
    if s[i] == '.' {
      n = n + 1
    }
  }
  n
}