import { redisTest } from "@internal/testcontainers";
import { describe, expect, vi, beforeEach } from "vitest";

vi.setConfig({ testTimeout: 30_000 }); // 30 seconds timeout

// Mock the logger
vi.mock("./logger.server", () => ({
  logger: {
    info: vi.fn(),
    error: vi.fn(),
  },
}));

import type { Express } from "express";
import express from "express";
import request from "supertest";
import { authorizationRateLimitMiddleware } from "../app/services/authorizationRateLimitMiddleware.server.js";

describe.skipIf(process.env.GITHUB_ACTIONS)("authorizationRateLimitMiddleware", () => {
  let app: Express;

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

  redisTest("should allow requests within the rate limit", async ({ redisOptions }) => {
    const rateLimitMiddleware = authorizationRateLimitMiddleware({
      redis: redisOptions,
      keyPrefix: "test",
      defaultLimiter: {
        type: "tokenBucket",
        refillRate: 10,
        interval: "1m",
        maxTokens: 100,
      },
      pathMatchers: [/^\/api/],
      log: {
        rejections: false,
        requests: false,
      },
    });

    app.use(rateLimitMiddleware);
    app.get("/api/test", (req, res) => {
      res.status(200).json({ message: "Success" });
    });

    const response = await request(app).get("/api/test").set("Authorization", "Bearer test-token");

    expect(response.status).toBe(200);
    expect(response.body).toEqual({ message: "Success" });
    expect(response.headers["x-ratelimit-limit"]).toBeDefined();
    expect(response.headers["x-ratelimit-remaining"]).toBeDefined();
    expect(response.headers["x-ratelimit-reset"]).toBeDefined();
  });

  redisTest("should reject requests without an Authorization header", async ({ redisOptions }) => {
    const rateLimitMiddleware = authorizationRateLimitMiddleware({
      redis: redisOptions,
      keyPrefix: "test",
      defaultLimiter: {
        type: "tokenBucket",
        refillRate: 10,
        interval: "1m",
        maxTokens: 100,
      },
      pathMatchers: [/^\/api/],
    });

    app.use(rateLimitMiddleware);
    app.get("/api/test", (req, res) => {
      res.status(200).json({ message: "Success" });
    });

    const response = await request(app).get("/api/test");

    expect(response.status).toBe(401);
    expect(response.body).toHaveProperty("title", "Unauthorized");
  });

  redisTest("should reject requests that exceed the rate limit", async ({ redisOptions }) => {
    const rateLimitMiddleware = authorizationRateLimitMiddleware({
      redis: redisOptions,
      keyPrefix: "test",
      defaultLimiter: {
        type: "tokenBucket",
        refillRate: 1,
        interval: "1m",
        maxTokens: 1,
      },
      pathMatchers: [/^\/api/],
    });

    app.use(rateLimitMiddleware);
    app.get("/api/test", (req, res) => {
      res.status(200).json({ message: "Success" });
    });

    // First request should succeed
    await request(app).get("/api/test").set("Authorization", "Bearer test-token");

    // Second request should be rate limited
    const response = await request(app).get("/api/test").set("Authorization", "Bearer test-token");

    expect(response.status).toBe(429);
    expect(response.body).toHaveProperty("title", "Rate Limit Exceeded");
  });

  redisTest("should not apply rate limiting to whitelisted paths", async ({ redisOptions }) => {
    const rateLimitMiddleware = authorizationRateLimitMiddleware({
      redis: redisOptions,
      keyPrefix: "test",
      defaultLimiter: {
        type: "tokenBucket",
        refillRate: 10,
        interval: "1m",
        maxTokens: 100,
      },
      pathMatchers: [/^\/api/],
      pathWhiteList: ["/api/whitelist"],
    });

    app.use(rateLimitMiddleware);
    app.get("/api/whitelist", (req, res) => {
      res.status(200).json({ message: "Whitelisted" });
    });

    const response = await request(app)
      .get("/api/whitelist")
      .set("Authorization", "Bearer test-token");

    expect(response.status).toBe(200);
    expect(response.body).toEqual({ message: "Whitelisted" });
    expect(response.headers["x-ratelimit-limit"]).toBeUndefined();
  });

  redisTest(
    "should apply different rate limits based on limiterConfigOverride",
    async ({ redisOptions }) => {
      const rateLimitMiddleware = authorizationRateLimitMiddleware({
        redis: redisOptions,
        keyPrefix: "test",
        defaultLimiter: {
          type: "tokenBucket",
          refillRate: 1,
          interval: "1m",
          maxTokens: 1,
        },
        pathMatchers: [/^\/api/],
        limiterConfigOverride: async (authorizationValue) => {
          if (authorizationValue === "Bearer premium-token") {
            return {
              type: "tokenBucket",
              refillRate: 10,
              interval: "1m",
              maxTokens: 100,
            };
          }
          return undefined;
        },
      });

      app.use(rateLimitMiddleware);
      app.get("/api/test", (req, res) => {
        res.status(200).json({ message: "Success" });
      });

      // Regular user should be rate limited after 1 request
      await request(app).get("/api/test").set("Authorization", "Bearer regular-token");
      const regularResponse = await request(app)
        .get("/api/test")
        .set("Authorization", "Bearer regular-token");
      expect(regularResponse.status).toBe(429);

      // Premium user should be able to make multiple requests
      const premiumResponse1 = await request(app)
        .get("/api/test")
        .set("Authorization", "Bearer premium-token");
      expect(premiumResponse1.status).toBe(200);
      const premiumResponse2 = await request(app)
        .get("/api/test")
        .set("Authorization", "Bearer premium-token");
      expect(premiumResponse2.status).toBe(200);
    }
  );

  describe("Advanced Cases", () => {
    // 1. Test different rate limit configurations
    redisTest("should enforce fixed window rate limiting", async ({ redisOptions }) => {
      const rateLimitMiddleware = authorizationRateLimitMiddleware({
        redis: redisOptions,
        keyPrefix: "test-fixed",
        defaultLimiter: {
          type: "fixedWindow",
          window: "10s",
          tokens: 3,
        },
        pathMatchers: [/^\/api/],
      });

      app.use(rateLimitMiddleware);
      app.get("/api/test", (req, res) => res.status(200).json({ message: "Success" }));

      const makeRequest = () =>
        request(app).get("/api/test").set("Authorization", "Bearer test-token");

      // Should allow 3 requests
      for (let i = 0; i < 3; i++) {
        const response = await makeRequest();
        expect(response.status).toBe(200);
      }

      // 4th request should be rate limited
      const limitedResponse = await makeRequest();
      expect(limitedResponse.status).toBe(429);

      // Wait for the window to reset
      await new Promise((resolve) => setTimeout(resolve, 10000));

      // Should allow requests again
      const newResponse = await makeRequest();
      expect(newResponse.status).toBe(200);
    });

    redisTest("should enforce sliding window rate limiting", async ({ redisOptions }) => {
      const rateLimitMiddleware = authorizationRateLimitMiddleware({
        redis: redisOptions,
        keyPrefix: "test-sliding",
        defaultLimiter: {
          type: "slidingWindow",
          window: "10s",
          tokens: 3,
        },
        pathMatchers: [/^\/api/],
      });

      app.use(rateLimitMiddleware);
      app.get("/api/test", (req, res) => res.status(200).json({ message: "Success" }));

      const makeRequest = () =>
        request(app).get("/api/test").set("Authorization", "Bearer test-token");

      // Should allow 3 requests
      for (let i = 0; i < 3; i++) {
        const response = await makeRequest();
        expect(response.status).toBe(200);
      }

      // 4th request should be rate limited
      const limitedResponse = await makeRequest();
      expect(limitedResponse.status).toBe(429);

      // Wait for part of the window to pass
      await new Promise((resolve) => setTimeout(resolve, 1000));

      // Should still be limited
      const stillLimitedResponse = await makeRequest();
      expect(stillLimitedResponse.status).toBe(429);

      // Wait for the full window to pass
      await new Promise((resolve) => setTimeout(resolve, 10000));

      // Should allow requests again
      const newResponse = await makeRequest();
      expect(newResponse.status).toBe(200);
    });

    // 2. Test edge cases around rate limit calculations
    redisTest("should handle token refill correctly", async ({ redisOptions }) => {
      const rateLimitMiddleware = authorizationRateLimitMiddleware({
        redis: redisOptions,
        keyPrefix: "test-refill",
        defaultLimiter: {
          type: "tokenBucket",
          refillRate: 1,
          interval: "5s",
          maxTokens: 3,
        },
        pathMatchers: [/^\/api/],
      });

      app.use(rateLimitMiddleware);
      app.get("/api/test", (req, res) => res.status(200).json({ message: "Success" }));

      const makeRequest = () =>
        request(app).get("/api/test").set("Authorization", "Bearer test-token");

      // Use up all tokens
      for (let i = 0; i < 3; i++) {
        const response = await makeRequest();
        expect(response.status).toBe(200);
      }

      // Next request should be limited
      const limitedResponse = await makeRequest();
      expect(limitedResponse.status).toBe(429);

      // Wait for one token to be refilled
      await new Promise((resolve) => setTimeout(resolve, 5000));

      // Should allow one request
      const newResponse = await makeRequest();
      expect(newResponse.status).toBe(200);

      // But the next one should be limited again
      const limitedAgainResponse = await makeRequest();
      expect(limitedAgainResponse.status).toBe(429);
    });

    redisTest("should handle near-zero remaining tokens correctly", async ({ redisOptions }) => {
      const rateLimitMiddleware = authorizationRateLimitMiddleware({
        redis: redisOptions,
        keyPrefix: "test-near-zero",
        defaultLimiter: {
          type: "tokenBucket",
          refillRate: 1, // 1 token every 5 seconds
          interval: "5s",
          maxTokens: 1,
        },
        pathMatchers: [/^\/api/],
      });

      app.use(rateLimitMiddleware);
      app.get("/api/test", (req, res) => res.status(200).json({ message: "Success" }));

      const makeRequest = () =>
        request(app).get("/api/test").set("Authorization", "Bearer test-token");

      // First request should succeed
      const firstResponse = await makeRequest();
      expect(firstResponse.status).toBe(200);

      // Immediate second request should fail
      const secondResponse = await makeRequest();
      expect(secondResponse.status).toBe(429);

      // Wait for almost one token to be refilled (4.9 seconds)
      await new Promise((resolve) => setTimeout(resolve, 4900));

      // This request should still fail as we're just shy of a full token
      const thirdResponse = await makeRequest();
      expect(thirdResponse.status).toBe(429);

      // Wait for the full token to be refilled (additional 200ms)
      await new Promise((resolve) => setTimeout(resolve, 200));

      // This request should now succeed
      const fourthResponse = await makeRequest();
      expect(fourthResponse.status).toBe(200);

      // Immediate next request should fail again
      const fifthResponse = await makeRequest();
      expect(fifthResponse.status).toBe(429);
    });

    // 3. Test the limiterCache functionality
    redisTest("should use cached limiter configurations", async ({ redisOptions }) => {
      let configOverrideCalls = 0;
      const rateLimitMiddleware = authorizationRateLimitMiddleware({
        redis: redisOptions,
        keyPrefix: "test-cache",
        defaultLimiter: {
          type: "tokenBucket",
          refillRate: 1,
          interval: "1m",
          maxTokens: 10,
        },
        pathMatchers: [/^\/api/],
        limiterCache: {
          fresh: 1000, // 1 second
          stale: 2000, // 2 seconds
          maxItems: 1000,
        },
        limiterConfigOverride: async (authorizationValue) => {
          configOverrideCalls++;
          if (authorizationValue === "Bearer premium-token") {
            return {
              type: "tokenBucket",
              refillRate: 10,
              interval: "1m",
              maxTokens: 100,
            };
          }
          return undefined;
        },
      });

      app.use(rateLimitMiddleware);
      app.get("/api/test", (req, res) => res.status(200).json({ message: "Success" }));

      const makeRequest = () =>
        request(app).get("/api/test").set("Authorization", "Bearer premium-token");

      // First request should call the override
      await makeRequest();
      expect(configOverrideCalls).toBe(1);

      // Subsequent requests within 1 second should use the cache
      await makeRequest();
      await makeRequest();
      expect(configOverrideCalls).toBe(1);

      // Wait for the cache to become stale
      await new Promise((resolve) => setTimeout(resolve, 1100));

      // This should still use the cache, but also trigger a refresh
      await makeRequest();
      expect(configOverrideCalls).toBe(2);

      // Wait for the cache to expire completely
      await new Promise((resolve) => setTimeout(resolve, 1000));

      // This should trigger a new override call
      await makeRequest();
      expect(configOverrideCalls).toBe(3);
    });
  });
});
