import { beforeEach, describe, expect, it } from 'vitest';
import { createComment, deleteComment, listComments, normaliseBody, parseAnchor, reopenComment, resolveComment, toCommentDTO, updateComment, MAX_COMMENT_BODY_LENGTH, MAX_COMMENT_LIST_PAGE_SIZE, } from '../../realtime/comments.js';
/**
 * Minimal in-memory fake that implements just enough Prisma surface to drive
 * the comments service. We do NOT try to be a complete Prisma re-implementation
 * — the real client is exercised by integration tests against the migration.
 */
function createFakeDB() {
    const rows = new Map();
    let counter = 0;
    const fake = {
        rows,
        documentComment: {
            async findUnique({ where }) {
                return rows.get(where.id) ?? null;
            },
            async findMany({ where, orderBy, take }) {
                const w = where;
                let result = Array.from(rows.values()).filter((r) => {
                    if (w.documentId !== undefined && r.documentId !== w.documentId)
                        return false;
                    if (Object.prototype.hasOwnProperty.call(w, 'resolvedAt') && r.resolvedAt !== null)
                        return false;
                    if (Object.prototype.hasOwnProperty.call(w, 'deletedAt') && r.deletedAt !== null)
                        return false;
                    return true;
                });
                if (orderBy?.createdAt === 'asc') {
                    result = result.sort((a, b) => {
                        const ax = a.createdAt instanceof Date ? a.createdAt.getTime() : Date.parse(String(a.createdAt));
                        const bx = b.createdAt instanceof Date ? b.createdAt.getTime() : Date.parse(String(b.createdAt));
                        return ax - bx;
                    });
                }
                if (typeof take === 'number')
                    result = result.slice(0, take);
                return result;
            },
            async create({ data }) {
                counter += 1;
                const now = new Date(Date.UTC(2026, 0, 1, 0, 0, counter));
                const row = {
                    id: `c-${counter}`,
                    documentId: data.documentId,
                    parentId: data.parentId,
                    userId: data.userId,
                    body: data.body,
                    anchor: data.anchor,
                    resolvedAt: null,
                    resolvedById: null,
                    deletedAt: null,
                    createdAt: now,
                    updatedAt: now,
                };
                rows.set(row.id, row);
                return row;
            },
            async update({ where, data }) {
                const existing = rows.get(where.id);
                if (!existing)
                    throw new Error(`row ${where.id} not found`);
                const next = {
                    ...existing,
                    ...data,
                    updatedAt: new Date(),
                };
                rows.set(where.id, next);
                return next;
            },
        },
    };
    return fake;
}
const actor = { userId: 'user-1', isAdmin: false, canResolve: true };
const otherActor = { userId: 'user-2', isAdmin: false, canResolve: false };
describe('comments: input validation helpers', () => {
    describe('normaliseBody', () => {
        it('rejects non-strings', () => {
            const r = normaliseBody(123);
            expect(r.ok).toBe(false);
            if (!r.ok)
                expect(r.error.code).toBe('validation');
        });
        it('rejects empty / whitespace-only', () => {
            expect(normaliseBody('').ok).toBe(false);
            expect(normaliseBody('   \t  ').ok).toBe(false);
        });
        it('trims surrounding whitespace', () => {
            const r = normaliseBody('  hi  ');
            expect(r.ok).toBe(true);
            if (r.ok)
                expect(r.value).toBe('hi');
        });
        it('enforces the length cap', () => {
            const big = 'x'.repeat(MAX_COMMENT_BODY_LENGTH + 1);
            const r = normaliseBody(big);
            expect(r.ok).toBe(false);
        });
        it('accepts a body at the exact cap', () => {
            const exact = 'x'.repeat(MAX_COMMENT_BODY_LENGTH);
            const r = normaliseBody(exact);
            expect(r.ok).toBe(true);
        });
    });
    describe('parseAnchor', () => {
        it('returns null for missing input', () => {
            const r = parseAnchor(undefined);
            expect(r.ok).toBe(true);
            if (r.ok)
                expect(r.value).toBeNull();
        });
        it('returns null for explicit null', () => {
            const r = parseAnchor(null);
            expect(r.ok).toBe(true);
            if (r.ok)
                expect(r.value).toBeNull();
        });
        it('rejects arrays / primitives', () => {
            expect(parseAnchor('nope').ok).toBe(false);
            expect(parseAnchor([1, 2]).ok).toBe(false);
            expect(parseAnchor(42).ok).toBe(false);
        });
        it('rejects missing fields', () => {
            expect(parseAnchor({ from: 'a' }).ok).toBe(false);
            expect(parseAnchor({ to: 'b' }).ok).toBe(false);
        });
        it('rejects empty fields', () => {
            expect(parseAnchor({ from: '', to: 'b' }).ok).toBe(false);
            expect(parseAnchor({ from: 'a', to: '' }).ok).toBe(false);
        });
        it('rejects overlong fields', () => {
            const big = 'x'.repeat(5_000);
            expect(parseAnchor({ from: big, to: 'b' }).ok).toBe(false);
            expect(parseAnchor({ from: 'a', to: big }).ok).toBe(false);
        });
        it('accepts a valid anchor', () => {
            const r = parseAnchor({ from: 'AQ==', to: 'AQ==' });
            expect(r.ok).toBe(true);
            if (r.ok)
                expect(r.value).toEqual({ from: 'AQ==', to: 'AQ==' });
        });
    });
});
describe('toCommentDTO', () => {
    it('normalises Date fields to ISO strings and derives isResolved', () => {
        const row = {
            id: 'c-1',
            documentId: 'd-1',
            parentId: null,
            userId: 'u-1',
            body: 'hi',
            anchor: null,
            resolvedAt: new Date('2026-01-02T03:04:05.000Z'),
            resolvedById: 'u-2',
            deletedAt: null,
            createdAt: new Date('2026-01-01T00:00:00.000Z'),
            updatedAt: new Date('2026-01-01T00:00:01.000Z'),
        };
        const dto = toCommentDTO(row);
        expect(dto.createdAt).toBe('2026-01-01T00:00:00.000Z');
        expect(dto.resolvedAt).toBe('2026-01-02T03:04:05.000Z');
        expect(dto.deletedAt).toBeNull();
        expect(dto.isResolved).toBe(true);
    });
    it('passes through ISO strings unchanged', () => {
        const row = {
            id: 'c-2',
            documentId: 'd-1',
            parentId: null,
            userId: 'u-1',
            body: 'hi',
            anchor: { from: 'AQ==', to: 'Ag==' },
            resolvedAt: null,
            resolvedById: null,
            deletedAt: null,
            createdAt: '2026-01-01T00:00:00.000Z',
            updatedAt: '2026-01-01T00:00:00.000Z',
        };
        const dto = toCommentDTO(row);
        expect(dto.createdAt).toBe('2026-01-01T00:00:00.000Z');
        expect(dto.isResolved).toBe(false);
        expect(dto.anchor).toEqual({ from: 'AQ==', to: 'Ag==' });
    });
});
describe('createComment', () => {
    let db;
    beforeEach(() => {
        db = createFakeDB();
    });
    it('creates a top-level comment with an anchor', async () => {
        const r = await createComment(db, {
            documentId: 'd-1',
            userId: 'u-1',
            body: 'first',
            anchor: { from: 'AQ==', to: 'Ag==' },
        });
        expect(r.ok).toBe(true);
        if (!r.ok)
            return;
        expect(r.value.parentId).toBeNull();
        expect(r.value.anchor).toEqual({ from: 'AQ==', to: 'Ag==' });
        expect(db.rows.size).toBe(1);
    });
    it('allows document-level (no-anchor) comments', async () => {
        const r = await createComment(db, { documentId: 'd-1', userId: 'u-1', body: 'general' });
        expect(r.ok).toBe(true);
        if (r.ok)
            expect(r.value.anchor).toBeNull();
    });
    it('rejects an empty body', async () => {
        const r = await createComment(db, { documentId: 'd-1', userId: 'u-1', body: '   ' });
        expect(r.ok).toBe(false);
        if (!r.ok)
            expect(r.error.code).toBe('validation');
    });
    it('rejects a body over the cap', async () => {
        const r = await createComment(db, {
            documentId: 'd-1',
            userId: 'u-1',
            body: 'x'.repeat(MAX_COMMENT_BODY_LENGTH + 1),
        });
        expect(r.ok).toBe(false);
    });
    it('rejects a malformed anchor', async () => {
        const r = await createComment(db, {
            documentId: 'd-1',
            userId: 'u-1',
            body: 'hello',
            anchor: { from: 123, to: 'AQ==' },
        });
        expect(r.ok).toBe(false);
        if (!r.ok)
            expect(r.error.code).toBe('validation');
    });
    it('threads a reply onto a top-level comment', async () => {
        const root = await createComment(db, { documentId: 'd-1', userId: 'u-1', body: 'root' });
        expect(root.ok).toBe(true);
        if (!root.ok)
            return;
        const reply = await createComment(db, {
            documentId: 'd-1',
            userId: 'u-2',
            body: 'reply',
            parentId: root.value.id,
        });
        expect(reply.ok).toBe(true);
        if (!reply.ok)
            return;
        expect(reply.value.parentId).toBe(root.value.id);
    });
    it('rejects two-level threading', async () => {
        const root = await createComment(db, { documentId: 'd-1', userId: 'u-1', body: 'root' });
        if (!root.ok)
            throw new Error('setup failed');
        const reply = await createComment(db, {
            documentId: 'd-1',
            userId: 'u-2',
            body: 'reply',
            parentId: root.value.id,
        });
        if (!reply.ok)
            throw new Error('setup failed');
        const nested = await createComment(db, {
            documentId: 'd-1',
            userId: 'u-3',
            body: 'nested',
            parentId: reply.value.id,
        });
        expect(nested.ok).toBe(false);
        if (!nested.ok)
            expect(nested.error.code).toBe('validation');
    });
    it('rejects replying across documents', async () => {
        const root = await createComment(db, { documentId: 'd-1', userId: 'u-1', body: 'root' });
        if (!root.ok)
            throw new Error('setup failed');
        const cross = await createComment(db, {
            documentId: 'd-2',
            userId: 'u-2',
            body: 'reply',
            parentId: root.value.id,
        });
        expect(cross.ok).toBe(false);
        if (!cross.ok)
            expect(cross.error.code).toBe('validation');
    });
    it('returns not_found when parent does not exist', async () => {
        const r = await createComment(db, {
            documentId: 'd-1',
            userId: 'u-1',
            body: 'orphan',
            parentId: 'c-nope',
        });
        expect(r.ok).toBe(false);
        if (!r.ok)
            expect(r.error.code).toBe('not_found');
    });
    it('rejects replying to a soft-deleted parent', async () => {
        const root = await createComment(db, { documentId: 'd-1', userId: 'u-1', body: 'root' });
        if (!root.ok)
            throw new Error('setup failed');
        await deleteComment(db, root.value.id, { userId: 'u-1', isAdmin: false });
        const r = await createComment(db, {
            documentId: 'd-1',
            userId: 'u-2',
            body: 'reply',
            parentId: root.value.id,
        });
        expect(r.ok).toBe(false);
        if (!r.ok)
            expect(r.error.code).toBe('not_found');
    });
    it('rejects missing documentId / userId', async () => {
        expect((await createComment(db, { documentId: '', userId: 'u-1', body: 'hi' })).ok).toBe(false);
        expect((await createComment(db, { documentId: 'd-1', userId: '', body: 'hi' })).ok).toBe(false);
    });
});
describe('listComments', () => {
    let db;
    beforeEach(async () => {
        db = createFakeDB();
        await createComment(db, { documentId: 'd-1', userId: 'u-1', body: 'one' });
        await createComment(db, { documentId: 'd-1', userId: 'u-1', body: 'two' });
        const resolved = await createComment(db, { documentId: 'd-1', userId: 'u-1', body: 'three' });
        if (resolved.ok) {
            await resolveComment(db, resolved.value.id, { userId: 'u-1', canResolve: true });
        }
        const deleted = await createComment(db, { documentId: 'd-1', userId: 'u-1', body: 'four' });
        if (deleted.ok) {
            await deleteComment(db, deleted.value.id, { userId: 'u-1', isAdmin: false });
        }
        await createComment(db, { documentId: 'd-2', userId: 'u-1', body: 'other doc' });
    });
    it('returns only active comments for the requested document', async () => {
        const r = await listComments(db, { documentId: 'd-1' });
        expect(r.ok).toBe(true);
        if (!r.ok)
            return;
        expect(r.value.map((c) => c.body)).toEqual(['one', 'two']);
    });
    it('includes resolved threads when asked', async () => {
        const r = await listComments(db, { documentId: 'd-1', includeResolved: true });
        expect(r.ok).toBe(true);
        if (!r.ok)
            return;
        expect(r.value.map((c) => c.body)).toEqual(['one', 'two', 'three']);
    });
    it('includes deleted threads when asked', async () => {
        const r = await listComments(db, { documentId: 'd-1', includeDeleted: true });
        expect(r.ok).toBe(true);
        if (!r.ok)
            return;
        expect(r.value.map((c) => c.body)).toEqual(['one', 'two', 'four']);
    });
    it('clamps pageSize to the upper limit', async () => {
        const r = await listComments(db, {
            documentId: 'd-1',
            pageSize: 9999,
            includeResolved: true,
            includeDeleted: true,
        });
        expect(r.ok).toBe(true);
        if (!r.ok)
            return;
        expect(r.value.length).toBeLessThanOrEqual(MAX_COMMENT_LIST_PAGE_SIZE);
    });
    it('clamps pageSize to >= 1', async () => {
        const r = await listComments(db, { documentId: 'd-1', pageSize: 0 });
        expect(r.ok).toBe(true);
        if (r.ok)
            expect(r.value.length).toBeLessThanOrEqual(1);
    });
    it('rejects missing documentId', async () => {
        const r = await listComments(db, { documentId: '' });
        expect(r.ok).toBe(false);
    });
});
describe('updateComment', () => {
    let db;
    let commentId;
    beforeEach(async () => {
        db = createFakeDB();
        const r = await createComment(db, { documentId: 'd-1', userId: actor.userId, body: 'before' });
        if (!r.ok)
            throw new Error('setup failed');
        commentId = r.value.id;
    });
    it('lets the author edit their own comment', async () => {
        const r = await updateComment(db, commentId, { body: 'after' }, actor);
        expect(r.ok).toBe(true);
        if (r.ok)
            expect(r.value.body).toBe('after');
    });
    it('rejects edits from other users', async () => {
        const r = await updateComment(db, commentId, { body: 'evil' }, otherActor);
        expect(r.ok).toBe(false);
        if (!r.ok)
            expect(r.error.code).toBe('forbidden');
    });
    it('lets admins edit any comment', async () => {
        const r = await updateComment(db, commentId, { body: 'admin override' }, {
            userId: 'admin-1',
            isAdmin: true,
        });
        expect(r.ok).toBe(true);
    });
    it('returns not_found for soft-deleted comments', async () => {
        await deleteComment(db, commentId, actor);
        const r = await updateComment(db, commentId, { body: 'ghost' }, actor);
        expect(r.ok).toBe(false);
        if (!r.ok)
            expect(r.error.code).toBe('not_found');
    });
    it('rejects empty bodies', async () => {
        const r = await updateComment(db, commentId, { body: '   ' }, actor);
        expect(r.ok).toBe(false);
    });
});
describe('resolveComment / reopenComment', () => {
    let db;
    let commentId;
    beforeEach(async () => {
        db = createFakeDB();
        const r = await createComment(db, { documentId: 'd-1', userId: actor.userId, body: 'open' });
        if (!r.ok)
            throw new Error('setup failed');
        commentId = r.value.id;
    });
    it('resolves an open thread and stamps resolvedById', async () => {
        const r = await resolveComment(db, commentId, { userId: 'editor-1', canResolve: true });
        expect(r.ok).toBe(true);
        if (!r.ok)
            return;
        expect(r.value.isResolved).toBe(true);
        expect(r.value.resolvedById).toBe('editor-1');
    });
    it('is a no-op when already resolved', async () => {
        await resolveComment(db, commentId, { userId: 'editor-1', canResolve: true });
        const r = await resolveComment(db, commentId, { userId: 'editor-2', canResolve: true });
        expect(r.ok).toBe(true);
        if (r.ok)
            expect(r.value.resolvedById).toBe('editor-1');
    });
    it('rejects resolve without permission and non-author', async () => {
        const r = await resolveComment(db, commentId, { userId: 'random', canResolve: false });
        expect(r.ok).toBe(false);
        if (!r.ok)
            expect(r.error.code).toBe('forbidden');
    });
    it('lets the author resolve their own thread even without write role', async () => {
        const r = await resolveComment(db, commentId, { userId: actor.userId, canResolve: false });
        expect(r.ok).toBe(true);
    });
    it('reopens a resolved thread and clears resolvedById', async () => {
        await resolveComment(db, commentId, { userId: 'editor-1', canResolve: true });
        const r = await reopenComment(db, commentId, { userId: 'editor-1', canResolve: true });
        expect(r.ok).toBe(true);
        if (!r.ok)
            return;
        expect(r.value.isResolved).toBe(false);
        expect(r.value.resolvedById).toBeNull();
    });
    it('reopen is a no-op when already open', async () => {
        const r = await reopenComment(db, commentId, { userId: actor.userId, canResolve: true });
        expect(r.ok).toBe(true);
        if (r.ok)
            expect(r.value.isResolved).toBe(false);
    });
});
describe('deleteComment', () => {
    let db;
    let commentId;
    beforeEach(async () => {
        db = createFakeDB();
        const r = await createComment(db, { documentId: 'd-1', userId: actor.userId, body: 'd' });
        if (!r.ok)
            throw new Error('setup failed');
        commentId = r.value.id;
    });
    it('lets the author delete', async () => {
        const r = await deleteComment(db, commentId, actor);
        expect(r.ok).toBe(true);
        if (r.ok)
            expect(r.value.deletedAt).not.toBeNull();
    });
    it('lets admins delete anyone', async () => {
        const r = await deleteComment(db, commentId, { userId: 'admin-1', isAdmin: true });
        expect(r.ok).toBe(true);
    });
    it('rejects delete from a different non-admin user', async () => {
        const r = await deleteComment(db, commentId, otherActor);
        expect(r.ok).toBe(false);
        if (!r.ok)
            expect(r.error.code).toBe('forbidden');
    });
    it('is a no-op when already deleted', async () => {
        await deleteComment(db, commentId, actor);
        const r = await deleteComment(db, commentId, actor);
        expect(r.ok).toBe(true);
    });
    it('returns not_found for unknown ids', async () => {
        const r = await deleteComment(db, 'c-missing', actor);
        expect(r.ok).toBe(false);
        if (!r.ok)
            expect(r.error.code).toBe('not_found');
    });
});
//# sourceMappingURL=comments.test.js.map