import { __awaiter } from "tslib";
import { assert, createLogicSuite } from '@genesislcap/foundation-testing';
import { clearDiagnosticsCursorRegistry } from './diagnostics-cursors';
import { clearPersisterRegistry } from './persister-registry';
import { SessionPersister } from './session-persister';
import { createPersistedSession } from './session-snapshot';
// GENC-1351 §5.10: the shared per-session persister owns restore-core save/restore/
// freeze. Unit-tested in isolation with fake driver/store/provider — the win the
// extraction buys (this logic was previously only exercised by manual testing).
const HOST = window.location.host;
const KEY = 'session-1';
const msg = (o) => (Object.assign({ role: 'user', content: '' }, o));
const tick = () => new Promise((r) => {
    setTimeout(r, 0);
});
function makePersister(over = {}) {
    const f = Object.assign({ saved: [], cleared: 0, loadResult: null, loadedHistory: [], primed: [], loadSessionCalls: [], inputValues: [], history: [], activeAgent: undefined, enabled: true, restoring: false, diagnosticEntries: [], appended: [], savedPrefs: [], loadPrefsResult: undefined, diagnosticsCapable: true }, over);
    const provider = Object.assign({ saveDebounceMs: 0, diagnosticsDebounceMs: 0, save: (_k, snap) => __awaiter(this, void 0, void 0, function* () {
            f.saved.push(snap);
        }), load: () => __awaiter(this, void 0, void 0, function* () { return f.loadResult; }), clear: () => __awaiter(this, void 0, void 0, function* () {
            f.cleared += 1;
        }), loadPreferences: () => __awaiter(this, void 0, void 0, function* () { return f.loadPrefsResult; }) }, (f.diagnosticsCapable
        ? {
            appendDiagnostics: (_k, entries) => __awaiter(this, void 0, void 0, function* () {
                f.appended.push(entries);
            }),
            savePreferences: (_k, prefs) => __awaiter(this, void 0, void 0, function* () {
                f.savedPrefs.push(prefs);
            }),
        }
        : {}));
    const driver = {
        getRawHistory: () => f.history,
        getActiveAgent: () => f.activeAgent,
        loadHistory: (h) => f.loadedHistory.push(h),
        primeRestoredAgentState: (s) => f.primed.push(s),
        setPinnedAgent: () => { },
    };
    const store = {
        store: {
            aiAssistant: {
                messages: f.history,
                flowOwnerAgentName: null,
                flowActivationPrompt: null,
                activeAgent: undefined,
                pinnedAgentName: null,
                // Inline rather than via `emptyUsage()` to keep this mock self-contained. The
                // shape matters: the persister reads `sessionUsage.costUsd`, so a mock missing
                // the object crashes the autosave timer rather than failing an assertion.
                sessionUsage: {
                    costUsd: 0,
                    uncachedInputTokens: 0,
                    cacheReadTokens: 0,
                    cacheWriteTokens: 0,
                    outputTokens: 0,
                },
                get restoring() {
                    return f.restoring;
                },
            },
        },
        actions: {
            aiAssistant: {
                loadSession: (p) => f.loadSessionCalls.push(p),
                setInputValue: (v) => f.inputValues.push(v),
            },
        },
    };
    const deps = {
        stateKey: KEY,
        getPersistence: () => ({ enabled: f.enabled, provider: provider }),
        getAgentNames: () => [],
        getDriver: () => driver,
        getStore: () => store,
        getDiagnosticEntries: () => f.diagnosticEntries,
    };
    return { p: new SessionPersister(deps), f };
}
const Suite = createLogicSuite('SessionPersister');
Suite.before.each(() => {
    clearPersisterRegistry();
    clearDiagnosticsCursorRegistry();
});
Suite('restore() hydrates the store + driver and appends a "Restored" divider', () => __awaiter(void 0, void 0, void 0, function* () {
    const snapshot = createPersistedSession({
        sessionKey: KEY,
        host: HOST,
        messages: [msg({ role: 'user', content: 'hi' }), msg({ role: 'assistant', content: 'hello' })],
        pinnedAgentName: null,
        flowOwnerAgentName: null,
        sessionCostUsd: 0,
    });
    const { p, f } = makePersister({ loadResult: snapshot });
    yield p.restore();
    assert.is(f.loadSessionCalls.length, 1, 'store hydrated once');
    assert.is(f.loadedHistory.length, 1, 'driver history loaded once');
    const loaded = f.loadedHistory[0];
    assert.is(loaded.length, 3, 'two restored messages + a trailer');
    assert.is(loaded[2].role, 'system-event');
    assert.is(f.inputValues.length, 0, 'no composer prefill for a normal restore');
}));
Suite('restore() of an interrupted snapshot re-appends the triggering message + notice + pre-fills', () => __awaiter(void 0, void 0, void 0, function* () {
    const snapshot = Object.assign(Object.assign({}, createPersistedSession({
        sessionKey: KEY,
        host: HOST,
        // The frozen snapshot is the prior *resumable* conversation — it does NOT
        // contain the interrupted flow's triggering message; that is re-appended from
        // the `interrupted.prompt` so the exchange reads as a conversation.
        messages: [
            msg({ role: 'user', content: 'hi' }),
            msg({ role: 'assistant', content: 'hello' }),
        ],
        pinnedAgentName: null,
        flowOwnerAgentName: null,
        sessionCostUsd: 0,
    })), { interrupted: { message: 'I did not finish — send it again', prompt: 'do the thing' } });
    const { p, f } = makePersister({ loadResult: snapshot });
    yield p.restore();
    const loaded = f.loadedHistory[0];
    // …prior conversation, user('do the thing'), assistant(notice)
    assert.is(loaded[loaded.length - 2].role, 'user', 'triggering message re-appended before the notice');
    assert.is(loaded[loaded.length - 2].content, 'do the thing');
    assert.is(loaded[loaded.length - 1].role, 'assistant', 'notice is an assistant bubble');
    assert.is(loaded[loaded.length - 1].content, 'I did not finish — send it again');
    assert.equal(f.inputValues, ['do the thing'], 'composer pre-filled with the flow prompt');
}));
Suite('restore() of an interrupted snapshot without a captured prompt appends only the notice', () => __awaiter(void 0, void 0, void 0, function* () {
    const snapshot = Object.assign(Object.assign({}, createPersistedSession({
        sessionKey: KEY,
        host: HOST,
        messages: [
            msg({ role: 'user', content: 'hi' }),
            msg({ role: 'assistant', content: 'hello' }),
        ],
        pinnedAgentName: null,
        flowOwnerAgentName: null,
        sessionCostUsd: 0,
    })), { interrupted: { message: 'I did not finish — send it again' } });
    const { p, f } = makePersister({ loadResult: snapshot });
    yield p.restore();
    const loaded = f.loadedHistory[0];
    assert.is(loaded.length, 3, 'prior 2 messages + notice only — no synthesized user message');
    assert.is(loaded[loaded.length - 1].role, 'assistant', 'notice is an assistant bubble');
    assert.equal(f.inputValues, [], 'no composer pre-fill without a captured prompt');
}));
Suite('restore() of a fresh-freeze interrupted snapshot does NOT duplicate the trigger message', () => __awaiter(void 0, void 0, void 0, function* () {
    // The fresh-freeze fallback (no prior resumable snapshot) keeps the transcript
    // THROUGH the last user message (`keepThroughLastUserMessage`), so the triggering
    // prompt is already the final message. restore() must NOT re-append it from
    // `interrupted.prompt` — doing so duplicated the bubble on a fresh session that
    // went non-resumable on its first turn (GENC-1351 §5.9).
    const snapshot = Object.assign(Object.assign({}, createPersistedSession({
        sessionKey: KEY,
        host: HOST,
        messages: [msg({ role: 'user', content: 'cancel trade T123' })],
        pinnedAgentName: null,
        flowOwnerAgentName: null,
        sessionCostUsd: 0,
    })), { interrupted: { message: 'I did not finish — send it again', prompt: 'cancel trade T123' } });
    const { p, f } = makePersister({ loadResult: snapshot });
    yield p.restore();
    const loaded = f.loadedHistory[0];
    assert.is(loaded.filter((m) => m.role === 'user').length, 1, 'trigger appears once');
    assert.is(loaded.length, 2, 'trigger + notice only — no duplicate user bubble');
    assert.is(loaded[0].content, 'cancel trade T123');
    assert.is(loaded[1].role, 'assistant', 'notice is an assistant bubble');
    assert.equal(f.inputValues, ['cancel trade T123'], 'composer still pre-filled with the prompt');
}));
Suite('restore() with nothing stored is a silent no-op', () => __awaiter(void 0, void 0, void 0, function* () {
    const { p, f } = makePersister({ loadResult: null });
    yield p.restore();
    assert.is(f.loadSessionCalls.length, 0);
    assert.is(f.loadedHistory.length, 0);
}));
Suite('scheduleSave() persists the current transcript (single debounced save)', () => __awaiter(void 0, void 0, void 0, function* () {
    const history = [msg({ role: 'user', content: 'save me' })];
    const { p, f } = makePersister({ history });
    p.scheduleSave();
    p.scheduleSave(); // second view — must coalesce into ONE save
    yield tick();
    yield tick();
    assert.is(f.saved.length, 1, 'coalesced to a single save');
    assert.is(f.saved[0].messages.length, 1);
    assert.is(f.saved[0].messages[0].content, 'save me');
    assert.is(f.saved[0].interrupted, undefined, 'a normal save carries no interrupted marker');
}));
Suite('scheduleSave() is a no-op when persistence is disabled', () => __awaiter(void 0, void 0, void 0, function* () {
    const { p, f } = makePersister({ enabled: false, history: [msg({ content: 'x' })] });
    p.scheduleSave();
    yield tick();
    yield tick();
    assert.is(f.saved.length, 0);
}));
Suite('freeze: a non-resumable agent stamps the PRIOR snapshot, never an empty one', () => __awaiter(void 0, void 0, void 0, function* () {
    var _a;
    // Prior persisted (resumable) snapshot with real conversation.
    const prior = createPersistedSession({
        sessionKey: KEY,
        host: HOST,
        messages: [
            msg({ role: 'user', content: 'earlier' }),
            msg({ role: 'assistant', content: 'ok' }),
        ],
        pinnedAgentName: 'Trade Ops',
        flowOwnerAgentName: null,
        sessionCostUsd: 0,
    });
    const { p, f } = makePersister({
        loadResult: prior,
        // in-flight transcript has extra agent work we must NOT persist
        history: [
            msg({ role: 'user', content: 'earlier' }),
            msg({ role: 'assistant', content: 'ok' }),
            msg({ role: 'assistant', content: 'in-flight work' }),
        ],
        activeAgent: {
            name: 'Trade Ops',
            resumable: () => __awaiter(void 0, void 0, void 0, function* () { return false; }),
            notResumableMessage: 'start again',
            serialize: () => ({}),
        },
    });
    p.scheduleSave();
    yield tick();
    yield tick();
    assert.is(f.saved.length, 1, 'one freeze-stamp write');
    const written = f.saved[0];
    assert.ok(written.interrupted, 'stamped with an interrupted marker');
    assert.is((_a = written.interrupted) === null || _a === void 0 ? void 0 : _a.message, 'start again');
    assert.is(written.messages.length, 2, 'froze at the PRIOR transcript, not the in-flight one');
}));
Suite('clearRestoreCore() drops the snapshot', () => __awaiter(void 0, void 0, void 0, function* () {
    const { p, f } = makePersister();
    yield p.clearRestoreCore();
    assert.is(f.cleared, 1);
}));
// ── Phase 2: preferences ────────────────────────────────────────────────────
Suite('loadPreferences() populates storedPreferences from the provider blob (null when none)', () => __awaiter(void 0, void 0, void 0, function* () {
    const prefs = { enabled: true, showToolCalls: true };
    const { p } = makePersister({ loadPrefsResult: prefs });
    yield p.loadPreferences();
    assert.equal(p.storedPreferences, prefs);
    const { p: p2 } = makePersister(); // provider present, nothing stored
    yield p2.loadPreferences();
    assert.is(p2.storedPreferences, null);
}));
Suite('savePreferences() persists, records the (shared) user change, and refreshes storedPreferences', () => {
    const blob = { enabled: false, showNarration: true };
    const { p, f } = makePersister();
    assert.is(p.preferencesUserChanged, false);
    p.savePreferences(blob);
    assert.equal(f.savedPrefs, [blob]);
    assert.is(p.preferencesUserChanged, true, 'the shared latch is set so the reconciler freezes');
    assert.equal(p.storedPreferences, blob, 'stored blob kept fresh for a later re-reconcile');
    // No savePreferences on the provider → no write, but the user change is still
    // recorded (freezing the baseline regardless of a persistence-capable backend).
    const { p: p2, f: f2 } = makePersister({ diagnosticsCapable: false });
    p2.savePreferences(blob);
    assert.is(f2.savedPrefs.length, 0);
    assert.is(p2.preferencesUserChanged, true);
});
// ── Phase 3: diagnostics-append ───────────────────────────────────────────────
const evt = (index) => ({
    kind: 'event',
    type: 'turn.start',
    index,
    timestamp: `2026-01-01T00:00:0${index}.000Z`,
});
const metaSnap = (dedupSignature, meta) => ({
    kind: 'meta-snapshot',
    timestamp: '2026-01-01T00:00:00.000Z',
    meta,
    dedupSignature,
});
// A diagnostic MESSAGE entry as the reasoning/narration/answer split (GENC-1411) produces them —
// several messages from ONE response, sharing timestamp + role, differing by `category` / content.
const diagMsg = (over = {}) => (Object.assign({ kind: 'message', role: 'assistant', timestamp: '2026-01-01T00:00:05.000Z', content: '' }, over));
Suite('scheduleDiagnostics() appends the collected delta once per window', () => __awaiter(void 0, void 0, void 0, function* () {
    const { p, f } = makePersister({ diagnosticEntries: [evt(0), evt(1)] });
    p.scheduleDiagnostics();
    p.scheduleDiagnostics(); // second view → coalesced
    yield tick();
    yield tick();
    assert.is(f.appended.length, 1, 'one append despite two schedules');
    assert.is(f.appended[0].length, 2);
}));
Suite('scheduleDiagnostics() holds off while the session is restoring', () => __awaiter(void 0, void 0, void 0, function* () {
    const { p, f } = makePersister({ diagnosticEntries: [evt(0)], restoring: true });
    p.scheduleDiagnostics();
    yield tick();
    yield tick();
    assert.is(f.appended.length, 0, 'no capture mid-restore (watermark not yet set)');
}));
Suite('flushDiagnostics() appends immediately and de-dups via the shared cursors', () => __awaiter(void 0, void 0, void 0, function* () {
    const { p, f } = makePersister({ diagnosticEntries: [evt(0), evt(1)] });
    yield p.flushDiagnostics();
    assert.is(f.appended.length, 1, 'first flush appends the two events');
    assert.is(f.appended[0].length, 2);
    // Nothing new since — the event cursor has advanced, so the delta is empty.
    yield p.flushDiagnostics();
    assert.is(f.appended.length, 1, 'no re-append of already-emitted events');
}));
Suite('flushDiagnostics() is a no-op without a diagnostics-capable provider', () => __awaiter(void 0, void 0, void 0, function* () {
    const { p, f } = makePersister({ diagnosticsCapable: false, diagnosticEntries: [evt(0)] });
    yield p.flushDiagnostics();
    assert.is(f.appended.length, 0);
}));
Suite('flushDiagnostics() dedups meta-snapshots by dedupSignature, not the volatile meta', () => __awaiter(void 0, void 0, void 0, function* () {
    // Regression for #12: the guard compared JSON.stringify(entry.meta), which embeds a
    // fresh timestamp + per-turn context, so it never matched → the bulky block was
    // re-appended on every flush. It must dedup on the stable config signature instead.
    const { p, f } = makePersister({
        diagnosticEntries: [metaSnap('cfg-1', { timestamp: 't1', context: { tokens: 1 } })],
    });
    yield p.flushDiagnostics();
    assert.is(f.appended.length, 1, 'first meta-snapshot appended');
    // Same config signature, but the volatile meta changed (new timestamp + tokens) → skip.
    f.diagnosticEntries = [metaSnap('cfg-1', { timestamp: 't2', context: { tokens: 999 } })];
    yield p.flushDiagnostics();
    assert.is(f.appended.length, 1, 'unchanged config → no re-append despite volatile churn');
    // Config signature changed → append again.
    f.diagnosticEntries = [metaSnap('cfg-2', { timestamp: 't3', context: { tokens: 999 } })];
    yield p.flushDiagnostics();
    assert.is(f.appended.length, 2, 'changed config → re-appended');
}));
Suite('flushDiagnostics() keeps co-timestamped split messages (reasoning + answer), not deduped as one', () => __awaiter(void 0, void 0, void 0, function* () {
    // GENC-1461: the reasoning/narration/answer split emits several messages from ONE response, so
    // they share timestamp + role + subAgentOf. The forward-capture keyed only on those, so it
    // emitted the reasoning message and dropped the answer as a "duplicate" — a server generation's
    // answer message vanished from the persisted download while its reasoning survived. Both must be
    // captured.
    const reasoning = diagMsg({ category: 'reasoning', content: 'thinking…' });
    const answer = diagMsg({ content: 'the generated code' }); // no category = the answer/tool-call slot
    const { p, f } = makePersister({ diagnosticEntries: [reasoning, answer] });
    yield p.flushDiagnostics();
    assert.is(f.appended.length, 1, 'one flush');
    assert.is(f.appended[0].length, 2, 'both the reasoning and the answer message are captured, not collapsed to one');
}));
Suite('flushDiagnostics() still dedups a genuine re-append of the same message across flushes', () => __awaiter(void 0, void 0, void 0, function* () {
    // The dedup must still stop the SAME message being re-appended on every throttled flush.
    const m = diagMsg({ category: 'reasoning', content: 'thinking…' });
    const { p, f } = makePersister({ diagnosticEntries: [m] });
    yield p.flushDiagnostics();
    assert.is(f.appended.length, 1, 'first flush appends the message');
    yield p.flushDiagnostics();
    assert.is(f.appended.length, 1, 'unchanged message → no re-append');
}));
Suite.run();
