/**
 * @fileoverview Test utilities for MCP server development.
 * Provides `createMockContext()` for testing tool and resource handlers
 * against the unified Context interface, plus `createMockLogger()` and
 * `createInMemoryStorage()` for unit-testing services in isolation.
 * @module src/testing/index
 */
import { attachTypedFail, createContentCollect, createContentStore, createEnrich, createEnrichmentStore, readContentStore, readEnrichmentStore, stashContentStore, stashEnrichmentStore, } from '../core/context.js';
import { StorageService } from '../storage/core/StorageService.js';
import { InMemoryProvider, } from '../storage/providers/inMemory/inMemoryProvider.js';
/**
 * Create a `ContextLogger` whose calls are recorded for inspection.
 * Useful when unit-testing code that accepts a `ContextLogger` directly.
 *
 * @example
 * ```ts
 * import { createMockLogger } from '@cyanheads/mcp-ts-core/testing';
 *
 * const log = createMockLogger();
 * log.info('started', { step: 1 });
 * expect(log.calls).toEqual([{ level: 'info', msg: 'started', data: { step: 1 } }]);
 * ```
 */
export function createMockLogger() {
    const calls = [];
    const logFn = (level) => (msg, data) => {
        calls.push({ level, msg, data });
    };
    return {
        calls,
        debug: logFn('debug'),
        info: logFn('info'),
        notice: logFn('notice'),
        warning: logFn('warning'),
        error: (msg, _error, data) => {
            calls.push({ level: 'error', msg, data });
        },
    };
}
function createMockState(tenantId) {
    const store = new Map();
    const requireTenant = () => {
        if (!tenantId) {
            throw new Error('tenantId required for state operations');
        }
    };
    return {
        get(key, schema) {
            requireTenant();
            const value = store.get(key);
            if (value === undefined)
                return Promise.resolve(null);
            return Promise.resolve(schema ? schema.parse(value) : value);
        },
        set(key, value) {
            requireTenant();
            store.set(key, value);
            return Promise.resolve();
        },
        delete(key) {
            requireTenant();
            store.delete(key);
            return Promise.resolve();
        },
        deleteMany(keys) {
            requireTenant();
            let count = 0;
            for (const key of keys) {
                if (store.delete(key))
                    count++;
            }
            return Promise.resolve(count);
        },
        getMany(keys) {
            requireTenant();
            const result = new Map();
            for (const key of keys) {
                if (store.has(key))
                    result.set(key, store.get(key));
            }
            return Promise.resolve(result);
        },
        setMany(entries) {
            requireTenant();
            for (const [key, value] of entries) {
                store.set(key, value);
            }
            return Promise.resolve();
        },
        list(prefix) {
            requireTenant();
            const items = [];
            for (const [key, value] of store) {
                if (!prefix || key.startsWith(prefix)) {
                    items.push({ key, value });
                }
            }
            return Promise.resolve({ items });
        },
    };
}
function createMockProgress() {
    const state = { _total: 0, _completed: 0, _messages: [] };
    return {
        get _total() {
            return state._total;
        },
        get _completed() {
            return state._completed;
        },
        get _messages() {
            return state._messages;
        },
        setTotal(n) {
            state._total = n;
            state._completed = 0;
            return Promise.resolve();
        },
        increment(amount = 1) {
            state._completed = Math.min(state._completed + amount, state._total || state._completed + amount);
            return Promise.resolve();
        },
        update(message) {
            state._messages.push(message);
            return Promise.resolve();
        },
    };
}
// ---------------------------------------------------------------------------
// Factory
// ---------------------------------------------------------------------------
/**
 * Creates a mock Context for testing tool and resource handlers.
 *
 * @example
 * ```ts
 * // Minimal — works for most tests
 * const ctx = createMockContext();
 *
 * // With tenant (for tools that use ctx.state)
 * const ctx = createMockContext({ tenantId: 'test-tenant' });
 *
 * // With task progress
 * const ctx = createMockContext({ progress: true });
 * ```
 */
export function createMockContext(options = {}) {
    const log = createMockLogger();
    const state = createMockState(options.tenantId);
    const progress = options.progress ? createMockProgress() : undefined;
    const enrichmentStore = createEnrichmentStore();
    const contentStore = createContentStore();
    // Wrap the caller's elicit mock into an ElicitFn so that tests calling
    // ctx.elicit.url(...) don't throw TypeError. The default url stub returns a
    // cancelled result and can be overridden by casting the mock to ElicitFn.
    let elicit;
    if (options.elicit) {
        const base = options.elicit;
        base.url = async (_message, _url) => ({ action: 'cancel' });
        elicit = base;
    }
    const ctx = {
        requestId: options.requestId ?? 'test-request-id',
        timestamp: new Date().toISOString(),
        log,
        state,
        signal: options.signal ?? new AbortController().signal,
        tenantId: options.tenantId,
        sessionId: options.sessionId,
        auth: options.auth,
        elicit,
        notifyPromptListChanged: options.notifyPromptListChanged,
        notifyResourceListChanged: options.notifyResourceListChanged,
        notifyResourceUpdated: options.notifyResourceUpdated,
        notifyToolListChanged: options.notifyToolListChanged,
        progress,
        uri: options.uri,
        content: createContentCollect(contentStore),
        enrich: createEnrich(enrichmentStore),
        // No-op resolver for definitions without a contract. `attachTypedFail` below
        // overwrites it with a contract-aware resolver when `options.errors` is set.
        recoveryFor: () => ({}),
    };
    // Stash the enrichment store so `getEnrichment(ctx)` can read what a handler
    // (or the service layer) accumulated via `ctx.enrich(...)` during the test.
    stashEnrichmentStore(ctx, enrichmentStore);
    // Stash the content store so `getContentBlocks(ctx)` can read what a handler
    // emitted via `ctx.content(...)` during the test.
    stashContentStore(ctx, contentStore);
    // Mirror the production handler factory: when a contract is declared, attach
    // a typed `fail` and `recoveryFor` keyed by the contract's reasons. Empty
    // contracts leave the no-op resolver in place.
    return attachTypedFail(ctx, options.errors);
}
/**
 * Reads the enrichment a handler accumulated via `ctx.enrich(...)` on a mock
 * context, for assertions. Returns the merged field values (empty object when
 * nothing was enriched).
 *
 * @example
 * ```ts
 * const ctx = createMockContext();
 * await search.handler(search.input.parse({ query: 'x' }), ctx);
 * expect(getEnrichment(ctx)).toMatchObject({ effectiveQuery: 'x', totalCount: 0 });
 * ```
 */
export function getEnrichment(ctx) {
    return readEnrichmentStore(ctx)?.values ?? {};
}
/**
 * Reads the content blocks a handler emitted via `ctx.content(...)` on a mock
 * context, for assertions. Returns them in insertion order (empty array when none
 * were emitted) — the same blocks the handler factory prepends to `content[]`,
 * never placed in `structuredContent`.
 *
 * @example
 * ```ts
 * const ctx = createMockContext();
 * await render.handler(render.input.parse({ text: 'hi' }), ctx);
 * expect(getContentBlocks(ctx)).toEqual([{ type: 'image', data: '...', mimeType: 'image/png' }]);
 * ```
 */
export function getContentBlocks(ctx) {
    return readContentStore(ctx)?.blocks ?? [];
}
// ---------------------------------------------------------------------------
// Storage helpers
// ---------------------------------------------------------------------------
/**
 * Build a real `StorageService` backed by an in-memory provider, suitable for
 * unit-testing services that accept a `StorageService` dependency.
 *
 * Because this uses the production `StorageService` + `InMemoryProvider`, the
 * behavior (tenant isolation, TTL, validation, list pagination) matches what
 * you'd see in a running server — no hand-rolled fake required.
 *
 * @example
 * ```ts
 * import { createInMemoryStorage, createMockContext } from '@cyanheads/mcp-ts-core/testing';
 *
 * const storage = createInMemoryStorage();
 * const svc = new MyService(config, storage);
 * const ctx = createMockContext({ tenantId: 'test-tenant' });
 * await svc.doWork(input, ctx);
 * ```
 */
export function createInMemoryStorage(options) {
    return new StorageService(new InMemoryProvider(options));
}
//# sourceMappingURL=index.js.map