;; identity/tests/membership.sx — membership state machine + per-app
;; grant projection. Valid transitions advance state; invalid ones are
;; explicit errors. The projection renders one canonical state per app.

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

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

(define idm-ev erlang-eval-ast)
(define idmnm (fn (v) (get v :name)))

(identity-load-membership!)

;; ── request → pending → approve → active ─────────────────────────

(id-membership-test
  "request leaves the subject pending"
  (idmnm
    (idm-ev
      "M = identity_membership:start(),\n       identity_membership:request(M, alice, basic),\n       case identity_membership:status(M, alice) of\n         {ok, St, _} -> St;\n         {none} -> none\n       end"))
  "pending")

(id-membership-test
  "approve activates a pending membership"
  (idmnm
    (idm-ev
      "M = identity_membership:start(),\n       identity_membership:request(M, alice, basic),\n       identity_membership:approve(M, alice),\n       case identity_membership:status(M, alice) of\n         {ok, St, _} -> St;\n         {none} -> none\n       end"))
  "active")

(id-membership-test
  "status keeps the requested tier"
  (idmnm
    (idm-ev
      "M = identity_membership:start(),\n       identity_membership:request(M, alice, supporter),\n       identity_membership:approve(M, alice),\n       case identity_membership:status(M, alice) of\n         {ok, _, Tier} -> Tier\n       end"))
  "supporter")

;; ── guarded transitions: invalid moves are explicit errors ───────

(id-membership-test
  "requesting twice is an error"
  (idmnm
    (idm-ev
      "M = identity_membership:start(),\n       identity_membership:request(M, alice, basic),\n       case identity_membership:request(M, alice, basic) of\n         ok -> ok;\n         {error, Why} -> Why\n       end"))
  "exists")

(id-membership-test
  "approving an unknown subject is not_found"
  (idmnm
    (idm-ev
      "M = identity_membership:start(),\n       case identity_membership:approve(M, ghost) of\n         ok -> ok;\n         {error, Why} -> Why\n       end"))
  "not_found")

(id-membership-test
  "approving an already-active membership is an error"
  (idmnm
    (idm-ev
      "M = identity_membership:start(),\n       identity_membership:request(M, alice, basic),\n       identity_membership:approve(M, alice),\n       case identity_membership:approve(M, alice) of\n         ok -> ok;\n         {error, Why} -> Why\n       end"))
  "active")

;; ── lapse / reinstate ────────────────────────────────────────────

(id-membership-test
  "active member can lapse"
  (idmnm
    (idm-ev
      "M = identity_membership:start(),\n       identity_membership:request(M, alice, basic),\n       identity_membership:approve(M, alice),\n       identity_membership:lapse(M, alice),\n       case identity_membership:status(M, alice) of\n         {ok, St, _} -> St\n       end"))
  "lapsed")

(id-membership-test
  "lapsing a pending membership is an error"
  (idmnm
    (idm-ev
      "M = identity_membership:start(),\n       identity_membership:request(M, alice, basic),\n       case identity_membership:lapse(M, alice) of\n         ok -> ok;\n         {error, Why} -> Why\n       end"))
  "pending")

(id-membership-test
  "lapsed member can reinstate to active"
  (idmnm
    (idm-ev
      "M = identity_membership:start(),\n       identity_membership:request(M, alice, basic),\n       identity_membership:approve(M, alice),\n       identity_membership:lapse(M, alice),\n       identity_membership:reinstate(M, alice),\n       case identity_membership:status(M, alice) of\n         {ok, St, _} -> St\n       end"))
  "active")

;; ── revoke is terminal ───────────────────────────────────────────

(id-membership-test
  "any member can be revoked"
  (idmnm
    (idm-ev
      "M = identity_membership:start(),\n       identity_membership:request(M, alice, basic),\n       identity_membership:approve(M, alice),\n       identity_membership:revoke(M, alice),\n       case identity_membership:status(M, alice) of\n         {ok, St, _} -> St\n       end"))
  "revoked")

(id-membership-test
  "a revoked membership cannot be reinstated"
  (idmnm
    (idm-ev
      "M = identity_membership:start(),\n       identity_membership:request(M, alice, basic),\n       identity_membership:approve(M, alice),\n       identity_membership:revoke(M, alice),\n       case identity_membership:reinstate(M, alice) of\n         ok -> ok;\n         {error, Why} -> Why\n       end"))
  "revoked")

;; ── per-app grant projection ─────────────────────────────────────

(id-membership-test
  "active member projects as member"
  (idmnm
    (idm-ev
      "M = identity_membership:start(),\n       identity_membership:request(M, alice, basic),\n       identity_membership:approve(M, alice),\n       case identity_membership:project(M, alice, blog) of\n         {member, _, _} -> member;\n         {Tag, _} -> Tag\n       end"))
  "member")

(id-membership-test
  "projection carries the requesting app"
  (idmnm
    (idm-ev
      "M = identity_membership:start(),\n       identity_membership:request(M, alice, basic),\n       identity_membership:approve(M, alice),\n       case identity_membership:project(M, alice, market) of\n         {member, _, App} -> App\n       end"))
  "market")

(id-membership-test
  "the same subject projects consistently across apps"
  (idmnm
    (idm-ev
      "M = identity_membership:start(),\n       identity_membership:request(M, alice, supporter),\n       identity_membership:approve(M, alice),\n       {member, T1, blog} = identity_membership:project(M, alice, blog),\n       {member, T2, events} = identity_membership:project(M, alice, events),\n       case T1 =:= T2 of\n         true -> T1;\n         false -> mismatch\n       end"))
  "supporter")

(id-membership-test
  "unknown subject projects as non_member"
  (idmnm
    (idm-ev
      "M = identity_membership:start(),\n       case identity_membership:project(M, ghost, blog) of\n         {Tag, _} -> Tag;\n         {Tag, _, _} -> Tag\n       end"))
  "non_member")

(id-membership-test
  "lapsed member projects as lapsed"
  (idmnm
    (idm-ev
      "M = identity_membership:start(),\n       identity_membership:request(M, alice, basic),\n       identity_membership:approve(M, alice),\n       identity_membership:lapse(M, alice),\n       case identity_membership:project(M, alice, blog) of\n         {Tag, _} -> Tag;\n         {Tag, _, _} -> Tag\n       end"))
  "lapsed")

(id-membership-test
  "revoked member projects as denied"
  (idmnm
    (idm-ev
      "M = identity_membership:start(),\n       identity_membership:request(M, alice, basic),\n       identity_membership:approve(M, alice),\n       identity_membership:revoke(M, alice),\n       case identity_membership:project(M, alice, blog) of\n         {Tag, _} -> Tag;\n         {Tag, _, _} -> Tag\n       end"))
  "denied")

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