/**
 * Regression tests for the `/api/cms/collections` discovery endpoint
 * shipped in PR F (Phase 7 — MCP-parity stylistic cleanup).
 *
 * Purpose: enumerate the configured collections without relying on the
 * OpenAPI generator, which has historically drifted out of sync with
 * the admin-visible surface. The endpoint is unauthenticated by design
 * — the response contains schema-shape metadata that is already public
 * via `/openapi.json` — but we test that contract here so a future
 * change can't silently widen it.
 */
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
import { handleActuateAPI } from '../../api/index.js';
import { setActuateConfig } from '../../config/runtime.js';
const SECRET = 'test-secret-collections-discovery-pr-f-12345';
describe('GET /api/cms/collections (PR F discovery endpoint)', () => {
    // Snapshot every env var any test in this file touches so we can
    // restore them in `afterEach`. Mirrors the pattern in health.test.ts —
    // PR #155 review (Bugbot) caught that the prior implementation set
    // CMS_SECRET in beforeEach but never restored it, leaking state into
    // any other test that runs after this file.
    const original = {
        CMS_SECRET: process.env.CMS_SECRET,
        CMS_SESSION_SECRET: process.env.CMS_SESSION_SECRET,
    };
    beforeEach(() => {
        process.env.CMS_SECRET = SECRET;
        delete process.env.CMS_SESSION_SECRET;
    });
    afterEach(() => {
        for (const [k, v] of Object.entries(original)) {
            if (v === undefined)
                delete process.env[k];
            else
                process.env[k] = v;
        }
        delete globalThis.__actuateConfig;
    });
    function makeHandler() {
        setActuateConfig({
            secret: SECRET,
            collections: {
                posts: {
                    slug: 'posts',
                    labels: { singular: 'Post', plural: 'Posts' },
                    type: 'post',
                    urlPrefix: 'blog',
                    fields: {
                        title: { type: 'text', required: true },
                        body: { type: 'richText' },
                        author: { type: 'relationship', relationTo: 'users' },
                    },
                },
                pages: {
                    slug: 'pages',
                    labels: { singular: 'Page', plural: 'Pages' },
                    type: 'page',
                    fields: {
                        title: { type: 'text', required: true },
                        slug: { type: 'slug', required: true },
                    },
                },
                hidden_internal: {
                    slug: 'hidden_internal',
                    labels: { singular: 'Internal', plural: 'Internal' },
                    fields: { name: { type: 'text' } },
                    admin: { hidden: true },
                },
            },
        });
        return handleActuateAPI({ prismaClient: {} });
    }
    it('returns one entry per configured collection with field summaries', async () => {
        const handler = makeHandler();
        const response = await handler(new Request('https://example.com/api/cms/collections', { method: 'GET' }));
        expect(response.status).toBe(200);
        const payload = (await response.json());
        expect(payload.data).toHaveLength(3);
        const bySlug = new Map(payload.data.map((c) => [c.slug, c]));
        const posts = bySlug.get('posts');
        expect(posts.labels).toEqual({ singular: 'Post', plural: 'Posts' });
        expect(posts.type).toBe('post');
        expect(posts.fieldCount).toBe(3);
        expect(posts.fields).toEqual(expect.arrayContaining([
            { name: 'title', type: 'text', required: true },
            { name: 'body', type: 'richText', required: false },
            // Relationship fields now surface their target collection so an agent
            // can resolve slugs against it (WS-C C2).
            { name: 'author', type: 'relationship', required: false, relationTo: 'users' },
        ]));
        const pages = bySlug.get('pages');
        expect(pages.type).toBe('page');
        expect(pages.fieldCount).toBe(2);
        expect(pages.fields.find((f) => f.name === 'title')?.required).toBe(true);
    });
    it('surfaces admin.hidden as `hidden: true` so agents can choose to skip them', async () => {
        // Internal collections should still be discoverable — the agent
        // might legitimately want to read from them — but the flag lets
        // the agent down-rank them in its planning.
        const handler = makeHandler();
        const response = await handler(new Request('https://example.com/api/cms/collections', { method: 'GET' }));
        const payload = (await response.json());
        const hiddenEntry = payload.data.find((c) => c.slug === 'hidden_internal');
        expect(hiddenEntry?.hidden).toBe(true);
        const postsEntry = payload.data.find((c) => c.slug === 'posts');
        expect(postsEntry?.hidden).toBe(false);
    });
    it('returns 500 when no CMS config is registered (deploy misconfig)', async () => {
        // Defensive: route must error loudly when getActuateConfig() is
        // missing instead of returning `{ data: [] }`, which would lie to
        // the MCP fallback chain about the server's actual state.
        const handler = handleActuateAPI({ prismaClient: {} });
        const response = await handler(new Request('https://example.com/api/cms/collections', { method: 'GET' }));
        expect(response.status).toBe(500);
    });
    it('surfaces admin.description / icon / color / group for the Posts admin area', async () => {
        // The Posts dashboard area renders per-collection cards that need
        // these visual hints (icon, accent color, description) plus the
        // grouping marker. Locking the contract here keeps the admin
        // client and server in sync — if a field is renamed server-side,
        // the test catches it before /posts/types renders empty cards.
        setActuateConfig({
            secret: SECRET,
            collections: {
                blog: {
                    slug: 'blog',
                    labels: { singular: 'Blog Post', plural: 'Blog' },
                    type: 'post',
                    urlPrefix: 'blog',
                    fields: { title: { type: 'text', required: true } },
                    admin: {
                        group: 'Posts',
                        description: 'Blog posts and articles.',
                        icon: 'file-text',
                        color: 'purple',
                    },
                },
                pages: {
                    slug: 'pages',
                    labels: { singular: 'Page', plural: 'Pages' },
                    type: 'page',
                    fields: { title: { type: 'text', required: true } },
                },
            },
        });
        const handler = handleActuateAPI({ prismaClient: {} });
        const response = await handler(new Request('https://example.com/api/cms/collections', { method: 'GET' }));
        const payload = (await response.json());
        const blog = payload.data.find((c) => c.slug === 'blog');
        expect(blog.description).toBe('Blog posts and articles.');
        expect(blog.icon).toBe('file-text');
        expect(blog.color).toBe('purple');
        expect(blog.group).toBe('Posts');
        expect(blog.urlPrefix).toBe('blog');
        // Collections without admin metadata should emit `null` (not
        // `undefined`) so the wire shape stays JSON-stable.
        const pages = payload.data.find((c) => c.slug === 'pages');
        expect(pages.description).toBeNull();
        expect(pages.icon).toBeNull();
        expect(pages.color).toBeNull();
        expect(pages.group).toBeNull();
    });
    it('does not leak deeper schema (validators, hooks, access functions)', async () => {
        // The response is unauthenticated — field-level validators or
        // access functions could include source pointers / closure state
        // that mustn't ship in the public surface. Keep the contract tight:
        // every field always carries `name`/`type`/`required`, and may ONLY
        // additionally carry the relationship-resolver hints (`relationTo`,
        // `hasMany`, `options`) plus the WS-B4 nested-schema keys (`fields`,
        // `minRows`, `maxRows`, `blocks`). Anything else is a leak. The walk is
        // recursive so nested array/blocks subfields are held to the same bar.
        const ALLOWED_FIELD_KEYS = new Set([
            'name',
            'type',
            'required',
            'relationTo',
            'hasMany',
            'options',
            'fields',
            'minRows',
            'maxRows',
            'blocks',
        ]);
        const ALLOWED_BLOCK_KEYS = new Set(['slug', 'label', 'fields']);
        function assertFieldClean(field) {
            expect(field).toHaveProperty('name');
            expect(field).toHaveProperty('type');
            expect(field).toHaveProperty('required');
            for (const key of Object.keys(field)) {
                expect(ALLOWED_FIELD_KEYS.has(key)).toBe(true);
            }
            if (Array.isArray(field.fields)) {
                for (const sub of field.fields)
                    assertFieldClean(sub);
            }
            if (Array.isArray(field.blocks)) {
                for (const block of field.blocks) {
                    for (const key of Object.keys(block)) {
                        expect(ALLOWED_BLOCK_KEYS.has(key)).toBe(true);
                    }
                    for (const sub of block.fields ?? []) {
                        assertFieldClean(sub);
                    }
                }
            }
        }
        // Config exercises array + blocks so the recursive guard actually runs.
        setActuateConfig({
            secret: SECRET,
            collections: {
                landing: {
                    slug: 'landing',
                    labels: { singular: 'Landing', plural: 'Landing' },
                    type: 'page',
                    fields: {
                        title: { type: 'text', required: true },
                        features: {
                            type: 'array',
                            fields: { heading: { type: 'text' }, icon: { type: 'text' } },
                        },
                        layout: {
                            type: 'blocks',
                            blocks: [
                                {
                                    slug: 'hero',
                                    labels: { singular: 'Hero', plural: 'Heroes' },
                                    fields: { headline: { type: 'text' } },
                                },
                            ],
                        },
                    },
                },
            },
        });
        const handler = handleActuateAPI({ prismaClient: {} });
        const response = await handler(new Request('https://example.com/api/cms/collections', { method: 'GET' }));
        const payload = (await response.json());
        for (const collection of payload.data) {
            for (const field of collection.fields)
                assertFieldClean(field);
        }
    });
    it('surfaces array element subfields and blocks variant subfields (WS-B4)', async () => {
        // An agent planning a deep create needs the shape of each array row and
        // each block variant — not just `type: 'array'`. Lock the recursive
        // contract so a refactor can't silently flatten it back to top-level only.
        setActuateConfig({
            secret: SECRET,
            collections: {
                landing: {
                    slug: 'landing',
                    labels: { singular: 'Landing', plural: 'Landing' },
                    type: 'page',
                    fields: {
                        gallery: {
                            type: 'array',
                            minRows: 1,
                            maxRows: 8,
                            fields: {
                                image: { type: 'media', required: true },
                                caption: { type: 'text' },
                            },
                        },
                        layout: {
                            type: 'blocks',
                            blocks: [
                                {
                                    slug: 'hero',
                                    labels: { singular: 'Hero', plural: 'Heroes' },
                                    fields: { headline: { type: 'text', required: true } },
                                },
                                {
                                    slug: 'cta',
                                    labels: { singular: 'CTA', plural: 'CTAs' },
                                    fields: { url: { type: 'url' } },
                                },
                            ],
                        },
                    },
                },
            },
        });
        const handler = handleActuateAPI({ prismaClient: {} });
        const response = await handler(new Request('https://example.com/api/cms/collections', { method: 'GET' }));
        const payload = (await response.json());
        const landing = payload.data.find((c) => c.slug === 'landing');
        const byField = new Map(landing.fields.map((f) => [f.name, f]));
        const gallery = byField.get('gallery');
        expect(gallery.type).toBe('array');
        expect(gallery.minRows).toBe(1);
        expect(gallery.maxRows).toBe(8);
        const galleryFields = new Map((gallery.fields ?? []).map((f) => [f.name, f]));
        expect(galleryFields.get('image')).toEqual({ name: 'image', type: 'media', required: true });
        expect(galleryFields.get('caption')?.required).toBe(false);
        const layout = byField.get('layout');
        expect(layout.type).toBe('blocks');
        expect(layout.blocks).toHaveLength(2);
        const hero = layout.blocks.find((b) => b.slug === 'hero');
        expect(hero.label).toBe('Hero');
        expect(hero.fields.find((f) => f.name === 'headline')?.required).toBe(true);
        const cta = layout.blocks.find((b) => b.slug === 'cta');
        expect(cta.fields.map((f) => f.name)).toEqual(['url']);
    });
    it('surfaces relationTo / hasMany / select options for the relationship resolver (WS-C C2)', async () => {
        // The MCP relationship-resolver tools (get_relationship_fields,
        // resolve_relationship) depend on the discovery endpoint exposing where a
        // relationship field points and the enum choices for a select field. Lock
        // the contract so a future refactor can't silently drop these.
        setActuateConfig({
            secret: SECRET,
            collections: {
                articles: {
                    slug: 'articles',
                    labels: { singular: 'Article', plural: 'Articles' },
                    type: 'post',
                    fields: {
                        author: { type: 'relationship', relationTo: 'team' },
                        related: { type: 'relationship', relationTo: ['posts', 'case-studies'], hasMany: true },
                        hero: { type: 'media' },
                        status: {
                            type: 'select',
                            options: [
                                { label: 'News', value: 'news' },
                                { label: 'Guide', value: 'guide' },
                            ],
                        },
                    },
                },
            },
        });
        const handler = handleActuateAPI({ prismaClient: {} });
        const response = await handler(new Request('https://example.com/api/cms/collections', { method: 'GET' }));
        const payload = (await response.json());
        const articles = payload.data.find((c) => c.slug === 'articles');
        const byName = new Map(articles.fields.map((f) => [f.name, f]));
        expect(byName.get('author')?.relationTo).toBe('team');
        expect(byName.get('author')?.hasMany).toBeUndefined();
        expect(byName.get('related')?.relationTo).toEqual(['posts', 'case-studies']);
        expect(byName.get('related')?.hasMany).toBe(true);
        // Media fields have no relationTo (the media store is implicit).
        expect(byName.get('hero')?.relationTo).toBeUndefined();
        expect(byName.get('status')?.options).toEqual([
            { label: 'News', value: 'news' },
            { label: 'Guide', value: 'guide' },
        ]);
    });
});
//# sourceMappingURL=collections-discovery.test.js.map