import { beforeEach, describe, expect, it } from 'vitest';
import { handleActuateAPI } from '../../api/index.js';
import { createSession } from '../../auth/session.js';
import { initDB } from '../../db.js';
import { generateToken as generateCsrfToken } from '../../security/csrf.js';
import { __resetNotificationBus, notificationSubscriberCount, } from '../../realtime/notification-bus.js';
/**
 * Integration tests for the notifications REST endpoints introduced in
 * slice 6 of Phase 3. The pure service is covered by
 * `realtime/notifications.test.ts`; these tests exercise the handler
 * layer end-to-end (auth, JSON envelopes, status codes) and verify
 * that the bell only sees its own user's rows.
 */
const SECRET = 'test-secret-for-notifications-routes-1234567890ab';
function createMockDB() {
    const rows = new Map();
    let counter = 0;
    let clock = Date.UTC(2026, 5, 1, 0, 0, 0);
    return {
        rows,
        session: { findUnique: async () => ({ revokedAt: null }) },
        user: {},
        media: {},
        documentComment: {},
        auditLog: { create: async () => ({}) },
        documentNotification: {
            async create({ data, }) {
                counter += 1;
                clock += 1000;
                const row = {
                    id: `n-${counter}`,
                    userId: data.userId,
                    kind: data.kind,
                    documentId: data.documentId ?? null,
                    commentId: data.commentId ?? null,
                    payload: data.payload,
                    readAt: data.readAt ?? null,
                    createdAt: new Date(clock),
                };
                rows.set(row.id, row);
                return row;
            },
            async findUnique({ where }) {
                return rows.get(where.id) ?? null;
            },
            async findMany({ where, orderBy, take, }) {
                let r = Array.from(rows.values()).filter((row) => {
                    if (row.userId !== where.userId)
                        return false;
                    if (Object.prototype.hasOwnProperty.call(where, 'readAt')) {
                        if (where.readAt === null && row.readAt !== null)
                            return false;
                    }
                    return true;
                });
                r.sort((a, b) => orderBy?.createdAt === 'asc'
                    ? a.createdAt.getTime() - b.createdAt.getTime()
                    : b.createdAt.getTime() - a.createdAt.getTime());
                if (typeof take === 'number')
                    r = r.slice(0, take);
                return r;
            },
            async update({ where, data }) {
                const row = rows.get(where.id);
                if (!row)
                    throw new Error(`row ${where.id} not found`);
                if (data.readAt !== undefined)
                    row.readAt = data.readAt;
                return row;
            },
            async updateMany({ where, data, }) {
                let count = 0;
                for (const row of rows.values()) {
                    if (row.userId !== where.userId)
                        continue;
                    if (where.readAt === null && row.readAt !== null)
                        continue;
                    row.readAt = data.readAt;
                    count += 1;
                }
                return { count };
            },
            async count({ where }) {
                let n = 0;
                for (const row of rows.values()) {
                    if (row.userId === where.userId && row.readAt === null)
                        n += 1;
                }
                return n;
            },
        },
    };
}
async function sessionHeaders(role, userId = 'user-1') {
    const token = await createSession({ userId, role, sessionId: `session-${userId}` }, { secret: SECRET });
    const csrf = await generateCsrfToken();
    return {
        cookie: `actuate_session=${token}; actuate_csrf=${csrf}`,
        'x-csrf-token': csrf,
        'content-type': 'application/json',
    };
}
async function seedNotification(db, userId, override = {}) {
    return db.documentNotification.create({
        data: {
            userId,
            kind: 'comment_reply',
            documentId: 'doc-1',
            commentId: 'c-1',
            payload: { actorId: 'someone', actorName: 'Someone', excerpt: 'hi' },
            readAt: null,
            ...override,
        },
    });
}
describe('notifications REST endpoints (Phase 3 slice 6)', () => {
    let db;
    let handler;
    beforeEach(() => {
        process.env.CMS_SECRET = SECRET;
        db = createMockDB();
        initDB(db);
        handler = handleActuateAPI({ prismaClient: db });
    });
    it('rejects unauthenticated GET', async () => {
        const res = await handler(new Request('https://example.com/api/cms/notifications'));
        expect(res.status).toBe(401);
    });
    it('lists only the current user notifications', async () => {
        await seedNotification(db, 'user-1', {
            payload: { actorId: 'a', actorName: 'A', excerpt: 'mine' },
        });
        await seedNotification(db, 'user-2', {
            payload: { actorId: 'a', actorName: 'A', excerpt: 'theirs' },
        });
        const headers = await sessionHeaders('EDITOR', 'user-1');
        const res = await handler(new Request('https://example.com/api/cms/notifications', {
            headers: { cookie: headers.cookie },
        }));
        expect(res.status).toBe(200);
        const body = (await res.json());
        expect(body.data).toHaveLength(1);
        expect(body.data[0].payload.excerpt).toBe('mine');
    });
    it('returns the unread badge count', async () => {
        await seedNotification(db, 'user-1', {
            payload: { actorId: 'a', actorName: 'A', excerpt: 'a' },
        });
        await seedNotification(db, 'user-1', {
            payload: { actorId: 'a', actorName: 'A', excerpt: 'b' },
            readAt: new Date(),
        });
        const headers = await sessionHeaders('EDITOR', 'user-1');
        const res = await handler(new Request('https://example.com/api/cms/notifications/unread-count', {
            headers: { cookie: headers.cookie },
        }));
        expect(res.status).toBe(200);
        const body = (await res.json());
        expect(body.data.count).toBe(1);
    });
    it('marks a single notification read (idempotent)', async () => {
        const n = await seedNotification(db, 'user-1');
        const headers = await sessionHeaders('EDITOR', 'user-1');
        const first = await handler(new Request(`https://example.com/api/cms/notifications/${n.id}/read`, {
            method: 'POST',
            headers,
        }));
        expect(first.status).toBe(200);
        const firstBody = (await first.json());
        expect(firstBody.data.readAt).not.toBeNull();
        // Second mark — still 200 with the same readAt.
        const second = await handler(new Request(`https://example.com/api/cms/notifications/${n.id}/read`, {
            method: 'POST',
            headers,
        }));
        expect(second.status).toBe(200);
    });
    it("forbids marking another user's notification", async () => {
        const n = await seedNotification(db, 'user-1');
        const headers = await sessionHeaders('EDITOR', 'user-2');
        const res = await handler(new Request(`https://example.com/api/cms/notifications/${n.id}/read`, {
            method: 'POST',
            headers,
        }));
        expect(res.status).toBe(403);
    });
    it('marks all unread notifications read in one shot', async () => {
        for (let i = 0; i < 3; i++) {
            await seedNotification(db, 'user-1', {
                payload: { actorId: 'a', actorName: 'A', excerpt: String(i) },
            });
        }
        const headers = await sessionHeaders('EDITOR', 'user-1');
        const res = await handler(new Request('https://example.com/api/cms/notifications/read-all', {
            method: 'POST',
            headers,
        }));
        expect(res.status).toBe(200);
        const body = (await res.json());
        expect(body.data.count).toBe(3);
        const after = await handler(new Request('https://example.com/api/cms/notifications/unread-count', {
            headers: { cookie: headers.cookie },
        }));
        expect((await after.json()).data.count).toBe(0);
    });
    describe('GET /notifications/stream (SSE push channel)', () => {
        beforeEach(() => {
            __resetNotificationBus();
        });
        it('rejects unauthenticated stream requests with 401', async () => {
            const res = await handler(new Request('https://example.com/api/cms/notifications/stream'));
            expect(res.status).toBe(401);
        });
        it('returns 200 + text/event-stream + a ready frame for the session user', async () => {
            const headers = await sessionHeaders('EDITOR', 'user-1');
            const res = await handler(new Request('https://example.com/api/cms/notifications/stream', {
                headers: { cookie: headers.cookie },
            }));
            expect(res.status).toBe(200);
            expect(res.headers.get('content-type')).toMatch(/text\/event-stream/);
            expect(res.headers.get('cache-control')).toBe('no-cache, no-transform');
            // SSE buffering guard — without this proxies hold events back.
            expect(res.headers.get('x-accel-buffering')).toBe('no');
            const reader = res.body.getReader();
            try {
                // Subscription is attached synchronously in the stream's
                // start() — by the time we get a reader, the bus should
                // already have the listener registered. That contract matters
                // because the comment-write path publishes immediately after
                // the DB insert; a lazy subscribe would race.
                expect(notificationSubscriberCount('user-1')).toBe(1);
                const decoder = new TextDecoder();
                const first = await reader.read();
                expect(first.done).toBe(false);
                const text = decoder.decode(first.value);
                expect(text).toContain('event: ready');
                expect(text).toContain('"userId":"user-1"');
            }
            finally {
                // Cancel triggers the stream's cleanup hook — verifies the
                // bus subscription is dropped on hang-up.
                await reader.cancel().catch(() => undefined);
            }
            // Yield once so the cancel() callback has a chance to run
            // before we sample the subscriber count again.
            await new Promise((r) => setTimeout(r, 0));
            expect(notificationSubscriberCount('user-1')).toBe(0);
        });
    });
    it('honours includeRead=true', async () => {
        await seedNotification(db, 'user-1', {
            payload: { actorId: 'a', actorName: 'A', excerpt: 'unread' },
        });
        await seedNotification(db, 'user-1', {
            payload: { actorId: 'a', actorName: 'A', excerpt: 'read' },
            readAt: new Date(),
        });
        const headers = await sessionHeaders('EDITOR', 'user-1');
        const onlyUnread = await handler(new Request('https://example.com/api/cms/notifications', {
            headers: { cookie: headers.cookie },
        }));
        expect((await onlyUnread.json()).data.map((n) => n.payload.excerpt)).toEqual(['unread']);
        const all = await handler(new Request('https://example.com/api/cms/notifications?includeRead=true', {
            headers: { cookie: headers.cookie },
        }));
        const allExcerpts = (await all.json()).data.map((n) => n.payload.excerpt);
        expect(allExcerpts).toEqual(expect.arrayContaining(['unread', 'read']));
    });
});
//# sourceMappingURL=notifications-routes.test.js.map