// Audit Class 1 (Gate C follow-up) — RBAC grant/revoke ownership check [DIRECT-DEV].
//
// /api/rbac/grant and /revoke took a `tenantId` from the request BODY and passed it straight to
// grant/revokeTenantRole — a tenant-scoped admin could mutate ANOTHER tenant's role assignments
// (cross-tenant privilege manipulation, a WRITE hole). assertRbacTenantOwnership (rbac-routes.ts)
// requires the body tenantId to match the caller's SESSION-derived tenant (req.tenantId, set by
// tenantMiddleware — not forgeable), unless the caller is a GLOBAL platform admin.
//
// This proves the four-part matrix the higher-stakes role-mutation route needs:
//   1. in-tenant grant works (self-service preserved, no over-lock);
//   2. cross-tenant grant blocked (the hole closed);
//   3. global platform-admin cross-tenant works (the legitimate exemption);
//   4. the check anchors on the NON-FORGEABLE session tenant — an absent session tenant cannot
//      be satisfied by supplying a body tenantId (empty-session guard).
//
// Requires DATABASE_URL + boot secrets (runs under test:integration; the global-admin branch
// calls storage.getUser).

import { test, describe, before, after } from "node:test";
import assert from "node:assert/strict";
import { eq } from "drizzle-orm";
import { db } from "../server/db.js";
import { users } from "../shared/schema.js";
import { assertRbacTenantOwnership } from "../server/lib/rbac-routes.js";

const RUN = `rbacgo-${Date.now()}`;
const GLOBAL_ADMIN_ID = `${RUN}-global-admin`;  // users.role = 'admin'
const TENANT_ADMIN_ID = `${RUN}-tenant-admin`;  // GLOBAL role non-admin; tenant-scoped admin
const TENANT_A = `${RUN}-tenant-A`;
const TENANT_B = `${RUN}-tenant-B`;

// Invoke the helper with a mock req/res and capture (allowed, status).
async function check(userId: string | undefined, sessionTenant: string | undefined, bodyTenantId: string) {
  const req: any = { session: { userId }, tenantId: sessionTenant };
  let status = 200;
  const res: any = { status(c: number) { status = c; return this; }, json(_: unknown) { return this; } };
  const allowed = await assertRbacTenantOwnership(req, res, bodyTenantId);
  return { allowed, status };
}

describe("Audit Class 1 — RBAC grant/revoke tenant-ownership check", () => {
  before(async () => {
    await db.delete(users).where(eq(users.id, GLOBAL_ADMIN_ID));
    await db.delete(users).where(eq(users.id, TENANT_ADMIN_ID));
    await db.insert(users).values({ id: GLOBAL_ADMIN_ID, username: GLOBAL_ADMIN_ID, password: "x", role: "admin", fullName: "Global Admin" });
    await db.insert(users).values({ id: TENANT_ADMIN_ID, username: TENANT_ADMIN_ID, password: "x", role: "risk_manager", fullName: "Tenant Admin" });
  });
  after(async () => {
    await db.delete(users).where(eq(users.id, GLOBAL_ADMIN_ID));
    await db.delete(users).where(eq(users.id, TENANT_ADMIN_ID));
  });

  test("1. in-tenant grant works — tenant admin, body tenant == session tenant -> allowed", async () => {
    const r = await check(TENANT_ADMIN_ID, TENANT_A, TENANT_A);
    assert.equal(r.allowed, true, "a tenant admin must manage roles within their own tenant (self-service)");
    assert.equal(r.status, 200);
  });

  test("2. cross-tenant grant BLOCKED — tenant admin, body tenant != session tenant -> 403", async () => {
    const r = await check(TENANT_ADMIN_ID, TENANT_A, TENANT_B);
    assert.equal(r.allowed, false, "a tenant admin must NOT mutate another tenant's roles (write hole closed)");
    assert.equal(r.status, 403);
  });

  test("3. platform-admin cross-tenant works — global admin, body tenant != session tenant -> allowed", async () => {
    const r = await check(GLOBAL_ADMIN_ID, TENANT_A, TENANT_B);
    assert.equal(r.allowed, true, "a genuine global platform admin may administer RBAC across tenants");
    assert.equal(r.status, 200);
  });

  test("4. anchors on the session, not a forgeable body — absent session tenant cannot be satisfied by a body value -> 403", async () => {
    // No session tenant bound. A non-admin caller supplying any body tenantId must NOT pass:
    // the empty-session guard means the match branch cannot be forged via the body.
    const r = await check(TENANT_ADMIN_ID, "", TENANT_A);
    assert.equal(r.allowed, false, "an empty session tenant must not be satisfiable by a caller-supplied body tenantId");
    assert.equal(r.status, 403);
  });

  test("unauthenticated (no session user) cross-tenant -> 403", async () => {
    const r = await check(undefined, TENANT_A, TENANT_B);
    assert.equal(r.allowed, false);
    assert.equal(r.status, 403);
  });
});
