/**
 * Unit tests for lib/rate-limiter.ts
 * Tests rate limiting logic, IP extraction, window parsing, in-memory fallback
 */

// Mock Upstash before importing
jest.mock("@upstash/ratelimit", () => ({
  Ratelimit: jest.fn().mockImplementation(() => ({
    limit: jest.fn().mockResolvedValue({
      success: true,
      limit: 10,
      reset: Date.now() + 60000,
      remaining: 5,
    }),
  })),
}));

jest.mock("@upstash/redis", () => ({
  Redis: jest.fn().mockImplementation(() => ({
    get: jest.fn(),
    set: jest.fn(),
    del: jest.fn(),
  })),
}));

import { NextRequest, NextResponse } from "next/server";
import {
  getClientIdentifier,
  applyRateLimit,
  withRateLimit,
  getEmailIdentifier,
  rateLimiters,
} from "@/lib/rate-limiter";

// Mock NextRequest
const createMockRequest = (
  headers: Record<string, string> = {},
  body?: unknown,
): NextRequest => {
  const request = {
    headers: {
      get: jest.fn((key: string) => headers[key] || null),
    },
    ip: headers["mock-ip"] || undefined,
    clone: jest.fn(() => ({
      json: jest.fn().mockResolvedValue(body || {}),
    })),
  } as unknown as NextRequest;

  return request;
};

describe("lib/rate-limiter.ts", () => {
  describe("getClientIdentifier", () => {
    it("should extract IP from x-forwarded-for header", () => {
      const request = createMockRequest({
        "x-forwarded-for": "203.0.113.1, 203.0.113.2",
      });

      const ip = getClientIdentifier(request);

      expect(ip).toBe("203.0.113.1"); // First IP in chain
    });

    it("should extract IP from x-real-ip header", () => {
      const request = createMockRequest({
        "x-real-ip": "198.51.100.42",
      });

      const ip = getClientIdentifier(request);

      expect(ip).toBe("198.51.100.42");
    });

    it("should prefer x-forwarded-for over x-real-ip", () => {
      const request = createMockRequest({
        "x-forwarded-for": "203.0.113.1",
        "x-real-ip": "198.51.100.42",
      });

      const ip = getClientIdentifier(request);

      expect(ip).toBe("203.0.113.1");
    });

    it("should use request.ip as fallback", () => {
      const request = createMockRequest({ "mock-ip": "192.168.1.1" });

      const ip = getClientIdentifier(request);

      expect(ip).toBe("192.168.1.1");
    });

    it("should default to 127.0.0.1 if no IP available", () => {
      const request = createMockRequest({});

      const ip = getClientIdentifier(request);

      expect(ip).toBe("127.0.0.1");
    });

    it("should trim whitespace from x-forwarded-for", () => {
      const request = createMockRequest({
        "x-forwarded-for": "  203.0.113.1  , 203.0.113.2",
      });

      const ip = getClientIdentifier(request);

      expect(ip).toBe("203.0.113.1");
    });

    it("should handle IPv6 addresses", () => {
      const request = createMockRequest({
        "x-forwarded-for": "2001:0db8:85a3:0000:0000:8a2e:0370:7334",
      });

      const ip = getClientIdentifier(request);

      expect(ip).toBe("2001:0db8:85a3:0000:0000:8a2e:0370:7334");
    });
  });

  describe("getEmailIdentifier", () => {
    it("should extract email from request body", async () => {
      const request = createMockRequest({}, { email: "test@example.com" });

      const identifier = await getEmailIdentifier(request);

      expect(identifier).toBe("email:test@example.com");
    });

    it("should use identifier field if email not present", async () => {
      const request = createMockRequest({}, { identifier: "user@example.com" });

      const identifier = await getEmailIdentifier(request);

      expect(identifier).toBe("email:user@example.com");
    });

    it("should fallback to IP if no email in body", async () => {
      const request = createMockRequest(
        {
          "x-forwarded-for": "203.0.113.1",
        },
        {},
      );

      const identifier = await getEmailIdentifier(request);

      expect(identifier).toBe("203.0.113.1");
    });

    it("should fallback to IP if JSON parsing fails", async () => {
      const request = createMockRequest({
        "x-forwarded-for": "203.0.113.1",
      });

      // Mock JSON parsing failure
      request.clone = jest.fn(() => ({
        json: jest.fn().mockRejectedValue(new Error("Invalid JSON")),
      })) as any;

      const identifier = await getEmailIdentifier(request);

      expect(identifier).toBe("203.0.113.1");
    });
  });

  describe("applyRateLimit", () => {
    const createMockLimiter = (
      success: boolean,
      reset = Date.now() + 60000,
    ) => ({
      limit: jest.fn().mockResolvedValue({
        success,
        limit: 10,
        reset,
        remaining: success ? 5 : 0,
      }),
    });

    it("should return null if rate limit not exceeded", async () => {
      const request = createMockRequest({});
      const limiter = createMockLimiter(true);

      const response = await applyRateLimit(request, limiter);

      expect(response).toBeNull();
      expect(limiter.limit).toHaveBeenCalledWith("127.0.0.1");
    });

    it("should return 429 response if rate limit exceeded", async () => {
      const request = createMockRequest({ "x-forwarded-for": "203.0.113.1" });
      const resetTime = Date.now() + 60000;
      const limiter = createMockLimiter(false, resetTime);

      const response = await applyRateLimit(request, limiter);

      expect(response).toBeInstanceOf(NextResponse);
      expect(response?.status).toBe(429);

      const body = await response?.json();
      expect(body.error).toBe("Too many requests. Please try again later.");
      expect(body.retryAfter).toBeGreaterThan(0);
    });

    it("should include rate limit headers in 429 response", async () => {
      const request = createMockRequest({});
      const resetTime = Date.now() + 60000;
      const limiter = createMockLimiter(false, resetTime);

      const response = await applyRateLimit(request, limiter);

      // Verify response is 429
      expect(response?.status).toBe(429);

      // Verify error message
      const body = await response?.json();
      expect(body.error).toBe("Too many requests. Please try again later.");
      expect(body.retryAfter).toBeGreaterThan(0);
    });

    it("should use custom identifier if provided", async () => {
      const request = createMockRequest({});
      const limiter = createMockLimiter(true);
      const customId = "user:12345";

      await applyRateLimit(request, limiter, customId);

      expect(limiter.limit).toHaveBeenCalledWith(customId);
    });

    it("should fail open on limiter errors", async () => {
      const request = createMockRequest({});
      const limiter = {
        limit: jest
          .fn()
          .mockRejectedValue(new Error("Redis connection failed")),
      };

      const response = await applyRateLimit(request, limiter);

      // Should allow request through despite error
      expect(response).toBeNull();
    });

    it("should calculate retry-after correctly", async () => {
      const request = createMockRequest({});
      const resetTime = Date.now() + 120000; // 120 seconds from now
      const limiter = createMockLimiter(false, resetTime);

      const response = await applyRateLimit(request, limiter);
      const body = await response?.json();

      expect(body.retryAfter).toBeGreaterThanOrEqual(119); // Allow 1 second margin
      expect(body.retryAfter).toBeLessThanOrEqual(121);
    });
  });

  describe("withRateLimit", () => {
    const mockHandler = jest
      .fn()
      .mockResolvedValue(NextResponse.json({ success: true }));

    beforeEach(() => {
      mockHandler.mockClear();
    });

    it("should call handler if rate limit passes", async () => {
      const request = createMockRequest({});
      const limiter = {
        limit: jest.fn().mockResolvedValue({
          success: true,
          limit: 10,
          reset: Date.now() + 60000,
          remaining: 5,
        }),
      };

      const wrappedHandler = withRateLimit(mockHandler, limiter);
      const response = await wrappedHandler(request);

      expect(mockHandler).toHaveBeenCalledWith(request);
      expect(response.status).toBe(200);
    });

    it("should return 429 without calling handler if rate limit exceeded", async () => {
      const request = createMockRequest({});
      const limiter = {
        limit: jest.fn().mockResolvedValue({
          success: false,
          limit: 10,
          reset: Date.now() + 60000,
          remaining: 0,
        }),
      };

      const wrappedHandler = withRateLimit(mockHandler, limiter);
      const response = await wrappedHandler(request);

      expect(mockHandler).not.toHaveBeenCalled();
      expect(response.status).toBe(429);
    });

    it("should use custom identifier function if provided", async () => {
      const request = createMockRequest({}, { email: "test@example.com" });
      const limiter = {
        limit: jest.fn().mockResolvedValue({
          success: true,
          limit: 10,
          reset: Date.now() + 60000,
          remaining: 5,
        }),
      };

      const customIdentifier = async (req: NextRequest) => {
        const body = await req.clone().json();
        return `custom:${body.email}`;
      };

      const wrappedHandler = withRateLimit(
        mockHandler,
        limiter,
        customIdentifier,
      );
      await wrappedHandler(request);

      expect(limiter.limit).toHaveBeenCalledWith("custom:test@example.com");
    });
  });

  describe("Rate Limiter Configurations", () => {
    it("should have otpVerify limiter configured", () => {
      expect(rateLimiters.otpVerify).toBeDefined();
    });

    it("should have otpSend limiter configured", () => {
      expect(rateLimiters.otpSend).toBeDefined();
    });

    it("should have login limiter configured", () => {
      expect(rateLimiters.login).toBeDefined();
    });

    it("should have register limiter configured", () => {
      expect(rateLimiters.register).toBeDefined();
    });

    it("should have authGeneral limiter configured", () => {
      expect(rateLimiters.authGeneral).toBeDefined();
    });

    it("should have checkout limiter configured", () => {
      expect(rateLimiters.checkout).toBeDefined();
    });

    it("should have demoBooking limiter configured", () => {
      expect(rateLimiters.demoBooking).toBeDefined();
    });
  });

  describe("Integration Tests", () => {
    it("should handle complete rate limit flow", async () => {
      const request = createMockRequest({ "x-forwarded-for": "203.0.113.1" });

      // Mock limiter that succeeds
      const limiter = {
        limit: jest.fn().mockResolvedValue({
          success: true,
          limit: 5,
          reset: Date.now() + 60000,
          remaining: 3,
        }),
      };

      const handler = jest
        .fn()
        .mockResolvedValue(NextResponse.json({ data: "success" }));

      const wrappedHandler = withRateLimit(handler, limiter);
      const response = await wrappedHandler(request);

      expect(response.status).toBe(200);
      expect(handler).toHaveBeenCalled();
    });

    it("should extract IP correctly in rate limit flow", async () => {
      const request = createMockRequest({ "x-real-ip": "198.51.100.42" });

      const limiter = {
        limit: jest.fn().mockResolvedValue({
          success: true,
          limit: 10,
          reset: Date.now() + 60000,
          remaining: 8,
        }),
      };

      await applyRateLimit(request, limiter);

      expect(limiter.limit).toHaveBeenCalledWith("198.51.100.42");
    });
  });
});
