import { describe, test, expect, vi } from 'vitest'
import { createAgentListener, type MtlsVerifier, type RouteRegistrar, type TlsServerOptions } from '../data-plane/agent-listener.js'
import { loadDataPlaneConfig, type DataPlaneConfig } from '../data-plane/config.js'
import type { WebSocketLike } from '../data-plane/ws-like.js'
import type { ScheduleHandle } from '../mux/heartbeat.js'

function cfg(): DataPlaneConfig {
  return loadDataPlaneConfig({
    BASE_DOMAIN: 'term.example.com',
    TLS_CERT_PATH: '/c',
    TLS_KEY_PATH: '/k',
    AGENT_CA_CERT_PATH: '/ca',
    RELAY_NODE_ID: 'node-1',
    HEARTBEAT_INTERVAL_MS: '1000',
    ROUTE_TTL_MS: '5000',
  })
}

function fakeWs() {
  const closeCbs: (() => void)[] = []
  return {
    send: vi.fn(),
    close: vi.fn(),
    onMessage: vi.fn(),
    onClose: (h: () => void) => closeCbs.push(h),
    emitClose: () => closeCbs.forEach((f) => f()),
  } satisfies WebSocketLike & { emitClose: () => void }
}

function goodMtls(): MtlsVerifier {
  return { verifyPeer: (cert) => (cert[0] === 1 ? { hostId: 'host-1', accountId: 'acct-1' } : null) }
}

function registrar(): RouteRegistrar {
  return { register: vi.fn(async () => {}), heartbeat: vi.fn(async () => {}), deregister: vi.fn(async () => {}) }
}

function fakeScheduler() {
  const fns: (() => void)[] = []
  const schedule = (fn: () => void): ScheduleHandle => {
    fns.push(fn)
    return { cancel: () => {} }
  }
  return { schedule, tick: () => fns.forEach((f) => f()) }
}

const flush = () => new Promise((r) => setTimeout(r, 0))

describe('agent tunnel listener (T9, INV4/INV7/INV14)', () => {
  test('valid peer cert → MuxSession created, route registered, tunnel indexed by hostId', async () => {
    const reg = registrar()
    const l = createAgentListener({ config: cfg(), mtls: goodMtls(), registrar: reg, caBundle: [], onTunnel: () => {}, schedule: fakeScheduler().schedule })
    l.attach(fakeWs(), new Uint8Array([1]))
    await flush()
    expect(reg.register).toHaveBeenCalledWith('host-1', 'node-1', 5000)
    expect(l.tunnels().get('host-1')?.hostId).toBe('host-1')
  })

  test('TLS-layer enforcement (Finding-4): server built with requestCert+rejectUnauthorized+ca', () => {
    let opts: TlsServerOptions | undefined
    const ca = [new Uint8Array([9, 9])]
    createAgentListener({
      config: cfg(),
      mtls: goodMtls(),
      registrar: registrar(),
      caBundle: ca,
      onTunnel: () => {},
      tlsServerFactory: (o) => {
        opts = o
        return { close: () => {} }
      },
    })
    expect(opts).toEqual({ requestCert: true, rejectUnauthorized: true, ca })
  })

  test('invalid cert (verifyPeer null) → ws closed, NO route registered (INV14)', async () => {
    const reg = registrar()
    const ws = fakeWs()
    const l = createAgentListener({ config: cfg(), mtls: goodMtls(), registrar: reg, caBundle: [], onTunnel: () => {}, schedule: fakeScheduler().schedule })
    l.attach(ws, new Uint8Array([0])) // cert[0] !== 1 → verifyPeer null
    await flush()
    expect(ws.close).toHaveBeenCalled()
    expect(reg.register).not.toHaveBeenCalled()
    expect(l.tunnels().size).toBe(0)
  })

  test('heartbeat: registrar.heartbeat runs on the interval; deregister on ws close', async () => {
    const reg = registrar()
    const sched = fakeScheduler()
    const ws = fakeWs()
    const l = createAgentListener({ config: cfg(), mtls: goodMtls(), registrar: reg, caBundle: [], onTunnel: () => {}, schedule: sched.schedule })
    l.attach(ws, new Uint8Array([1]))
    sched.tick()
    await flush()
    expect(reg.heartbeat).toHaveBeenCalledWith('host-1')
    ws.emitClose()
    await flush()
    expect(reg.deregister).toHaveBeenCalledWith('host-1')
    expect(l.tunnels().size).toBe(0)
  })

  test('closeTunnel tears down + deregisters (revocation lever, INV12)', async () => {
    const reg = registrar()
    const l = createAgentListener({ config: cfg(), mtls: goodMtls(), registrar: reg, caBundle: [], onTunnel: () => {}, schedule: fakeScheduler().schedule })
    l.attach(fakeWs(), new Uint8Array([1]))
    l.tunnels().get('host-1')!.closeTunnel()
    await flush()
    expect(reg.deregister).toHaveBeenCalledWith('host-1')
    expect(l.tunnels().size).toBe(0)
  })

  test('reconnection: a second attach for the same hostId replaces the prior tunnel', async () => {
    const reg = registrar()
    const l = createAgentListener({ config: cfg(), mtls: goodMtls(), registrar: reg, caBundle: [], onTunnel: () => {}, schedule: fakeScheduler().schedule })
    l.attach(fakeWs(), new Uint8Array([1]))
    const first = l.tunnels().get('host-1')
    l.attach(fakeWs(), new Uint8Array([1]))
    const second = l.tunnels().get('host-1')
    await flush()
    expect(second).not.toBe(first)
    expect(reg.deregister).toHaveBeenCalledWith('host-1') // old one deregistered
    expect(reg.register).toHaveBeenCalledTimes(2)
    expect(l.tunnels().size).toBe(1)
  })
})
