/**
 * Rate Limiter Unit Tests
 *
 * Tests for the central rate limiting module.
 * Covers in-memory store, IP extraction, header generation, and Redis fallback.
 */

import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'

// Mock Redis before importing the module
vi.mock('ioredis', () => ({
  default: vi.fn().mockImplementation(() => ({
    incr: vi.fn(),
    pexpire: vi.fn(),
    pttl: vi.fn(),
    keys: vi.fn(),
    del: vi.fn(),
    quit: vi.fn(),
    on: vi.fn(),
  })),
}))

// Mock the redis module
vi.mock('@/lib/redis', () => ({
  getRedisClient: vi.fn(() => null),
  isRedisAvailable: vi.fn(() => false),
}))

import {
  createRateLimiter,
  getClientIp,
  rateLimitHeaders,
  publicApiLimiter,
  authLimiter,
  searchLimiter,
} from '@/lib/security/rate-limiter'

describe('Rate Limiter', () => {
  beforeEach(() => {
    vi.clearAllMocks()
    vi.useFakeTimers()
  })

  afterEach(() => {
    vi.useRealTimers()
  })

  describe('createRateLimiter', () => {
    it('creates a limiter with custom configuration', () => {
      const limiter = createRateLimiter({
        name: 'test-limiter',
        maxRequests: 10,
        windowMs: 60000,
      })

      expect(limiter).toBeDefined()
      expect(limiter.check).toBeDefined()
      expect(typeof limiter.check).toBe('function')
    })

    it('allows requests within limit', async () => {
      const limiter = createRateLimiter({
        name: 'test-allow',
        maxRequests: 5,
        windowMs: 60000,
      })

      const result = await limiter.check('test-ip-1')

      expect(result.allowed).toBe(true)
      expect(result.remaining).toBe(4) // 5 - 1
    })

    it('tracks requests per identifier', async () => {
      const limiter = createRateLimiter({
        name: 'test-track',
        maxRequests: 3,
        windowMs: 60000,
      })

      // First request
      const result1 = await limiter.check('user-a')
      expect(result1.remaining).toBe(2)

      // Second request same user
      const result2 = await limiter.check('user-a')
      expect(result2.remaining).toBe(1)

      // Different user should have full quota
      const result3 = await limiter.check('user-b')
      expect(result3.remaining).toBe(2)
    })

    it('blocks requests exceeding limit', async () => {
      const limiter = createRateLimiter({
        name: 'test-block',
        maxRequests: 3,
        windowMs: 60000,
      })

      // Exhaust the limit
      await limiter.check('blocked-ip')
      await limiter.check('blocked-ip')
      await limiter.check('blocked-ip')

      // Next request should be blocked
      const result = await limiter.check('blocked-ip')

      expect(result.allowed).toBe(false)
      expect(result.remaining).toBe(0)
      expect(result.retryAfter).toBeGreaterThan(0)
    })

    it('resets after window expires', async () => {
      const windowMs = 60000
      const limiter = createRateLimiter({
        name: 'test-reset',
        maxRequests: 2,
        windowMs,
      })

      // Exhaust the limit
      await limiter.check('reset-ip')
      await limiter.check('reset-ip')
      const blockedResult = await limiter.check('reset-ip')
      expect(blockedResult.allowed).toBe(false)

      // Advance time past window
      vi.advanceTimersByTime(windowMs + 1000)

      // Should be allowed again
      const result = await limiter.check('reset-ip')
      expect(result.allowed).toBe(true)
      expect(result.remaining).toBe(1)
    })

    it('provides correct resetIn value', async () => {
      const windowMs = 60000
      const limiter = createRateLimiter({
        name: 'test-resetin',
        maxRequests: 5,
        windowMs,
      })

      const result = await limiter.check('resetin-ip')

      // resetIn should be close to windowMs
      expect(result.resetIn).toBeLessThanOrEqual(windowMs)
      expect(result.resetIn).toBeGreaterThan(windowMs - 1000)
    })
  })

  describe('getClientIp', () => {
    describe('with TRUST_PROXY=true', () => {
      beforeEach(() => {
        vi.stubEnv('TRUST_PROXY', 'true')
      })

      afterEach(() => {
        vi.unstubAllEnvs()
      })

      it('extracts IP from x-forwarded-for header', () => {
        const headers = new Headers()
        headers.set('x-forwarded-for', '203.0.113.50, 70.41.3.18, 150.172.238.178')

        const ip = getClientIp(headers)

        expect(ip).toBe('203.0.113.50')
      })

      it('extracts IP from x-real-ip header', () => {
        const headers = new Headers()
        headers.set('x-real-ip', '192.168.1.100')

        const ip = getClientIp(headers)

        expect(ip).toBe('192.168.1.100')
      })

      it('prefers x-forwarded-for over x-real-ip', () => {
        const headers = new Headers()
        headers.set('x-forwarded-for', '10.0.0.1')
        headers.set('x-real-ip', '10.0.0.2')

        const ip = getClientIp(headers)

        expect(ip).toBe('10.0.0.1')
      })

      it('trims whitespace from forwarded IPs', () => {
        const headers = new Headers()
        headers.set('x-forwarded-for', '  192.168.1.1  , 10.0.0.1')

        const ip = getClientIp(headers)

        expect(ip).toBe('192.168.1.1')
      })
    })

    describe('without TRUST_PROXY (default)', () => {
      beforeEach(() => {
        vi.stubEnv('TRUST_PROXY', '')
      })

      afterEach(() => {
        vi.unstubAllEnvs()
      })

      it('ignores x-forwarded-for header to prevent IP spoofing', () => {
        const headers = new Headers()
        headers.set('x-forwarded-for', '203.0.113.50')

        const ip = getClientIp(headers)

        expect(ip).toBe('direct-connection')
      })

      it('ignores x-real-ip header to prevent IP spoofing', () => {
        const headers = new Headers()
        headers.set('x-real-ip', '192.168.1.100')

        const ip = getClientIp(headers)

        expect(ip).toBe('direct-connection')
      })

      it('returns direct-connection for missing headers', () => {
        const headers = new Headers()

        const ip = getClientIp(headers)

        expect(ip).toBe('direct-connection')
      })
    })
  })

  describe('rateLimitHeaders', () => {
    it('generates correct headers for allowed request', () => {
      const result = {
        allowed: true,
        remaining: 25,
        resetIn: 45000,
      }

      const headers = rateLimitHeaders(result, 30) as Record<string, string>

      expect(headers['X-RateLimit-Limit']).toBe('30')
      expect(headers['X-RateLimit-Remaining']).toBe('25')
      expect(headers['X-RateLimit-Reset']).toBeDefined()
    })

    it('includes Retry-After for blocked request', () => {
      const result = {
        allowed: false,
        remaining: 0,
        resetIn: 30000,
        retryAfter: 30,
      }

      const headers = rateLimitHeaders(result, 10) as Record<string, string>

      expect(headers['Retry-After']).toBe('30')
      expect(headers['X-RateLimit-Remaining']).toBe('0')
    })

    it('calculates reset timestamp correctly', () => {
      const result = {
        allowed: true,
        remaining: 5,
        resetIn: 60000,
      }

      const headers = rateLimitHeaders(result, 10) as Record<string, string>
      const resetValue = headers['X-RateLimit-Reset']

      // The reset value should be a number (either timestamp or seconds)
      expect(resetValue).toBeDefined()
      expect(parseInt(resetValue, 10)).toBeGreaterThan(0)
    })
  })

  describe('Predefined Limiters', () => {
    it('publicApiLimiter allows 60 requests per minute', async () => {
      const result = await publicApiLimiter.check('public-test')

      expect(result.allowed).toBe(true)
      expect(result.remaining).toBe(59)
    })

    it('authLimiter allows 5 requests per 15 minutes', async () => {
      const result = await authLimiter.check('auth-test')

      expect(result.allowed).toBe(true)
      expect(result.remaining).toBe(4)
    })

    it('searchLimiter allows 30 requests per minute', async () => {
      const result = await searchLimiter.check('search-test')

      expect(result.allowed).toBe(true)
      expect(result.remaining).toBe(29)
    })
  })

  describe('Memory Store Cleanup', () => {
    it('removes expired entries', async () => {
      const windowMs = 1000 // 1 second window
      const limiter = createRateLimiter({
        name: 'cleanup-test',
        maxRequests: 10,
        windowMs,
      })

      // Create entries
      await limiter.check('cleanup-ip-1')
      await limiter.check('cleanup-ip-2')

      // Advance past expiry (2x window + cleanup interval)
      vi.advanceTimersByTime(windowMs * 2 + 300001)

      // New requests should have full quota (entries cleaned up)
      const result = await limiter.check('cleanup-ip-1')
      expect(result.remaining).toBe(9)
    })
  })

  describe('Edge Cases', () => {
    it('handles empty identifier', async () => {
      const limiter = createRateLimiter({
        name: 'empty-id-test',
        maxRequests: 5,
        windowMs: 60000,
      })

      const result = await limiter.check('')

      expect(result.allowed).toBe(true)
    })

    it('handles special characters in identifier', async () => {
      const limiter = createRateLimiter({
        name: 'special-char-test',
        maxRequests: 5,
        windowMs: 60000,
      })

      const result = await limiter.check('user@example.com:192.168.1.1')

      expect(result.allowed).toBe(true)
      expect(result.remaining).toBe(4)
    })

    it('handles concurrent requests', async () => {
      const limiter = createRateLimiter({
        name: 'concurrent-test',
        maxRequests: 10,
        windowMs: 60000,
      })

      // Fire multiple requests concurrently
      const promises = Array.from({ length: 5 }, () => limiter.check('concurrent-ip'))
      const results = await Promise.all(promises)

      // All should be allowed, remaining should decrement
      results.forEach((result) => {
        expect(result.allowed).toBe(true)
      })

      // Check total consumed
      const finalResult = await limiter.check('concurrent-ip')
      expect(finalResult.remaining).toBe(4) // 10 - 5 - 1 = 4
    })
  })
})
