/**
 * @license
 * Copyright 2026 Google LLC
 * SPDX-License-Identifier: Apache-2.0
 */
import { describe, it, expect, vi } from 'vitest';
import { render } from './render.js';
import { NodeType } from './types.js';
import { ContextWorkingBufferImpl } from '../pipeline/contextWorkingBuffer.js';
describe('render', () => {
    it('should render all provided nodes', async () => {
        const mockNodes = [
            {
                id: '1',
                type: NodeType.USER_PROMPT,
                payload: {},
            },
            {
                id: '2',
                type: NodeType.AGENT_THOUGHT,
                payload: {},
            },
            {
                id: 'preview-1',
                type: NodeType.USER_PROMPT,
                payload: {},
            },
        ];
        const orchestrator = {};
        const sidecar = { config: {} }; // No budget
        const mockAdvancedTokenCalculator = {
            calculateTokensAndBaseUnits: vi.fn().mockReturnValue({
                tokens: 100,
                baseUnits: 100,
            }),
            getRawBaseUnits: vi.fn().mockReturnValue(100),
            calculateConcreteListTokens: vi.fn().mockReturnValue(100),
            getRawBaseUnitsForContent: vi.fn().mockReturnValue(0),
        };
        const env = {
            tokenCalculator: {
                calculateConcreteListTokens: vi.fn().mockReturnValue(100),
                calculateTokenBreakdown: vi.fn().mockReturnValue({}),
            },
            graphMapper: {
                fromGraph: vi.fn((nodes) => nodes.map((n) => ({ text: n.id }))),
            },
        };
        const tracer = {
            logEvent: vi.fn(),
        };
        const result = await render(mockNodes, orchestrator, sidecar, tracer, env, mockAdvancedTokenCalculator, {
            protectionReasons: new Map(),
            header: undefined,
        });
        expect(result.history).toEqual([
            { text: '1' },
            { text: '2' },
            { text: 'preview-1' },
        ]);
        expect(result.baseUnits).toBe(100);
    });
    it('simulates the boundary knapsack problem (loose boundary policy)', async () => {
        // 10k, 20k, 40k, 5k
        const mockNodes = [
            {
                id: 'D',
                type: NodeType.USER_PROMPT,
                payload: {},
            },
            {
                id: 'C',
                type: NodeType.AGENT_THOUGHT,
                payload: {},
            },
            {
                id: 'B',
                type: NodeType.USER_PROMPT,
                payload: {},
            },
            {
                id: 'A',
                type: NodeType.AGENT_THOUGHT,
                payload: {},
            },
        ];
        const tokenMap = {
            D: 5000,
            C: 40000,
            B: 20000,
            A: 10000,
        };
        const orchestrator = {
            executeTriggerSync: vi.fn(async (trigger, buffer, agedOutNodes) => {
                const filteredNodes = buffer.nodes.filter((n) => !agedOutNodes.has(n.id));
                return ContextWorkingBufferImpl.initialize(filteredNodes);
            }),
        };
        const sidecar = {
            config: {
                budget: { maxTokens: 150000, retainedTokens: 65000 },
            },
        };
        const currentTokens = 160000;
        const mockAdvancedTokenCalculator = {
            calculateTokensAndBaseUnits: vi.fn((nodes) => {
                const tokens = nodes.length === 1 ? tokenMap[nodes[0].id] : currentTokens;
                return { tokens, baseUnits: tokens };
            }),
            getRawBaseUnits: vi.fn((nodes) => {
                if (nodes.length === 1)
                    return tokenMap[nodes[0].id];
                return currentTokens;
            }),
            calculateConcreteListTokens: vi.fn((nodes) => {
                if (nodes.length === 1)
                    return tokenMap[nodes[0].id];
                return currentTokens;
            }),
        };
        const env = {
            llmClient: {
                countTokens: vi.fn().mockResolvedValue({ totalTokens: 1000 }),
            },
            tokenCalculator: {
                calculateConcreteListTokens: vi.fn((nodes) => {
                    if (nodes.length === 1)
                        return tokenMap[nodes[0].id];
                    return currentTokens;
                }),
                calculateTokenBreakdown: vi.fn(() => ({})),
            },
            graphMapper: {
                fromGraph: vi.fn((nodes) => nodes.map((n) => ({ text: n.id }))),
            },
        };
        const tracer = {
            logEvent: vi.fn(),
        };
        const result = await render(mockNodes, orchestrator, sidecar, tracer, env, mockAdvancedTokenCalculator, {
            protectionReasons: new Map(),
            header: undefined,
        });
        // eslint-disable-next-line @typescript-eslint/no-explicit-any
        const surviving = result.history.map((c) => c.text);
        // Loose Boundary: A (10k), B (20k), C (40k). Total = 70k.
        // Adding C pushes rolling total (70k) above retainedTokens (65k).
        // Under loose policy, C survives. D is strictly older and drops.
        expect(surviving).toEqual(['C', 'B', 'A']); // D is dropped
        expect(result.baseUnits).toBe(160000);
    });
    it('drops nodes that are STRICTLY older than the boundary node', async () => {
        const mockNodes = [
            {
                id: 'A',
                type: NodeType.USER_PROMPT,
                payload: {},
            },
            {
                id: 'B',
                type: NodeType.AGENT_THOUGHT,
                payload: {},
            },
            {
                id: 'C',
                type: NodeType.USER_PROMPT,
                payload: {},
            },
        ];
        const tokenMap = {
            C: 40000,
            B: 40000,
            A: 10000,
        };
        const orchestrator = {
            executeTriggerSync: vi.fn(async (trigger, buffer, agedOutNodes) => {
                const filteredNodes = buffer.nodes.filter((n) => !agedOutNodes.has(n.id));
                return ContextWorkingBufferImpl.initialize(filteredNodes);
            }),
        };
        const sidecar = {
            config: {
                budget: { maxTokens: 150000, retainedTokens: 65000 },
            },
        };
        const currentTokens = 160000;
        const mockAdvancedTokenCalculator = {
            calculateTokensAndBaseUnits: vi.fn((nodes) => {
                const tokens = nodes.length === 1 ? tokenMap[nodes[0].id] : currentTokens;
                return { tokens, baseUnits: tokens };
            }),
            getRawBaseUnits: vi.fn((nodes) => {
                if (nodes.length === 1)
                    return tokenMap[nodes[0].id];
                return currentTokens;
            }),
            calculateConcreteListTokens: vi.fn((nodes) => {
                if (nodes.length === 1)
                    return tokenMap[nodes[0].id];
                return currentTokens;
            }),
        };
        const env = {
            llmClient: {
                countTokens: vi.fn().mockResolvedValue({ totalTokens: 1000 }),
            },
            tokenCalculator: {
                calculateConcreteListTokens: vi.fn((nodes) => {
                    if (nodes.length === 1)
                        return tokenMap[nodes[0].id];
                    return currentTokens;
                }),
                calculateTokenBreakdown: vi.fn(() => ({})),
            },
            graphMapper: {
                fromGraph: vi.fn((nodes) => nodes.map((n) => ({ text: n.id }))),
            },
        };
        const tracer = {
            logEvent: vi.fn(),
        };
        const result = await render(mockNodes, orchestrator, sidecar, tracer, env, mockAdvancedTokenCalculator, {
            protectionReasons: new Map(),
            header: undefined,
        });
        // eslint-disable-next-line @typescript-eslint/no-explicit-any
        const surviving = result.history.map((c) => c.text);
        // C(40k), B(40k). Adding B pushes total to 80k. B is the boundary node and survives. A drops.
        expect(surviving).toEqual(['B', 'C']); // A is dropped
        expect(result.baseUnits).toBe(160000);
    });
    it('should exclude the last turn when lateBindPrompt is true', async () => {
        const mockNodes = [
            {
                id: '1',
                type: NodeType.USER_PROMPT,
                turnId: 'turn-1',
                payload: {},
            },
            {
                id: '2',
                type: NodeType.AGENT_THOUGHT,
                turnId: 'turn-2',
                payload: {},
            },
        ];
        const orchestrator = {
            executeTriggerSync: vi.fn(async (trigger, buffer) => buffer),
        };
        const sidecar = { config: {} }; // No budget
        const mockAdvancedTokenCalculator = {
            calculateTokensAndBaseUnits: vi.fn().mockReturnValue({
                tokens: 100,
                baseUnits: 100,
            }),
            getRawBaseUnits: vi.fn().mockReturnValue(50),
            calculateConcreteListTokens: vi.fn().mockReturnValue(100),
            getRawBaseUnitsForContent: vi.fn().mockReturnValue(0),
        };
        const env = {
            tokenCalculator: {
                calculateConcreteListTokens: vi.fn().mockReturnValue(100),
                calculateTokenBreakdown: vi.fn().mockReturnValue({}),
            },
            graphMapper: {
                fromGraph: vi.fn((nodes) => nodes.map((n) => ({ text: n.id }))),
            },
        };
        const tracer = {
            logEvent: vi.fn(),
        };
        const result = await render(mockNodes, orchestrator, sidecar, tracer, env, mockAdvancedTokenCalculator, {
            lateBindPrompt: true,
        });
        expect(result.history).toEqual([{ text: '1' }]); // Turn 2 (node 2) is excluded
        expect(result.pendingHistory).toEqual([{ text: '2' }]); // Turn 2 is included here
        expect(result.baseUnits).toBe(50);
    });
});
//# sourceMappingURL=render.test.js.map