import { assert, createLogicSuite } from '@genesislcap/foundation-testing';
import { addUsage, emptyUsage, sumUsage, totalTokens } from './sum-usage';
const suite = createLogicSuite('sumUsage');
const msg = (over = {}) => (Object.assign({ role: 'assistant', content: '' }, over));
suite('returns zeroes when no message carries usage', () => {
    const usage = sumUsage([msg({ role: 'user', content: 'hi' })]);
    assert.equal(usage, emptyUsage());
    assert.is(totalTokens(usage), 0);
});
suite('splits the prompt into three disjoint input buckets', () => {
    // `inputTokens` is the WHOLE prompt; the cache fields break it down. So uncached is
    // the remainder, and the three must re-add to the prompt exactly — that identity is
    // what stops the buckets double-counting the prompt when summed.
    const usage = sumUsage([
        msg({ inputTokens: 1000, cacheReadTokens: 700, cacheWriteTokens: 200, outputTokens: 50 }),
    ]);
    assert.is(usage.uncachedInputTokens, 100);
    assert.is(usage.cacheReadTokens, 700);
    assert.is(usage.cacheWriteTokens, 200);
    assert.is(usage.outputTokens, 50);
    assert.is(usage.uncachedInputTokens + usage.cacheReadTokens + usage.cacheWriteTokens, 1000, 'input buckets re-add to the prompt total');
    assert.is(totalTokens(usage), 1050);
});
suite('treats the whole prompt as uncached when no cache split is reported', () => {
    // Providers that report no cache breakdown (and messages persisted before the fields
    // existed) must still contribute their full prompt, not drop it.
    const usage = sumUsage([msg({ inputTokens: 400, outputTokens: 60 })]);
    assert.is(usage.uncachedInputTokens, 400);
    assert.is(usage.cacheReadTokens, 0);
    assert.is(usage.cacheWriteTokens, 0);
    assert.is(totalTokens(usage), 460);
});
suite('clamps uncached input at zero when buckets exceed the prompt total', () => {
    // Malformed or hand-edited history must not be able to drive a cost display negative.
    const usage = sumUsage([
        msg({ inputTokens: 100, cacheReadTokens: 90, cacheWriteTokens: 90, outputTokens: 0 }),
    ]);
    assert.is(usage.uncachedInputTokens, 0);
});
suite('sums LLM cost and widget-reported external cost', () => {
    const usage = sumUsage([msg({ cost: 0.25 }), msg({ externalCostUsd: 0.1, cost: 0.05 })]);
    assert.is(Math.round(usage.costUsd * 100) / 100, 0.4);
});
suite('recurses into sub-agent traces', () => {
    // A sub-agent's whole conversation is embedded in the parent's tool call, so this is
    // the only place its spend is visible to a host total.
    const usage = sumUsage([
        msg({
            toolCalls: [
                {
                    id: 'tc-1',
                    name: 'delegate',
                    args: {},
                    subAgentTrace: [
                        msg({ cost: 0.2, inputTokens: 300, cacheReadTokens: 100, outputTokens: 40 }),
                    ],
                },
            ],
        }),
    ]);
    assert.is(usage.costUsd, 0.2);
    assert.is(usage.uncachedInputTokens, 200);
    assert.is(usage.cacheReadTokens, 100);
    assert.is(usage.outputTokens, 40);
});
suite('counts usage banked onto a compaction summary', () => {
    // Compaction destroys the messages it summarises. Without the banked rollup, a total
    // re-derived from the transcript would fall by whatever those turns had spent.
    const before = sumUsage([
        msg({ cost: 0.5, inputTokens: 1000, cacheReadTokens: 400, outputTokens: 100 }),
        msg({ cost: 0.3, inputTokens: 500, outputTokens: 50 }),
    ]);
    const afterCompaction = sumUsage([
        msg({
            role: 'compacted-summary',
            content: 'summary of the above',
            compaction: {
                compactedCount: 2,
                createdAt: '2026-06-14T12:00:00.000Z',
                rolledUpUsage: before,
            },
        }),
    ]);
    assert.equal(afterCompaction, before, 'compaction preserves the running total');
});
suite('ignores tokensBefore, which is a context reading rather than spend', () => {
    // `tokensBefore` is one turn's context-window size. Counting it as cumulative spend
    // would silently inflate the session total on every compaction.
    const usage = sumUsage([
        msg({
            role: 'compacted-summary',
            content: 'summary',
            compaction: {
                compactedCount: 3,
                createdAt: '2026-06-14T12:00:00.000Z',
                tokensBefore: 90000,
            },
        }),
    ]);
    assert.is(totalTokens(usage), 0);
});
suite('addUsage adds every bucket and mutates neither operand', () => {
    const a = Object.assign(Object.assign({}, emptyUsage()), { costUsd: 1.5, uncachedInputTokens: 100, outputTokens: 10 });
    const b = Object.assign(Object.assign({}, emptyUsage()), { costUsd: 0.25, cacheReadTokens: 900, outputTokens: 5 });
    const sum = addUsage(a, b);
    assert.is(sum.costUsd, 1.75);
    assert.is(sum.uncachedInputTokens, 100);
    assert.is(sum.cacheReadTokens, 900);
    assert.is(sum.outputTokens, 15);
    // Both operands are live state (a row's banked figure and the session total), so a
    // caller must be able to add without either being altered underneath it.
    assert.is(a.costUsd, 1.5);
    assert.is(b.cacheReadTokens, 900);
});
suite('addUsage with an empty operand is the identity', () => {
    const a = Object.assign(Object.assign({}, emptyUsage()), { costUsd: 3, cacheWriteTokens: 42 });
    assert.equal(addUsage(a, emptyUsage()), a);
    assert.equal(addUsage(emptyUsage(), a), a);
});
suite.run();
