import { beforeEach, describe, expect, it, vi } from 'vitest';
// IMPORTANT: vi.mock calls are hoisted. We mock `@actuate-media/plugin-ai`
// so the dynamic import inside handlers.ts resolves to a deterministic
// generator instead of trying to hit a real LLM provider.
vi.mock('@actuate-media/plugin-ai', () => ({
    generateDocumentContent: vi.fn(async (input) => ({
        data: {
            title: 'Five Spring Cleaning Tips',
            slug: 'five-spring-cleaning-tips',
            excerpt: 'A practical guide.',
            body: 'When spring arrives, tackle these five tasks first.',
        },
        title: 'Five Spring Cleaning Tips',
        slug: 'five-spring-cleaning-tips',
        seo: {
            metaTitle: 'Spring Cleaning Tips',
            metaDescription: 'Five practical spring cleaning tips for busy households.',
            schemaType: input?.collection?.seo?.defaultSchemaType ?? 'Article',
        },
        usage: { promptTokens: 100, completionTokens: 250 },
        durationMs: 42,
    })),
}));
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;
    let lastCreatedDoc = null;
    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 () => [],
            findUnique: async () => lastCreatedDoc,
            findFirst: async ({ where }) => {
                // Return the most recently created doc if its id matches; otherwise
                // null. updateDocument calls findFirst({ id, collection, deletedAt: null }).
                if (!lastCreatedDoc)
                    return null;
                if (where?.id && where.id !== lastCreatedDoc.id)
                    return null;
                return lastCreatedDoc;
            },
            count: async () => 0,
            create: async ({ data }) => {
                lastCreatedDoc = {
                    ...data,
                    id: 'doc-ai-1',
                    createdAt: new Date(),
                    updatedAt: new Date(),
                };
                return lastCreatedDoc;
            },
            update: async ({ data }) => {
                lastCreatedDoc = { ...lastCreatedDoc, ...data, updatedAt: new Date() };
                return lastCreatedDoc;
            },
        },
        version: {
            create: async ({ data }) => ({ ...data, id: 'ver-1' }),
            findMany: async () => [],
        },
        documentRevision: {
            create: async ({ data }) => ({ ...data, id: 'rev-1' }),
        },
        auditLog: { create: async () => ({}) },
        webhook: { findMany: async () => [] },
        webhookDelivery: { create: async () => ({}) },
        user: {},
        session: {},
        media: {},
    };
    // createDocument may wrap the create in a transaction. Run the callback
    // against the mock itself so the same model proxies fire.
    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',
            keyHash,
            keyPrefix,
            userId: 'user-1',
            scopes,
            ipRestrictions: null,
            expiresAt: null,
            lastUsedAt: null,
            revokedAt: null,
        },
    };
}
const postsCollection = {
    slug: 'posts',
    labels: { singular: 'Post', plural: 'Posts' },
    type: 'post',
    seo: { defaultSchemaType: 'BlogPosting' },
    fields: {
        title: { type: 'text', label: 'Title', required: true },
        slug: { type: 'slug', label: 'Slug', required: true },
        excerpt: { type: 'text', label: 'Excerpt' },
        body: { type: 'richText', label: 'Body' },
    },
};
const pagesCollection = {
    slug: 'pages',
    labels: { singular: 'Page', plural: 'Pages' },
    type: 'page',
    fields: {
        title: { type: 'text', label: 'Title', required: true },
        slug: { type: 'slug', label: 'Slug', required: true },
        layout: { type: 'blocks', label: 'Layout' },
    },
};
describe('POST /collections/:slug/ai-create', () => {
    beforeEach(() => {
        delete globalThis.__actuateConfig;
        process.env.CMS_SECRET = VALID_SECRET;
    });
    it('creates a document via AI when caller has create scope on the collection', async () => {
        const { key, record } = await makeKey({ collections: ['posts'], actions: ['create'] });
        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/collections/posts/ai-create', {
            method: 'POST',
            headers: {
                Authorization: `Bearer ${key}`,
                'Content-Type': 'application/json',
            },
            body: JSON.stringify({ prompt: 'Write a post about spring cleaning' }),
        }));
        const body = await response.json();
        expect({ status: response.status, body }).toMatchObject({ status: 201 });
        expect(body.data.document.id).toBe('doc-ai-1');
        expect(body.data.document.title).toBe('Five Spring Cleaning Tips');
        expect(body.data.document.slug).toBe('five-spring-cleaning-tips');
        expect(body.data.document.status).toBe('DRAFT');
        // SEO metadata flows back through the generation block (not pageSettings,
        // which is a page-builder-only wire shape).
        expect(body.data.generation.seo.schemaType).toBe('BlogPosting');
        expect(body.data.generation.usage.completionTokens).toBe(250);
    });
    it('returns 400 when prompt is missing', async () => {
        const { key, record } = await makeKey({ collections: ['posts'], actions: ['create'] });
        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/collections/posts/ai-create', {
            method: 'POST',
            headers: { Authorization: `Bearer ${key}`, 'Content-Type': 'application/json' },
            body: JSON.stringify({}),
        }));
        expect(response.status).toBe(400);
        const body = await response.json();
        expect(body.error).toContain('prompt');
    });
    it('returns 404 when the collection is not configured', async () => {
        const { key, record } = await makeKey({ admin: true });
        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/collections/no-such-collection/ai-create', {
            method: 'POST',
            headers: { Authorization: `Bearer ${key}`, 'Content-Type': 'application/json' },
            body: JSON.stringify({ prompt: 'test' }),
        }));
        expect(response.status).toBe(404);
    });
    it('refuses page-builder collections and points to /page-builder/create', async () => {
        const { key, record } = await makeKey({ collections: ['pages'], actions: ['create'] });
        const db = createMockDB({ apiKey: record });
        initDB(db);
        const handler = handleActuateAPI({
            prismaClient: db,
            config: { collections: { pages: pagesCollection } },
        });
        const response = await handler(new Request('https://example.com/api/cms/collections/pages/ai-create', {
            method: 'POST',
            headers: { Authorization: `Bearer ${key}`, 'Content-Type': 'application/json' },
            body: JSON.stringify({ prompt: 'Create a homepage' }),
        }));
        expect(response.status).toBe(400);
        const body = await response.json();
        expect(body.error).toContain('page-builder/create');
    });
    it('rejects callers without create scope on the target collection', async () => {
        // Key has read scope only.
        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/collections/posts/ai-create', {
            method: 'POST',
            headers: { Authorization: `Bearer ${key}`, 'Content-Type': 'application/json' },
            body: JSON.stringify({ prompt: 'test' }),
        }));
        expect(response.status).toBe(403);
    });
    it('publishes immediately when publish: true is passed AND caller has admin scope', async () => {
        // Publish requires admin scope on the API key — under the hood that
        // maps to role=ADMIN, which clears the publish gate in updateDocument.
        // A non-admin key that has only create+update will still create the
        // draft but be denied the publish transition (see the next test).
        const { key, record } = await makeKey({ admin: true });
        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/collections/posts/ai-create', {
            method: 'POST',
            headers: { Authorization: `Bearer ${key}`, 'Content-Type': 'application/json' },
            body: JSON.stringify({ prompt: 'test', publish: true }),
        }));
        expect(response.status).toBe(201);
        const body = await response.json();
        expect(body.data.document.status).toBe('PUBLISHED');
    });
    it('falls back to DRAFT + warning when publish requested but update scope is missing', async () => {
        // create-only key — draft will be created, publish will fail.
        const { key, record } = await makeKey({ collections: ['posts'], actions: ['create'] });
        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/collections/posts/ai-create', {
            method: 'POST',
            headers: { Authorization: `Bearer ${key}`, 'Content-Type': 'application/json' },
            body: JSON.stringify({ prompt: 'test', publish: true }),
        }));
        expect(response.status).toBe(201);
        const body = await response.json();
        expect(body.data.document.status).toBe('DRAFT');
        expect(body.warning).toMatch(/publish failed/i);
    });
    it('honors caller-supplied title and slug overrides', async () => {
        const { key, record } = await makeKey({ collections: ['posts'], actions: ['create'] });
        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/collections/posts/ai-create', {
            method: 'POST',
            headers: { Authorization: `Bearer ${key}`, 'Content-Type': 'application/json' },
            body: JSON.stringify({
                prompt: 'test',
                title: 'My Custom Title',
                slug: 'my-custom-slug',
            }),
        }));
        expect(response.status).toBe(201);
        const body = await response.json();
        expect(body.data.document.title).toBe('My Custom Title');
        expect(body.data.document.slug).toBe('my-custom-slug');
    });
    it('returns 400 when prompt exceeds 4000 char limit', async () => {
        const { key, record } = await makeKey({ collections: ['posts'], actions: ['create'] });
        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/collections/posts/ai-create', {
            method: 'POST',
            headers: { Authorization: `Bearer ${key}`, 'Content-Type': 'application/json' },
            body: JSON.stringify({ prompt: 'A'.repeat(5000) }),
        }));
        expect(response.status).toBe(400);
        const body = await response.json();
        expect(body.error).toContain('exceeds');
    });
});
//# sourceMappingURL=collections-ai-create.test.js.map