/**
 * Tests for components/history_search.js
 *
 * Covers the public API exposed via window.HistorySearch:
 * - triggerIndexing: convert-all then index-start sequence + error handling
 * - semanticSearchHistory: POST shape + error mapping
 * - renderSemanticResults: empty-state and SemanticSearch delegation
 *
 * Polling (startPolling) holds private module state (isIndexing,
 * pollErrorCount, indexingPollInterval) and uses real setInterval — testing
 * it directly here would leak state between tests, so we exercise the
 * setup-and-error paths and trust integration coverage for the loop body.
 */

// Stubs MUST be in place before the module is imported, because the IIFE
// auto-runs initSemanticSearch() at the bottom (DOM is "loaded" in tests).
window.URLS = {
    LIBRARY_API: {
        RESEARCH_HISTORY_COLLECTION: '/library/api/research-history/collection',
        RESEARCH_HISTORY_CONVERT_ALL: '/library/api/research-history/convert-all',
        COLLECTION_INDEX_START: '/library/api/collections/{id}/index/start',
        COLLECTION_INDEX_STATUS: '/library/api/collections/{id}/index/status',
        COLLECTION_SEARCH: '/library/api/collections/{id}/search',
    },
};
window.URLBuilder = {
    build: (template, id) => template.replace('{id}', id),
};
window.ResearchStates = {
    isTerminal: (s) => ['completed', 'failed', 'cancelled'].includes(s),
    isCompleted: (s) => s === 'completed',
    isFailed: (s) => s === 'failed',
    isCancelled: (s) => s === 'cancelled',
};
window.api = { getCsrfToken: () => 'test-token' };

// Initial init fetch — return a collection ID so triggerIndexing won't bail
const initialFetchMock = vi.fn().mockResolvedValue({
    ok: true,
    json: () => Promise.resolve({
        success: true,
        collection_id: 'coll-1',
        indexed_documents: 0,
        total_documents: 10,
    }),
});
globalThis.fetch = initialFetchMock;

let HS;
beforeAll(async () => {
    await import('@js/components/history_search.js');
    HS = window.HistorySearch;
    // Wait for the auto-init's fetches to settle
    await new Promise(r => setTimeout(r, 0));
});

beforeEach(() => {
    // Reset to a no-op fetch; each test installs what it needs
    globalThis.fetch = vi.fn().mockResolvedValue({
        ok: true,
        json: () => Promise.resolve({}),
    });
});

describe('HistorySearch namespace', () => {
    it('exposes the expected functions', () => {
        expect(typeof HS.toggleSemanticPanel).toBe('function');
        expect(typeof HS.triggerIndexing).toBe('function');
        expect(typeof HS.semanticSearchHistory).toBe('function');
        expect(typeof HS.renderSemanticResults).toBe('function');
        expect(typeof HS.getSemanticCollectionId).toBe('function');
    });

    it('caches collection ID from initial loadIndexingStatus', () => {
        // initialFetchMock returned collection_id 'coll-1'
        expect(HS.getSemanticCollectionId()).toBe('coll-1');
    });
});

describe('HistorySearch.semanticSearchHistory', () => {
    it('returns [] for empty query', async () => {
        const r = await HS.semanticSearchHistory('');
        expect(r).toEqual([]);
        expect(globalThis.fetch).not.toHaveBeenCalled();
    });

    it('POSTs to the collection search endpoint with query and CSRF token', async () => {
        globalThis.fetch = vi.fn().mockResolvedValue({
            ok: true,
            json: () => Promise.resolve({
                success: true,
                results: [{ research_id: 'r1', similarity: 0.8 }],
            }),
        });

        const results = await HS.semanticSearchHistory('hello world');

        expect(globalThis.fetch).toHaveBeenCalledTimes(1);
        const [url, opts] = globalThis.fetch.mock.calls[0];
        expect(url).toContain('coll-1');
        expect(url).toContain('search');
        expect(opts.method).toBe('POST');
        expect(opts.headers['X-CSRFToken']).toBe('test-token');
        expect(JSON.parse(opts.body)).toEqual({ query: 'hello world', limit: 20 });
        expect(results).toHaveLength(1);
    });

    it('throws when the API returns success: false', async () => {
        globalThis.fetch = vi.fn().mockResolvedValue({
            ok: true,
            json: () => Promise.resolve({ success: false, error: 'Index missing' }),
        });

        await expect(HS.semanticSearchHistory('q')).rejects.toThrow('Index missing');
    });

    it('throws on non-OK responses', async () => {
        globalThis.fetch = vi.fn().mockResolvedValue({
            ok: false,
            status: 500,
            json: () => Promise.resolve({}),
        });

        await expect(HS.semanticSearchHistory('q')).rejects.toThrow(/500/);
    });

    it('returns empty array when results field is missing', async () => {
        globalThis.fetch = vi.fn().mockResolvedValue({
            ok: true,
            json: () => Promise.resolve({ success: true }),
        });

        const r = await HS.semanticSearchHistory('q');
        expect(r).toEqual([]);
    });
});

describe('HistorySearch.renderSemanticResults', () => {
    afterEach(() => {
        document.body.innerHTML = '';
    });

    it('does nothing when the history-items container is missing', () => {
        // No container in DOM
        expect(() => HS.renderSemanticResults([{ research_id: 'a' }], 'q')).not.toThrow();
    });

    it('renders an empty-state message for null results', () => {
        const c = document.createElement('div');
        c.id = 'history-items';
        document.body.appendChild(c);

        HS.renderSemanticResults(null, 'q');
        expect(c.querySelector('.ldr-empty-state')).not.toBeNull();
    });

    it('renders an empty-state message for an empty array', () => {
        const c = document.createElement('div');
        c.id = 'history-items';
        document.body.appendChild(c);

        HS.renderSemanticResults([], 'q');
        expect(c.querySelector('.ldr-empty-state')).not.toBeNull();
        expect(c.textContent).toContain('No matching results');
    });

    it('delegates each result to SemanticSearch.createSemanticResultCard', () => {
        const c = document.createElement('div');
        c.id = 'history-items';
        document.body.appendChild(c);

        const fakeCard = () => {
            const el = document.createElement('div');
            el.className = 'fake-card';
            return el;
        };
        const savedSS = window.SemanticSearch;
        window.SemanticSearch = {
            createSemanticResultCard: vi.fn().mockImplementation(fakeCard),
        };

        try {
            HS.renderSemanticResults(
                [{ research_id: 'a' }, { research_id: 'b' }],
                'q'
            );
            expect(window.SemanticSearch.createSemanticResultCard).toHaveBeenCalledTimes(2);
            expect(c.querySelectorAll('.fake-card')).toHaveLength(2);
        } finally {
            window.SemanticSearch = savedSS;
        }
    });
});

describe('HistorySearch.toggleSemanticPanel', () => {
    afterEach(() => {
        document.body.innerHTML = '';
    });

    it('toggles display style and chevron icon class', () => {
        const content = document.createElement('div');
        content.id = 'semantic-panel-content';
        content.style.display = 'block';
        const toggle = document.createElement('i');
        toggle.id = 'semantic-panel-toggle';
        toggle.className = 'fas fa-chevron-down';
        const header = document.createElement('div');
        header.id = 'semantic-panel-header';
        header.setAttribute('aria-expanded', 'true');
        document.body.append(content, toggle, header);

        HS.toggleSemanticPanel();
        expect(content.style.display).toBe('none');
        expect(toggle.className).toBe('fas fa-chevron-right');
        expect(header.getAttribute('aria-expanded')).toBe('false');

        HS.toggleSemanticPanel();
        expect(content.style.display).toBe('block');
        expect(toggle.className).toBe('fas fa-chevron-down');
        expect(header.getAttribute('aria-expanded')).toBe('true');
    });

    it('does nothing when expected elements are missing', () => {
        expect(() => HS.toggleSemanticPanel()).not.toThrow();
    });
});
