import { afterEach, describe, expect, it, vi } from 'vitest';
import { generateTextForFeature, isAiFeatureEnabled, resolveFeatureRuntime, } from '../../ai/runtime.js';
const NO_CAPS = {
    text: false,
    vision: false,
    imageGeneration: false,
    embeddings: false,
    toolCalling: false,
    structuredOutput: false,
    streaming: false,
    reasoning: false,
    longContext: false,
};
function model(id, caps, over = {}) {
    const [provider = 'openai', providerModelId = id] = id.split(':');
    return {
        id,
        provider: provider,
        providerModelId,
        displayName: id,
        status: 'available',
        capabilities: { ...NO_CAPS, ...caps },
        inputModalities: ['text'],
        outputModalities: ['text'],
        lastSyncedAt: '2026-01-01T00:00:00.000Z',
        source: 'provider',
        metadata: {},
        ...over,
    };
}
function conn(over = {}) {
    return {
        id: 'p-openai',
        provider: 'openai',
        displayName: 'OpenAI',
        enabled: true,
        status: 'connected',
        createdAt: '2026-01-01T00:00:00.000Z',
        updatedAt: '2026-01-01T00:00:00.000Z',
        updatedBy: 'u1',
        ...over,
    };
}
const CONTENT_FEATURE = {
    key: 'ai.features.contentWriting',
    label: 'Content writing',
    description: '',
    enabled: true,
    category: 'content',
    requiredCapabilities: ['text'],
    approvalRequired: false,
    allowBulkApply: false,
    rolesAllowed: ['ADMIN'],
    permissionsRequired: ['ai.use'],
};
function makeDb(opts) {
    const created = [];
    const data = {
        providers: opts.providers ?? [],
        settings: opts.settings ?? {},
        catalog: opts.catalog,
    };
    const db = {
        document: {
            findFirst: vi.fn(async () => ({ data })),
        },
        auditLog: {
            findMany: vi.fn(async () => opts.auditRows ?? []),
            create: vi.fn(async (args) => {
                created.push(args.data);
                return args.data;
            }),
        },
    };
    return { db, created };
}
const KEY = CONTENT_FEATURE.key;
afterEach(() => {
    delete process.env.OPENAI_API_KEY;
    delete process.env.ANTHROPIC_API_KEY;
});
describe('isAiFeatureEnabled', () => {
    it('reflects the stored toggle and defaults unknown keys to enabled', async () => {
        const { db } = makeDb({
            providers: [conn()],
            settings: { features: [{ ...CONTENT_FEATURE, enabled: false }] },
        });
        expect(await isAiFeatureEnabled(db, KEY)).toBe(false);
        expect(await isAiFeatureEnabled(db, 'ai.features.unknown')).toBe(true);
    });
});
describe('resolveFeatureRuntime', () => {
    it('blocks a disabled feature', async () => {
        const { db } = makeDb({
            providers: [conn()],
            settings: { features: [{ ...CONTENT_FEATURE, enabled: false }] },
        });
        const r = await resolveFeatureRuntime(db, KEY);
        expect(r.ok).toBe(false);
        if (!r.ok)
            expect(r.code).toBe('feature_disabled');
    });
    it('returns no_model when nothing is configured', async () => {
        const { db } = makeDb({ providers: [], settings: { features: [CONTENT_FEATURE] } });
        const r = await resolveFeatureRuntime(db, KEY);
        expect(r.ok).toBe(false);
        if (!r.ok)
            expect(r.code).toBe('no_model');
    });
    it('returns no_provider when the only matching connection is disabled', async () => {
        process.env.OPENAI_API_KEY = 'sk-env-test';
        const { db } = makeDb({
            providers: [conn({ enabled: false })],
            settings: { defaultModelId: 'openai:gpt-x', features: [CONTENT_FEATURE] },
            catalog: { 'p-openai': { models: [model('openai:gpt-x', { text: true })] } },
        });
        const r = await resolveFeatureRuntime(db, KEY);
        expect(r.ok).toBe(false);
        if (!r.ok)
            expect(r.code).toBe('no_provider');
    });
    it('returns no_key when neither env nor a stored key exists', async () => {
        const { db } = makeDb({
            providers: [conn({ provider: 'anthropic', id: 'p-anthropic' })],
            settings: { defaultModelId: 'anthropic:claude-x', features: [CONTENT_FEATURE] },
            catalog: { 'p-anthropic': { models: [model('anthropic:claude-x', { text: true })] } },
        });
        const r = await resolveFeatureRuntime(db, KEY);
        expect(r.ok).toBe(false);
        if (!r.ok)
            expect(r.code).toBe('no_key');
    });
    it('resolves a ready runtime, stripping the provider prefix from the model id', async () => {
        process.env.OPENAI_API_KEY = 'sk-env-test';
        const { db } = makeDb({
            providers: [conn()],
            settings: { defaultModelId: 'openai:gpt-x', features: [CONTENT_FEATURE] },
            catalog: { 'p-openai': { models: [model('openai:gpt-x', { text: true })] } },
        });
        const r = await resolveFeatureRuntime(db, KEY);
        expect(r.ok).toBe(true);
        if (r.ok) {
            expect(r.providerKey).toBe('openai');
            expect(r.apiKey).toBe('sk-env-test');
            expect(r.modelId).toBe('openai:gpt-x');
            expect(r.providerModelId).toBe('gpt-x');
        }
    });
    it('blocks when the monthly budget is exceeded and enforcement is on', async () => {
        process.env.OPENAI_API_KEY = 'sk-env-test';
        const { db } = makeDb({
            providers: [conn()],
            settings: {
                defaultModelId: 'openai:gpt-x',
                features: [CONTENT_FEATURE],
                budget: { alertAtPercentages: [], disableWhenExceeded: true, monthlyTokenLimit: 100 },
            },
            catalog: { 'p-openai': { models: [model('openai:gpt-x', { text: true })] } },
            auditRows: [{ details: { totalTokensUsed: 250 } }],
        });
        const r = await resolveFeatureRuntime(db, KEY);
        expect(r.ok).toBe(false);
        if (!r.ok)
            expect(r.code).toBe('budget_exceeded');
    });
});
describe('generateTextForFeature', () => {
    function okFetch(body) {
        return vi.fn(async () => new Response(JSON.stringify(body), {
            status: 200,
            headers: { 'content-type': 'application/json' },
        }));
    }
    it('generates text and records real token usage', async () => {
        process.env.OPENAI_API_KEY = 'sk-env-test';
        const { db, created } = makeDb({
            providers: [conn()],
            settings: { defaultModelId: 'openai:gpt-x', features: [CONTENT_FEATURE] },
            catalog: { 'p-openai': { models: [model('openai:gpt-x', { text: true })] } },
        });
        const fetchImpl = okFetch({
            choices: [{ message: { content: 'rewritten copy' } }],
            usage: { prompt_tokens: 12, completion_tokens: 34, total_tokens: 46 },
        });
        const r = await generateTextForFeature(db, KEY, {
            prompt: 'hello',
            system: 'be terse',
            userId: 'u1',
            fetchImpl,
        });
        expect(r.ok).toBe(true);
        if (r.ok) {
            expect(r.text).toBe('rewritten copy');
            expect(r.usage.totalTokens).toBe(46);
            expect(r.modelId).toBe('openai:gpt-x');
        }
        expect(fetchImpl).toHaveBeenCalledTimes(1);
        // A usage audit row is recorded with fields the usage meter reads.
        expect(created).toHaveLength(1);
        expect(created[0].event).toBe('ai_usage');
        expect(created[0].details.totalTokensUsed).toBe(46);
        expect(created[0].details.feature).toBe(KEY);
    });
    it('surfaces a provider HTTP failure as provider_error without recording usage', async () => {
        process.env.OPENAI_API_KEY = 'sk-env-test';
        const { db, created } = makeDb({
            providers: [conn()],
            settings: { defaultModelId: 'openai:gpt-x', features: [CONTENT_FEATURE] },
            catalog: { 'p-openai': { models: [model('openai:gpt-x', { text: true })] } },
        });
        const fetchImpl = vi.fn(async () => new Response('nope', { status: 401 }));
        const r = await generateTextForFeature(db, KEY, { prompt: 'hello', fetchImpl });
        expect(r.ok).toBe(false);
        if (!r.ok)
            expect(r.code).toBe('provider_error');
        expect(created).toHaveLength(0);
    });
    it('includes provider error detail for HTTP 400 responses', async () => {
        process.env.OPENAI_API_KEY = 'sk-env-test';
        const { db } = makeDb({
            providers: [conn()],
            settings: { defaultModelId: 'openai:gpt-x', features: [CONTENT_FEATURE] },
            catalog: { 'p-openai': { models: [model('openai:gpt-x', { text: true })] } },
        });
        const fetchImpl = vi.fn(async () => new Response(JSON.stringify({ error: { message: 'The model `gpt-x` does not exist.' } }), {
            status: 400,
            headers: { 'content-type': 'application/json' },
        }));
        const r = await generateTextForFeature(db, KEY, { prompt: 'hello', fetchImpl });
        expect(r.ok).toBe(false);
        if (!r.ok) {
            expect(r.code).toBe('provider_error');
            expect(r.message).toContain('does not exist');
        }
    });
    it('remaps legacy Anthropic curated model ids before calling the provider', async () => {
        process.env.ANTHROPIC_API_KEY = 'sk-ant-test';
        const { db } = makeDb({
            providers: [{ ...conn(), id: 'p-anthropic', provider: 'anthropic' }],
            settings: {
                defaultModelId: 'anthropic:claude-sonnet-4-latest',
                defaultProviderId: 'p-anthropic',
                features: [CONTENT_FEATURE],
            },
            catalog: {
                'p-anthropic': {
                    models: [model('anthropic:claude-sonnet-4-latest', { text: true })],
                },
            },
        });
        let sentBody;
        const fetchImpl = vi.fn(async (_url, init) => {
            sentBody = JSON.parse(init.body);
            return new Response(JSON.stringify({
                content: [{ type: 'text', text: 'ok' }],
                usage: { input_tokens: 1, output_tokens: 1 },
            }), { status: 200, headers: { 'content-type': 'application/json' } });
        });
        const r = await generateTextForFeature(db, KEY, { prompt: 'hello', fetchImpl });
        expect(r.ok).toBe(true);
        expect(sentBody?.model).toBe('claude-sonnet-4-6');
    });
    it('omits temperature for reasoning models such as claude-opus-4-8', async () => {
        process.env.ANTHROPIC_API_KEY = 'sk-ant-test';
        const { db } = makeDb({
            providers: [{ ...conn(), id: 'p-anthropic', provider: 'anthropic' }],
            settings: {
                defaultModelId: 'anthropic:claude-opus-4-8',
                defaultProviderId: 'p-anthropic',
                features: [CONTENT_FEATURE],
            },
            catalog: {
                'p-anthropic': {
                    models: [model('anthropic:claude-opus-4-8', { text: true, reasoning: true })],
                },
            },
        });
        let sentBody;
        const fetchImpl = vi.fn(async (_url, init) => {
            sentBody = JSON.parse(init.body);
            return new Response(JSON.stringify({
                content: [{ type: 'text', text: 'ok' }],
                usage: { input_tokens: 1, output_tokens: 1 },
            }), { status: 200, headers: { 'content-type': 'application/json' } });
        });
        const r = await generateTextForFeature(db, KEY, {
            prompt: 'hello',
            temperature: 0.3,
            fetchImpl,
        });
        expect(r.ok).toBe(true);
        expect(sentBody?.model).toBe('claude-opus-4-8');
        expect(sentBody?.temperature).toBeUndefined();
    });
    it('still sends temperature for non-reasoning text models', async () => {
        process.env.OPENAI_API_KEY = 'sk-env-test';
        const { db } = makeDb({
            providers: [conn()],
            settings: { defaultModelId: 'openai:gpt-x', features: [CONTENT_FEATURE] },
            catalog: { 'p-openai': { models: [model('openai:gpt-x', { text: true })] } },
        });
        let sentBody;
        const fetchImpl = vi.fn(async (_url, init) => {
            sentBody = JSON.parse(init.body);
            return new Response(JSON.stringify({
                choices: [{ message: { content: 'ok' } }],
                usage: { prompt_tokens: 1, completion_tokens: 1, total_tokens: 2 },
            }), { status: 200, headers: { 'content-type': 'application/json' } });
        });
        const r = await generateTextForFeature(db, KEY, {
            prompt: 'hello',
            temperature: 0.3,
            fetchImpl,
        });
        expect(r.ok).toBe(true);
        expect(sentBody?.temperature).toBe(0.3);
    });
    it('injects the active brand voice into the system prompt when opted in', async () => {
        process.env.OPENAI_API_KEY = 'sk-env-test';
        const { db } = makeDb({
            providers: [conn()],
            settings: {
                defaultModelId: 'openai:gpt-x',
                features: [CONTENT_FEATURE],
                brandVoice: {
                    enabled: true,
                    name: 'Acme',
                    tone: 'confident, warm',
                    guidelines: 'No jargon.',
                },
            },
            catalog: { 'p-openai': { models: [model('openai:gpt-x', { text: true })] } },
        });
        let sentBody;
        const fetchImpl = vi.fn(async (_url, init) => {
            sentBody = JSON.parse(init.body);
            return new Response(JSON.stringify({
                choices: [{ message: { content: 'ok' } }],
                usage: { prompt_tokens: 1, completion_tokens: 1, total_tokens: 2 },
            }), { status: 200, headers: { 'content-type': 'application/json' } });
        });
        const r = await generateTextForFeature(db, KEY, {
            prompt: 'write a headline',
            system: 'Be concise.',
            useBrandVoice: true,
            fetchImpl,
        });
        expect(r.ok).toBe(true);
        const systemMsg = sentBody?.messages.find((m) => m.role === 'system')?.content ?? '';
        expect(systemMsg).toContain('brand voice');
        expect(systemMsg).toContain('confident, warm');
        // Task instruction still present, after the voice.
        expect(systemMsg).toContain('Be concise.');
    });
    it('does NOT inject brand voice unless the caller opts in', async () => {
        process.env.OPENAI_API_KEY = 'sk-env-test';
        const { db } = makeDb({
            providers: [conn()],
            settings: {
                defaultModelId: 'openai:gpt-x',
                features: [CONTENT_FEATURE],
                brandVoice: { enabled: true, name: 'Acme', tone: 'confident', guidelines: 'No jargon.' },
            },
            catalog: { 'p-openai': { models: [model('openai:gpt-x', { text: true })] } },
        });
        let sentBody;
        const fetchImpl = vi.fn(async (_url, init) => {
            sentBody = JSON.parse(init.body);
            return new Response(JSON.stringify({
                choices: [{ message: { content: 'ok' } }],
                usage: { prompt_tokens: 1, completion_tokens: 1, total_tokens: 2 },
            }), { status: 200, headers: { 'content-type': 'application/json' } });
        });
        await generateTextForFeature(db, KEY, { prompt: 'x', system: 'Be concise.', fetchImpl });
        const systemMsg = sentBody?.messages.find((m) => m.role === 'system')?.content ?? '';
        expect(systemMsg).toBe('Be concise.');
        expect(systemMsg).not.toContain('brand voice');
    });
    it('short-circuits a disabled feature before any network call', async () => {
        const { db } = makeDb({
            providers: [conn()],
            settings: { features: [{ ...CONTENT_FEATURE, enabled: false }] },
        });
        const fetchImpl = vi.fn();
        const r = await generateTextForFeature(db, KEY, { prompt: 'hi', fetchImpl });
        expect(r.ok).toBe(false);
        if (!r.ok)
            expect(r.code).toBe('feature_disabled');
        expect(fetchImpl).not.toHaveBeenCalled();
    });
});
//# sourceMappingURL=runtime.test.js.map