;; identity/tests/sso.sx — silent SSO (prompt=none, OIDC §3.1.2.1) as a
;; fast-path through the authorization-code machine. One subject session,
;; many client apps; no session → login_required (a negative state, not a
;; redirect). Silently-issued codes carry the same client/redirect/PKCE
;; binding as consented codes.

(define id-sso-test-count 0)
(define id-sso-test-pass 0)
(define id-sso-test-fails (list))

(define
  id-sso-test
  (fn
    (name actual expected)
    (set! id-sso-test-count (+ id-sso-test-count 1))
    (if
      (= actual expected)
      (set! id-sso-test-pass (+ id-sso-test-pass 1))
      (append! id-sso-test-fails {:name name :expected expected :actual actual}))))

(define ids-ev erlang-eval-ast)
(define idsnm (fn (v) (get v :name)))

(identity-load-token!)
(identity-load-session!)
(identity-load-registry!)
(identity-load-oauth!)

;; ── no session → login_required ──────────────────────────────────

(id-sso-test
  "silent authorize without a session is login_required"
  (idsnm
    (ids-ev
      "O = identity_oauth:start(),\n       case identity_oauth:silent_authorize(O, dashboard, uri2, read, alice, vv) of\n         {code, _} -> got_code;\n         {error, Why} -> Why\n       end"))
  "login_required")

;; ── established session → silent code ────────────────────────────

(id-sso-test
  "silent authorize for the same client returns a code"
  (idsnm
    (ids-ev
      "O = identity_oauth:start(),\n       {ok, _Sid} = identity_oauth:establish(O, alice, web),\n       case identity_oauth:silent_authorize(O, web, uri1, read, alice, vv) of\n         {code, _} -> got_code;\n         {error, Why} -> Why\n       end"))
  "got_code")

;; ── one session, many clients ────────────────────────────────────

(id-sso-test
  "a different client gets a silent code off the same session"
  (idsnm
    (ids-ev
      "O = identity_oauth:start(),\n       {ok, _Sid} = identity_oauth:establish(O, alice, web),\n       case identity_oauth:silent_authorize(O, dashboard, uri2, read, alice, vv) of\n         {code, _} -> got_code;\n         {error, Why} -> Why\n       end"))
  "got_code")

(id-sso-test
  "many clients all silently authorize off one session"
  (idsnm
    (ids-ev
      "O = identity_oauth:start(),\n       {ok, _Sid} = identity_oauth:establish(O, alice, web),\n       {code, _C1} = identity_oauth:silent_authorize(O, dashboard, uri2, read, alice, vv),\n       {code, _C2} = identity_oauth:silent_authorize(O, mobile, uri3, read, alice, vv),\n       case identity_oauth:silent_authorize(O, billing, uri4, read, alice, vv) of\n         {code, _} -> got_code;\n         {error, Why} -> Why\n       end"))
  "got_code")

;; ── full SSO → token ─────────────────────────────────────────────

(id-sso-test
  "silent code exchanges to a working token"
  (idsnm
    (ids-ev
      "O = identity_oauth:start(),\n       {ok, _Sid} = identity_oauth:establish(O, alice, web),\n       {code, C} = identity_oauth:silent_authorize(O, dashboard, uri2, read, alice, vv),\n       {ok, A, _R} = identity_oauth:exchange(O, C, dashboard, uri2, vv),\n       case identity_oauth:introspect(O, A) of\n         {active, _, _, _} -> active;\n         {inactive} -> inactive\n       end"))
  "active")

(id-sso-test
  "SSO token carries the subject"
  (idsnm
    (ids-ev
      "O = identity_oauth:start(),\n       {ok, _Sid} = identity_oauth:establish(O, alice, web),\n       {code, C} = identity_oauth:silent_authorize(O, dashboard, uri2, read, alice, vv),\n       {ok, A, _R} = identity_oauth:exchange(O, C, dashboard, uri2, vv),\n       case identity_oauth:introspect(O, A) of\n         {active, Subject, _, _} -> Subject\n       end"))
  "alice")

;; ── silent codes keep the full binding ───────────────────────────

(id-sso-test
  "silent code still enforces PKCE at exchange"
  (idsnm
    (ids-ev
      "O = identity_oauth:start(),\n       {ok, _Sid} = identity_oauth:establish(O, alice, web),\n       {code, C} = identity_oauth:silent_authorize(O, dashboard, uri2, read, alice, vv),\n       case identity_oauth:exchange(O, C, dashboard, uri2, wrongverif) of\n         {ok, _, _} -> ok;\n         {error, Why} -> Why\n       end"))
  "invalid_grant")

(id-sso-test
  "silent code still enforces client binding at exchange"
  (idsnm
    (ids-ev
      "O = identity_oauth:start(),\n       {ok, _Sid} = identity_oauth:establish(O, alice, web),\n       {code, C} = identity_oauth:silent_authorize(O, dashboard, uri2, read, alice, vv),\n       case identity_oauth:exchange(O, C, attacker, uri2, vv) of\n         {ok, _, _} -> ok;\n         {error, Why} -> Why\n       end"))
  "invalid_grant")

;; ── subject scoping: SSO is per subject ──────────────────────────

(id-sso-test
  "another subject is still login_required"
  (idsnm
    (ids-ev
      "O = identity_oauth:start(),\n       {ok, _Sid} = identity_oauth:establish(O, alice, web),\n       case identity_oauth:silent_authorize(O, dashboard, uri2, read, bob, vv) of\n         {code, _} -> got_code;\n         {error, Why} -> Why\n       end"))
  "login_required")

;; ── ending the session closes the SSO fast-path ──────────────────

(id-sso-test
  "after end_session, silent authorize is login_required"
  (idsnm
    (ids-ev
      "O = identity_oauth:start(),\n       {ok, Sid} = identity_oauth:establish(O, alice, web),\n       identity_oauth:end_session(O, Sid),\n       case identity_oauth:silent_authorize(O, dashboard, uri2, read, alice, vv) of\n         {code, _} -> got_code;\n         {error, Why} -> Why\n       end"))
  "login_required")

(define
  id-sso-test-summary
  (str "sso " id-sso-test-pass "/" id-sso-test-count))
