import { beforeEach, describe, expect, it, vi } from 'vitest';
// vi.mock is hoisted. Mock @actuate-media/plugin-ai so handlers.ts'
// dynamic import resolves to deterministic stubs — we want to verify the
// HTTP wiring (auth, validation, branching, response shape) without
// pulling in the real scoring pipeline (covered by plugin-ai's own tests).
vi.mock('@actuate-media/plugin-ai', () => ({
    rewrite: vi.fn(async (text, style, tone) => `[rewrite style=${style ?? ''} tone=${tone ?? ''}] ${text}`),
    expand: vi.fn(async (text, instructions) => `[expand ${instructions ?? ''}] ${text} (more detail)`),
    compress: vi.fn(async (text, target) => `[compress target=${target ?? ''}] ${text.slice(0, Math.min(text.length, target ?? 40))}`),
    proofread: vi.fn(async (text) => ({
        corrected: text.replace(/teh/g, 'the'),
        changes: text.includes('teh') ? [{ original: 'teh', corrected: 'the', type: 'spelling' }] : [],
    })),
    auditDocument: vi.fn((input) => ({
        composite: { overall: 78, grade: 'C', breakdown: {} },
        scores: { seo: 80, readability: 75, accessibility: 90 },
        readability: { averageGradeLevel: 9.2, difficulty: 'moderate' },
        issues: [
            {
                severity: 'warning',
                category: 'structure',
                rule: 'missing-meta-description',
                message: 'Document is missing a meta description.',
                field: 'metaDescription',
            },
        ],
        diagnostics: {
            seoBreakdown: [{ criterion: 'Title length', score: 15, maxScore: 15 }],
            accessibilityIssues: [],
        },
        __echo: input,
    })),
    suggestInternalLinks: vi.fn(async (content, pages) => {
        // Return one fake suggestion per page so the test can verify candidate
        // assembly worked.
        return pages.map((p, i) => ({
            anchorText: p.title,
            targetPage: p,
            confidence: 0.9 - i * 0.1,
            position: { start: 0, end: p.title.length },
        }));
    }),
}));
import { handleActuateAPI } from '../../api/index.js';
import { initDB } from '../../db.js';
import { generateApiKey } from '../../security/api-key-enhanced.js';
const VALID_SECRET = 'a'.repeat(48);
function createMockDB(opts) {
    const apiKey = opts.apiKey;
    const documents = opts.documents ?? [];
    const dbMock = {
        apiKey: {
            findMany: async () => (apiKey ? [apiKey] : []),
            findUnique: async ({ where }) => {
                if (!apiKey)
                    return null;
                if (apiKey.keyHash !== where.keyHash)
                    return null;
                return apiKey;
            },
            update: async () => apiKey,
        },
        document: {
            findMany: async ({ where }) => {
                if (!where)
                    return documents;
                return documents.filter((d) => {
                    if (where.collection && d.collection !== where.collection)
                        return false;
                    if (where.status && d.status !== where.status)
                        return false;
                    if (where.deletedAt === null && d.deletedAt)
                        return false;
                    return true;
                });
            },
            findFirst: async ({ where }) => {
                return (documents.find((d) => {
                    if (where.id && d.id !== where.id)
                        return false;
                    if (where.collection && d.collection !== where.collection)
                        return false;
                    if (where.deletedAt === null && d.deletedAt)
                        return false;
                    return true;
                }) ?? null);
            },
            count: async () => documents.length,
        },
        auditLog: { create: async () => ({}) },
    };
    dbMock.$transaction = async (arg) => {
        if (typeof arg === 'function')
            return await arg(dbMock);
        if (Array.isArray(arg))
            return Promise.all(arg);
        return arg;
    };
    return dbMock;
}
async function makeKey(scopes) {
    const { key, keyHash, keyPrefix } = await generateApiKey({ prefix: 'act_sk', scopes });
    return {
        key,
        record: {
            id: 'apikey-ai-quality',
            keyHash,
            keyPrefix,
            userId: 'user-1',
            scopes,
            ipRestrictions: null,
            expiresAt: null,
            lastUsedAt: null,
            revokedAt: null,
        },
    };
}
/**
 * A stored `__ai_config__` document that wires a working OpenAI connection +
 * default model, so the co-author's governed path resolves end-to-end.
 */
function aiConfigDoc(opts) {
    return {
        id: 'ai-config-doc',
        collection: '__ai_config__',
        deletedAt: null,
        data: {
            providers: [
                {
                    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',
                },
            ],
            settings: {
                defaultModelId: 'openai:gpt-x',
                features: [{ key: 'ai.features.contentWriting', enabled: opts.enabled }],
            },
            catalog: {
                'p-openai': {
                    models: [
                        {
                            id: 'openai:gpt-x',
                            provider: 'openai',
                            providerModelId: 'gpt-x',
                            displayName: 'GPT-X',
                            status: 'available',
                            capabilities: { text: true },
                            inputModalities: ['text'],
                            outputModalities: ['text'],
                            lastSyncedAt: '2026-01-01T00:00:00.000Z',
                            source: 'provider',
                            metadata: {},
                        },
                    ],
                },
            },
        },
    };
}
const postsCollection = {
    slug: 'posts',
    labels: { singular: 'Post', plural: 'Posts' },
    type: 'post',
    fields: {
        title: { type: 'text', label: 'Title', required: true },
        slug: { type: 'slug', label: 'Slug', required: true },
        body: { type: 'richText', label: 'Body' },
        metaDescription: { type: 'text', label: 'Meta description' },
    },
};
describe('POST /ai/audit-document', () => {
    beforeEach(() => {
        delete globalThis.__actuateConfig;
        process.env.CMS_SECRET = VALID_SECRET;
    });
    it('audits ad-hoc content when title+body are supplied', async () => {
        const { key, record } = await makeKey({ collections: ['posts'], actions: ['read'] });
        const db = createMockDB({ apiKey: record });
        initDB(db);
        const handler = handleActuateAPI({
            prismaClient: db,
            config: { collections: { posts: postsCollection } },
        });
        const response = await handler(new Request('https://example.com/api/cms/ai/audit-document', {
            method: 'POST',
            headers: { Authorization: `Bearer ${key}`, 'Content-Type': 'application/json' },
            body: JSON.stringify({
                title: 'How to brew pour-over coffee',
                body: 'Pour-over coffee is one of the cleanest ways to extract flavor from beans. Start with fresh roasted whole beans, grind them medium-fine right before brewing, and use water just off the boil.',
                metaDescription: 'Learn the essentials of pour-over coffee: bean selection, grind size, and water temperature.',
                targetKeyword: 'pour-over coffee',
            }),
        }));
        expect(response.status).toBe(200);
        const body = await response.json();
        expect(body.data.composite.grade).toBe('C');
        expect(body.data.scores.seo).toBe(80);
        expect(body.data.issues[0].rule).toBe('missing-meta-description');
    });
    it('audits a stored document when { collection, id } is supplied', async () => {
        const { key, record } = await makeKey({ collections: ['posts'], actions: ['read'] });
        const db = createMockDB({
            apiKey: record,
            documents: [
                {
                    id: 'doc-1',
                    collection: 'posts',
                    title: 'Stored doc',
                    slug: 'stored-doc',
                    status: 'PUBLISHED',
                    deletedAt: null,
                    data: {
                        title: 'Stored doc',
                        body: 'Stored body content from the database.',
                        metaDescription: 'Stored meta',
                    },
                    publishedAt: new Date('2026-01-01'),
                    updatedAt: new Date('2026-02-01'),
                },
            ],
        });
        initDB(db);
        const handler = handleActuateAPI({
            prismaClient: db,
            config: { collections: { posts: postsCollection } },
        });
        const response = await handler(new Request('https://example.com/api/cms/ai/audit-document', {
            method: 'POST',
            headers: { Authorization: `Bearer ${key}`, 'Content-Type': 'application/json' },
            body: JSON.stringify({ collection: 'posts', id: 'doc-1' }),
        }));
        expect(response.status).toBe(200);
        const body = await response.json();
        // The mocked auditor echoes its input back so we can verify the
        // handler extracted the right fields from the persisted document.
        expect(body.data.__echo.title).toBe('Stored doc');
        expect(body.data.__echo.body).toBe('Stored body content from the database.');
        expect(body.data.__echo.metaDescription).toBe('Stored meta');
    });
    it('returns 404 when the referenced document does not exist', async () => {
        const { key, record } = await makeKey({ collections: ['posts'], actions: ['read'] });
        const db = createMockDB({ apiKey: record, documents: [] });
        initDB(db);
        const handler = handleActuateAPI({
            prismaClient: db,
            config: { collections: { posts: postsCollection } },
        });
        const response = await handler(new Request('https://example.com/api/cms/ai/audit-document', {
            method: 'POST',
            headers: { Authorization: `Bearer ${key}`, 'Content-Type': 'application/json' },
            body: JSON.stringify({ collection: 'posts', id: 'nope' }),
        }));
        expect(response.status).toBe(404);
    });
    it('returns 400 when neither id+collection nor content is supplied', async () => {
        const { key, record } = await makeKey({ collections: ['posts'], actions: ['read'] });
        const db = createMockDB({ apiKey: record });
        initDB(db);
        const handler = handleActuateAPI({
            prismaClient: db,
            config: { collections: { posts: postsCollection } },
        });
        const response = await handler(new Request('https://example.com/api/cms/ai/audit-document', {
            method: 'POST',
            headers: { Authorization: `Bearer ${key}`, 'Content-Type': 'application/json' },
            body: JSON.stringify({}),
        }));
        expect(response.status).toBe(400);
    });
    it('requires authentication', async () => {
        const db = createMockDB({});
        initDB(db);
        const handler = handleActuateAPI({
            prismaClient: db,
            config: { collections: { posts: postsCollection } },
        });
        const response = await handler(new Request('https://example.com/api/cms/ai/audit-document', {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify({ title: 'x', body: 'y' }),
        }));
        expect(response.status).toBeGreaterThanOrEqual(401);
        expect(response.status).toBeLessThan(500);
    });
});
describe('POST /ai/suggest-internal-links', () => {
    beforeEach(() => {
        delete globalThis.__actuateConfig;
        process.env.CMS_SECRET = VALID_SECRET;
    });
    it('suggests links based on published documents in the requested collection', async () => {
        const { key, record } = await makeKey({ collections: ['posts'], actions: ['read'] });
        const db = createMockDB({
            apiKey: record,
            documents: [
                {
                    id: 'doc-1',
                    collection: 'posts',
                    title: 'Coffee grinders explained',
                    slug: 'coffee-grinders',
                    status: 'PUBLISHED',
                    deletedAt: null,
                    data: { title: 'Coffee grinders explained', tags: ['grinder', 'brewing'] },
                },
                {
                    id: 'doc-2',
                    collection: 'posts',
                    title: 'Pour-over basics',
                    slug: 'pour-over-basics',
                    status: 'PUBLISHED',
                    deletedAt: null,
                    data: { title: 'Pour-over basics' },
                },
                {
                    id: 'doc-3',
                    collection: 'posts',
                    title: 'Draft post',
                    slug: 'draft',
                    status: 'DRAFT',
                    deletedAt: null,
                    data: { title: 'Draft post' },
                },
            ],
        });
        initDB(db);
        const handler = handleActuateAPI({
            prismaClient: db,
            config: { collections: { posts: postsCollection } },
        });
        const response = await handler(new Request('https://example.com/api/cms/ai/suggest-internal-links', {
            method: 'POST',
            headers: { Authorization: `Bearer ${key}`, 'Content-Type': 'application/json' },
            body: JSON.stringify({
                content: 'A great brew starts with the right grind. See coffee grinders explained for more.',
                collection: 'posts',
            }),
        }));
        expect(response.status).toBe(200);
        const body = await response.json();
        // Only PUBLISHED docs are passed to the suggester — the DRAFT post is filtered.
        expect(body.data.candidatesScanned).toBe(2);
        expect(body.data.suggestions).toHaveLength(2);
        expect(body.data.suggestions[0].targetPage.title).toBe('Coffee grinders explained');
    });
    it('honors the excludeId option to skip the doc being edited', async () => {
        const { key, record } = await makeKey({ collections: ['posts'], actions: ['read'] });
        const db = createMockDB({
            apiKey: record,
            documents: [
                {
                    id: 'doc-1',
                    collection: 'posts',
                    title: 'First',
                    slug: 'first',
                    status: 'PUBLISHED',
                    deletedAt: null,
                    data: { title: 'First' },
                },
                {
                    id: 'doc-2',
                    collection: 'posts',
                    title: 'Second',
                    slug: 'second',
                    status: 'PUBLISHED',
                    deletedAt: null,
                    data: { title: 'Second' },
                },
            ],
        });
        initDB(db);
        const handler = handleActuateAPI({
            prismaClient: db,
            config: { collections: { posts: postsCollection } },
        });
        const response = await handler(new Request('https://example.com/api/cms/ai/suggest-internal-links', {
            method: 'POST',
            headers: { Authorization: `Bearer ${key}`, 'Content-Type': 'application/json' },
            body: JSON.stringify({
                content: 'Some content.',
                collection: 'posts',
                excludeId: 'doc-1',
            }),
        }));
        expect(response.status).toBe(200);
        const body = await response.json();
        expect(body.data.candidatesScanned).toBe(1);
        expect(body.data.suggestions[0].targetPage.id).toBe('doc-2');
    });
    it('returns 400 when content is missing', async () => {
        const { key, record } = await makeKey({ collections: ['posts'], actions: ['read'] });
        const db = createMockDB({ apiKey: record });
        initDB(db);
        const handler = handleActuateAPI({
            prismaClient: db,
            config: { collections: { posts: postsCollection } },
        });
        const response = await handler(new Request('https://example.com/api/cms/ai/suggest-internal-links', {
            method: 'POST',
            headers: { Authorization: `Bearer ${key}`, 'Content-Type': 'application/json' },
            body: JSON.stringify({}),
        }));
        expect(response.status).toBe(400);
    });
});
describe('POST /ai/coauthor', () => {
    beforeEach(() => {
        delete globalThis.__actuateConfig;
        process.env.CMS_SECRET = VALID_SECRET;
        process.env.ACTUATE_DISABLE_RATE_LIMIT = '1';
    });
    it('rewrites text with style + tone hints', async () => {
        const { key, record } = await makeKey({ collections: ['posts'], actions: ['read'] });
        const db = createMockDB({ apiKey: record });
        initDB(db);
        const handler = handleActuateAPI({
            prismaClient: db,
            config: { collections: { posts: postsCollection } },
        });
        const response = await handler(new Request('https://example.com/api/cms/ai/coauthor', {
            method: 'POST',
            headers: { Authorization: `Bearer ${key}`, 'Content-Type': 'application/json' },
            body: JSON.stringify({
                action: 'rewrite',
                text: 'Original content here.',
                style: 'casual',
                tone: 'friendly',
            }),
        }));
        expect(response.status).toBe(200);
        const body = await response.json();
        expect(body.data.action).toBe('rewrite');
        expect(body.data.text).toContain('Original content here.');
        expect(body.data.text).toContain('casual');
        expect(body.data.text).toContain('friendly');
    });
    it('expands and compresses text', async () => {
        const { key, record } = await makeKey({ collections: ['posts'], actions: ['read'] });
        const db = createMockDB({ apiKey: record });
        initDB(db);
        const handler = handleActuateAPI({
            prismaClient: db,
            config: { collections: { posts: postsCollection } },
        });
        const expandRes = await handler(new Request('https://example.com/api/cms/ai/coauthor', {
            method: 'POST',
            headers: { Authorization: `Bearer ${key}`, 'Content-Type': 'application/json' },
            body: JSON.stringify({ action: 'expand', text: 'Short.', instructions: 'add detail' }),
        }));
        expect(expandRes.status).toBe(200);
        expect((await expandRes.json()).data.text).toContain('more detail');
        const compressRes = await handler(new Request('https://example.com/api/cms/ai/coauthor', {
            method: 'POST',
            headers: { Authorization: `Bearer ${key}`, 'Content-Type': 'application/json' },
            body: JSON.stringify({
                action: 'compress',
                text: 'Lorem ipsum dolor sit amet consectetur adipiscing elit.',
                targetLength: 20,
            }),
        }));
        expect(compressRes.status).toBe(200);
        expect((await compressRes.json()).data.text.length).toBeLessThanOrEqual(60);
    });
    it('proofreads and surfaces structured changes', async () => {
        const { key, record } = await makeKey({ collections: ['posts'], actions: ['read'] });
        const db = createMockDB({ apiKey: record });
        initDB(db);
        const handler = handleActuateAPI({
            prismaClient: db,
            config: { collections: { posts: postsCollection } },
        });
        const response = await handler(new Request('https://example.com/api/cms/ai/coauthor', {
            method: 'POST',
            headers: { Authorization: `Bearer ${key}`, 'Content-Type': 'application/json' },
            body: JSON.stringify({ action: 'proofread', text: 'teh quick brown fox' }),
        }));
        expect(response.status).toBe(200);
        const body = await response.json();
        expect(body.data.text).toBe('the quick brown fox');
        expect(body.data.changes).toEqual([{ original: 'teh', corrected: 'the', type: 'spelling' }]);
    });
    it('returns 400 when text is missing', async () => {
        const { key, record } = await makeKey({ collections: ['posts'], actions: ['read'] });
        const db = createMockDB({ apiKey: record });
        initDB(db);
        const handler = handleActuateAPI({
            prismaClient: db,
            config: { collections: { posts: postsCollection } },
        });
        const response = await handler(new Request('https://example.com/api/cms/ai/coauthor', {
            method: 'POST',
            headers: { Authorization: `Bearer ${key}`, 'Content-Type': 'application/json' },
            body: JSON.stringify({ action: 'rewrite' }),
        }));
        expect(response.status).toBe(400);
    });
    it('returns 400 for unknown action', async () => {
        const { key, record } = await makeKey({ collections: ['posts'], actions: ['read'] });
        const db = createMockDB({ apiKey: record });
        initDB(db);
        const handler = handleActuateAPI({
            prismaClient: db,
            config: { collections: { posts: postsCollection } },
        });
        const response = await handler(new Request('https://example.com/api/cms/ai/coauthor', {
            method: 'POST',
            headers: { Authorization: `Bearer ${key}`, 'Content-Type': 'application/json' },
            body: JSON.stringify({ action: 'translate', text: 'x' }),
        }));
        expect(response.status).toBe(400);
    });
    it('routes through the governed provider + model when configured', async () => {
        process.env.OPENAI_API_KEY = 'sk-env-test';
        const fetchSpy = vi.fn(async () => new Response(JSON.stringify({
            choices: [{ message: { content: 'governed rewrite' } }],
            usage: { prompt_tokens: 5, completion_tokens: 7, total_tokens: 12 },
        }), { status: 200, headers: { 'content-type': 'application/json' } }));
        vi.stubGlobal('fetch', fetchSpy);
        try {
            const { key, record } = await makeKey({ collections: ['posts'], actions: ['read'] });
            const db = createMockDB({ apiKey: record, documents: [aiConfigDoc({ enabled: true })] });
            initDB(db);
            const handler = handleActuateAPI({
                prismaClient: db,
                config: { collections: { posts: postsCollection } },
            });
            const response = await handler(new Request('https://example.com/api/cms/ai/coauthor', {
                method: 'POST',
                headers: { Authorization: `Bearer ${key}`, 'Content-Type': 'application/json' },
                body: JSON.stringify({ action: 'rewrite', text: 'hello world' }),
            }));
            expect(response.status).toBe(200);
            const body = await response.json();
            expect(body.data.text).toBe('governed rewrite');
            expect(fetchSpy).toHaveBeenCalled();
            const [url, init] = fetchSpy.mock.calls[0];
            expect(String(url)).toContain('/chat/completions');
            expect(JSON.parse(init.body).model).toBe('gpt-x');
        }
        finally {
            vi.unstubAllGlobals();
            delete process.env.OPENAI_API_KEY;
        }
    });
    it('returns 403 when content writing is disabled in Settings > AI', async () => {
        const { key, record } = await makeKey({ collections: ['posts'], actions: ['read'] });
        const db = createMockDB({ apiKey: record, documents: [aiConfigDoc({ enabled: false })] });
        initDB(db);
        const handler = handleActuateAPI({
            prismaClient: db,
            config: { collections: { posts: postsCollection } },
        });
        const response = await handler(new Request('https://example.com/api/cms/ai/coauthor', {
            method: 'POST',
            headers: { Authorization: `Bearer ${key}`, 'Content-Type': 'application/json' },
            body: JSON.stringify({ action: 'rewrite', text: 'hello world' }),
        }));
        expect(response.status).toBe(403);
    });
});
describe('POST /ai/seo/generate-field (governed)', () => {
    beforeEach(() => {
        delete globalThis.__actuateConfig;
    });
    function seoEntityDoc() {
        return {
            id: 'doc-seo-1',
            collection: 'posts',
            deletedAt: null,
            title: 'Best Hiking Boots',
            plainText: 'A detailed guide to choosing hiking boots for every terrain.',
            data: { title: 'Best Hiking Boots', body: 'A detailed guide to choosing hiking boots.' },
            status: 'PUBLISHED',
        };
    }
    it('routes focus-keyword generation through the governed provider + model', async () => {
        process.env.OPENAI_API_KEY = 'sk-env-test';
        const fetchSpy = vi.fn(async () => new Response(JSON.stringify({
            choices: [{ message: { content: 'hiking boots' } }],
            usage: { prompt_tokens: 10, completion_tokens: 3, total_tokens: 13 },
        }), { status: 200, headers: { 'content-type': 'application/json' } }));
        vi.stubGlobal('fetch', fetchSpy);
        try {
            const { key, record } = await makeKey({
                collections: ['posts'],
                actions: ['read'],
                admin: true,
            });
            const db = createMockDB({
                apiKey: record,
                documents: [aiConfigDoc({ enabled: true }), seoEntityDoc()],
            });
            initDB(db);
            const handler = handleActuateAPI({
                prismaClient: db,
                config: { collections: { posts: postsCollection } },
            });
            const response = await handler(new Request('https://example.com/api/cms/ai/seo/generate-field', {
                method: 'POST',
                headers: { Authorization: `Bearer ${key}`, 'Content-Type': 'application/json' },
                body: JSON.stringify({
                    field: 'focusKeyword',
                    entityType: 'posts',
                    entityId: 'doc-seo-1',
                }),
            }));
            expect(response.status).toBe(200);
            const body = await response.json();
            expect(body.data.text).toBe('hiking boots');
            expect(fetchSpy).toHaveBeenCalled();
        }
        finally {
            vi.unstubAllGlobals();
            delete process.env.OPENAI_API_KEY;
        }
    });
    it('returns 403 when SEO meta generation is disabled', async () => {
        const doc = aiConfigDoc({ enabled: true });
        doc.data.settings.features = [{ key: 'ai.features.seoMetaGeneration', enabled: false }];
        const { key, record } = await makeKey({
            collections: ['posts'],
            actions: ['read'],
            admin: true,
        });
        const db = createMockDB({ apiKey: record, documents: [doc, seoEntityDoc()] });
        initDB(db);
        const handler = handleActuateAPI({
            prismaClient: db,
            config: { collections: { posts: postsCollection } },
        });
        const response = await handler(new Request('https://example.com/api/cms/ai/seo/generate-field', {
            method: 'POST',
            headers: { Authorization: `Bearer ${key}`, 'Content-Type': 'application/json' },
            body: JSON.stringify({
                field: 'metaDescription',
                entityType: 'posts',
                entityId: 'doc-seo-1',
            }),
        }));
        expect(response.status).toBe(403);
    });
});
//# sourceMappingURL=ai-quality.test.js.map