import { describe, it, expect, vi, beforeEach } from 'vitest';
const cmsApiMock = vi.fn();
vi.mock('../../lib/api.js', () => ({
    cmsApi: (...args) => cmsApiMock(...args),
}));
const { createComment, deleteComment, groupCommentsIntoThreads, listComments, reopenComment, resolveComment, updateComment, } = await import('../../lib/comments-client.js');
function commentRow(overrides = {}) {
    return {
        id: 'c-1',
        documentId: 'doc-1',
        parentId: null,
        userId: 'user-1',
        body: 'hi',
        anchor: null,
        resolvedAt: null,
        resolvedById: null,
        deletedAt: null,
        createdAt: '2026-01-01T00:00:00.000Z',
        updatedAt: '2026-01-01T00:00:00.000Z',
        isResolved: false,
        ...overrides,
    };
}
beforeEach(() => {
    cmsApiMock.mockReset();
});
describe('createComment', () => {
    it('posts to the document scoped endpoint and unwraps the envelope', async () => {
        const row = commentRow({ id: 'c-99', body: 'fresh' });
        cmsApiMock.mockResolvedValueOnce({ data: row, status: 201 });
        const out = await createComment({ documentId: 'doc-1', body: '  fresh  ' });
        expect(out.ok).toBe(true);
        if (!out.ok)
            return;
        expect(out.result.id).toBe('c-99');
        expect(cmsApiMock).toHaveBeenCalledWith('/documents/doc-1/comments', {
            method: 'POST',
            body: JSON.stringify({ body: 'fresh' }),
        });
    });
    it('forwards anchor + parentId when supplied', async () => {
        cmsApiMock.mockResolvedValueOnce({ data: commentRow(), status: 201 });
        await createComment({
            documentId: 'doc-1',
            body: 'reply',
            anchor: { from: 'AQ==', to: 'Ag==' },
            parentId: 'c-1',
        });
        const [, init] = cmsApiMock.mock.calls[0];
        expect(JSON.parse(init.body)).toEqual({
            body: 'reply',
            anchor: { from: 'AQ==', to: 'Ag==' },
            parentId: 'c-1',
        });
    });
    it('rejects empty bodies before hitting the wire', async () => {
        const out = await createComment({ documentId: 'doc-1', body: '   ' });
        expect(out.ok).toBe(false);
        expect(cmsApiMock).not.toHaveBeenCalled();
    });
    it('rejects missing documentId before hitting the wire', async () => {
        const out = await createComment({ documentId: '', body: 'hi' });
        expect(out.ok).toBe(false);
        expect(cmsApiMock).not.toHaveBeenCalled();
    });
    it('URL-encodes documentIds with awkward characters', async () => {
        cmsApiMock.mockResolvedValueOnce({ data: commentRow(), status: 201 });
        await createComment({ documentId: 'doc with space/?', body: 'hi' });
        expect(cmsApiMock).toHaveBeenCalledWith(`/documents/${encodeURIComponent('doc with space/?')}/comments`, expect.anything());
    });
    it('propagates server errors as { ok: false }', async () => {
        cmsApiMock.mockResolvedValueOnce({ error: 'CSRF rejected', status: 403 });
        const out = await createComment({ documentId: 'doc-1', body: 'hi' });
        expect(out.ok).toBe(false);
        if (out.ok)
            return;
        expect(out.status).toBe(403);
        expect(out.error).toBe('CSRF rejected');
    });
    it('falls back to a generic message when the server omits one', async () => {
        cmsApiMock.mockResolvedValueOnce({ status: 500 });
        const out = await createComment({ documentId: 'doc-1', body: 'hi' });
        expect(out.ok).toBe(false);
        if (out.ok)
            return;
        expect(out.error).toContain('500');
    });
});
describe('listComments', () => {
    it('GETs the document endpoint with no query when no filters are set', async () => {
        cmsApiMock.mockResolvedValueOnce({ data: [commentRow()], status: 200 });
        const out = await listComments('doc-1');
        expect(out.ok).toBe(true);
        expect(cmsApiMock).toHaveBeenCalledWith('/documents/doc-1/comments', { method: 'GET' });
    });
    it('appends ?includeResolved + ?includeDeleted + ?pageSize when set', async () => {
        cmsApiMock.mockResolvedValueOnce({ data: [], status: 200 });
        await listComments('doc-1', { includeResolved: true, includeDeleted: true, pageSize: 25 });
        expect(cmsApiMock).toHaveBeenCalledWith('/documents/doc-1/comments?includeResolved=true&includeDeleted=true&pageSize=25', { method: 'GET' });
    });
    it('rejects empty documentId before hitting the wire', async () => {
        const out = await listComments('');
        expect(out.ok).toBe(false);
        expect(cmsApiMock).not.toHaveBeenCalled();
    });
    it('returns the data array unchanged', async () => {
        const rows = [commentRow({ id: 'c-1' }), commentRow({ id: 'c-2' })];
        cmsApiMock.mockResolvedValueOnce({ data: rows, status: 200 });
        const out = await listComments('doc-1');
        expect(out.ok).toBe(true);
        if (!out.ok)
            return;
        expect(out.result.map((c) => c.id)).toEqual(['c-1', 'c-2']);
    });
});
describe('updateComment / resolveComment / reopenComment / deleteComment', () => {
    it('PATCHes the comment endpoint with the trimmed body', async () => {
        cmsApiMock.mockResolvedValueOnce({ data: commentRow({ body: 'next' }), status: 200 });
        await updateComment('c-1', '  next  ');
        expect(cmsApiMock).toHaveBeenCalledWith('/comments/c-1', {
            method: 'PATCH',
            body: JSON.stringify({ body: 'next' }),
        });
    });
    it('rejects empty bodies on update', async () => {
        const out = await updateComment('c-1', '   ');
        expect(out.ok).toBe(false);
        expect(cmsApiMock).not.toHaveBeenCalled();
    });
    it('POSTs to /resolve and /reopen with an empty body', async () => {
        cmsApiMock
            .mockResolvedValueOnce({
            data: commentRow({ resolvedAt: 'now', isResolved: true }),
            status: 200,
        })
            .mockResolvedValueOnce({ data: commentRow(), status: 200 });
        await resolveComment('c-1');
        await reopenComment('c-1');
        expect(cmsApiMock).toHaveBeenNthCalledWith(1, '/comments/c-1/resolve', {
            method: 'POST',
            body: '{}',
        });
        expect(cmsApiMock).toHaveBeenNthCalledWith(2, '/comments/c-1/reopen', {
            method: 'POST',
            body: '{}',
        });
    });
    it('DELETEs the comment endpoint and returns the soft-deleted DTO', async () => {
        cmsApiMock.mockResolvedValueOnce({
            data: commentRow({ deletedAt: '2026-01-02T00:00:00.000Z' }),
            status: 200,
        });
        const out = await deleteComment('c-1');
        expect(out.ok).toBe(true);
        if (!out.ok)
            return;
        expect(out.result.deletedAt).toBe('2026-01-02T00:00:00.000Z');
        expect(cmsApiMock).toHaveBeenCalledWith('/comments/c-1', { method: 'DELETE' });
    });
    it('rejects missing id without hitting the wire', async () => {
        expect((await updateComment('', 'x')).ok).toBe(false);
        expect((await resolveComment('')).ok).toBe(false);
        expect((await reopenComment('')).ok).toBe(false);
        expect((await deleteComment('')).ok).toBe(false);
        expect(cmsApiMock).not.toHaveBeenCalled();
    });
});
describe('groupCommentsIntoThreads', () => {
    it('returns roots in chronological order with their replies attached', () => {
        const root1 = commentRow({
            id: 'r-1',
            createdAt: '2026-01-01T00:00:00.000Z',
        });
        const root2 = commentRow({
            id: 'r-2',
            createdAt: '2026-01-01T00:00:02.000Z',
        });
        const reply1 = commentRow({
            id: 'reply-1',
            parentId: 'r-1',
            createdAt: '2026-01-01T00:00:01.000Z',
        });
        const reply2 = commentRow({
            id: 'reply-2',
            parentId: 'r-1',
            createdAt: '2026-01-01T00:00:03.000Z',
        });
        const threads = groupCommentsIntoThreads([reply2, root1, root2, reply1]);
        expect(threads.map((t) => t.root.id)).toEqual(['r-1', 'r-2']);
        expect(threads[0].replies.map((r) => r.id)).toEqual(['reply-1', 'reply-2']);
        expect(threads[1].replies).toEqual([]);
    });
    it('drops replies whose root is missing from the input', () => {
        const orphan = commentRow({ id: 'orphan', parentId: 'missing-root' });
        const threads = groupCommentsIntoThreads([orphan]);
        expect(threads).toEqual([]);
    });
    it('handles an empty input', () => {
        expect(groupCommentsIntoThreads([])).toEqual([]);
    });
});
//# sourceMappingURL=comments-client.test.js.map