import { describe, expect, it } from 'vitest'
import { decodeMuxFrame } from 'relay-contracts'
import { holdTunnel } from '../src/transport/tunnel.js'
import { createHeartbeat, HEARTBEAT_INTERVAL_MS } from '../src/transport/heartbeat.js'
import { FakeWs, FakeTimer } from './fixtures/fakes.js'

describe('Heartbeat (T9, §4.1)', () => {
  it('replies PONG echoing the inbound PING token byte-exact', () => {
    const ws = new FakeWs()
    const tunnel = holdTunnel(ws)
    const hb = createHeartbeat(tunnel, { timer: new FakeTimer() })
    const token = new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8])
    hb.onPing(token)
    const frame = decodeMuxFrame(ws.sent.at(-1)!)
    expect(frame.header.type).toBe('pong')
    expect(Buffer.from(frame.payload).equals(Buffer.from(token))).toBe(true)
  })

  it('fires onDead when no PONG arrives within the interval', () => {
    const ws = new FakeWs()
    const tunnel = holdTunnel(ws)
    const timer = new FakeTimer()
    const hb = createHeartbeat(tunnel, { timer, intervalMs: 1000 })
    let dead = false
    hb.onDead(() => {
      dead = true
    })
    hb.start() // sends first ping, arms deadline
    timer.advance(1000) // deadline fires, no pong seen
    expect(dead).toBe(true)
  })

  it('does not fire onDead when PONG arrives in time', () => {
    const ws = new FakeWs()
    const tunnel = holdTunnel(ws)
    const timer = new FakeTimer()
    const hb = createHeartbeat(tunnel, { timer, intervalMs: 1000, genToken: () => new Uint8Array(8) })
    let dead = false
    hb.onDead(() => {
      dead = true
    })
    hb.start()
    hb.onPong(new Uint8Array(8)) // clears pending before the deadline
    timer.advance(1000)
    expect(dead).toBe(false)
  })

  it('exposes the frozen 15s interval', () => {
    expect(HEARTBEAT_INTERVAL_MS).toBe(15_000)
  })

  it('stop() cancels timers (no leak)', () => {
    const ws = new FakeWs()
    const hb = createHeartbeat(holdTunnel(ws), { timer: new FakeTimer(), intervalMs: 1000 })
    hb.start()
    expect(() => hb.stop()).not.toThrow()
  })
})
