import { __awaiter } from "tslib";
import { assert, createLogicSuite } from '@genesislcap/foundation-testing';
import { defineStatefulAgent } from '../../config/define-stateful-agent';
// Side-effect import — MUST come before `./chat-driver` so the driver subclasses
// jsdom's EventTarget rather than Node's native one. Mirrors chat-driver.test.ts.
import './align-event-globals';
import { ChatDriver } from './chat-driver';
// ---------------------------------------------------------------------------
// Sub-agent state scoping.
//
// A sub-agent config lives in the parent's `subAgentsMap`, keyed by NAME — one
// config object, reused for every invocation. Anything a handler closes over is
// therefore scoped to the *config*, not to the invocation.
//
// These tests PIN that behaviour; they do not wish it away. Per-conversation
// registration is deliberate, so a closure in a sub-agent config will always be
// config-scoped — no planned platform change (F8 included) alters it. F8 adds a
// separate invocation-scoped store to write to instead; it does not re-scope
// closures. Consumers that accumulate across a child's turns must use that store
// (today: a WeakMap keyed on `promptCtx.signal`), never a closure.
//
// ⚠ TO WHOEVER IMPLEMENTS THE PER-ACTIVATION-STATE TICKET
// (`defineStatefulAgent`: per-activation state and headless execution)
//
// Its acceptance criterion 2 — "two concurrent activations of the same config
// hold independent state, with a test that fails against today's shared closure
// variable" — reads almost word-for-word like the first two tests below. It is
// not the same subject, and they must be treated differently:
//
//   • Criterion 2 concerns FRAMEWORK-owned state: `defineStatefulAgent`'s single
//     `let state` (define-stateful-agent.ts:374). That is the bug it fixes, and
//     it needs a NEW test of its own.
//   • The first two tests below concern CONSUMER-owned closures in a plain
//     `AgentConfig`. The platform cannot re-scope a closure someone wrote in
//     their own factory, so these stay true afterwards. Do not delete or invert
//     them to make criterion 2 look satisfied.
//   • The third test — "a defineStatefulAgent config cannot be used as a
//     sub-agent" — IS invalidated by that ticket (its criteria 1 and 3), and
//     must be rewritten when it lands, not before.
//
// The provider here answers by rule rather than by scripted queue: sub-agent
// invocations dispatched from one assistant turn run concurrently under
// `Promise.all`, so a shared FIFO queue would make ordering — and the test —
// nondeterministic.
// ---------------------------------------------------------------------------
/**
 * Answers by rule, not by scripted queue: sub-agent invocations dispatched from
 * one assistant turn run concurrently under `Promise.all`, so a shared FIFO
 * would make ordering — and the test — nondeterministic.
 *
 * `fanOut` controls how many `delegate` calls the parent emits in its single
 * delegating turn. Parent turns after the first return plain text so the loop
 * terminates.
 */
const ruleProvider = (fanOut) => {
    let parentTurns = 0;
    return {
        chat: (_history, _userMessage, options) => __awaiter(void 0, void 0, void 0, function* () {
            var _a;
            const tools = ((_a = options === null || options === void 0 ? void 0 : options.tools) !== null && _a !== void 0 ? _a : []).map((t) => t.name);
            // Child turn: it can only finish via its completion tool.
            if (tools.includes('work')) {
                return {
                    role: 'assistant',
                    content: '',
                    toolCalls: [{ id: 'w1', name: 'work', args: {} }],
                };
            }
            // Parent turn — delegate once, then wrap up.
            if (tools.includes('delegate') && parentTurns === 0) {
                parentTurns += 1;
                return {
                    role: 'assistant',
                    content: '',
                    toolCalls: Array.from({ length: fanOut }, (_, i) => ({
                        id: `d${i}`,
                        name: 'delegate',
                        args: {},
                    })),
                };
            }
            return { role: 'assistant', content: 'done' };
        }),
    };
};
const makeRegistry = (provider) => ({
    get: () => provider,
    default: () => provider,
    defaultName: () => 'test',
    names: () => ['test'],
    getStatus: () => __awaiter(void 0, void 0, void 0, function* () { return null; }),
    listStatuses: () => __awaiter(void 0, void 0, void 0, function* () { return []; }),
});
const agent = (overrides) => (Object.assign({ description: 'test agent' }, overrides));
const makeDriver = (config, provider) => {
    const driver = new ChatDriver(makeRegistry(provider), {
        maxToolIterations: 20,
        maxFoldOperations: 5,
        sessionKey: '',
    });
    driver.applyAgent(config);
    return driver;
};
const scope = createLogicSuite('ChatDriver sub-agent invocation scope');
// ---------------------------------------------------------------------------
scope('closure state is config-scoped, so concurrent invocations contaminate each other', () => __awaiter(void 0, void 0, void 0, function* () {
    // The natural-looking way to accumulate across a sub-agent's turns — a closure
    // in the config factory — is created once, with the config, and shared by every
    // invocation. This is the trap; the assertion records it rather than wanting it.
    const collected = [];
    const worker = agent({
        name: 'worker',
        toolDefinitions: [
            { name: 'work', description: 'work', parameters: { type: 'object', properties: {} } },
        ],
        toolHandlers: {
            work: (_args, ctx) => __awaiter(void 0, void 0, void 0, function* () {
                var _a;
                collected.push('item');
                // Report what THIS invocation believes it has accumulated.
                (_a = ctx.completeSubAgent) === null || _a === void 0 ? void 0 : _a.call(ctx, { seen: collected.length });
                return 'worked';
            }),
        },
    });
    const outcomes = [];
    const parent = agent({
        name: 'boss',
        subAgents: [worker],
        toolDefinitions: [
            {
                name: 'delegate',
                description: 'delegate',
                parameters: { type: 'object', properties: {} },
            },
        ],
        toolHandlers: {
            delegate: (_args, ctx) => __awaiter(void 0, void 0, void 0, function* () {
                const outcome = yield ctx.requestSubAgent('worker', { task: 'go' });
                outcomes.push(outcome);
                return 'delegated';
            }),
        },
    });
    yield makeDriver(parent, ruleProvider(2)).sendMessage('go');
    assert.is(outcomes.length, 2, 'both delegations should have resolved');
    // Each invocation did exactly one unit of work, so an invocation-scoped
    // accumulator would report 1 from both. A config-scoped one reports 1 and 2.
    // Deterministic despite the concurrency: the handler pushes and reads with no
    // await between, so two invocations cannot interleave mid-body.
    const seen = outcomes.map((o) => { var _a; return (_a = o === null || o === void 0 ? void 0 : o.result) === null || _a === void 0 ? void 0 : _a.seen; }).sort();
    assert.equal(seen, [1, 2], "the second invocation sees the first's work — if this ever reports [1,1], closures " +
        'have become invocation-scoped and this test should be rewritten, not deleted');
}));
// ---------------------------------------------------------------------------
scope('closure state is config-scoped, so a later invocation resumes the earlier one', () => __awaiter(void 0, void 0, void 0, function* () {
    // The sequential form of the same trap: nothing resets the closure between
    // invocations, so a second delegation resumes where the first left off. This is
    // the failure CG8 describes — a retried child whose cursor is already exhausted
    // emits nothing and completes silently.
    const collected = [];
    const worker = agent({
        name: 'worker',
        toolDefinitions: [
            { name: 'work', description: 'work', parameters: { type: 'object', properties: {} } },
        ],
        toolHandlers: {
            work: (_args, ctx) => __awaiter(void 0, void 0, void 0, function* () {
                var _a;
                collected.push('item');
                (_a = ctx.completeSubAgent) === null || _a === void 0 ? void 0 : _a.call(ctx, { seen: collected.length });
                return 'worked';
            }),
        },
    });
    const outcomes = [];
    const parent = agent({
        name: 'boss',
        subAgents: [worker],
        toolDefinitions: [
            { name: 'delegate', description: 'delegate', parameters: { type: 'object', properties: {} } },
        ],
        toolHandlers: {
            // Await them one after the other — no concurrency involved.
            delegate: (_args, ctx) => __awaiter(void 0, void 0, void 0, function* () {
                outcomes.push(yield ctx.requestSubAgent('worker', { task: 'first' }));
                outcomes.push(yield ctx.requestSubAgent('worker', { task: 'second' }));
                return 'delegated';
            }),
        },
    });
    yield makeDriver(parent, ruleProvider(1)).sendMessage('go');
    const seen = outcomes.map((o) => { var _a; return (_a = o === null || o === void 0 ? void 0 : o.result) === null || _a === void 0 ? void 0 : _a.seen; });
    assert.equal(seen, [1, 2], "a second invocation does NOT start from zero — it resumes the first's accumulator");
}));
// ---------------------------------------------------------------------------
scope('a defineStatefulAgent config cannot be used as a sub-agent', () => __awaiter(void 0, void 0, void 0, function* () {
    // `init` runs from `onActivate`, which only OrchestratingDriver fires on an
    // agent switch. `invokeSubAgent` builds a raw ChatDriver and calls
    // `applyAgent` directly, so a stateful sub-agent never initialises. This test
    // pins that boundary — it is a documented-by-behaviour constraint, not a bug,
    // and the failure is loud rather than silent.
    const stateful = defineStatefulAgent({
        name: 'worker',
        description: 'stateful worker',
        init: () => ({ count: 0 }),
        systemPrompt: ({ state }) => `count=${state.count}`,
        toolDefinitions: [
            { name: 'work', description: 'work', parameters: { type: 'object', properties: {} } },
        ],
        toolHandlers: () => ({
            work: (_args, ctx) => __awaiter(void 0, void 0, void 0, function* () {
                var _a;
                (_a = ctx.completeSubAgent) === null || _a === void 0 ? void 0 : _a.call(ctx, { ok: true });
                return 'worked';
            }),
        }),
    });
    let outcome;
    const parent = agent({
        name: 'boss',
        subAgents: [stateful],
        toolDefinitions: [
            { name: 'delegate', description: 'delegate', parameters: { type: 'object', properties: {} } },
        ],
        toolHandlers: {
            delegate: (_args, ctx) => __awaiter(void 0, void 0, void 0, function* () {
                outcome = yield ctx.requestSubAgent('worker', { task: 'go' });
                return 'delegated';
            }),
        },
    });
    yield makeDriver(parent, ruleProvider(1)).sendMessage('go');
    assert.ok(outcome, 'the delegation should have resolved rather than hanging');
    assert.is(outcome.ok, false, `a stateful sub-agent never runs init (onActivate is orchestrator-only), so the ` +
        `invocation must fail rather than silently run with undefined state — got ${JSON.stringify(outcome)}`);
}));
scope.run();
