import { beforeEach, describe, expect, it, vi } from "vitest";
import { SystemRole } from "@nexus/shared";

vi.mock("../lib/audit.js", () => ({ createAuditEntry: vi.fn() }));
vi.mock("../lib/audit-helpers.js", () => ({
  makeAuditLogger: () => vi.fn(),
}));

const invalidateRoleDefaultsCache = vi.hoisted(() => vi.fn());
vi.mock("../trpc.js", () => ({
  invalidateRoleDefaultsCache,
}));

import {
  resetUserPermissions,
  setUserPermissions,
  updateUserRole,
} from "../router/user-procedure-support.js";

/**
 * Ticket #57 — when a privileged-state mutation happens we MUST:
 *   1. delete every ActiveSession for the affected user (forces next-request
 *      re-auth, because the tRPC route validates `jti` against ActiveSession),
 *   2. call `invalidateRoleDefaultsCache()` so peer instances drop their
 *      10 s cache entries via the Redis pub/sub fan-out.
 *
 * Without (1), a demoted admin keeps their JWT valid until it expires, so
 * permissions resolved server-side still reflect the old role. Without (2),
 * peer instances keep serving the old role defaults for up to the TTL.
 */
describe("RBAC mutation side effects (#57)", () => {
  beforeEach(() => {
    vi.clearAllMocks();
  });

  function makeCtx(dbOverrides: Record<string, unknown> = {}) {
    const defaultDb = {
      user: {
        findUnique: vi.fn(),
        update: vi.fn(),
      },
      activeSession: {
        deleteMany: vi.fn().mockResolvedValue({ count: 3 }),
      },
      ...dbOverrides,
    };
    return {
      ctx: {
        db: defaultDb as never,
        dbUser: {
          id: "admin_1",
          systemRole: SystemRole.ADMIN,
          permissionOverrides: null,
        },
        session: {
          user: { email: "admin@example.com", name: "Admin", image: null },
          expires: "2099-01-01T00:00:00.000Z",
        },
      },
      db: defaultDb,
    };
  }

  describe("updateUserRole", () => {
    it("deletes active sessions and invalidates cache when role changes", async () => {
      const { ctx, db } = makeCtx({
        user: {
          findUnique: vi.fn().mockResolvedValue({
            id: "user_victim",
            name: "Victim",
            email: "victim@example.com",
            systemRole: SystemRole.ADMIN,
          }),
          update: vi.fn().mockResolvedValue({
            id: "user_victim",
            name: "Victim",
            email: "victim@example.com",
            systemRole: SystemRole.USER,
          }),
        },
      });

      await updateUserRole(ctx as never, {
        id: "user_victim",
        systemRole: SystemRole.USER,
      });

      expect(db.activeSession.deleteMany).toHaveBeenCalledWith({
        where: { userId: "user_victim" },
      });
      expect(invalidateRoleDefaultsCache).toHaveBeenCalledTimes(1);
    });

    it("does NOT delete sessions or invalidate when role is unchanged", async () => {
      const { ctx, db } = makeCtx({
        user: {
          findUnique: vi.fn().mockResolvedValue({
            id: "user_1",
            name: "Alice",
            email: "alice@example.com",
            systemRole: SystemRole.MANAGER,
          }),
          update: vi.fn().mockResolvedValue({
            id: "user_1",
            name: "Alice",
            email: "alice@example.com",
            systemRole: SystemRole.MANAGER,
          }),
        },
      });

      await updateUserRole(ctx as never, {
        id: "user_1",
        systemRole: SystemRole.MANAGER,
      });

      expect(db.activeSession.deleteMany).not.toHaveBeenCalled();
      expect(invalidateRoleDefaultsCache).not.toHaveBeenCalled();
    });
  });

  describe("setUserPermissions", () => {
    it("deletes active sessions and invalidates cache on every call", async () => {
      const { ctx, db } = makeCtx({
        user: {
          findUnique: vi.fn().mockResolvedValue({
            id: "user_1",
            name: "Alice",
            email: "alice@example.com",
            permissionOverrides: null,
          }),
          update: vi.fn().mockResolvedValue({
            id: "user_1",
            name: "Alice",
            email: "alice@example.com",
            permissionOverrides: { granted: ["x"], denied: [] },
          }),
        },
      });

      await setUserPermissions(ctx as never, {
        userId: "user_1",
        overrides: { granted: ["x"], denied: [] },
      });

      expect(db.activeSession.deleteMany).toHaveBeenCalledWith({
        where: { userId: "user_1" },
      });
      expect(invalidateRoleDefaultsCache).toHaveBeenCalledTimes(1);
    });
  });

  describe("resetUserPermissions", () => {
    it("deletes active sessions and invalidates cache", async () => {
      const { ctx, db } = makeCtx({
        user: {
          findUnique: vi.fn().mockResolvedValue({
            id: "user_1",
            name: "Alice",
            email: "alice@example.com",
            permissionOverrides: { granted: ["x"], denied: [] },
          }),
          update: vi.fn().mockResolvedValue({
            id: "user_1",
            name: "Alice",
            email: "alice@example.com",
            permissionOverrides: null,
          }),
        },
      });

      await resetUserPermissions(ctx as never, { userId: "user_1" });

      expect(db.activeSession.deleteMany).toHaveBeenCalledWith({
        where: { userId: "user_1" },
      });
      expect(invalidateRoleDefaultsCache).toHaveBeenCalledTimes(1);
    });
  });
});
