/**
 * Regression test for the resultCount interpolation in the news search
 * history list (pages/news.js renderSearchHistory).
 *
 * result_count is enforced as Integer by SQLAlchemy (user_news_search_history.py),
 * so the existing `|| 0` fallback already handles NULL. The Number() wrap
 * added here is defense-in-depth: it matches the pattern used in PR #3095
 * for impact_score / upvotes / downvotes, and neutralizes any future
 * schema drift or proxy layer that returns the count as a string.
 *
 * The render path runs the template through safeRenderHTML -> DOMPurify, so
 * we spy on DOMPurify.sanitize to inspect the pre-sanitized output (same
 * approach as news-xss.test.js).
 */

let sanitizeSpy;
let fetchResponses;

function makeItem(overrides = {}) {
    return {
        id: 1,
        query: 'safe query',
        type: 'quick',
        timestamp: new Date().toISOString(),
        resultCount: 0,
        ...overrides,
    };
}

beforeAll(async () => {
    await import('@js/security/xss-protection.js');

    sanitizeSpy = vi.fn((htmlString) => {
        const template = document.createElement('template');
        // eslint-disable-next-line no-unsanitized/property -- test-only spy: capturing the pre-sanitized template output for assertions. Runs only under Vitest.
        template.innerHTML = htmlString;
        return template.content;
    });
    window.DOMPurify = { sanitize: sanitizeSpy };

    // fetchResponses is keyed by URL substring so individual tests can swap
    // in payloads for /news/api/search-history without affecting other
    // endpoints initializeNewsPage touches.
    fetchResponses = { default: { search_history: [] } };
    globalThis.fetch = vi.fn((url) => {
        const urlStr = String(url);
        let body = {};
        for (const [key, value] of Object.entries(fetchResponses)) {
            if (key !== 'default' && urlStr.includes(key)) {
                body = value;
                break;
            }
        }
        if (Object.keys(body).length === 0) body = fetchResponses.default;
        return Promise.resolve({
            ok: true,
            status: 200,
            json: () => Promise.resolve(body),
            text: () => Promise.resolve(''),
        });
    });

    document.body.innerHTML = `
        <div id="news-feed-content"></div>
        <input id="table-view-toggle" type="checkbox">
        <tbody id="news-table-body"></tbody>
        <input id="news-search" type="text">
        <button id="search-btn"></button>
        <button id="create-subscription-btn"></button>
        <button id="run-template-btn"></button>
        <input id="impact-filter" type="range" min="0" max="10" value="0">
        <span class="ldr-impact-value"></span>
        <div id="sidebar-subscriptions-list"></div>
        <div id="news-semantic-results"></div>
        <div id="news-query"></div>
        <div id="newsSubscriptionModal"></div>
        <div id="news-subscription-query"></div>
        <div class="ldr-time-filter-group"><button class="ldr-filter-btn"></button></div>
        <input id="auto-refresh" type="checkbox">
        <button id="refresh-feed-btn"></button>
        <div id="recent-searches"></div>
    `;

    await import('@js/pages/news.js');
});

async function renderSearchHistoryWith(items) {
    sanitizeSpy.mockClear();
    fetchResponses['/news/api/search-history'] = { search_history: items };
    if (typeof window.loadSearchHistory === 'function') {
        await window.loadSearchHistory();
    } else {
        throw new Error('loadSearchHistory is not exported on window — test harness regression');
    }
    await new Promise(resolve => setTimeout(resolve, 10));
}

function renderedHtmlForQuery(query) {
    for (const [htmlString] of sanitizeSpy.mock.calls) {
        if (typeof htmlString === 'string' && htmlString.includes(query)) {
            return htmlString;
        }
    }
    return '';
}

describe('pages/news.js — resultCount defense-in-depth (PR #3095 follow-up)', () => {
    it('does not leak raw <script> from resultCount into the search history item', async () => {
        const payload = '<script>alert(1)</script>';
        const html = await renderSearchHistoryWith([makeItem({
            id: 2,
            query: 'RESULTCOUNT_CANARY',
            resultCount: payload,
        })]).then(() => renderedHtmlForQuery('RESULTCOUNT_CANARY'));

        expect(html).not.toBe('');
        // Number('<script>...') is NaN; `|| 0` falls back to 0. The literal
        // payload substring must not survive into the rendered template.
        expect(html).not.toContain(payload);
        expect(html).toContain('0 results');
    });

    it('renders the actual count for normal integer input', async () => {
        const html = await renderSearchHistoryWith([makeItem({
            id: 3,
            query: 'INTEGER_CANARY',
            resultCount: 42,
        })]).then(() => renderedHtmlForQuery('INTEGER_CANARY'));

        expect(html).toContain('42 results');
    });
});
