/**
 * Crypto Module Tests
 * @countermeasure/web-components
 */

import { describe, it, expect, beforeAll } from 'vitest'

import {
  // Encoding
  toBase64,
  fromBase64,
  toHex,
  fromHex,
  stringToBytes,
  bytesToString,
  encodeEncryptedData,
  decodeEncryptedData,
  constantTimeEqual,
  // Keys
  generateSymmetricKey,
  importSymmetricKey,
  generateIV,
  randomBytes,
  generateKeyPair,
  exportPublicKey,
  exportPrivateKey,
  importPublicKey,
  importPrivateKey,
  deriveKeyFromPassword,
  // Symmetric
  createSymmetricCipher,
  encryptSecret,
  decryptSecret,
  // Hybrid
  createHybridCipher,
  hybridEncrypt,
  hybridDecrypt,
  hybridDecryptToString,
  // Manager
  createCryptoManager,
  cryptum,
  // Types
  CryptoError,
} from './index'

// Skip tests if Web Crypto API is not available (e.g., in some test environments)
const hasWebCrypto = typeof crypto !== 'undefined' && crypto.subtle !== undefined

describe.skipIf(!hasWebCrypto)('Crypto Module', () => {
  // =========================================================================
  // ENCODING TESTS
  // =========================================================================

  describe('Encoding', () => {
    describe('Base64', () => {
      it('should encode and decode base64', () => {
        const original = new Uint8Array([72, 101, 108, 108, 111]) // "Hello"
        const encoded = toBase64(original)
        const decoded = fromBase64(encoded)
        expect(decoded).toEqual(original)
      })

      it('should handle empty data', () => {
        const empty = new Uint8Array(0)
        const encoded = toBase64(empty)
        const decoded = fromBase64(encoded)
        expect(decoded).toEqual(empty)
      })

      it('should throw on invalid base64', () => {
        expect(() => fromBase64('!!invalid!!')).toThrow(CryptoError)
      })
    })

    describe('Hex', () => {
      it('should encode and decode hex', () => {
        const original = new Uint8Array([0x48, 0x65, 0x6c, 0x6c, 0x6f])
        const encoded = toHex(original)
        expect(encoded).toBe('48656c6c6f')
        const decoded = fromHex(encoded)
        expect(decoded).toEqual(original)
      })

      it('should handle uppercase hex', () => {
        const decoded = fromHex('48656C6C6F')
        expect(decoded).toEqual(new Uint8Array([0x48, 0x65, 0x6c, 0x6c, 0x6f]))
      })

      it('should throw on invalid hex (odd length)', () => {
        expect(() => fromHex('123')).toThrow(CryptoError)
      })

      it('should throw on invalid hex (non-hex chars)', () => {
        expect(() => fromHex('ghij')).toThrow(CryptoError)
      })
    })

    describe('String encoding', () => {
      it('should convert string to bytes and back', () => {
        const original = 'Hello, 世界! 🎉'
        const bytes = stringToBytes(original)
        const result = bytesToString(bytes)
        expect(result).toBe(original)
      })
    })

    describe('Encrypted data format', () => {
      it('should encode and decode encrypted data', () => {
        const iv = new Uint8Array(12).fill(1)
        const ciphertext = new Uint8Array([1, 2, 3, 4, 5])
        const encoded = encodeEncryptedData(iv, ciphertext)
        const decoded = decodeEncryptedData(encoded)
        expect(decoded.iv).toEqual(iv)
        expect(decoded.ciphertext).toEqual(ciphertext)
      })

      it('should throw on too short data', () => {
        expect(() => decodeEncryptedData(new Uint8Array(5))).toThrow(CryptoError)
      })

      it('should throw on invalid version', () => {
        const invalid = new Uint8Array(20)
        invalid[0] = 0xff // Invalid version
        expect(() => decodeEncryptedData(invalid)).toThrow(CryptoError)
      })
    })

    describe('Constant time comparison', () => {
      it('should return true for equal arrays', () => {
        const a = new Uint8Array([1, 2, 3, 4])
        const b = new Uint8Array([1, 2, 3, 4])
        expect(constantTimeEqual(a, b)).toBe(true)
      })

      it('should return false for different arrays', () => {
        const a = new Uint8Array([1, 2, 3, 4])
        const b = new Uint8Array([1, 2, 3, 5])
        expect(constantTimeEqual(a, b)).toBe(false)
      })

      it('should return false for different lengths', () => {
        const a = new Uint8Array([1, 2, 3])
        const b = new Uint8Array([1, 2, 3, 4])
        expect(constantTimeEqual(a, b)).toBe(false)
      })
    })
  })

  // =========================================================================
  // KEY TESTS
  // =========================================================================

  describe('Keys', () => {
    describe('Symmetric key generation', () => {
      it('should generate a 64-character hex key', async () => {
        const key = await generateSymmetricKey()
        expect(key).toHaveLength(64)
        expect(/^[0-9a-f]+$/.test(key)).toBe(true)
      })

      it('should generate unique keys', async () => {
        const key1 = await generateSymmetricKey()
        const key2 = await generateSymmetricKey()
        expect(key1).not.toBe(key2)
      })
    })

    describe('Symmetric key import', () => {
      it('should import a valid hex key', async () => {
        const hexKey = '0'.repeat(64)
        const key = await importSymmetricKey(hexKey)
        expect(key).toBeDefined()
        expect(key.type).toBe('secret')
      })

      it('should throw on invalid key length', async () => {
        await expect(importSymmetricKey('0'.repeat(32))).rejects.toThrow(CryptoError)
      })
    })

    describe('IV generation', () => {
      it('should generate 12-byte IV', () => {
        const iv = generateIV()
        expect(iv).toHaveLength(12)
      })

      it('should generate unique IVs', () => {
        const iv1 = generateIV()
        const iv2 = generateIV()
        expect(constantTimeEqual(iv1, iv2)).toBe(false)
      })
    })

    describe('Random bytes', () => {
      it('should generate specified length', () => {
        const bytes = randomBytes(32)
        expect(bytes).toHaveLength(32)
      })
    })

    describe('RSA key pair', () => {
      it('should generate 2048-bit key pair', async () => {
        const keypair = await generateKeyPair(2048)
        expect(keypair.publicKey).toBeDefined()
        expect(keypair.privateKey).toBeDefined()
        expect(keypair.publicKey.type).toBe('public')
        expect(keypair.privateKey.type).toBe('private')
      })

      it('should export and import public key', async () => {
        const keypair = await generateKeyPair(2048)
        const pem = await exportPublicKey(keypair.publicKey)
        expect(pem).toContain('-----BEGIN PUBLIC KEY-----')
        expect(pem).toContain('-----END PUBLIC KEY-----')

        const imported = await importPublicKey(pem)
        expect(imported.type).toBe('public')
      })

      it('should export and import private key', async () => {
        const keypair = await generateKeyPair(2048)
        const pem = await exportPrivateKey(keypair.privateKey)
        expect(pem).toContain('-----BEGIN PRIVATE KEY-----')
        expect(pem).toContain('-----END PRIVATE KEY-----')

        const imported = await importPrivateKey(pem)
        expect(imported.type).toBe('private')
      })
    })

    describe('Key derivation', () => {
      it('should derive key from password', async () => {
        const result = await deriveKeyFromPassword('test-password')
        expect(result.key).toBeDefined()
        expect(result.salt).toHaveLength(16)
      })

      it('should derive same key with same salt', async () => {
        const salt = randomBytes(16)
        const result1 = await deriveKeyFromPassword('test-password', salt)
        const result2 = await deriveKeyFromPassword('test-password', salt)

        const raw1 = await crypto.subtle.exportKey('raw', result1.key)
        const raw2 = await crypto.subtle.exportKey('raw', result2.key)

        expect(new Uint8Array(raw1)).toEqual(new Uint8Array(raw2))
      })

      it('should derive different key with different password', async () => {
        const salt = randomBytes(16)
        const result1 = await deriveKeyFromPassword('password1', salt)
        const result2 = await deriveKeyFromPassword('password2', salt)

        const raw1 = await crypto.subtle.exportKey('raw', result1.key)
        const raw2 = await crypto.subtle.exportKey('raw', result2.key)

        expect(new Uint8Array(raw1)).not.toEqual(new Uint8Array(raw2))
      })
    })
  })

  // =========================================================================
  // SYMMETRIC ENCRYPTION TESTS
  // =========================================================================

  describe('Symmetric Encryption', () => {
    let testKey: string

    beforeAll(async () => {
      testKey = await generateSymmetricKey()
    })

    it('should encrypt and decrypt string', async () => {
      const cipher = await createSymmetricCipher(testKey)
      const plaintext = 'Hello, FluxEncrypt!'
      const ciphertext = await cipher.encrypt(plaintext)
      const decrypted = await cipher.decrypt(ciphertext)
      expect(decrypted).toBe(plaintext)
    })

    it('should encrypt and decrypt bytes', async () => {
      const cipher = await createSymmetricCipher(testKey)
      const plaintext = new Uint8Array([1, 2, 3, 4, 5])
      const ciphertext = await cipher.encryptBytes(plaintext)
      const decrypted = await cipher.decryptBytes(ciphertext)
      expect(decrypted).toEqual(plaintext)
    })

    it('should produce different ciphertext for same plaintext', async () => {
      const cipher = await createSymmetricCipher(testKey)
      const plaintext = 'test'
      const ct1 = await cipher.encrypt(plaintext)
      const ct2 = await cipher.encrypt(plaintext)
      expect(ct1).not.toBe(ct2) // Different IV each time
    })

    it('should work with convenience functions', async () => {
      const plaintext = 'secret message'
      const ciphertext = await encryptSecret(testKey, plaintext)
      const decrypted = await decryptSecret(testKey, ciphertext)
      expect(decrypted).toBe(plaintext)
    })

    it('should handle unicode', async () => {
      const cipher = await createSymmetricCipher(testKey)
      const plaintext = '你好世界! 🔐🔑'
      const ciphertext = await cipher.encrypt(plaintext)
      const decrypted = await cipher.decrypt(ciphertext)
      expect(decrypted).toBe(plaintext)
    })

    it('should handle empty string', async () => {
      const cipher = await createSymmetricCipher(testKey)
      const ciphertext = await cipher.encrypt('')
      const decrypted = await cipher.decrypt(ciphertext)
      expect(decrypted).toBe('')
    })

    it('should fail with wrong key', async () => {
      const cipher1 = await createSymmetricCipher(testKey)
      const wrongKey = await generateSymmetricKey()
      const cipher2 = await createSymmetricCipher(wrongKey)

      const ciphertext = await cipher1.encrypt('secret')
      await expect(cipher2.decrypt(ciphertext)).rejects.toThrow()
    })

    it('should support hex encoding', async () => {
      const cipher = await createSymmetricCipher(testKey, { encoding: 'hex' })
      const ciphertext = await cipher.encrypt('test')
      expect(/^[0-9a-f]+$/.test(ciphertext)).toBe(true)
      const decrypted = await cipher.decrypt(ciphertext)
      expect(decrypted).toBe('test')
    })
  })

  // =========================================================================
  // HYBRID ENCRYPTION TESTS
  // =========================================================================

  describe('Hybrid Encryption', () => {
    it('should encrypt and decrypt with hybrid cipher', async () => {
      const keypair = await generateKeyPair(2048)
      const cipher = createHybridCipher()

      const plaintext = 'Hello, Hybrid Encryption!'
      const ciphertext = await cipher.encrypt(keypair.publicKey, plaintext)
      const decrypted = await cipher.decryptToString(keypair.privateKey, ciphertext)

      expect(decrypted).toBe(plaintext)
    })

    it('should work with convenience functions', async () => {
      const keypair = await generateKeyPair(2048)
      const plaintext = 'secret data'

      const ciphertext = await hybridEncrypt(keypair.publicKey, plaintext)
      const decrypted = await hybridDecryptToString(keypair.privateKey, ciphertext)

      expect(decrypted).toBe(plaintext)
    })

    it('should handle binary data', async () => {
      const keypair = await generateKeyPair(2048)
      const plaintext = new Uint8Array([0, 1, 2, 255, 254, 253])

      const ciphertext = await hybridEncrypt(keypair.publicKey, plaintext)
      const decrypted = await hybridDecrypt(keypair.privateKey, ciphertext)

      expect(decrypted).toEqual(plaintext)
    })

    it('should fail with wrong private key', async () => {
      const keypair1 = await generateKeyPair(2048)
      const keypair2 = await generateKeyPair(2048)

      const ciphertext = await hybridEncrypt(keypair1.publicKey, 'secret')
      await expect(hybridDecrypt(keypair2.privateKey, ciphertext)).rejects.toThrow()
    })
  })

  // =========================================================================
  // CRYPTO MANAGER TESTS
  // =========================================================================

  describe('CryptoManager', () => {
    it('should create manager with default config', () => {
      const manager = createCryptoManager()
      const config = manager.getConfig()
      expect(config.encoding).toBe('base64')
      expect(config.rsaKeySize).toBe(4096)
      expect(config.pbkdf2Iterations).toBe(100000)
    })

    it('should create manager with custom config', () => {
      const manager = createCryptoManager({
        encoding: 'hex',
        rsaKeySize: 2048,
        pbkdf2Iterations: 50000,
      })
      const config = manager.getConfig()
      expect(config.encoding).toBe('hex')
      expect(config.rsaKeySize).toBe(2048)
      expect(config.pbkdf2Iterations).toBe(50000)
    })

    it('should work with cryptum() convenience function', () => {
      const manager = cryptum()
      expect(manager).toBeDefined()
      expect(manager.getConfig.bind(manager)).toBeDefined()
    })

    it('should perform symmetric encryption', async () => {
      const manager = createCryptoManager()
      const key = await manager.generateSymmetricKey()
      const plaintext = 'test secret'

      const ciphertext = await manager.encryptSecret(key, plaintext)
      const decrypted = await manager.decryptSecret(key, ciphertext)

      expect(decrypted).toBe(plaintext)
    })

    it('should perform hybrid encryption', async () => {
      const manager = createCryptoManager({ rsaKeySize: 2048 })
      const keypair = await manager.generateKeyPair()
      const plaintext = 'test data'

      const ciphertext = await manager.encrypt(keypair.publicKey, plaintext)
      const decrypted = await manager.decryptToString(keypair.privateKey, ciphertext)

      expect(decrypted).toBe(plaintext)
    })

    it('should export and import keys', async () => {
      const manager = createCryptoManager({ rsaKeySize: 2048 })
      const keypair = await manager.generateKeyPair()

      const publicPem = await manager.exportPublicKey(keypair.publicKey)
      const privatePem = await manager.exportPrivateKey(keypair.privateKey)

      const importedPublic = await manager.importPublicKey(publicPem)
      const importedPrivate = await manager.importPrivateKey(privatePem)

      // Verify imported keys work
      const ciphertext = await manager.encrypt(importedPublic, 'test')
      const decrypted = await manager.decryptToString(importedPrivate, ciphertext)
      expect(decrypted).toBe('test')
    })

    it('should derive key from password', async () => {
      const manager = createCryptoManager()
      const result = await manager.deriveKeyFromPassword('my-password')
      expect(result.key).toBeDefined()
      expect(result.salt).toBeDefined()
    })
  })
})
