/**
 * Test fixtures and helpers for Agent testing.
 * This module provides utilities for testing Agent-related implementations.
 */
import { expect } from 'vitest';
import { Message, TextBlock } from '../types/messages.js';
import { StateStore } from '../state-store.js';
import { ToolRegistry } from '../registry/tool-registry.js';
import { defaultSandbox } from '../sandbox/default.js';
import { expectLoopMetrics } from './metrics-helpers.js';
/**
 * Helper to create a mock Agent for testing.
 * Provides minimal Agent interface with messages, appState, and tool registry.
 * `addHook` captures registrations into `trackedHooks` for test inspection.
 *
 * @param data - Optional mock agent data
 * @returns Mock Agent with trackedHooks
 */
export function createMockAgent(data) {
    const trackedHooks = [];
    return {
        messages: data?.messages ?? [],
        appState: new StateStore(data?.appState ?? {}),
        modelState: new StateStore(),
        toolRegistry: data?.toolRegistry ?? new ToolRegistry(),
        cancelSignal: new AbortController().signal,
        // Mirror the real Agent.sandbox getter: resolve the environment default lazily.
        // An explicit `extra.sandbox` below overrides this accessor.
        get sandbox() {
            return defaultSandbox.get();
        },
        addHook: (eventType, callback) => {
            trackedHooks.push({
                eventType: eventType,
                callback: callback,
            });
            return () => { };
        },
        // No-op so plugins that register middleware in initAgent (e.g. MemoryManager injection) work
        // out of the box. Tests that inspect registrations override this via `extra.addMiddleware`.
        addMiddleware: () => () => { },
        ...data?.extra,
        trackedHooks,
    };
}
/**
 * Creates a Message with the given role containing a single TextBlock.
 *
 * @param role - The message role
 * @param text - The text content
 * @returns A Message with the specified role
 */
export function textMessage(role, text) {
    return new Message({ role, content: [new TextBlock(text)] });
}
/**
 * Finds the tracked hook for the given event type and invokes it with the provided event.
 * Throws if no hook is registered for that event type.
 *
 * @param agent - The mock agent with tracked hooks
 * @param event - The event instance to dispatch
 */
export async function invokeTrackedHook(agent, event) {
    const hook = agent.trackedHooks.find((h) => h.eventType === event.constructor);
    if (!hook) {
        throw new Error(`No hook registered for event type: ${event.constructor.name}`);
    }
    await hook.callback(event);
}
/**
 * Creates an asymmetric matcher that validates AgentResult structure and values.
 * Reduces nesting in test assertions by providing a clean, readable matcher.
 *
 * @param options - Expected result values
 * @returns An asymmetric matcher suitable for use in expect().toEqual()
 *
 * @example
 * ```typescript
 * expect(result).toEqual(expectAgentResult({
 *   stopReason: 'endTurn',
 *   messageText: 'Hello',
 *   cycleCount: 1,
 * }))
 * ```
 */
export function expectAgentResult(options) {
    const { stopReason, messageText, cycleCount, traceCount, toolNames, usage, invocationState } = options;
    const expectedLastMessage = messageText
        ? expect.objectContaining({
            role: 'assistant',
            content: expect.arrayContaining([expect.objectContaining({ type: 'textBlock', text: messageText })]),
        })
        : expect.objectContaining({ role: 'assistant' });
    const expectedTraces = traceCount !== undefined
        ? expect.objectContaining({ length: traceCount })
        : expect.arrayContaining([expect.objectContaining({ name: expect.any(String) })]);
    // Build metrics matcher options, only including defined properties
    const metricsOptions = { cycleCount };
    if (toolNames !== undefined) {
        metricsOptions.toolNames = toolNames;
    }
    if (usage !== undefined) {
        metricsOptions.usage = usage;
    }
    return expect.objectContaining({
        type: 'agentResult',
        stopReason,
        lastMessage: expectedLastMessage,
        metrics: expectLoopMetrics(metricsOptions),
        traces: expectedTraces,
        invocationState: invocationState ?? expect.any(Object),
    });
}
/**
 * Creates a minimal InvokableAgent that sleeps for `delayMs` before resolving,
 * aborting the sleep early when the invocation's `cancelSignal` fires. Used to
 * exercise timeout and cancellation behavior deterministically without spinning
 * up a full Agent.
 *
 * @param id - The agent id
 * @param delayMs - How long the agent should sleep before returning
 * @param structuredOutput - Optional structured output (e.g. a swarm handoff). When present,
 *   its `message` field is used as the assistant text.
 */
export function createCancellableAgent(id, delayMs, structuredOutput = { message: 'done' }) {
    const sleep = (signal) => new Promise((resolve, reject) => {
        const timer = setTimeout(resolve, delayMs);
        if (signal) {
            const onAbort = () => {
                clearTimeout(timer);
                reject(new Error('cancelled'));
            };
            if (signal.aborted)
                onAbort();
            else
                signal.addEventListener('abort', onAbort, { once: true });
        }
    });
    return {
        id,
        description: `Agent ${id}`,
        async invoke(_args, options) {
            await sleep(options?.cancelSignal);
            return {
                stopReason: 'endTurn',
                lastMessage: { role: 'assistant', content: [new TextBlock(structuredOutput.message)] },
                structuredOutput,
            };
        },
        // eslint-disable-next-line require-yield
        async *stream(args, options) {
            return await this.invoke(args, options);
        },
    };
}
//# sourceMappingURL=agent-helpers.js.map