/**
 * AOL/Yahoo answer an over-quota LOGIN with `[LIMIT] LOGIN Rate limit hit.`
 * Before v1.2.240 every lane treated that as transient and retried on its own
 * 60 s cadence, so a refusal was followed by another LOGIN a minute later for
 * as long as it lasted (measured 2026-08-09: four runs, up to 22 minutes,
 * ~one wasted LOGIN per minute).
 *
 * These checks drive the real ImapManager methods over the cooldown state.
 * Run: node packages/mailx-imap/test/rate-limit-backoff.test.mjs
 */
import assert from "node:assert";

const { ImapManager } = await import("file:///Y:/dev/email/mailx/app/packages/mailx-imap/index.js");

const mgr = Object.create(ImapManager.prototype);
mgr.rateLimited = new Map();
const events = [];
mgr.emit = (name, payload) => { events.push({ name, payload }); };

const LIMIT = "Login failed: [LIMIT] LOGIN Rate limit hit.";

// 1. A refusal starts a cooldown, and it is reported (not swallowed).
assert.strictEqual(mgr.rateLimitRemaining("aol"), 0, "clean account has no cooldown");
mgr.noteRateLimited("aol", LIMIT);
const first = mgr.rateLimitRemaining("aol");
assert.ok(first > 4 * 60_000 && first <= 5 * 60_000, `first cooldown should be ~5 min, got ${Math.round(first / 1000)}s`);
assert.ok(events.some(e => e.name === "rateLimited" && e.payload.accountId === "aol"),
    "a cooldown must be announced — a silently paused account looks broken");

// 2. Repeated refusals widen it: 5 → 10 → 20 → 30 (capped).
const mins = [];
for (let i = 0; i < 4; i++) { mgr.noteRateLimited("aol", LIMIT); mins.push(Math.round(mgr.rateLimitRemaining("aol") / 60_000)); }
assert.deepStrictEqual(mins, [10, 20, 30, 30], `backoff should widen and cap at 30, got ${mins.join(",")}`);

// 3. Other accounts are untouched — the limit is per-account, not global.
assert.strictEqual(mgr.rateLimitRemaining("bobma"), 0, "one account's cooldown must not pause another");

// 4. A successful connection clears it, and says so.
mgr.noteRateLimitCleared("aol");
assert.strictEqual(mgr.rateLimitRemaining("aol"), 0, "success must clear the cooldown");
assert.ok(events.some(e => e.name === "rateLimitCleared"), "recovery must be announced too");

// 5. An expired cooldown lapses on its own (no success needed).
mgr.rateLimited.set("aol", { until: Date.now() - 1, strikes: 3 });
assert.strictEqual(mgr.rateLimitRemaining("aol"), 0, "a lapsed cooldown must not block forever");
assert.ok(!mgr.rateLimited.has("aol"), "lapsed entry should be dropped, not left to accumulate");

// 6. THE POINT: while cooling down, newClient must refuse to spend a LOGIN —
//    for every lane, since each one used to discover the limit separately.
mgr.rateLimited.set("aol", { until: Date.now() + 5 * 60_000, strikes: 1 });
let attempted = 0;
mgr.acquireHostSlot = async () => { attempted++; return () => {}; };   // reached only if the guard lets it through
for (const lane of ["sync", "quickCheck", "idle", "outbox", "fetch"]) {
    await assert.rejects(
        () => ImapManager.prototype.newClient.call(mgr, "aol", lane),
        /rate-limited by server/,
        `lane "${lane}" must be refused while the account is cooling down`);
}
assert.strictEqual(attempted, 0, "no lane may reach the connect path during a cooldown");

console.log("rate-limit backoff: 6 scenarios passed");
