import { afterEach, describe, expect, mock, test } from "bun:test";

// access.ts capture `getActiveOrgId` / `auth` à l'import : les stubs doivent
// être posés AVANT le chargement du module (d'où l'import dynamique plus bas).
// Même convention que actions.test.ts / assert-entities.test.ts de ce
// répertoire. Les deux stubs sont pilotés par des variables mutables remises à
// leur valeur neutre après chaque test — pas de session, org résolue — pour ne
// pas changer le comportement des autres cas ni fuir vers les fichiers voisins.
const NO_SESSION = null as unknown;
let sessionResult: unknown = NO_SESSION;
let resolveOrgId: () => Promise<string> = async () => "test-org";

mock.module("next/headers", () => ({
  headers: async () => new Headers(),
  cookies: async () => ({ get: () => undefined }),
}));
mock.module("@/lib/auth", () => ({
  auth: { api: { getSession: async () => sessionResult } },
}));
mock.module("@/lib/active-org", () => ({
  getActiveOrgId: () => resolveOrgId(),
  getActiveUserId: async () => "test-user",
}));

const {
  __clearTestCaller,
  __setTestCaller,
  currentUserCanAccessSite,
  hasFullSiteAccess,
} = await import("./access");

afterEach(() => {
  __clearTestCaller();
  sessionResult = NO_SESSION;
  resolveOrgId = async () => "test-org";
});

describe("currentUserCanAccessSite", () => {
  test("fails closed when no caller resolves", async () => {
    expect(await currentUserCanAccessSite("any-site")).toBe(false);
  });

  // currentUserCanAccessSite délègue désormais à userCanAccessSite, qui déduit
  // l'org du site (`sites.org_id`) au lieu de faire confiance au rôle porté par
  // le caller : un site inconnu est refusé même pour un owner. Les chemins
  // passants (owner/admin de l'org du site, member avec site_access) sont
  // couverts par user-can-access-site.test.ts, qui sème une vraie org.
  // Gated DB : la délégation interroge la base dès qu'un caller est résolu.
  test.skipIf(process.env.DB_TESTS !== "1")(
    "owner caller (via test seam) sur un site inconnu → refus",
    async () => {
      __setTestCaller({ userId: "u1", orgId: "o1", role: "owner" });
      // Preuve que le seam est vivant : sans elle, ce test attendrait la même
      // valeur que « fails closed when no caller resolves » et passerait aussi
      // bien si __setTestCaller cessait d'injecter quoi que ce soit.
      expect(await hasFullSiteAccess()).toBe(true);
      expect(await currentUserCanAccessSite("any-site")).toBe(false);
    },
  );
});

describe("hasFullSiteAccess", () => {
  // Le caller passe par le self-heal de getActiveOrgId(), qui HARD-THROW quand
  // aucune org ne peut être résolue. Ce que la garde achète : hasFullSiteAccess()
  // renvoie `false` au lieu de laisser remonter ce throw, donc la décision
  // d'affichage du dialogue premier-site (layout racine du groupe (app)) reste
  // calculable. Elle ne protège PAS le gate de section à elle seule : ce dernier
  // attend aussi getSitesForActiveOrg(), qui appelle getActiveOrgId() sans garde
  // et lèverait dans ce même scénario (comme le layout racine, en amont).
  test("resolves false instead of throwing when the active org can't be resolved", async () => {
    sessionResult = {
      user: { id: "u1", email: "u1@example.com" },
      session: { activeOrganizationId: null },
    };
    resolveOrgId = async () => {
      throw new Error("No active organization in session");
    };
    expect(await hasFullSiteAccess()).toBe(false);
  });
});
