import {
  createMemoryErrorReporter,
  type MemoryErrorReporterPort,
} from "../error-reporting/index.js";
import { createMemoryFlags, type MemoryFlagsPort } from "../flags/index.js";
import {
  createMemoryIdempotencyStore,
  type MemoryIdempotencyStore,
} from "../idempotency/index.js";
import { createMemoryLocks, type MemoryLocksPort } from "../locks/index.js";
import { createMemoryMailer, type MemoryMailerPort } from "../mail/index.js";
import {
  createMemoryNotificationPort,
  type MemoryNotificationPort,
} from "../notifications/index.js";
import {
  createMemoryOutbox,
  createOutboxEventRecorder,
  type MemoryOutboxPort,
} from "../outbox/index.js";
import {
  createMemoryPayments,
  type MemoryPaymentsPort,
} from "../payments/index.js";
import {
  type ActivityActor,
  type ActivityTenant,
  type AuditLogPort,
  createMemoryAuditLog,
  type MemoryAuditLogPort,
} from "../ports/audit.js";
import type {
  BestEffortWork,
  BestEffortWorkPort,
} from "../ports/best-effort-work.js";
import { type CachePort, createMemoryCache } from "../ports/cache.js";
import {
  type ClockPort,
  createFrozenClock,
  createSystemClock,
} from "../ports/clock.js";
import type { EventBusPort, JobDispatcherPort } from "../ports/events.js";
import {
  createUuidIdGenerator,
  type IdGeneratorPort,
} from "../ports/id-generator.js";
import {
  type AnyPorts,
  type BoundGate,
  type BufferedDomainEventRecorder,
  createGate,
  createMemoryRateLimiter,
  createMemoryStorage,
  createNoopLogger,
  createNoopUnitOfWork,
  type GatePort,
  type LoggerPort,
  type RateLimitPort,
  type StoragePort,
  type UnitOfWorkPort,
} from "../ports/index.js";
import {
  createRecordingBestEffortWork,
  createRecordingEventBus,
  createRecordingJobDispatcher,
  createTestActivityContext,
  createTestSystemActor,
  createTestUserActor,
  type RecordedEvent,
  type RecordedJobDispatch,
} from "../ports/testing.js";
import type {
  AnyServiceProvider,
  ProviderSetupResult,
} from "../providers/provider.js";
import { createMemorySearch, type MemorySearchPort } from "../search/index.js";
import { createAmbientAuditLog } from "../server/audit-context.js";
import { loadProviderConfig } from "../server/providers/loadProviderConfig.js";
import {
  clearActiveRequestContext,
  enterActiveRequestContext,
} from "../server/request-context.js";

export * from "../ports/testing.js";

/**
 * Value that may be returned synchronously or asynchronously.
 */
export type MaybePromise<T> = T | Promise<T>;

/**
 * Common Beignet ports created by `createTestPorts(...)`.
 */
export interface CommonTestPorts<TxPorts = AnyPorts> {
  [key: string]: unknown;
  /**
   * Ambient-enriched in-memory audit log. Entries recorded through this port
   * inherit actor, tenant, request ID, and trace ID from the active request
   * context, matching production server behavior. Use the fixture's `audit`
   * for entry assertions.
   */
  audit: AuditLogPort;
  /**
   * In-memory string cache.
   */
  cache: CachePort;
  /**
   * Mutable deterministic test clock.
   */
  clock: ClockPort;
  /**
   * Recording event bus.
   */
  eventBus: EventBusPort;
  /**
   * Recording non-durable best-effort work.
   */
  bestEffortWork: BestEffortWorkPort;
  /**
   * In-memory error reporter.
   */
  errorReporter: MemoryErrorReporterPort;
  /**
   * Authorization gate. Apps with real policies should provide their own gate
   * through `overrides`.
   */
  gate: GatePort<unknown, []>;
  /**
   * In-memory feature flags.
   */
  flags: MemoryFlagsPort;
  /**
   * In-memory idempotency store.
   */
  idempotency: MemoryIdempotencyStore;
  /**
   * Sequence or UUID generator used by tests.
   */
  ids: IdGeneratorPort;
  /**
   * Recording job dispatcher.
   */
  jobs: JobDispatcherPort;
  /**
   * No-op logger.
   */
  logger: LoggerPort;
  /**
   * In-memory lease-backed locks.
   */
  locks: MemoryLocksPort;
  /**
   * In-memory mailer.
   */
  mailer: MemoryMailerPort;
  /**
   * In-memory notification port.
   */
  notifications: MemoryNotificationPort;
  /**
   * In-memory outbox.
   */
  outbox: MemoryOutboxPort;
  /**
   * In-memory payments port.
   */
  payments: MemoryPaymentsPort;
  /**
   * In-memory search port.
   */
  search: MemorySearchPort;
  /**
   * In-memory rate limiter.
   */
  rateLimit: RateLimitPort;
  /**
   * In-memory object storage.
   */
  storage: StoragePort;
  /**
   * No-op Unit of Work over the configured transaction ports.
   */
  uow: UnitOfWorkPort<TxPorts>;
}

/**
 * Captured state and ports returned by `createTestPorts(...)`.
 */
export interface TestPortsFixture<
  Ports extends AnyPorts = CommonTestPorts,
  TxPorts = Ports,
> {
  /**
   * App ports to pass into a use-case or route test context.
   */
  ports: Ports;
  /**
   * Memory audit port, exposed for assertions.
   */
  audit: MemoryAuditLogPort;
  /**
   * Recording event bus port.
   */
  eventBus: EventBusPort;
  /**
   * Recording best-effort-work port.
   */
  bestEffortWork: BestEffortWorkPort;
  /**
   * Work currently waiting in the recording best-effort-work adapter.
   */
  pendingBestEffortWork: BestEffortWork[];
  /**
   * Run the currently pending best-effort-work batch in FIFO order.
   */
  flushBestEffortWork(): Promise<void>;
  /**
   * Recorded events published through `eventBus`.
   */
  events: RecordedEvent[];
  /**
   * Memory error reporter, exposed for assertions.
   */
  errorReporter: MemoryErrorReporterPort;
  /**
   * Recording job dispatcher port.
   */
  jobs: JobDispatcherPort;
  /**
   * Jobs dispatched through `jobs`.
   */
  dispatchedJobs: RecordedJobDispatch[];
  /**
   * Memory mailer, exposed for delivery assertions.
   */
  mailer: MemoryMailerPort;
  /**
   * Memory notification port, exposed for delivery assertions.
   */
  notifications: MemoryNotificationPort;
  /**
   * Memory outbox, exposed for durable workflow assertions.
   */
  outbox: MemoryOutboxPort;
  /**
   * Memory payments port.
   */
  payments: MemoryPaymentsPort;
  /**
   * Memory storage port.
   */
  storage: StoragePort;
  /**
   * Memory idempotency store.
   */
  idempotency: MemoryIdempotencyStore;
  /**
   * Test cache port.
   */
  cache: CachePort;
  /**
   * Test feature flags port.
   */
  flags: MemoryFlagsPort;
  /**
   * Test rate-limit port.
   */
  rateLimit: RateLimitPort;
  /**
   * Test logger port.
   */
  logger: LoggerPort;
  /**
   * Test locks port.
   */
  locks: MemoryLocksPort;
  /**
   * Test search port.
   */
  search: MemorySearchPort;
  /**
   * Test clock port.
   */
  clock: ClockPort;
  /**
   * Test ID generator.
   */
  ids: IdGeneratorPort;
  /**
   * Unit of Work port installed on `ports`.
   */
  uow: UnitOfWorkPort<TxPorts>;
}

/**
 * Unit of Work behavior for `createTestPorts(...)`.
 */
export interface CreateTestPortsTransactionOptions<
  Ports extends AnyPorts,
  TxPorts,
> {
  /**
   * Transaction-scoped ports or a function that derives them from final ports.
   *
   * Defaults to the final `ports` object.
   */
  ports?: TxPorts | ((ports: Ports) => TxPorts);
  /**
   * Hook run after the transaction callback resolves.
   */
  afterCommit?: (tx: TxPorts) => MaybePromise<void>;
  /**
   * Hook run after the transaction callback throws.
   */
  afterRollback?: (error: unknown, tx: TxPorts) => MaybePromise<void>;
  /**
   * Enqueue `tx.events` (a buffered domain event recorder) to `ports.outbox`
   * after the transaction commits, and clear it after a rollback.
   *
   * Requires `transaction.ports` to include an `events` recorder created by
   * `createDomainEventRecorder()`.
   */
  outbox?: boolean;
}

/**
 * Override shape for one test port.
 *
 * Object-valued ports may be supplied one level deep as partials: the missing
 * members are completed behind a proxy that throws a named error on use.
 * Function-valued ports must be supplied whole, so the check for functions
 * happens before the object branch.
 */
export type TestPortOverride<Port> = Port extends (...args: never[]) => unknown
  ? Port
  : Port extends object
    ? Partial<Port>
    : Port;

/**
 * Typed partial port overrides accepted by `createTestPorts(...)` and
 * `createTestContext(...)`.
 */
export type TestPortsOverrides<Ports extends AnyPorts> = {
  [K in keyof Ports]?: TestPortOverride<Ports[K]>;
};

/**
 * Options for `createTestPorts(...)`.
 */
export interface CreateTestPortsOptions<
  Ports extends AnyPorts,
  TxPorts = Ports,
> {
  /**
   * App-owned default ports, usually imported from `infra/port-wiring`. Common
   * Beignet test defaults replace matching keys from `base`; use `overrides`
   * for app ports that should win.
   *
   * Values are intentionally loose: boot-time app ports may bind wider types
   * (or deferred placeholders) than the provider-contributed runtime ports.
   */
  base?: { [K in keyof Ports]?: unknown };
  /**
   * Test-specific ports that should replace generated defaults or `base`.
   *
   * Plain-object ports may be supplied as one-level-deep partials; missing
   * members throw a named error when called. Function-valued ports, class
   * instances, and other exotic objects are passed through whole.
   */
  overrides?: TestPortsOverrides<Ports>;
  /**
   * Clock implementation. Defaults to a frozen clock at the Unix epoch.
   */
  clock?: ClockPort;
  /**
   * ID generator. Defaults to UUIDs.
   */
  ids?: IdGeneratorPort;
  /**
   * Unit of Work configuration. A no-op UOW is installed by default.
   */
  transaction?: CreateTestPortsTransactionOptions<Ports, TxPorts>;
}

/**
 * Create standard memory/fake Beignet ports for tests.
 *
 * Use this as the starting point for use-case and route tests, then layer
 * app-owned repositories or provider fakes through `base` and `overrides`.
 *
 * @param options - Optional app ports, overrides, and Unit of Work behavior.
 * @returns Ports plus captured state for assertions.
 */
export function createTestPorts<
  Ports extends AnyPorts = CommonTestPorts,
  TxPorts = Ports,
>(
  options: CreateTestPortsOptions<Ports, TxPorts> = {},
): TestPortsFixture<Ports, TxPorts> {
  const audit = createMemoryAuditLog();
  const cache = createMemoryCache();
  const clock = options.clock ?? createFrozenClock();
  const {
    bestEffortWork,
    pending: pendingBestEffortWork,
    flush: flushBestEffortWork,
  } = createRecordingBestEffortWork();
  const { bus: eventBus, events } = createRecordingEventBus();
  const errorReporter = createMemoryErrorReporter();
  const flags = createMemoryFlags();
  const { jobs, dispatchedJobs } = createRecordingJobDispatcher();
  const ids = options.ids ?? createUuidIdGenerator();
  const idempotency = createMemoryIdempotencyStore({
    now: () => clock.now(),
  });
  const logger = createNoopLogger();
  const locks = createMemoryLocks({
    now: () => clock.now(),
    sleep: createClockAwareSleep(clock),
  });
  const mailer = createMemoryMailer();
  const notifications = createMemoryNotificationPort();
  const outbox = createMemoryOutbox({
    id: () => ids.nextId(),
    now: () => clock.now(),
  });
  const payments = createMemoryPayments();
  const rateLimit = createMemoryRateLimiter();
  const search = createMemorySearch();
  const storage = createMemoryStorage();

  const defaultPorts = {
    audit: createAmbientAuditLog(audit),
    cache,
    clock,
    bestEffortWork,
    eventBus,
    errorReporter,
    flags,
    gate: createGate({ policies: [] }),
    idempotency,
    ids,
    jobs,
    logger,
    locks,
    mailer,
    notifications,
    outbox,
    payments,
    rateLimit,
    search,
    storage,
  };
  const overrides = completeTestPortOverrides(options.overrides);
  // The kit's one sanctioned cast: boot-time bases and completed partial
  // overrides are widened to the app's full Ports shape.
  const portsWithoutUow = {
    ...options.base,
    ...defaultPorts,
    ...overrides,
  } as unknown as Ports;
  const transactionOptions = options.transaction;
  const flushEventsToOutbox = transactionOptions?.outbox === true;
  const resolveTransactionPorts = (): TxPorts => {
    const txPorts = transactionOptions?.ports;
    if (typeof txPorts === "function") {
      return (txPorts as (ports: Ports) => TxPorts)(portsWithoutUow);
    }

    return txPorts ?? (portsWithoutUow as unknown as TxPorts);
  };
  const generatedUow = createNoopUnitOfWork<TxPorts>(
    () => {
      const tx = resolveTransactionPorts();
      if (flushEventsToOutbox) {
        // Validate up front so both commit and rollback paths fail loudly.
        resolveBufferedTransactionEvents(tx);
      }

      return tx;
    },
    {
      afterCommit: async (tx) => {
        if (flushEventsToOutbox) {
          const events = resolveBufferedTransactionEvents(tx);
          const outbox = (
            portsWithoutUow as unknown as {
              outbox: MemoryOutboxPort;
            }
          ).outbox;
          const outboxRecorder = createOutboxEventRecorder(outbox);
          await events.flush({
            publish(event, payload, options) {
              return outboxRecorder.record(event, payload, options);
            },
            subscribe() {
              throw new Error(
                "The test outbox event publisher does not support subscriptions.",
              );
            },
          });
        }
        await transactionOptions?.afterCommit?.(tx);
      },
      afterRollback: async (error, tx) => {
        if (flushEventsToOutbox) {
          resolveBufferedTransactionEvents(tx).clear();
        }
        await transactionOptions?.afterRollback?.(error, tx);
      },
    },
  );
  // The kit consumes `uow` directly, so keep the supplied port's identity
  // instead of the completed override.
  const uow =
    (options.overrides as { uow?: UnitOfWorkPort<TxPorts> } | undefined)?.uow ??
    generatedUow;
  const ports = {
    ...portsWithoutUow,
    uow,
  } as Ports;

  return {
    ports,
    audit,
    bestEffortWork,
    pendingBestEffortWork,
    flushBestEffortWork,
    eventBus,
    events,
    errorReporter,
    jobs,
    dispatchedJobs,
    mailer,
    notifications,
    outbox,
    payments,
    storage,
    idempotency,
    cache,
    flags,
    rateLimit,
    logger,
    locks,
    search,
    clock,
    ids,
    uow,
  };
}

function isPlainObject(value: unknown): value is Record<string, unknown> {
  if (value === null || typeof value !== "object") return false;
  const proto = Object.getPrototypeOf(value);

  return proto === Object.prototype || proto === null;
}

function createClockAwareSleep(
  clock: ClockPort,
): (ms: number) => Promise<void> {
  const maybeFrozenClock = clock as Partial<{ advance(ms: number): void }>;
  if (typeof maybeFrozenClock.advance === "function") {
    return async (ms) => {
      maybeFrozenClock.advance?.(ms);
    };
  }

  return (ms) => new Promise((resolve) => setTimeout(resolve, ms));
}

function createMissingTestPortMember(
  portKey: string,
  member: string,
): () => never {
  const memberName = `${portKey}.${member}`;
  const throwMissingMember = () => {
    throw new Error(
      `Test port "${memberName}" was called but not provided. Pass it through createTestPorts overrides.`,
    );
  };
  Object.defineProperty(throwMissingMember, "name", {
    value: memberName,
    configurable: true,
  });

  return throwMissingMember;
}

function completeTestPortOverride(portKey: string, value: unknown): unknown {
  // Functions are objects: the function check must run first so
  // function-valued ports pass through whole.
  if (typeof value === "function") return value;
  // Class instances (repositories, Maps, gates built from classes) rely on
  // internal slots or prototype identity, so only plain objects are completed.
  if (!isPlainObject(value)) return value;

  return new Proxy(value, {
    get(target, member, receiver) {
      if (
        typeof member === "symbol" ||
        member === "then" ||
        member === "constructor" ||
        member in target
      ) {
        return Reflect.get(target, member, receiver);
      }

      return createMissingTestPortMember(portKey, member);
    },
  });
}

function completeTestPortOverrides<Ports extends AnyPorts>(
  overrides: TestPortsOverrides<Ports> | undefined,
): Record<string, unknown> {
  const completed: Record<string, unknown> = {};
  for (const [key, value] of Object.entries(overrides ?? {})) {
    if (value === undefined) continue;
    completed[key] = completeTestPortOverride(key, value);
  }

  return completed;
}

function resolveBufferedTransactionEvents(
  tx: unknown,
): Pick<BufferedDomainEventRecorder, "entries" | "clear" | "flush"> {
  const events =
    tx && typeof tx === "object"
      ? (tx as { events?: unknown }).events
      : undefined;
  const recorder = events as
    | Partial<Pick<BufferedDomainEventRecorder, "entries" | "clear" | "flush">>
    | undefined;
  if (
    !recorder ||
    typeof recorder.entries !== "function" ||
    typeof recorder.clear !== "function" ||
    typeof recorder.flush !== "function"
  ) {
    throw new Error(
      "createTestPorts transaction.outbox requires tx.events to be a buffered domain event recorder. " +
        "Add events: createDomainEventRecorder() to transaction.ports.",
    );
  }

  return recorder as Pick<
    BufferedDomainEventRecorder,
    "entries" | "clear" | "flush"
  >;
}

/**
 * Bound gate type inferred from a ports object.
 */
export type TestBoundGateForPorts<Ports extends AnyPorts> = Ports extends {
  gate?: { bind: (ctx: never) => infer Bound };
}
  ? Bound
  : BoundGate<readonly []>;

/**
 * Minimal gate shape used by `createTestContextFactory(...)`.
 */
type TestGatePort<BoundGateValue = BoundGate<readonly []>> = {
  /**
   * Bind the gate to a context.
   */
  bind: (ctx: never) => BoundGateValue;
};

/**
 * Common app context fields built by `createTestContextFactory(...)`.
 */
export type TestContextFields<Ports extends AnyPorts> = {
  /**
   * Actor under test.
   */
  actor: ActivityActor;
  /**
   * Optional tenant under test.
   */
  tenant?: ActivityTenant;
  /**
   * Stable request ID.
   */
  requestId: string;
  /**
   * Stable trace ID.
   */
  traceId?: string;
  /**
   * Optional auth/session value.
   */
  auth?: unknown;
  /**
   * App ports under test.
   */
  ports: Ports;
  /**
   * Bound authorization gate when `ports.gate` is available.
   */
  gate?: TestBoundGateForPorts<Ports>;
};

/**
 * Options for `createTestContextFactory(...)`.
 */
export interface CreateTestContextFactoryOptions<Ctx, Ports extends AnyPorts> {
  /**
   * Ports or a callback returning ports for each created context.
   */
  ports: Ports | (() => Ports);
  /**
   * Actor used by default.
   *
   * @default createTestUserActor()
   */
  actor?: ActivityActor;
  /**
   * Tenant used by default. Pass `null` to omit tenant context.
   */
  tenant?: ActivityTenant | null;
  /**
   * Request ID used by default.
   */
  requestId?: string;
  /**
   * Trace ID used by default.
   */
  traceId?: string;
  /**
   * Auth/session value added to the context.
   */
  auth?: unknown;
  /**
   * Additional app-specific context fields.
   */
  extra?: Partial<Ctx> | ((args: TestContextFields<Ports>) => Partial<Ctx>);
}

/**
 * Per-call overrides for a test context factory.
 */
export type TestContextFactoryOverrides<Ctx, Ports extends AnyPorts> = Partial<
  Pick<
    CreateTestContextFactoryOptions<Ctx, Ports>,
    "actor" | "tenant" | "requestId" | "traceId" | "auth" | "extra"
  >
> & {
  /**
   * Ports for this context call.
   */
  ports?: Ports;
};

/**
 * Create a repeatable app context factory for use-case and route tests.
 *
 * The factory adds actor, tenant, request ID, trace ID, ports, and auth fields,
 * and attaches a live `ctx.gate` automatically when `ports.gate` exposes
 * `bind(...)`. The gate is attached after all `extra` and override fields are
 * merged, so it always authorizes against the final context identity.
 *
 * @param options - Default context values.
 * @returns A function that creates one app context.
 */
export function createTestContextFactory<
  Ctx extends object,
  Ports extends AnyPorts,
>(
  options: CreateTestContextFactoryOptions<Ctx, Ports>,
): (overrides?: TestContextFactoryOverrides<Ctx, Ports>) => Ctx {
  return (overrides = {}) => {
    const ports = overrides.ports ?? resolvePorts(options.ports);
    const activity = createTestActivityContext({
      actor: overrides.actor ?? options.actor ?? createTestUserActor(),
      tenant:
        overrides.tenant !== undefined ? overrides.tenant : options.tenant,
      requestId: overrides.requestId ?? options.requestId,
      traceId: overrides.traceId ?? options.traceId,
    });
    const auth =
      overrides.auth !== undefined ? overrides.auth : (options.auth ?? null);
    const base = {
      ...activity,
      auth,
      ports,
    } as TestContextFields<Ports>;
    const fields = attachTestGate(ports, base);
    const extra = resolveExtra(options.extra, fields);
    const overrideExtra = resolveExtra(overrides.extra, fields);
    const merged = {
      ...fields,
      ...extra,
      ...overrideExtra,
    };

    if (Object.hasOwn(merged, "gate")) {
      return merged as Ctx;
    }

    return attachTestGate(ports, merged) as Ctx;
  };
}

function resolvePorts<Ports extends AnyPorts>(
  ports: Ports | (() => Ports),
): Ports {
  return typeof ports === "function" ? (ports as () => Ports)() : ports;
}

function resolveExtra<Ctx, Ports extends AnyPorts>(
  extra:
    | Partial<Ctx>
    | ((args: TestContextFields<Ports>) => Partial<Ctx>)
    | undefined,
  fields: TestContextFields<Ports>,
): Partial<Ctx> {
  return typeof extra === "function" ? extra(fields) : (extra ?? {});
}

function attachTestGate<Ports extends AnyPorts, Ctx extends object>(
  ports: Ports,
  ctx: Ctx,
): Ctx {
  const maybeGate = (
    ports as { gate?: TestGatePort<TestBoundGateForPorts<Ports>> }
  ).gate;
  if (!maybeGate) return ctx;

  // Mirror GatePort.attach: a live, non-enumerable getter that re-binds
  // against the receiver so in-place identity changes are never stale and
  // spread copies drop the gate loudly.
  Object.defineProperty(ctx, "gate", {
    configurable: true,
    enumerable: false,
    get() {
      return maybeGate.bind(this as never);
    },
  });

  return ctx;
}

const disposeSymbol: typeof Symbol.dispose =
  Symbol.dispose ?? (Symbol.for("Symbol.dispose") as typeof Symbol.dispose);

/**
 * Options for `createTestContext(...)`.
 */
export interface CreateTestContextOptions<Ctx, Ports extends AnyPorts> {
  /**
   * Actor under test.
   *
   * @default createTestSystemActor("test-system")
   */
  actor?: ActivityActor;
  /**
   * Tenant under test. Pass `null` to omit tenant context.
   *
   * @default createTestTenant()
   */
  tenant?: ActivityTenant | null;
  /**
   * Request ID exposed on the context.
   *
   * @default "test-request"
   */
  requestId?: string;
  /**
   * Trace ID exposed on the context.
   *
   * @default "test-trace"
   */
  traceId?: string;
  /**
   * Auth/session value added to the context.
   *
   * @default null
   */
  auth?: unknown;
  /**
   * App-owned default ports, usually imported from `infra/port-wiring`. Common
   * Beignet test defaults replace matching keys from `base`; use `ports` for
   * app ports that should win.
   */
  base?: { [K in keyof Ports]?: unknown };
  /**
   * Typed partial port overrides that replace generated defaults or `base`.
   */
  ports?: TestPortsOverrides<Ports>;
  /**
   * Clock implementation. Defaults to a frozen clock at the Unix epoch.
   */
  clock?: ClockPort;
  /**
   * ID generator. Defaults to UUIDs.
   */
  ids?: IdGeneratorPort;
  /**
   * Unit of Work configuration. A no-op UOW is installed by default.
   */
  transaction?: CreateTestPortsTransactionOptions<Ports, unknown>;
  /**
   * Additional app-specific context fields. An explicit `gate` here wins over
   * the kit-attached gate.
   */
  extra?: Partial<Ctx>;
  /**
   * Enter the ambient request context with the test actor, tenant, request
   * ID, and trace ID so ambient enrichment (such as the default audit port)
   * works like production. Call `dispose()` (or use `using`) to clear it.
   *
   * @default true
   */
  ambient?: boolean;
}

/**
 * Test context fixture returned by `createTestContext(...)`.
 */
export interface TestContextFixture<Ctx, Ports extends AnyPorts>
  extends TestPortsFixture<Ports, unknown> {
  /**
   * Assembled app context under test.
   */
  ctx: Ctx;
  /**
   * Clear the ambient request context entered by this fixture.
   */
  dispose(): void;
  /**
   * Explicit-resource-management alias for `dispose()`, so fixtures work with
   * `using`.
   */
  [disposeSymbol](): void;
}

/**
 * Create a one-call test context fixture for jobs, listeners, schedules,
 * notifications, payments, tasks, and use-case tests.
 *
 * The fixture builds common memory ports through `createTestPorts(...)`,
 * assembles an app context with actor, tenant, request ID, trace ID, auth,
 * and a live bound gate, and enters the ambient request context so ambient
 * enrichment matches production. Reading an app port that is neither a kit
 * default nor supplied throws a named error on use.
 *
 * @example
 * ```ts
 * const makeContext = createTestContext<AppContext>();
 *
 * using fixture = makeContext({
 *   ports: { issues: { create: async (input) => issueRecord(input) } },
 * });
 * await runCreateIssue(fixture.ctx);
 * ```
 *
 * @returns A factory that creates one disposable test context fixture.
 */
export function createTestContext<Ctx extends { ports: AnyPorts }>(): (
  options?: CreateTestContextOptions<Ctx, Ctx["ports"]>,
) => TestContextFixture<Ctx, Ctx["ports"]> {
  return (options = {}) => {
    const fixture = createTestPorts<Ctx["ports"], unknown>({
      base: options.base,
      overrides: options.ports,
      clock: options.clock,
      ids: options.ids,
      transaction: options.transaction,
    });
    const ports = createUnboundPortGuard(fixture.ports);
    const activity = createTestActivityContext({
      actor: options.actor ?? createTestSystemActor("test-system"),
      tenant: options.tenant,
      requestId: options.requestId,
      traceId: options.traceId,
    });
    const auth = options.auth !== undefined ? options.auth : null;
    const base = {
      ...activity,
      auth,
      ports,
    } as TestContextFields<Ctx["ports"]>;
    const fields = attachTestGate(ports, base);
    const merged = {
      ...fields,
      ...(options.extra ?? {}),
    };
    const ctx = (Object.hasOwn(merged, "gate")
      ? merged
      : attachTestGate(ports, merged)) as unknown as Ctx;

    const ambient = options.ambient ?? true;
    if (ambient) {
      enterActiveRequestContext({
        requestId: activity.requestId,
        traceId: activity.traceId,
        actor: activity.actor,
        tenant: activity.tenant,
      });
    }
    let disposed = false;
    const dispose = () => {
      if (!ambient || disposed) return;
      disposed = true;
      clearActiveRequestContext();
    };

    return {
      ...fixture,
      ports,
      ctx,
      dispose,
      [disposeSymbol]: dispose,
    };
  };
}

function createUnboundPortGuard<Ports extends AnyPorts>(ports: Ports): Ports {
  return new Proxy(ports, {
    get(target, key, receiver) {
      if (
        typeof key === "symbol" ||
        key === "then" ||
        key === "constructor" ||
        key in target
      ) {
        return Reflect.get(target, key, receiver);
      }

      throw new Error(
        `App port "${key}" is not bound in this test context. Pass it through createTestContext ports.`,
      );
    },
  });
}

/**
 * Options for `installProviderForTest(...)`.
 */
export interface InstallProviderForTestOptions {
  /**
   * Environment variables used to resolve the provider's config through the
   * same loader the server uses (including the provider's `overrides`).
   * Ignored when `config` is passed explicitly.
   */
  env?: Record<string, string | undefined>;
  /**
   * Base app ports visible to provider setup, such as `{ devtools }`.
   */
  ports?: Record<string, unknown>;
  /**
   * Config passed to provider setup as-is. The provider config schema is not
   * run, matching server startup where config is validated before setup.
   */
  config?: unknown;
  /**
   * Service-context factory passed to setup and lifecycle hooks. The default
   * factory rejects, mirroring the late-bound runtime factory before all
   * providers have started.
   */
  createServiceContext?: (input?: unknown) => Promise<unknown>;
}

/**
 * Installed provider fixture returned by `installProviderForTest(...)`.
 */
export interface InstalledProviderFixture {
  /**
   * Base ports merged with the ports contributed by provider setup.
   */
  ports: Record<string, unknown>;
  /**
   * Raw provider setup result, exposed for assertions on contributed ports
   * and optional lifecycle hooks.
   */
  result: ProviderSetupResult<Record<string, unknown>, Record<string, unknown>>;
  /**
   * Run the provider `start` hook when the provider declares one.
   */
  start(): Promise<void>;
  /**
   * Run the provider `stop` hook when the provider declares one.
   */
  stop(): Promise<void>;
}

/**
 * Run provider setup against test ports and return the merged ports plus
 * lifecycle runners.
 *
 * Use this in provider tests instead of hand-rolling the setup, port merge,
 * and lifecycle plumbing around `provider.setup(...)`.
 *
 * @param provider - Provider under test.
 * @param options - Base ports, raw config, and service-context factory.
 * @returns Merged ports, the raw setup result, and start/stop runners.
 */
export async function installProviderForTest(
  provider: AnyServiceProvider,
  options: InstallProviderForTestOptions = {},
): Promise<InstalledProviderFixture> {
  const basePorts: Record<string, unknown> = { ...options.ports };
  const createServiceContext =
    options.createServiceContext ??
    (async () => {
      throw new Error(
        `Provider "${provider.name}" called createServiceContext during a test install. ` +
          "Pass createServiceContext to installProviderForTest(...) when the test needs a service context.",
      );
    });
  const config =
    options.config !== undefined
      ? options.config
      : await loadProviderConfig(provider, options.env ?? {}, {});
  const result = await provider.setup({
    ports: basePorts,
    config,
    createServiceContext,
  });
  const ports: Record<string, unknown> = { ...basePorts, ...result.ports };
  const lifecycleCtx = { ports, createServiceContext };

  return {
    ports,
    result,
    async start() {
      await result.start?.(lifecycleCtx);
    },
    async stop() {
      await result.stop?.(lifecycleCtx);
    },
  };
}

/**
 * Arguments passed to a factory default builder.
 */
export interface FactoryBuildArgs<Name extends string = string> {
  /**
   * Factory name.
   */
  readonly name: Name;
  /**
   * Current sequence value.
   */
  readonly sequence: number;
  /**
   * ID generator available to the factory.
   */
  readonly ids: IdGeneratorPort;
  /**
   * Clock available to the factory.
   */
  readonly clock: ClockPort;
}

/**
 * Arguments passed to a factory persistence function.
 */
export type FactoryPersistArgs<Name extends string = string> =
  FactoryBuildArgs<Name>;

/**
 * Overrides accepted by factory build and create methods.
 */
export type FactoryOverrides<
  Value extends object,
  Name extends string = string,
> =
  | Partial<Value>
  | ((value: Value, args: FactoryBuildArgs<Name>) => Partial<Value>);

/**
 * Options for declaring a test data factory.
 */
export interface CreateFactoryOptions<
  Name extends string,
  Value extends object,
  Ctx = unknown,
  Created = Value,
> {
  /**
   * Build the default value for the current sequence.
   */
  defaults(args: FactoryBuildArgs<Name>): Value;
  /**
   * Persist a built value and return the created record.
   */
  persist?(
    ctx: Ctx,
    value: Value,
    args: FactoryPersistArgs<Name>,
  ): MaybePromise<Created>;
  /**
   * ID generator to expose to the factory.
   */
  ids?: IdGeneratorPort;
  /**
   * Clock to expose to the factory.
   */
  clock?: ClockPort;
  /**
   * Initial sequence number. Defaults to `1`.
   */
  start?: number;
}

/**
 * Declared test data factory.
 */
export interface FactoryDef<
  Name extends string = string,
  Value extends object = object,
  Ctx = unknown,
  Created = Value,
> {
  /**
   * Discriminator for factory definitions.
   */
  readonly kind: "factory";
  /**
   * Factory name.
   */
  readonly name: Name;
  /**
   * Build an in-memory value.
   */
  build(overrides?: FactoryOverrides<Value, Name>): Value;
  /**
   * Build multiple in-memory values.
   */
  buildList(count: number, overrides?: FactoryOverrides<Value, Name>): Value[];
  /**
   * Build and persist a value.
   */
  create(ctx: Ctx, overrides?: FactoryOverrides<Value, Name>): Promise<Created>;
  /**
   * Build and persist multiple values.
   */
  createList(
    ctx: Ctx,
    count: number,
    overrides?: FactoryOverrides<Value, Name>,
  ): Promise<Created[]>;
  /**
   * Reset the factory sequence.
   */
  resetSequence(next?: number): void;
}

/**
 * Options for declaring a seed.
 */
export interface DefineSeedOptions<Ctx> {
  /**
   * Optional human-readable seed description.
   */
  description?: string;
  /**
   * Execute the seed.
   */
  run(ctx: Ctx): MaybePromise<void>;
}

/**
 * Declared seed that can be run with `runSeeds(...)`.
 */
export interface SeedDef<Ctx = unknown, Name extends string = string> {
  /**
   * Discriminator for seed definitions.
   */
  readonly kind: "seed";
  /**
   * Seed name.
   */
  readonly name: Name;
  /**
   * Optional human-readable seed description.
   */
  readonly description?: string;
  /**
   * Execute the seed.
   */
  run(ctx: Ctx): MaybePromise<void>;
}

/**
 * Options for running a list of seeds.
 */
export interface RunSeedsOptions<Ctx> {
  /**
   * Context passed to every seed.
   */
  ctx: Ctx;
  /**
   * Seeds to run in order.
   */
  seeds: readonly SeedDef<Ctx>[];
}

/**
 * Options for creating a database test harness.
 */
export interface CreateDatabaseTestHarnessOptions<Database, Ctx> {
  /**
   * Create an isolated app-owned database fixture.
   */
  create(): MaybePromise<Database>;
  /**
   * Build the context passed to factories and seeds from the database fixture.
   */
  ctx(database: Database): Ctx;
  /**
   * Reset the database fixture, when supported by the app.
   */
  reset?(database: Database): MaybePromise<void>;
  /**
   * Close and dispose the database fixture.
   */
  close?(database: Database): MaybePromise<void>;
  /**
   * Factories whose sequences should reset before each setup and reset.
   */
  factories?: readonly Pick<FactoryDef, "resetSequence">[];
  /**
   * Default seeds available to `setup({ seed: true })` and `session.runSeeds()`.
   */
  seeds?: readonly SeedDef<Ctx>[];
}

/**
 * Options for setting up one database test session.
 */
export interface DatabaseTestSetupOptions<Ctx> {
  /**
   * Run seeds after creating the database session.
   *
   * Pass `true` to use the harness default seeds, or pass an explicit seed
   * list for this setup.
   */
  seed?: boolean | readonly SeedDef<Ctx>[];
}

/**
 * Active database fixture created by `createDatabaseTestHarness(...)`.
 */
export interface DatabaseTestSession<Database, Ctx> {
  /**
   * App-owned database fixture returned by the harness `create` function.
   */
  readonly database: Database;
  /**
   * Context built from the database fixture for factories and seeds.
   */
  readonly ctx: Ctx;
  /**
   * Run the default harness seeds or an explicit seed list.
   */
  runSeeds(seeds?: readonly SeedDef<Ctx>[]): Promise<void>;
  /**
   * Reset factory sequences and the database fixture.
   */
  reset(): Promise<void>;
  /**
   * Close and dispose this session.
   */
  close(): Promise<void>;
}

/**
 * Database test harness that coordinates app-owned database fixtures with
 * Beignet factories and seeds.
 */
export interface DatabaseTestHarness<Database, Ctx> {
  /**
   * Create one isolated database test session.
   */
  setup(
    options?: DatabaseTestSetupOptions<Ctx>,
  ): Promise<DatabaseTestSession<Database, Ctx>>;
  /**
   * Reset all configured factory sequences.
   */
  resetFactories(): void;
  /**
   * Close all sessions that have not already been closed.
   */
  cleanup(): Promise<void>;
}

/**
 * Error thrown when a seed fails.
 */
export class SeedRunError extends Error {
  /**
   * Seed that failed.
   */
  readonly seed: SeedDef;
  /**
   * Original thrown value.
   */
  readonly cause: unknown;

  constructor(seed: SeedDef, cause: unknown) {
    super(`Seed "${seed.name}" failed: ${errorMessage(cause)}`);
    this.name = "SeedRunError";
    this.seed = seed;
    this.cause = cause;
  }
}

/**
 * Reset one or more factory sequences to their configured starting values.
 */
export function resetFactories(
  ...factories: readonly Pick<FactoryDef, "resetSequence">[]
): void {
  for (const factory of factories) {
    factory.resetSequence();
  }
}

function errorMessage(error: unknown): string {
  return error instanceof Error ? error.message : String(error);
}

function factoryArgs<Name extends string>(args: {
  name: Name;
  sequence: number;
  ids: IdGeneratorPort;
  clock: ClockPort;
}): FactoryBuildArgs<Name> {
  return args;
}

function applyOverrides<Value extends object, Name extends string>(
  value: Value,
  args: FactoryBuildArgs<Name>,
  overrides: FactoryOverrides<Value, Name> | undefined,
): Value {
  if (!overrides) return value;

  const resolved =
    typeof overrides === "function" ? overrides(value, args) : overrides;

  return {
    ...value,
    ...resolved,
  };
}

function assertCount(kind: "buildList" | "createList", count: number): void {
  if (!Number.isInteger(count) || count < 0) {
    throw new Error(`Factory ${kind} count must be a non-negative integer.`);
  }
}

/**
 * Create a typed test data factory with deterministic sequence support.
 */
export function createFactory<
  const Name extends string,
  Value extends object,
  Ctx = unknown,
  Created = Value,
>(
  name: Name,
  options: CreateFactoryOptions<Name, Value, Ctx, Created>,
): FactoryDef<Name, Value, Ctx, Created> {
  const start = options.start ?? 1;
  const ids = options.ids ?? createUuidIdGenerator();
  const clock = options.clock ?? createSystemClock();
  let nextSequence = start;

  function build(overrides?: FactoryOverrides<Value, Name>): Value {
    const args = factoryArgs({
      name,
      sequence: nextSequence++,
      ids,
      clock,
    });
    const value = options.defaults(args);

    return applyOverrides(value, args, overrides);
  }

  async function create(
    ctx: Ctx,
    overrides?: FactoryOverrides<Value, Name>,
  ): Promise<Created> {
    if (!options.persist) {
      throw new Error(
        `Factory "${name}" cannot create persisted records without a persist function.`,
      );
    }

    const value = build(overrides);
    const args = factoryArgs({
      name,
      sequence: nextSequence - 1,
      ids,
      clock,
    });

    return options.persist(ctx, value, args);
  }

  return {
    kind: "factory",
    name,
    build,
    buildList(count, overrides) {
      assertCount("buildList", count);
      return Array.from({ length: count }, () => build(overrides));
    },
    create,
    async createList(ctx, count, overrides) {
      assertCount("createList", count);
      const created: Created[] = [];

      for (let index = 0; index < count; index += 1) {
        created.push(await create(ctx, overrides));
      }

      return created;
    },
    resetSequence(next = start) {
      nextSequence = next;
    },
  };
}

/**
 * Define a named seed.
 */
export function defineSeed<const Name extends string, Ctx = unknown>(
  name: Name,
  options: DefineSeedOptions<Ctx>,
): SeedDef<Ctx, Name> {
  return {
    kind: "seed",
    name,
    description: options.description,
    run: options.run,
  };
}

/**
 * Run seeds in order and wrap failures with the seed name.
 */
export async function runSeeds<Ctx>(
  options: RunSeedsOptions<Ctx>,
): Promise<void> {
  for (const seed of options.seeds) {
    try {
      await seed.run(options.ctx);
    } catch (error) {
      throw new SeedRunError(seed, error);
    }
  }
}

/**
 * Create a database test harness for app-owned database fixtures.
 *
 * The harness does not know about an ORM or database provider. Apps provide
 * creation, reset, close, and context mapping functions, while Beignet handles
 * the repetitive test lifecycle around factory sequences and seed execution.
 */
export function createDatabaseTestHarness<Database, Ctx>(
  options: CreateDatabaseTestHarnessOptions<Database, Ctx>,
): DatabaseTestHarness<Database, Ctx> {
  const sessions = new Set<DatabaseTestSession<Database, Ctx>>();

  function resetFactorySequences(): void {
    resetFactories(...(options.factories ?? []));
  }

  function defaultSeeds(): readonly SeedDef<Ctx>[] {
    return options.seeds ?? [];
  }

  async function closeSession(
    session: DatabaseTestSession<Database, Ctx>,
  ): Promise<void> {
    if (!sessions.delete(session)) return;
    await options.close?.(session.database);
  }

  return {
    async setup(setupOptions = {}) {
      resetFactorySequences();

      const database = await options.create();
      const ctx = options.ctx(database);
      const session: DatabaseTestSession<Database, Ctx> = {
        database,
        ctx,
        async runSeeds(seeds = defaultSeeds()) {
          await runSeeds({ ctx, seeds });
        },
        async reset() {
          resetFactorySequences();
          await options.reset?.(database);
        },
        async close() {
          await closeSession(session);
        },
      };

      sessions.add(session);

      try {
        if (setupOptions.seed) {
          const seeds =
            setupOptions.seed === true ? defaultSeeds() : setupOptions.seed;
          await session.runSeeds(seeds);
        }
      } catch (error) {
        await session.close();
        throw error;
      }

      return session;
    },
    resetFactories: resetFactorySequences,
    async cleanup() {
      await Promise.all(Array.from(sessions, (session) => session.close()));
    },
  };
}
