/**
 * Regression tests for the `/api/cms/page-sections/*` routes — the
 * section-aware surface an AI agent (e.g. in Cursor / VS Code) drives via
 * MCP to build a real, editable Page Editor page.
 *
 * Discovery (`/types`) + validation (`/validate`) are deterministic and
 * only need auth. The mutation routes operate on a page document's
 * `data.sections` array through the audited `updateDocument` path, so we
 * exercise them against a mock Prisma client.
 */
import { beforeEach, describe, expect, it } from 'vitest';
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);
const PAGES_CONFIG = {
    collections: {
        pages: {
            slug: 'pages',
            labels: { singular: 'Page', plural: 'Pages' },
            type: 'page',
            fields: {
                title: { type: 'text', required: true },
                slug: { type: 'slug' },
                sections: { type: 'json' },
            },
        },
        // A collection WITHOUT a sections field — used to prove the guard.
        notes: {
            slug: 'notes',
            labels: { singular: 'Note', plural: 'Notes' },
            fields: { title: { type: 'text' } },
        },
        'post-templates': {
            slug: 'post-templates',
            labels: { singular: 'Post Template', plural: 'Post Templates' },
            fields: {
                title: { type: 'text' },
                slug: { type: 'slug' },
                postType: { type: 'text' },
                header: { type: 'json' },
                sections: { type: 'json' },
            },
            admin: { hidden: true },
        },
    },
};
async function makeKey(scopes) {
    const { key, keyHash, keyPrefix } = await generateApiKey({ prefix: 'act_sk', scopes });
    return {
        key,
        record: {
            id: 'apikey-1',
            keyHash,
            keyPrefix,
            userId: 'user-1',
            scopes,
            ipRestrictions: null,
            expiresAt: null,
            lastUsedAt: null,
            revokedAt: null,
        },
    };
}
/**
 * Mock Prisma client covering the read/update path the section routes use:
 * apiKey auth, `document.findFirst`, and a `$transaction` that runs
 * `document.update` + `version.create`. `auditLog.create` is a no-op stub.
 */
function createMockDB(opts) {
    let current = opts.doc
        ? {
            id: opts.doc.id,
            collection: 'pages',
            title: 'About',
            slug: 'about',
            status: 'DRAFT',
            data: opts.doc.data,
            deletedAt: null,
            publishedAt: null,
            updatedAt: new Date(),
            createdAt: new Date(),
        }
        : null;
    return {
        apiKey: {
            findMany: async () => [opts.apiKey],
            findUnique: async ({ where }) => opts.apiKey.keyHash === where.keyHash ? opts.apiKey : null,
            update: async () => opts.apiKey,
        },
        document: {
            findMany: async () => [],
            count: async () => 0,
            findFirst: async () => current,
            findUnique: async () => current,
            update: async ({ data }) => {
                current = { ...current, ...data, updatedAt: new Date() };
                return current;
            },
        },
        version: { create: async () => ({ id: 'v-1' }) },
        auditLog: { create: async () => ({ id: 'a-1' }) },
        user: {},
        session: {},
        media: {},
        $transaction: async (fn) => fn({
            document: {
                update: async ({ data }) => {
                    current = { ...current, ...data, updatedAt: new Date() };
                    return current;
                },
            },
            version: { create: async () => ({ id: 'v-1' }) },
        }),
    };
}
describe('/api/cms/page-sections', () => {
    beforeEach(() => {
        delete globalThis.__actuateConfig;
        process.env.CMS_SECRET = VALID_SECRET;
    });
    it('GET /page-sections/types returns the registered section types', async () => {
        const { key, record } = await makeKey({ collections: ['pages'], actions: ['read'] });
        const db = createMockDB({ apiKey: record });
        initDB(db);
        const handler = handleActuateAPI({ prismaClient: db, config: PAGES_CONFIG });
        const res = await handler(new Request('https://x.com/api/cms/page-sections/types', {
            headers: { Authorization: `Bearer ${key}` },
        }));
        expect(res.status).toBe(200);
        const payload = (await res.json());
        expect(payload.data.map((t) => t.id).sort()).toEqual([
            'article-body',
            'author-bio',
            'cta',
            'feature',
            'form',
            'gallery',
            'hero',
            'quote',
            'related-posts',
            'stats',
            'text',
        ]);
    });
    it('GET /page-sections/types?scope=post filters to post-usable types', async () => {
        const { key, record } = await makeKey({ collections: ['pages'], actions: ['read'] });
        const db = createMockDB({ apiKey: record });
        initDB(db);
        const handler = handleActuateAPI({ prismaClient: db, config: PAGES_CONFIG });
        const res = await handler(new Request('https://x.com/api/cms/page-sections/types?scope=post', {
            headers: { Authorization: `Bearer ${key}` },
        }));
        expect(res.status).toBe(200);
        const payload = (await res.json());
        const ids = payload.data.map((t) => t.id);
        expect(ids).toContain('article-body');
        expect(ids).toContain('hero'); // shared types still included
        // page-only behavior: scope=page must exclude post-only types
        const pageRes = await handler(new Request('https://x.com/api/cms/page-sections/types?scope=page', {
            headers: { Authorization: `Bearer ${key}` },
        }));
        const pagePayload = (await pageRes.json());
        expect(pagePayload.data.map((t) => t.id)).not.toContain('article-body');
    });
    it('GET /page-sections/types requires authentication', async () => {
        const { record } = await makeKey({ collections: ['pages'], actions: ['read'] });
        const db = createMockDB({ apiKey: record });
        initDB(db);
        const handler = handleActuateAPI({ prismaClient: db, config: PAGES_CONFIG });
        const res = await handler(new Request('https://x.com/api/cms/page-sections/types'));
        expect(res.status).toBe(401);
    });
    it('POST /page-sections/validate reports errors for invalid content', async () => {
        const { key, record } = await makeKey({ collections: ['pages'], actions: ['read'] });
        const db = createMockDB({ apiKey: record });
        initDB(db);
        const handler = handleActuateAPI({ prismaClient: db, config: PAGES_CONFIG });
        const res = await handler(new Request('https://x.com/api/cms/page-sections/validate', {
            method: 'POST',
            headers: { Authorization: `Bearer ${key}`, 'Content-Type': 'application/json' },
            body: JSON.stringify({ sectionType: 'hero', content: { heading: '' } }),
        }));
        expect(res.status).toBe(200);
        const payload = (await res.json());
        expect(payload.data.valid).toBe(false);
        expect(payload.data.errors.length).toBeGreaterThan(0);
    });
    it('POST /page-sections/validate 400s when sectionType is missing', async () => {
        const { key, record } = await makeKey({ collections: ['pages'], actions: ['read'] });
        const db = createMockDB({ apiKey: record });
        initDB(db);
        const handler = handleActuateAPI({ prismaClient: db, config: PAGES_CONFIG });
        const res = await handler(new Request('https://x.com/api/cms/page-sections/validate', {
            method: 'POST',
            headers: { Authorization: `Bearer ${key}`, 'Content-Type': 'application/json' },
            body: JSON.stringify({ content: {} }),
        }));
        expect(res.status).toBe(400);
    });
    it('POST /page-sections/:collection/:id adds a section and returns validation', async () => {
        const { key, record } = await makeKey({ collections: ['pages'], actions: ['read', 'update'] });
        const db = createMockDB({ apiKey: record, doc: { id: 'page-1', data: { sections: [] } } });
        initDB(db);
        const handler = handleActuateAPI({ prismaClient: db, config: PAGES_CONFIG });
        const res = await handler(new Request('https://x.com/api/cms/page-sections/pages/page-1', {
            method: 'POST',
            headers: { Authorization: `Bearer ${key}`, 'Content-Type': 'application/json' },
            body: JSON.stringify({ sectionType: 'hero', content: { heading: 'Welcome' } }),
        }));
        expect(res.status).toBe(201);
        const payload = (await res.json());
        expect(payload.data.section.sectionType).toBe('hero');
        expect(payload.data.section.content.heading).toBe('Welcome');
        expect(payload.data.sections).toHaveLength(1);
        expect(payload.data.validation.valid).toBe(true);
    });
    it('POST add preserves an existing externally-managed (unmanaged) section', async () => {
        const { key, record } = await makeKey({ collections: ['pages'], actions: ['read', 'update'] });
        // The page already carries a seed/code-owned section of an unknown type.
        const db = createMockDB({
            apiKey: record,
            doc: {
                id: 'page-1',
                data: { sections: [{ id: 'u1', sectionType: 'opal-hero', content: { title: 'Keep me' } }] },
            },
        });
        initDB(db);
        const handler = handleActuateAPI({ prismaClient: db, config: PAGES_CONFIG });
        const addRes = await handler(new Request('https://x.com/api/cms/page-sections/pages/page-1', {
            method: 'POST',
            headers: { Authorization: `Bearer ${key}`, 'Content-Type': 'application/json' },
            body: JSON.stringify({ sectionType: 'hero', content: { heading: 'New' } }),
        }));
        expect(addRes.status).toBe(201);
        const added = (await addRes.json());
        // The unknown section is preserved (flagged unmanaged) alongside the new one.
        expect(added.data.sections.map((s) => s.sectionType)).toEqual(['opal-hero', 'hero']);
        expect(added.data.sections[0].unmanaged).toBe(true);
        // It survives a re-read with its content intact (no silent data loss).
        const readRes = await handler(new Request('https://x.com/api/cms/page-sections/pages/page-1', {
            headers: { Authorization: `Bearer ${key}` },
        }));
        const read = (await readRes.json());
        const opal = read.data.find((s) => s.sectionType === 'opal-hero');
        expect(opal.content).toEqual({ title: 'Keep me' });
    });
    it('registers config-declared custom section types (discovery + add)', async () => {
        const CUSTOM_CONFIG = {
            ...PAGES_CONFIG,
            sections: {
                customTypes: [
                    {
                        id: 'pricing',
                        name: 'Pricing Table',
                        scope: ['page'],
                        fields: [{ key: 'heading', label: 'Heading', type: 'text', required: true }],
                        defaultContent: { heading: 'Plans' },
                    },
                ],
            },
        };
        const { key, record } = await makeKey({ collections: ['pages'], actions: ['read', 'update'] });
        const db = createMockDB({ apiKey: record, doc: { id: 'page-1', data: { sections: [] } } });
        initDB(db);
        const handler = handleActuateAPI({ prismaClient: db, config: CUSTOM_CONFIG });
        const typesRes = await handler(new Request('https://x.com/api/cms/page-sections/types?scope=page', {
            headers: { Authorization: `Bearer ${key}` },
        }));
        const types = (await typesRes.json());
        expect(types.data.map((t) => t.id)).toContain('pricing');
        const addRes = await handler(new Request('https://x.com/api/cms/page-sections/pages/page-1', {
            method: 'POST',
            headers: { Authorization: `Bearer ${key}`, 'Content-Type': 'application/json' },
            body: JSON.stringify({ sectionType: 'pricing', content: { heading: 'Our plans' } }),
        }));
        expect(addRes.status).toBe(201);
        const added = (await addRes.json());
        // A registered custom type is a first-class, editable section — not unmanaged.
        expect(added.data.section.sectionType).toBe('pricing');
        expect(added.data.section.unmanaged).toBeUndefined();
        expect(added.data.validation.valid).toBe(true);
    });
    it('accepts the canonical sections.types key and lists customs unscoped', async () => {
        // Item 1: `sections.types` is the canonical key (customTypes is a back-compat
        // alias). Item 2: the unscoped /types endpoint must include custom types so
        // the admin can hydrate its registry from the API on boot.
        const CONFIG = {
            ...PAGES_CONFIG,
            sections: {
                types: [
                    {
                        id: 'pricing',
                        name: 'Pricing Table',
                        scope: ['page'],
                        fields: [{ key: 'heading', label: 'Heading', type: 'text', required: true }],
                        defaultContent: { heading: 'Plans' },
                    },
                ],
            },
        };
        const { key, record } = await makeKey({ collections: ['pages'], actions: ['read'] });
        const db = createMockDB({ apiKey: record });
        initDB(db);
        const handler = handleActuateAPI({ prismaClient: db, config: CONFIG });
        const res = await handler(new Request('https://x.com/api/cms/page-sections/types', {
            headers: { Authorization: `Bearer ${key}` },
        }));
        expect(res.status).toBe(200);
        const payload = (await res.json());
        const ids = payload.data.map((t) => t.id);
        expect(ids).toContain('pricing'); // custom type surfaced unscoped
        expect(ids).toContain('hero'); // built-ins still present
    });
    it('POST add rejects an unknown section type with 400', async () => {
        const { key, record } = await makeKey({ collections: ['pages'], actions: ['read', 'update'] });
        const db = createMockDB({ apiKey: record, doc: { id: 'page-1', data: { sections: [] } } });
        initDB(db);
        const handler = handleActuateAPI({ prismaClient: db, config: PAGES_CONFIG });
        const res = await handler(new Request('https://x.com/api/cms/page-sections/pages/page-1', {
            method: 'POST',
            headers: { Authorization: `Bearer ${key}`, 'Content-Type': 'application/json' },
            body: JSON.stringify({ sectionType: 'ghost' }),
        }));
        expect(res.status).toBe(400);
    });
    it('POST add 409s on a page owned by the visual builder (_layout present)', async () => {
        // ADR 0002 ownership guard: a builder-owned page compiles its sections from
        // `_layout` on save, so direct section edits would be silently overwritten.
        // The write routes must refuse it (the read route still works).
        const { key, record } = await makeKey({ collections: ['pages'], actions: ['read', 'update'] });
        const db = createMockDB({
            apiKey: record,
            doc: { id: 'page-1', data: { _layout: { type: 'page', children: [] }, sections: [] } },
        });
        initDB(db);
        const handler = handleActuateAPI({ prismaClient: db, config: PAGES_CONFIG });
        const res = await handler(new Request('https://x.com/api/cms/page-sections/pages/page-1', {
            method: 'POST',
            headers: { Authorization: `Bearer ${key}`, 'Content-Type': 'application/json' },
            body: JSON.stringify({ sectionType: 'hero', content: { heading: 'Nope' } }),
        }));
        expect(res.status).toBe(409);
        // …but reading the page's sections is still allowed.
        const readRes = await handler(new Request('https://x.com/api/cms/page-sections/pages/page-1', {
            headers: { Authorization: `Bearer ${key}` },
        }));
        expect(readRes.status).toBe(200);
    });
    it('POST add 400s on a collection without a sections field', async () => {
        const { key, record } = await makeKey({ collections: ['notes'], actions: ['read', 'update'] });
        const db = createMockDB({ apiKey: record, doc: { id: 'n-1', data: {} } });
        initDB(db);
        const handler = handleActuateAPI({ prismaClient: db, config: PAGES_CONFIG });
        const res = await handler(new Request('https://x.com/api/cms/page-sections/notes/n-1', {
            method: 'POST',
            headers: { Authorization: `Bearer ${key}`, 'Content-Type': 'application/json' },
            body: JSON.stringify({ sectionType: 'hero' }),
        }));
        expect(res.status).toBe(400);
    });
});
describe('/api/cms/post-templates', () => {
    beforeEach(() => {
        delete globalThis.__actuateConfig;
        process.env.CMS_SECRET = VALID_SECRET;
    });
    it('GET /post-templates/:type returns an unsaved default when none exists', async () => {
        const { key, record } = await makeKey({ collections: ['post-templates'], actions: ['read'] });
        const db = createMockDB({ apiKey: record });
        initDB(db);
        const handler = handleActuateAPI({ prismaClient: db, config: PAGES_CONFIG });
        const res = await handler(new Request('https://x.com/api/cms/post-templates/blog', {
            headers: { Authorization: `Bearer ${key}` },
        }));
        expect(res.status).toBe(200);
        const payload = (await res.json());
        expect(payload.data.postType).toBe('blog');
        expect(payload.data.docId).toBeNull();
        expect(payload.data.header.layout).toBe('centered');
        // With no saved template the endpoint hands back a usable starting
        // template (article body + related posts) rather than a blank one, so new
        // posts and the template editor are never empty. docId stays null so
        // callers can tell it hasn't been persisted.
        const sectionTypes = payload.data.sections.map((s) => s.sectionType);
        expect(sectionTypes).toEqual(['article-body', 'related-posts']);
    });
    it('GET /post-templates/:type requires authentication', async () => {
        const { record } = await makeKey({ collections: ['post-templates'], actions: ['read'] });
        const db = createMockDB({ apiKey: record });
        initDB(db);
        const handler = handleActuateAPI({ prismaClient: db, config: PAGES_CONFIG });
        const res = await handler(new Request('https://x.com/api/cms/post-templates/blog'));
        expect(res.status).toBe(401);
    });
});
//# sourceMappingURL=page-sections-routes.test.js.map