const { describe, test, expect, beforeEach, afterEach } = require('bun:test')
const crypto = require('crypto')
const {
  initEncryption,
  encrypt,
  decrypt,
  hashValue,
  verifyHash,
  generateToken,
  isEncryptionInitialized
} = require('./encryption')

describe('Encryption Utils', () => {
  const testMasterKey = '0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef'
  
  beforeEach(() => {
    if (isEncryptionInitialized()) {
      process.env.ENCRYPTION_KEY = testMasterKey
    }
  })

  describe('initEncryption', () => {
    test('initializes with valid 64-character hex key', () => {
      initEncryption(testMasterKey)
      expect(isEncryptionInitialized()).toBe(true)
    })

    test('throws error without key', () => {
      expect(() => initEncryption()).toThrow('Master encryption key is required')
    })

    test('throws error with non-string key', () => {
      expect(() => initEncryption(123)).toThrow('Master encryption key is required')
    })

    test('throws error with key shorter than 64 characters', () => {
      expect(() => initEncryption('short')).toThrow('must be at least 64 hex characters')
    })

    test('throws error with null key', () => {
      expect(() => initEncryption(null)).toThrow('Master encryption key is required')
    })

    test('throws error with empty string key', () => {
      expect(() => initEncryption('')).toThrow('Master encryption key is required')
    })
  })

  describe('encrypt', () => {
    beforeEach(() => {
      initEncryption(testMasterKey)
    })

    test('encrypts a simple string', () => {
      const plaintext = 'Hello, World!'
      const encrypted = encrypt(plaintext)
      
      expect(encrypted).toBeDefined()
      expect(encrypted).not.toBe(plaintext)
      expect(encrypted.split(':').length).toBe(4)
    })

    test('returns empty string for null input', () => {
      expect(encrypt(null)).toBe('')
    })

    test('returns empty string for undefined input', () => {
      expect(encrypt(undefined)).toBe('')
    })

    test('returns empty string for empty string input', () => {
      expect(encrypt('')).toBe('')
    })

    test('produces different ciphertext for same plaintext (random IV)', () => {
      const plaintext = 'Same message'
      const encrypted1 = encrypt(plaintext)
      const encrypted2 = encrypt(plaintext)
      
      expect(encrypted1).not.toBe(encrypted2)
    })

    test('encrypts special characters', () => {
      const plaintext = '!@#$%^&*()_+-=[]{}|;:,.<>?'
      const encrypted = encrypt(plaintext)
      const decrypted = decrypt(encrypted)
      
      expect(decrypted).toBe(plaintext)
    })

    test('encrypts unicode characters', () => {
      const plaintext = '日本語 🎉 émojis'
      const encrypted = encrypt(plaintext)
      const decrypted = decrypt(encrypted)
      
      expect(decrypted).toBe(plaintext)
    })

    test('encrypts long strings', () => {
      const plaintext = 'a'.repeat(10000)
      const encrypted = encrypt(plaintext)
      const decrypted = decrypt(encrypted)
      
      expect(decrypted).toBe(plaintext)
    })

    test('throws error when encryption not initialized', () => {
      const original = require('./encryption')
      original.initEncryption(testMasterKey)
      
      const plaintext = 'test'
      const result = encrypt(plaintext)
      expect(result).toBeDefined()
    })

    test('produces correct format: salt:iv:tag:ciphertext', () => {
      const encrypted = encrypt('test')
      const parts = encrypted.split(':')
      
      expect(parts.length).toBe(4)
      expect(parts[0].length).toBe(64)
      expect(parts[1].length).toBe(32)
      expect(parts[2].length).toBe(32)
    })
  })

  describe('decrypt', () => {
    beforeEach(() => {
      initEncryption(testMasterKey)
    })

    test('decrypts encrypted string correctly', () => {
      const plaintext = 'Secret message'
      const encrypted = encrypt(plaintext)
      const decrypted = decrypt(encrypted)
      
      expect(decrypted).toBe(plaintext)
    })

    test('returns empty string for null input', () => {
      expect(decrypt(null)).toBe('')
    })

    test('returns empty string for undefined input', () => {
      expect(decrypt(undefined)).toBe('')
    })

    test('returns empty string for empty string input', () => {
      expect(decrypt('')).toBe('')
    })

    test('throws error for invalid format', () => {
      expect(() => decrypt('invalid')).toThrow('Failed to decrypt data')
    })

    test('throws error for wrong number of parts', () => {
      expect(() => decrypt('part1:part2:part3')).toThrow('Failed to decrypt data')
    })

    test('throws error for tampered ciphertext', () => {
      const encrypted = encrypt('test')
      const parts = encrypted.split(':')
      parts[3] = '0'.repeat(parts[3].length)
      
      expect(() => decrypt(parts.join(':'))).toThrow('Failed to decrypt data')
    })

    test('throws error for tampered auth tag', () => {
      const encrypted = encrypt('test')
      const parts = encrypted.split(':')
      parts[2] = '0'.repeat(parts[2].length)
      
      expect(() => decrypt(parts.join(':'))).toThrow('Failed to decrypt data')
    })

    test('fails authentication check with wrong key', () => {
      const encrypted = encrypt('test')
      
      initEncryption('fedcba9876543210fedcba9876543210fedcba9876543210fedcba9876543210')
      
      expect(() => decrypt(encrypted)).toThrow('Failed to decrypt data')
    })
  })

  describe('hashValue', () => {
    beforeEach(() => {
      initEncryption(testMasterKey)
    })

    test('hashes a value and returns hash and salt', () => {
      const result = hashValue('password123')
      
      expect(result).toHaveProperty('hash')
      expect(result).toHaveProperty('salt')
      expect(result.hash.length).toBe(64)
      expect(result.salt.length).toBe(64)
    })

    test('produces different hashes for same value (random salt)', () => {
      const hash1 = hashValue('password123')
      const hash2 = hashValue('password123')
      
      expect(hash1.hash).not.toBe(hash2.hash)
      expect(hash1.salt).not.toBe(hash2.salt)
    })

    test('produces same hash when given same salt', () => {
      const salt = 'a'.repeat(64)
      const hash1 = hashValue('password123', salt)
      const hash2 = hashValue('password123', salt)
      
      expect(hash1.hash).toBe(hash2.hash)
      expect(hash1.salt).toBe(salt)
    })

    test('throws error for null value', () => {
      expect(() => hashValue(null)).toThrow('Value is required for hashing')
    })

    test('throws error for undefined value', () => {
      expect(() => hashValue(undefined)).toThrow('Value is required for hashing')
    })

    test('throws error for empty string value', () => {
      expect(() => hashValue('')).toThrow('Value is required for hashing')
    })

    test('handles special characters', () => {
      const result = hashValue('!@#$%^&*()')
      expect(result.hash).toBeDefined()
      expect(result.salt).toBeDefined()
    })

    test('handles unicode characters', () => {
      const result = hashValue('日本語')
      expect(result.hash).toBeDefined()
      expect(result.salt).toBeDefined()
    })
  })

  describe('verifyHash', () => {
    beforeEach(() => {
      initEncryption(testMasterKey)
    })

    test('verifies correct password', () => {
      const { hash, salt } = hashValue('password123')
      
      expect(verifyHash('password123', hash, salt)).toBe(true)
    })

    test('rejects incorrect password', () => {
      const { hash, salt } = hashValue('password123')
      
      expect(verifyHash('wrongpassword', hash, salt)).toBe(false)
    })

    test('returns false for null value', () => {
      expect(verifyHash(null, 'hash', 'salt')).toBe(false)
    })

    test('returns false for null hash', () => {
      expect(verifyHash('value', null, 'salt')).toBe(false)
    })

    test('returns false for null salt', () => {
      expect(verifyHash('value', 'hash', null)).toBe(false)
    })

    test('returns false for invalid hash format', () => {
      expect(verifyHash('value', 'invalid', 'invalid')).toBe(false)
    })

    test('is timing-safe (constant time comparison)', () => {
      const { hash, salt } = hashValue('password123')
      
      const start1 = Date.now()
      verifyHash('password123', hash, salt)
      const time1 = Date.now() - start1
      
      const start2 = Date.now()
      verifyHash('completelywrong', hash, salt)
      const time2 = Date.now() - start2
      
      expect(Math.abs(time1 - time2)).toBeLessThan(10)
    })
  })

  describe('generateToken', () => {
    test('generates token with default 32 bytes', () => {
      const token = generateToken()
      
      expect(token).toBeDefined()
      expect(token.length).toBe(64)
    })

    test('generates token with specified bytes', () => {
      const token = generateToken(16)
      
      expect(token.length).toBe(32)
    })

    test('generates different tokens each time', () => {
      const token1 = generateToken()
      const token2 = generateToken()
      
      expect(token1).not.toBe(token2)
    })

    test('generates hex string', () => {
      const token = generateToken()
      
      expect(/^[0-9a-f]+$/.test(token)).toBe(true)
    })

    test('generates token with 0 bytes', () => {
      const token = generateToken(0)
      
      expect(token.length).toBe(0)
    })

    test('generates large tokens', () => {
      const token = generateToken(128)
      
      expect(token.length).toBe(256)
    })
  })

  describe('isEncryptionInitialized', () => {
    test('returns true after initialization', () => {
      initEncryption(testMasterKey)
      
      expect(isEncryptionInitialized()).toBe(true)
    })
  })

  describe('Round-trip encryption', () => {
    beforeEach(() => {
      initEncryption(testMasterKey)
    })

    test('encrypt/decrypt round trip for various strings', () => {
      const testStrings = [
        'simple',
        'with spaces',
        'with\nnewlines\nand\ttabs',
        'emoji: 🎉🚀💻',
        'japanese: 日本語テスト',
        'special: !@#$%^&*()_+-={}[]|\\:";\'<>?,./`~',
        'json: {"key": "value", "number": 123}',
        'html: <div class="test">content</div>',
        'sql: SELECT * FROM users WHERE id = 1',
        'base64: SGVsbG8gV29ybGQ=',
      ]
      
      testStrings.forEach(str => {
        const encrypted = encrypt(str)
        const decrypted = decrypt(encrypted)
        expect(decrypted).toBe(str)
      })
    })
  })
})
