import crypto from 'crypto'
import { describe, it, expect, beforeAll } from 'vitest'
import { encryptCredential, decryptCredential, setKek, CURRENT_KEY_ID } from '../server/encryption'

beforeAll(() => {
  setKek(CURRENT_KEY_ID, crypto.randomBytes(32))
})

describe('Credential Encryption', () => {
  describe('encryptCredential', () => {
    it('encrypts plaintext to base64-encoded JSON string', () => {
      const plaintext = 'my-secret-api-key'
      const encrypted = encryptCredential(plaintext)

      // Should be a valid JSON string
      expect(() => JSON.parse(encrypted)).not.toThrow()

      const parsed = JSON.parse(encrypted)
      expect(parsed).toHaveProperty('iv')
      expect(parsed).toHaveProperty('authTag')
      expect(parsed).toHaveProperty('data')
    })

    it('produces different ciphertext for same input (random IV)', () => {
      const plaintext = 'same-secret'

      const encrypted1 = encryptCredential(plaintext)
      const encrypted2 = encryptCredential(plaintext)

      // Different IVs mean different ciphertext
      expect(encrypted1).not.toBe(encrypted2)

      // But both decrypt to same plaintext
      expect(decryptCredential(encrypted1)).toBe(plaintext)
      expect(decryptCredential(encrypted2)).toBe(plaintext)
    })

    it('handles empty string', () => {
      const plaintext = ''
      const encrypted = encryptCredential(plaintext)
      const decrypted = decryptCredential(encrypted)

      expect(decrypted).toBe('')
    })

    it('handles long strings', () => {
      const plaintext = 'x'.repeat(10000)
      const encrypted = encryptCredential(plaintext)
      const decrypted = decryptCredential(encrypted)

      expect(decrypted).toBe(plaintext)
    })

    it('handles special characters and unicode', () => {
      const plaintext = 'Hello 世界! 🔐 Special chars: \n\t\r"\'\\`'
      const encrypted = encryptCredential(plaintext)
      const decrypted = decryptCredential(encrypted)

      expect(decrypted).toBe(plaintext)
    })

    it('validates key is configured at module load', () => {
      // We can't test missing key dynamically since MASTER_KEY is set at module load
      // Instead, verify the key is set and encryption works
      const encrypted = encryptCredential('test')
      expect(encrypted).toBeDefined()
    })
  })

  describe('decryptCredential', () => {
    it('decrypts ciphertext back to original plaintext', () => {
      const plaintext = 'my-secret-api-key'
      const encrypted = encryptCredential(plaintext)
      const decrypted = decryptCredential(encrypted)

      expect(decrypted).toBe(plaintext)
    })

    it('successfully decrypts with configured key', () => {
      const encrypted = encryptCredential('test')
      const decrypted = decryptCredential(encrypted)

      expect(decrypted).toBe('test')
    })

    it('throws error on malformed JSON', () => {
      expect(() => decryptCredential('not-json')).toThrow()
    })

    it('throws error on missing fields', () => {
      const invalidData = JSON.stringify({ iv: 'abc', authTag: 'def' }) // Missing 'data'

      expect(() => decryptCredential(invalidData)).toThrow()
    })

    it('throws error on tampered ciphertext', () => {
      const encrypted = encryptCredential('secret')
      const parsed = JSON.parse(encrypted)

      // Tamper with the data
      parsed.data = 'tampered-data'
      const tampered = JSON.stringify(parsed)

      expect(() => decryptCredential(tampered)).toThrow()
    })

    it('throws error on tampered auth tag', () => {
      const encrypted = encryptCredential('secret')
      const parsed = JSON.parse(encrypted)

      // Tamper with the auth tag
      parsed.authTag = 'AAAAAAAAAAAAAAAAAAAAAA=='
      const tampered = JSON.stringify(parsed)

      expect(() => decryptCredential(tampered)).toThrow()
    })

    it('detects tampering via auth tag (GCM mode)', () => {
      const encrypted = encryptCredential('secret')
      const parsed = JSON.parse(encrypted)

      // Tamper with just one byte of data
      const dataBuffer = Buffer.from(parsed.data, 'base64')
      dataBuffer[0] ^= 0x01 // Flip one bit
      parsed.data = dataBuffer.toString('base64')

      const tampered = JSON.stringify(parsed)

      // GCM should detect the tampering and throw
      expect(() => decryptCredential(tampered)).toThrow()
    })

    it('throws error on invalid base64 encoding', () => {
      const invalidData = JSON.stringify({
        iv: 'not-valid-base64!!!',
        authTag: 'AAAA',
        data: 'BBBB'
      })

      expect(() => decryptCredential(invalidData)).toThrow()
    })
  })

  describe('Round-trip encryption/decryption', () => {
    it('preserves API key format', () => {
      const apiKey = 'sk_test_1234567890abcdefghijklmnopqrstuvwxyz'
      const encrypted = encryptCredential(apiKey)
      const decrypted = decryptCredential(encrypted)

      expect(decrypted).toBe(apiKey)
    })

    it('preserves database connection strings', () => {
      const connString = 'postgresql://user:password@localhost:5432/database?sslmode=require'
      const encrypted = encryptCredential(connString)
      const decrypted = decryptCredential(encrypted)

      expect(decrypted).toBe(connString)
    })

    it('preserves JSON credentials', () => {
      const jsonCreds = JSON.stringify({
        type: 'service_account',
        project_id: 'my-project',
        private_key: '-----BEGIN PRIVATE KEY-----\nMIIE...\n-----END PRIVATE KEY-----\n'
      })

      const encrypted = encryptCredential(jsonCreds)
      const decrypted = decryptCredential(encrypted)

      expect(decrypted).toBe(jsonCreds)
      expect(JSON.parse(decrypted)).toEqual(JSON.parse(jsonCreds))
    })
  })

  describe('Security properties', () => {
    it('uses AES-256-GCM (authenticated encryption)', () => {
      const encrypted = encryptCredential('test')
      const parsed = JSON.parse(encrypted)

      // Auth tag should be present (GCM mode)
      expect(parsed.authTag).toBeDefined()
      expect(parsed.authTag.length).toBeGreaterThan(0)
    })

    it('uses random IV for each encryption', () => {
      const ivs = new Set()

      for (let i = 0; i < 100; i++) {
        const encrypted = encryptCredential('test')
        const parsed = JSON.parse(encrypted)
        ivs.add(parsed.iv)
      }

      // All IVs should be unique
      expect(ivs.size).toBe(100)
    })

    it('IV is 16 bytes (128 bits)', () => {
      const encrypted = encryptCredential('test')
      const parsed = JSON.parse(encrypted)

      // Base64-encoded 16 bytes = 24 characters
      const ivBuffer = Buffer.from(parsed.iv, 'base64')
      expect(ivBuffer.length).toBe(16)
    })

    it('auth tag is 16 bytes (128 bits)', () => {
      const encrypted = encryptCredential('test')
      const parsed = JSON.parse(encrypted)

      // Base64-encoded 16 bytes = 24 characters
      const authTagBuffer = Buffer.from(parsed.authTag, 'base64')
      expect(authTagBuffer.length).toBe(16)
    })
  })
})
