import { describe, it, expect } from "vitest";

process.env.ADMIN_JWT_SECRET = "secret-de-test-suffisamment-long-pour-passer-32";

const { createFullAppWith } = await import("./helpers/testApp.js");
const { makeFakePool, rows } = await import("./helpers/fakePool.js");

/**
 * Les autres fichiers de test partagent une app avec des plafonds de débit
 * neutralisés (sinon l'état du limiteur fuiterait d'un cas à l'autre). C'EST
 * ICI, et seulement ici, que les vraies valeurs de production sont vérifiées —
 * sinon la paramétrisation introduite pour la performance créerait un angle
 * mort sur une protection de sécurité.
 */

const TOKEN = "jeton-de-test";

async function appWithRealLimits(overrides = {}) {
  return createFullAppWith({
    pool: makeFakePool([{ match: /./, result: rows({ total: 0 }) }]),
    importToken: TOKEN,
    ...overrides,
  });
}

describe("plafond global", () => {
  it("répond 429 au-delà du plafond", async () => {
    const app = await appWithRealLimits({ globalRateLimitMax: 3 });
    const codes = [];
    for (let i = 0; i < 5; i++) {
      codes.push((await app.inject({ method: "GET", url: "/api/health" })).statusCode);
    }
    expect(codes).toEqual([200, 200, 200, 429, 429]);
    await app.close();
  });

  it("le 429 traverse le gestionnaire d'erreurs sans devenir un 500", async () => {
    // Régression : setErrorHandler écrasait tous les statuts en 500, ce qui
    // transformait une limitation de débit en fausse panne serveur.
    const app = await appWithRealLimits({ globalRateLimitMax: 1 });
    await app.inject({ method: "GET", url: "/api/health" });
    const res = await app.inject({ method: "GET", url: "/api/health" });
    expect(res.statusCode).toBe(429);
    expect(res.json().ok).toBe(false);
    await app.close();
  });

  it("la valeur de production par défaut est 1000/minute", async () => {
    const app = await appWithRealLimits();
    const res = await app.inject({ method: "GET", url: "/api/health" });
    expect(res.headers["x-ratelimit-limit"]).toBe("1000");
    await app.close();
  });
});

describe("plafond de /admin/import", () => {
  it("la valeur de production par défaut est 10/minute", async () => {
    const app = await appWithRealLimits();
    const res = await app.inject({
      method: "POST", url: "/admin/import",
      headers: { authorization: `Bearer ${TOKEN}` }, payload: { bands: [] },
    });
    expect(res.headers["x-ratelimit-limit"]).toBe("10");
    await app.close();
  });

  it("limite par jeton, pas globalement", async () => {
    const app = await appWithRealLimits({ adminRateLimitMax: 2 });
    const call = (token) =>
      app.inject({
        method: "POST", url: "/admin/import",
        headers: { authorization: `Bearer ${token}` }, payload: { bands: [] },
      });

    await call(TOKEN);
    await call(TOKEN);
    // Le 3e appel avec CE jeton est bloqué…
    expect((await call(TOKEN)).statusCode).toBe(429);
    // …mais un autre jeton dispose de son propre compteur (il sera rejeté en
    // 401, pas en 429 : c'est bien l'authentification qui tranche, pas le débit).
    expect((await call("un-autre-jeton-de-la-meme-taille")).statusCode).toBe(401);
    await app.close();
  });
});

describe("plafond de /admin/auth/login", () => {
  it("la valeur de production par défaut est 10/minute", async () => {
    const app = await appWithRealLimits();
    const res = await app.inject({
      method: "POST", url: "/admin/auth/login", payload: { username: "", password: "" },
    });
    expect(res.headers["x-ratelimit-limit"]).toBe("10");
    await app.close();
  });

  // Le verrouillage de compte (5 échecs / 15 min, en base) et le rate-limit HTTP
  // sont deux protections distinctes : celle-ci s'applique même à des requêtes
  // malformées qui n'atteignent jamais la base.
  it("limite le bruteforce même sur des corps invalides", async () => {
    const app = await appWithRealLimits({ authRateLimitMax: 2 });
    const call = () =>
      app.inject({ method: "POST", url: "/admin/auth/login", payload: {} });

    expect((await call()).statusCode).toBe(400);
    expect((await call()).statusCode).toBe(400);
    expect((await call()).statusCode).toBe(429);
    await app.close();
  });

  it("limite par IP", async () => {
    const app = await appWithRealLimits({ authRateLimitMax: 1 });
    const call = (ip) =>
      app.inject({
        method: "POST", url: "/admin/auth/login",
        headers: { "x-forwarded-for": ip },
        payload: {},
      });

    expect((await call("1.2.3.4")).statusCode).toBe(400);
    expect((await call("1.2.3.4")).statusCode).toBe(429);
    // Une autre IP garde son propre quota (trustProxy est actif : l'app est
    // derrière Traefik, l'IP réelle vient de X-Forwarded-For).
    expect((await call("5.6.7.8")).statusCode).toBe(400);
    await app.close();
  });
});
