import { describe, expect, it } from 'vitest'
import { readFileSync } from 'node:fs'
import { join } from 'node:path'
import { GOAWAY_REASON_TO_CODE } from 'relay-contracts'
import {
  applyGoAway,
  applyGoAwayCode,
  classifyGoAway,
  createRevocationState,
} from '../src/lifecycle/revocation.js'

describe('classifyGoAway (T14, frozen 3-value union)', () => {
  it('maps operatorDrain and shutdown → drain, revoked → revoked', () => {
    expect(classifyGoAway('operatorDrain')).toBe('drain')
    expect(classifyGoAway('shutdown')).toBe('drain')
    expect(classifyGoAway('revoked')).toBe('revoked')
  })
})

describe('createRevocationState (T14, INV12)', () => {
  it('revoke tears down once and makes isRevoked() true (suppresses reconnect)', () => {
    let teardowns = 0
    const state = createRevocationState(() => {
      teardowns += 1
    })
    expect(state.isRevoked()).toBe(false)
    state.markRevoked('goaway-revoked')
    expect(state.isRevoked()).toBe(true)
    state.markRevoked('operator') // idempotent
    expect(teardowns).toBe(1)
  })
})

describe('applyGoAway / applyGoAwayCode (T14)', () => {
  it('a revoked GOAWAY tears down; a drain GOAWAY does not', () => {
    const revokeState = createRevocationState(() => {})
    expect(applyGoAway('revoked', revokeState)).toBe('revoked')
    expect(revokeState.isRevoked()).toBe(true)

    const drainState = createRevocationState(() => {})
    expect(applyGoAway('operatorDrain', drainState)).toBe('drain')
    expect(applyGoAway('shutdown', drainState)).toBe('drain')
    expect(drainState.isRevoked()).toBe(false)
  })

  it('decodes the frozen wire codes via decodeGoAwayReason', () => {
    const state = createRevocationState(() => {})
    expect(applyGoAwayCode(GOAWAY_REASON_TO_CODE.operatorDrain, state)).toBe('drain')
    expect(applyGoAwayCode(GOAWAY_REASON_TO_CODE.revoked, state)).toBe('revoked')
  })

  it('an unknown reason code FAILS CLOSED to revoked (INV12 safety)', () => {
    const state = createRevocationState(() => {})
    expect(applyGoAwayCode(99, state)).toBe('revoked')
    expect(state.isRevoked()).toBe(true)
  })

  it('cross-plan-drift guard: no bare integer reason literal in revocation.ts', () => {
    const src = readFileSync(
      join(import.meta.dirname, '..', 'src', 'lifecycle', 'revocation.ts'),
      'utf8',
    )
    // The reason mapping must come only from the frozen union/decoder — never a hardcoded
    // `=== 2` / `reason: 2` style integer. (Comments are allowed to mention numbers.)
    const code = src.replace(/\/\*[\s\S]*?\*\//g, '').replace(/\/\/.*$/gm, '')
    expect(code).not.toMatch(/reason\s*[=:]\s*\d/)
    expect(code).not.toMatch(/===\s*[123]\b/)
  })
})
