import { describe, expect, it } from "bun:test";
import type { TourDef } from "./catalog";
import {
  anchorSelector,
  buildDriveSteps,
  CONTENT_SELECTOR,
  EMPTY_TOUR_SESSION,
  isReplayRequested,
  planTour,
  refuseReplay,
  resolveTourSteps,
  sessionAfterStart,
  sessionAfterTeardown,
  TOUR_LIFT_ATTRIBUTE,
  TOUR_LIFT_SELECTOR,
  type TourSession,
} from "./plan";

const TOUR: TourDef = {
  id: "reservoir",
  route: "/keywords/reservoir",
  steps: [
    { anchor: "reservoir-table", key: "whatIsIt" },
    { anchor: "keywords-import", key: "import" },
    { anchor: "keywords-sync", key: "sync" },
  ],
};

/** Faux document : seules les ancres listées existent. */
const present =
  (...anchors: string[]) =>
  (anchor: string) =>
    anchors.includes(anchor);

const ALL = present("reservoir-table", "keywords-import", "keywords-sync");

/** Demande de relance émise par le bouton « revoir ». */
const demande = (tourId: string, nonce = 1) => ({ tourId, nonce });

/** Session de navigation : ce que le tutoriel a déjà fait sur cette visite. */
const session = (
  startedTourIds: readonly string[],
  consumedReplayNonce = 0,
): TourSession => ({ startedTourIds, consumedReplayNonce });

const base = {
  tour: TOUR,
  status: undefined,
  replayRequest: null,
  session: EMPTY_TOUR_SESSION,
  anotherTourRunning: false,
  isAnchorPresent: ALL,
  pageDeclaresContent: true,
} as const;

describe("anchorSelector", () => {
  it("ne vise QUE data-tour, jamais une classe utilitaire", () => {
    expect(anchorSelector("reservoir-table")).toBe(
      '[data-tour="reservoir-table"]',
    );
    expect(CONTENT_SELECTOR).toBe('[data-tour-has-content="true"]');
  });
});

describe("resolveTourSteps", () => {
  it("garde tous les points quand toutes les ancres sont là", () => {
    expect(resolveTourSteps(TOUR, ALL).map((s) => s.key)).toEqual([
      "whatIsIt",
      "import",
      "sync",
    ]);
  });

  it("laisse tomber SILENCIEUSEMENT les points dont l'ancre manque", () => {
    // Cas réel attendu : keywords-import / keywords-sync / planning-dispatch
    // doivent être posées sur des déclencheurs. Si la tâche 4 se trompe de
    // cible, le tour se contente de rétrécir — il ne casse pas.
    expect(
      resolveTourSteps(TOUR, present("reservoir-table", "keywords-sync")).map(
        (s) => s.key,
      ),
    ).toEqual(["whatIsIt", "sync"]);
  });

  it("préserve l'ordre du catalogue, pas celui du document", () => {
    expect(
      resolveTourSteps(TOUR, present("keywords-sync", "reservoir-table")).map(
        (s) => s.anchor,
      ),
    ).toEqual(["reservoir-table", "keywords-sync"]);
  });

  it("rend une liste vide quand aucune ancre n'existe", () => {
    expect(resolveTourSteps(TOUR, present())).toEqual([]);
  });

  it("porte le sélecteur data-tour déjà construit", () => {
    expect(resolveTourSteps(TOUR, present("keywords-import"))).toEqual([
      {
        anchor: "keywords-import",
        key: "import",
        element: '[data-tour="keywords-import"]',
        lift: false,
      },
    ]);
  });
});

/**
 * La remontée dans l'ordre de peinture.
 *
 * Le halo d'un point ancré sous une bande collante ne montre que le fond de la
 * bande : `driver.css` ne donne aucun z-index à l'élément qu'il met en avant.
 * Le remède est un attribut posé le temps du point — et la décision de le
 * poser vit ICI, parce qu'écrite dans le composant elle serait hors d'atteinte
 * des tests (ni DOM, ni driver.js, ni cycle de vie React en environnement de
 * test).
 */
describe("remontée au-dessus de la bande collante", () => {
  const SOUS_LA_BANDE: TourDef = {
    ...TOUR,
    steps: [
      { anchor: "reservoir-table", key: "whatIsIt" },
      { anchor: "keywords-import", key: "import", underStickyBand: true },
    ],
  };

  it("nomme un ATTRIBUT, jamais une classe utilitaire", () => {
    // Même règle que l'ancrage : un remaniement de style ne doit pas éteindre
    // le remède en silence. Le sélecteur du balayage doit désigner cet attribut
    // et rien d'autre.
    expect(TOUR_LIFT_ATTRIBUTE).toBe("data-tour-lift");
    expect(TOUR_LIFT_SELECTOR).toBe(`[${TOUR_LIFT_ATTRIBUTE}]`);
  });

  it("remonte le point que le catalogue déclare sous la bande", () => {
    expect(resolveTourSteps(SOUS_LA_BANDE, ALL).map((s) => s.lift)).toEqual([
      false,
      true,
    ]);
  });

  it("ne remonte RIEN par défaut", () => {
    // Le second bord : une remontée générale ferait peindre le conteneur du
    // tableau (point 1 des trois tours) par-dessus la bande, dont il masquerait
    // la barre d'outils pendant tout le point.
    expect(resolveTourSteps(TOUR, ALL).map((s) => s.lift)).toEqual([
      false,
      false,
      false,
    ]);
  });

  it("porte la décision jusqu'aux points remis à driver.js", () => {
    // Le maillon qui manquait ferait tout le reste juste et sans effet : la
    // décision doit arriver INTACTE à ce que le runner passe à la
    // bibliothèque.
    expect(
      buildDriveSteps(
        "reservoir",
        resolveTourSteps(SOUS_LA_BANDE, ALL),
        (k) => k,
      ).map((s) => s.lift),
    ).toEqual([false, true]);
  });
});

describe("planTour", () => {
  it("démarre à la première visite d'une page qui a du contenu", () => {
    const verdict = planTour(base);
    expect(verdict.kind).toBe("start");
    if (verdict.kind !== "start") throw new Error("unreachable");
    expect(verdict.tourId).toBe("reservoir");
    expect(verdict.steps).toHaveLength(3);
  });

  it("ne démarre rien hors des routes du catalogue", () => {
    expect(planTour({ ...base, tour: null }).kind).toBe("no-tour");
  });

  it("REPORTE quand la page ne se déclare pas non vide", () => {
    expect(planTour({ ...base, pageDeclaresContent: false }).kind).toBe(
      "defer-empty",
    );
  });

  it("REPORTE quand l'ancre du premier point est absente", () => {
    expect(
      planTour({
        ...base,
        isAnchorPresent: present("keywords-import", "keywords-sync"),
      }).kind,
    ).toBe("defer-no-anchor");
  });

  it("l'écran vide l'emporte sur un statut déjà enregistré", () => {
    // Précédence identique à shouldStartTour : le verdict rendu doit rester un
    // REPORT, jamais un « déjà vu » — sans quoi un appelant tenté d'enregistrer
    // sur « déjà vu » brûlerait le tour d'un utilisateur au tableau vide.
    expect(
      planTour({ ...base, status: "completed", pageDeclaresContent: false })
        .kind,
    ).toBe("defer-empty");
  });

  it("ne rejoue pas un tour terminé", () => {
    expect(planTour({ ...base, status: "completed" }).kind).toBe(
      "already-seen",
    );
  });

  it("ne rejoue pas un tour rejeté", () => {
    expect(planTour({ ...base, status: "dismissed" }).kind).toBe(
      "already-seen",
    );
  });

  it("n'en démarre pas un second par-dessus un tour en cours", () => {
    expect(planTour({ ...base, anotherTourRunning: true }).kind).toBe("busy");
  });

  it("ne relance pas un tour déjà lancé pendant cette session de navigation", () => {
    expect(planTour({ ...base, session: session(["reservoir"]) }).kind).toBe(
      "already-started",
    );
  });

  it("un tour lancé sur une AUTRE route ne bloque pas celui-ci", () => {
    expect(planTour({ ...base, session: session(["planning"]) }).kind).toBe(
      "start",
    );
  });

  it("REPORTE un écran vide même quand l'ancre manque aussi", () => {
    // LE cas discriminant de l'ordre des deux reports dans planTour. Un
    // tableau à zéro ligne rend souvent les deux vrais en même temps : ni le
    // conteneur ne se déclare non vide, ni la ligne d'ancrage n'existe. Sans
    // cette assertion, supprimer le report d'écran vide laissait tout vert —
    // la précédence revendiquée par le commentaire n'était observée nulle part.
    expect(
      planTour({
        ...base,
        pageDeclaresContent: false,
        isAnchorPresent: present(),
      }).kind,
    ).toBe("defer-empty");
  });

  describe("relance explicite", () => {
    it("passe outre le statut enregistré", () => {
      expect(
        planTour({
          ...base,
          status: "completed",
          replayRequest: demande("reservoir"),
        }).kind,
      ).toBe("start");
    });

    it("passe outre le fait de l'avoir déjà lancé sur cette route", () => {
      expect(
        planTour({
          ...base,
          session: session(["reservoir"]),
          replayRequest: demande("reservoir"),
        }).kind,
      ).toBe("start");
    });

    it("ne vaut QUE pour le tour qu'elle nomme", () => {
      // La portée par tour EST la décision : une demande globale ferait
      // ignorer le statut enregistré de tous les autres tours. Elle vit ici,
      // et non dans le composant, précisément pour que ce test l'atteigne.
      expect(
        planTour({
          ...base,
          status: "completed",
          replayRequest: demande("planning"),
        }).kind,
      ).toBe("already-seen");
      expect(
        planTour({
          ...base,
          session: session(["reservoir"]),
          replayRequest: demande("planning"),
        }).kind,
      ).toBe("already-started");
    });

    it("ne passe PAS outre l'écran vide", () => {
      expect(
        planTour({
          ...base,
          status: "completed",
          replayRequest: demande("reservoir"),
          pageDeclaresContent: false,
        }).kind,
      ).toBe("defer-empty");
    });

    it("ne s'ouvre pas par-dessus un tour déjà en cours", () => {
      expect(
        planTour({
          ...base,
          replayRequest: demande("reservoir"),
          anotherTourRunning: true,
        }).kind,
      ).toBe("busy");
    });
  });

  it("ne rend que les points dont l'ancre existe", () => {
    const verdict = planTour({
      ...base,
      isAnchorPresent: present("reservoir-table", "keywords-sync"),
    });
    if (verdict.kind !== "start") throw new Error("attendu: start");
    expect(verdict.steps.map((s) => s.key)).toEqual(["whatIsIt", "sync"]);
  });

  it("aucun verdict de refus ne porte de statut à enregistrer", () => {
    // Garde de forme : le seul verdict qui transporte des données est "start".
    // Rien dans un refus ne peut être pris pour un ordre d'écriture.
    for (const input of [
      { ...base, tour: null },
      { ...base, pageDeclaresContent: false },
      { ...base, isAnchorPresent: present() },
      { ...base, status: "completed" as const },
      { ...base, anotherTourRunning: true },
      { ...base, session: session(["reservoir"]) },
    ]) {
      const verdict = planTour(input);
      expect(verdict.kind).not.toBe("start");
      expect(Object.keys(verdict)).toEqual(["kind"]);
    }
  });
});

describe("isReplayRequested", () => {
  it("sert une demande fraîche qui nomme ce tour", () => {
    expect(
      isReplayRequested(demande("reservoir"), EMPTY_TOUR_SESSION, "reservoir"),
    ).toBe(true);
  });

  it("ignore une demande qui nomme un autre tour", () => {
    expect(
      isReplayRequested(demande("planning"), EMPTY_TOUR_SESSION, "reservoir"),
    ).toBe(false);
  });

  it("ignore une demande déjà servie", () => {
    expect(
      isReplayRequested(demande("reservoir", 1), session([], 1), "reservoir"),
    ).toBe(false);
  });

  it("sert une NOUVELLE demande du même tour", () => {
    // Le nonce est ce qui distingue deux clics successifs sur « revoir ».
    expect(
      isReplayRequested(demande("reservoir", 2), session([], 1), "reservoir"),
    ).toBe(true);
  });

  it("ne sert rien quand rien n'a été demandé", () => {
    expect(isReplayRequested(null, EMPTY_TOUR_SESSION, "reservoir")).toBe(
      false,
    );
  });
});

describe("session de navigation", () => {
  it("consomme la demande de relance au démarrage", () => {
    expect(
      sessionAfterStart(EMPTY_TOUR_SESSION, "reservoir", demande("reservoir")),
    ).toEqual({ startedTourIds: ["reservoir"], consumedReplayNonce: 1 });
  });

  it("ne consomme pas la demande d'un AUTRE tour", () => {
    expect(
      sessionAfterStart(EMPTY_TOUR_SESSION, "reservoir", demande("planning")),
    ).toEqual({ startedTourIds: ["reservoir"], consumedReplayNonce: 0 });
  });

  it("empile les tours lancés au lieu de les remplacer", () => {
    // La mémoire vaut pour TOUS les tours à la fois : les trois routes sont
    // sœurs, et le layout qui porte la progression du serveur n'est pas
    // re-rendu quand on passe de l'une à l'autre.
    const apres = sessionAfterStart(
      session(["planning"], 3),
      "reservoir",
      null,
    );
    expect(apres.startedTourIds).toEqual(["planning", "reservoir"]);
  });

  it("n'inscrit pas deux fois le même tour", () => {
    const apres = sessionAfterStart(
      session(["reservoir"]),
      "reservoir",
      demande("reservoir"),
    );
    expect(apres.startedTourIds).toEqual(["reservoir"]);
  });

  it("ANNULE un démarrage que l'utilisateur n'a pas consommé", () => {
    const avant = session(["planning"], 3);
    const apres = sessionAfterStart(
      avant,
      "reservoir",
      demande("reservoir", 4),
    );
    // Le tour annulé sort de la mémoire — et LUI SEUL : le souvenir du tour
    // lu sur la page voisine doit y rester, sans quoi il se rejouerait au
    // premier aller-retour.
    expect(sessionAfterTeardown(avant, apres, true).startedTourIds).toEqual([
      "planning",
    ]);
  });

  it("NE REND PAS la marque de consommation d'une relance abandonnée", () => {
    // Les deux champs vivaient dans le même objet et la restitution les rendait
    // ENSEMBLE. Conséquence vue au navigateur (2026-07-31, reproduite trois
    // fois) : cliquer « revoir » puis quitter la page sans terminer ni fermer
    // laissait la demande fraîche, et le tour se rouvrait à CHAQUE retour, pour
    // toute la session.
    //
    // Les deux comportements sont découplés : une reprise DEMANDÉE et AFFICHÉE
    // est consommée, point. C'est un geste explicite ; quitter une page n'en
    // est pas un.
    const avant = session(["planning"], 3);
    const apres = sessionAfterStart(
      avant,
      "reservoir",
      demande("reservoir", 4),
    );
    expect(sessionAfterTeardown(avant, apres, true).consumedReplayNonce).toBe(
      4,
    );
  });

  it("CONFIRME un démarrage que l'utilisateur a terminé ou rejeté", () => {
    const avant = session(["planning"], 3);
    const apres = sessionAfterStart(
      avant,
      "reservoir",
      demande("reservoir", 4),
    );
    expect(sessionAfterTeardown(avant, apres, false)).toEqual(apres);
  });

  // ── Les trois parcours que la session doit tenir ─────────────────────────

  it("le double montage du mode strict rejoue le tour au second passage", () => {
    // React monte, démonte puis remonte chaque effet en développement. Le
    // premier passage démarre le tour ; le nettoyage le détruit ALORS QU'IL EST
    // ENCORE À L'ÉCRAN — l'utilisateur n'a rien vu, rien fermé. Sans la
    // restitution, le second passage lit un tour « déjà lancé » et n'affiche
    // plus rien : charger directement une route à tour ne montre RIEN, et pour
    // toute la session.
    const avant = EMPTY_TOUR_SESSION;
    expect(planTour({ ...base, session: avant }).kind).toBe("start");

    const apres = sessionAfterStart(avant, "reservoir", null);
    expect(planTour({ ...base, session: apres }).kind).toBe("already-started");

    const rendue = sessionAfterTeardown(avant, apres, true);
    expect(planTour({ ...base, session: rendue }).kind).toBe("start");
  });

  it("un tour terminé ne se rouvre pas au retour sur la page", () => {
    // L'écriture en base ne revalide aucune route : la progression reçue par
    // le composant reste PÉRIMÉE (`status: undefined`) jusqu'au prochain rendu
    // serveur. C'est la session confirmée, et elle seule, qui tient la porte.
    const avant = EMPTY_TOUR_SESSION;
    const apres = sessionAfterStart(avant, "reservoir", null);
    const confirmee = sessionAfterTeardown(avant, apres, false);
    expect(
      planTour({ ...base, session: confirmee, status: undefined }).kind,
    ).toBe("already-started");
  });

  it("une relance jouée jusqu'au bout ne se rouvre pas à chaque retour", () => {
    // Le parcours signalé deux fois : l'utilisateur termine un tour, clique
    // « revoir », le rejoue, part et revient. Tant que la demande n'était pas
    // consommée, elle restait vraie pour TOUTE la session et rouvrait le tour
    // à chaque retour, sans qu'on l'ait redemandé.
    const requete = demande("reservoir", 1);
    const avant = session(["reservoir"]);
    expect(
      planTour({
        ...base,
        session: avant,
        status: "completed",
        replayRequest: requete,
      }).kind,
    ).toBe("start");

    const apres = sessionAfterStart(avant, "reservoir", requete);
    const confirmee = sessionAfterTeardown(avant, apres, false);
    expect(
      planTour({
        ...base,
        session: confirmee,
        status: "completed",
        replayRequest: requete,
      }).kind,
    ).toBe("already-started");
  });

  // ── Ce que la session doit tenir D'UNE PAGE À L'AUTRE ────────────────────
  //
  // Ces trois-là ne sont pas des variantes des précédents : ils font intervenir
  // DEUX tours. La progression lue au serveur descend du layout (app), qu'une
  // navigation cliente entre routes sœurs NE RE-REND PAS — le statut reste
  // périmé pour toute la session. La mémoire de session est donc la seule
  // gardienne, et elle doit tenir sur TOUS les tours à la fois, pas sur le
  // dernier en date.

  it("un tour terminé ne se rejoue pas après un détour par une AUTRE page", () => {
    // Le parcours signalé : terminer Réservoir, aller sur Recherche (dont le
    // tour démarre puis se termine), revenir sur Réservoir. Tant que la session
    // n'avait qu'un créneau, le démarrage de Recherche ÉCRASAIT le souvenir de
    // Réservoir et le tour terminé trente secondes plus tôt se rejouait — à
    // chaque aller-retour.
    const neuf = EMPTY_TOUR_SESSION;
    const reservoirLance = sessionAfterStart(neuf, "reservoir", null);
    const apresReservoir = sessionAfterTeardown(neuf, reservoirLance, false);

    const rechercheLance = sessionAfterStart(apresReservoir, "recherche", null);
    const apresRecherche = sessionAfterTeardown(
      apresReservoir,
      rechercheLance,
      false,
    );

    expect(
      planTour({ ...base, session: apresRecherche, status: undefined }).kind,
    ).toBe("already-started");
  });

  it("quitter une AUTRE page sans lire son tour n'efface pas les tours déjà lus", () => {
    // Même parcours, mais l'utilisateur quitte Recherche sans terminer :
    // la restitution ne doit retirer QUE le démarrage abandonné.
    const neuf = EMPTY_TOUR_SESSION;
    const reservoirLance = sessionAfterStart(neuf, "reservoir", null);
    const apresReservoir = sessionAfterTeardown(neuf, reservoirLance, false);

    const rechercheLance = sessionAfterStart(apresReservoir, "recherche", null);
    const abandonnee = sessionAfterTeardown(
      apresReservoir,
      rechercheLance,
      true,
    );

    // Réservoir reste lu…
    expect(
      planTour({ ...base, session: abandonnee, status: undefined }).kind,
    ).toBe("already-started");
    // …et Recherche est bien reproposé.
    expect(
      planTour({
        ...base,
        tour: { ...TOUR, id: "recherche" },
        session: abandonnee,
        status: undefined,
      }).kind,
    ).toBe("start");
  });

  it("une reprise abandonnée ne rend pas rejouable un tour déjà lu", () => {
    // Le tour a été lu AVANT le démarrage : la restitution retire ce que ce
    // démarrage a ajouté, et rien de plus — sans quoi une reprise abandonnée
    // effacerait le souvenir de la lecture d'origine.
    const requete = demande("reservoir", 1);
    const dejaLu = sessionAfterTeardown(
      EMPTY_TOUR_SESSION,
      sessionAfterStart(EMPTY_TOUR_SESSION, "reservoir", null),
      false,
    );

    const repriseLancee = sessionAfterStart(dejaLu, "reservoir", requete);
    const abandonnee = sessionAfterTeardown(dejaLu, repriseLancee, true);

    expect(
      planTour({ ...base, session: abandonnee, status: undefined }).kind,
    ).toBe("already-started");
  });

  it("une relance ABANDONNÉE ne se rouvre pas non plus au retour", () => {
    // Le parcours exact du défaut : un tour déjà terminé, un clic sur
    // « revoir », puis un départ SANS terminer ni fermer. Le tour a bien été
    // affiché — la demande est servie une fois, point.
    const requete = demande("reservoir", 1);
    const enCours = {
      ...base,
      status: "completed" as const,
      replayRequest: requete,
    };

    const avant = EMPTY_TOUR_SESSION;
    expect(planTour({ ...enCours, session: avant }).kind).toBe("start");

    const apres = sessionAfterStart(avant, "reservoir", requete);
    // `true` : driver.js était ENCORE à l'écran au démontage — l'utilisateur
    // a changé de page sans rien fermer.
    const rendue = sessionAfterTeardown(avant, apres, true);

    expect(planTour({ ...enCours, session: rendue }).kind).toBe("already-seen");
  });
});

describe("demande de reprise que le verdict n'honore pas", () => {
  const requete = demande("reservoir", 7);
  /** Écran encore vide : « revoir ce tour » rend un REPORT, pas un tour. */
  const ecranVide = {
    ...base,
    replayRequest: requete,
    pageDeclaresContent: false,
  };

  it("CONSOMME la demande au lieu de la laisser armée", () => {
    // Le nonce n'était consommé qu'au DÉMARRAGE. Sur un écran vide, la demande
    // survivait donc au refus : après un import, le premier aller-retour venu
    // relançait l'effet et le tour s'ouvrait sans qu'on vienne de le demander.
    const verdict = planTour(ecranVide);
    expect(verdict.kind).toBe("defer-empty");

    const suite = refuseReplay({
      verdict,
      session: EMPTY_TOUR_SESSION,
      request: requete,
      tour: TOUR,
    });
    expect(suite.session.consumedReplayNonce).toBe(7);

    // L'écran se remplit : la vieille demande ne rouvre plus rien.
    expect(
      planTour({
        ...base,
        session: suite.session,
        replayRequest: requete,
        status: "completed",
      }).kind,
    ).toBe("already-seen");
  });

  it("PRÉVIENT l'utilisateur — un bouton muet n'est pas une porte de sortie", () => {
    expect(
      refuseReplay({
        verdict: planTour(ecranVide),
        session: EMPTY_TOUR_SESSION,
        request: requete,
        tour: TOUR,
      }).notify,
    ).toBe(true);
  });

  it("ne consomme ni ne prévient quand le tour a bien démarré", () => {
    const verdict = planTour({ ...base, replayRequest: requete });
    expect(verdict.kind).toBe("start");
    expect(
      refuseReplay({
        verdict,
        session: EMPTY_TOUR_SESSION,
        request: requete,
        tour: TOUR,
      }),
    ).toEqual({ session: EMPTY_TOUR_SESSION, notify: false });
  });

  it("ne consomme ni ne prévient pour la demande d'un AUTRE tour", () => {
    // Le refus est prononcé sur la page de CE tour-ci ; la demande qui nomme
    // le voisin doit rester intacte, elle sera servie là-bas.
    const autre = demande("planning", 7);
    expect(
      refuseReplay({
        verdict: planTour({ ...base, pageDeclaresContent: false }),
        session: EMPTY_TOUR_SESSION,
        request: autre,
        tour: TOUR,
      }),
    ).toEqual({ session: EMPTY_TOUR_SESSION, notify: false });
  });

  it("ne prévient pas deux fois de la même demande", () => {
    // Le mode strict (développement) rejoue l'effet : le second passage lit une
    // demande DÉJÀ servie et ne doit plus rien annoncer.
    const dejaServie = session([], 7);
    expect(
      refuseReplay({
        verdict: planTour({ ...ecranVide, session: dejaServie }),
        session: dejaServie,
        request: requete,
        tour: TOUR,
      }),
    ).toEqual({ session: dejaServie, notify: false });
  });

  it("ne prévient de rien hors des routes du catalogue", () => {
    expect(
      refuseReplay({
        verdict: planTour({ ...base, tour: null }),
        session: EMPTY_TOUR_SESSION,
        request: requete,
        tour: null,
      }),
    ).toEqual({ session: EMPTY_TOUR_SESSION, notify: false });
  });

  it("ne prévient de rien quand rien n'a été demandé", () => {
    expect(
      refuseReplay({
        verdict: planTour({ ...base, pageDeclaresContent: false }),
        session: EMPTY_TOUR_SESSION,
        request: null,
        tour: TOUR,
      }),
    ).toEqual({ session: EMPTY_TOUR_SESSION, notify: false });
  });
});

describe("buildDriveSteps", () => {
  const steps = resolveTourSteps(TOUR, present("reservoir-table"));

  it("compose les clés i18n sous <tourId>.<key>.title|body", () => {
    expect(buildDriveSteps("reservoir", steps, (k) => `«${k}»`)).toEqual([
      {
        element: '[data-tour="reservoir-table"]',
        popover: {
          title: "«reservoir.whatIsIt.title»",
          description: "«reservoir.whatIsIt.body»",
        },
        lift: false,
      },
    ]);
  });

  it("rend une liste vide pour une liste de points vide", () => {
    expect(buildDriveSteps("reservoir", [], (k) => k)).toEqual([]);
  });
});
