UNPKG

@beignet/core

Version:

Core framework primitives for Beignet

2,469 lines 64.8 kB
import type {
  MemoryIdempotencyEntry,
  MemoryIdempotencyStore,
} from "../idempotency/index.js";
import type {
  MemoryMailDelivery,
  NormalizedMailMessage,
} from "../mail/index.js";
import type { MemoryNotificationDelivery } from "../notifications/index.js";
import type {
  DrainOutboxResult,
  OutboxErrorInfo,
  OutboxMessage,
  OutboxMessageKind,
  OutboxMessageStatus,
} from "../outbox/index.js";
import type {
  ProviderInstrumentationEventInput,
  ProviderInstrumentationPort,
} from "../providers/index.js";
import type {
  InferSchedulePayload,
  ScheduleDef,
  ScheduleRunnerPort,
  ScheduleRunOptions,
} from "../schedules/index.js";
import {
  type ActivityActor,
  type ActivityMetadata,
  type ActivityMetadataValue,
  type ActivityResource,
  type ActivityTenant,
  type AuditLogEntry,
  type AuditOutcome,
  createAnonymousActor,
  createServiceActor,
  createSystemActor,
  createTenant,
  createUserActor,
} from "./audit.js";
import type { EventBusPort, JobDef, JobDispatcherPort } from "./events.js";
import {
  type CreateGateOptions,
  createGate,
  type GateDecision,
  type GatePort,
  type PolicyContextFromDefinitions,
  type PolicyDefinition,
  type PolicyMapFromDefinitions,
  type PolicySubjectArgs,
} from "./policy.js";
import type {
  StorageObject,
  StoragePort,
  StorageVisibility,
} from "./storage.js";

/**
 * A recorded event entry from the recording event bus.
 */
export interface RecordedEvent {
  name: string;
  payload: unknown;
}

/**
 * Expected fields for a recorded event assertion.
 */
export interface RecordedEventExpectation {
  /**
   * Expected event name.
   */
  name?: string;
  /**
   * Expected event payload. Object values are matched as partial objects.
   */
  payload?: unknown;
}

/**
 * Create a recording event bus for testing.
 *
 * This bus records all published events for later assertion,
 * but does not support subscription (throws if called).
 *
 * @example
 * ```ts
 * const { bus, events } = createRecordingEventBus();
 *
 * // Inject bus into your use case
 * await createUser({ ports: { eventBus: bus } });
 *
 * // Assert on recorded events
 * expect(events).toHaveLength(1);
 * expect(events[0].name).toBe("user.registered");
 * expect(events[0].payload).toEqual({ userId: "123", email: "test@example.com" });
 * ```
 */
export function createRecordingEventBus(): {
  bus: EventBusPort;
  events: RecordedEvent[];
} {
  const events: RecordedEvent[] = [];

  const bus: EventBusPort = {
    publish(event, payload) {
      events.push({ name: event.name, payload });
    },
    subscribe() {
      throw new Error("Not implemented for recording bus");
    },
  };

  return { bus, events };
}

/**
 * A job dispatch captured by `createRecordingJobDispatcher(...)`.
 */
export interface RecordedJobDispatch {
  /**
   * Dispatched job name.
   */
  name: string;
  /**
   * Job definition supplied to the dispatcher.
   */
  job: JobDef;
  /**
   * Payload supplied to the dispatcher.
   */
  payload: unknown;
}

/**
 * Expected fields for a recorded job dispatch assertion.
 */
export interface RecordedJobDispatchExpectation {
  /**
   * Expected job name.
   */
  name?: string;
  /**
   * Expected job payload. Object values are matched as partial objects.
   */
  payload?: unknown;
}

/**
 * Create a recording job dispatcher for tests.
 *
 * The dispatcher records dispatch intent without running the job handler. Use
 * this when a use case or listener should enqueue work but the test does not
 * need to execute that work inline.
 *
 * @returns A job dispatcher plus its captured dispatches.
 */
export function createRecordingJobDispatcher(): {
  jobs: JobDispatcherPort;
  dispatchedJobs: RecordedJobDispatch[];
} {
  const dispatchedJobs: RecordedJobDispatch[] = [];

  const jobs: JobDispatcherPort = {
    dispatch(job, payload) {
      dispatchedJobs.push({ name: job.name, job, payload });
    },
  };

  return { jobs, dispatchedJobs };
}

/**
 * A schedule run captured by `createRecordingScheduleRunner(...)`.
 */
export interface RecordedScheduleRun {
  /**
   * Schedule name.
   */
  name: string;
  /**
   * Schedule definition supplied to the runner.
   */
  schedule: ScheduleDef;
  /**
   * Payload supplied to the runner, when present.
   */
  payload?: unknown;
  /**
   * Run ID supplied by the provider or test, when present.
   */
  id?: string;
  /**
   * Provider or app source label, when present.
   */
  source?: string;
  /**
   * Scheduled timestamp supplied to the runner, when present.
   */
  scheduledAt?: ScheduleRunOptions["scheduledAt"];
  /**
   * Triggered timestamp supplied to the runner, when present.
   */
  triggeredAt?: ScheduleRunOptions["triggeredAt"];
}

/**
 * Expected fields for a recorded schedule run assertion.
 */
export interface RecordedScheduleRunExpectation {
  /**
   * Expected schedule name.
   */
  name?: string;
  /**
   * Expected schedule payload. Object values are matched as partial objects.
   */
  payload?: unknown;
  /**
   * Expected run ID.
   */
  id?: string;
  /**
   * Expected source label.
   */
  source?: string;
}

/**
 * Expected fields for a provider instrumentation event assertion.
 */
export interface ProviderInstrumentationEventExpectation {
  /**
   * Expected instrumentation event type.
   */
  type?: ProviderInstrumentationEventInput["type"];
  /**
   * Expected event ID.
   */
  id?: string;
  /**
   * Expected ISO timestamp.
   */
  timestamp?: string;
  /**
   * Expected request correlation ID.
   */
  requestId?: string;
  /**
   * Expected trace ID.
   */
  traceId?: string;
  /**
   * Expected span ID.
   */
  spanId?: string;
  /**
   * Expected parent span ID.
   */
  parentSpanId?: string;
  /**
   * Expected traceparent header value.
   */
  traceparent?: string;
  /**
   * Expected watcher name.
   */
  watcher?: string;
  /**
   * Expected provider name. Matches `providerName` on provider lifecycle events
   * and `details.providerName` on provider instrumentation events.
   */
  providerName?: string;
  /**
   * Expected structured details. Object values are matched as partial objects.
   */
  details?: unknown;
  /**
   * Expected request method.
   */
  method?: string;
  /**
   * Expected request path.
   */
  path?: string;
  /**
   * Expected contract name.
   */
  contractName?: string;
  /**
   * Expected status. This matches request status codes as well as job, outbox,
   * and schedule status strings.
   */
  status?: unknown;
  /**
   * Expected duration in milliseconds.
   */
  durationMs?: number;
  /**
   * Expected human-readable summary.
   */
  summary?: string;
  /**
   * Expected error message.
   */
  message?: string;
  /**
   * Expected stack trace.
   */
  stack?: string;
  /**
   * Expected use-case name on error events.
   */
  useCaseName?: string;
  /**
   * Expected use-case or custom event name.
   */
  name?: string;
  /**
   * Expected use-case kind.
   */
  kind?: "command" | "query";
  /**
   * Expected use-case phase.
   */
  phase?: "start" | "end" | "error";
  /**
   * Expected error summary.
   */
  error?: string;
  /**
   * Expected event bus event name.
   */
  eventName?: string;
  /**
   * Expected job name.
   */
  jobName?: string;
  /**
   * Expected outbox message ID.
   */
  messageId?: string;
  /**
   * Expected outbox message kind.
   */
  messageKind?: "event" | "job";
  /**
   * Expected outbox message name.
   */
  messageName?: string;
  /**
   * Expected schedule name.
   */
  scheduleName?: string;
  /**
   * Expected schedule cron expression.
   */
  cron?: string;
  /**
   * Expected schedule time zone.
   */
  timezone?: string;
  /**
   * Expected provider lifecycle action.
   */
  action?: "setup" | "start" | "stop";
  /**
   * Expected custom event label.
   */
  label?: string;
}

/**
 * Source accepted by provider instrumentation assertion helpers.
 */
export type ProviderInstrumentationAssertionSource =
  | readonly ProviderInstrumentationEventInput[]
  | {
      /**
       * Recorded provider instrumentation events.
       */
      readonly events: readonly ProviderInstrumentationEventInput[];
    };

/**
 * Create a recording schedule runner for tests.
 *
 * The runner records schedule run intent without executing the schedule
 * handler. Use `createInlineScheduleRunner(...)` when the test should run the
 * handler.
 *
 * @returns A schedule runner plus its captured runs.
 */
export function createRecordingScheduleRunner(): {
  runner: ScheduleRunnerPort;
  runs: RecordedScheduleRun[];
} {
  const runs: RecordedScheduleRun[] = [];

  const runner: ScheduleRunnerPort = {
    async run<S extends ScheduleDef>(
      schedule: S,
      options: ScheduleRunOptions<InferSchedulePayload<S>> = {},
    ) {
      runs.push({
        name: schedule.name,
        schedule,
        ...(Object.hasOwn(options, "payload")
          ? { payload: options.payload }
          : {}),
        ...(options.id !== undefined ? { id: options.id } : {}),
        ...(options.source !== undefined ? { source: options.source } : {}),
        ...(options.scheduledAt !== undefined
          ? { scheduledAt: options.scheduledAt }
          : {}),
        ...(options.triggeredAt !== undefined
          ? { triggeredAt: options.triggeredAt }
          : {}),
      });
    },
  };

  return { runner, runs };
}

/**
 * Create a provider instrumentation port for tests.
 *
 * The port records every event and can optionally disable specific watchers.
 * Use the returned `events` array with provider instrumentation assertion
 * helpers.
 *
 * @returns A provider instrumentation port plus its captured events.
 */
export function createRecordingProviderInstrumentation(
  options: {
    enabledWatchers?: readonly string[];
    disabledWatchers?: readonly string[];
  } = {},
): {
  instrumentation: ProviderInstrumentationPort;
  events: ProviderInstrumentationEventInput[];
} {
  const events: ProviderInstrumentationEventInput[] = [];
  const enabledWatchers = options.enabledWatchers
    ? new Set(options.enabledWatchers)
    : null;
  const disabledWatchers = new Set(options.disabledWatchers ?? []);

  const instrumentation: ProviderInstrumentationPort = {
    record(event) {
      events.push(event);
    },
    isWatcherEnabled(name) {
      if (disabledWatchers.has(name)) return false;
      return enabledWatchers ? enabledWatchers.has(name) : true;
    },
  };

  return { instrumentation, events };
}

/**
 * Options for creating a test user actor.
 */
export interface CreateTestUserActorOptions
  extends Omit<ActivityActor, "type" | "id" | "metadata"> {
  /**
   * Optional role stored as `actor.metadata.role`.
   */
  role?: string;
  /**
   * Additional redaction-safe actor metadata.
   */
  metadata?: ActivityMetadata;
}

/**
 * Options for creating a test actor that represents impersonated user access.
 */
export interface CreateTestImpersonatedUserActorOptions
  extends CreateTestUserActorOptions {
  /**
   * Stable ID for the actor performing the impersonation.
   */
  impersonatorId: string;
}

/**
 * Options for creating a test tenant.
 */
export type CreateTestTenantOptions = Omit<ActivityTenant, "id">;

/**
 * Context fields commonly shared by Beignet tests that exercise audit,
 * authorization, route hooks, and use cases.
 */
export interface TestActivityContext {
  /**
   * Actor under test.
   */
  actor: ActivityActor;
  /**
   * Tenant/account/workspace scope under test.
   */
  tenant?: ActivityTenant;
  /**
   * Stable request ID for assertions.
   */
  requestId: string;
  /**
   * Optional trace ID for assertions.
   */
  traceId?: string;
}

/**
 * Options for creating a test activity context.
 */
export interface CreateTestActivityContextOptions {
  /**
   * Actor under test.
   *
   * @default createTestUserActor()
   */
  actor?: ActivityActor;
  /**
   * Tenant under test. Pass `null` to omit tenant context.
   *
   * @default createTestTenant()
   */
  tenant?: ActivityTenant | null;
  /**
   * Request ID to expose on the context.
   *
   * @default "test-request"
   */
  requestId?: string;
  /**
   * Trace ID to expose on the context.
   *
   * @default "test-trace"
   */
  traceId?: string;
}

/**
 * Create a predictable user actor for tests.
 *
 * Use this when authorization or audit assertions need a stable actor shape
 * without repeating the `ActivityActor` object in every test.
 *
 * @param id - Stable user ID for the test actor.
 * @param options - Optional display name, role, and metadata.
 * @returns A user actor with `type: "user"`.
 */
export function createTestUserActor(
  id = "user_test",
  options: CreateTestUserActorOptions = {},
): ActivityActor {
  const { role, metadata, ...actorOptions } = options;
  const mergedMetadata = mergeRoleMetadata(metadata, role);

  return createUserActor(id, {
    ...actorOptions,
    ...(mergedMetadata ? { metadata: mergedMetadata } : {}),
  });
}

/**
 * Create a predictable user actor for tests that exercise impersonation.
 *
 * The returned actor remains the effective user, with `metadata.impersonatorId`
 * recording who initiated the impersonated access.
 *
 * @param id - Stable user ID being impersonated.
 * @param options - Impersonator ID plus optional display name, role, and metadata.
 * @returns A user actor with impersonation metadata.
 */
export function createTestImpersonatedUserActor(
  id: string,
  options: CreateTestImpersonatedUserActorOptions,
): ActivityActor {
  const { impersonatorId, ...actorOptions } = options;

  return createTestUserActor(id, {
    ...actorOptions,
    metadata: {
      ...actorOptions.metadata,
      impersonatorId,
    },
  });
}

/**
 * Create a predictable anonymous actor for tests.
 *
 * @param options - Optional display name or metadata.
 * @returns An anonymous actor with `type: "anonymous"`.
 */
export function createTestAnonymousActor(
  options: Omit<ActivityActor, "type"> = {},
): ActivityActor {
  return createAnonymousActor(options);
}

/**
 * Create a predictable service actor for tests.
 *
 * @param id - Stable service ID.
 * @param options - Optional display name or metadata.
 * @returns A service actor with `type: "service"`.
 */
export function createTestServiceActor(
  id = "service_test",
  options: Omit<ActivityActor, "type" | "id"> = {},
): ActivityActor {
  return createServiceActor(id, options);
}

/**
 * Create a predictable system actor for tests.
 *
 * @param id - Stable system actor ID.
 * @param options - Optional display name or metadata.
 * @returns A system actor with `type: "system"`.
 */
export function createTestSystemActor(
  id = "system_test",
  options: Omit<ActivityActor, "type" | "id"> = {},
): ActivityActor {
  return createSystemActor(id, options);
}

/**
 * Create a predictable tenant for tests.
 *
 * @param id - Stable tenant ID.
 * @param options - Optional slug or metadata.
 * @returns A tenant descriptor.
 */
export function createTestTenant(
  id = "tenant_test",
  options: CreateTestTenantOptions = {},
): ActivityTenant {
  return createTenant(id, options);
}

/**
 * Create the activity fields commonly copied onto app test contexts.
 *
 * @param options - Optional actor, tenant, request ID, and trace ID overrides.
 * @returns Stable activity context fields for a test.
 */
export function createTestActivityContext(
  options: CreateTestActivityContextOptions = {},
): TestActivityContext {
  return {
    actor: options.actor ?? createTestUserActor(),
    ...(options.tenant === null
      ? {}
      : { tenant: options.tenant ?? createTestTenant() }),
    requestId: options.requestId ?? "test-request",
    traceId: options.traceId ?? "test-trace",
  };
}

/**
 * Expected audit fields used by audit assertion helpers.
 */
export interface AuditLogEntryExpectation {
  /**
   * Expected action name.
   */
  action?: string;
  /**
   * Expected actor fields.
   */
  actor?: Partial<ActivityActor>;
  /**
   * Convenience matcher for `entry.actor.id`.
   */
  actorId?: string;
  /**
   * Convenience matcher for `entry.actor.type`.
   */
  actorType?: ActivityActor["type"];
  /**
   * Expected tenant fields.
   */
  tenant?: Partial<ActivityTenant>;
  /**
   * Convenience matcher for `entry.tenant.id`.
   */
  tenantId?: string;
  /**
   * Expected resource fields.
   */
  resource?: Partial<ActivityResource>;
  /**
   * Convenience matcher for `entry.resource.id`.
   */
  resourceId?: string;
  /**
   * Convenience matcher for `entry.resource.type`.
   */
  resourceType?: string;
  /**
   * Expected audit outcome.
   */
  outcome?: AuditOutcome;
  /**
   * Convenience matcher for `entry.metadata.severity`.
   */
  severity?: ActivityMetadataValue;
  /**
   * Expected request ID.
   */
  requestId?: string;
  /**
   * Expected trace ID.
   */
  traceId?: string;
  /**
   * Expected metadata fields. Object values are matched as partial objects.
   */
  metadata?: ActivityMetadata;
}

/**
 * Expected mail delivery fields used by mail assertion helpers.
 */
export interface MailDeliveryExpectation {
  /**
   * Expected memory delivery ID.
   */
  id?: string;
  /**
   * Expected subject.
   */
  subject?: string;
  /**
   * Expected recipients.
   */
  to?: NormalizedMailMessage["to"];
  /**
   * Expected sender.
   */
  from?: NormalizedMailMessage["from"];
  /**
   * Expected text body.
   */
  text?: string;
  /**
   * Expected HTML body.
   */
  html?: string;
  /**
   * Expected message headers.
   */
  headers?: Record<string, string>;
  /**
   * Expected normalized message fields.
   */
  message?: Partial<NormalizedMailMessage>;
}

/**
 * Expected notification delivery fields used by notification assertion helpers.
 */
export interface NotificationDeliveryExpectation {
  /**
   * Expected memory delivery ID.
   */
  id?: string;
  /**
   * Expected notification name.
   */
  notificationName?: string;
  /**
   * Expected parsed payload. Object values are matched as partial objects.
   */
  payload?: unknown;
  /**
   * Expected selected channels.
   */
  channels?: readonly string[];
  /**
   * Expected delivery metadata. Object values are matched as partial objects.
   */
  metadata?: Record<string, unknown>;
}

/**
 * Expected storage object fields used by storage assertion helpers.
 */
export interface StorageObjectExpectation {
  /**
   * Object key to look up.
   */
  key: string;
  /**
   * Expected size in bytes.
   */
  size?: number;
  /**
   * Expected content type.
   */
  contentType?: string;
  /**
   * Expected cache-control value.
   */
  cacheControl?: string;
  /**
   * Expected storage metadata.
   */
  metadata?: Record<string, string>;
  /**
   * Expected visibility.
   */
  visibility?: StorageVisibility;
  /**
   * Expected text body. Cannot be combined with `bytes`.
   */
  text?: string;
  /**
   * Expected object bytes. Cannot be combined with `text`.
   */
  bytes?: Uint8Array;
}

/**
 * Source accepted by outbox message assertion helpers.
 *
 * Use a `MemoryOutboxPort` or a snapshot returned by a test adapter. Durable
 * SQL adapters should expose app-owned snapshots rather than widening the
 * production `OutboxPort` read surface.
 */
export type OutboxMessageAssertionSource =
  | readonly OutboxMessage[]
  | {
      /**
       * Current outbox message snapshots.
       */
      readonly messages: readonly OutboxMessage[];
    };

/**
 * Expected outbox message fields used by outbox assertion helpers.
 */
export interface OutboxMessageExpectation {
  /**
   * Expected message ID.
   */
  id?: string;
  /**
   * Expected message kind.
   */
  kind?: OutboxMessageKind;
  /**
   * Expected event or job name.
   */
  name?: string;
  /**
   * Expected JSON payload. Object values are matched as partial objects.
   */
  payload?: unknown;
  /**
   * Expected delivery status.
   */
  status?: OutboxMessageStatus;
  /**
   * Expected claim attempt count.
   */
  attempts?: number;
  /**
   * Expected maximum delivery attempts.
   */
  maxAttempts?: number;
  /**
   * Expected delivery timestamp.
   */
  deliveredAt?: Date | null;
  /**
   * Expected serialized delivery error. Object values are matched as partial
   * objects. Pass `null` to assert no error has been recorded.
   */
  lastError?: Partial<OutboxErrorInfo> | null;
}

/**
 * Expected fields for one outbox drain result assertion.
 */
export interface OutboxDrainResultExpectation {
  /**
   * Expected claimed count.
   */
  claimed?: number;
  /**
   * Expected delivered count.
   */
  delivered?: number;
  /**
   * Expected retried count.
   */
  retried?: number;
  /**
   * Expected dead-lettered count.
   */
  deadLettered?: number;
}

/**
 * Source accepted by idempotency entry assertion helpers.
 *
 * Use a `MemoryIdempotencyStore` or a snapshot array from an app-owned adapter.
 * Durable SQL adapters should expose app-owned snapshots rather than widening
 * the production `IdempotencyPort` read surface.
 */
export type IdempotencyEntryAssertionSource =
  | readonly MemoryIdempotencyEntry[]
  | Pick<MemoryIdempotencyStore, "entries">;

/**
 * Expected idempotency entry fields used by idempotency assertion helpers.
 */
export interface IdempotencyEntryExpectation {
  /**
   * Expected operation namespace.
   */
  namespace?: string;
  /**
   * Expected client-provided idempotency key.
   */
  key?: string;
  /**
   * Expected normalized scope key.
   */
  scopeKey?: string;
  /**
   * Expected request fingerprint.
   */
  fingerprint?: string;
  /**
   * Expected reservation status.
   */
  status?: MemoryIdempotencyEntry["status"];
  /**
   * Expected replay result. Object values are matched as partial objects.
   */
  result?: unknown;
  /**
   * Expected reservation timestamp.
   */
  reservedAt?: Date;
  /**
   * Expected completion timestamp.
   */
  completedAt?: Date;
  /**
   * Expected expiration timestamp, or `null` when no expiration is set.
   */
  expiresAt?: Date | null;
}

/**
 * Find the first audit entry matching the expected fields.
 *
 * @param entries - Audit entries captured by a memory or fake audit port.
 * @param expectation - Partial audit fields to match.
 * @returns The first matching entry, or `undefined`.
 */
export function findAuditEntry(
  entries: readonly AuditLogEntry[],
  expectation: AuditLogEntryExpectation,
): AuditLogEntry | undefined {
  return entries.find((entry) => auditEntryMatches(entry, expectation));
}

/**
 * Assert that an audit entry exists and return the matching entry.
 *
 * The helper throws a plain `Error`, so it works with Bun, Vitest, Jest, and
 * other test runners.
 *
 * @param entries - Audit entries captured by a memory or fake audit port.
 * @param expectation - Partial audit fields to match.
 * @returns The matching audit entry.
 * @throws Error when no entry matches.
 */
export function assertAuditEntry(
  entries: readonly AuditLogEntry[],
  expectation: AuditLogEntryExpectation,
): AuditLogEntry {
  const entry = findAuditEntry(entries, expectation);
  if (entry) return entry;

  throw new Error(
    `Expected audit entry matching ${formatAuditExpectation(expectation)}, but only found ${entries.length} entr${
      entries.length === 1 ? "y" : "ies"
    }.`,
  );
}

/**
 * Assert that no audit entry matches the expected fields.
 *
 * @param entries - Audit entries captured by a memory or fake audit port.
 * @param expectation - Partial audit fields to reject.
 * @throws Error when a matching entry exists.
 */
export function assertNoAuditEntry(
  entries: readonly AuditLogEntry[],
  expectation: AuditLogEntryExpectation,
): void {
  const entry = findAuditEntry(entries, expectation);
  if (!entry) return;

  throw new Error(
    `Expected no audit entry matching ${formatAuditExpectation(expectation)}, but found ${entry.action}.`,
  );
}

/**
 * Find the first recorded event matching the expected fields.
 *
 * @param events - Events captured by `createRecordingEventBus(...)`.
 * @param expectation - Partial event fields to match.
 * @returns The first matching event, or `undefined`.
 */
export function findRecordedEvent(
  events: readonly RecordedEvent[],
  expectation: RecordedEventExpectation,
): RecordedEvent | undefined {
  return events.find((event) => recordedEventMatches(event, expectation));
}

/**
 * Assert that a recorded event exists and return the matching event.
 *
 * @param events - Events captured by `createRecordingEventBus(...)`.
 * @param expectation - Partial event fields to match.
 * @returns The matching event.
 * @throws Error when no event matches.
 */
export function assertRecordedEvent(
  events: readonly RecordedEvent[],
  expectation: RecordedEventExpectation,
): RecordedEvent {
  const event = findRecordedEvent(events, expectation);
  if (event) return event;

  throw new Error(
    `Expected recorded event matching ${formatTestingExpectation(expectation)}, but only found ${events.length} ${pluralize(
      "event",
      events.length,
    )}.`,
  );
}

/**
 * Assert that no recorded event matches the expected fields.
 *
 * @param events - Events captured by `createRecordingEventBus(...)`.
 * @param expectation - Partial event fields to reject.
 * @throws Error when a matching event exists.
 */
export function assertNoRecordedEvent(
  events: readonly RecordedEvent[],
  expectation: RecordedEventExpectation,
): void {
  const event = findRecordedEvent(events, expectation);
  if (!event) return;

  throw new Error(
    `Expected no recorded event matching ${formatTestingExpectation(expectation)}, but found ${event.name}.`,
  );
}

/**
 * Find the first recorded job dispatch matching the expected fields.
 *
 * @param jobs - Job dispatches captured by `createRecordingJobDispatcher(...)`.
 * @param expectation - Partial job fields to match.
 * @returns The first matching dispatch, or `undefined`.
 */
export function findDispatchedJob(
  jobs: readonly RecordedJobDispatch[],
  expectation: RecordedJobDispatchExpectation,
): RecordedJobDispatch | undefined {
  return jobs.find((job) => recordedJobMatches(job, expectation));
}

/**
 * Assert that a job was dispatched and return the matching dispatch.
 *
 * @param jobs - Job dispatches captured by `createRecordingJobDispatcher(...)`.
 * @param expectation - Partial job fields to match.
 * @returns The matching dispatch.
 * @throws Error when no dispatch matches.
 */
export function assertDispatchedJob(
  jobs: readonly RecordedJobDispatch[],
  expectation: RecordedJobDispatchExpectation,
): RecordedJobDispatch {
  const job = findDispatchedJob(jobs, expectation);
  if (job) return job;

  throw new Error(
    `Expected dispatched job matching ${formatTestingExpectation(expectation)}, but only found ${jobs.length} ${pluralize(
      "job",
      jobs.length,
    )}.`,
  );
}

/**
 * Assert that no job dispatch matches the expected fields.
 *
 * @param jobs - Job dispatches captured by `createRecordingJobDispatcher(...)`.
 * @param expectation - Partial job fields to reject.
 * @throws Error when a matching dispatch exists.
 */
export function assertNoDispatchedJob(
  jobs: readonly RecordedJobDispatch[],
  expectation: RecordedJobDispatchExpectation,
): void {
  const job = findDispatchedJob(jobs, expectation);
  if (!job) return;

  throw new Error(
    `Expected no dispatched job matching ${formatTestingExpectation(expectation)}, but found ${job.name}.`,
  );
}

/**
 * Find the first recorded schedule run matching the expected fields.
 *
 * @param runs - Runs captured by `createRecordingScheduleRunner(...)`.
 * @param expectation - Partial schedule fields to match.
 * @returns The first matching schedule run, or `undefined`.
 */
export function findScheduleRun(
  runs: readonly RecordedScheduleRun[],
  expectation: RecordedScheduleRunExpectation,
): RecordedScheduleRun | undefined {
  return runs.find((run) => recordedScheduleRunMatches(run, expectation));
}

/**
 * Find the first provider instrumentation event matching the expected fields.
 *
 * @param source - Recording instrumentation result or event snapshot array.
 * @param expectation - Partial event fields to match.
 * @returns The first matching event, or `undefined`.
 */
export function findProviderInstrumentationEvent(
  source: ProviderInstrumentationAssertionSource,
  expectation: ProviderInstrumentationEventExpectation,
): ProviderInstrumentationEventInput | undefined {
  return getProviderInstrumentationEvents(source).find((event) =>
    providerInstrumentationEventMatches(event, expectation),
  );
}

/**
 * Assert that a provider instrumentation event exists and return it.
 *
 * @param source - Recording instrumentation result or event snapshot array.
 * @param expectation - Partial event fields to match.
 * @returns The matching event.
 * @throws Error when no event matches.
 */
export function assertProviderInstrumentationEvent(
  source: ProviderInstrumentationAssertionSource,
  expectation: ProviderInstrumentationEventExpectation,
): ProviderInstrumentationEventInput {
  const event = findProviderInstrumentationEvent(source, expectation);
  if (event) return event;

  const events = getProviderInstrumentationEvents(source);
  throw new Error(
    `Expected provider instrumentation event matching ${formatTestingExpectation(
      expectation,
    )}, but only found ${events.length} ${pluralize("event", events.length)}.`,
  );
}

/**
 * Assert that no provider instrumentation event matches the expected fields.
 *
 * @param source - Recording instrumentation result or event snapshot array.
 * @param expectation - Partial event fields to reject.
 * @throws Error when a matching event exists.
 */
export function assertNoProviderInstrumentationEvent(
  source: ProviderInstrumentationAssertionSource,
  expectation: ProviderInstrumentationEventExpectation,
): void {
  const event = findProviderInstrumentationEvent(source, expectation);
  if (!event) return;

  throw new Error(
    `Expected no provider instrumentation event matching ${formatTestingExpectation(
      expectation,
    )}, but found ${event.type}.`,
  );
}

/**
 * Assert that a schedule run was recorded and return the matching run.
 *
 * @param runs - Runs captured by `createRecordingScheduleRunner(...)`.
 * @param expectation - Partial schedule fields to match.
 * @returns The matching schedule run.
 * @throws Error when no run matches.
 */
export function assertScheduleRun(
  runs: readonly RecordedScheduleRun[],
  expectation: RecordedScheduleRunExpectation,
): RecordedScheduleRun {
  const run = findScheduleRun(runs, expectation);
  if (run) return run;

  throw new Error(
    `Expected schedule run matching ${formatTestingExpectation(expectation)}, but only found ${runs.length} ${pluralize(
      "run",
      runs.length,
    )}.`,
  );
}

/**
 * Assert that no schedule run matches the expected fields.
 *
 * @param runs - Runs captured by `createRecordingScheduleRunner(...)`.
 * @param expectation - Partial schedule fields to reject.
 * @throws Error when a matching run exists.
 */
export function assertNoScheduleRun(
  runs: readonly RecordedScheduleRun[],
  expectation: RecordedScheduleRunExpectation,
): void {
  const run = findScheduleRun(runs, expectation);
  if (!run) return;

  throw new Error(
    `Expected no schedule run matching ${formatTestingExpectation(expectation)}, but found ${run.name}.`,
  );
}

/**
 * Find the first mail delivery matching the expected fields.
 *
 * @param deliveries - Deliveries captured by `createMemoryMailer(...)`.
 * @param expectation - Partial delivery or message fields to match.
 * @returns The first matching delivery, or `undefined`.
 */
export function findMailDelivery(
  deliveries: readonly MemoryMailDelivery[],
  expectation: MailDeliveryExpectation,
): MemoryMailDelivery | undefined {
  return deliveries.find((delivery) =>
    mailDeliveryMatches(delivery, expectation),
  );
}

/**
 * Assert that a mail delivery exists and return the matching delivery.
 *
 * @param deliveries - Deliveries captured by `createMemoryMailer(...)`.
 * @param expectation - Partial delivery or message fields to match.
 * @returns The matching delivery.
 * @throws Error when no delivery matches.
 */
export function assertMailDelivery(
  deliveries: readonly MemoryMailDelivery[],
  expectation: MailDeliveryExpectation,
): MemoryMailDelivery {
  const delivery = findMailDelivery(deliveries, expectation);
  if (delivery) return delivery;

  throw new Error(
    `Expected mail delivery matching ${formatTestingExpectation(expectation)}, but only found ${deliveries.length} ${pluralize(
      "delivery",
      deliveries.length,
    )}.`,
  );
}

/**
 * Assert that no mail delivery matches the expected fields.
 *
 * @param deliveries - Deliveries captured by `createMemoryMailer(...)`.
 * @param expectation - Partial delivery or message fields to reject.
 * @throws Error when a matching delivery exists.
 */
export function assertNoMailDelivery(
  deliveries: readonly MemoryMailDelivery[],
  expectation: MailDeliveryExpectation,
): void {
  const delivery = findMailDelivery(deliveries, expectation);
  if (!delivery) return;

  throw new Error(
    `Expected no mail delivery matching ${formatTestingExpectation(expectation)}, but found ${delivery.message.subject}.`,
  );
}

/**
 * Find the first notification delivery matching the expected fields.
 *
 * @param deliveries - Deliveries captured by `createMemoryNotificationPort(...)`.
 * @param expectation - Partial delivery fields to match.
 * @returns The first matching delivery, or `undefined`.
 */
export function findNotificationDelivery(
  deliveries: readonly MemoryNotificationDelivery[],
  expectation: NotificationDeliveryExpectation,
): MemoryNotificationDelivery | undefined {
  return deliveries.find((delivery) =>
    notificationDeliveryMatches(delivery, expectation),
  );
}

/**
 * Assert that a notification delivery exists and return the matching delivery.
 *
 * @param deliveries - Deliveries captured by `createMemoryNotificationPort(...)`.
 * @param expectation - Partial delivery fields to match.
 * @returns The matching delivery.
 * @throws Error when no delivery matches.
 */
export function assertNotificationDelivery(
  deliveries: readonly MemoryNotificationDelivery[],
  expectation: NotificationDeliveryExpectation,
): MemoryNotificationDelivery {
  const delivery = findNotificationDelivery(deliveries, expectation);
  if (delivery) return delivery;

  throw new Error(
    `Expected notification delivery matching ${formatTestingExpectation(expectation)}, but only found ${deliveries.length} ${pluralize(
      "delivery",
      deliveries.length,
    )}.`,
  );
}

/**
 * Assert that no notification delivery matches the expected fields.
 *
 * @param deliveries - Deliveries captured by `createMemoryNotificationPort(...)`.
 * @param expectation - Partial delivery fields to reject.
 * @throws Error when a matching delivery exists.
 */
export function assertNoNotificationDelivery(
  deliveries: readonly MemoryNotificationDelivery[],
  expectation: NotificationDeliveryExpectation,
): void {
  const delivery = findNotificationDelivery(deliveries, expectation);
  if (!delivery) return;

  throw new Error(
    `Expected no notification delivery matching ${formatTestingExpectation(expectation)}, but found ${delivery.notificationName}.`,
  );
}

/**
 * Assert that a storage object exists and optionally matches metadata/body
 * expectations.
 *
 * This helper works against any `StoragePort`, not only memory storage.
 *
 * @param storage - Storage port under test.
 * @param expectation - Object key and expected fields.
 * @returns The matching object metadata.
 * @throws Error when the object is missing or does not match.
 */
export async function assertStorageObject(
  storage: StoragePort,
  expectation: StorageObjectExpectation,
): Promise<StorageObject> {
  if (expectation.text !== undefined && expectation.bytes !== undefined) {
    throw new Error(
      `Expected storage object "${expectation.key}" to check either text or bytes, not both.`,
    );
  }

  const object =
    expectation.text !== undefined || expectation.bytes !== undefined
      ? await storage.get(expectation.key)
      : await storage.stat(expectation.key);

  if (!object) {
    throw new Error(`Expected storage object "${expectation.key}" to exist.`);
  }

  if (!storageObjectMatches(object, expectation)) {
    throw new Error(
      `Expected storage object "${expectation.key}" to match ${formatTestingExpectation(
        omitStorageBodyExpectation(expectation),
      )}.`,
    );
  }

  if (expectation.text !== undefined) {
    const actualText = hasStorageTextReader(object)
      ? await object.text()
      : undefined;
    if (actualText !== expectation.text) {
      throw new Error(
        `Expected storage object "${expectation.key}" text to match ${JSON.stringify(
          expectation.text,
        )}.`,
      );
    }
  } else if (expectation.bytes !== undefined) {
    const actualBytes = hasStorageBytesReader(object)
      ? await object.bytes()
      : undefined;
    if (!uint8ArrayMatches(actualBytes, expectation.bytes)) {
      throw new Error(
        `Expected storage object "${expectation.key}" bytes to match.`,
      );
    }
  }

  return object;
}

/**
 * Assert that a storage object does not exist.
 *
 * @param storage - Storage port under test.
 * @param key - Object key expected to be absent.
 * @throws Error when the object exists.
 */
export async function assertNoStorageObject(
  storage: StoragePort,
  key: string,
): Promise<void> {
  const exists = await storage.exists(key);
  if (!exists) return;

  throw new Error(`Expected storage object "${key}" not to exist.`);
}

/**
 * Find the first outbox message matching expected fields.
 *
 * @param source - Memory outbox or message snapshot array.
 * @param expectation - Partial message fields to match.
 * @returns The first matching message, or `undefined`.
 */
export function findOutboxMessage(
  source: OutboxMessageAssertionSource,
  expectation: OutboxMessageExpectation,
): OutboxMessage | undefined {
  return getOutboxMessages(source).find((message) =>
    outboxMessageMatches(message, expectation),
  );
}

/**
 * Find the first idempotency entry matching expected fields.
 *
 * @param source - Memory idempotency store or entry snapshot array.
 * @param expectation - Partial entry fields to match.
 * @returns The first matching entry, or `undefined`.
 */
export function findIdempotencyEntry(
  source: IdempotencyEntryAssertionSource,
  expectation: IdempotencyEntryExpectation,
): MemoryIdempotencyEntry | undefined {
  return getIdempotencyEntries(source).find((entry) =>
    idempotencyEntryMatches(entry, expectation),
  );
}

/**
 * Assert that an idempotency entry exists and return it.
 *
 * @param source - Memory idempotency store or entry snapshot array.
 * @param expectation - Partial entry fields to match.
 * @returns The matching entry.
 * @throws Error when no entry matches.
 */
export function assertIdempotencyEntry(
  source: IdempotencyEntryAssertionSource,
  expectation: IdempotencyEntryExpectation,
): MemoryIdempotencyEntry {
  const entry = findIdempotencyEntry(source, expectation);
  if (entry) return entry;

  const entries = getIdempotencyEntries(source);
  throw new Error(
    `Expected idempotency entry matching ${formatTestingExpectation(
      expectation,
    )}, but only found ${entries.length} ${pluralize("entry", entries.length)}.`,
  );
}

/**
 * Assert that no idempotency entry matches expected fields.
 *
 * @param source - Memory idempotency store or entry snapshot array.
 * @param expectation - Partial entry fields to reject.
 * @throws Error when a matching entry exists.
 */
export function assertNoIdempotencyEntry(
  source: IdempotencyEntryAssertionSource,
  expectation: IdempotencyEntryExpectation,
): void {
  const entry = findIdempotencyEntry(source, expectation);
  if (!entry) return;

  throw new Error(
    `Expected no idempotency entry matching ${formatTestingExpectation(
      expectation,
    )}, but found ${entry.namespace}:${entry.key}.`,
  );
}

/**
 * Assert that a matching idempotency entry is still in progress.
 *
 * @param source - Memory idempotency store or entry snapshot array.
 * @param expectation - Entry fields to match.
 * @returns The matching in-progress entry.
 */
export function assertIdempotencyInProgress(
  source: IdempotencyEntryAssertionSource,
  expectation: Omit<IdempotencyEntryExpectation, "status"> = {},
): MemoryIdempotencyEntry {
  return assertIdempotencyEntry(source, {
    ...expectation,
    status: "in-progress",
  });
}

/**
 * Assert that a matching idempotency entry completed.
 *
 * @param source - Memory idempotency store or entry snapshot array.
 * @param expectation - Entry fields to match.
 * @returns The matching completed entry.
 */
export function assertIdempotencyCompleted(
  source: IdempotencyEntryAssertionSource,
  expectation: Omit<IdempotencyEntryExpectation, "status"> = {},
): MemoryIdempotencyEntry {
  return assertIdempotencyEntry(source, {
    ...expectation,
    status: "completed",
  });
}

/**
 * Assert that an outbox message exists and return it.
 *
 * @param source - Memory outbox or message snapshot array.
 * @param expectation - Partial message fields to match.
 * @returns The matching message.
 * @throws Error when no message matches.
 */
export function assertOutboxMessage(
  source: OutboxMessageAssertionSource,
  expectation: OutboxMessageExpectation,
): OutboxMessage {
  const message = findOutboxMessage(source, expectation);
  if (message) return message;

  const messages = getOutboxMessages(source);
  throw new Error(
    `Expected outbox message matching ${formatTestingExpectation(expectation)}, but only found ${messages.length} ${pluralize(
      "message",
      messages.length,
    )}.`,
  );
}

/**
 * Assert that no outbox message matches expected fields.
 *
 * @param source - Memory outbox or message snapshot array.
 * @param expectation - Partial message fields to reject.
 * @throws Error when a matching message exists.
 */
export function assertNoOutboxMessage(
  source: OutboxMessageAssertionSource,
  expectation: OutboxMessageExpectation,
): void {
  const message = findOutboxMessage(source, expectation);
  if (!message) return;

  throw new Error(
    `Expected no outbox message matching ${formatTestingExpectation(expectation)}, but found ${message.kind} "${message.name}".`,
  );
}

/**
 * Assert that an outbox message is pending.
 *
 * @param source - Memory outbox or message snapshot array.
 * @param expectation - Message fields to match.
 * @returns The matching pending message.
 */
export function assertOutboxPending(
  source: OutboxMessageAssertionSource,
  expectation: Omit<OutboxMessageExpectation, "status"> = {},
): OutboxMessage {
  return assertOutboxMessage(source, {
    ...expectation,
    status: "pending",
  });
}

/**
 * Assert that an outbox message was delivered.
 *
 * @param source - Memory outbox or message snapshot array.
 * @param expectation - Message fields to match.
 * @returns The matching delivered message.
 */
export function assertOutboxDelivered(
  source: OutboxMessageAssertionSource,
  expectation: Omit<OutboxMessageExpectation, "status"> = {},
): OutboxMessage {
  return assertOutboxMessage(source, {
    ...expectation,
    status: "delivered",
  });
}

/**
 * Assert that an outbox message is pending after at least one failed attempt.
 *
 * Use this for retry-scheduled assertions after `drainOutbox(...)` returns a
 * retried count.
 *
 * @param source - Memory outbox or message snapshot array.
 * @param expectation - Message fields to match.
 * @returns The matching retry-scheduled message.
 */
export function assertOutboxRetryScheduled(
  source: OutboxMessageAssertionSource,
  expectation: Omit<OutboxMessageExpectation, "status"> = {},
): OutboxMessage {
  const message = assertOutboxMessage(source, {
    ...expectation,
    status: "pending",
  });

  if (message.attempts < 1 || !message.lastError) {
    throw new Error(
      `Expected outbox message "${message.id}" to have a recorded failed attempt.`,
    );
  }

  return message;
}

/**
 * Assert that an outbox message was dead-lettered.
 *
 * @param source - Memory outbox or message snapshot array.
 * @param expectation - Message fields to match.
 * @returns The matching dead-lettered message.
 */
export function assertOutboxDeadLettered(
  source: OutboxMessageAssertionSource,
  expectation: Omit<OutboxMessageExpectation, "status"> = {},
): OutboxMessage {
  return assertOutboxMessage(source, {
    ...expectation,
    status: "deadLettered",
  });
}

/**
 * Assert that a drain result matches expected counts.
 *
 * @param result - Result returned by `drainOutbox(...)`.
 * @param expectation - Partial count expectation.
 * @throws Error when any supplied count differs.
 */
export function assertOutboxDrainResult(
  result: DrainOutboxResult,
  expectation: OutboxDrainResultExpectation,
): void {
  if (!outboxDrainResultMatches(result, expectation)) {
    throw new Error(
      `Expected outbox drain result matching ${formatTestingExpectation(
        expectation,
      )}, but received ${formatTestingExpectation(result)}.`,
    );
  }
}

/**
 * Expected outcome for one policy matrix case.
 */
export type PolicyMatrixExpectation = "allow" | "deny";

type PolicyMatrixSubject<TResolver> =
  PolicySubjectArgs<TResolver> extends [subject: infer Subject]
    ? { subject: Subject }
    : { subject?: never };

/**
 * One typed authorization matrix case for a policy ability.
 */
export type PolicyMatrixCase<
  TContext,
  TPolicies extends readonly PolicyDefinition[] = readonly PolicyDefinition[],
> = {
  [TAbility in keyof PolicyMapFromDefinitions<TPolicies> & string]: {
    name: string;
    ctx: TContext;
    ability: TAbility;
    expected: PolicyMatrixExpectation;
    reason?: string;
    code?: string;
  } & PolicyMatrixSubject<PolicyMapFromDefinitions<TPolicies>[TAbility]>;
}[keyof PolicyMapFromDefinitions<TPolicies> & string];

/**
 * Untyped policy matrix case used internally for failure reporting.
 */
export type UntypedPolicyMatrixCase<TContext> = {
  name: string;
  ctx: TContext;
  ability: string;
  subject?: unknown;
  expected: PolicyMatrixExpectation;
  reason?: string;
  code?: string;
};

/**
 * Result for one evaluated policy matrix case.
 */
export type PolicyMatrixResult<
  TContext,
  TPolicies extends readonly PolicyDefinition[] = readonly PolicyDefinition[],
> = {
  case: PolicyMatrixCase<TContext, TPolicies>;
  decision: GateDecision;
  passed: boolean;
  message?: string;
};

/**
 * Test helper for evaluating authorization policies.
 */
export type PolicyTester<
  TContext,
  TPolicies extends readonly PolicyDefinition[],
> = {
  /**
   * Gate created from the same policies, useful for direct assertions.
   */
  gate: GatePort<TContext, TPolicies>;
  /**
   * Evaluate cases and return structured pass/fail results.
   */
  evaluateMatrix(
    cases: readonly PolicyMatrixCase<TContext, TPolicies>[],
  ): Promise<PolicyMatrixResult<TContext, TPolicies>[]>;
  /**
   * Evaluate cases and throw a combined assertion error when any fail.
   */
  assertMatrix(
    cases: readonly PolicyMatrixCase<TContext, TPolicies>[],
  ): Promise<void>;
};

/**
 * Create a policy tester from the same options used by `createGate(...)`.
 *
 * Use this for table-driven authorization tests that document who can perform
 * each ability against which subject.
 *
 * @param options - Policy definitions and optional denial mapper.
 * @returns A policy tester with a gate plus matrix helpers.
 */
export function createPolicyTester<
  const TPolicies extends readonly PolicyDefinition[],
>(
  options: CreateGateOptions<
    PolicyContextFromDefinitions<TPolicies>,
    TPolicies
  >,
): PolicyTester<PolicyContextFromDefinitions<TPolicies>, TPolicies> {
  const gate = createGate(options);

  return {
    gate,
    evaluateMatrix: (cases) => evaluatePolicyMatrix(gate, cases),
    assertMatrix: (cases) => assertPolicyMatrix(gate, cases),
  };
}

/**
 * Evaluate a table of policy cases without throwing.
 *
 * @param gate - Gate under test.
 * @param cases - Matrix cases to evaluate.
 * @returns Structured result for each case.
 */
export async function evaluatePolicyMatrix<
  TContext,
  TPolicies extends readonly PolicyDefinition[],
>(
  gate: GatePort<TContext, TPolicies>,
  cases: readonly PolicyMatrixCase<TContext, TPolicies>[],
): Promise<PolicyMatrixResult<TContext, TPolicies>[]> {
  const results: PolicyMatrixResult<TContext, TPolicies>[] = [];

  for (const matrixCase of cases) {
    const decision = await inspectPolicyMatrixCase(gate, matrixCase);
    const passed = policyMatrixCasePassed(matrixCase, decision);

    results.push({
      case: matrixCase,
      decision,
      passed,
      message: passed
        ? undefined
        : policyMatrixFailureMessage(matrixCase, decision),
    });
  }

  return results;
}

/**
 * Assert that all policy matrix cases pass.
 *
 * @param gate - Gate under test.
 * @param cases - Matrix cases to evaluate.
 * @throws Combined error listing every failed case.
 */
export async function assertPolicyMatrix<
  TContext,
  TPolicies extends readonly PolicyDefinition[],
>(
  gate: GatePort<TContext, TPolicies>,
  cases: readonly PolicyMatrixCase<TContext, TPolicies>[],
): Promise<void> {
  const results = await evaluatePolicyMatrix(gate, cases);
  const failures = results.filter((result) => !result.passed);

  if (failures.length === 0) return;

  throw new Error(
    [
      `Policy matrix failed for ${failures.length} case${
        failures.length === 1 ? "" : "s"
      }.`,
      ...failures.map((failure) => `- ${failure.message}`),
    ].join("\n"),
  );
}

async function inspectPolicyMatrixCase<
  TContext,
  TPolicies extends readonly PolicyDefinition[],
>(
  gate: GatePort<TContext, TPolicies>,
  matrixCase: PolicyMatrixCase<TContext, TPolicies>,
): Promise<GateDecision> {
  const inspect = gate.inspect as DynamicGateInspect<TContext>;

  if ("subject" in matrixCase) {
    return inspect(matrixCase.ctx, matrixCase.ability, matrixCase.subject);
  }

  return inspect(matrixCase.ctx, matrixCase.ability);
}

function policyMatrixCasePassed<TContext>(
  matrixCase: UntypedPolicyMatrixCase<TContext>,
  decision: GateDecision,
): boolean {
  if (matrixCase.expected === "allow" && !decision.allowed) return false;
  if (matrixCase.expected === "deny" && decision.allowed) return false;
  if (matrixCase.reason && decision.allowed) return false;
  if (matrixCase.code && decision.allowed) return false;
  if (decision.allowed) return true;

  return (
    (!matrixCase.reason || decision.reason === matrixCase.reason) &&
    (!matrixCase.code || decision.code === matrixCase.code)
  );
}

function policyMatrixFailureMessage<TContext>(
  matrixCase: UntypedPolicyMatrixCase<TContext>,
  decision: GateDecision,
): string {
  const expected: string[] = [matrixCase.expected];
  if (matrixCase.reason) expected.push(`reason "${matrixCase.reason}"`);
  if (matrixCase.code) expected.push(`code "${matrixCase.code}"`);

  const actual = decision.allowed
    ? "allow"
    : [
        "deny",
        decision.reason ? `reason "${decision.reason}"` : undefined,
        decision.code ? `code "${decision.code}"` : undefined,
      ]
        .filter(Boolean)
        .join(", ");

  return `${matrixCase.name}: expected ${expected.join(", ")}, received ${actual}.`;
}

type DynamicGateInspect<TContext> = (
  ctx: TContext,
  ability: string,
  subject?: unknown,
) => Promise<GateDecision>;

function mergeRoleMetadata(
  metadata: ActivityMetadata | undefined,
  role: string | undefined,
): ActivityMetadata | undefined {
  if (!role) return metadata;

  return {
    ...metadata,
    role,
  };
}

function auditEntryMatches(
  entry: AuditLogEntry,
  expectation: AuditLogEntryExpectation,
): boolean {
  if (expectation.action !== undefined && entry.action !== expectation.action) {
    return false;
  }
  if (
    expectation.outcome !== undefined &&
    entry.outcome !== expectation.outcome
  ) {
    return false;
  }
  if (
    expectation.requestId !== undefined &&
    entry.requestId !== expectation.requestId
  ) {
    return false;
  }
  if (
    expectation.traceId !== undefined &&
    entry.traceId !== expectation.traceId
  ) {
    return false;
  }
  if (
    expectation.actorId !== undefined &&
    entry.actor.id !== expectation.actorId
  ) {
    return false;
  }
  if (
    expectation.actorType !== undefined &&
    entry.actor.type !== expectation.actorType
  ) {
    return false;
  }
  if (
    expectation.tenantId !== undefined &&
    entry.tenant?.id !== expectation.tenantId
  ) {
    return false;
  }
  if (
    expectation.resourceId !== undefined &&
    entry.resource?.id !== expectation.resourceId
  ) {
    return false;
  }
  if (
    expectation.resourceType !== undefined &&
    entry.resource?.type !== expectation.resourceType
  ) {
    return false;
  }
  if (
    expectation.severity !== undefined &&
    entry.metadata?.severity !== expectation.severity
  ) {
    return false;
  }

  return (
    partialObjectMatches(entry.actor, expectation.actor) &&
    partialObjectMatches(entry.tenant, expectation.tenant) &&
    partialObjectMatches(entry.resource, expectation.resource) &&
    partialObjectMatches(entry.metadata, expectation.metadata)
  );
}

function recordedEventMatches(
  event: RecordedEvent,
  expectation: RecordedEventExpectation,
): boolean {
  if (expectation.name !== undefined && event.name !== expectation.name) {
    return false;
  }

  return expectation.payload === undefined
    ? true
    : valueMatches(event.payload, expectation.payload);
}

function recordedJobMatches(
  job: RecordedJobDispatch,
  expectation: RecordedJobDispatchExpectation,
): boolean {
  if (expectation.name !== undefined && job.name !== expectation.name) {
    return false;
  }

  return expectation.payload === undefined
    ? true
    : valueMatches(job.payload, expectation.payload);
}

function recordedScheduleRunMatches(
  run: RecordedScheduleRun,
  expectation: RecordedScheduleRunExpectation,
): boolean {
  if (expectation.name !== undefined && run.name !== expectation.name) {
    return false;
  }
  if (expectation.id !== undefined && run.id !== expectation.id) {
    return false;
  }
  if (expectation.source !== undefined && run.source !== expectation.source) {
    return false;
  }

  return expectation.payload === undefined
    ? true
    : valueMatches(run.payload, expectation.payload);
}

function getProviderInstrumentationEvents(
  source: ProviderInstrumentationAssertionSource,
): readonly ProviderInstrumentationEventInput[] {
  return "events" in source ? source.events : source;
}

function providerInstrumentationEventMatches(
  event: ProviderInstrumentationEventInput,
  expectation: ProviderInstrumentationEventExpectation,
): boolean {
  const eventRecord = event as unknown as Record<string, unknown>;
  const expectationRecord = expectation as Record<string, unknown>;
  const directKeys = [
    "type",
    "id",
    "timestamp",
    "requestId",
    "traceId",
    "spanId",
    "parentSpanId",
    "traceparent",
    "watcher",
    "method",
    "path",
    "contractName",
    "status",
    "durationMs",
    "summary",
    "message",
    "stack",
    "useCaseName",
    "name",
    "kind",
    "phase",
    "error",
    "eventName",
    "jobName",
    "messageId",
    "messageKind",
    "messageName",
    "scheduleName",
    "cron",
    "timezone",
    "action",
    "label",
  ] as const;

  for (const key of directKeys) {
    if (
      Object.hasOwn(expectation, key) &&
      !valueMatches(eventRecord[key], expectationRecord[key])
    ) {
      return false;
    }
  }

  if (
    expectation.providerName !== undefined &&
    providerNameFromInstrumentationEvent(event) !== expectation.providerName
  ) {
    return false;
  }

  return expectation.details === undefined
    ? true
    : valueMatches(event.details, expectation.details);
}

function providerNameFromInstrumentationEvent(
  event: ProviderInstrumentationEventInput,
): string | undefined {
  if (event.type === "provider") return event.providerName;
  if (!event.details || typeof event.details !== "object") return undefined;
  return (event.details as { providerName?: unknown }).providerName as
    | string
    | undefined;
}

function mailDeliveryMatches(
  delivery: MemoryMailDelivery,
  expectation: MailDeliveryExpectation,
): boolean {
  if (expectation.id !== undefined && delivery.id !== expectation.id) {
    return false;
  }
  if (
    expectation.subject !== undefined &&
    delivery.message.subject !== expectation.subject
  ) {
    return false;
  }
  if (
    expectation.to !== undefined &&
    !valueMatches(delivery.message.to, expectation.to)
  ) {
    return false;
  }
  if (
    expectation.from !== undefined &&
    !valueMatches(delivery.message.from, expectation.from)
  ) {
    return false;
  }
  if (
    expectation.text !== undefined &&
    delivery.message.text !== expectation.text
  ) {
    return false;
  }
  if (
    expectation.html !== undefined &&
    delivery.message.html !== expectation.html
  ) {
    return false;
  }

  return (
    partialObjectMatches(delivery.message.headers, expectation.headers) &&
    partialObjectMatches(delivery.message, expectation.message)
  );
}

function notificationDeliveryMatches(
  delivery: MemoryNotificationDelivery,
  expectation: NotificationDeliveryExpectation,
): boolean {
  if (expectation.id !== undefined && delivery.id !== expectation.id) {
    return false;
  }
  if (
    expectation.notificationName !== undefined &&
    delivery.notificationName !== expectation.notificationName
  ) {
    return false;
  }
  if (
    expectation.channels !== undefined &&
    !valueMatches(delivery.channels, expectation.channels)
  ) {
    return false;
  }
  if (
    expectation.payload !== undefined &&
    !valueMatches(delivery.payload, expectation.payload)
  ) {
    return false;
  }

  return partialObjectMatches(delivery.metadata, expectation.metadata);
}

function storageObjectMatches(
  object: StorageObject,
  expectation: StorageObjectExpectation,
): boolean {
  if (object.key !== expectation.key) return false;
  if (expectation.size !== undefined && object.size !== expectation.size) {
    return false;
  }
  if (
    expectation.contentType !== undefined &&
    object.contentType !== expectation.contentType
  ) {
    return false;
  }
  if (
    expectation.cacheControl !== undefined &&
    object.cacheControl !== expectation.cacheControl
  ) {
    return false;
  }
  if (
    expectation.visibility !== undefined &&
    object.visibility !== expectation.visibility
  ) {
    return false;
  }

  return partialObjectMatches(object.metadata, expectation.metadata);
}

function getOutboxMessages(
  source: OutboxMessageAssertionSource,
): readonly OutboxMessage[] {
  return "messages" in source ? source.messages : source;
}

function getIdempotencyEntries(
  source: IdempotencyEntryAssertionSource,
): readonly MemoryIdempotencyEntry[] {
  if (Array.isArray(source)) return source;
  return (source as Pick<MemoryIdempotencyStore, "entries">).entries;
}

function idempotencyEntryMatches(
  entry: MemoryIdempotencyEntry,
  expectation: IdempotencyEntryExpectation,
): boolean {
  if (
    expectation.namespace !== undefined &&
    entry.namespace !== expectation.namespace
  ) {
    return false;
  }
  if (expectation.key !== undefined && entry.key !== expectation.key) {
    return false;
  }
  if (
    expectation.scopeKey !== undefined &&
    entry.scopeKey !== expectation.scopeKey
  ) {
    return false;
  }
  if (
    expectation.fingerprint !== undefined &&
    entry.fingerprint !== expectation.fingerprint
  ) {
    return false;
  }
  if (expectation.status !== undefined && entry.status !== expectation.status) {
    return false;
  }
  if (
    expectation.result !== undefined &&
    !valueMatches(entry.result, expectation.result)
  ) {
    return false;
  }
  if (
    expectation.reservedAt !== undefined &&
    !valueMatches(entry.reservedAt, expectation.reservedAt)
  ) {
    return false;
  }
  if (
    expectation.completedAt !== undefined &&
    !valueMatches(entry.completedAt, expectation.completedAt)
  ) {
    return false;
  }
  if (
    expectation.expiresAt !== undefined &&
    !valueMatches(entry.expiresAt, expectation.expiresAt)
  ) {
    return false;
  }

  return true;
}

function outboxMessageMatches(
  message: OutboxMessage,
  expectation: OutboxMessageExpectation,
): boolean {
  if (expectation.id !== undefined && message.id !== expectation.id) {
    return false;
  }
  if (expectation.kind !== undefined && message.kind !== expectation.kind) {
    return false;
  }
  if (expectation.name !== undefined && message.name !== expectation.name) {
    return false;
  }
  if (
    expectation.status !== undefined &&
    message.status !== expectation.status
  ) {
    return false;
  }
  if (
    expectation.attempts !== undefined &&
    message.attempts !== expectation.attempts
  ) {
    return false;
  }
  if (
    expectation.maxAttempts !== undefined &&
    message.maxAttempts !== expectation.maxAttempts
  ) {
    return false;
  }
  if (
    expectation.deliveredAt !== undefined &&
    !valueMatches(message.deliveredAt, expectation.deliveredAt)
  ) {
    return false;
  }
  if (
    expectation.lastError !== undefined &&
    !valueMatches(message.lastError, expectation.lastError)
  ) {
    return false;
  }

  return expectation.payload === undefined
    ? true
    : valueMatches(message.payload, expectation.payload);
}

function outboxDrainResultMatches(
  result: DrainOutboxResult,
  expectation: OutboxDrainResultExpectation,
): boolean {
  if (
    expectation.claimed !== undefined &&
    result.claimed !== expectation.claimed
  ) {
    return false;
  }
  if (
    expectation.delivered !== undefined &&
    result.delivered !== expectation.delivered
  ) {
    return false;
  }
  if (
    expectation.retried !== undefined &&
    result.retried !== expectation.retried
  ) {
    return false;
  }
  if (
    expectation.deadLettered !== undefined &&
    result.deadLettered !== expectation.deadLettered
  ) {
    return false;
  }

  return true;
}

function hasStorageTextReader(
  object: StorageObject,
): object is StorageObject & { text(): Promise<string> } {
  return typeof (object as { text?: unknown }).text === "function";
}

function hasStorageBytesReader(
  object: StorageObject,
): object is StorageObject & { bytes(): Promise<Uint8Array> } {
  return typeof (object as { bytes?: unknown }).bytes === "function";
}

function omitStorageBodyExpectation(
  expectation: StorageObjectExpectation,
): Omit<StorageObjectExpectation, "bytes" | "text"> {
  const { bytes: _bytes, text: _text, ...metadataExpectation } = expectation;
  return metadataExpectation;
}

function partialObjectMatches(
  actual: unknown,
  expected: Record<string, unknown> | undefined,
): boolean {
  if (!expected) return true;
  if (!actual || typeof actual !== "object") return false;

  const actualRecord = actual as Record<string, unknown>;

  for (const [key, expectedValue] of Object.entries(expected)) {
    if (!valueMatches(actualRecord[key], expectedValue)) return false;
  }

  return true;
}

function valueMatches(actual: unknown, expected: unknown): boolean {
  if (actual instanceof Date || expected instanceof Date) {
    return (
      actual instanceof Date &&
      expected instanceof Date &&
      actual.getTime() === expected.getTime()
    );
  }

  if (actual instanceof Uint8Array || expected instanceof Uint8Array) {
    return uint8ArrayMatches(actual, expected);
  }

  if (
    expected &&
    typeof expected === "object" &&
    !Array.isArray(expected) &&
    actual &&
    typeof actual === "object" &&
    !Array.isArray(actual)
  ) {
    return partialObjectMatches(actual, expected as Record<string, unknown>);
  }

  if (Array.isArray(expected) || Array.isArray(actual)) {
    return JSON.stringify(actual) === JSON.stringify(expected);
  }

  return Object.is(actual, expected);
}

function uint8ArrayMatches(actual: unknown, expected: unknown): boolean {
  if (!(actual instanceof Uint8Array) || !(expected instanceof Uint8Array)) {
    return false;
  }
  if (actual.byteLength !== expected.byteLength) return false;

  return actual.every((byte, index) => byte === expected[index]);
}

function pluralize(word: string, count: number): string {
  return count === 1 ? word : `${word}s`;
}

function formatTestingExpectation(expectation: unknown): string {
  return JSON.stringify(expectation);
}

function formatAuditExpectation(expectation: AuditLogEntryExpectation): string {
  return formatTestingExpectation(expectation);
}