/**
 * In-memory port fakes — the ONLY seams that stand in for the true I/O boundaries P1/P3 own
 * (host registry / session registry / revocation store / token buckets / audit sink / revocation
 * bus). Every security decision still runs through the REAL relay-auth exports; these fakes only
 * supply storage. Adapted from `relay-auth/test/_helpers.ts` (same shapes, bare import paths).
 */
import type { HostRecord, KillSignal, RevocationBus } from 'relay-contracts'
import type {
  AuthenticatedPrincipal,
  AuditEvent,
  AuditSink,
  HostRegistryPort,
  SessionRegistryPort,
  RevocationStore,
  TokenBucketStore,
} from 'relay-auth'

/** An authenticated principal (the ONLY source of accountId, INV3). */
export function principal(
  accountId: string,
  overrides: Partial<AuthenticatedPrincipal> = {},
): AuthenticatedPrincipal {
  return {
    kind: 'human',
    accountId,
    principalId: 'cred-' + accountId,
    amr: ['passkey'],
    authAt: 1000,
    stepUpAt: null,
    ...overrides,
  }
}

/** A minimal online HostRecord for the authz registry (agentPubkey here is unused by authz). */
export function makeHostRecord(
  accountId: string,
  hostId: string,
  status: HostRecord['status'] = 'online',
): HostRecord {
  return {
    hostId,
    accountId,
    subdomain: 'sub-' + hostId,
    agentPubkey: new Uint8Array(32),
    enrollFpr: 'fpr-' + hostId,
    status,
    lastSeen: '2026-01-01T00:00:00.000Z',
    createdAt: '2026-01-01T00:00:00.000Z',
    revokedAt: null,
  }
}

export function fakeHostRegistry(hosts: readonly HostRecord[]): HostRegistryPort {
  const map = new Map(hosts.map((h) => [h.hostId, h]))
  return { getById: async (id) => map.get(id) ?? null }
}

export interface MutableSessionRegistry extends SessionRegistryPort {
  add(entry: { sessionId: string; hostId: string; accountId: string }): void
}

export function fakeSessionRegistry(
  seed: readonly { sessionId: string; hostId: string; accountId: string }[] = [],
): MutableSessionRegistry {
  const map = new Map(seed.map((s) => [s.sessionId, { hostId: s.hostId, accountId: s.accountId }]))
  return {
    getById: async (id) => map.get(id) ?? null,
    add: (e) => {
      map.set(e.sessionId, { hostId: e.hostId, accountId: e.accountId })
    },
  }
}

export interface RevocationFake extends RevocationStore {
  readonly revoked: Set<string>
  readonly consumed: Set<string>
}

export function fakeRevocationStore(): RevocationFake {
  const revoked = new Set<string>()
  const consumed = new Set<string>()
  return {
    revoked,
    consumed,
    isRevoked: async (jti) => revoked.has(jti),
    revokeJti: async (jti) => {
      revoked.add(jti)
    },
    consumeOnce: async (jti) => {
      if (consumed.has(jti)) return false
      consumed.add(jti)
      return true
    },
  }
}

export interface TokenBucketFake extends TokenBucketStore {
  readonly blocked: Set<string>
  readonly calls: string[]
}

/** Token bucket that always allows unless a key is added to `blocked`. */
export function fakeTokenBucket(): TokenBucketFake {
  const blocked = new Set<string>()
  const calls: string[] = []
  return {
    blocked,
    calls,
    take: async (key) => {
      calls.push(key)
      return !blocked.has(key)
    },
  }
}

export interface AuditFake extends AuditSink {
  readonly events: AuditEvent[]
}

export function fakeAuditSink(): AuditFake {
  const events: AuditEvent[] = []
  return { events, append: async (e) => void events.push(e) }
}

export interface RevocationBusFake extends RevocationBus {
  readonly published: KillSignal[]
}

export function fakeRevocationBus(): RevocationBusFake {
  const published: KillSignal[] = []
  return { published, publish: async (s) => void published.push(s) }
}
