// src/lib/onboarding/wizard-gate.test.ts — asservit la porte d'entrée du wizard.
//
// Ce test protège autant la redirection que ses SORTIES : ce projet a déjà
// produit deux enfermements d'opérateur sur des gates de ce type (un écran dont
// l'utilisateur ne pouvait plus sortir). Chaque échappatoire a donc son cas.

import { describe, expect, it } from "bun:test";
import {
  DEFER_DEFAULT_DESTINATION,
  resolveDeferDestination,
  shouldEnterWizard,
  type WizardGateInput,
} from "./wizard-gate";

/** Base « pire cas » : tout est à faire, rien n'empêche d'y aller. */
const base: WizardGateInput = {
  requiredDone: false,
  deferred: false,
  canCreateSite: true,
  isImpersonating: false,
};

describe("shouldEnterWizard", () => {
  it("envoie au wizard quand les étapes requises manquent", () => {
    expect(shouldEnterWizard(base)).toBe(true);
  });

  it("n'y envoie pas quand les étapes requises sont faites", () => {
    expect(shouldEnterWizard({ ...base, requiredDone: true })).toBe(false);
  });

  it("n'y envoie pas quand l'utilisateur a différé", () => {
    expect(shouldEnterWizard({ ...base, deferred: true })).toBe(false);
  });

  it("n'y envoie jamais un membre qui ne peut pas créer de site", () => {
    expect(shouldEnterWizard({ ...base, canCreateSite: false })).toBe(false);
  });

  it("n'y envoie jamais un opérateur en impersonation", () => {
    expect(shouldEnterWizard({ ...base, isImpersonating: true })).toBe(false);
  });

  it("laisse passer l'opérateur même sur un compte cible entièrement vide", () => {
    // Le cas réel du support : la cible n'a rien fait, donc requiredDone est
    // faux et canCreateSite vrai. Sans la garde d'impersonation, l'opérateur
    // serait détourné vers le wizard DU CLIENT — exactement l'enfermement que
    // les gardes existantes du layout (app) évitent déjà.
    expect(
      shouldEnterWizard({
        requiredDone: false,
        deferred: false,
        canCreateSite: true,
        isImpersonating: true,
      }),
    ).toBe(false);
  });

  it("chaque échappatoire suffit seule à ouvrir la porte de sortie", () => {
    // Aucune ne doit dépendre d'une autre : une seule vraie annule la redirection.
    const escapes: readonly Partial<WizardGateInput>[] = [
      { requiredDone: true },
      { deferred: true },
      { canCreateSite: false },
      { isImpersonating: true },
    ];
    for (const exit of escapes) {
      expect(shouldEnterWizard({ ...base, ...exit })).toBe(false);
    }
  });

  it("rouvre le parcours sur un site neuf, mais seulement pour qui peut agir dessus", () => {
    // Un site fraîchement créé a toutes ses étapes de données à faux : le
    // parcours se rouvre (comportement VOULU, pas un effet de bord — la
    // décision produit est « à chaque création de site », pas « à la première »).
    const freshSite = { ...base, requiredDone: false };
    expect(shouldEnterWizard({ ...freshSite, canCreateSite: true })).toBe(true);
    // Le membre voit le même site neuf sans pouvoir en créer : pas de wizard.
    expect(shouldEnterWizard({ ...freshSite, canCreateSite: false })).toBe(
      false,
    );
  });
});

// Chaque cas de refus a son test, et ce n'est pas de la ceinture-bretelles : la
// route pose un cookie de session AVANT de rediriger. Un `to` non validé ferait
// donc de `/onboarding/defer` une redirection ouverte servie par notre domaine.
// Sans ces cas asservis, un futur nettoyage retirerait la garde sans rien
// casser de visible.
describe("resolveDeferDestination", () => {
  it("garde un chemin interne", () => {
    expect(resolveDeferDestination("/keywords/reservoir?import=csv")).toBe(
      "/keywords/reservoir?import=csv",
    );
    expect(resolveDeferDestination("/settings/billing")).toBe(
      "/settings/billing",
    );
  });

  it("retombe sur la destination par défaut sans valeur", () => {
    expect(resolveDeferDestination(null)).toBe(DEFER_DEFAULT_DESTINATION);
    expect(resolveDeferDestination(undefined)).toBe(DEFER_DEFAULT_DESTINATION);
    expect(resolveDeferDestination("")).toBe(DEFER_DEFAULT_DESTINATION);
  });

  it("refuse une URL absolue", () => {
    expect(resolveDeferDestination("https://evil.test/pay")).toBe(
      DEFER_DEFAULT_DESTINATION,
    );
    expect(resolveDeferDestination("javascript:alert(1)")).toBe(
      DEFER_DEFAULT_DESTINATION,
    );
  });

  it("refuse une URL protocole-relatif", () => {
    // `//evil.test` a l'air interne (il commence par « / ») et ne l'est pas :
    // le navigateur y ajoute le schéma courant et part sur un autre domaine.
    expect(resolveDeferDestination("//evil.test")).toBe(
      DEFER_DEFAULT_DESTINATION,
    );
    // Variante à l'antislash, normalisée en slash par les navigateurs.
    expect(resolveDeferDestination("/\\evil.test")).toBe(
      DEFER_DEFAULT_DESTINATION,
    );
  });

  it("refuse toute valeur contenant ://", () => {
    expect(resolveDeferDestination("/redirect?url=https://evil.test")).toBe(
      DEFER_DEFAULT_DESTINATION,
    );
  });

  it("refuse un retour à la ligne ou un caractère de contrôle", () => {
    expect(
      resolveDeferDestination("/keywords\nLocation: https://evil.test"),
    ).toBe(DEFER_DEFAULT_DESTINATION);
    expect(resolveDeferDestination("/keywords\r\n")).toBe(
      DEFER_DEFAULT_DESTINATION,
    );
    expect(resolveDeferDestination("/keywords\t")).toBe(
      DEFER_DEFAULT_DESTINATION,
    );
  });

  it("refuse un chemin relatif", () => {
    // Résolu contre /onboarding/defer, il n'atterrirait pas où on croit.
    expect(resolveDeferDestination("keywords/planning")).toBe(
      DEFER_DEFAULT_DESTINATION,
    );
  });
});
