import { describe, it, expect, vi, beforeEach } from 'vitest';
import { metrics as otelMetrics } from '@opentelemetry/api';
import { Meter, AgentMetrics } from '../meter.js';
import { MockMeter } from '../../__fixtures__/mock-meter.js';
describe('Meter', () => {
    const makeTool = (name, toolUseId) => ({
        name,
        toolUseId,
        input: {},
    });
    let meter;
    beforeEach(() => {
        meter = new Meter();
    });
    describe('metrics getter', () => {
        it('returns an AgentMetrics instance', () => {
            expect(meter.metrics).toBeInstanceOf(AgentMetrics);
        });
        it('returns zeroed snapshot for fresh instance', () => {
            const snapshot = meter.metrics;
            expect(snapshot).toStrictEqual(new AgentMetrics({
                cycleCount: 0,
                toolMetrics: {},
                agentInvocations: [],
                accumulatedUsage: { inputTokens: 0, outputTokens: 0, totalTokens: 0 },
                accumulatedMetrics: { latencyMs: 0 },
            }));
        });
        it('returns complete snapshot after a realistic agent execution', () => {
            vi.useFakeTimers();
            vi.setSystemTime(100_000);
            meter.startNewInvocation();
            const c1 = meter.startCycle();
            meter.updateCycle({
                type: 'modelMetadataEvent',
                usage: { inputTokens: 10, outputTokens: 5, totalTokens: 15 },
                metrics: { latencyMs: 100 },
            });
            meter.endToolCall({
                tool: makeTool('search', 'tid-1'),
                duration: 0.5,
                success: true,
            });
            vi.setSystemTime(103_000);
            meter.endCycle(c1.startTime);
            vi.setSystemTime(200_000);
            const c2 = meter.startCycle();
            meter.updateCycle({
                type: 'modelMetadataEvent',
                usage: { inputTokens: 20, outputTokens: 10, totalTokens: 30 },
                metrics: { latencyMs: 250 },
            });
            meter.endToolCall({
                tool: makeTool('search', 'tid-2'),
                duration: 1.5,
                success: false,
            });
            vi.setSystemTime(205_000);
            meter.endCycle(c2.startTime);
            const snapshot = meter.metrics;
            expect(snapshot.cycleCount).toBe(2);
            expect(snapshot.totalDuration).toBe(8000);
            expect(snapshot.accumulatedUsage).toStrictEqual({ inputTokens: 30, outputTokens: 15, totalTokens: 45 });
            expect(snapshot.accumulatedMetrics).toStrictEqual({ latencyMs: 350 });
            expect(snapshot.toolMetrics).toStrictEqual({
                search: {
                    callCount: 2,
                    successCount: 1,
                    errorCount: 1,
                    totalTime: 2.0,
                },
            });
            expect(snapshot.agentInvocations).toStrictEqual([
                {
                    usage: { inputTokens: 30, outputTokens: 15, totalTokens: 45 },
                    cycles: [
                        { cycleId: 'cycle-1', duration: 3000, usage: { inputTokens: 10, outputTokens: 5, totalTokens: 15 } },
                        { cycleId: 'cycle-2', duration: 5000, usage: { inputTokens: 20, outputTokens: 10, totalTokens: 30 } },
                    ],
                },
            ]);
            vi.useRealTimers();
        });
        it('tracks multiple invocations independently', () => {
            meter.startNewInvocation();
            meter.startCycle();
            meter.updateCycle({
                type: 'modelMetadataEvent',
                usage: { inputTokens: 10, outputTokens: 5, totalTokens: 15 },
            });
            meter.startNewInvocation();
            meter.startCycle();
            meter.updateCycle({
                type: 'modelMetadataEvent',
                usage: { inputTokens: 20, outputTokens: 10, totalTokens: 30 },
            });
            expect(meter.metrics.agentInvocations).toStrictEqual([
                {
                    usage: { inputTokens: 10, outputTokens: 5, totalTokens: 15 },
                    cycles: [{ cycleId: 'cycle-1', duration: 0, usage: { inputTokens: 10, outputTokens: 5, totalTokens: 15 } }],
                },
                {
                    usage: { inputTokens: 20, outputTokens: 10, totalTokens: 30 },
                    cycles: [{ cycleId: 'cycle-2', duration: 0, usage: { inputTokens: 20, outputTokens: 10, totalTokens: 30 } }],
                },
            ]);
        });
    });
    describe('startNewInvocation', () => {
        it('appends an invocation with empty cycles and zeroed usage', () => {
            meter.startNewInvocation();
            expect(meter.metrics.agentInvocations).toStrictEqual([
                { cycles: [], usage: { inputTokens: 0, outputTokens: 0, totalTokens: 0 } },
            ]);
        });
        it('latestAgentInvocation returns the most recently added invocation', () => {
            meter.startNewInvocation();
            meter.startNewInvocation();
            const snapshot = meter.metrics;
            expect(snapshot.agentInvocations).toHaveLength(2);
            expect(snapshot.latestAgentInvocation).toBe(snapshot.agentInvocations[1]);
        });
        it('evicts oldest entries when exceeding max history cap', () => {
            for (let i = 0; i < 60; i++) {
                meter.startNewInvocation();
                meter.startCycle();
            }
            const invocations = meter.metrics.agentInvocations;
            expect(invocations).toHaveLength(50);
            // First retained entry is the 11th invocation (oldest 10 were evicted)
            expect(invocations[0].cycles[0].cycleId).toBe('cycle-11');
            expect(invocations[49].cycles[0].cycleId).toBe('cycle-60');
        });
    });
    describe('startCycle', () => {
        it('returns cycle id and start time', () => {
            vi.useFakeTimers();
            vi.setSystemTime(100_000);
            const result = meter.startCycle();
            expect(result).toStrictEqual({
                cycleId: 'cycle-1',
                startTime: 100_000,
            });
            expect(meter.metrics.cycleCount).toBe(1);
            vi.useRealTimers();
        });
        it('adds cycle entry to the latest invocation', () => {
            meter.startNewInvocation();
            meter.startCycle();
            expect(meter.metrics.latestAgentInvocation.cycles).toStrictEqual([
                { cycleId: 'cycle-1', duration: 0, usage: { inputTokens: 0, outputTokens: 0, totalTokens: 0 } },
            ]);
        });
        it('does not fail when no invocation exists', () => {
            const result = meter.startCycle();
            expect(result.cycleId).toBe('cycle-1');
            expect(meter.metrics.agentInvocations).toStrictEqual([]);
        });
    });
    describe('endCycle', () => {
        it('records duration on the latest cycle', () => {
            vi.useFakeTimers();
            vi.setSystemTime(200_000);
            meter.startNewInvocation();
            meter.startCycle();
            meter.endCycle(100_000);
            expect(meter.metrics.latestAgentInvocation.cycles[0].duration).toBe(100_000);
            vi.useRealTimers();
        });
        it('does not fail when no invocation exists', () => {
            vi.useFakeTimers();
            vi.setSystemTime(200_000);
            meter.startCycle();
            expect(() => meter.endCycle(100_000)).not.toThrow();
            expect(meter.metrics.agentInvocations).toStrictEqual([]);
            vi.useRealTimers();
        });
        it('does not fail when invocation has no cycles', () => {
            vi.useFakeTimers();
            vi.setSystemTime(200_000);
            meter.startNewInvocation();
            expect(() => meter.endCycle(100_000)).not.toThrow();
            expect(meter.metrics.latestAgentInvocation.cycles).toStrictEqual([]);
            vi.useRealTimers();
        });
    });
    describe('endToolCall', () => {
        it('records success', () => {
            meter.endToolCall({
                tool: makeTool('myTool', 'id-1'),
                duration: 1.5,
                success: true,
            });
            expect(meter.metrics.toolMetrics).toStrictEqual({
                myTool: { callCount: 1, successCount: 1, errorCount: 0, totalTime: 1.5 },
            });
        });
        it('records failure', () => {
            meter.endToolCall({
                tool: makeTool('myTool', 'id-1'),
                duration: 0.5,
                success: false,
            });
            expect(meter.metrics.toolMetrics).toStrictEqual({
                myTool: { callCount: 1, successCount: 0, errorCount: 1, totalTime: 0.5 },
            });
        });
        it('accumulates across multiple calls to the same tool', () => {
            meter.endToolCall({
                tool: makeTool('myTool', 'id-1'),
                duration: 1.0,
                success: true,
            });
            meter.endToolCall({
                tool: makeTool('myTool', 'id-2'),
                duration: 2.0,
                success: false,
            });
            expect(meter.metrics.toolMetrics).toStrictEqual({
                myTool: { callCount: 2, successCount: 1, errorCount: 1, totalTime: 3.0 },
            });
        });
        it('tracks different tools independently', () => {
            meter.endToolCall({
                tool: makeTool('toolA', 'id-1'),
                duration: 1.0,
                success: true,
            });
            meter.endToolCall({
                tool: makeTool('toolB', 'id-2'),
                duration: 2.0,
                success: false,
            });
            expect(meter.metrics.toolMetrics).toStrictEqual({
                toolA: { callCount: 1, successCount: 1, errorCount: 0, totalTime: 1.0 },
                toolB: { callCount: 1, successCount: 0, errorCount: 1, totalTime: 2.0 },
            });
        });
    });
    describe('latestContextSize', () => {
        it('is undefined when no invocations have occurred', () => {
            expect(meter.metrics.latestContextSize).toBeUndefined();
        });
        it('returns the most recent inputTokens after a model call', () => {
            meter.updateCycle({
                type: 'modelMetadataEvent',
                usage: { inputTokens: 100, outputTokens: 50, totalTokens: 150 },
            });
            expect(meter.metrics.latestContextSize).toBe(100);
        });
        it('updates across multiple cycles', () => {
            meter.updateCycle({
                type: 'modelMetadataEvent',
                usage: { inputTokens: 100, outputTokens: 50, totalTokens: 150 },
            });
            meter.updateCycle({
                type: 'modelMetadataEvent',
                usage: { inputTokens: 200, outputTokens: 80, totalTokens: 280 },
            });
            expect(meter.metrics.latestContextSize).toBe(200);
        });
        it('updates across multiple invocations', () => {
            meter.startNewInvocation();
            meter.startCycle();
            meter.updateCycle({
                type: 'modelMetadataEvent',
                usage: { inputTokens: 100, outputTokens: 50, totalTokens: 150 },
            });
            meter.startNewInvocation();
            meter.startCycle();
            meter.updateCycle({
                type: 'modelMetadataEvent',
                usage: { inputTokens: 300, outputTokens: 100, totalTokens: 400 },
            });
            expect(meter.metrics.latestContextSize).toBe(300);
        });
        it('remains undefined when metadata has no usage', () => {
            meter.updateCycle({
                type: 'modelMetadataEvent',
                metrics: { latencyMs: 100 },
            });
            expect(meter.metrics.latestContextSize).toBeUndefined();
        });
        it('remains undefined when updateCycle is called with undefined', () => {
            meter.updateCycle(undefined);
            expect(meter.metrics.latestContextSize).toBeUndefined();
        });
    });
    describe('projectedContextSize', () => {
        it('is undefined when no invocations have occurred', () => {
            expect(meter.metrics.projectedContextSize).toBeUndefined();
        });
        it('returns inputTokens + outputTokens after a model call', () => {
            meter.updateCycle({
                type: 'modelMetadataEvent',
                usage: { inputTokens: 100, outputTokens: 50, totalTokens: 150 },
            });
            expect(meter.metrics.projectedContextSize).toBe(150);
        });
        it('updates across multiple cycles', () => {
            meter.updateCycle({
                type: 'modelMetadataEvent',
                usage: { inputTokens: 100, outputTokens: 50, totalTokens: 150 },
            });
            meter.updateCycle({
                type: 'modelMetadataEvent',
                usage: { inputTokens: 200, outputTokens: 80, totalTokens: 280 },
            });
            expect(meter.metrics.projectedContextSize).toBe(280);
        });
    });
    describe('updateCycle', () => {
        it('accumulates usage and latency from metadata', () => {
            meter.updateCycle({
                type: 'modelMetadataEvent',
                usage: { inputTokens: 5, outputTokens: 3, totalTokens: 8 },
                metrics: { latencyMs: 100 },
            });
            meter.updateCycle({
                type: 'modelMetadataEvent',
                usage: { inputTokens: 10, outputTokens: 7, totalTokens: 17 },
                metrics: { latencyMs: 200 },
            });
            expect(meter.metrics.accumulatedUsage).toStrictEqual({
                inputTokens: 15,
                outputTokens: 10,
                totalTokens: 25,
            });
            expect(meter.metrics.accumulatedMetrics).toStrictEqual({ latencyMs: 300 });
        });
        it('accumulates cache tokens across calls', () => {
            meter.updateCycle({
                type: 'modelMetadataEvent',
                usage: {
                    inputTokens: 10,
                    outputTokens: 5,
                    totalTokens: 15,
                    cacheReadInputTokens: 3,
                    cacheWriteInputTokens: 2,
                },
            });
            meter.updateCycle({
                type: 'modelMetadataEvent',
                usage: {
                    inputTokens: 5,
                    outputTokens: 2,
                    totalTokens: 7,
                    cacheReadInputTokens: 4,
                },
            });
            expect(meter.metrics.accumulatedUsage).toStrictEqual({
                inputTokens: 15,
                outputTokens: 7,
                totalTokens: 22,
                cacheReadInputTokens: 7,
                cacheWriteInputTokens: 2,
            });
        });
        it('handles usage-only metadata', () => {
            meter.updateCycle({
                type: 'modelMetadataEvent',
                usage: { inputTokens: 10, outputTokens: 5, totalTokens: 15 },
            });
            expect(meter.metrics.accumulatedUsage).toStrictEqual({
                inputTokens: 10,
                outputTokens: 5,
                totalTokens: 15,
            });
            expect(meter.metrics.accumulatedMetrics).toStrictEqual({ latencyMs: 0 });
        });
        it('handles metrics-only metadata', () => {
            meter.updateCycle({
                type: 'modelMetadataEvent',
                metrics: { latencyMs: 250 },
            });
            expect(meter.metrics.accumulatedUsage).toStrictEqual({
                inputTokens: 0,
                outputTokens: 0,
                totalTokens: 0,
            });
            expect(meter.metrics.accumulatedMetrics).toStrictEqual({ latencyMs: 250 });
        });
        it('propagates usage to invocation and current cycle', () => {
            meter.startNewInvocation();
            meter.startCycle();
            meter.updateCycle({
                type: 'modelMetadataEvent',
                usage: { inputTokens: 10, outputTokens: 5, totalTokens: 15 },
            });
            const invocation = meter.metrics.latestAgentInvocation;
            expect(invocation).toStrictEqual({
                usage: { inputTokens: 10, outputTokens: 5, totalTokens: 15 },
                cycles: [{ cycleId: 'cycle-1', duration: 0, usage: { inputTokens: 10, outputTokens: 5, totalTokens: 15 } }],
            });
        });
        it('is a no-op when metadata is undefined', () => {
            meter.updateCycle(undefined);
            expect(meter.metrics.accumulatedUsage).toStrictEqual({
                inputTokens: 0,
                outputTokens: 0,
                totalTokens: 0,
            });
            expect(meter.metrics.accumulatedMetrics).toStrictEqual({ latencyMs: 0 });
        });
        it('is a no-op when called with no arguments', () => {
            meter.updateCycle();
            expect(meter.metrics.accumulatedUsage).toStrictEqual({
                inputTokens: 0,
                outputTokens: 0,
                totalTokens: 0,
            });
            expect(meter.metrics.accumulatedMetrics).toStrictEqual({ latencyMs: 0 });
        });
        it('is a no-op when metadata has neither usage nor metrics', () => {
            meter.updateCycle({ type: 'modelMetadataEvent' });
            expect(meter.metrics.accumulatedUsage).toStrictEqual({
                inputTokens: 0,
                outputTokens: 0,
                totalTokens: 0,
            });
            expect(meter.metrics.accumulatedMetrics).toStrictEqual({ latencyMs: 0 });
        });
        it('does not fail when no invocation exists', () => {
            expect(() => {
                meter.updateCycle({
                    type: 'modelMetadataEvent',
                    usage: { inputTokens: 10, outputTokens: 5, totalTokens: 15 },
                });
            }).not.toThrow();
            expect(meter.metrics.accumulatedUsage).toStrictEqual({
                inputTokens: 10,
                outputTokens: 5,
                totalTokens: 15,
            });
        });
    });
    describe('OTEL instrument emission', () => {
        let mockMeter;
        beforeEach(() => {
            mockMeter = new MockMeter();
            vi.spyOn(otelMetrics, 'getMeter').mockReturnValue(mockMeter);
        });
        it('emits invocation counter on startNewInvocation', () => {
            const m = new Meter();
            m.startNewInvocation();
            expect(mockMeter.getCounter('gen_ai.agent.invocation.count')?.sum).toBe(1);
        });
        it('emits cycle counter on startCycle', () => {
            const m = new Meter();
            m.startCycle();
            expect(mockMeter.getCounter('gen_ai.agent.cycle.count')?.sum).toBe(1);
        });
        it('emits cycle duration histogram on endCycle', () => {
            vi.useFakeTimers();
            vi.setSystemTime(5000);
            const m = new Meter();
            m.endCycle(3000);
            expect(mockMeter.getHistogram('gen_ai.agent.cycle.duration')?.sum).toBe(2000);
            vi.useRealTimers();
        });
        it('emits tool call counter and duration on successful endToolCall', () => {
            const m = new Meter();
            m.endToolCall({ tool: makeTool('search', 'id-1'), duration: 150, success: true });
            expect(mockMeter.getCounter('gen_ai.agent.tool.call.count')?.dataPoints).toStrictEqual([
                { value: 1, attributes: { 'gen_ai.tool.name': 'search' } },
            ]);
            expect(mockMeter.getHistogram('gen_ai.agent.tool.duration')?.dataPoints).toStrictEqual([
                { value: 150, attributes: { 'gen_ai.tool.name': 'search' } },
            ]);
            expect(mockMeter.getCounter('gen_ai.agent.tool.error.count')?.dataPoints).toStrictEqual([]);
        });
        it('emits tool call counter, error counter, and duration on failed endToolCall', () => {
            const m = new Meter();
            m.endToolCall({ tool: makeTool('search', 'id-1'), duration: 50, success: false });
            expect(mockMeter.getCounter('gen_ai.agent.tool.call.count')?.dataPoints).toStrictEqual([
                { value: 1, attributes: { 'gen_ai.tool.name': 'search' } },
            ]);
            expect(mockMeter.getCounter('gen_ai.agent.tool.error.count')?.dataPoints).toStrictEqual([
                { value: 1, attributes: { 'gen_ai.tool.name': 'search' } },
            ]);
            expect(mockMeter.getHistogram('gen_ai.agent.tool.duration')?.dataPoints).toStrictEqual([
                { value: 50, attributes: { 'gen_ai.tool.name': 'search' } },
            ]);
        });
        it('emits input token counter, output token counter, and model latency on updateCycle', () => {
            const m = new Meter();
            m.updateCycle({
                type: 'modelMetadataEvent',
                usage: { inputTokens: 100, outputTokens: 50, totalTokens: 150 },
                metrics: { latencyMs: 200 },
            });
            expect(mockMeter.getCounter('gen_ai.agent.tokens.input')?.sum).toBe(100);
            expect(mockMeter.getCounter('gen_ai.agent.tokens.output')?.sum).toBe(50);
            expect(mockMeter.getHistogram('gen_ai.agent.model.latency')?.sum).toBe(200);
        });
        it('does not emit token counters or latency when updateCycle has no usage or metrics', () => {
            const m = new Meter();
            m.updateCycle({ type: 'modelMetadataEvent' });
            expect(mockMeter.getCounter('gen_ai.agent.tokens.input')?.dataPoints).toStrictEqual([]);
            expect(mockMeter.getCounter('gen_ai.agent.tokens.output')?.dataPoints).toStrictEqual([]);
            expect(mockMeter.getHistogram('gen_ai.agent.model.latency')?.dataPoints).toStrictEqual([]);
        });
        it('does not emit any OTEL instruments when updateCycle is called with undefined', () => {
            const m = new Meter();
            m.updateCycle(undefined);
            expect(mockMeter.getCounter('gen_ai.agent.tokens.input')?.dataPoints).toStrictEqual([]);
            expect(mockMeter.getCounter('gen_ai.agent.tokens.output')?.dataPoints).toStrictEqual([]);
            expect(mockMeter.getHistogram('gen_ai.agent.model.latency')?.dataPoints).toStrictEqual([]);
        });
        it('emits time-to-first-token histogram in seconds when timeToFirstByteMs is provided', () => {
            const m = new Meter();
            m.updateCycle({
                type: 'modelMetadataEvent',
                metrics: { latencyMs: 500, timeToFirstByteMs: 150 },
            });
            expect(mockMeter.getHistogram('gen_ai.server.time_to_first_token')?.sum).toBeCloseTo(0.15);
        });
        it('does not emit time-to-first-token histogram when timeToFirstByteMs is undefined', () => {
            const m = new Meter();
            m.updateCycle({
                type: 'modelMetadataEvent',
                metrics: { latencyMs: 500 },
            });
            expect(mockMeter.getHistogram('gen_ai.server.time_to_first_token')?.dataPoints).toStrictEqual([]);
        });
        it('does not emit time-to-first-token histogram when timeToFirstByteMs is zero', () => {
            const m = new Meter();
            m.updateCycle({
                type: 'modelMetadataEvent',
                metrics: { latencyMs: 500, timeToFirstByteMs: 0 },
            });
            expect(mockMeter.getHistogram('gen_ai.server.time_to_first_token')?.dataPoints).toStrictEqual([]);
        });
    });
});
describe('AgentMetrics', () => {
    describe('toJSON', () => {
        it('returns complete zeroed data for default instance', () => {
            const metrics = new AgentMetrics();
            expect(metrics.toJSON()).toStrictEqual({
                cycleCount: 0,
                accumulatedUsage: { inputTokens: 0, outputTokens: 0, totalTokens: 0 },
                accumulatedMetrics: { latencyMs: 0 },
                agentInvocations: [],
                toolMetrics: {},
                totalDuration: 0,
            });
        });
        it('returns data from provided metrics', () => {
            const metrics = new AgentMetrics({
                cycleCount: 2,
                totalDuration: 8000,
                toolMetrics: {
                    search: { callCount: 2, successCount: 1, errorCount: 1, totalTime: 2.0 },
                },
                accumulatedUsage: { inputTokens: 30, outputTokens: 15, totalTokens: 45 },
                accumulatedMetrics: { latencyMs: 350 },
                agentInvocations: [
                    {
                        usage: { inputTokens: 30, outputTokens: 15, totalTokens: 45 },
                        cycles: [
                            { cycleId: 'cycle-1', duration: 3000, usage: { inputTokens: 10, outputTokens: 5, totalTokens: 15 } },
                            { cycleId: 'cycle-2', duration: 5000, usage: { inputTokens: 20, outputTokens: 10, totalTokens: 30 } },
                        ],
                    },
                ],
            });
            expect(metrics.toJSON()).toStrictEqual({
                cycleCount: 2,
                accumulatedUsage: { inputTokens: 30, outputTokens: 15, totalTokens: 45 },
                accumulatedMetrics: { latencyMs: 350 },
                agentInvocations: [
                    {
                        usage: { inputTokens: 30, outputTokens: 15, totalTokens: 45 },
                        cycles: [
                            { cycleId: 'cycle-1', duration: 3000, usage: { inputTokens: 10, outputTokens: 5, totalTokens: 15 } },
                            { cycleId: 'cycle-2', duration: 5000, usage: { inputTokens: 20, outputTokens: 10, totalTokens: 30 } },
                        ],
                    },
                ],
                toolMetrics: {
                    search: { callCount: 2, successCount: 1, errorCount: 1, totalTime: 2.0 },
                },
                totalDuration: 8000,
            });
        });
    });
    describe('toJSON with latestContextSize', () => {
        it('includes latestContextSize when set', () => {
            const metrics = new AgentMetrics({ latestContextSize: 42 });
            expect(metrics.toJSON()).toHaveProperty('latestContextSize', 42);
        });
        it('omits latestContextSize when undefined', () => {
            const metrics = new AgentMetrics();
            expect(metrics.toJSON()).not.toHaveProperty('latestContextSize');
        });
    });
    describe('toJSON roundtrip', () => {
        it('reconstructs equivalent AgentMetrics from serialized data', () => {
            const original = new AgentMetrics({
                cycleCount: 3,
                accumulatedUsage: { inputTokens: 50, outputTokens: 25, totalTokens: 75 },
                accumulatedMetrics: { latencyMs: 500 },
                agentInvocations: [
                    {
                        usage: { inputTokens: 50, outputTokens: 25, totalTokens: 75 },
                        cycles: [
                            { cycleId: 'cycle-1', duration: 1000, usage: { inputTokens: 10, outputTokens: 5, totalTokens: 15 } },
                            { cycleId: 'cycle-2', duration: 2000, usage: { inputTokens: 20, outputTokens: 10, totalTokens: 30 } },
                            { cycleId: 'cycle-3', duration: 3000, usage: { inputTokens: 20, outputTokens: 10, totalTokens: 30 } },
                        ],
                    },
                ],
                toolMetrics: {
                    search: { callCount: 2, successCount: 2, errorCount: 0, totalTime: 1.5 },
                    calc: { callCount: 1, successCount: 0, errorCount: 1, totalTime: 0.3 },
                },
            });
            const json = JSON.stringify(original);
            const restored = new AgentMetrics(JSON.parse(json));
            expect(restored.toJSON()).toStrictEqual(original.toJSON());
        });
    });
    describe('computed getters', () => {
        it('latestAgentInvocation returns the last invocation', () => {
            const metrics = new AgentMetrics({
                agentInvocations: [
                    { cycles: [], usage: { inputTokens: 10, outputTokens: 5, totalTokens: 15 } },
                    { cycles: [], usage: { inputTokens: 20, outputTokens: 10, totalTokens: 30 } },
                ],
            });
            expect(metrics.latestAgentInvocation).toBe(metrics.agentInvocations[1]);
        });
        it('latestAgentInvocation returns undefined when empty', () => {
            const metrics = new AgentMetrics();
            expect(metrics.latestAgentInvocation).toBeUndefined();
        });
        it('accumulatedData returns usage and metrics together', () => {
            const metrics = new AgentMetrics({
                accumulatedUsage: { inputTokens: 30, outputTokens: 15, totalTokens: 45 },
                accumulatedMetrics: { latencyMs: 350 },
            });
            expect(metrics.accumulatedData).toStrictEqual({
                usage: { inputTokens: 30, outputTokens: 15, totalTokens: 45 },
                metrics: { latencyMs: 350 },
            });
        });
        it('averageCycleTime computes average', () => {
            const metrics = new AgentMetrics({
                cycleCount: 2,
                totalDuration: 8000,
            });
            expect(metrics.averageCycleTime).toBe(4000);
        });
        it('averageCycleTime returns 0 when no cycles', () => {
            const metrics = new AgentMetrics();
            expect(metrics.averageCycleTime).toBe(0);
        });
        it('toolUsage adds computed averageTime and successRate', () => {
            const metrics = new AgentMetrics({
                toolMetrics: {
                    search: { callCount: 2, successCount: 1, errorCount: 1, totalTime: 2.0 },
                },
            });
            expect(metrics.toolUsage).toStrictEqual({
                search: {
                    callCount: 2,
                    successCount: 1,
                    errorCount: 1,
                    totalTime: 2.0,
                    averageTime: 1.0,
                    successRate: 0.5,
                },
            });
        });
        it('toolUsage returns 0 for averageTime and successRate when callCount is 0', () => {
            const metrics = new AgentMetrics({
                toolMetrics: {
                    broken: { callCount: 0, successCount: 0, errorCount: 0, totalTime: 0 },
                },
            });
            expect(metrics.toolUsage).toStrictEqual({
                broken: {
                    callCount: 0,
                    successCount: 0,
                    errorCount: 0,
                    totalTime: 0,
                    averageTime: 0,
                    successRate: 0,
                },
            });
        });
    });
});
//# sourceMappingURL=meter.test.js.map