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';
// Slice E: preview tokens + per-document schedule endpoints.
//
// We mock just enough of Prisma to let the route handlers do their job:
//   - `apiKey.findUnique` for auth,
//   - a mutable `document` collection so update/findFirst can round-trip,
//   - the audit/version models the handlers touch as side-effects.
const VALID_SECRET = 'a'.repeat(48);
function createMockDB(opts) {
    const apiKey = opts.apiKey;
    const docs = (opts.docs ?? []).map((d) => ({ ...d, deletedAt: d.deletedAt ?? null }));
    const matches = (d, where) => {
        if (where.id && d.id !== where.id)
            return false;
        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;
        if (where.OR && Array.isArray(where.OR)) {
            return where.OR.some((clause) => {
                if (clause.slug !== undefined)
                    return d.slug === clause.slug;
                if (clause.data?.path && clause.data?.equals) {
                    const path = clause.data.path[0];
                    return d.data?.[path] === clause.data.equals;
                }
                return false;
            });
        }
        return true;
    };
    return {
        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 } = {}) => docs.filter((d) => (where ? matches(d, where) : true)),
            findFirst: async ({ where }) => docs.find((d) => matches(d, where ?? {})) ?? null,
            count: async () => docs.length,
            create: async ({ data }) => {
                const doc = { ...data, id: data.id ?? `doc-${docs.length + 1}` };
                docs.push(doc);
                return doc;
            },
            update: async ({ where, data }) => {
                const idx = docs.findIndex((d) => d.id === where.id);
                if (idx === -1)
                    throw new Error('not found');
                docs[idx] = { ...docs[idx], ...data };
                return docs[idx];
            },
        },
        version: {
            create: async ({ data }) => ({ ...data, id: 'ver-1' }),
            findMany: async () => [],
            count: async () => 0,
        },
        documentRevision: { create: async ({ data }) => ({ ...data, id: 'rev-1' }) },
        auditLog: { create: async () => ({}) },
        webhook: { findMany: async () => [] },
        webhookDelivery: { create: async () => ({}) },
        user: {},
        session: {},
        media: {},
    };
}
async function makeKey(scopes) {
    const { key, keyHash, keyPrefix } = await generateApiKey({ prefix: 'act_sk', scopes });
    return {
        key,
        record: {
            id: 'apikey-e',
            keyHash,
            keyPrefix,
            userId: 'user-e',
            scopes,
            ipRestrictions: null,
            expiresAt: null,
            lastUsedAt: null,
            revokedAt: null,
        },
    };
}
const postsCollection = {
    slug: 'posts',
    labels: { singular: 'Post', plural: 'Posts' },
    type: 'post',
    urlPrefix: 'blog',
    fields: {
        title: { type: 'text', label: 'Title', required: true },
        slug: { type: 'slug', label: 'Slug', required: true },
        body: { type: 'richText', label: 'Body' },
    },
};
const cmsConfig = { collections: { posts: postsCollection } };
beforeEach(() => {
    delete globalThis.__actuateConfig;
    process.env.CMS_SECRET = VALID_SECRET;
});
describe('POST /preview/token', () => {
    it('issues a token for an existing document and includes its slug', async () => {
        const { key, record } = await makeKey({ admin: true });
        const docs = [
            {
                id: 'doc-1',
                collection: 'posts',
                slug: 'spring-cleaning',
                status: 'DRAFT',
                data: { title: 'Spring Cleaning', slug: 'spring-cleaning' },
                publishedAt: null,
            },
        ];
        const db = createMockDB({ apiKey: record, docs });
        initDB(db);
        const handler = handleActuateAPI({ prismaClient: db, config: cmsConfig });
        const response = await handler(new Request('https://example.com/api/cms/preview/token', {
            method: 'POST',
            headers: { Authorization: `Bearer ${key}`, 'Content-Type': 'application/json' },
            body: JSON.stringify({ collection: 'posts', documentId: 'doc-1', ttlSeconds: 3600 }),
        }));
        expect(response.status).toBe(200);
        const body = await response.json();
        expect(typeof body.data.token).toBe('string');
        expect(body.data.token.split('.')).toHaveLength(3);
        expect(body.data.slug).toBe('spring-cleaning');
        const ttlMs = new Date(body.data.expiresAt).getTime() - Date.now();
        expect(ttlMs).toBeGreaterThan(3500 * 1000);
        expect(ttlMs).toBeLessThanOrEqual(3700 * 1000);
    });
    it('returns 404 when issuing a token for a document that does not exist', async () => {
        const { key, record } = await makeKey({ admin: true });
        const db = createMockDB({ apiKey: record, docs: [] });
        initDB(db);
        const handler = handleActuateAPI({ prismaClient: db, config: cmsConfig });
        const response = await handler(new Request('https://example.com/api/cms/preview/token', {
            method: 'POST',
            headers: { Authorization: `Bearer ${key}`, 'Content-Type': 'application/json' },
            body: JSON.stringify({ collection: 'posts', documentId: 'missing' }),
        }));
        expect(response.status).toBe(404);
    });
});
describe('GET /resolve?preview=<token>', () => {
    it('returns DRAFT content when a valid preview token is supplied', async () => {
        const { key, record } = await makeKey({ admin: true });
        const docs = [
            {
                id: 'doc-1',
                collection: 'posts',
                slug: 'fresh-draft',
                status: 'DRAFT',
                data: { title: 'Fresh Draft', slug: 'fresh-draft', body: '<p>Hi</p>' },
                publishedAt: null,
                updatedAt: new Date('2026-04-01T00:00:00Z'),
            },
        ];
        const db = createMockDB({ apiKey: record, docs });
        initDB(db);
        const handler = handleActuateAPI({ prismaClient: db, config: cmsConfig });
        const tokenRes = await handler(new Request('https://example.com/api/cms/preview/token', {
            method: 'POST',
            headers: { Authorization: `Bearer ${key}`, 'Content-Type': 'application/json' },
            body: JSON.stringify({ collection: 'posts', documentId: 'doc-1' }),
        }));
        const tokenBody = await tokenRes.json();
        const token = tokenBody.data.token;
        const resolveRes = await handler(new Request(`https://example.com/api/cms/resolve?path=/blog/fresh-draft&preview=${encodeURIComponent(token)}`));
        expect(resolveRes.status).toBe(200);
        const body = await resolveRes.json();
        expect(body.data.id).toBe('doc-1');
        expect(body.data.status).toBe('DRAFT');
        expect(body.data.data.title).toBe('Fresh Draft');
        expect(body.preview?.active).toBe(true);
        expect(typeof body.preview.expiresAt).toBe('string');
    });
    it('returns 404 when no preview token is supplied and the doc is DRAFT', async () => {
        const docs = [
            {
                id: 'doc-1',
                collection: 'posts',
                slug: 'fresh-draft',
                status: 'DRAFT',
                data: { title: 'Fresh Draft', slug: 'fresh-draft' },
                publishedAt: null,
            },
        ];
        const db = createMockDB({ docs });
        initDB(db);
        const handler = handleActuateAPI({ prismaClient: db, config: cmsConfig });
        const response = await handler(new Request('https://example.com/api/cms/resolve?path=/blog/fresh-draft'));
        expect(response.status).toBe(404);
    });
    it('rejects a preview token that does not match the resolved document', async () => {
        const { key, record } = await makeKey({ admin: true });
        const docs = [
            {
                id: 'doc-1',
                collection: 'posts',
                slug: 'draft-a',
                status: 'DRAFT',
                data: { title: 'A', slug: 'draft-a' },
                publishedAt: null,
            },
            {
                id: 'doc-2',
                collection: 'posts',
                slug: 'draft-b',
                status: 'DRAFT',
                data: { title: 'B', slug: 'draft-b' },
                publishedAt: null,
            },
        ];
        const db = createMockDB({ apiKey: record, docs });
        initDB(db);
        const handler = handleActuateAPI({ prismaClient: db, config: cmsConfig });
        const tokenRes = await handler(new Request('https://example.com/api/cms/preview/token', {
            method: 'POST',
            headers: { Authorization: `Bearer ${key}`, 'Content-Type': 'application/json' },
            body: JSON.stringify({ collection: 'posts', documentId: 'doc-1' }),
        }));
        const token = (await tokenRes.json()).data.token;
        // Try to use the doc-1 token to peek at doc-2.
        const resolveRes = await handler(new Request(`https://example.com/api/cms/resolve?path=/blog/draft-b&preview=${encodeURIComponent(token)}`));
        expect(resolveRes.status).toBe(403);
    });
    it('falls back to the PUBLISHED-only filter when the token is invalid', async () => {
        const docs = [
            {
                id: 'doc-1',
                collection: 'posts',
                slug: 'fresh-draft',
                status: 'DRAFT',
                data: { title: 'Fresh Draft', slug: 'fresh-draft' },
                publishedAt: null,
            },
        ];
        const db = createMockDB({ docs });
        initDB(db);
        const handler = handleActuateAPI({ prismaClient: db, config: cmsConfig });
        const response = await handler(new Request('https://example.com/api/cms/resolve?path=/blog/fresh-draft&preview=not-a-jwt'));
        expect(response.status).toBe(404);
    });
});
describe('GET /preview/stream', () => {
    it('rejects when no token is supplied', async () => {
        const db = createMockDB({ docs: [] });
        initDB(db);
        const handler = handleActuateAPI({ prismaClient: db, config: cmsConfig });
        const response = await handler(new Request('https://example.com/api/cms/preview/stream', {
            headers: { Accept: 'text/event-stream' },
        }));
        expect(response.status).toBe(401);
    });
    it('rejects an invalid token', async () => {
        const db = createMockDB({ docs: [] });
        initDB(db);
        const handler = handleActuateAPI({ prismaClient: db, config: cmsConfig });
        const response = await handler(new Request('https://example.com/api/cms/preview/stream?token=garbage', {
            headers: { Accept: 'text/event-stream' },
        }));
        expect(response.status).toBe(401);
    });
    it('opens an SSE stream and emits a ready event for a valid token', async () => {
        const { key, record } = await makeKey({ admin: true });
        const docs = [
            {
                id: 'doc-1',
                collection: 'posts',
                slug: 'live',
                status: 'DRAFT',
                data: { title: 'Live' },
                publishedAt: null,
            },
        ];
        const db = createMockDB({ apiKey: record, docs });
        initDB(db);
        const handler = handleActuateAPI({ prismaClient: db, config: cmsConfig });
        const tokenRes = await handler(new Request('https://example.com/api/cms/preview/token', {
            method: 'POST',
            headers: { Authorization: `Bearer ${key}`, 'Content-Type': 'application/json' },
            body: JSON.stringify({ collection: 'posts', documentId: 'doc-1' }),
        }));
        const token = (await tokenRes.json()).data.token;
        const streamRes = await handler(new Request(`https://example.com/api/cms/preview/stream?token=${encodeURIComponent(token)}`, {
            headers: { Accept: 'text/event-stream' },
        }));
        expect(streamRes.status).toBe(200);
        expect(streamRes.headers.get('Content-Type')).toMatch(/text\/event-stream/);
        const reader = streamRes.body?.getReader();
        expect(reader).toBeDefined();
        const { value } = await reader.read();
        expect(value).toBeDefined();
        expect(new TextDecoder().decode(value)).toContain('event: ready');
        await reader.cancel().catch(() => undefined);
    });
});
describe('POST /collections/:slug/:id/schedule', () => {
    it('sets scheduledAt and flips status to SCHEDULED', async () => {
        const { key, record } = await makeKey({ admin: true });
        const docs = [
            {
                id: 'doc-1',
                collection: 'posts',
                slug: 'spring',
                status: 'DRAFT',
                data: { title: 'Spring', slug: 'spring' },
                publishedAt: null,
            },
        ];
        const db = createMockDB({ apiKey: record, docs });
        initDB(db);
        const handler = handleActuateAPI({ prismaClient: db, config: cmsConfig });
        const future = new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString();
        const response = await handler(new Request('https://example.com/api/cms/collections/posts/doc-1/schedule', {
            method: 'POST',
            headers: { Authorization: `Bearer ${key}`, 'Content-Type': 'application/json' },
            body: JSON.stringify({ scheduledAt: future }),
        }));
        expect(response.status).toBe(200);
        const body = await response.json();
        expect(body.data.status).toBe('SCHEDULED');
        expect(new Date(body.data.scheduledAt).toISOString()).toBe(future);
    });
    it('rejects a schedule with a past date', async () => {
        const { key, record } = await makeKey({ admin: true });
        const docs = [
            {
                id: 'doc-1',
                collection: 'posts',
                slug: 'spring',
                status: 'DRAFT',
                data: { title: 'Spring', slug: 'spring' },
                publishedAt: null,
            },
        ];
        const db = createMockDB({ apiKey: record, docs });
        initDB(db);
        const handler = handleActuateAPI({ prismaClient: db, config: cmsConfig });
        const past = new Date(Date.now() - 1000).toISOString();
        const response = await handler(new Request('https://example.com/api/cms/collections/posts/doc-1/schedule', {
            method: 'POST',
            headers: { Authorization: `Bearer ${key}`, 'Content-Type': 'application/json' },
            body: JSON.stringify({ scheduledAt: past }),
        }));
        expect(response.status).toBe(400);
        const body = await response.json();
        expect(body.error).toMatch(/future/);
    });
    it('rejects when scheduledUnpublishAt is not strictly after scheduledAt', async () => {
        const { key, record } = await makeKey({ admin: true });
        const docs = [
            {
                id: 'doc-1',
                collection: 'posts',
                slug: 'spring',
                status: 'DRAFT',
                data: { title: 'Spring', slug: 'spring' },
                publishedAt: null,
            },
        ];
        const db = createMockDB({ apiKey: record, docs });
        initDB(db);
        const handler = handleActuateAPI({ prismaClient: db, config: cmsConfig });
        const publishAt = new Date(Date.now() + 2 * 24 * 60 * 60 * 1000).toISOString();
        const unpublishAt = new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString();
        const response = await handler(new Request('https://example.com/api/cms/collections/posts/doc-1/schedule', {
            method: 'POST',
            headers: { Authorization: `Bearer ${key}`, 'Content-Type': 'application/json' },
            body: JSON.stringify({
                scheduledAt: publishAt,
                scheduledUnpublishAt: unpublishAt,
            }),
        }));
        expect(response.status).toBe(400);
        const body = await response.json();
        expect(body.error).toMatch(/after/);
    });
    it('returns 404 when the document does not exist', async () => {
        const { key, record } = await makeKey({ admin: true });
        const db = createMockDB({ apiKey: record, docs: [] });
        initDB(db);
        const handler = handleActuateAPI({ prismaClient: db, config: cmsConfig });
        const future = new Date(Date.now() + 60 * 60 * 1000).toISOString();
        const response = await handler(new Request('https://example.com/api/cms/collections/posts/missing/schedule', {
            method: 'POST',
            headers: { Authorization: `Bearer ${key}`, 'Content-Type': 'application/json' },
            body: JSON.stringify({ scheduledAt: future }),
        }));
        expect(response.status).toBe(404);
    });
    it('rejects when neither scheduledAt nor scheduledUnpublishAt is provided', async () => {
        const { key, record } = await makeKey({ admin: true });
        const docs = [
            {
                id: 'doc-1',
                collection: 'posts',
                slug: 'spring',
                status: 'DRAFT',
                data: { title: 'Spring', slug: 'spring' },
                publishedAt: null,
            },
        ];
        const db = createMockDB({ apiKey: record, docs });
        initDB(db);
        const handler = handleActuateAPI({ prismaClient: db, config: cmsConfig });
        const response = await handler(new Request('https://example.com/api/cms/collections/posts/doc-1/schedule', {
            method: 'POST',
            headers: { Authorization: `Bearer ${key}`, 'Content-Type': 'application/json' },
            body: JSON.stringify({}),
        }));
        expect(response.status).toBe(400);
    });
});
describe('DELETE /collections/:slug/:id/schedule', () => {
    it('clears the schedule and reverts SCHEDULED to DRAFT', async () => {
        const { key, record } = await makeKey({ admin: true });
        const docs = [
            {
                id: 'doc-1',
                collection: 'posts',
                slug: 'spring',
                status: 'SCHEDULED',
                data: { title: 'Spring', slug: 'spring' },
                publishedAt: null,
                scheduledAt: new Date(Date.now() + 24 * 60 * 60 * 1000),
            },
        ];
        const db = createMockDB({ apiKey: record, docs });
        initDB(db);
        const handler = handleActuateAPI({ prismaClient: db, config: cmsConfig });
        const response = await handler(new Request('https://example.com/api/cms/collections/posts/doc-1/schedule', {
            method: 'DELETE',
            headers: { Authorization: `Bearer ${key}` },
        }));
        expect(response.status).toBe(200);
        const body = await response.json();
        expect(body.data.status).toBe('DRAFT');
        expect(body.data.scheduledAt).toBeNull();
    });
    it('preserves PUBLISHED status when only an unpublish was scheduled', async () => {
        const { key, record } = await makeKey({ admin: true });
        const docs = [
            {
                id: 'doc-1',
                collection: 'posts',
                slug: 'spring',
                status: 'PUBLISHED',
                data: { title: 'Spring', slug: 'spring' },
                publishedAt: new Date('2026-01-01T00:00:00Z'),
                scheduledUnpublishAt: new Date(Date.now() + 24 * 60 * 60 * 1000),
            },
        ];
        const db = createMockDB({ apiKey: record, docs });
        initDB(db);
        const handler = handleActuateAPI({ prismaClient: db, config: cmsConfig });
        const response = await handler(new Request('https://example.com/api/cms/collections/posts/doc-1/schedule', {
            method: 'DELETE',
            headers: { Authorization: `Bearer ${key}` },
        }));
        expect(response.status).toBe(200);
        const body = await response.json();
        expect(body.data.status).toBe('PUBLISHED');
        expect(body.data.scheduledUnpublishAt).toBeNull();
    });
});
//# sourceMappingURL=preview-and-scheduling.test.js.map