/**
 * Data Masking Unit Tests
 *
 * Tests for the data masking module.
 * Covers field detection, pattern matching, recursive masking, and special formats.
 */

import { describe, it, expect, beforeEach, vi } from 'vitest'
import {
  maskString,
  maskObject,
  maskError,
  isSensitiveField,
  safeStringify,
  createSafeLogger,
  maskSmtpError,
} from '@/lib/security/data-masking'

describe('Data Masking', () => {
  describe('isSensitiveField', () => {
    it('detects password field', () => {
      expect(isSensitiveField('password')).toBe(true)
      expect(isSensitiveField('PASSWORD')).toBe(true)
      expect(isSensitiveField('userPassword')).toBe(true)
      expect(isSensitiveField('password_hash')).toBe(true)
    })

    it('detects token fields', () => {
      expect(isSensitiveField('token')).toBe(true)
      expect(isSensitiveField('accessToken')).toBe(true)
      expect(isSensitiveField('refreshToken')).toBe(true)
      expect(isSensitiveField('apiToken')).toBe(true)
    })

    it('detects API key fields', () => {
      expect(isSensitiveField('apiKey')).toBe(true)
      expect(isSensitiveField('api_key')).toBe(true)
      expect(isSensitiveField('apikey')).toBe(true)
    })

    it('detects secret fields', () => {
      expect(isSensitiveField('secret')).toBe(true)
      expect(isSensitiveField('clientSecret')).toBe(true)
      expect(isSensitiveField('payloadSecret')).toBe(true)
    })

    it('detects credential fields', () => {
      expect(isSensitiveField('credentials')).toBe(true)
      expect(isSensitiveField('authCredentials')).toBe(true)
    })

    it('detects private key fields', () => {
      expect(isSensitiveField('privateKey')).toBe(true)
      expect(isSensitiveField('private_key')).toBe(true)
    })

    it('detects SMTP-related fields', () => {
      expect(isSensitiveField('smtpPassword')).toBe(true)
      expect(isSensitiveField('smtp_pass')).toBe(true)
    })

    it('returns false for non-sensitive fields', () => {
      expect(isSensitiveField('email')).toBe(false)
      expect(isSensitiveField('name')).toBe(false)
      expect(isSensitiveField('id')).toBe(false)
      expect(isSensitiveField('status')).toBe(false)
    })
  })

  describe('maskString', () => {
    it('masks JWT tokens', () => {
      const jwt =
        'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c'

      const masked = maskString(jwt)

      // JWT format: keeps header, redacts payload and signature
      expect(masked).toContain('eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9')
      expect(masked).toContain('[PAYLOAD REDACTED]')
      expect(masked).toContain('[SIGNATURE REDACTED]')
    })

    it('masks connection strings', () => {
      const connectionString = 'postgresql://user:secret123@localhost:5432/mydb'

      const masked = maskString(connectionString)

      expect(masked).toContain('[REDACTED]')
      expect(masked).not.toContain('secret123')
    })

    it('masks private keys (inline format)', () => {
      // Test with inline private key format
      const privateKey = '-----BEGIN RSA PRIVATE KEY-----MIIEpAIBAAKCAQEA...-----END RSA PRIVATE KEY-----'

      const masked = maskString(privateKey)

      expect(masked).toBe('[PRIVATE KEY REDACTED]')
    })

    it('masks password patterns in text', () => {
      const text = 'User logged in with password=secret123 successfully'

      const masked = maskString(text)

      // The pattern replaces password=... with password: [REDACTED] or pass: [REDACTED]
      expect(masked).toContain('[REDACTED]')
      expect(masked).not.toContain('secret123')
    })

    it('preserves non-sensitive text', () => {
      const text = 'Normal log message without sensitive data'

      const masked = maskString(text)

      expect(masked).toBe(text)
    })

    it('handles empty string', () => {
      expect(maskString('')).toBe('')
    })

    it('handles null/undefined gracefully', () => {
      // The function returns the input for non-strings
      expect(maskString(null as unknown as string)).toBeNull()
      expect(maskString(undefined as unknown as string)).toBeUndefined()
    })
  })

  describe('maskObject', () => {
    it('masks sensitive fields in flat object', () => {
      const obj = {
        email: 'user@example.com',
        password: 'supersecret',
        name: 'John Doe',
      }

      const masked = maskObject(obj)

      expect(masked.email).toBe('user@example.com')
      expect(masked.password).toBe('[REDACTED]')
      expect(masked.name).toBe('John Doe')
    })

    it('masks sensitive fields in nested object', () => {
      const obj = {
        user: {
          email: 'user@example.com',
          credentials: {
            password: 'secret',
            apiKey: 'key-12345',
          },
        },
        status: 'active',
      }

      const masked = maskObject(obj) as typeof obj

      expect(masked.user.email).toBe('user@example.com')
      // credentials itself is a sensitive field name
      expect(masked.user.credentials).toBe('[REDACTED OBJECT]')
      expect(masked.status).toBe('active')
    })

    it('masks arrays with sensitive values', () => {
      const obj = {
        tokens: ['token1', 'token2'],
        users: [{ name: 'Alice', password: 'secret1' }, { name: 'Bob', password: 'secret2' }],
      }

      const masked = maskObject(obj) as typeof obj

      // tokens is a sensitive field name (contains 'token')
      expect(masked.tokens).toBe('[REDACTED OBJECT]')
      expect(masked.users[0].name).toBe('Alice')
      expect(masked.users[0].password).toBe('[REDACTED]')
    })

    it('handles circular references gracefully', () => {
      const obj: Record<string, unknown> = { name: 'test' }
      obj.self = obj

      // Should not throw
      expect(() => maskObject(obj)).not.toThrow()
    })

    it('respects depth limit', () => {
      const deepObj = {
        level1: {
          level2: {
            level3: {
              level4: {
                level5: {
                  password: 'secret',
                },
              },
            },
          },
        },
      }

      const masked = maskObject(deepObj, { maxDepth: 3 })

      // Deep nested should still be handled
      expect(masked).toBeDefined()
    })

    it('preserves specified fields', () => {
      const obj = {
        password: 'secret',
        debugPassword: 'debug-secret',
      }

      const masked = maskObject(obj, { preserveFields: ['debugPassword'] })

      expect(masked.password).toBe('[REDACTED]')
      expect(masked.debugPassword).toBe('debug-secret')
    })

    it('masks additional custom fields', () => {
      const obj = {
        customSecret: 'my-custom-value',
        normalField: 'normal',
      }

      const masked = maskObject(obj, { additionalFields: ['customSecret'] })

      expect(masked.customSecret).toBe('[REDACTED]')
      expect(masked.normalField).toBe('normal')
    })

    it('masks string values containing secrets', () => {
      const obj = {
        config:
          'database_url=postgresql://admin:password123@localhost/db apiKey=sk-12345',
      }

      const masked = maskObject(obj) as typeof obj

      expect(masked.config).not.toContain('password123')
    })

    it('handles null and undefined values', () => {
      const obj = {
        password: null,
        token: undefined,
        name: 'test',
      }

      const masked = maskObject(obj)

      expect(masked.password).toBeNull()
      expect(masked.token).toBeUndefined()
      expect(masked.name).toBe('test')
    })
  })

  describe('maskError', () => {
    it('masks error message', () => {
      const error = new Error('Database connection failed: password=secret123')

      const masked = maskError(error)

      expect(masked.message).not.toContain('secret123')
      expect(masked.message).toContain('[REDACTED]')
    })

    it('masks error stack', () => {
      const error = new Error('Error with password=secret')

      const masked = maskError(error)

      if (masked.stack) {
        expect(masked.stack).not.toContain('secret')
      }
    })

    it('preserves error structure', () => {
      const error = new Error('Test error')
      error.name = 'CustomError'

      const masked = maskError(error)

      expect(masked.name).toBe('CustomError')
      expect(masked.message).toBe('Test error')
    })

    it('handles non-Error objects', () => {
      const notAnError = { message: 'password=secret', code: 500 }

      const masked = maskError(notAnError as unknown as Error)

      expect(masked).toBeDefined()
    })

    it('handles string errors', () => {
      const stringError = 'Connection failed: apiKey=12345'

      const masked = maskError(stringError as unknown as Error)

      expect(masked).toBeDefined()
    })
  })

  describe('safeStringify', () => {
    it('stringifies and masks object', () => {
      const obj = {
        user: 'admin',
        password: 'secret',
      }

      const result = safeStringify(obj)

      expect(result).toContain('admin')
      expect(result).toContain('[REDACTED]')
      expect(result).not.toContain('secret')
    })

    it('handles circular references', () => {
      const obj: Record<string, unknown> = { name: 'test' }
      obj.self = obj

      // Should not throw
      expect(() => safeStringify(obj)).not.toThrow()
    })

    it('formats with spaces when specified', () => {
      const obj = { name: 'test' }

      const result = safeStringify(obj, 2)

      expect(result).toContain('\n')
    })

    it('handles arrays', () => {
      const arr = [{ password: 'secret' }, { name: 'test' }]

      const result = safeStringify(arr)

      expect(result).toContain('[REDACTED]')
      expect(result).toContain('test')
    })
  })

  describe('createSafeLogger', () => {
    beforeEach(() => {
      vi.spyOn(console, 'log').mockImplementation(() => {})
      vi.spyOn(console, 'error').mockImplementation(() => {})
      vi.spyOn(console, 'warn').mockImplementation(() => {})
      // Clear any previous calls
      vi.mocked(console.log).mockClear()
      vi.mocked(console.error).mockClear()
      vi.mocked(console.warn).mockClear()
    })

    it('creates logger with info method', () => {
      const logger = createSafeLogger('TestModule')

      logger.info('Test message')

      expect(console.log).toHaveBeenCalledWith(
        expect.stringContaining('[TestModule]'),
        expect.any(String),
        expect.any(String),
      )
    })

    it('masks sensitive data in log messages', () => {
      const logger = createSafeLogger('Auth')

      logger.info('User login', { password: 'secret123' })

      const lastCall = (console.log as ReturnType<typeof vi.fn>).mock.calls[0]
      const logOutput = JSON.stringify(lastCall)

      expect(logOutput).toContain('[REDACTED]')
      expect(logOutput).not.toContain('secret123')
    })

    it('provides error method', () => {
      const logger = createSafeLogger('Error')

      logger.error('Error occurred', new Error('password=secret'))

      expect(console.error).toHaveBeenCalled()
    })

    it('provides warn method', () => {
      const logger = createSafeLogger('Warn')

      logger.warn('Warning', { apiKey: 'key-123' })

      expect(console.warn).toHaveBeenCalled()
    })
  })

  describe('maskSmtpError', () => {
    it('masks SMTP authentication failure with credentials', () => {
      const error = 'AUTH LOGIN failed: Username and Password not accepted.'

      const masked = maskSmtpError(error)

      expect(masked).toContain('[')
      expect(masked).not.toContain('Username and Password')
    })

    it('masks 535 authentication error codes with details', () => {
      const error = '535 5.7.8 Error: Username and Password not accepted for user@example.com'

      const masked = maskSmtpError(error)

      expect(masked).not.toContain('user@example.com')
      expect(masked).toContain('535')
    })

    it('masks invalid login errors with user details', () => {
      const error = 'Invalid login: 535-5.7.8 Username and Password not accepted'

      const masked = maskSmtpError(error)

      expect(masked).toContain('[')
    })

    it('masks connection strings in SMTP errors', () => {
      const error = 'Connection failed to smtp://admin:secret123@mail.example.com:587'

      const masked = maskSmtpError(error)

      expect(masked).not.toContain('secret123')
      expect(masked).toContain('[REDACTED]')
    })

    it('preserves safe SMTP error codes', () => {
      const error = '530 5.7.0 Must issue a STARTTLS command first'

      const masked = maskSmtpError(error)

      // Should keep the diagnostic error code
      expect(masked).toContain('530')
      expect(masked).toContain('STARTTLS')
    })

    it('truncates excessively long error messages', () => {
      const longError = 'Error: ' + 'A'.repeat(500)

      const masked = maskSmtpError(longError)

      expect(masked!.length).toBeLessThanOrEqual(203) // 200 + "..."
      expect(masked).toContain('...')
    })

    it('handles null and undefined gracefully', () => {
      expect(maskSmtpError(null)).toBeNull()
      expect(maskSmtpError(undefined)).toBeNull()
    })

    it('handles empty string', () => {
      expect(maskSmtpError('')).toBeNull()
    })

    it('passes through simple error messages unchanged', () => {
      const error = 'Connection timeout'

      const masked = maskSmtpError(error)

      expect(masked).toBe('Connection timeout')
    })

    it('masks Base64 encoded credentials in AUTH', () => {
      const error = 'AUTH PLAIN dXNlcm5hbWU6cGFzc3dvcmQxMjM= failed'

      const masked = maskSmtpError(error)

      expect(masked).not.toContain('dXNlcm5hbWU6cGFzc3dvcmQxMjM=')
    })

    it('masks password= patterns in SMTP error details', () => {
      const error = 'SMTP config: host=mail.example.com password=supersecret123'

      const masked = maskSmtpError(error)

      expect(masked).not.toContain('supersecret123')
      expect(masked).toContain('[REDACTED]')
    })
  })

  describe('Real-world Scenarios', () => {
    it('masks SMTP configuration', () => {
      const smtpConfig = {
        host: 'smtp.example.com',
        port: 587,
        smtpUser: 'sender@example.com',
        smtpPassword: 'smtp-password-123',
      }

      const masked = maskObject(smtpConfig) as typeof smtpConfig

      expect(masked.host).toBe('smtp.example.com')
      expect(masked.smtpUser).toBe('sender@example.com')
      expect(masked.smtpPassword).toBe('[REDACTED]')
    })

    it('masks tenant data', () => {
      const tenant = {
        id: 1,
        name: 'Test Tenant',
        smtpSettings: {
          host: 'smtp.tenant.com',
          password: 'tenant-smtp-pass',
          apiKey: 'sendgrid-key-123',
        },
      }

      const masked = maskObject(tenant) as typeof tenant

      expect(masked.id).toBe(1)
      expect(masked.name).toBe('Test Tenant')
      expect(masked.smtpSettings.host).toBe('smtp.tenant.com')
      expect(masked.smtpSettings.password).toBe('[REDACTED]')
      expect(masked.smtpSettings.apiKey).toBe('[REDACTED]')
    })

    it('masks audit log data', () => {
      const auditLog = {
        action: 'login_failed',
        userEmail: 'user@example.com',
        metadata: {
          attemptedPassword: 'wrong-password',
          ipAddress: '192.168.1.1',
        },
      }

      const masked = maskObject(auditLog) as typeof auditLog

      expect(masked.action).toBe('login_failed')
      expect(masked.userEmail).toBe('user@example.com')
      expect(masked.metadata.attemptedPassword).toBe('[REDACTED]')
      expect(masked.metadata.ipAddress).toBe('192.168.1.1')
    })

    it('masks database connection errors', () => {
      const error = new Error(
        'FATAL: password authentication failed for user "admin" at host "db.example.com"',
      )

      const masked = maskError(error)

      expect(masked.message).not.toContain('authentication failed')
    })
  })
})
