import { afterEach, beforeEach, describe, expect, it } from 'vitest';
import { handleActuateAPI } from '../../api/index.js';
import { createSession } from '../../auth/session.js';
import { setActuateConfig } from '../../config/runtime.js';
import { initDB } from '../../db.js';
import { generateToken as generateCsrfToken } from '../../security/csrf.js';
/**
 * /api/cms/stats must surface "real CMS content" only — never the
 * benchmark suite's `benchmarks` / `bench` collections, never any
 * orphan/test collection sitting in the table.
 *
 * Two consumer shapes are supported and tested here:
 *   1. Production: the consumer registers collections via
 *      `handleActuateAPI({ config: { collections: { ... } } })`. The
 *      registered slugs become the allowlist; anything else is hidden.
 *   2. Dev: the consumer ships collections only to the admin client
 *      (no server-side config). The endpoint falls back to a denylist
 *      of internal slugs (`benchmarks`, `bench`).
 */
const SECRET = 'test-secret-for-stats-collection-filter-aaaaaaa';
/**
 * Apply a Prisma-style `where` clause to an in-memory row list.
 * Supports the exact subset our endpoint emits: `deletedAt: null`,
 * `status` equality, and `collection: { in: string[] | notIn: string[] }`.
 */
function matchesWhere(row, where) {
    if ('deletedAt' in where && where.deletedAt === null && row.deletedAt != null)
        return false;
    if ('status' in where && typeof where.status === 'string' && row.status !== where.status)
        return false;
    const c = where.collection;
    if (c?.in && !c.in.includes(row.collection))
        return false;
    if (c?.notIn && c.notIn.includes(row.collection))
        return false;
    return true;
}
function createMockDB(rows) {
    return {
        session: { findUnique: async () => ({ revokedAt: null }) },
        user: { count: async () => rows.length },
        media: { count: async () => 0 },
        formSubmission: { count: async () => 0 },
        webhookEndpoint: { count: async () => 0 },
        document: {
            async count({ where } = {}) {
                return rows.filter((r) => matchesWhere(r, where ?? {})).length;
            },
            async findMany({ where = {}, orderBy: _orderBy, take, select: _select }) {
                const filtered = rows.filter((r) => matchesWhere(r, where));
                filtered.sort((a, b) => b.updatedAt.getTime() - a.updatedAt.getTime());
                return typeof take === 'number' ? filtered.slice(0, take) : filtered;
            },
            async groupBy({ by, where = {}, _count: _c }) {
                const filtered = rows.filter((r) => matchesWhere(r, where));
                const key = by[0];
                const counts = new Map();
                for (const r of filtered) {
                    const k = String(r[key]);
                    counts.set(k, (counts.get(k) ?? 0) + 1);
                }
                return Array.from(counts, ([k, count]) => ({ [key]: k, _count: { _all: count } }));
            },
        },
    };
}
async function sessionHeaders(userId = 'user-1') {
    const token = await createSession({ userId, role: 'ADMIN', sessionId: `session-${userId}` }, { secret: SECRET });
    const csrf = await generateCsrfToken();
    return { cookie: `actuate_session=${token}; actuate_csrf=${csrf}` };
}
function row(overrides) {
    return {
        id: 'd-' + Math.random().toString(36).slice(2, 8),
        title: 'Untitled',
        status: 'PUBLISHED',
        collection: 'posts',
        updatedAt: new Date(),
        createdById: 'u-1',
        createdBy: { name: 'David', email: 'david@example.com' },
        data: {},
        deletedAt: null,
        ...overrides,
    };
}
describe('GET /api/cms/stats — collection filter', () => {
    const originalSecret = process.env.CMS_SECRET;
    beforeEach(() => {
        process.env.CMS_SECRET = SECRET;
    });
    afterEach(() => {
        if (originalSecret === undefined)
            delete process.env.CMS_SECRET;
        else
            process.env.CMS_SECRET = originalSecret;
        delete globalThis.__actuateConfig;
    });
    it('uses registered collections as an allowlist when config.collections is populated', async () => {
        const docs = [
            row({ id: 'p1', collection: 'posts', title: 'Real Post' }),
            row({ id: 'pg1', collection: 'pages', title: 'Real Page' }),
            // Benchmark docs sitting alongside in the same table:
            row({ id: 'b1', collection: 'benchmarks', title: 'Bench A' }),
            row({ id: 'b2', collection: 'bench', title: 'Bench B' }),
            // An orphan collection from a removed feature:
            row({ id: 'o1', collection: 'old_removed', title: 'Orphan' }),
        ];
        const db = createMockDB(docs);
        initDB(db);
        const handler = handleActuateAPI({
            prismaClient: db,
            config: {
                collections: {
                    posts: { slug: 'posts', fields: {} },
                    pages: { slug: 'pages', fields: {} },
                },
            },
        });
        const headers = await sessionHeaders();
        const res = await handler(new Request('https://example.com/api/cms/stats', { headers: { cookie: headers.cookie } }));
        expect(res.status).toBe(200);
        const body = (await res.json());
        expect(body.data.totalDocuments).toBe(2); // posts + pages only
        expect(body.data.collectionCounts).toEqual({ posts: 1, pages: 1 });
        const recentIds = body.data.recentDocuments.map((d) => d.id).sort();
        expect(recentIds).toEqual(['p1', 'pg1']);
        // Belt and suspenders:
        const collections = new Set(body.data.recentDocuments.map((d) => d.collection));
        expect(collections.has('benchmarks')).toBe(false);
        expect(collections.has('bench')).toBe(false);
        expect(collections.has('old_removed')).toBe(false);
    });
    it('falls back to the internal denylist when config.collections is empty', async () => {
        // Dev-style setup: handler is created without `config.collections`,
        // simulating the apps/dev pattern where collections live on the
        // admin client only.
        const docs = [
            row({ id: 'p1', collection: 'posts', title: 'Real Post' }),
            row({ id: 'pg1', collection: 'pages', title: 'Real Page' }),
            row({ id: 'b1', collection: 'benchmarks', title: 'Bench A' }),
            row({ id: 'b2', collection: 'bench', title: 'Bench B' }),
            row({ id: 'o1', collection: 'old_removed', title: 'Orphan' }),
        ];
        const db = createMockDB(docs);
        initDB(db);
        setActuateConfig({});
        const handler = handleActuateAPI({ prismaClient: db });
        const headers = await sessionHeaders();
        const res = await handler(new Request('https://example.com/api/cms/stats', { headers: { cookie: headers.cookie } }));
        expect(res.status).toBe(200);
        const body = (await res.json());
        // Anything NOT in the denylist counts as content — including the
        // legacy `old_removed` rows. That's the right trade-off for the
        // dev fallback: better to over-show than to hide live editor
        // collections we don't know about.
        const recentCollections = body.data.recentDocuments.map((d) => d.collection);
        expect(recentCollections).toContain('posts');
        expect(recentCollections).toContain('pages');
        expect(recentCollections).toContain('old_removed');
        expect(recentCollections).not.toContain('benchmarks');
        expect(recentCollections).not.toContain('bench');
        expect(body.data.collectionCounts.benchmarks).toBeUndefined();
        expect(body.data.collectionCounts.bench).toBeUndefined();
        expect(body.data.collectionCounts.posts).toBe(1);
    });
    it('still respects deletedAt: null alongside the collection filter', async () => {
        const docs = [
            row({ id: 'p1', collection: 'posts', title: 'Live' }),
            row({
                id: 'p2',
                collection: 'posts',
                title: 'Trashed',
                deletedAt: new Date('2026-01-01'),
            }),
        ];
        const db = createMockDB(docs);
        initDB(db);
        const handler = handleActuateAPI({
            prismaClient: db,
            config: { collections: { posts: { slug: 'posts', fields: {} } } },
        });
        const headers = await sessionHeaders();
        const res = await handler(new Request('https://example.com/api/cms/stats', { headers: { cookie: headers.cookie } }));
        const body = (await res.json());
        expect(body.data.totalDocuments).toBe(1);
        expect(body.data.recentDocuments).toHaveLength(1);
        expect(body.data.recentDocuments[0].id).toBe('p1');
    });
});
//# sourceMappingURL=stats-collection-filter.test.js.map