/**
 * CSRF Protection Unit Tests
 *
 * Tests for the CSRF protection module.
 * Covers token generation/validation, origin checking, and double-submit pattern.
 */

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

// Mock environment variables before importing
vi.stubEnv('CSRF_SECRET', 'test-csrf-secret-key-12345')
vi.stubEnv('PAYLOAD_PUBLIC_SERVER_URL', 'https://test.example.com')
vi.stubEnv('NEXT_PUBLIC_SERVER_URL', 'https://test.example.com')
// Clear CI environment variable to ensure CSRF validation works normally during tests
vi.stubEnv('CI', '')

import {
  generateCsrfToken,
  validateCsrfToken,
  validateOrigin,
  validateCsrf,
  CSRF_HEADER_NAME,
  CSRF_COOKIE,
} from '@/lib/security/csrf'

describe('CSRF Protection', () => {
  beforeEach(() => {
    vi.useFakeTimers()
    vi.setSystemTime(new Date('2024-01-15T12:00:00.000Z'))
  })

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

  describe('generateCsrfToken', () => {
    it('generates a token with correct format', () => {
      const token = generateCsrfToken()

      // Token format: timestamp:random:signature
      const parts = token.split(':')
      expect(parts.length).toBe(3)
    })

    it('generates unique tokens', () => {
      const token1 = generateCsrfToken()
      const token2 = generateCsrfToken()

      expect(token1).not.toBe(token2)
    })

    it('includes timestamp in base36', () => {
      const token = generateCsrfToken()
      const [timestampPart] = token.split(':')

      // Should be parseable as base36
      const timestamp = parseInt(timestampPart, 36)
      expect(timestamp).toBeGreaterThan(0)
    })

    it('generates consistent signature for same input', () => {
      // Since random part is different, signatures will differ
      // This test verifies the token is well-formed
      const token = generateCsrfToken()
      const [, , signature] = token.split(':')

      expect(signature.length).toBe(16) // HMAC hex truncated to 16 chars
    })
  })

  describe('validateCsrfToken', () => {
    it('validates a valid token', () => {
      const token = generateCsrfToken()
      const result = validateCsrfToken(token)

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

    it('rejects empty token', () => {
      const result = validateCsrfToken('')

      expect(result.valid).toBe(false)
      expect(result.reason).toBe('No token provided')
    })

    it('rejects malformed token (wrong parts)', () => {
      const result = validateCsrfToken('only-one-part')

      expect(result.valid).toBe(false)
      expect(result.reason).toBe('Invalid token format')
    })

    it('rejects token with invalid signature', () => {
      const token = generateCsrfToken()
      const parts = token.split(':')
      const tamperedToken = `${parts[0]}:${parts[1]}:invalidsignature`

      const result = validateCsrfToken(tamperedToken)

      expect(result.valid).toBe(false)
      expect(result.reason).toBe('Invalid signature')
    })

    it('rejects expired token', () => {
      const token = generateCsrfToken()

      // Advance time by 2 hours (past 1-hour expiry)
      vi.advanceTimersByTime(2 * 60 * 60 * 1000)

      const result = validateCsrfToken(token)

      expect(result.valid).toBe(false)
      expect(result.reason).toBe('Token expired')
    })

    it('accepts token within expiry window', () => {
      const token = generateCsrfToken()

      // Advance time by 30 minutes (within 1-hour expiry)
      vi.advanceTimersByTime(30 * 60 * 1000)

      const result = validateCsrfToken(token)

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

    it('rejects token with invalid timestamp', () => {
      const result = validateCsrfToken('invalid:random:signature')

      expect(result.valid).toBe(false)
    })
  })

  describe('validateOrigin', () => {
    it('allows requests without origin (server-to-server)', () => {
      const result = validateOrigin(null)

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

    it('allows configured server URL', () => {
      const result = validateOrigin('https://test.example.com')

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

    it('allows localhost for development', () => {
      const result1 = validateOrigin('http://localhost:3000')
      const result2 = validateOrigin('http://localhost:3001')

      expect(result1.valid).toBe(true)
      expect(result2.valid).toBe(true)
    })

    it('allows production domains', () => {
      const productionDomains = [
        'https://pl.c2sgmbh.de',
        'https://porwoll.de',
        'https://complexcaresolutions.de',
        'https://gunshin.de',
      ]

      productionDomains.forEach((origin) => {
        const result = validateOrigin(origin)
        expect(result.valid).toBe(true)
      })
    })

    it('allows subdomains of production domains', () => {
      const result = validateOrigin('https://www.porwoll.de')

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

    it('rejects unknown origins', () => {
      const result = validateOrigin('https://malicious-site.com')

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

    it('rejects HTTP for production domains', () => {
      const result = validateOrigin('http://porwoll.de')

      expect(result.valid).toBe(false)
    })
  })

  describe('validateCsrf', () => {
    function createMockRequest(
      method: string,
      options: {
        origin?: string | null
        csrfHeader?: string
        csrfCookie?: string
        authorization?: string
        contentType?: string
        referer?: string
      } = {},
    ): NextRequest {
      const headers = new Headers()

      if (options.origin !== null) {
        headers.set('origin', options.origin || 'https://test.example.com')
      }

      if (options.csrfHeader) {
        headers.set(CSRF_HEADER_NAME, options.csrfHeader)
      }

      if (options.authorization) {
        headers.set('authorization', options.authorization)
      }

      if (options.contentType) {
        headers.set('content-type', options.contentType)
      }

      if (options.referer) {
        headers.set('referer', options.referer)
      }

      const url = 'https://test.example.com/api/test'
      const request = new NextRequest(url, {
        method,
        headers,
      })

      // Mock cookies
      if (options.csrfCookie) {
        Object.defineProperty(request, 'cookies', {
          value: {
            get: (name: string) =>
              name === CSRF_COOKIE ? { value: options.csrfCookie } : undefined,
          },
        })
      } else {
        Object.defineProperty(request, 'cookies', {
          value: {
            get: () => undefined,
          },
        })
      }

      return request
    }

    describe('Safe Methods', () => {
      it('allows GET requests without CSRF token', () => {
        const req = createMockRequest('GET')
        const result = validateCsrf(req)

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

      it('allows HEAD requests without CSRF token', () => {
        const req = createMockRequest('HEAD')
        const result = validateCsrf(req)

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

      it('allows OPTIONS requests without CSRF token', () => {
        const req = createMockRequest('OPTIONS')
        const result = validateCsrf(req)

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

    describe('Server-to-Server Requests', () => {
      it('allows JSON requests with Authorization header but no Origin', () => {
        const req = createMockRequest('POST', {
          origin: null,
          authorization: 'Bearer some-token',
          contentType: 'application/json',
        })

        const result = validateCsrf(req)

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

      it('requires CSRF for requests with Origin even if authenticated', () => {
        const req = createMockRequest('POST', {
          origin: 'https://test.example.com',
          authorization: 'Bearer some-token',
          contentType: 'application/json',
        })

        const result = validateCsrf(req)

        expect(result.valid).toBe(false)
        expect(result.reason).toContain('CSRF token missing')
      })
    })

    describe('Admin Panel Requests', () => {
      it('allows requests from /admin without CSRF token', () => {
        const req = createMockRequest('POST', {
          referer: 'https://test.example.com/admin/login',
        })

        const result = validateCsrf(req)

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

      it('allows requests from /admin subpaths without CSRF token', () => {
        const req = createMockRequest('POST', {
          referer: 'https://test.example.com/admin/collections/users',
        })

        const result = validateCsrf(req)

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

      it('requires CSRF for non-admin referers', () => {
        const req = createMockRequest('POST', {
          referer: 'https://test.example.com/some-other-page',
        })

        const result = validateCsrf(req)

        expect(result.valid).toBe(false)
        expect(result.reason).toContain('CSRF token missing')
      })
    })

    describe('Double Submit Cookie Pattern', () => {
      it('validates matching header and cookie tokens', () => {
        const token = generateCsrfToken()
        const req = createMockRequest('POST', {
          csrfHeader: token,
          csrfCookie: token,
        })

        const result = validateCsrf(req)

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

      it('rejects missing header token', () => {
        const token = generateCsrfToken()
        const req = createMockRequest('POST', {
          csrfCookie: token,
        })

        const result = validateCsrf(req)

        expect(result.valid).toBe(false)
        expect(result.reason).toBe('CSRF token missing')
      })

      it('rejects missing cookie token', () => {
        const token = generateCsrfToken()
        const req = createMockRequest('POST', {
          csrfHeader: token,
        })

        const result = validateCsrf(req)

        expect(result.valid).toBe(false)
        expect(result.reason).toBe('CSRF token missing')
      })

      it('rejects mismatched tokens', () => {
        const token1 = generateCsrfToken()
        const token2 = generateCsrfToken()
        const req = createMockRequest('POST', {
          csrfHeader: token1,
          csrfCookie: token2,
        })

        const result = validateCsrf(req)

        expect(result.valid).toBe(false)
        expect(result.reason).toBe('CSRF token mismatch')
      })

      it('validates token content after matching', () => {
        const invalidToken = 'invalid:token:format'
        const req = createMockRequest('POST', {
          csrfHeader: invalidToken,
          csrfCookie: invalidToken,
        })

        const result = validateCsrf(req)

        expect(result.valid).toBe(false)
      })
    })

    describe('Origin Validation', () => {
      it('rejects requests from disallowed origins', () => {
        const token = generateCsrfToken()
        const req = createMockRequest('POST', {
          origin: 'https://evil-site.com',
          csrfHeader: token,
          csrfCookie: token,
        })

        const result = validateCsrf(req)

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

    describe('PUT and DELETE Methods', () => {
      it('requires CSRF for PUT requests', () => {
        const req = createMockRequest('PUT', {})
        const result = validateCsrf(req)

        expect(result.valid).toBe(false)
      })

      it('requires CSRF for DELETE requests', () => {
        const req = createMockRequest('DELETE', {})
        const result = validateCsrf(req)

        expect(result.valid).toBe(false)
      })

      it('validates CSRF for PUT with valid tokens', () => {
        const token = generateCsrfToken()
        const req = createMockRequest('PUT', {
          csrfHeader: token,
          csrfCookie: token,
        })

        const result = validateCsrf(req)

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

  describe('Constants', () => {
    it('exports correct header name', () => {
      expect(CSRF_HEADER_NAME).toBe('X-CSRF-Token')
    })

    it('exports correct cookie name', () => {
      expect(CSRF_COOKIE).toBe('csrf-token')
    })
  })
})
