import { describe, expect, it } from 'vitest'
import type { MailMessage } from '@platform-modules/mail'
import { createOtpEmail, OtpAttemptsExceededError, OtpExpiredError, type OtpRecord } from './otp-email.js'

function memoryStore() {
  const map = new Map<string, OtpRecord>()
  return {
    async get(key: string) {
      return map.get(key) ?? null
    },
    async set(key: string, record: OtpRecord) {
      map.set(key, record)
    },
    async delete(key: string) {
      map.delete(key)
    },
    _map: map,
  }
}

describe('createOtpEmail', () => {
  it('verifies a code once within TTL', async () => {
    const store = memoryStore()
    let captured: MailMessage | undefined
    const otp = createOtpEmail({
      mail: {
        async send(msg) {
          captured = structuredClone(msg)
          return { id: '1', provider: 'fake' }
        },
      },
      store,
      from: 'noreply@example.com',
      ttlMs: 60_000,
    })

    await otp.issueAndSend('user@example.com', {
      subject: 'Code',
      text: 'Your code is {{code}}',
    })
    const match = captured?.text?.match(/\d{6}/)
    expect(match).toBeTruthy()
    await expect(otp.verify('user@example.com', match![0]!)).resolves.toBe(true)
    await expect(otp.verify('user@example.com', match![0]!)).resolves.toBe(false)
  })

  it('rejects expired codes', async () => {
    const store = memoryStore()
    const otp = createOtpEmail({
      mail: { async send() { return { id: '1', provider: 'fake' } } },
      store,
      from: 'noreply@example.com',
      ttlMs: 1,
    })
    await otp.issueAndSend('exp@example.com', { subject: 'x', text: '{{code}}' })
    const record = await store.get('exp@example.com')
    if (record) record.expiresAt = Date.now() - 1
    await store.set('exp@example.com', record!)
    const code = '000000'
    await expect(otp.verify('exp@example.com', code)).rejects.toBeInstanceOf(OtpExpiredError)
  })

  it('rejects after attempt cap', async () => {
    const store = memoryStore()
    const otp = createOtpEmail({
      mail: { async send() { return { id: '1', provider: 'fake' } } },
      store,
      from: 'noreply@example.com',
      maxAttempts: 2,
    })
    await otp.issueAndSend('cap@example.com', { subject: 'x', text: '{{code}}' })
    await expect(otp.verify('cap@example.com', '000000')).resolves.toBe(false)
    await expect(otp.verify('cap@example.com', '000001')).resolves.toBe(false)
    await expect(otp.verify('cap@example.com', '000002')).rejects.toBeInstanceOf(
      OtpAttemptsExceededError,
    )
  })

  it('charges the attempt BEFORE comparing — charge failure blocks even a correct code (fail-closed)', async () => {
    const store = memoryStore()
    let captured: MailMessage | undefined
    const otp = createOtpEmail({
      mail: {
        async send(msg) {
          captured = structuredClone(msg)
          return { id: '1', provider: 'fake' }
        },
      },
      store,
      from: 'noreply@example.com',
    })
    await otp.issueAndSend('pre@example.com', { subject: 'x', text: '{{code}}' })
    const code = captured?.text?.match(/\d{6}/)![0]!

    store.set = async () => {
      throw new Error('kv down')
    }
    await expect(otp.verify('pre@example.com', code)).rejects.toThrow('kv down')
  })

  it('a wrong guess persists the attempt count', async () => {
    const store = memoryStore()
    const otp = createOtpEmail({
      mail: { async send() { return { id: '1', provider: 'fake' } } },
      store,
      from: 'noreply@example.com',
    })
    await otp.issueAndSend('count@example.com', { subject: 'x', text: '{{code}}' })
    await expect(otp.verify('count@example.com', '999999')).resolves.toBe(false)
    expect(store._map.get('count@example.com')?.attempts).toBe(1)
  })
})
