// Mock express-rate-limit
const mockRateLimitInstance = jest.fn();

jest.mock('express-rate-limit', () => {
  const rateLimitFn = jest.fn((config) => {
    // Store the config for inspection in tests
    rateLimitFn.lastConfig = config;
    return mockRateLimitInstance;
  });
  rateLimitFn.defaultKeyGenerator = jest.fn().mockReturnValue('127.0.0.1');
  return rateLimitFn;
});

const rateLimit = require('express-rate-limit');

const {
  placesAutocomplete,
  placeDetails,
  geocoding,
  loginLimiter,
  registerLimiter,
  passwordResetLimiter,
  generalLimiter,
  burstProtection,
  createMapsRateLimiter,
  createUserBasedRateLimiter
} = require('../../../middleware/rateLimiter');

describe('Rate Limiter Middleware', () => {
  let req, res, next;

  beforeEach(() => {
    req = {
      ip: '127.0.0.1',
      user: null
    };
    res = {
      status: jest.fn().mockReturnThis(),
      json: jest.fn(),
      set: jest.fn()
    };
    next = jest.fn();
    jest.clearAllMocks();
  });

  describe('createMapsRateLimiter', () => {
    it('should create rate limiter with correct configuration', () => {
      const windowMs = 60000;
      const max = 30;
      const message = 'Test message';

      createMapsRateLimiter(windowMs, max, message);

      expect(rateLimit).toHaveBeenCalledWith({
        windowMs,
        max,
        message: {
          error: message,
          retryAfter: Math.ceil(windowMs / 1000)
        },
        standardHeaders: true,
        legacyHeaders: false,
        keyGenerator: expect.any(Function)
      });
    });

    describe('keyGenerator', () => {
      it('should use user ID when user is authenticated', () => {
        const windowMs = 60000;
        const max = 30;
        const message = 'Test message';

        createMapsRateLimiter(windowMs, max, message);
        const config = rateLimit.lastConfig;

        const reqWithUser = { user: { id: 123 } };
        const key = config.keyGenerator(reqWithUser);

        expect(key).toBe('user:123');
      });

      it('should use default IP generator when user is not authenticated', () => {
        const windowMs = 60000;
        const max = 30;
        const message = 'Test message';

        createMapsRateLimiter(windowMs, max, message);
        const config = rateLimit.lastConfig;

        const reqWithoutUser = { user: null };
        config.keyGenerator(reqWithoutUser);

        expect(rateLimit.defaultKeyGenerator).toHaveBeenCalledWith(reqWithoutUser);
      });

      it('should use default IP generator when user has no ID', () => {
        const windowMs = 60000;
        const max = 30;
        const message = 'Test message';

        createMapsRateLimiter(windowMs, max, message);
        const config = rateLimit.lastConfig;

        const reqWithUserNoId = { user: {} };
        config.keyGenerator(reqWithUserNoId);

        expect(rateLimit.defaultKeyGenerator).toHaveBeenCalledWith(reqWithUserNoId);
      });
    });

    it('should calculate retryAfter correctly', () => {
      const windowMs = 90000; // 90 seconds
      const max = 10;
      const message = 'Test message';

      createMapsRateLimiter(windowMs, max, message);

      expect(rateLimit).toHaveBeenCalledWith(expect.objectContaining({
        message: {
          error: message,
          retryAfter: 90 // Math.ceil(90000 / 1000)
        }
      }));
    });
  });

  describe('Pre-configured rate limiters', () => {
    describe('placesAutocomplete', () => {
      it('should be a function (rate limiter middleware)', () => {
        expect(typeof placesAutocomplete).toBe('function');
      });
    });

    describe('placeDetails', () => {
      it('should be a function (rate limiter middleware)', () => {
        expect(typeof placeDetails).toBe('function');
      });
    });

    describe('geocoding', () => {
      it('should be a function (rate limiter middleware)', () => {
        expect(typeof geocoding).toBe('function');
      });
    });

    describe('loginLimiter', () => {
      it('should be a function (rate limiter middleware)', () => {
        expect(typeof loginLimiter).toBe('function');
      });
    });

    describe('registerLimiter', () => {
      it('should be a function (rate limiter middleware)', () => {
        expect(typeof registerLimiter).toBe('function');
      });
    });

    describe('passwordResetLimiter', () => {
      it('should be a function (rate limiter middleware)', () => {
        expect(typeof passwordResetLimiter).toBe('function');
      });
    });

    describe('generalLimiter', () => {
      it('should be a function (rate limiter middleware)', () => {
        expect(typeof generalLimiter).toBe('function');
      });
    });
  });

  describe('createUserBasedRateLimiter', () => {
    let userBasedLimiter;
    const windowMs = 10000; // 10 seconds
    const max = 5;
    const message = 'Too many requests';

    beforeEach(() => {
      userBasedLimiter = createUserBasedRateLimiter(windowMs, max, message);
    });

    describe('Key generation', () => {
      it('should use user ID when user is authenticated', () => {
        req.user = { id: 123 };

        userBasedLimiter(req, res, next);

        expect(next).toHaveBeenCalled();
        expect(res.status).not.toHaveBeenCalled();
      });

      it('should use IP when user is not authenticated', () => {
        req.user = null;
        rateLimit.defaultKeyGenerator.mockReturnValue('192.168.1.1');

        userBasedLimiter(req, res, next);

        expect(rateLimit.defaultKeyGenerator).toHaveBeenCalledWith(req);
        expect(next).toHaveBeenCalled();
      });
    });

    describe('Rate limiting logic', () => {
      it('should allow requests within limit', () => {
        req.user = { id: 123 };

        // Make requests within limit
        for (let i = 0; i < max; i++) {
          jest.clearAllMocks();
          userBasedLimiter(req, res, next);
          expect(next).toHaveBeenCalled();
          expect(res.status).not.toHaveBeenCalled();
        }
      });

      it('should block requests when limit exceeded', () => {
        req.user = { id: 123 };

        // Exhaust the limit
        for (let i = 0; i < max; i++) {
          userBasedLimiter(req, res, next);
        }

        // Next request should be blocked
        jest.clearAllMocks();
        userBasedLimiter(req, res, next);

        expect(res.status).toHaveBeenCalledWith(429);
        expect(res.json).toHaveBeenCalledWith({
          error: message,
          retryAfter: expect.any(Number)
        });
        expect(next).not.toHaveBeenCalled();
      });

      it('should set correct rate limit headers', () => {
        req.user = { id: 123 };

        userBasedLimiter(req, res, next);

        expect(res.set).toHaveBeenCalledWith({
          'RateLimit-Limit': max,
          'RateLimit-Remaining': max - 1,
          'RateLimit-Reset': expect.any(String)
        });
      });

      it('should update remaining count correctly', () => {
        req.user = { id: 123 };

        // First request
        userBasedLimiter(req, res, next);
        expect(res.set).toHaveBeenCalledWith(expect.objectContaining({
          'RateLimit-Remaining': 4
        }));

        // Second request
        jest.clearAllMocks();
        userBasedLimiter(req, res, next);
        expect(res.set).toHaveBeenCalledWith(expect.objectContaining({
          'RateLimit-Remaining': 3
        }));
      });

      it('should not go below 0 for remaining count', () => {
        req.user = { id: 123 };

        // Exhaust the limit
        for (let i = 0; i < max; i++) {
          userBasedLimiter(req, res, next);
        }

        // Check that remaining doesn't go negative
        const lastCall = res.set.mock.calls[res.set.mock.calls.length - 1][0];
        expect(lastCall['RateLimit-Remaining']).toBe(0);
      });
    });

    describe('Window management', () => {
      it('should reset count after window expires', () => {
        req.user = { id: 123 };
        const originalDateNow = Date.now;

        // Mock time to start of window
        let currentTime = 1000000000;
        Date.now = jest.fn(() => currentTime);

        // Exhaust the limit
        for (let i = 0; i < max; i++) {
          userBasedLimiter(req, res, next);
        }

        // Verify limit is reached
        jest.clearAllMocks();
        userBasedLimiter(req, res, next);
        expect(res.status).toHaveBeenCalledWith(429);

        // Move time forward past the window
        currentTime += windowMs + 1000;
        jest.clearAllMocks();

        // Should allow requests again
        userBasedLimiter(req, res, next);
        expect(next).toHaveBeenCalled();
        expect(res.status).not.toHaveBeenCalled();

        // Restore original Date.now
        Date.now = originalDateNow;
      });

      it('should clean up old entries from store', () => {
        const originalDateNow = Date.now;
        let currentTime = 1000000000;
        Date.now = jest.fn(() => currentTime);

        // Create entries for different users
        req.user = { id: 1 };
        userBasedLimiter(req, res, next);

        req.user = { id: 2 };
        userBasedLimiter(req, res, next);

        // Move time forward to expire first entries
        currentTime += windowMs + 1000;

        req.user = { id: 3 };
        userBasedLimiter(req, res, next);

        // The cleanup should have occurred when processing user 3's request
        // We can't directly test the internal store, but we can verify the behavior

        expect(next).toHaveBeenCalled();

        // Restore original Date.now
        Date.now = originalDateNow;
      });
    });

    describe('Different users/IPs', () => {
      it('should maintain separate counts for different users', () => {
        // User 1 makes max requests
        req.user = { id: 1 };
        for (let i = 0; i < max; i++) {
          userBasedLimiter(req, res, next);
        }

        // User 1 should be blocked
        jest.clearAllMocks();
        userBasedLimiter(req, res, next);
        expect(res.status).toHaveBeenCalledWith(429);

        // User 2 should still be allowed
        jest.clearAllMocks();
        req.user = { id: 2 };
        userBasedLimiter(req, res, next);
        expect(next).toHaveBeenCalled();
        expect(res.status).not.toHaveBeenCalled();
      });

      it('should maintain separate counts for different IPs', () => {
        req.user = null;

        // IP 1 makes max requests
        rateLimit.defaultKeyGenerator.mockReturnValue('192.168.1.1');
        for (let i = 0; i < max; i++) {
          userBasedLimiter(req, res, next);
        }

        // IP 1 should be blocked
        jest.clearAllMocks();
        userBasedLimiter(req, res, next);
        expect(res.status).toHaveBeenCalledWith(429);

        // IP 2 should still be allowed
        jest.clearAllMocks();
        rateLimit.defaultKeyGenerator.mockReturnValue('192.168.1.2');
        userBasedLimiter(req, res, next);
        expect(next).toHaveBeenCalled();
        expect(res.status).not.toHaveBeenCalled();
      });
    });

    describe('Edge cases', () => {
      it('should handle undefined user gracefully', () => {
        req.user = undefined;
        rateLimit.defaultKeyGenerator.mockReturnValue('127.0.0.1');

        userBasedLimiter(req, res, next);

        expect(rateLimit.defaultKeyGenerator).toHaveBeenCalledWith(req);
        expect(next).toHaveBeenCalled();
      });

      it('should handle user object without id', () => {
        req.user = { email: 'test@test.com' };
        rateLimit.defaultKeyGenerator.mockReturnValue('127.0.0.1');

        userBasedLimiter(req, res, next);

        expect(rateLimit.defaultKeyGenerator).toHaveBeenCalledWith(req);
        expect(next).toHaveBeenCalled();
      });

      it('should set correct reset time in ISO format', () => {
        req.user = { id: 123 };

        userBasedLimiter(req, res, next);

        const setCall = res.set.mock.calls[0][0];
        const resetTime = setCall['RateLimit-Reset'];

        // Should be a valid ISO string
        expect(() => new Date(resetTime)).not.toThrow();
        expect(resetTime).toMatch(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/);
      });

      it('should calculate retry after correctly when limit exceeded', () => {
        req.user = { id: 123 };
        const originalDateNow = Date.now;
        const currentTime = 1000000000;
        Date.now = jest.fn(() => currentTime);

        // Exhaust the limit
        for (let i = 0; i < max; i++) {
          userBasedLimiter(req, res, next);
        }

        jest.clearAllMocks();
        userBasedLimiter(req, res, next);

        const jsonCall = res.json.mock.calls[0][0];
        expect(jsonCall.retryAfter).toBe(Math.ceil(windowMs / 1000));

        // Restore original Date.now
        Date.now = originalDateNow;
      });
    });
  });

  describe('burstProtection', () => {
    it('should be a function', () => {
      expect(typeof burstProtection).toBe('function');
    });

    it('should allow requests within burst limit', () => {
      req.user = { id: 123 };

      // Should allow up to 5 requests in 10 seconds
      for (let i = 0; i < 5; i++) {
        jest.clearAllMocks();
        burstProtection(req, res, next);
        expect(next).toHaveBeenCalled();
        expect(res.status).not.toHaveBeenCalled();
      }
    });

    it('should block requests when burst limit exceeded', () => {
      req.user = { id: 123 };

      // Exhaust burst limit
      for (let i = 0; i < 5; i++) {
        burstProtection(req, res, next);
      }

      // Next request should be blocked
      jest.clearAllMocks();
      burstProtection(req, res, next);

      expect(res.status).toHaveBeenCalledWith(429);
      expect(res.json).toHaveBeenCalledWith({
        error: 'Too many requests in a short period. Please slow down.',
        retryAfter: expect.any(Number)
      });
      expect(next).not.toHaveBeenCalled();
    });
  });

  describe('Module exports', () => {
    it('should export all required rate limiters', () => {
      const rateLimiterModule = require('../../../middleware/rateLimiter');

      expect(rateLimiterModule).toHaveProperty('placesAutocomplete');
      expect(rateLimiterModule).toHaveProperty('placeDetails');
      expect(rateLimiterModule).toHaveProperty('geocoding');
      expect(rateLimiterModule).toHaveProperty('loginLimiter');
      expect(rateLimiterModule).toHaveProperty('registerLimiter');
      expect(rateLimiterModule).toHaveProperty('passwordResetLimiter');
      expect(rateLimiterModule).toHaveProperty('generalLimiter');
      expect(rateLimiterModule).toHaveProperty('burstProtection');
      expect(rateLimiterModule).toHaveProperty('createMapsRateLimiter');
      expect(rateLimiterModule).toHaveProperty('createUserBasedRateLimiter');
    });

    it('should export functions for utility methods', () => {
      const rateLimiterModule = require('../../../middleware/rateLimiter');

      expect(typeof rateLimiterModule.createMapsRateLimiter).toBe('function');
      expect(typeof rateLimiterModule.createUserBasedRateLimiter).toBe('function');
      expect(typeof rateLimiterModule.burstProtection).toBe('function');
    });
  });
});