/**
 * Shared test fixtures: Ed25519 key setup + in-memory port fakes (P3/P1 supply real impls).
 */
import { randomUUID } from 'node:crypto'
import type { HostRecord } from 'relay-contracts'
import {
  configureVerifyKey,
  resetVerifyKeyForTest,
} from '../src/config/keys.js'
import { generateEd25519KeyPair, exportEd25519PublicRaw } from '../src/crypto/ed25519.js'
import type {
  AuthenticatedPrincipal,
  AuditEvent,
  AuditSink,
  HostRegistryPort,
  SessionRegistryPort,
  RevocationStore,
  TokenBucketStore,
} from '../src/types.js'

export async function setupP5SigningKey(): Promise<{ signingKey: CryptoKey; publicRaw: Uint8Array }> {
  resetVerifyKeyForTest()
  const pair = await generateEd25519KeyPair()
  await configureVerifyKey(pair.publicKey)
  const publicRaw = await exportEd25519PublicRaw(pair.publicKey)
  return { signingKey: pair.privateKey, publicRaw }
}

export async function makeEphemeral(): Promise<{ privateKey: CryptoKey; publicRaw: Uint8Array }> {
  const pair = await generateEd25519KeyPair()
  return { privateKey: pair.privateKey, publicRaw: await exportEd25519PublicRaw(pair.publicKey) }
}

export function principal(accountId: string, overrides: Partial<AuthenticatedPrincipal> = {}): AuthenticatedPrincipal {
  return {
    kind: 'human',
    accountId,
    principalId: 'cred-' + accountId,
    amr: ['passkey'],
    authAt: 1000,
    stepUpAt: null,
    ...overrides,
  }
}

export function makeHost(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 const uuid = (): string => randomUUID()

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 function fakeSessionRegistry(
  sessions: readonly { sessionId: string; hostId: string; accountId: string }[],
): SessionRegistryPort {
  const map = new Map(sessions.map((s) => [s.sessionId, { hostId: s.hostId, accountId: s.accountId }]))
  return { getById: async (id) => map.get(id) ?? null }
}

export function fakeRevocationStore(): RevocationStore & { revoked: Set<string>; consumed: Set<string> } {
  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
    },
  }
}

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

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

/** True token bucket: per-key state initialized on first take, refilled by elapsed wall-time. */
export function fakeCountingBucket(): TokenBucketStore {
  const state = new Map<string, { tokens: number; last: number; burst: number; refill: number }>()
  return {
    take: async (key, refillPerSec, burst, now) => {
      let s = state.get(key)
      if (s === undefined) {
        s = { tokens: burst, last: now, burst, refill: refillPerSec }
        state.set(key, s)
      }
      const elapsed = Math.max(0, now - s.last)
      s.tokens = Math.min(s.burst, s.tokens + elapsed * s.refill)
      s.last = now
      if (s.tokens < 1) return false
      s.tokens -= 1
      return true
    },
  }
}

// ── Token + matching DPoP proof (for authz/enforce/tripwire tests) ──────────────────────────────
import { issueCapabilityToken } from '../src/capability/issue.js'
import { buildDpopProof, type DpopContext } from '../src/capability/verify.js'
import { jwkThumbprint } from '../src/crypto/thumbprint.js'
import type { CapabilityRight } from 'relay-contracts'

export interface IssuedBundle {
  readonly raw: string
  readonly dpop: DpopContext
}

export async function issueWithDpop(
  signingKey: CryptoKey,
  opts: {
    accountId: string
    host: string
    aud: string
    rights?: readonly CapabilityRight[]
    ttl?: number
    now: number
    htu?: string
    htm?: string
  },
): Promise<IssuedBundle> {
  const eph = await makeEphemeral()
  const cnfJkt = await jwkThumbprint(eph.publicRaw)
  const raw = await issueCapabilityToken(
    {
      principal: principal(opts.accountId),
      aud: opts.aud,
      host: opts.host,
      rights: opts.rights ?? ['attach'],
      ttlSeconds: opts.ttl ?? 45,
      cnfJkt,
    },
    signingKey,
    opts.now,
  )
  const htu = opts.htu ?? `https://${opts.aud}/ws`
  const htm = opts.htm ?? 'GET'
  const proofJws = await buildDpopProof(eph.privateKey, eph.publicRaw, {
    htu,
    htm,
    jti: uuid(),
    iat: opts.now,
  })
  return { raw, dpop: { proofJws, htu, htm } }
}
