import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { defaultAISettings, encryptProviderKey, getAIConfig, getEnvApiKey, last4, resolveProviderKey, toPublicProvider, } from '../../ai/config-store.js';
import { mergeFeatures } from '../../ai/feature-defaults.js';
import { curatedFallbackCatalog, normalizeModel } from '../../ai/catalog.js';
import { resolveModelOrUnavailable } from '../../ai/models.js';
import { getUsageSummary } from '../../ai/usage.js';
import * as openai from '../../ai/providers/openai.js';
import * as anthropic from '../../ai/providers/anthropic.js';
import * as custom from '../../ai/providers/custom.js';
function makeConn(overrides = {}) {
    return {
        id: 'c1',
        provider: 'anthropic',
        displayName: 'Anthropic',
        enabled: true,
        status: 'unknown',
        createdAt: '2026-01-01T00:00:00.000Z',
        updatedAt: '2026-01-01T00:00:00.000Z',
        updatedBy: 'u1',
        ...overrides,
    };
}
function mockResponse(status, body) {
    return {
        ok: status >= 200 && status < 300,
        status,
        json: async () => body,
    };
}
describe('ai/config-store', () => {
    const origEnv = { ...process.env };
    afterEach(() => {
        process.env = { ...origEnv };
        vi.restoreAllMocks();
    });
    it('encrypts a key and masks to last4 without leaking the key', async () => {
        const { apiKeyEncrypted, apiKeyLast4 } = await encryptProviderKey('sk-ant-secret-7788');
        expect(apiKeyLast4).toBe('7788');
        const conn = makeConn({ apiKeyEncrypted, apiKeyLast4 });
        const pub = toPublicProvider(conn);
        expect(pub.apiKeyEncrypted).toBeUndefined();
        expect(pub.apiKeyLast4).toBe('7788');
        expect(pub.hasKey).toBe(true);
        expect(pub.apiKeySource).toBe('database');
        expect(pub.keyManagedExternally).toBe(false);
    });
    it('treats an env-managed provider as read-only and reflects the env key suffix', () => {
        process.env.OPENAI_API_KEY = 'sk-env-key-9090';
        const conn = makeConn({ provider: 'openai' });
        const pub = toPublicProvider(conn);
        expect(pub.apiKeySource).toBe('environment');
        expect(pub.keyManagedExternally).toBe(true);
        expect(pub.apiKeyLast4).toBe('9090');
        expect(getEnvApiKey('openai')).toBe('sk-env-key-9090');
    });
    it('resolves the env key over a stored key, else decrypts the stored key', async () => {
        const { apiKeyEncrypted } = await encryptProviderKey('stored-key-1111');
        const conn = makeConn({ provider: 'xai', apiKeyEncrypted });
        expect(await resolveProviderKey(conn)).toBe('stored-key-1111');
        process.env.XAI_API_KEY = 'env-key-2222';
        expect(await resolveProviderKey(conn)).toBe('env-key-2222');
    });
    it('last4 handles short and empty values', () => {
        expect(last4('abcdef')).toBe('cdef');
        expect(last4('ab')).toBe('ab');
        expect(last4('')).toBeUndefined();
        expect(last4(undefined)).toBeUndefined();
    });
    it('getAIConfig returns populated defaults when no document exists', async () => {
        const db = { document: { findFirst: async () => null } };
        const config = await getAIConfig(db);
        expect(config.providers).toEqual([]);
        expect(config.settings.features.length).toBeGreaterThan(0);
        expect(config.settings.budget.alertAtPercentages).toEqual([50, 80, 100]);
    });
    it('getAIConfig merges stored feature toggles over current defaults', async () => {
        const db = {
            document: {
                findFirst: async () => ({
                    data: {
                        providers: [],
                        settings: {
                            features: [{ key: 'ai.features.contentWriting', enabled: false }],
                        },
                    },
                }),
            },
        };
        const config = await getAIConfig(db);
        const cw = config.settings.features.find((f) => f.key === 'ai.features.contentWriting');
        expect(cw?.enabled).toBe(false);
        // Other defaults still present.
        expect(config.settings.features.length).toBe(defaultAISettings().features.length);
    });
});
describe('ai/feature-defaults mergeFeatures', () => {
    it('preserves enabled state and drops unknown keys', () => {
        const merged = mergeFeatures([
            { key: 'ai.features.mediaAltText', enabled: false },
            { key: 'ai.features.removed', enabled: true },
        ]);
        expect(merged.find((f) => f.key === 'ai.features.mediaAltText')?.enabled).toBe(false);
        expect(merged.find((f) => f.key === 'ai.features.removed')).toBeUndefined();
    });
});
describe('ai/catalog normalizeModel', () => {
    const now = '2026-01-01T00:00:00.000Z';
    it('merges curated capabilities + display name for a dated provider id', () => {
        const m = normalizeModel({
            provider: 'openai',
            providerModelId: 'gpt-4o-2024-08-06',
            source: 'provider',
            lastSyncedAt: now,
        });
        expect(m.displayName).toBe('GPT-4o (balanced)');
        expect(m.capabilities.vision).toBe(true);
        expect(m.capabilities.text).toBe(true);
        expect(m.pricing?.inputPerMillion).toBe(2.5);
    });
    it('prefers the longest match (gpt-4o-mini over gpt-4o)', () => {
        const m = normalizeModel({
            provider: 'openai',
            providerModelId: 'gpt-4o-mini-2024-07-18',
            source: 'provider',
            lastSyncedAt: now,
        });
        expect(m.displayName).toBe('GPT-4o mini (fast)');
    });
    it('infers conservative capabilities for an unknown model', () => {
        const m = normalizeModel({
            provider: 'custom',
            providerModelId: 'mystery-llm-v9',
            source: 'manual',
            lastSyncedAt: now,
        });
        expect(m.capabilities.text).toBe(true);
        expect(m.capabilities.vision).toBe(false);
        expect(m.metadata).toEqual({});
    });
    it('detects embedding models by id', () => {
        const m = normalizeModel({
            provider: 'custom',
            providerModelId: 'my-embedding-model',
            source: 'manual',
            lastSyncedAt: now,
        });
        expect(m.capabilities.embeddings).toBe(true);
        expect(m.capabilities.text).toBe(false);
        expect(m.outputModalities).toEqual(['embedding']);
    });
    it('curatedFallbackCatalog returns normalized models for a provider', () => {
        const models = curatedFallbackCatalog('anthropic', now);
        expect(models.length).toBeGreaterThan(0);
        expect(models.every((m) => m.provider === 'anthropic')).toBe(true);
        expect(models.some((m) => m.displayName === 'Claude Sonnet (balanced)')).toBe(true);
    });
});
describe('ai/models resolveModelOrUnavailable', () => {
    it('returns the catalog model when present', () => {
        const catalog = curatedFallbackCatalog('anthropic', '2026-01-01T00:00:00.000Z');
        const target = catalog[0];
        expect(resolveModelOrUnavailable(target.id, catalog)?.status).not.toBe('unavailable');
    });
    it('synthesizes an unavailable model when the id is gone', () => {
        const catalog = curatedFallbackCatalog('anthropic', '2026-01-01T00:00:00.000Z');
        const m = resolveModelOrUnavailable('openai:ghost-model', catalog);
        expect(m?.status).toBe('unavailable');
        expect(m?.provider).toBe('openai');
        expect(m?.providerModelId).toBe('ghost-model');
    });
    it('returns null for an empty id', () => {
        expect(resolveModelOrUnavailable(undefined, [])).toBeNull();
    });
});
describe('ai/providers adapters (mocked fetch)', () => {
    it('openai testConnection maps 200 -> connected and lists model ids', async () => {
        const fetchImpl = vi
            .fn()
            .mockResolvedValue(mockResponse(200, { data: [{ id: 'gpt-4o' }, { id: 'gpt-4o-mini' }] }));
        const res = await openai.testConnection({ apiKey: 'k', fetchImpl });
        expect(res.status).toBe('connected');
        expect(res.ok).toBe(true);
        const ids = await openai.listModels({ apiKey: 'k', fetchImpl });
        expect(ids).toEqual(['gpt-4o', 'gpt-4o-mini']);
    });
    it('openai testConnection maps 401 -> invalid_key', async () => {
        const fetchImpl = vi.fn().mockResolvedValue(mockResponse(401, {}));
        const res = await openai.testConnection({ apiKey: 'bad', fetchImpl });
        expect(res.status).toBe('invalid_key');
        expect(res.ok).toBe(false);
    });
    it('openai testConnection reports needs_setup with no key', async () => {
        const res = await openai.testConnection({ apiKey: '' });
        expect(res.status).toBe('needs_setup');
    });
    it('anthropic sends x-api-key + version headers', async () => {
        const fetchImpl = vi
            .fn()
            .mockResolvedValue(mockResponse(200, { data: [] }));
        await anthropic.testConnection({ apiKey: 'k', fetchImpl });
        const call = fetchImpl.mock.calls[0];
        const headers = call[1].headers;
        expect(headers['x-api-key']).toBe('k');
        expect(headers['anthropic-version']).toBeTruthy();
    });
    it('custom provider rejects an SSRF base URL before fetching', async () => {
        const fetchImpl = vi.fn();
        const res = await custom.testConnection({
            apiKey: 'k',
            baseUrl: 'http://127.0.0.1/v1',
            fetchImpl,
        });
        expect(res.ok).toBe(false);
        expect(res.message.toLowerCase()).toContain('unsafe');
        expect(fetchImpl.mock.calls.length).toBe(0);
    });
    it('custom provider treats 404 /models as connected (manual model entry)', async () => {
        const fetchImpl = vi.fn().mockResolvedValue(mockResponse(404, {}));
        // Public IP literal so the SSRF guard's DNS resolution is skipped (offline test).
        const res = await custom.testConnection({
            apiKey: 'k',
            baseUrl: 'https://93.184.216.34/v1',
            fetchImpl,
        });
        expect(res.ok).toBe(true);
        expect(res.status).toBe('connected');
    });
});
describe('ai/usage getUsageSummary', () => {
    beforeEach(() => {
        vi.useFakeTimers();
        vi.setSystemTime(new Date('2026-06-15T12:00:00.000Z'));
    });
    afterEach(() => {
        vi.useRealTimers();
    });
    it('aggregates token totals from audit rows within the month', async () => {
        const db = {
            document: { findFirst: async () => null },
            auditLog: {
                findMany: async () => [
                    { details: { totalTokensUsed: 100 } },
                    { details: { tokensUsed: 50 } },
                    { details: { promptTokens: 10, completionTokens: 5 } },
                    { details: { action: 'noop' } },
                ],
            },
        };
        const summary = await getUsageSummary(db);
        expect(summary.totalTokens).toBe(165);
        expect(summary.runCount).toBe(3);
        expect(summary.periodStart).toBe('2026-06-01T00:00:00.000Z');
    });
    it('returns zero usage when there are no rows', async () => {
        const db = { document: { findFirst: async () => null }, auditLog: { findMany: async () => [] } };
        const summary = await getUsageSummary(db);
        expect(summary.totalTokens).toBe(0);
        expect(summary.runCount).toBe(0);
    });
});
//# sourceMappingURL=ai.test.js.map