import { describe, expect, it } from 'vitest'
import { base32Decode, base32Encode, findRecoveryCode, generateRecoveryCodes, generateTotpSecret, hashRecoveryCode, otpauthUri, totpCode, verifyTotp } from '@/lib/security/totp'
import { decryptSecret, encryptSecret } from '@/lib/security/crypto'

/** MFA primitives (2026-09-22): RFC 6238 vectors, drift window, recovery codes, secret encryption. */
describe('TOTP', () => {
  // RFC 6238 test secret "12345678901234567890" (SHA-1, 6 digits) → base32 GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ
  const SECRET = 'GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ'
  it('matches the RFC 6238 reference vectors', () => {
    expect(totpCode(SECRET, 59_000)).toBe('287082')
    expect(totpCode(SECRET, 1_111_111_109_000)).toBe('081804')
    expect(totpCode(SECRET, 1_234_567_890_000)).toBe('005924')
  })
  it('accepts one step of drift either side, rejects two', () => {
    const t = 1_234_567_890_000
    expect(verifyTotp(SECRET, totpCode(SECRET, t - 30_000), t)).toBe(true)
    expect(verifyTotp(SECRET, totpCode(SECRET, t + 30_000), t)).toBe(true)
    expect(verifyTotp(SECRET, totpCode(SECRET, t + 90_000), t)).toBe(false)
    expect(verifyTotp(SECRET, 'abc', t)).toBe(false)
  })
  it('base32 round-trips and secrets are 160-bit', () => {
    expect(base32Encode(base32Decode(SECRET))).toBe(SECRET)
    expect(base32Decode(generateTotpSecret()).length).toBe(20)
  })
  it('otpauth URI carries issuer, account and parameters', () => {
    const uri = otpauthUri({ issuer: 'HeavyHaul Agent', account: 'nash@example.com', secret: SECRET })
    expect(uri).toMatch(/^otpauth:\/\/totp\/HeavyHaul%20Agent%3Anash%40example\.com\?secret=GEZ/)
    expect(uri).toContain('issuer=HeavyHaul%20Agent')
    expect(uri).toContain('digits=6&period=30')
  })
})

describe('recovery codes', () => {
  it('eight single-use codes, matched by hash regardless of dashes or case', () => {
    const codes = generateRecoveryCodes()
    expect(codes).toHaveLength(8)
    const hashes = codes.map(hashRecoveryCode)
    expect(findRecoveryCode(codes[3].toUpperCase().replace(/-/g, ' '), hashes)).toBe(3)
    expect(findRecoveryCode('nope-nope-nope', hashes)).toBe(-1)
  })
})

describe('secret encryption', () => {
  it('round-trips under AUTH_SECRET and refuses tampering', () => {
    process.env.AUTH_SECRET = 'test-secret-at-least-16-chars'
    const packed = encryptSecret(SECRET_PLAIN)
    expect(packed.startsWith('v1.')).toBe(true)
    expect(decryptSecret(packed)).toBe(SECRET_PLAIN)
    const parts = packed.split('.')
    parts[3] = Buffer.from('tampered').toString('base64')
    expect(() => decryptSecret(parts.join('.'))).toThrow()
  })
})
const SECRET_PLAIN = 'GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ'
