#!/usr/bin/env node
/**
 * Download Manager UI Tests
 *
 * Tests for /library/download-manager — bulk PDF/text download page.
 *
 * Asserts on the page's real contracts (element IDs from
 * download_manager.html, API shapes from library_routes.py):
 *   - server-rendered structure: header, 4 summary stat cards, filters
 *   - collection selector resolves its async load (not stuck on
 *     "Loading collections..." and not in the error state)
 *   - download buttons stay disabled while nothing is selected and the
 *     Select All / Select None cycle keeps counts + disabled state
 *     consistent (works with any number of research items, incl. zero)
 *   - progress modal exists but is hidden
 *   - /library/api/collections/list contract
 *
 * Context: this page had a real production bug (#3941 UNIQUE-collision
 * cascade) and previously had zero UI test coverage.
 *
 * Run: node test_download_manager_ci.js
 */

const { setupTest, teardownTest, TestResults, log, navigateTo, withTimeout } = require('./test_lib');

const PAGE_PATH = '/library/download-manager';

// ============================================================================
// Page structure
// ============================================================================
const PageTests = {
    async pageLoads(page) {
        await page.waitForSelector('.ldr-download-manager-container', { timeout: 30000 });
        const result = await page.evaluate(() => ({
            headerText: document.querySelector('.ldr-manager-header h1')?.textContent?.trim(),
        }));

        const passed = result.headerText === 'Download Manager';
        return {
            passed,
            message: passed
                ? 'Download Manager page loaded with header'
                : `Unexpected header: "${result.headerText}"`
        };
    },

    async summaryStatsRendered(page) {
        // Four server-rendered stat cards, each with an integer value:
        // total researches, total resources, downloaded, available.
        const result = await page.evaluate(() => {
            const cards = Array.from(document.querySelectorAll('.ldr-summary-stats .ldr-stat-card'));
            return cards.map(c => ({
                value: c.querySelector('h3')?.textContent?.trim(),
                label: c.querySelector('p')?.textContent?.trim(),
            }));
        });

        if (result.length !== 4) {
            return { passed: false, message: `Expected 4 stat cards, found ${result.length}` };
        }
        const badValues = result.filter(c => !/^\d+$/.test(c.value || ''));
        return {
            passed: badValues.length === 0,
            message: badValues.length === 0
                ? `4 stat cards with integer values: ${result.map(c => `${c.label}=${c.value}`).join(', ')}`
                : `Non-integer stat values: ${JSON.stringify(badValues)}`
        };
    },

    async filterControlsPresent(page) {
        const result = await page.evaluate(() => {
            const mode = document.getElementById('filter-mode');
            const bulkTexts = Array.from(
                document.querySelectorAll('.ldr-bulk-actions .ldr-btn-bulk')
            ).map(b => b.textContent?.trim());
            return {
                modeOptions: mode ? Array.from(mode.options).map(o => o.value) : null,
                hasDateFrom: document.getElementById('filter-from')?.type === 'date',
                hasDateTo: document.getElementById('filter-to')?.type === 'date',
                hasRating: !!document.getElementById('filter-rating'),
                hasPdfs: !!document.getElementById('filter-pdfs'),
                hasSearch: !!document.getElementById('search-research'),
                bulkButtons: bulkTexts.length,
                hasSelectAll: bulkTexts.includes('Select All'),
                hasSelectNone: bulkTexts.includes('Select None'),
                hasSelectFiltered: bulkTexts.includes('Select Filtered'),
            };
        });

        const problems = [];
        if (!result.modeOptions?.includes('quick') || !result.modeOptions?.includes('detailed')) {
            problems.push(`mode filter options: ${JSON.stringify(result.modeOptions)}`);
        }
        if (!result.hasDateFrom || !result.hasDateTo) problems.push('date range inputs missing/wrong type');
        if (!result.hasRating) problems.push('rating filter missing');
        if (!result.hasPdfs) problems.push('PDF status filter missing');
        if (!result.hasSearch) problems.push('search input missing');
        if (result.bulkButtons < 3) problems.push(`expected at least 3 bulk buttons, found ${result.bulkButtons}`);
        if (!result.hasSelectAll) problems.push('Select All bulk button missing');
        if (!result.hasSelectNone) problems.push('Select None bulk button missing');
        if (!result.hasSelectFiltered) problems.push('Select Filtered bulk button missing');

        return {
            passed: problems.length === 0,
            message: problems.length === 0
                ? `All filters present (mode, date range, rating, PDF status, search) + ${result.bulkButtons} bulk buttons (Select All/None/Filtered)`
                : `Problems: ${problems.join('; ')}`
        };
    },

    async collectionSelectorResolves(page) {
        // loadCollections() runs at script eval and replaces the
        // "Loading collections..." placeholder with either real
        // collections or "No collections available". The error branch
        // ("Error loading collections") is a failure.
        await page.waitForFunction(() => {
            const select = document.getElementById('target-collection');
            if (!select || select.options.length === 0) return false;
            return !Array.from(select.options).some(o => o.textContent === 'Loading collections...');
        }, { timeout: 30000 });

        const result = await page.evaluate(() => {
            const select = document.getElementById('target-collection');
            const options = Array.from(select.options).map(o => o.textContent);
            return { options, errorState: options.includes('Error loading collections') };
        });

        if (result.errorState) {
            return { passed: false, message: 'Collection selector is in the error state (collections/list fetch failed)' };
        }
        return {
            passed: true,
            message: `Collection selector resolved: ${result.options.slice(0, 5).join(', ')}${result.options.length > 5 ? ', …' : ''}`
        };
    },

    async storageModeButtonPresent(page) {
        // One of three storage-mode variants is server-rendered; all
        // share the .ldr-storage-mode-btn class and "PDF Storage:" label.
        const result = await page.evaluate(() => {
            const btn = document.querySelector('.ldr-storage-mode-btn');
            return { exists: !!btn, text: btn?.textContent?.trim().replace(/\s+/g, ' ').slice(0, 80) };
        });

        const passed = result.exists && /PDF Storage:/.test(result.text || '');
        return {
            passed,
            message: passed
                ? `Storage mode button present: "${result.text}"`
                : `Storage mode button missing or unlabeled (text: "${result.text}")`
        };
    },

    async progressModalHidden(page) {
        const result = await page.evaluate(() => {
            const modal = document.getElementById('download-progress-modal');
            const breakdown = document.querySelector('.ldr-status-breakdown');
            return {
                exists: !!modal,
                hidden: modal ? getComputedStyle(modal).display === 'none' : null,
                hasProgressBar: !!document.getElementById('overall-progress'),
                hasLog: !!document.getElementById('download-log'),
                hasBreakdown: !!breakdown,
                breakdownHidden: breakdown ? getComputedStyle(breakdown).display === 'none' : null,
                hasSucceededCount: !!document.getElementById('succeeded-count'),
                hasSkippedCount: !!document.getElementById('skipped-count'),
                hasFailedCount: !!document.getElementById('failed-count'),
            };
        });

        const passed = result.exists && result.hidden && result.hasProgressBar && result.hasLog &&
            result.hasBreakdown && result.breakdownHidden &&
            result.hasSucceededCount && result.hasSkippedCount && result.hasFailedCount;
        return {
            passed,
            message: passed
                ? 'Progress modal exists with progress bar + log + status breakdown, hidden by default'
                : `Modal state wrong: ${JSON.stringify(result)}`
        };
    },

    async settingsLinkPresent(page) {
        const exists = await page.evaluate(() =>
            !!document.querySelector('a[href="/settings#research-library"]')
        );
        return {
            passed: exists,
            message: exists
                ? 'Configure Library Settings link points to /settings#research-library'
                : 'Settings link missing'
        };
    },
};

// ============================================================================
// Selection behavior
// ============================================================================
const SelectionTests = {
    async downloadButtonsDisabledAtZero(page) {
        const result = await page.evaluate(() => ({
            pdfDisabled: document.getElementById('download-pdfs')?.disabled,
            textDisabled: document.getElementById('download-text-db')?.disabled,
            pdfCount: document.querySelector('.ldr-selected-count-pdf')?.textContent?.trim(),
            textCount: document.querySelector('.ldr-selected-count-text-db')?.textContent?.trim(),
        }));

        const passed = result.pdfDisabled === true && result.textDisabled === true &&
            result.pdfCount === '0' && result.textCount === '0';
        return {
            passed,
            message: passed
                ? 'Both download buttons disabled with 0 selected'
                : `Initial state wrong: ${JSON.stringify(result)}`
        };
    },

    async selectAllNoneCycle(page) {
        // Select All must set the count to the number of checkboxes and
        // (when items exist) enable the Text Only button; Select None
        // must return to 0/disabled. Works with zero items too: the
        // count stays 0 and buttons stay disabled.
        // window.selectAll/selectNone are page globals defined in the
        // inline script of templates/pages/download_manager.html.
        // TODO: consider wrapping selectAll/selectNone in a test helper
        // (e.g. window.__ldrTest__.downloadManager) to reduce coupling
        // as more contract tests are added for this page.
        const result = await page.evaluate(() => {
            const checkboxCount = document.querySelectorAll('.ldr-research-select').length;
            const read = () => ({
                count: document.querySelector('.ldr-selected-count-text-db')?.textContent?.trim(),
                textDisabled: document.getElementById('download-text-db')?.disabled,
                selectedItems: document.querySelectorAll('.ldr-research-item.ldr-selected').length,
            });

            window.selectAll();
            const afterAll = read();
            window.selectNone();
            const afterNone = read();

            return { checkboxCount, afterAll, afterNone };
        });

        const problems = [];
        if (result.afterAll.count !== String(result.checkboxCount)) {
            problems.push(`Select All count=${result.afterAll.count}, expected ${result.checkboxCount}`);
        }
        if (result.afterAll.selectedItems !== result.checkboxCount) {
            problems.push(`Select All highlighted ${result.afterAll.selectedItems}/${result.checkboxCount} items`);
        }
        if (result.checkboxCount > 0 && result.afterAll.textDisabled !== false) {
            problems.push('Text Only button still disabled after Select All');
        }
        if (result.afterNone.count !== '0' || result.afterNone.textDisabled !== true || result.afterNone.selectedItems !== 0) {
            problems.push(`Select None state wrong: ${JSON.stringify(result.afterNone)}`);
        }

        return {
            passed: problems.length === 0,
            message: problems.length === 0
                ? `Select All/None cycle consistent (${result.checkboxCount} research items)`
                : `Problems: ${problems.join('; ')}`
        };
    },
};

// ============================================================================
// API contracts
// ============================================================================
const ApiTests = {
    async collectionsListContract(page) {
        const result = await page.evaluate(async () => {
            const resp = await fetch('/library/api/collections/list');
            if (!resp.ok) return { ok: false, status: resp.status };
            const data = await resp.json();
            return {
                ok: true,
                success: data.success,
                collectionsIsArray: Array.isArray(data.collections),
                count: Array.isArray(data.collections) ? data.collections.length : null,
                shapeOk: Array.isArray(data.collections) && data.collections.every(
                    c => c.id !== undefined && typeof c.name === 'string'
                ),
            };
        });

        if (!result.ok) {
            return { passed: false, message: `collections/list returned HTTP ${result.status}` };
        }
        const passed = result.success === true && result.collectionsIsArray && result.shapeOk;
        return {
            passed,
            message: passed
                ? `collections/list contract OK (${result.count} collections, each with id + name)`
                : `Contract violation: ${JSON.stringify(result)}`
        };
    },
};

// ============================================================================
// Main Test Runner
// ============================================================================
async function main() {
    log.section('Download Manager Tests');

    const ctx = await setupTest({ authenticate: true });
    const results = new TestResults('Download Manager Tests');
    const { page } = ctx;
    const { baseUrl } = ctx.config;

    // Collect uncaught page exceptions for the whole run.
    const pageErrors = [];
    page.on('pageerror', (err) => pageErrors.push(err.message));

    const subTestTimeout = ctx.config.isCI ? 60000 : 30000;
    async function run(category, name, testFn) {
        try {
            const result = await withTimeout(testFn(page), subTestTimeout, `${category}/${name}`);
            if (result.skipped) {
                results.skip(category, name, result.message);
            } else {
                results.add(category, name, result.passed, result.message);
            }
        } catch (error) {
            results.add(category, name, false, `Error: ${error.message}`);
        }
    }

    try {
        await navigateTo(page, `${baseUrl}${PAGE_PATH}`);

        log.section('Page Structure');
        await run('Page', 'Page Loads', PageTests.pageLoads);
        await run('Page', 'Summary Stats Rendered', PageTests.summaryStatsRendered);
        await run('Page', 'Filter Controls Present', PageTests.filterControlsPresent);
        await run('Page', 'Collection Selector Resolves', PageTests.collectionSelectorResolves);
        await run('Page', 'Storage Mode Button Present', PageTests.storageModeButtonPresent);
        await run('Page', 'Progress Modal Hidden', PageTests.progressModalHidden);
        await run('Page', 'Settings Link Present', PageTests.settingsLinkPresent);

        log.section('Selection Behavior');
        await run('Selection', 'Download Buttons Disabled At Zero', SelectionTests.downloadButtonsDisabledAtZero);
        await run('Selection', 'Select All/None Cycle', SelectionTests.selectAllNoneCycle);

        log.section('API Contracts');
        await run('API', 'Collections List Contract', ApiTests.collectionsListContract);

        log.section('Page Errors');
        results.add(
            'Errors', 'No Uncaught Page Exceptions',
            pageErrors.length === 0,
            pageErrors.length === 0
                ? 'No uncaught exceptions during the whole run'
                : `Uncaught exceptions: ${pageErrors.slice(0, 3).join(' | ')}`
        );
    } catch (error) {
        log.error(`Fatal error: ${error.message}`);
        console.error(error.stack);
        results.add('Fatal', 'Test Suite Execution', false, error.message);
    } finally {
        results.print();
        results.save();
        await teardownTest(ctx);
        process.exit(results.exitCode());
    }
}

if (require.main === module) {
    main().catch(error => {
        console.error('Test runner failed:', error);
        process.exit(1);
    });
}

module.exports = { PageTests, SelectionTests, ApiTests };
