import { describe, expect, it } from 'vitest'
import {
  base64url,
  decryptSecret,
  encryptSecret,
  hmacSign,
  hmacVerify,
  sha256,
  timingSafeEqual,
} from './crypto'

/** 32-byte AES-256 key as standard base64. */
const TEST_KEY_B64 = btoa(String.fromCharCode(...new Uint8Array(32).fill(0x42)))

describe('timingSafeEqual', () => {
  it('returns true for equal strings', () => {
    expect(timingSafeEqual('abc', 'abc')).toBe(true)
  })

  it('returns false for unequal same-length strings', () => {
    expect(timingSafeEqual('abc', 'abd')).toBe(false)
  })

  it('returns false on length mismatch without throwing', () => {
    expect(timingSafeEqual('short', 'much-longer')).toBe(false)
  })
})

describe('sha256', () => {
  it('returns the known SHA-256 hex digest', async () => {
    await expect(sha256('hello')).resolves.toBe(
      '2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824',
    )
  })
})

describe('base64url', () => {
  it('maps +/ to -_ and strips padding', () => {
    const bytes = new Uint8Array([251, 255, 239, 190, 186, 222, 238, 254])
    const standard = btoa(String.fromCharCode(...bytes))
    expect(standard).toMatch(/[+/=]/)
    const encoded = base64url(bytes)
    expect(encoded).not.toMatch(/[+/=]/)
    expect(encoded).toBe(standard.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, ''))
  })
})

describe('encryptSecret / decryptSecret', () => {
  it('round-trips plaintext', async () => {
    const cipher = await encryptSecret('secret-value', TEST_KEY_B64)
    await expect(decryptSecret(cipher, TEST_KEY_B64)).resolves.toBe('secret-value')
  })

  it('rejects decryption with the wrong key', async () => {
    const cipher = await encryptSecret('secret-value', TEST_KEY_B64)
    const wrongKey = btoa(String.fromCharCode(...new Uint8Array(32).fill(0x99)))
    await expect(decryptSecret(cipher, wrongKey)).rejects.toThrow()
  })

  it('rejects a tampered ciphertext blob', async () => {
    const cipher = await encryptSecret('secret-value', TEST_KEY_B64)
    const raw = Uint8Array.from(atob(cipher), (c) => c.charCodeAt(0))
    const last = raw.length - 1
    raw[last] = (raw[last] ?? 0) ^ 0xff
    const tampered = btoa(String.fromCharCode(...raw))
    await expect(decryptSecret(tampered, TEST_KEY_B64)).rejects.toThrow()
  })
})

describe('hmacSign / hmacVerify', () => {
  it('verifies a signature it produced', async () => {
    const sig = await hmacSign('msg-1', 'secret-key')
    await expect(hmacVerify('msg-1', 'secret-key', sig)).resolves.toBe(true)
  })

  it('is deterministic for the same message + secret', async () => {
    const a = await hmacSign('msg-1', 'secret-key')
    const b = await hmacSign('msg-1', 'secret-key')
    expect(a).toBe(b)
  })

  it('returns a URL-safe base64 signature (no +/= chars)', async () => {
    const sig = await hmacSign('msg-1', 'secret-key')
    expect(sig).not.toMatch(/[+/=]/)
  })

  it('rejects a tampered message', async () => {
    const sig = await hmacSign('msg-1', 'secret-key')
    await expect(hmacVerify('msg-2', 'secret-key', sig)).resolves.toBe(false)
  })

  it('rejects verification under the wrong secret', async () => {
    const sig = await hmacSign('msg-1', 'secret-key')
    await expect(hmacVerify('msg-1', 'wrong-key', sig)).resolves.toBe(false)
  })

  it('rejects a forged/garbage signature', async () => {
    await expect(hmacVerify('msg-1', 'secret-key', 'not-a-real-sig')).resolves.toBe(false)
  })

  it('fail-closed: throws when signing under an empty secret', async () => {
    await expect(hmacSign('msg-1', '')).rejects.toThrow()
  })

  it('fail-closed: verify returns false (never throws) under an empty secret', async () => {
    const sig = await hmacSign('msg-1', 'secret-key')
    await expect(hmacVerify('msg-1', '', sig)).resolves.toBe(false)
  })
})
