import { assert, createLogicSuite } from '@genesislcap/foundation-testing';
import { sumUsage } from './sum-usage';
import { usageRows } from './usage-rows';
const msg = (over) => (Object.assign({ role: 'assistant', content: '' }, over));
const rolled = (over) => (Object.assign({ costUsd: 0, uncachedInputTokens: 0, cacheReadTokens: 0, cacheWriteTokens: 0, outputTokens: 0 }, over));
/** Every place spend can hide, in one transcript: a delegation and a compaction. */
const richHistory = () => [
    msg({
        role: 'compacted-summary',
        content: 'summary',
        compaction: {
            compactedCount: 4,
            rolledUpUsage: rolled({ costUsd: 0.05, uncachedInputTokens: 400, outputTokens: 60 }),
            // The summariser's model — NOT what the banked spend ran on.
            model: 'claude-haiku-4-5-20251001',
        },
    }),
    msg({ role: 'user', content: 'go' }),
    msg({
        model: 'claude-sonnet-5',
        agentName: 'boss',
        cost: 0.01,
        inputTokens: 1000,
        cacheReadTokens: 900,
        outputTokens: 50,
        toolCalls: [
            {
                id: 'tc1',
                name: 'delegate',
                args: {},
                subAgentTrace: [
                    msg({
                        model: 'gemini-2.5-flash',
                        agentName: 'worker',
                        cost: 0.12,
                        inputTokens: 500,
                        outputTokens: 200,
                    }),
                ],
            },
        ],
    }),
];
const suite = createLogicSuite('usageRows');
suite('reconciles exactly with sumUsage over the same input', () => {
    // THE guarantee. A per-call view that disagrees with the total is worse than none,
    // because both look right in isolation — which is how the consumer's own copy
    // under-reported for a month.
    const history = richHistory();
    const rows = usageRows(history);
    const total = sumUsage(history);
    const rowCost = rows.reduce((n, r) => { var _a, _b; return n + ((_a = r.costUsd) !== null && _a !== void 0 ? _a : 0) + ((_b = r.externalCostUsd) !== null && _b !== void 0 ? _b : 0); }, 0);
    assert.ok(Math.abs(rowCost - total.costUsd) < 1e-12, `${rowCost} vs ${total.costUsd}`);
    const bucket = (k) => rows.reduce((n, r) => n + r[k], 0);
    assert.is(bucket('uncachedInputTokens'), total.uncachedInputTokens, 'uncached input');
    assert.is(bucket('cacheReadTokens'), total.cacheReadTokens, 'cache read');
    assert.is(bucket('cacheWriteTokens'), total.cacheWriteTokens, 'cache write');
    assert.is(bucket('outputTokens'), total.outputTokens, 'output');
});
suite('emits a compaction row so banked spend is not lost from a per-call view', () => {
    // The shortfall this function exists to close: the summary message carries no
    // `cost` and no token fields of its own, so a naive per-message walk drops the
    // spend of every turn the compaction deleted.
    const rows = usageRows(richHistory());
    const compactionRows = rows.filter((r) => r.source === 'compaction');
    assert.is(compactionRows.length, 1);
    assert.is(compactionRows[0].costUsd, 0.05);
    assert.is(compactionRows[0].uncachedInputTokens, 400);
    assert.is(compactionRows[0].model, undefined, 'no model — the banked spend may span several, and compaction.model is the summariser');
});
suite('recurses into sub-agent traces, tagging depth and the spawning call', () => {
    const rows = usageRows(richHistory());
    const child = rows.find((r) => r.model === 'gemini-2.5-flash');
    assert.ok(child, 'the sub-agent turn produced a row');
    assert.is(child.subAgentDepth, 1);
    assert.is(child.subAgentOf, 'tc1');
    assert.is(child.agentName, 'worker');
    const parent = rows.find((r) => r.model === 'claude-sonnet-5');
    assert.is(parent.subAgentDepth, 0);
    assert.is(parent.subAgentOf, undefined);
});
suite('prefers the provider the driver stamped over re-deriving it from the model', () => {
    // `ChatMessage.provider` comes from the resolved provider's own status, so it is
    // authoritative and knows vendors the model allowlists do not. Re-deriving from the
    // model id alone drops attribution for a server-proxied turn — exactly the rows a
    // usage ledger cares most about getting right. PR review.
    const rows = usageRows([
        msg({ model: 'gpt-5', provider: 'openai', cost: 1, inputTokens: 1 }),
        // Stamped provider disagrees with what the model id would derive: the stamp still wins.
        msg({
            model: 'claude-sonnet-5',
            provider: 'chrome',
            cost: 1,
            inputTokens: 1,
        }),
    ]);
    assert.equal(rows.map((r) => r.provider), ['openai', 'chrome']);
});
suite('resolves the provider from the model, and leaves it unset when unknown', () => {
    const rows = usageRows([
        msg({ model: 'claude-sonnet-5', cost: 1, inputTokens: 1 }),
        msg({ model: 'gemini-2.5-flash', cost: 1, inputTokens: 1 }),
        msg({ model: 'gpt-5', cost: 1, inputTokens: 1 }),
        msg({ cost: 1, inputTokens: 1 }),
    ]);
    assert.equal(rows.map((r) => r.provider), ['anthropic', 'gemini', undefined, undefined], 'an unrecognised or absent model leaves provider unset rather than guessing');
});
suite('splits the prompt into buckets the same way sumUsage does', () => {
    const [row] = usageRows([
        msg({
            model: 'claude-sonnet-5',
            inputTokens: 1000,
            cacheReadTokens: 600,
            cacheWriteTokens: 300,
        }),
    ]);
    assert.is(row.uncachedInputTokens, 100, 'the remainder, not an addition');
    assert.is(row.cacheReadTokens, 600);
    assert.is(row.cacheWriteTokens, 300);
});
suite('clamps a prompt whose cache buckets exceed the total', () => {
    const [row] = usageRows([msg({ inputTokens: 100, cacheReadTokens: 900 })]);
    assert.is(row.uncachedInputTokens, 0, 'never negative');
});
suite('leaves costUsd undefined when nothing was reported, rather than zero', () => {
    // What the "carried usage but no cost" audit keys off. A zero here would read as a
    // free call and the alarm would never fire.
    const [row] = usageRows([msg({ model: 'claude-sonnet-5', inputTokens: 500, outputTokens: 10 })]);
    assert.is(row.costUsd, undefined);
    assert.is(row.source, 'request');
});
suite('records external (non-LLM) cost and counts it in the reconciliation', () => {
    const history = [msg({ externalCostUsd: 0.4 }), msg({ model: 'claude-sonnet-5', cost: 0.1 })];
    const rows = usageRows(history);
    assert.is(rows.find((r) => r.externalCostUsd != null).externalCostUsd, 0.4);
    const rowCost = rows.reduce((n, r) => { var _a, _b; return n + ((_a = r.costUsd) !== null && _a !== void 0 ? _a : 0) + ((_b = r.externalCostUsd) !== null && _b !== void 0 ? _b : 0); }, 0);
    assert.ok(Math.abs(rowCost - sumUsage(history).costUsd) < 1e-12);
});
suite('produces no row for a message carrying no usage', () => {
    assert.equal(usageRows([
        msg({ role: 'user', content: 'hello' }),
        msg({ content: 'thinking out loud', category: 'reasoning' }),
    ]), []);
});
suite('is empty for an empty transcript', () => {
    assert.equal(usageRows([]), []);
});
suite('reconciles on a nested delegation two levels deep', () => {
    const history = [
        msg({
            model: 'claude-sonnet-5',
            cost: 0.01,
            inputTokens: 100,
            toolCalls: [
                {
                    id: 'a',
                    name: 'delegate',
                    args: {},
                    subAgentTrace: [
                        msg({
                            model: 'claude-sonnet-5',
                            cost: 0.02,
                            inputTokens: 200,
                            toolCalls: [
                                {
                                    id: 'b',
                                    name: 'delegate',
                                    args: {},
                                    subAgentTrace: [msg({ model: 'gemini-2.5-flash', cost: 0.04, inputTokens: 400 })],
                                },
                            ],
                        }),
                    ],
                },
            ],
        }),
    ];
    const rows = usageRows(history);
    assert.equal(rows.map((r) => r.subAgentDepth), [0, 1, 2]);
    const rowCost = rows.reduce((n, r) => { var _a; return n + ((_a = r.costUsd) !== null && _a !== void 0 ? _a : 0); }, 0);
    assert.ok(Math.abs(rowCost - sumUsage(history).costUsd) < 1e-12, 'still reconciles');
});
suite('does not mutate the transcript', () => {
    const history = richHistory();
    const before = JSON.stringify(history);
    usageRows(history);
    assert.is(JSON.stringify(history), before, 'a projection, not a transform');
});
suite.run();
