// src/lib/onboarding/activation/snapshot.test.ts — lecture serveur de l'état
// d'activation. Toutes les dépendances sont mockées : ce qui est testé ici
// c'est le câblage (dismiss cantonné à l'affichage, résolution du site actif)
// et la garantie fail-silent, pas les requêtes SQL (couvertes par
// derive.test.ts).
import { afterAll, describe, expect, it, mock } from "bun:test";

mock.module("server-only", () => ({}));

// ⚠️ `mock.module` est GLOBAL au process et survit au fichier de test (les
// fichiers d'un même répertoire tournent dans le même process). On capture donc
// les vrais modules AVANT de les remplacer, et on les remet en place en
// `afterAll` — sans quoi actions.test.ts, voisin de palier, hériterait de nos
// stubs (`dismissActivation is not a function`).
const REAL = {
  "@/lib/active-org": { ...(await import("@/lib/active-org")) },
  "@/lib/sites/access": { ...(await import("@/lib/sites/access")) },
  "@/lib/sites/active-site": { ...(await import("@/lib/sites/active-site")) },
  "./derive": { ...(await import("./derive")) },
  "./dismiss": { ...(await import("./dismiss")) },
};

afterAll(() => {
  for (const [specifier, exports] of Object.entries(REAL)) {
    mock.module(specifier, () => exports);
  }
});

const FLAGS = {
  "create-site": true,
  "connect-gsc": false,
  "add-keywords": false,
  "enrich-keywords": false,
  "target-country": false,
  "invite-teammate": false,
};

/** Recâble les cinq modules dont dépend snapshot.ts et le réimporte. */
async function loadSnapshot(overrides: {
  dismissed?: boolean;
  sites?: { id: string }[];
  fullSiteAccess?: boolean;
  derive?: (orgId: string, siteId: string | null) => Promise<unknown>;
}) {
  mock.module("@/lib/active-org", () => ({
    getActiveOrgId: async () => "org_x",
    getActiveUserId: async () => "user_x",
  }));
  mock.module("./dismiss", () => ({
    isActivationDismissed: async () => overrides.dismissed ?? false,
  }));
  mock.module("@/lib/sites/access", () => ({
    getSitesForActiveOrg: async () => overrides.sites ?? [{ id: "site_x" }],
    hasFullSiteAccess: async () => overrides.fullSiteAccess ?? true,
  }));
  mock.module("@/lib/sites/active-site", () => ({
    getActiveSiteId: async (ids: string[]) => ids[0] ?? "",
  }));
  mock.module("./derive", () => ({
    deriveActivationFlags: overrides.derive ?? (async () => FLAGS),
  }));
  const { getActivationSnapshot } = await import("./snapshot");
  return getActivationSnapshot;
}

describe("getActivationSnapshot", () => {
  it("retourne null quand la dérivation échoue, sans propager l'erreur", async () => {
    const getActivationSnapshot = await loadSnapshot({
      derive: async () => {
        throw new Error("colonne absente");
      },
    });
    expect(await getActivationSnapshot()).toBeNull();
  });

  // Le masquage ne court-circuite PLUS la dérivation. Il l'a fait, et c'était
  // un contresens : les drapeaux ont un second lecteur, le gate du wizard, qui
  // sans eux laisse passer. Masquer la carte — geste anodin, libellé comme un
  // masquage d'affichage — désactivait donc définitivement le parcours guidé,
  // pour toute l'org, là où le report explicite est borné à la session.
  it("dérive quand même quand la checklist est masquée : dismissed ne pilote que l'affichage", async () => {
    let derived = false;
    const getActivationSnapshot = await loadSnapshot({
      dismissed: true,
      derive: async () => {
        derived = true;
        return FLAGS;
      },
    });
    expect(await getActivationSnapshot()).toEqual({
      flags: FLAGS,
      dismissed: true,
      hasAccessibleSite: true,
      canCreateSite: true,
    });
    expect(derived).toBe(true);
  });

  it("dérive sur le site actif quand l'utilisateur en a un", async () => {
    const seen: [string, string | null][] = [];
    const getActivationSnapshot = await loadSnapshot({
      sites: [{ id: "site_x" }],
      derive: async (orgId, siteId) => {
        seen.push([orgId, siteId]);
        return FLAGS;
      },
    });
    expect(await getActivationSnapshot()).toEqual({
      flags: FLAGS,
      dismissed: false,
      hasAccessibleSite: true,
      canCreateSite: true,
    });
    expect(seen).toEqual([["org_x", "site_x"]]);
  });

  it("signale l'absence de site accessible sans appeler getActiveSiteId", async () => {
    const seen: [string, string | null][] = [];
    const getActivationSnapshot = await loadSnapshot({
      sites: [],
      derive: async (orgId, siteId) => {
        seen.push([orgId, siteId]);
        return { ...FLAGS, "create-site": true };
      },
    });
    const snapshot = await getActivationSnapshot();
    expect(snapshot?.hasAccessibleSite).toBe(false);
    expect(snapshot?.dismissed).toBe(false);
    expect(seen).toEqual([["org_x", null]]);
  });

  // `canCreateSite` décide de la bascule « aucun site attribué ». Il vient de
  // hasFullSiteAccess() et NON du drapeau `create-site` : sans lui, le membre
  // invité dans une org qui n'a pas encore de site (donc `create-site` faux)
  // se voyait proposer « Ajoutez votre site » → Forbidden.
  it("reporte le droit de créer un site, indépendamment du drapeau create-site", async () => {
    const owner = await loadSnapshot({
      sites: [],
      fullSiteAccess: true,
      derive: async () => ({ ...FLAGS, "create-site": false }),
    });
    expect((await owner())?.canCreateSite).toBe(true);

    const member = await loadSnapshot({
      sites: [],
      fullSiteAccess: false,
      derive: async () => ({ ...FLAGS, "create-site": false }),
    });
    const snapshot = await member();
    expect(snapshot?.canCreateSite).toBe(false);
    expect(snapshot?.hasAccessibleSite).toBe(false);
  });
});
