/**
 * Tests for cost tracker
 */
import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
import { CostTracker, BudgetExceededError, getTracker, removeTracker, getActiveTrackers, } from "./tracker.js";
// Mock fs/promises
vi.mock("fs/promises", () => ({
    readFile: vi.fn(),
    writeFile: vi.fn(),
    mkdir: vi.fn(),
}));
import { readFile, writeFile, mkdir } from "fs/promises";
describe("CostTracker", () => {
    const certificationId = "cert-test-123";
    const projectPath = "/test/project";
    beforeEach(() => {
        vi.clearAllMocks();
        vi.mocked(mkdir).mockResolvedValue(undefined);
        vi.mocked(writeFile).mockResolvedValue(undefined);
    });
    afterEach(() => {
        // Clean up any trackers
        removeTracker(certificationId, projectPath);
    });
    describe("constructor", () => {
        it("creates tracker with default options", () => {
            const tracker = new CostTracker(certificationId, projectPath);
            expect(tracker.getSummary().totalCost).toBe(0);
            expect(tracker.getSummary().totalCalls).toBe(0);
            expect(tracker.isWithinBudget()).toBe(true);
        });
        it("creates tracker with custom budget", () => {
            const tracker = new CostTracker(certificationId, projectPath, {
                budget: { maxCost: 5.0 },
            });
            expect(tracker.getBudget().maxCost).toBe(5.0);
        });
    });
    describe("recordCall", () => {
        it("records API call and updates summary", () => {
            const tracker = new CostTracker(certificationId, projectPath);
            const record = tracker.recordCall("claude-3.5-sonnet", {
                inputTokens: 1000,
                outputTokens: 500,
                totalTokens: 1500,
            });
            expect(record.model).toBe("claude-3.5-sonnet");
            expect(record.provider).toBe("anthropic");
            expect(record.cost.totalCost).toBeGreaterThan(0);
            const summary = tracker.getSummary();
            expect(summary.totalCalls).toBe(1);
            expect(summary.totalInputTokens).toBe(1000);
            expect(summary.totalOutputTokens).toBe(500);
        });
        it("tracks by agent", () => {
            const tracker = new CostTracker(certificationId, projectPath);
            tracker.recordCall("claude-3.5-sonnet", { inputTokens: 1000, outputTokens: 500, totalTokens: 1500 }, { agent: "security" });
            tracker.recordCall("claude-3.5-sonnet", { inputTokens: 2000, outputTokens: 1000, totalTokens: 3000 }, { agent: "reliability" });
            const summary = tracker.getSummary();
            expect(summary.byAgent["security"]).toBeDefined();
            expect(summary.byAgent["security"].calls).toBe(1);
            expect(summary.byAgent["reliability"]).toBeDefined();
            expect(summary.byAgent["reliability"].calls).toBe(1);
        });
        it("tracks by model", () => {
            const tracker = new CostTracker(certificationId, projectPath);
            tracker.recordCall("claude-3.5-sonnet", {
                inputTokens: 1000,
                outputTokens: 500,
                totalTokens: 1500,
            });
            tracker.recordCall("gpt-4-turbo", {
                inputTokens: 1000,
                outputTokens: 500,
                totalTokens: 1500,
            });
            const summary = tracker.getSummary();
            expect(summary.byModel["claude-3.5-sonnet"]).toBeDefined();
            expect(summary.byModel["gpt-4-turbo"]).toBeDefined();
        });
        it("returns dummy record when disabled", () => {
            const tracker = new CostTracker(certificationId, projectPath, {
                enabled: false,
            });
            const record = tracker.recordCall("claude-3.5-sonnet", {
                inputTokens: 1000,
                outputTokens: 500,
                totalTokens: 1500,
            });
            expect(record.id).toBe("disabled");
            expect(record.cost.totalCost).toBe(0);
            expect(tracker.getSummary().totalCalls).toBe(0);
        });
    });
    describe("budget management", () => {
        it("detects when budget exceeded", () => {
            const tracker = new CostTracker(certificationId, projectPath, {
                budget: { maxCost: 0.001 },
            });
            // This should exceed the $0.001 budget
            tracker.recordCall("claude-3.5-sonnet", {
                inputTokens: 10000,
                outputTokens: 5000,
                totalTokens: 15000,
            });
            expect(tracker.isWithinBudget()).toBe(false);
            expect(tracker.getStatus().exceeded.length).toBeGreaterThan(0);
        });
        it("detects warning threshold", () => {
            const tracker = new CostTracker(certificationId, projectPath, {
                budget: { maxCost: 1.0, warnAtPercent: 50 },
            });
            // Record calls to exceed 50% but not 100%
            // claude-3.5-sonnet: $3/1M input, $15/1M output
            // Need ~$0.50+ to trigger warning
            // 100K input = $0.30, 30K output = $0.45, total = $0.75 (75%)
            tracker.recordCall("claude-3.5-sonnet", {
                inputTokens: 100000,
                outputTokens: 30000,
                totalTokens: 130000,
            });
            const status = tracker.getStatus();
            expect(status.withinBudget).toBe(true);
            expect(status.warnings.length).toBeGreaterThan(0);
        });
        it("throws BudgetExceededError when abortOnExceeded", () => {
            const tracker = new CostTracker(certificationId, projectPath, {
                budget: { maxCost: 0.001, abortOnExceeded: true },
            });
            tracker.recordCall("claude-3.5-sonnet", {
                inputTokens: 10000,
                outputTokens: 5000,
                totalTokens: 15000,
            });
            expect(() => tracker.checkBudget()).toThrow(BudgetExceededError);
        });
        it("does not throw when abortOnExceeded is false", () => {
            const tracker = new CostTracker(certificationId, projectPath, {
                budget: { maxCost: 0.001, abortOnExceeded: false },
            });
            tracker.recordCall("claude-3.5-sonnet", {
                inputTokens: 10000,
                outputTokens: 5000,
                totalTokens: 15000,
            });
            expect(() => tracker.checkBudget()).not.toThrow();
        });
        it("can update budget", () => {
            const tracker = new CostTracker(certificationId, projectPath);
            tracker.setBudget({ maxCost: 20.0 });
            expect(tracker.getBudget().maxCost).toBe(20.0);
        });
        it("enforces token limit", () => {
            const tracker = new CostTracker(certificationId, projectPath, {
                budget: { maxTotalTokens: 1000 },
            });
            tracker.recordCall("claude-3.5-sonnet", {
                inputTokens: 800,
                outputTokens: 300,
                totalTokens: 1100,
            });
            expect(tracker.isWithinBudget()).toBe(false);
        });
        it("enforces call limit", () => {
            const tracker = new CostTracker(certificationId, projectPath, {
                budget: { maxCalls: 2 },
            });
            tracker.recordCall("claude-3.5-sonnet", {
                inputTokens: 100,
                outputTokens: 100,
                totalTokens: 200,
            });
            tracker.recordCall("claude-3.5-sonnet", {
                inputTokens: 100,
                outputTokens: 100,
                totalTokens: 200,
            });
            tracker.recordCall("claude-3.5-sonnet", {
                inputTokens: 100,
                outputTokens: 100,
                totalTokens: 200,
            });
            expect(tracker.isWithinBudget()).toBe(false);
        });
    });
    describe("persistence", () => {
        it("persists state to file", async () => {
            const tracker = new CostTracker(certificationId, projectPath);
            tracker.recordCall("claude-3.5-sonnet", {
                inputTokens: 1000,
                outputTokens: 500,
                totalTokens: 1500,
            });
            await tracker.persist();
            expect(mkdir).toHaveBeenCalled();
            expect(writeFile).toHaveBeenCalled();
        });
        it("loads state from file", async () => {
            const savedState = {
                certificationId,
                projectPath,
                budget: { maxCost: 10.0, warnAtPercent: 80, abortOnExceeded: true },
                calls: [],
                summary: {
                    totalCost: 5.0,
                    totalInputTokens: 100000,
                    totalOutputTokens: 50000,
                    totalCachedTokens: 0,
                    totalCalls: 5,
                    byModel: {},
                    byAgent: {},
                    startedAt: new Date().toISOString(),
                    updatedAt: new Date().toISOString(),
                },
                status: {
                    withinBudget: true,
                    costUsedPercent: 50,
                    tokensUsedPercent: 0,
                    callsUsedPercent: 0,
                    warnings: [],
                    exceeded: [],
                },
            };
            vi.mocked(readFile).mockResolvedValue(JSON.stringify(savedState));
            const tracker = new CostTracker(certificationId, projectPath);
            const loaded = await tracker.load();
            expect(loaded).toBe(true);
            expect(tracker.getSummary().totalCost).toBe(5.0);
            expect(tracker.getSummary().totalCalls).toBe(5);
        });
        it("returns false when load fails", async () => {
            vi.mocked(readFile).mockRejectedValue(new Error("Not found"));
            const tracker = new CostTracker(certificationId, projectPath);
            const loaded = await tracker.load();
            expect(loaded).toBe(false);
        });
    });
    describe("reporting", () => {
        it("generates markdown report", () => {
            const tracker = new CostTracker(certificationId, projectPath);
            tracker.recordCall("claude-3.5-sonnet", { inputTokens: 1000, outputTokens: 500, totalTokens: 1500 }, { agent: "security" });
            const report = tracker.generateReport();
            expect(report).toContain("# Cost Report");
            expect(report).toContain("## Summary");
            expect(report).toContain("Total Cost");
            expect(report).toContain("Budget");
        });
        it("generates compact summary", () => {
            const tracker = new CostTracker(certificationId, projectPath);
            tracker.recordCall("claude-3.5-sonnet", {
                inputTokens: 1000,
                outputTokens: 500,
                totalTokens: 1500,
            });
            const summary = tracker.generateCompactSummary();
            expect(summary).toContain("Cost:");
            expect(summary).toContain("Tokens:");
            expect(summary).toContain("Calls:");
        });
    });
    describe("getCalls", () => {
        it("returns all recorded calls", () => {
            const tracker = new CostTracker(certificationId, projectPath);
            tracker.recordCall("claude-3.5-sonnet", {
                inputTokens: 1000,
                outputTokens: 500,
                totalTokens: 1500,
            });
            tracker.recordCall("gpt-4-turbo", {
                inputTokens: 2000,
                outputTokens: 1000,
                totalTokens: 3000,
            });
            const calls = tracker.getCalls();
            expect(calls.length).toBe(2);
        });
    });
    describe("getState", () => {
        it("returns full state copy", () => {
            const tracker = new CostTracker(certificationId, projectPath);
            tracker.recordCall("claude-3.5-sonnet", {
                inputTokens: 1000,
                outputTokens: 500,
                totalTokens: 1500,
            });
            const state = tracker.getState();
            expect(state.certificationId).toBe(certificationId);
            expect(state.projectPath).toBe(projectPath);
            expect(state.calls.length).toBe(1);
            expect(state.summary.totalCalls).toBe(1);
        });
    });
});
describe("BudgetExceededError", () => {
    it("includes summary and budget in error", () => {
        const summary = {
            totalCost: 15.0,
            totalInputTokens: 100000,
            totalOutputTokens: 50000,
            totalCachedTokens: 0,
            totalCalls: 10,
            byModel: {},
            byAgent: {},
            startedAt: new Date().toISOString(),
            updatedAt: new Date().toISOString(),
        };
        const budget = { maxCost: 10.0, warnAtPercent: 80, abortOnExceeded: true };
        const error = new BudgetExceededError("Budget exceeded", summary, budget);
        expect(error.message).toBe("Budget exceeded");
        expect(error.summary).toBe(summary);
        expect(error.budget).toBe(budget);
        expect(error.name).toBe("BudgetExceededError");
    });
});
describe("Tracker Registry", () => {
    const certId1 = "cert-1";
    const certId2 = "cert-2";
    const projectPath = "/test/path";
    afterEach(() => {
        removeTracker(certId1, projectPath);
        removeTracker(certId2, projectPath);
    });
    describe("getTracker", () => {
        it("returns same tracker for same certification", () => {
            const tracker1 = getTracker(certId1, projectPath);
            const tracker2 = getTracker(certId1, projectPath);
            expect(tracker1).toBe(tracker2);
        });
        it("returns different trackers for different certifications", () => {
            const tracker1 = getTracker(certId1, projectPath);
            const tracker2 = getTracker(certId2, projectPath);
            expect(tracker1).not.toBe(tracker2);
        });
        it("creates tracker with provided options", () => {
            const tracker = getTracker(certId1, projectPath, {
                budget: { maxCost: 25.0 },
            });
            expect(tracker.getBudget().maxCost).toBe(25.0);
        });
    });
    describe("removeTracker", () => {
        it("removes tracker from registry", () => {
            const tracker1 = getTracker(certId1, projectPath);
            removeTracker(certId1, projectPath);
            const tracker2 = getTracker(certId1, projectPath);
            expect(tracker1).not.toBe(tracker2);
        });
    });
    describe("getActiveTrackers", () => {
        it("returns map of active trackers", () => {
            getTracker(certId1, projectPath);
            getTracker(certId2, projectPath);
            const trackers = getActiveTrackers();
            expect(trackers.size).toBe(2);
            expect(trackers.has(`${projectPath}:${certId1}`)).toBe(true);
            expect(trackers.has(`${projectPath}:${certId2}`)).toBe(true);
        });
    });
});
//# sourceMappingURL=tracker.test.js.map