import express from "express";
import request from "supertest";
import { describe, it, expect, beforeEach } from "vitest";
import { authLimiter, gameLimiter, lobbyLimiter } from "./rateLimiters.js";

import type { Session, User } from "better-auth";

function createTestApp() {
  const app = express();
  app.set("trust proxy", 1);
  app.use("/api/auth", authLimiter);
  app.all("/api/auth/*splat", (_req, res) => {
    res.status(200).json({ success: true });
  });
  return app;
}

describe("authLimiter", () => {
  let app: ReturnType<typeof createTestApp>;

  beforeEach(() => {
    app = createTestApp();
  });

  it("allows requests under the limit through on sensitive endpoints", async () => {
    const res = await request(app).post("/api/auth/sign-in");
    expect(res.status).toBe(200);
  });

  it("returns 429 after exceeding the limit on sensitive endpoints", async () => {
    const limit = 20;
    for (let i = 0; i < limit; i++) {
      await request(app).post("/api/auth/sign-in");
    }
    const res = await request(app).post("/api/auth/sign-in");
    expect(res.status).toBe(429);
  });

  it("does not rate limit /get-session", async () => {
    const limit = 20;
    for (let i = 0; i < limit + 5; i++) {
      await request(app).get("/api/auth/get-session");
    }
    const res = await request(app).get("/api/auth/get-session");
    expect(res.status).toBe(200);
  });

  it("does not rate limit /sign-out", async () => {
    const limit = 20;
    for (let i = 0; i < limit + 5; i++) {
      await request(app).post("/api/auth/sign-out");
    }
    const res = await request(app).post("/api/auth/sign-out");
    expect(res.status).toBe(200);
  });

  it("does not rate limit OAuth callbacks", async () => {
    const limit = 20;
    for (let i = 0; i < limit + 5; i++) {
      await request(app).get("/api/auth/callback/google");
    }
    const res = await request(app).get("/api/auth/callback/google");
    expect(res.status).toBe(200);
  });

  it("sets RateLimit headers on sensitive responses", async () => {
    const res = await request(app).post("/api/auth/sign-in");
    expect(res.headers).toHaveProperty("ratelimit");
  });
});

function fakeAuth(userId: string) {
  return (
    req: express.Request,
    _res: express.Response,
    next: express.NextFunction,
  ) => {
    req.session = { session: {} as Session, user: { id: userId } as User };
    next();
  };
}

function createGameTestApp(userId = "user-1") {
  const app = express();
  app.set("trust proxy", 1);
  app.use(fakeAuth(userId));
  app.use(gameLimiter);
  app.post("/game/start", (_req, res) =>
    res.status(200).json({ success: true }),
  );
  app.post("/game/answer", (_req, res) =>
    res.status(200).json({ success: true }),
  );
  return app;
}

describe("gameLimiter", () => {
  it("allows requests under the limit through", async () => {
    const app = createGameTestApp();
    const res = await request(app).post("/game/start");
    expect(res.status).toBe(200);
  });

  it("returns 429 after exceeding the limit", async () => {
    const app = createGameTestApp();
    const limit = 150;
    for (let i = 0; i < limit; i++) {
      await request(app).post("/game/answer");
    }
    const res = await request(app).post("/game/answer");
    expect(res.status).toBe(429);
    expect(res.body).toEqual({
      success: false,
      error: "Too many requests, please try again later.",
    });
  });

  it("tracks limits per user, not per IP", async () => {
    const app = express();
    app.set("trust proxy", 1);

    // Two routes, same limiter, different users
    app.use("/user1", fakeAuth("user-1"), gameLimiter, (_req, res) =>
      res.status(200).json({ success: true }),
    );
    app.use("/user2", fakeAuth("user-2"), gameLimiter, (_req, res) =>
      res.status(200).json({ success: true }),
    );

    const limit = 150;
    for (let i = 0; i < limit; i++) {
      await request(app).post("/user1");
    }

    // user-1 is exhausted
    const blocked = await request(app).post("/user1");
    expect(blocked.status).toBe(429);

    // user-2 is unaffected
    const allowed = await request(app).post("/user2");
    expect(allowed.status).toBe(200);
  });
});

function createLobbyTestApp(userId = "user-1") {
  const app = express();
  app.set("trust proxy", 1);
  app.use(fakeAuth(userId));
  app.use(lobbyLimiter);
  app.post("/lobbies", (_req, res) => res.status(200).json({ success: true }));
  app.post("/lobbies/:code/join", (_req, res) =>
    res.status(200).json({ success: true }),
  );
  return app;
}

describe("lobbyLimiter", () => {
  it("allows requests under the limit through", async () => {
    const app = createLobbyTestApp();
    const res = await request(app).post("/lobbies");
    expect(res.status).toBe(200);
  });

  it("returns 429 after exceeding the limit", async () => {
    const app = createLobbyTestApp();
    const limit = 20;
    for (let i = 0; i < limit; i++) {
      await request(app).post("/lobbies");
    }
    const res = await request(app).post("/lobbies");
    expect(res.status).toBe(429);
    expect(res.body).toEqual({
      success: false,
      error: "Too many requests, please try again later.",
    });
  });

  it("tracks limits per user, not per IP", async () => {
    const app = express();
    app.set("trust proxy", 1);

    app.use("/user1", fakeAuth("user-1"), lobbyLimiter, (_req, res) =>
      res.status(200).json({ success: true }),
    );
    app.use("/user2", fakeAuth("user-2"), lobbyLimiter, (_req, res) =>
      res.status(200).json({ success: true }),
    );

    const limit = 20;
    for (let i = 0; i < limit; i++) {
      await request(app).post("/user1");
    }

    const blocked = await request(app).post("/user1");
    expect(blocked.status).toBe(429);

    const allowed = await request(app).post("/user2");
    expect(allowed.status).toBe(200);
  });
});
