/**
 * IP Allowlist/Blocklist Unit Tests
 *
 * Tests for the IP access control module.
 * Covers CIDR matching, wildcards, allowlist/blocklist logic, and IP extraction.
 */

import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'
import { NextRequest } from 'next/server'

describe('IP Allowlist', () => {
  beforeEach(() => {
    // Reset environment variables
    vi.unstubAllEnvs()
  })

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

  describe('getClientIpFromRequest', () => {
    // Import dynamically to reset mocks
    async function getModule() {
      vi.resetModules()
      return import('@/lib/security/ip-allowlist')
    }

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

      it('extracts IP from x-forwarded-for header', async () => {
        const { getClientIpFromRequest } = await getModule()

        const req = new NextRequest('https://example.com/api/test', {
          headers: {
            'x-forwarded-for': '203.0.113.50, 70.41.3.18',
          },
        })

        const ip = getClientIpFromRequest(req)

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

      it('extracts IP from x-real-ip header', async () => {
        const { getClientIpFromRequest } = await getModule()

        const req = new NextRequest('https://example.com/api/test', {
          headers: {
            'x-real-ip': '192.168.1.100',
          },
        })

        const ip = getClientIpFromRequest(req)

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

      it('prefers x-forwarded-for over x-real-ip', async () => {
        const { getClientIpFromRequest } = await getModule()

        const req = new NextRequest('https://example.com/api/test', {
          headers: {
            'x-forwarded-for': '10.0.0.1',
            'x-real-ip': '10.0.0.2',
          },
        })

        const ip = getClientIpFromRequest(req)

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

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

      it('ignores x-forwarded-for header to prevent IP spoofing', async () => {
        const { getClientIpFromRequest } = await getModule()

        const req = new NextRequest('https://example.com/api/test', {
          headers: {
            'x-forwarded-for': '203.0.113.50, 70.41.3.18',
          },
        })

        const ip = getClientIpFromRequest(req)

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

      it('ignores x-real-ip header to prevent IP spoofing', async () => {
        const { getClientIpFromRequest } = await getModule()

        const req = new NextRequest('https://example.com/api/test', {
          headers: {
            'x-real-ip': '192.168.1.100',
          },
        })

        const ip = getClientIpFromRequest(req)

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

      it('returns direct-connection for missing headers', async () => {
        const { getClientIpFromRequest } = await getModule()

        const req = new NextRequest('https://example.com/api/test')

        const ip = getClientIpFromRequest(req)

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

  describe('isIpBlocked', () => {
    async function getModule() {
      vi.resetModules()
      return import('@/lib/security/ip-allowlist')
    }

    it('returns false when BLOCKED_IPS is not set', async () => {
      const { isIpBlocked } = await getModule()

      const result = isIpBlocked('192.168.1.1')

      expect(result).toBe(false)
    })

    it('blocks exact IP match', async () => {
      vi.stubEnv('BLOCKED_IPS', '192.168.1.100,10.0.0.50')
      const { isIpBlocked } = await getModule()

      expect(isIpBlocked('192.168.1.100')).toBe(true)
      expect(isIpBlocked('10.0.0.50')).toBe(true)
      expect(isIpBlocked('192.168.1.101')).toBe(false)
    })

    it('blocks CIDR range', async () => {
      vi.stubEnv('BLOCKED_IPS', '10.0.0.0/24')
      const { isIpBlocked } = await getModule()

      expect(isIpBlocked('10.0.0.1')).toBe(true)
      expect(isIpBlocked('10.0.0.255')).toBe(true)
      expect(isIpBlocked('10.0.1.1')).toBe(false)
    })

    it('blocks wildcard pattern', async () => {
      vi.stubEnv('BLOCKED_IPS', '192.168.*')
      const { isIpBlocked } = await getModule()

      expect(isIpBlocked('192.168.0.1')).toBe(true)
      expect(isIpBlocked('192.168.255.255')).toBe(true)
      expect(isIpBlocked('192.169.0.1')).toBe(false)
    })

    it('handles multiple patterns', async () => {
      vi.stubEnv('BLOCKED_IPS', '10.0.0.0/8,192.168.1.50,172.16.*')
      const { isIpBlocked } = await getModule()

      expect(isIpBlocked('10.5.5.5')).toBe(true) // CIDR
      expect(isIpBlocked('192.168.1.50')).toBe(true) // Exact
      expect(isIpBlocked('172.16.0.1')).toBe(true) // Wildcard
      expect(isIpBlocked('8.8.8.8')).toBe(false)
    })
  })

  describe('isIpAllowed', () => {
    async function getModule() {
      vi.resetModules()
      return import('@/lib/security/ip-allowlist')
    }

    describe('sendEmail endpoint', () => {
      it('allows all IPs when SEND_EMAIL_ALLOWED_IPS is not set', async () => {
        const { isIpAllowed } = await getModule()

        expect(isIpAllowed('192.168.1.1', 'sendEmail').allowed).toBe(true)
        expect(isIpAllowed('10.0.0.1', 'sendEmail').allowed).toBe(true)
      })

      it('restricts to allowlist when configured', async () => {
        vi.stubEnv('SEND_EMAIL_ALLOWED_IPS', '192.168.1.0/24,10.0.0.50')
        const { isIpAllowed } = await getModule()

        expect(isIpAllowed('192.168.1.100', 'sendEmail').allowed).toBe(true)
        expect(isIpAllowed('10.0.0.50', 'sendEmail').allowed).toBe(true)
        expect(isIpAllowed('172.16.0.1', 'sendEmail').allowed).toBe(false)
      })
    })

    describe('admin endpoint', () => {
      it('allows all IPs when ADMIN_ALLOWED_IPS is not set', async () => {
        const { isIpAllowed } = await getModule()

        expect(isIpAllowed('192.168.1.1', 'admin').allowed).toBe(true)
      })

      it('restricts to allowlist when configured', async () => {
        vi.stubEnv('ADMIN_ALLOWED_IPS', '10.0.0.0/8')
        const { isIpAllowed } = await getModule()

        expect(isIpAllowed('10.5.5.5', 'admin').allowed).toBe(true)
        expect(isIpAllowed('192.168.1.1', 'admin').allowed).toBe(false)
      })
    })

    describe('webhooks endpoint', () => {
      it('blocks all IPs when WEBHOOK_ALLOWED_IPS is not set', async () => {
        const { isIpAllowed } = await getModule()

        expect(isIpAllowed('192.168.1.1', 'webhooks').allowed).toBe(false)
      })

      it('allows only configured IPs', async () => {
        vi.stubEnv('WEBHOOK_ALLOWED_IPS', '203.0.113.0/24')
        const { isIpAllowed } = await getModule()

        expect(isIpAllowed('203.0.113.50', 'webhooks').allowed).toBe(true)
        expect(isIpAllowed('198.51.100.1', 'webhooks').allowed).toBe(false)
      })
    })
  })

  describe('validateIpAccess', () => {
    async function getModule() {
      vi.resetModules()
      return import('@/lib/security/ip-allowlist')
    }

    // These tests require TRUST_PROXY to extract IP from headers
    beforeEach(() => {
      vi.stubEnv('TRUST_PROXY', 'true')
    })

    it('blocks if IP is on blocklist', async () => {
      vi.stubEnv('BLOCKED_IPS', '192.168.1.100')
      vi.stubEnv('SEND_EMAIL_ALLOWED_IPS', '192.168.1.0/24')
      const { validateIpAccess } = await getModule()

      const req = new NextRequest('https://example.com/api/send-email', {
        headers: {
          'x-forwarded-for': '192.168.1.100',
        },
      })

      const result = validateIpAccess(req, 'sendEmail')

      expect(result.allowed).toBe(false)
      expect(result.reason).toContain('blocked')
    })

    it('allows if not blocked and on allowlist', async () => {
      vi.stubEnv('SEND_EMAIL_ALLOWED_IPS', '192.168.1.0/24')
      const { validateIpAccess } = await getModule()

      const req = new NextRequest('https://example.com/api/send-email', {
        headers: {
          'x-forwarded-for': '192.168.1.50',
        },
      })

      const result = validateIpAccess(req, 'sendEmail')

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

    it('blocks if not on allowlist (when allowlist configured)', async () => {
      vi.stubEnv('SEND_EMAIL_ALLOWED_IPS', '10.0.0.0/8')
      const { validateIpAccess } = await getModule()

      const req = new NextRequest('https://example.com/api/send-email', {
        headers: {
          'x-forwarded-for': '192.168.1.50',
        },
      })

      const result = validateIpAccess(req, 'sendEmail')

      expect(result.allowed).toBe(false)
      expect(result.reason).toContain('not in allowlist')
    })

    it('returns extracted IP in result', async () => {
      const { validateIpAccess } = await getModule()

      const req = new NextRequest('https://example.com/api/test', {
        headers: {
          'x-forwarded-for': '203.0.113.50',
        },
      })

      const result = validateIpAccess(req, 'sendEmail')

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

    it('returns direct-connection when TRUST_PROXY is not set', async () => {
      vi.stubEnv('TRUST_PROXY', '')
      const { validateIpAccess } = await getModule()

      const req = new NextRequest('https://example.com/api/test', {
        headers: {
          'x-forwarded-for': '203.0.113.50',
        },
      })

      const result = validateIpAccess(req, 'sendEmail')

      // Without TRUST_PROXY, IP spoofing is prevented by ignoring headers
      expect(result.ip).toBe('direct-connection')
    })
  })

  describe('CIDR Matching', () => {
    async function getModule() {
      vi.resetModules()
      return import('@/lib/security/ip-allowlist')
    }

    it('handles /32 (single IP)', async () => {
      vi.stubEnv('BLOCKED_IPS', '192.168.1.1/32')
      const { isIpBlocked } = await getModule()

      expect(isIpBlocked('192.168.1.1')).toBe(true)
      expect(isIpBlocked('192.168.1.2')).toBe(false)
    })

    it('handles /24 (256 IPs)', async () => {
      vi.stubEnv('BLOCKED_IPS', '10.20.30.0/24')
      const { isIpBlocked } = await getModule()

      expect(isIpBlocked('10.20.30.0')).toBe(true)
      expect(isIpBlocked('10.20.30.127')).toBe(true)
      expect(isIpBlocked('10.20.30.255')).toBe(true)
      expect(isIpBlocked('10.20.31.0')).toBe(false)
    })

    it('handles /16 (65536 IPs)', async () => {
      vi.stubEnv('BLOCKED_IPS', '172.16.0.0/16')
      const { isIpBlocked } = await getModule()

      expect(isIpBlocked('172.16.0.1')).toBe(true)
      expect(isIpBlocked('172.16.255.255')).toBe(true)
      expect(isIpBlocked('172.17.0.1')).toBe(false)
    })

    it('handles /8 (16M IPs)', async () => {
      vi.stubEnv('BLOCKED_IPS', '10.0.0.0/8')
      const { isIpBlocked } = await getModule()

      expect(isIpBlocked('10.0.0.1')).toBe(true)
      expect(isIpBlocked('10.255.255.255')).toBe(true)
      expect(isIpBlocked('11.0.0.1')).toBe(false)
    })

    it('handles /0 (all IPs)', async () => {
      vi.stubEnv('BLOCKED_IPS', '0.0.0.0/0')
      const { isIpBlocked } = await getModule()

      expect(isIpBlocked('192.168.1.1')).toBe(true)
      expect(isIpBlocked('8.8.8.8')).toBe(true)
      expect(isIpBlocked('10.0.0.1')).toBe(true)
    })
  })

  describe('Edge Cases', () => {
    async function getModule() {
      vi.resetModules()
      return import('@/lib/security/ip-allowlist')
    }

    it('handles empty allowlist string', async () => {
      vi.stubEnv('SEND_EMAIL_ALLOWED_IPS', '')
      const { isIpAllowed } = await getModule()

      // Empty string should be treated as not configured
      expect(isIpAllowed('192.168.1.1', 'sendEmail').allowed).toBe(true)
    })

    it('handles whitespace in IP list', async () => {
      vi.stubEnv('BLOCKED_IPS', '  192.168.1.1  ,  10.0.0.1  ')
      const { isIpBlocked } = await getModule()

      expect(isIpBlocked('192.168.1.1')).toBe(true)
      expect(isIpBlocked('10.0.0.1')).toBe(true)
    })

    it('handles invalid CIDR notation gracefully', async () => {
      vi.stubEnv('BLOCKED_IPS', '192.168.1.0/invalid')
      const { isIpBlocked } = await getModule()

      // Should not throw, but may not match
      expect(() => isIpBlocked('192.168.1.1')).not.toThrow()
    })

    it('handles IPv4-mapped IPv6 addresses', async () => {
      vi.stubEnv('BLOCKED_IPS', '192.168.1.1')
      const { isIpBlocked } = await getModule()

      // Standard IPv4 should work
      expect(isIpBlocked('192.168.1.1')).toBe(true)
    })

    it('handles unknown IP string', async () => {
      vi.stubEnv('BLOCKED_IPS', '192.168.1.1')
      const { isIpBlocked } = await getModule()

      expect(isIpBlocked('unknown')).toBe(false)
    })
  })
})
