#!/usr/bin/env node
/**
 * Settings Pages UI Tests
 *
 * Tests for the settings dashboard, tabs, and configuration options.
 *
 * Run: node test_settings_pages_ci.js
 */
const { setupTest, teardownTest, TestResults, log, delay, waitForVisible, navigateTo, withTimeout } = require('./test_lib');

// Open /settings/ on a clean "All Settings" tab. navigateTo() no-ops when the
// page is already on /settings/, so a prior test's active tab leaks in (e.g.
// the 'llm' tab, which hides search settings). Force the 'all' tab so every
// setting renders before the Input assertions read the DOM.
async function openSettingsAllTab(page, baseUrl) {
    await navigateTo(page, `${baseUrl}/settings/`);
    await page.waitForSelector('#settings-content .ldr-settings-item', { timeout: 15000 });
    // Require the 'all' tab to exist so a missing tab fails here with context,
    // rather than silently proceeding to an opaque downstream timeout.
    await page.waitForSelector('.ldr-settings-tab[data-tab="all"]', { timeout: 5000 });
    const isActive = await page.$eval('.ldr-settings-tab[data-tab="all"]', el => el.classList.contains('active'));
    if (!isActive) {
        await page.click('.ldr-settings-tab[data-tab="all"]');
        await page.waitForFunction(
            () => document.querySelector('.ldr-settings-tab[data-tab="all"]')?.classList.contains('active') === true,
            { timeout: 5000 }
        ).catch(() => {});
    }
}

// llm.provider and search.tool both render as custom dropdowns (custom_dropdown.html):
// a hidden input[name=<key>] carries the form value inside a .ldr-custom-dropdown,
// and the dropdown JS resolves the selection asynchronously after load. Assert a
// real value ends up selected. The .catch on the value-wait lets the assertion
// below report a specific `value=""` message rather than a raw timeout.
async function assertCustomDropdownSelected(page, name, label) {
    await page.waitForSelector(`#settings-content [name="${name}"]`, { timeout: 15000 });
    await page.waitForFunction(
        (n) => (document.querySelector(`[name="${n}"]`)?.value || '').length > 0,
        { timeout: 10000 }, name
    ).catch(() => {});
    const result = await page.evaluate((n) => {
        const hidden = document.querySelector(`[name="${n}"]`);
        return { value: hidden?.value || '', hasDropdown: !!hidden?.closest('.ldr-custom-dropdown') };
    }, name);
    const passed = result.value.length > 0 && result.hasDropdown;
    return {
        passed,
        message: passed
            ? `${label} renders a real selection ("${result.value}") in a custom dropdown`
            : `${label} incomplete (value="${result.value}", customDropdown=${result.hasDropdown})`
    };
}

// ============================================================================
// Settings Page Structure Tests
// ============================================================================
const SettingsPageTests = {
    async settingsPageLoads(page, baseUrl) {
        await navigateTo(page, `${baseUrl}/settings/`);

        const result = await page.evaluate(() => {
            return {
                hasSettingsContent: !!document.querySelector('.settings-container, .ldr-settings, #settings, .settings-dashboard'),
                hasForm: !!document.querySelector('form'),
                hasTabs: !!document.querySelector('.nav-tabs, .tab-list, [role="tablist"], .settings-tabs'),
                pageTitle: document.title,
                hasAnyInputs: document.querySelectorAll('input, select, textarea').length > 0
            };
        });

        const passed = result.hasSettingsContent || result.hasAnyInputs;
        return {
            passed,
            message: passed
                ? `Settings page loaded (content=${result.hasSettingsContent}, form=${result.hasForm}, tabs=${result.hasTabs})`
                : 'Settings page failed to load expected content'
        };
    },

    async settingsTabsExist(page, baseUrl) {
        await navigateTo(page, `${baseUrl}/settings/`);

        const result = await page.evaluate(() => {
            const tabs = document.querySelectorAll('.nav-tabs .nav-link, .tab-list button, [role="tab"], .settings-tab');
            const tabTexts = Array.from(tabs).map(t => t.textContent?.trim());

            // Also check for category sections if tabs aren't used
            const categories = document.querySelectorAll('.settings-category, .setting-section, h2, h3');
            const categoryTexts = Array.from(categories).map(c => c.textContent?.trim()).filter(t => t && t.length < 50);

            return {
                tabCount: tabs.length,
                tabNames: tabTexts.slice(0, 10),
                categoryCount: categories.length,
                categoryNames: categoryTexts.slice(0, 10)
            };
        });

        const hasTabs = result.tabCount > 0;
        const hasCategories = result.categoryCount > 0;

        return {
            passed: hasTabs || hasCategories,
            message: hasTabs
                ? `Found ${result.tabCount} tabs: ${result.tabNames.join(', ')}`
                : hasCategories
                    ? `Found ${result.categoryCount} setting categories`
                    : 'No tabs or categories found'
        };
    },

    async settingsTabNavigation(page, baseUrl) {
        await navigateTo(page, `${baseUrl}/settings/`);

        const tabs = await page.$$('.nav-tabs .nav-link, [role="tab"]');
        if (tabs.length < 2) {
            return { passed: null, skipped: true, message: 'Not enough tabs to test navigation' };
        }

        try {
            // Click second tab using evaluate to avoid stale element issues
            await page.evaluate(() => {
                const tabElements = document.querySelectorAll('.nav-tabs .nav-link, [role="tab"]');
                if (tabElements[1]) {
                    tabElements[1].click();
                }
            });

            // Wait for page to stabilize
            await delay(1000);

            // Wait for network to settle in case of navigation
            await page.waitForFunction(() => document.readyState === 'complete', { timeout: 5000 }).catch(() => {});

            const result = await page.evaluate(() => {
                const activeTab = document.querySelector('.nav-link.active, [role="tab"][aria-selected="true"]');
                const visiblePanel = document.querySelector('.tab-pane.active, .tab-pane.show, [role="tabpanel"]:not([hidden])');

                return {
                    hasActiveTab: !!activeTab,
                    activeTabText: activeTab?.textContent?.trim(),
                    hasVisiblePanel: !!visiblePanel
                };
            });

            return {
                passed: result.hasActiveTab,
                message: result.hasActiveTab
                    ? `Tab navigation works (active: "${result.activeTabText}")`
                    : 'Tab navigation not working properly'
            };
        } catch (err) {
            // Handle navigation or context destruction gracefully
            if (err.message && (err.message.includes('context') || err.message.includes('navigation') || err.message.includes('Target closed'))) {
                return { passed: null, skipped: true, message: 'Tab click caused page navigation - skipping' };
            }
            return { passed: null, skipped: true, message: `Tab click failed: ${err.message}` };
        }
    }
};

// ============================================================================
// Settings Input Tests
// ============================================================================
const SettingsInputTests = {
    async modelProviderSetting(page, baseUrl) {
        // llm.provider renders as a custom dropdown; assert a *real* provider
        // ends up selected (the old test returned passed:true for any input).
        await openSettingsAllTab(page, baseUrl);
        return assertCustomDropdownSelected(page, 'llm.provider', 'Provider setting');
    },

    async searchEngineSetting(page, baseUrl) {
        // search.tool also renders as a custom dropdown. The old test's
        // select[name*=search] matched an unrelated country <select> (e.g.
        // SearXNG region); target the real search.tool field instead.
        await openSettingsAllTab(page, baseUrl);
        return assertCustomDropdownSelected(page, 'search.tool', 'Search engine setting');
    },

    async temperatureSetting(page, baseUrl) {
        // llm.temperature renders as a range slider (settings_form.html). The
        // old test passed as long as any temperature input existed; assert the
        // real control contract: type=range with a numeric value within [min,max].
        await openSettingsAllTab(page, baseUrl);
        await page.waitForSelector('#settings-content [name="llm.temperature"]', { timeout: 15000 });

        const result = await page.evaluate(() => {
            const el = document.querySelector('[name="llm.temperature"]');
            if (!el) return { exists: false };
            return { exists: true, type: el.type, value: el.value, min: el.min, max: el.max, step: el.step };
        });

        if (!result.exists) {
            return { passed: false, message: 'llm.temperature input not rendered' };
        }
        const val = parseFloat(result.value), min = parseFloat(result.min),
              max = parseFloat(result.max), step = parseFloat(result.step);
        const passed = result.type === 'range'
            && Number.isFinite(val) && Number.isFinite(min) && Number.isFinite(max)
            && val >= min && val <= max
            && Number.isFinite(step) && step > 0;   // a real slider has a positive step
        return {
            passed,
            message: passed
                ? `Temperature is a range slider (value=${result.value} in [${result.min}, ${result.max}], step=${result.step})`
                : `Temperature contract failed (type=${result.type}, value=${result.value}, min=${result.min}, max=${result.max}, step=${result.step})`
        };
    },

    async apiKeyFieldMasked(page, baseUrl) {
        // API-key settings render as password inputs (settings_form.html,
        // ui_element == "password"). Assert *every* rendered api_key field is
        // masked, not just that one exists (the old test checked only the first).
        await openSettingsAllTab(page, baseUrl);
        await page.waitForSelector('#settings-content input[name$=".api_key"]', { timeout: 15000 });

        const result = await page.evaluate(() => {
            const fields = Array.from(document.querySelectorAll('#settings-content input[name$=".api_key"]'));
            const unmasked = fields.filter(f => f.type !== 'password').map(f => f.name);
            return { count: fields.length, unmasked };
        });

        const passed = result.count > 0 && result.unmasked.length === 0;
        return {
            passed,
            message: passed
                ? `All ${result.count} api_key fields are masked (type=password)`
                : `${result.unmasked.length}/${result.count} api_key fields NOT masked: ${result.unmasked.slice(0, 3).join(', ')}`
        };
    }
};

// ============================================================================
// Settings Action Tests
// ============================================================================
const SettingsActionTests = {
    async saveButtonExists(page, baseUrl) {
        await navigateTo(page, `${baseUrl}/settings/`);

        const result = await page.evaluate(() => {
            // Note: :contains() is not valid CSS, removed from selector
            const saveButtonsByCss = document.querySelectorAll(
                'button[type="submit"], ' +
                '.btn-save, ' +
                '[onclick*="save"], ' +
                'button.btn-primary'
            );

            // More thorough search - find buttons by text content
            const allButtons = Array.from(document.querySelectorAll('button, input[type="submit"]'));
            const saveBtn = allButtons.find(b =>
                b.textContent?.toLowerCase().includes('save') ||
                b.value?.toLowerCase().includes('save')
            );

            return {
                hasSaveButton: !!saveBtn || saveButtonsByCss.length > 0,
                buttonText: saveBtn?.textContent?.trim() || saveBtn?.value,
                buttonCount: allButtons.filter(b =>
                    b.textContent?.toLowerCase().includes('save') ||
                    b.value?.toLowerCase().includes('save')
                ).length
            };
        });

        return {
            passed: result.hasSaveButton,
            message: result.hasSaveButton
                ? `Save button found ("${result.buttonText}")`
                : 'No save button found'
        };
    },

    async resetButtonExists(page, baseUrl) {
        await navigateTo(page, `${baseUrl}/settings/`);

        const result = await page.evaluate(() => {
            const allButtons = Array.from(document.querySelectorAll('button, input[type="button"], input[type="reset"]'));
            const resetBtn = allButtons.find(b =>
                b.textContent?.toLowerCase().includes('reset') ||
                b.textContent?.toLowerCase().includes('default') ||
                b.value?.toLowerCase().includes('reset')
            );

            return {
                hasResetButton: !!resetBtn,
                buttonText: resetBtn?.textContent?.trim() || resetBtn?.value
            };
        });

        if (!result.hasResetButton) {
            return { passed: null, skipped: true, message: 'No reset/defaults button found' };
        }

        return {
            passed: true,
            message: `Reset button found ("${result.buttonText}")`
        };
    },

    async searchFilterExists(page, baseUrl) {
        await navigateTo(page, `${baseUrl}/settings/`);

        const result = await page.evaluate(() => {
            const searchInput = document.querySelector(
                'input[type="search"], ' +
                'input[placeholder*="search"], ' +
                'input[placeholder*="filter"], ' +
                '#settings-search, ' +
                '.settings-filter'
            );

            return {
                hasSearch: !!searchInput,
                placeholder: searchInput?.placeholder
            };
        });

        if (!result.hasSearch) {
            return { passed: null, skipped: true, message: 'No search/filter input on settings page' };
        }

        return {
            passed: true,
            message: `Settings search/filter found (placeholder: "${result.placeholder}")`
        };
    }
};

// ============================================================================
// Settings Status Tests
// ============================================================================
const SettingsStatusTests = {
    async warningsDisplay(page, baseUrl) {
        await navigateTo(page, `${baseUrl}/settings/`);

        const result = await page.evaluate(() => {
            const warnings = document.querySelectorAll(
                '.alert-warning, ' +
                '.warning, ' +
                '.config-warning, ' +
                '[class*="warning"]'
            );

            const warningTexts = Array.from(warnings)
                .map(w => w.textContent?.trim())
                .filter(t => t && t.length < 200);

            return {
                warningCount: warnings.length,
                warningTexts: warningTexts.slice(0, 3)
            };
        });

        // This is informational - either warnings or no warnings is OK
        return {
            passed: true,
            message: result.warningCount > 0
                ? `${result.warningCount} configuration warning(s) displayed`
                : 'No configuration warnings (good configuration)'
        };
    },

    async ollamaStatusIndicator(page, baseUrl) {
        await navigateTo(page, `${baseUrl}/settings/`);

        // Wait for settings form to render before checking for ollama elements
        try {
            await page.waitForSelector('[name="llm.provider"], input[name*="api_key"]', { timeout: 15000 });
        } catch {
            // Form didn't render — skip
        }

        const result = await page.evaluate(() => {
            const ollamaStatus = document.querySelector(
                '.ollama-status, ' +
                '[data-ollama-status], ' +
                '#ollama-status, ' +
                '.status-indicator[class*="ollama"]'
            );

            const ollamaSection = document.querySelector('[class*="ollama"], [id*="ollama"]');

            return {
                hasStatusIndicator: !!ollamaStatus,
                hasOllamaSection: !!ollamaSection,
                statusText: ollamaStatus?.textContent?.trim()
            };
        });

        if (!result.hasStatusIndicator && !result.hasOllamaSection) {
            return { passed: null, skipped: true, message: 'No Ollama status indicator found' };
        }

        return {
            passed: true,
            message: result.hasStatusIndicator
                ? `Ollama status: "${result.statusText}"`
                : 'Ollama section present on page'
        };
    },

    async availableModelsApiWorks(page, baseUrl) {
        await navigateTo(page, `${baseUrl}/settings/`);

        const result = await page.evaluate(async (url) => {
            try {
                const response = await fetch(`${url}/settings/api/available-models`);
                if (!response.ok) return { ok: false, status: response.status };

                const data = await response.json();
                return {
                    ok: true,
                    hasProviders: Object.keys(data).length > 0,
                    providers: Object.keys(data).slice(0, 5)
                };
            } catch (e) {
                return { ok: false, error: e.message };
            }
        }, baseUrl);

        return {
            passed: result.ok,
            message: result.ok
                ? `Available models API works (providers: ${result.providers?.join(', ') || 'none'})`
                : `Available models API failed: ${result.error || 'status ' + result.status}`
        };
    },

    async availableSearchEnginesApiWorks(page, baseUrl) {
        await navigateTo(page, `${baseUrl}/settings/`);

        const result = await page.evaluate(async (url) => {
            try {
                const response = await fetch(`${url}/settings/api/available-search-engines`);
                if (!response.ok) return { ok: false, status: response.status };

                const data = await response.json();
                const engines = Array.isArray(data) ? data : Object.keys(data);
                return {
                    ok: true,
                    engineCount: engines.length,
                    engines: engines.slice(0, 5)
                };
            } catch (e) {
                return { ok: false, error: e.message };
            }
        }, baseUrl);

        return {
            passed: result.ok,
            message: result.ok
                ? `Search engines API works (${result.engineCount} engines: ${result.engines?.join(', ')})`
                : `Search engines API failed: ${result.error || 'status ' + result.status}`
        };
    }
};

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

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

    const subTestTimeout = ctx.config.isCI ? 60000 : 30000;
    async function run(category, name, testFn) {
        try {
            const result = await withTimeout(
                testFn(page, baseUrl),
                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 {
        // Page Structure Tests
        log.section('Page Structure');
        await run('Structure', 'Settings Page Loads', (p, u) => SettingsPageTests.settingsPageLoads(p, u));
        await run('Structure', 'Settings Tabs Exist', (p, u) => SettingsPageTests.settingsTabsExist(p, u));
        await run('Structure', 'Tab Navigation', (p, u) => SettingsPageTests.settingsTabNavigation(p, u));

        // Input Tests
        log.section('Settings Inputs');
        await run('Inputs', 'Model Provider Setting', (p, u) => SettingsInputTests.modelProviderSetting(p, u));
        await run('Inputs', 'Search Engine Setting', (p, u) => SettingsInputTests.searchEngineSetting(p, u));
        await run('Inputs', 'Temperature Setting', (p, u) => SettingsInputTests.temperatureSetting(p, u));
        await run('Inputs', 'API Key Field Masked', (p, u) => SettingsInputTests.apiKeyFieldMasked(p, u));

        // Action Tests
        log.section('Settings Actions');
        await run('Actions', 'Save Button Exists', (p, u) => SettingsActionTests.saveButtonExists(p, u));
        await run('Actions', 'Reset Button Exists', (p, u) => SettingsActionTests.resetButtonExists(p, u));
        await run('Actions', 'Search Filter Exists', (p, u) => SettingsActionTests.searchFilterExists(p, u));

        // Status Tests
        log.section('Settings Status & APIs');
        await run('Status', 'Warnings Display', (p, u) => SettingsStatusTests.warningsDisplay(p, u));
        await run('Status', 'Ollama Status Indicator', (p, u) => SettingsStatusTests.ollamaStatusIndicator(p, u));
        await run('API', 'Available Models API', (p, u) => SettingsStatusTests.availableModelsApiWorks(p, u));
        await run('API', 'Available Search Engines API', (p, u) => SettingsStatusTests.availableSearchEnginesApiWorks(p, u));

    } catch (error) {
        log.error(`Fatal error: ${error.message}`);
        console.error(error.stack);
    } finally {
        results.print();
        results.save();
        await teardownTest(ctx);
        process.exit(results.exitCode());
    }
}

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

module.exports = { SettingsPageTests, SettingsInputTests, SettingsActionTests, SettingsStatusTests };
