import { describe, expect, it } from 'vitest';
import { collectRelationRefs, parsePopulateParam, populateRelations, validateRelations, } from '../../fields/relations.js';
// ─── Test fixtures ────────────────────────────────────────────────────────
function buildLookup(docs) {
    return {
        async fetchByIds(collections, ids) {
            const result = new Map();
            for (const id of ids) {
                const doc = docs[id];
                if (doc && collections.includes(doc.collection)) {
                    result.set(id, doc);
                }
            }
            return result;
        },
    };
}
const blogFields = {
    title: { type: 'text', label: 'Title' },
    author: { type: 'relationship', label: 'Author', relationTo: 'team' },
    relatedPosts: {
        type: 'relationship',
        label: 'Related posts',
        relationTo: 'posts',
        hasMany: true,
    },
};
const arrayFields = {
    title: { type: 'text', label: 'Title' },
    sections: {
        type: 'array',
        label: 'Sections',
        fields: {
            heading: { type: 'text', label: 'Heading' },
            service: { type: 'relationship', label: 'Service', relationTo: 'services' },
        },
    },
};
// ─── collectRelationRefs ─────────────────────────────────────────────────
describe('collectRelationRefs', () => {
    it('extracts top-level single + many relation ids', () => {
        const { byCollection, refsByPath } = collectRelationRefs(blogFields, {
            title: 'Hello',
            author: 'team-1',
            relatedPosts: ['post-1', 'post-2'],
        });
        expect(byCollection.get('team')).toEqual(new Set(['team-1']));
        expect(byCollection.get('posts')).toEqual(new Set(['post-1', 'post-2']));
        expect(refsByPath).toHaveLength(2);
        expect(refsByPath[0]?.path).toBe('author');
        expect(refsByPath[1]?.path).toBe('relatedPosts');
    });
    it('recurses into array fields', () => {
        const { byCollection, refsByPath } = collectRelationRefs(arrayFields, {
            title: 'Page',
            sections: [
                { heading: 'A', service: 'svc-1' },
                { heading: 'B', service: 'svc-2' },
            ],
        });
        expect(byCollection.get('services')).toEqual(new Set(['svc-1', 'svc-2']));
        expect(refsByPath.map((r) => r.path)).toEqual(['sections[0].service', 'sections[1].service']);
    });
    it('ignores empty / null / non-string values', () => {
        const { byCollection } = collectRelationRefs(blogFields, {
            title: 'X',
            author: '',
            relatedPosts: [null, 'post-1', '', undefined, 123],
        });
        expect(byCollection.get('team')).toBeUndefined();
        expect(byCollection.get('posts')).toEqual(new Set(['post-1']));
    });
    it('handles multi-target relationTo arrays', () => {
        const fields = {
            featured: {
                type: 'relationship',
                label: 'Featured',
                relationTo: ['posts', 'case-studies'],
            },
        };
        const { byCollection } = collectRelationRefs(fields, { featured: 'doc-1' });
        expect(byCollection.get('posts')).toEqual(new Set(['doc-1']));
        expect(byCollection.get('case-studies')).toEqual(new Set(['doc-1']));
    });
});
// ─── validateRelations ───────────────────────────────────────────────────
describe('validateRelations', () => {
    it('passes when every id resolves to a live document', async () => {
        const lookup = buildLookup({
            'team-1': { id: 'team-1', collection: 'team', title: 'Jane' },
            'post-1': { id: 'post-1', collection: 'posts', title: 'A' },
        });
        await expect(validateRelations(blogFields, { title: 'X', author: 'team-1', relatedPosts: ['post-1'] }, lookup)).resolves.toBeUndefined();
    });
    it('throws when a single-ref id is missing, naming the path + collection', async () => {
        const lookup = buildLookup({});
        await expect(validateRelations(blogFields, { title: 'X', author: 'ghost' }, lookup)).rejects.toThrow(/Relation field "author" references document "ghost" which does not exist in collection "team"/);
    });
    it('throws when ANY id in a hasMany array is missing', async () => {
        const lookup = buildLookup({
            'post-1': { id: 'post-1', collection: 'posts' },
            // post-2 missing
        });
        await expect(validateRelations(blogFields, { relatedPosts: ['post-1', 'post-2'] }, lookup)).rejects.toThrow(/post-2.*posts/);
    });
    it('treats null / empty values as "not set" (no validation needed)', async () => {
        const lookup = buildLookup({});
        await expect(validateRelations(blogFields, { title: 'X', author: '', relatedPosts: [] }, lookup)).resolves.toBeUndefined();
    });
    it('accepts a doc when it lives in ANY of the multi-target collections', async () => {
        const fields = {
            featured: {
                type: 'relationship',
                label: 'Featured',
                relationTo: ['posts', 'case-studies'],
            },
        };
        const lookup = buildLookup({
            'cs-1': { id: 'cs-1', collection: 'case-studies', title: 'Big Co' },
        });
        await expect(validateRelations(fields, { featured: 'cs-1' }, lookup)).resolves.toBeUndefined();
    });
    it('validates relations nested inside array fields', async () => {
        const lookup = buildLookup({
            'svc-1': { id: 'svc-1', collection: 'services' },
        });
        await expect(validateRelations(arrayFields, {
            sections: [
                { heading: 'A', service: 'svc-1' },
                { heading: 'B', service: 'missing' },
            ],
        }, lookup)).rejects.toThrow(/sections\[1\]\.service.*missing.*services/);
    });
    it('batches into a single fetchByIds call across every target collection', async () => {
        // Regression: the previous implementation issued one call per
        // collection (N round-trips). Cross-region Function -> Postgres traffic
        // makes each round-trip ~80 ms, so we now collapse every relation
        // collection into a single batched call.
        let callCount = 0;
        const calls = [];
        const lookup = {
            async fetchByIds(cols, ids) {
                callCount++;
                calls.push({ collections: cols.slice(), ids: ids.slice() });
                const m = new Map();
                // The dispatch layer asks for both collections in one call; the
                // implementation has to return summaries with the right
                // `collection` tag so the caller can bucket. We mimic that by
                // alternating the collection per id deterministically.
                for (const id of ids) {
                    if (id.startsWith('team'))
                        m.set(id, { id, collection: 'team' });
                    else if (id.startsWith('post'))
                        m.set(id, { id, collection: 'posts' });
                }
                return m;
            },
        };
        await validateRelations(blogFields, { author: 'team-1', relatedPosts: ['post-1', 'post-2', 'post-3'] }, lookup);
        expect(callCount).toBe(1);
        expect(new Set(calls[0].collections)).toEqual(new Set(['team', 'posts']));
        expect(new Set(calls[0].ids)).toEqual(new Set(['team-1', 'post-1', 'post-2', 'post-3']));
    });
    it('deduplicates an id that appears under multiple target collections', async () => {
        // Polymorphic relationTo: a `featured` field can point at posts OR
        // case-studies, so the same id is registered under both targets. We
        // should still issue one IN-list element per unique id, not two.
        const fields = {
            featured: {
                type: 'relationship',
                label: 'Featured',
                relationTo: ['posts', 'case-studies'],
            },
        };
        let idsSeen = [];
        const lookup = {
            async fetchByIds(_cols, ids) {
                idsSeen = ids.slice();
                return new Map([['cs-1', { id: 'cs-1', collection: 'case-studies' }]]);
            },
        };
        await validateRelations(fields, { featured: 'cs-1' }, lookup);
        expect(idsSeen.filter((id) => id === 'cs-1')).toHaveLength(1);
    });
});
// ─── populateRelations ───────────────────────────────────────────────────
describe('populateRelations', () => {
    const lookup = buildLookup({
        'team-1': {
            id: 'team-1',
            collection: 'team',
            title: 'Jane',
            slug: 'jane',
            status: 'PUBLISHED',
        },
        'post-1': {
            id: 'post-1',
            collection: 'posts',
            title: 'Post A',
            slug: 'a',
            status: 'PUBLISHED',
        },
        'post-2': { id: 'post-2', collection: 'posts', title: 'Post B', slug: 'b', status: 'DRAFT' },
    });
    it('expands a single-ref id into a summary object', async () => {
        const populated = await populateRelations(blogFields, { title: 'X', author: 'team-1' }, lookup, { paths: '*' });
        expect(populated.title).toBe('X');
        expect(populated.author.id).toBe('team-1');
        expect(populated.author.title).toBe('Jane');
        expect(populated.author.collection).toBe('team');
    });
    it('expands a hasMany id array into summary objects in order', async () => {
        const populated = await populateRelations(blogFields, { title: 'X', relatedPosts: ['post-1', 'post-2'] }, lookup, { paths: '*' });
        const list = populated.relatedPosts;
        expect(list).toHaveLength(2);
        expect(list[0].title).toBe('Post A');
        expect(list[1].title).toBe('Post B');
    });
    it('leaves the original document untouched (returns a clone)', async () => {
        const input = { title: 'X', author: 'team-1' };
        const populated = await populateRelations(blogFields, input, lookup, { paths: '*' });
        expect(input.author).toBe('team-1'); // original id preserved
        expect(populated.author).not.toBe(input.author);
    });
    it('returns input verbatim when populate paths is empty / not provided', async () => {
        const input = { title: 'X', author: 'team-1' };
        const populated = await populateRelations(blogFields, input, lookup, {});
        expect(populated).toEqual(input);
    });
    it('only populates the requested top-level paths', async () => {
        const populated = await populateRelations(blogFields, { title: 'X', author: 'team-1', relatedPosts: ['post-1'] }, lookup, { paths: ['author'] });
        expect(populated.author.title).toBe('Jane');
        // relatedPosts not in whitelist → left as raw ids
        expect(populated.relatedPosts).toEqual(['post-1']);
    });
    it('falls back to the raw id when a referenced doc cannot be found', async () => {
        const populated = await populateRelations(blogFields, { title: 'X', author: 'ghost' }, lookup, {
            paths: '*',
        });
        expect(populated.author).toBe('ghost');
    });
    it('populates relations nested in array fields', async () => {
        const populated = await populateRelations(arrayFields, { title: 'P', sections: [{ heading: 'A', service: 'svc-1' }] }, buildLookup({
            'svc-1': { id: 'svc-1', collection: 'services', title: 'Cleaning' },
        }), { paths: '*' });
        const sec = populated.sections[0];
        expect(sec.service.title).toBe('Cleaning');
    });
    it('issues a single fetchByIds call across every populated collection', async () => {
        // Companion to the validateRelations regression: populate now batches
        // every collection into one round-trip.
        let callCount = 0;
        const lookup = {
            async fetchByIds(_cols, ids) {
                callCount++;
                const m = new Map();
                for (const id of ids) {
                    if (id.startsWith('team'))
                        m.set(id, { id, collection: 'team', title: 'T' });
                    else if (id.startsWith('post'))
                        m.set(id, { id, collection: 'posts', title: 'P' });
                }
                return m;
            },
        };
        await populateRelations(blogFields, { author: 'team-1', relatedPosts: ['post-1', 'post-2'] }, lookup, { paths: '*' });
        expect(callCount).toBe(1);
    });
});
// ─── parsePopulateParam ──────────────────────────────────────────────────
describe('parsePopulateParam', () => {
    it('returns null for missing / falsy values', () => {
        expect(parsePopulateParam(null)).toBeNull();
        expect(parsePopulateParam(undefined)).toBeNull();
        expect(parsePopulateParam('')).toBeNull();
        expect(parsePopulateParam('false')).toBeNull();
        expect(parsePopulateParam('0')).toBeNull();
    });
    it('treats * and true as "expand all"', () => {
        expect(parsePopulateParam('*')).toEqual({ paths: '*' });
        expect(parsePopulateParam('true')).toEqual({ paths: '*' });
    });
    it('parses a comma-separated whitelist', () => {
        expect(parsePopulateParam('author,relatedPosts')).toEqual({
            paths: ['author', 'relatedPosts'],
        });
    });
    it('promotes a list that contains * to expand-all', () => {
        expect(parsePopulateParam('author,*,relatedPosts')).toEqual({ paths: '*' });
    });
    it('trims whitespace and drops empty entries', () => {
        expect(parsePopulateParam('  author , , relatedPosts ')).toEqual({
            paths: ['author', 'relatedPosts'],
        });
    });
});
//# sourceMappingURL=relations.test.js.map