import { describe, test, expect } from 'vitest'
import { createMemoryStores } from '../src/store/memory.js'
import { createHostRegistry } from '../src/registry/hosts.js'
import { createRoutingTable } from '../src/routing/table.js'
import { createInMemoryRevocationBus } from '../src/routing/bus.js'
import { createRevoker, createInMemoryRevokedTokenStore } from '../src/revoke/revoke.js'
import { fingerprint } from '../src/ca/fingerprint.js'
import { generateEd25519 } from '../src/util/crypto.js'
import type { KillSignal } from 'relay-contracts'
import type { NodeIdentity } from '../src/node-auth/identity.js'

const node: NodeIdentity = { nodeId: 'spiffe://relay/node-a' }
const ACCOUNT = '11111111-1111-4111-8111-111111111111'

async function harness() {
  const stores = createMemoryStores()
  const hosts = createHostRegistry({ hosts: stores.hosts })
  const routing = createRoutingTable({ routes: stores.routes })
  const bus = createInMemoryRevocationBus()
  const tokens = createInMemoryRevokedTokenStore()
  const revoker = createRevoker({ hosts, routing, bus, tokens })
  const { publicKeyRaw } = generateEd25519()
  const host = await hosts.bindHost({ accountId: ACCOUNT, subdomain: 'alice', agentPubkey: publicKeyRaw, enrollFpr: fingerprint(publicKeyRaw) })
  await routing.upsertRoute(node, host.hostId, { relayNodeId: node.nodeId, updatedAt: new Date().toISOString() }, 60)
  return { stores, hosts, routing, bus, tokens, revoker, host }
}

describe('T13 revocation (INV12/INV8/INV10)', () => {
  test('revokeHost: route dropped + host revoked + KillSignal on relay:revocations, zero payload', async () => {
    const h = await harness()
    const received: KillSignal[] = []
    h.bus.subscribe((s) => received.push(s))
    await h.revoker.revokeHost(h.host.hostId)
    expect(await h.routing.resolveRoute(h.host.hostId)).toBeNull()
    expect((await h.hosts.getHost(h.host.hostId))?.status).toBe('revoked')
    expect(received.length).toBe(1)
    expect(received[0]?.scope).toEqual({ kind: 'host', hostId: h.host.hostId })
    // zero-payload: reason is short metadata, contains no terminal bytes
    expect(received[0]?.reason).toBe('revoked')
  })

  test('revokeHost is idempotent', async () => {
    const h = await harness()
    await h.revoker.revokeHost(h.host.hostId)
    await expect(h.revoker.revokeHost(h.host.hostId)).resolves.toBeUndefined()
  })

  test('revokeAccount publishes ONE account-scoped signal + cascades to hosts', async () => {
    const h = await harness()
    const received: KillSignal[] = []
    h.bus.subscribe((s) => received.push(s))
    await h.revoker.revokeAccount(ACCOUNT)
    expect((await h.hosts.getHost(h.host.hostId))?.status).toBe('revoked')
    const accountSignals = received.filter((s) => s.scope.kind === 'account')
    expect(accountSignals.length).toBe(1)
  })

  test('revokeToken → isTokenRevoked true, stops validating', async () => {
    const h = await harness()
    const jti = 'jti-123'
    expect(await h.revoker.isTokenRevoked(jti)).toBe(false)
    await h.revoker.revokeToken(jti, Math.floor(Date.now() / 1000) + 3600)
    expect(await h.revoker.isTokenRevoked(jti)).toBe(true)
  })
})
