import { describe, expect, it, vi } from 'vitest'
import { createBackoff, reconnectLoop, BACKOFF_CAP_MS } from '../src/transport/backoff.js'
import type { Tunnel } from '../src/transport/tunnel.js'

describe('createBackoff (T10)', () => {
  it('follows 1s,2s,4s…cap 30s', () => {
    const b = createBackoff()
    const seq = [b.nextDelayMs(), b.nextDelayMs(), b.nextDelayMs(), b.nextDelayMs(), b.nextDelayMs(), b.nextDelayMs(), b.nextDelayMs()]
    expect(seq).toEqual([1000, 2000, 4000, 8000, 16000, 30000, 30000])
    expect(BACKOFF_CAP_MS).toBe(30_000)
  })

  it('reset() returns to 1s', () => {
    const b = createBackoff()
    b.nextDelayMs()
    b.nextDelayMs()
    b.reset()
    expect(b.nextDelayMs()).toBe(1000)
  })

  it('jitter stays within [0.5×, 1×]', () => {
    const b = createBackoff({ jitter: true, rng: () => 0 })
    expect(b.nextDelayMs()).toBe(500) // 1000 * 0.5
    const b2 = createBackoff({ jitter: true, rng: () => 1 })
    expect(b2.nextDelayMs()).toBe(1000) // 1000 * 1.0
  })
})

describe('reconnectLoop (T10, INV12)', () => {
  const fakeTunnel = {} as Tunnel

  it('resolves on the first successful dial and resets backoff', async () => {
    const dial = vi.fn().mockRejectedValueOnce(new Error('down')).mockResolvedValueOnce(fakeTunnel)
    const backoff = createBackoff()
    const reset = vi.spyOn(backoff, 'reset')
    const sleeps: number[] = []
    const result = await reconnectLoop(dial, backoff, () => false, async (ms) => {
      sleeps.push(ms)
    })
    expect(result).toBe(fakeTunnel)
    expect(sleeps).toEqual([1000])
    expect(reset).toHaveBeenCalled()
  })

  it('a revoked host does not reconnect (INV12)', async () => {
    const dial = vi.fn().mockResolvedValue(fakeTunnel)
    const result = await reconnectLoop(dial, createBackoff(), () => true, async () => {})
    expect(dial).not.toHaveBeenCalled()
    expect(result).toBeNull()
  })

  it('stops retrying once revoked mid-loop', async () => {
    let revoked = false
    const dial = vi.fn(async () => {
      revoked = true
      throw new Error('down')
    })
    const result = await reconnectLoop(dial, createBackoff(), () => revoked, async () => {})
    expect(result).toBeNull()
    expect(dial).toHaveBeenCalledTimes(1)
  })
})
