/**
 * Tests for components/library_search.js
 *
 * Tests the LibrarySearch utilities:
 * fileTypeIcon, initLibrarySearch, getIndexedCollectionIds,
 * getDefaultCollectionId, searchAllCollections (deduplication logic),
 * renderSemanticResults.
 */

// Stub URLBuilder before loading the module
window.URLBuilder = {
    build: (template, id) => template.replace('{id}', id),
    documentPage: (id) => `/library/document/${id}`,
};
window.URLS = {
    LIBRARY_API: {
        COLLECTION_SEARCH: '/library/api/collections/{id}/search',
    },
};

import '@js/components/library_search.js';

const LS = window.LibrarySearch;

describe('LibrarySearch', () => {
    describe('initLibrarySearch', () => {
        it('stores default collection ID', () => {
            LS.initLibrarySearch('default-coll', []);
            expect(LS.getDefaultCollectionId()).toBe('default-coll');
        });

        it('stores collections data', () => {
            LS.initLibrarySearch(null, [
                { id: 'a', indexed_document_count: 5 },
                { id: 'b', indexed_document_count: 0 },
                { id: 'c', indexed_document_count: 10 },
            ]);
            expect(LS.getIndexedCollectionIds()).toEqual(['a', 'c']);
        });

        it('handles null/undefined args', () => {
            LS.initLibrarySearch(null, null);
            expect(LS.getDefaultCollectionId()).toBeNull();
            expect(LS.getIndexedCollectionIds()).toEqual([]);
        });
    });

    describe('getIndexedCollectionIds', () => {
        it('filters out collections with 0 indexed documents', () => {
            LS.initLibrarySearch(null, [
                { id: 'a', indexed_document_count: 5 },
                { id: 'b', indexed_document_count: 0 },
                { id: 'c' }, // missing field — treated as 0
            ]);
            expect(LS.getIndexedCollectionIds()).toEqual(['a']);
        });

        it('returns empty array for no collections', () => {
            LS.initLibrarySearch(null, []);
            expect(LS.getIndexedCollectionIds()).toEqual([]);
        });
    });

    describe('LIBRARY_CARD_CONFIG', () => {
        const config = LS.getLibraryCardConfig();

        it('getId returns document_id', () => {
            expect(config.getId({ document_id: 'doc-1' })).toBe('doc-1');
            expect(config.getId({})).toBe('');
        });

        it('getTitle returns title or "Untitled"', () => {
            expect(config.getTitle({ title: 'My Doc' })).toBe('My Doc');
            expect(config.getTitle({})).toBe('Untitled');
        });

        it('getUrl builds document page URL', () => {
            expect(config.getUrl({ document_id: 'abc' })).toBe('/library/document/abc');
        });

        it('getUrl returns # for missing document_id', () => {
            expect(config.getUrl({})).toBe('#');
        });

        it('getBadges returns file type badge', () => {
            const badges = config.getBadges({ file_type: 'pdf' });
            expect(badges).toHaveLength(1);
            expect(badges[0].label).toBe('PDF');
            expect(badges[0].icon).toBe('file-pdf');
        });

        it('getBadges returns default DOC badge when no file type', () => {
            const badges = config.getBadges({});
            expect(badges[0].label).toBe('DOC');
            expect(badges[0].icon).toBe('file');
        });

        it('getBadges maps known file types to icons', () => {
            expect(config.getBadges({ file_type: 'txt' })[0].icon).toBe('file-alt');
            expect(config.getBadges({ file_type: 'md' })[0].icon).toBe('file-code');
            expect(config.getBadges({ file_type: 'markdown' })[0].icon).toBe('file-code');
            expect(config.getBadges({ file_type: 'html' })[0].icon).toBe('file-code');
        });

        it('getDate returns created_at', () => {
            expect(config.getDate({ created_at: '2025-01-15' })).toBe('2025-01-15');
            expect(config.getDate({})).toBeNull();
        });

        it('getSubtitle returns domain', () => {
            expect(config.getSubtitle({ domain: 'example.com' })).toBe('example.com');
            expect(config.getSubtitle({})).toBeNull();
        });
    });

    describe('searchAllCollections deduplication', () => {
        let originalFetch;

        beforeEach(() => {
            originalFetch = globalThis.fetch;
        });

        afterEach(() => {
            globalThis.fetch = originalFetch;
        });

        it('returns empty array for empty collection list', async () => {
            const result = await LS.searchAllCollections([], 'query');
            expect(result).toEqual([]);
        });

        it('deduplicates results by document_id, keeping highest similarity', async () => {
            // Mock fetch to return overlapping results from two collections
            globalThis.fetch = vi.fn()
                .mockResolvedValueOnce({
                    ok: true,
                    json: () => Promise.resolve({
                        success: true,
                        results: [
                            { document_id: 'doc1', similarity: 0.5 },
                            { document_id: 'doc2', similarity: 0.9 },
                        ],
                    }),
                })
                .mockResolvedValueOnce({
                    ok: true,
                    json: () => Promise.resolve({
                        success: true,
                        results: [
                            { document_id: 'doc1', similarity: 0.8 }, // higher than first
                            { document_id: 'doc3', similarity: 0.6 },
                        ],
                    }),
                });

            const result = await LS.searchAllCollections(['c1', 'c2'], 'test');
            // Should have 3 unique docs
            expect(result).toHaveLength(3);
            // doc1 should have similarity 0.8 (the higher one)
            const doc1 = result.find(r => r.document_id === 'doc1');
            expect(doc1.similarity).toBe(0.8);
        });

        it('sorts results by similarity descending', async () => {
            globalThis.fetch = vi.fn().mockResolvedValue({
                ok: true,
                json: () => Promise.resolve({
                    success: true,
                    results: [
                        { document_id: 'a', similarity: 0.3 },
                        { document_id: 'b', similarity: 0.9 },
                        { document_id: 'c', similarity: 0.6 },
                    ],
                }),
            });

            const result = await LS.searchAllCollections(['c1'], 'test');
            expect(result.map(r => r.document_id)).toEqual(['b', 'c', 'a']);
        });

        it('handles failed search responses gracefully', async () => {
            globalThis.fetch = vi.fn()
                .mockResolvedValueOnce({
                    ok: true,
                    json: () => Promise.resolve({ success: false }),
                })
                .mockResolvedValueOnce({
                    ok: true,
                    json: () => Promise.resolve({
                        success: true,
                        results: [{ document_id: 'd1', similarity: 0.7 }],
                    }),
                });

            const result = await LS.searchAllCollections(['c1', 'c2'], 'test');
            expect(result).toHaveLength(1);
            expect(result[0].document_id).toBe('d1');
        });

        it('catches per-collection fetch errors', async () => {
            globalThis.fetch = vi.fn()
                .mockRejectedValueOnce(new Error('network'))
                .mockResolvedValueOnce({
                    ok: true,
                    json: () => Promise.resolve({
                        success: true,
                        results: [{ document_id: 'd1', similarity: 0.5 }],
                    }),
                });

            const result = await LS.searchAllCollections(['c1', 'c2'], 'test');
            expect(result).toHaveLength(1);
        });

        it('respects limit parameter', async () => {
            globalThis.fetch = vi.fn().mockResolvedValue({
                ok: true,
                json: () => Promise.resolve({
                    success: true,
                    results: Array.from({ length: 30 }, (_, i) => ({
                        document_id: `doc${i}`,
                        similarity: 1 - i * 0.01,
                    })),
                }),
            });

            const result = await LS.searchAllCollections(['c1'], 'test', 5);
            expect(result).toHaveLength(5);
        });

        it('skips results without document_id', async () => {
            globalThis.fetch = vi.fn().mockResolvedValue({
                ok: true,
                json: () => Promise.resolve({
                    success: true,
                    results: [
                        { document_id: 'a', similarity: 0.5 },
                        { similarity: 0.9 }, // missing document_id
                    ],
                }),
            });

            const result = await LS.searchAllCollections(['c1'], 'test');
            expect(result).toHaveLength(1);
            expect(result[0].document_id).toBe('a');
        });
    });

    describe('renderSemanticResults', () => {
        it('renders empty state when no results', () => {
            const container = document.createElement('div');
            LS.renderSemanticResults([], container, 'query');
            expect(container.querySelector('.ldr-empty-state')).not.toBeNull();
        });

        it('renders empty state for null results', () => {
            const container = document.createElement('div');
            LS.renderSemanticResults(null, container, 'query');
            expect(container.querySelector('.ldr-empty-state')).not.toBeNull();
        });

        it('does nothing when container is null', () => {
            expect(() => LS.renderSemanticResults([], null, 'q')).not.toThrow();
        });

        it('renders fallback message when SemanticSearch module not loaded', () => {
            const container = document.createElement('div');
            // SemanticSearch not loaded in tests
            LS.renderSemanticResults([{ document_id: '1' }], container, 'q');
            expect(container.textContent).toContain('Search module not loaded');
        });
    });
});
