import { describe, it, expect, beforeEach } from 'vitest';
import Fastify from 'fastify';
import { budgetRoutes } from '../../modules/token-accounting/interface-adapters/controllers/http/budget.routes.js';
import { GetBudgetStatusUseCase } from '../../modules/token-accounting/usecases/getBudgetStatus/getBudgetStatus.usecase.js';
import { UpdateBudgetUseCase } from '../../modules/token-accounting/usecases/updateBudget/updateBudget.usecase.js';
import { EnforceBudgetUseCase } from '../../modules/token-accounting/usecases/enforceBudget/enforceBudget.usecase.js';
import { BudgetStatusPresenter } from '../../modules/token-accounting/interface-adapters/presenters/budgetStatus.presenter.js';
import { StubBudgetGateway } from '../../tests/stubs/budget.stub.js';
import { StubTokenUsageGateway } from '../../tests/stubs/tokenUsage.stub.js';
import { TokenUsageRecordFactory } from '../../tests/factories/tokenUsage.factory.js';
const LOCAL_PATH_A = '/tmp/acceptance-project-a';
const LOCAL_PATH_B = '/tmp/acceptance-project-b';
const PROJECT_PATH = 'group/project';
const PLATFORM = 'gitlab';
async function buildContext(now) {
    const budgetGateway = new StubBudgetGateway();
    const tokenUsageGateway = new StubTokenUsageGateway();
    const getBudgetStatus = new GetBudgetStatusUseCase({
        budgetGateway,
        tokenUsageGateway,
    });
    const updateBudget = new UpdateBudgetUseCase({ budgetGateway });
    const enforceBudget = new EnforceBudgetUseCase({ getBudgetStatus });
    const presenter = new BudgetStatusPresenter();
    const app = Fastify();
    await app.register(budgetRoutes, {
        getBudgetStatus,
        updateBudget,
        budgetGateway,
        presenter,
        getRepositories: () => [
            { name: 'repo-a', platform: PLATFORM, remoteUrl: '', localPath: LOCAL_PATH_A, skill: 'review', enabled: true },
            { name: 'repo-b', platform: PLATFORM, remoteUrl: '', localPath: LOCAL_PATH_B, skill: 'review', enabled: true },
        ],
        now: () => now,
    });
    return { app, budgetGateway, tokenUsageGateway, enforceBudget, presenter, now };
}
describe('Acceptance — Spec #163: Token Budget Cap with Live Indicator', () => {
    describe('Scenario 2: Move the slider to 350', () => {
        let context;
        beforeEach(async () => {
            context = await buildContext(new Date('2026-05-20T12:00:00Z'));
            await context.budgetGateway.save({ limitUsd: 200 });
        });
        it('updates the budget to 350 and persists it', async () => {
            const response = await context.app.inject({
                method: 'POST',
                url: '/api/budget',
                payload: { limitUsd: 350 },
            });
            expect(response.statusCode).toBe(200);
            const body = response.json();
            expect(body.success).toBe(true);
            expect(body.limitUsd).toBe(350);
            const persisted = await context.budgetGateway.load();
            expect(persisted).toEqual({ limitUsd: 350 });
            await context.app.close();
        });
    });
    describe('Scenario 3: Try to push the budget above the ceiling', () => {
        let context;
        beforeEach(async () => {
            context = await buildContext(new Date('2026-05-20T12:00:00Z'));
            await context.budgetGateway.save({ limitUsd: 200 });
        });
        it('rejects 750 with HTTP 400 and leaves the on-disk budget unchanged', async () => {
            const response = await context.app.inject({
                method: 'POST',
                url: '/api/budget',
                payload: { limitUsd: 750 },
            });
            expect(response.statusCode).toBe(400);
            const body = response.json();
            expect(body.success).toBe(false);
            expect(body.error).toContain('0');
            expect(body.error).toContain('600');
            const persisted = await context.budgetGateway.load();
            expect(persisted).toEqual({ limitUsd: 200 });
            await context.app.close();
        });
    });
    describe('Scenario 6: Block a fresh review when the GLOBAL sum across repos exceeds the cap', () => {
        let context;
        beforeEach(async () => {
            context = await buildContext(new Date('2026-05-20T12:00:00Z'));
            await context.budgetGateway.save({ limitUsd: 200 });
            // Each repo individually is under the cap, but their SUM ($130 + $90 = $220) is over.
            // This is the meaningful R2 ("global cap") integration assertion.
            context.tokenUsageGateway.setRecordsForPath(LOCAL_PATH_A, [
                TokenUsageRecordFactory.create({
                    recordedAt: '2026-05-10T00:00:00Z',
                    localPath: LOCAL_PATH_A,
                    usage: { inputTokens: 0, outputTokens: 0, cacheCreationInputTokens: 0, cacheReadInputTokens: 0, costUsd: 130 },
                }),
            ]);
            context.tokenUsageGateway.setRecordsForPath(LOCAL_PATH_B, [
                TokenUsageRecordFactory.create({
                    recordedAt: '2026-05-12T00:00:00Z',
                    localPath: LOCAL_PATH_B,
                    usage: { inputTokens: 0, outputTokens: 0, cacheCreationInputTokens: 0, cacheReadInputTokens: 0, costUsd: 90 },
                }),
            ]);
        });
        it('returns accepted=false when the GLOBAL multi-repo sum crosses the cap', async () => {
            const decision = await context.enforceBudget.execute({
                localPaths: [LOCAL_PATH_A, LOCAL_PATH_B],
                now: context.now,
            });
            expect(decision.accepted).toBe(false);
            expect(decision.status.exceeded).toBe(true);
            expect(decision.status.limitUsd).toBe(200);
            expect(decision.status.consumedUsd).toBeCloseTo(220, 2);
            const viewModel = context.presenter.present(decision.status);
            expect(viewModel.exceeded).toBe(true);
            await context.app.close();
        });
    });
    describe('Scenario 8: Allow when the GLOBAL multi-repo sum stays under the cap', () => {
        let context;
        beforeEach(async () => {
            context = await buildContext(new Date('2026-05-20T12:00:00Z'));
            await context.budgetGateway.save({ limitUsd: 200 });
            // Two repos summing to $189.99 — under the $200 cap.
            context.tokenUsageGateway.setRecordsForPath(LOCAL_PATH_A, [
                TokenUsageRecordFactory.create({
                    recordedAt: '2026-05-10T00:00:00Z',
                    localPath: LOCAL_PATH_A,
                    usage: { inputTokens: 0, outputTokens: 0, cacheCreationInputTokens: 0, cacheReadInputTokens: 0, costUsd: 100 },
                }),
            ]);
            context.tokenUsageGateway.setRecordsForPath(LOCAL_PATH_B, [
                TokenUsageRecordFactory.create({
                    recordedAt: '2026-05-12T00:00:00Z',
                    localPath: LOCAL_PATH_B,
                    usage: { inputTokens: 0, outputTokens: 0, cacheCreationInputTokens: 0, cacheReadInputTokens: 0, costUsd: 89.99 },
                }),
            ]);
        });
        it('returns accepted=true when the multi-repo sum stays under the limit', async () => {
            const decision = await context.enforceBudget.execute({
                localPaths: [LOCAL_PATH_A, LOCAL_PATH_B],
                now: context.now,
            });
            expect(decision.accepted).toBe(true);
            expect(decision.status.exceeded).toBe(false);
            expect(decision.status.consumedUsd).toBeCloseTo(189.99, 2);
            await context.app.close();
        });
        void PROJECT_PATH;
    });
    describe('Scenario 10: New calendar month resets the period', () => {
        let context;
        beforeEach(async () => {
            context = await buildContext(new Date('2026-06-01T00:00:00Z'));
            await context.budgetGateway.save({ limitUsd: 200 });
            context.tokenUsageGateway.setRecordsForPath(LOCAL_PATH_A, [
                TokenUsageRecordFactory.create({
                    recordedAt: '2026-05-31T23:00:00Z',
                    localPath: LOCAL_PATH_A,
                    usage: {
                        inputTokens: 0,
                        outputTokens: 0,
                        cacheCreationInputTokens: 0,
                        cacheReadInputTokens: 0,
                        costUsd: 200.5,
                    },
                }),
            ]);
        });
        it('ignores prior-month consumption and accepts new reviews in June', async () => {
            const decision = await context.enforceBudget.execute({
                localPaths: [LOCAL_PATH_A, LOCAL_PATH_B],
                now: context.now,
            });
            expect(decision.accepted).toBe(true);
            expect(decision.status.consumedUsd).toBe(0);
            expect(decision.status.periodStart).toBe('2026-06-01T00:00:00.000Z');
            await context.app.close();
        });
    });
});
//# sourceMappingURL=tokenBudgetCap.acceptance.test.js.map