#!/usr/bin/env node
/**
 * Keyboard & Accessibility UI Tests
 *
 * Tests for keyboard navigation, shortcuts, ARIA labels,
 * and accessibility features.
 *
 * Run: node test_keyboard_accessibility_ci.js
 */

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

// ============================================================================
// Keyboard Navigation Tests
// ============================================================================
const KeyboardNavigationTests = {
    async tabNavigationWorks(page, baseUrl) {
        // Use settings page — it has many tabbable elements (inputs, toggles, dropdowns)
        // The research page only has a textarea which traps tab focus
        await navigateTo(page, `${baseUrl}/settings`);
        // Wait for page to be fully loaded (all scripts executed) and interactive
        await page.waitForFunction(() => document.readyState === 'complete', { timeout: 10000 }).catch(() => {});
        await page.waitForSelector('input, select, button', { timeout: 5000 }).catch(() => {});
        // Click on the page first to establish keyboard focus in headless Chrome
        await page.click('body').catch(() => {});
        await delay(500);

        // Press Tab multiple times and check focus moves
        const focusedElements = [];

        for (let i = 0; i < 5; i++) {
            await page.keyboard.press('Tab');
            await delay(200);

            const focused = await page.evaluate(() => {
                const el = document.activeElement;
                return {
                    tag: el?.tagName?.toLowerCase(),
                    type: el?.type,
                    id: el?.id,
                    className: el?.className?.substring(0, 30)
                };
            });

            focusedElements.push(focused);
        }

        const uniqueFocused = new Set(focusedElements.map(e => `${e.tag}-${e.id || e.className}`));

        // In headless Chrome, Tab focus can be unreliable — skip instead of fail
        if (uniqueFocused.size <= 1 && process.env.CI) {
            return {
                passed: null,
                skipped: true,
                message: `Tab navigation unreliable in headless Chrome: ${focusedElements.map(e => e.tag).join(' -> ')}`
            };
        }

        return {
            passed: uniqueFocused.size > 1,
            message: `Tab navigation: ${uniqueFocused.size} unique elements focused (${focusedElements.map(e => e.tag).join(' -> ')})`
        };
    },

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

        // Open something that can be closed with Escape (like a dropdown or modal)
        await page.evaluate(() => {
            const dropdown = document.querySelector('.dropdown-toggle, [data-toggle="dropdown"]');
            if (dropdown) dropdown.click();

            const modal = document.querySelector('[data-toggle="modal"]');
            if (modal) modal.click();
        });

        await delay(300);

        const beforeEscape = await page.evaluate(() => {
            const openDropdown = document.querySelector('.dropdown.show, .dropdown-menu.show');
            const openModal = document.querySelector('.modal.show, .modal[style*="display: block"]');
            return {
                hasOpenDropdown: !!openDropdown,
                hasOpenModal: !!openModal
            };
        });

        // Press Escape
        await page.keyboard.press('Escape');
        await delay(300);

        const afterEscape = await page.evaluate(() => {
            const openDropdown = document.querySelector('.dropdown.show, .dropdown-menu.show');
            const openModal = document.querySelector('.modal.show, .modal[style*="display: block"]');
            return {
                hasOpenDropdown: !!openDropdown,
                hasOpenModal: !!openModal
            };
        });

        if (!beforeEscape.hasOpenDropdown && !beforeEscape.hasOpenModal) {
            return { passed: null, skipped: true, message: 'No dropdown/modal to test Escape key' };
        }

        const closed = (beforeEscape.hasOpenDropdown && !afterEscape.hasOpenDropdown) ||
                      (beforeEscape.hasOpenModal && !afterEscape.hasOpenModal);

        return {
            passed: closed,
            message: closed
                ? 'Escape key closes open elements'
                : 'Escape key did not close open elements'
        };
    },

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

        // Tab to first button
        for (let i = 0; i < 10; i++) {
            await page.keyboard.press('Tab');
            const isButton = await page.evaluate(() => {
                return document.activeElement?.tagName?.toLowerCase() === 'button' ||
                       document.activeElement?.tagName?.toLowerCase() === 'a';
            });
            if (isButton) break;
        }

        const focusedElement = await page.evaluate(() => ({
            tag: document.activeElement?.tagName?.toLowerCase(),
            text: document.activeElement?.textContent?.trim()?.substring(0, 30)
        }));

        if (focusedElement.tag !== 'button' && focusedElement.tag !== 'a') {
            return { passed: null, skipped: true, message: 'Could not focus on a button to test Enter key' };
        }

        return {
            passed: true,
            message: `Button "${focusedElement.text}" is focusable and can be activated with Enter`
        };
    },

    async arrowKeysInDropdowns(page, baseUrl) {
        // Native <select> elements don't consistently respond to arrow keys
        // in headless Chrome — this is a known limitation, not an app bug
        if (process.env.CI) {
            return { passed: null, skipped: true, message: 'Native select arrow keys unreliable in headless Chrome' };
        }

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

        // Find and focus a select element
        const selectInfo = await page.evaluate(() => {
            const select = document.querySelector('select');
            if (select) {
                select.focus();
                return {
                    exists: true,
                    optionsCount: select.options.length,
                    selectedIndex: select.selectedIndex
                };
            }
            return { exists: false };
        });

        if (!selectInfo.exists) {
            return { passed: null, skipped: true, message: 'No select element to test arrow keys' };
        }

        if (selectInfo.optionsCount <= 1) {
            return { passed: null, skipped: true, message: 'Select has only one option' };
        }

        const beforeIndex = selectInfo.selectedIndex;

        // If at last item, use ArrowUp; otherwise use ArrowDown
        const useArrowUp = beforeIndex >= selectInfo.optionsCount - 1;
        const keyToPress = useArrowUp ? 'ArrowUp' : 'ArrowDown';

        await page.keyboard.press(keyToPress);
        await delay(100);

        const afterIndex = await page.evaluate(() => {
            return document.activeElement?.selectedIndex;
        });

        return {
            passed: beforeIndex !== afterIndex,
            message: `Arrow keys in dropdown: index ${beforeIndex} -> ${afterIndex} (${keyToPress})`
        };
    }
};

// ============================================================================
// Keyboard Shortcuts Tests
// ============================================================================
const KeyboardShortcutsTests = {
    async shortcutsDocumented(page, baseUrl) {
        await navigateTo(page, `${baseUrl}/`);

        const result = await page.evaluate(() => {
            const pageText = document.body.textContent.toLowerCase();

            return {
                hasCtrl: pageText.includes('ctrl+') || pageText.includes('ctrl +'),
                hasAlt: pageText.includes('alt+') || pageText.includes('alt +'),
                hasShift: pageText.includes('shift+') || pageText.includes('shift +'),
                hasEsc: pageText.includes('esc') || pageText.includes('escape'),
                hasEnter: pageText.includes('enter'),
                hasShortcutHelp: document.querySelector('.shortcuts, .keyboard-shortcuts, [class*="shortcut"]') !== null
            };
        });

        const hasDocumentation = result.hasCtrl || result.hasAlt || result.hasShift ||
                                result.hasEsc || result.hasEnter || result.hasShortcutHelp;

        if (!hasDocumentation) {
            return { passed: null, skipped: true, message: 'No keyboard shortcuts documentation found' };
        }

        return {
            passed: true,
            message: `Shortcuts documented: Ctrl=${result.hasCtrl}, Alt=${result.hasAlt}, Esc=${result.hasEsc}, Enter=${result.hasEnter}`
        };
    },

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

        // Focus on textarea
        const textarea = await page.$('textarea[name="query"], textarea#query');
        if (!textarea) {
            return { passed: null, skipped: true, message: 'No textarea to test Ctrl+Enter' };
        }

        await textarea.type('Test query');

        const beforeUrl = page.url();

        // Press Ctrl+Enter
        await page.keyboard.down('Control');
        await page.keyboard.press('Enter');
        await page.keyboard.up('Control');

        await delay(1000);

        const afterUrl = page.url();

        return {
            passed: beforeUrl !== afterUrl,
            message: beforeUrl !== afterUrl
                ? 'Ctrl+Enter submits form'
                : 'Ctrl+Enter did not submit form'
        };
    }
};

// ============================================================================
// Focus Management Tests
// ============================================================================
const FocusManagementTests = {
    async focusIndicatorVisible(page, baseUrl) {
        await navigateTo(page, `${baseUrl}/`);

        // Tab to an element
        await page.keyboard.press('Tab');
        await delay(100);

        const result = await page.evaluate(() => {
            const focused = document.activeElement;
            if (!focused || focused === document.body) return { hasFocusedElement: false };

            const style = window.getComputedStyle(focused);
            const outlineStyle = style.outline;
            const outlineWidth = style.outlineWidth;
            const boxShadow = style.boxShadow;

            // Check for focus styles
            const hasOutline = outlineWidth !== '0px' && outlineStyle !== 'none';
            const hasBoxShadow = boxShadow !== 'none';
            const hasBorderChange = focused.classList.contains('focus') || focused.classList.contains('focused');

            return {
                hasFocusedElement: true,
                tag: focused.tagName,
                hasOutline,
                hasBoxShadow,
                hasBorderChange,
                outline: outlineStyle
            };
        });

        if (!result.hasFocusedElement) {
            return { passed: null, skipped: true, message: 'No element received focus' };
        }

        const hasFocusIndicator = result.hasOutline || result.hasBoxShadow || result.hasBorderChange;

        return {
            passed: hasFocusIndicator,
            message: `Focus indicator: outline=${result.hasOutline}, boxShadow=${result.hasBoxShadow}, outline="${result.outline}"`
        };
    },

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

        // Try to open a modal
        const opened = await page.evaluate(() => {
            const modalTrigger = document.querySelector('[data-toggle="modal"], [data-bs-toggle="modal"], .open-modal');
            if (modalTrigger) {
                modalTrigger.click();
                return true;
            }
            return false;
        });

        if (!opened) {
            return { passed: null, skipped: true, message: 'No modal to test focus trap' };
        }

        await delay(500);

        // Tab multiple times and check if focus stays in modal
        const focusedElements = [];
        for (let i = 0; i < 10; i++) {
            await page.keyboard.press('Tab');
            await delay(50);

            const inModal = await page.evaluate(() => {
                const focused = document.activeElement;
                const modal = document.querySelector('.modal.show, .modal[style*="display: block"], [role="dialog"]');
                return modal?.contains(focused) ?? false;
            });

            focusedElements.push(inModal);
        }

        const allInModal = focusedElements.every(x => x);

        return {
            passed: allInModal,
            message: allInModal
                ? 'Focus trap works in modal'
                : `Focus escaped modal (${focusedElements.filter(x => x).length}/10 in modal)`
        };
    },

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

        const result = await page.evaluate(() => {
            const skipLink = document.querySelector(
                'a[href="#main"], ' +
                'a[href="#content"], ' +
                '.skip-link, ' +
                '.skip-to-content, ' +
                'a.visually-hidden, ' +
                'a.sr-only'
            );

            return {
                hasSkipLink: !!skipLink,
                href: skipLink?.href,
                text: skipLink?.textContent?.trim()
            };
        });

        if (!result.hasSkipLink) {
            return { passed: null, skipped: true, message: 'No skip to content link found' };
        }

        return {
            passed: true,
            message: `Skip link: "${result.text}" -> ${result.href}`
        };
    }
};

// ============================================================================
// ARIA Labels Tests
// ============================================================================
const AriaLabelsTests = {
    async buttonsHaveLabels(page, baseUrl) {
        await navigateTo(page, `${baseUrl}/`);

        const result = await page.evaluate(() => {
            const buttons = document.querySelectorAll('button, [role="button"]');
            let labeled = 0;
            let unlabeled = 0;
            const unlabeledButtons = [];

            buttons.forEach(btn => {
                const hasText = btn.textContent?.trim().length > 0;
                const hasAriaLabel = btn.hasAttribute('aria-label');
                const hasAriaLabelledBy = btn.hasAttribute('aria-labelledby');
                const hasTitle = btn.hasAttribute('title');

                if (hasText || hasAriaLabel || hasAriaLabelledBy || hasTitle) {
                    labeled++;
                } else {
                    unlabeled++;
                    unlabeledButtons.push(btn.className.substring(0, 30));
                }
            });

            return {
                total: buttons.length,
                labeled,
                unlabeled,
                unlabeledExamples: unlabeledButtons.slice(0, 3)
            };
        });

        const percentLabeled = result.total > 0 ? Math.round((result.labeled / result.total) * 100) : 100;

        return {
            passed: percentLabeled >= 80,
            message: `Buttons labeled: ${result.labeled}/${result.total} (${percentLabeled}%), unlabeled: ${result.unlabeledExamples.join(', ')}`
        };
    },

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

        const result = await page.evaluate(() => {
            const images = document.querySelectorAll('img');
            let withAlt = 0;
            let withoutAlt = 0;
            const missingAlt = [];

            images.forEach(img => {
                if (img.hasAttribute('alt')) {
                    withAlt++;
                } else {
                    withoutAlt++;
                    missingAlt.push(img.src?.split('/').pop()?.substring(0, 20));
                }
            });

            return {
                total: images.length,
                withAlt,
                withoutAlt,
                missingExamples: missingAlt.slice(0, 3)
            };
        });

        if (result.total === 0) {
            return { passed: null, skipped: true, message: 'No images found on page' };
        }

        const percentWithAlt = Math.round((result.withAlt / result.total) * 100);

        return {
            passed: percentWithAlt >= 80,
            message: `Images with alt: ${result.withAlt}/${result.total} (${percentWithAlt}%), missing: ${result.missingExamples.join(', ')}`
        };
    },

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

        const result = await page.evaluate(() => {
            const inputs = document.querySelectorAll('input:not([type="hidden"]):not([type="submit"]), textarea, select');
            let labeled = 0;
            let unlabeled = 0;
            const unlabeledInputs = [];

            inputs.forEach(input => {
                const id = input.id;
                const hasLabelFor = id && document.querySelector(`label[for="${id}"]`);
                const hasAriaLabel = input.hasAttribute('aria-label');
                const hasAriaLabelledBy = input.hasAttribute('aria-labelledby');
                const hasPlaceholder = input.hasAttribute('placeholder');
                const wrappedInLabel = input.closest('label');

                if (hasLabelFor || hasAriaLabel || hasAriaLabelledBy || wrappedInLabel) {
                    labeled++;
                } else {
                    unlabeled++;
                    unlabeledInputs.push(input.name || input.id || input.type);
                }
            });

            return {
                total: inputs.length,
                labeled,
                unlabeled,
                unlabeledExamples: unlabeledInputs.slice(0, 3)
            };
        });

        if (result.total === 0) {
            return { passed: null, skipped: true, message: 'No form inputs found' };
        }

        const percentLabeled = Math.round((result.labeled / result.total) * 100);

        return {
            passed: percentLabeled >= 70,
            message: `Inputs labeled: ${result.labeled}/${result.total} (${percentLabeled}%), unlabeled: ${result.unlabeledExamples.join(', ')}`
        };
    },

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

        const result = await page.evaluate(() => {
            const roles = {
                main: document.querySelectorAll('[role="main"], main').length,
                navigation: document.querySelectorAll('[role="navigation"], nav').length,
                button: document.querySelectorAll('[role="button"]').length,
                dialog: document.querySelectorAll('[role="dialog"]').length,
                alert: document.querySelectorAll('[role="alert"]').length,
                tab: document.querySelectorAll('[role="tab"]').length,
                tabpanel: document.querySelectorAll('[role="tabpanel"]').length
            };

            return roles;
        });

        const totalRoles = Object.values(result).reduce((a, b) => a + b, 0);
        const rolesList = Object.entries(result).filter(([k, v]) => v > 0).map(([k, v]) => `${k}:${v}`);

        return {
            passed: totalRoles > 0,
            message: `ARIA roles: ${rolesList.join(', ') || 'none found'}`
        };
    }
};

// ============================================================================
// Color and Contrast Tests
// ============================================================================
const ColorContrastTests = {
    async semanticColors(page, baseUrl) {
        await navigateTo(page, `${baseUrl}/`);

        const result = await page.evaluate(() => {
            const errorElements = document.querySelectorAll('.error, .danger, .alert-danger, [class*="error"]');
            const successElements = document.querySelectorAll('.success, .alert-success, [class*="success"]');
            const warningElements = document.querySelectorAll('.warning, .alert-warning, [class*="warning"]');

            return {
                errorCount: errorElements.length,
                successCount: successElements.length,
                warningCount: warningElements.length
            };
        });

        return {
            passed: true,
            message: `Semantic colors: error=${result.errorCount}, success=${result.successCount}, warning=${result.warningCount}`
        };
    }
};

// ============================================================================
// Main Test Runner
// ============================================================================
async function main() {
    log.section('Keyboard & Accessibility Tests');

    const ctx = await setupTest({ authenticate: true });
    const results = new TestResults('Keyboard Accessibility 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 {
        // Keyboard Navigation
        log.section('Keyboard Navigation');

        await run('Keyboard', 'Tab Navigation', (p, u) => KeyboardNavigationTests.tabNavigationWorks(p, u));
        await run('Keyboard', 'Escape Key', (p, u) => KeyboardNavigationTests.escapeKeyFunction(p, u));
        await run('Keyboard', 'Enter on Buttons', (p, u) => KeyboardNavigationTests.enterKeyOnButtons(p, u));
        await run('Keyboard', 'Arrow Keys', (p, u) => KeyboardNavigationTests.arrowKeysInDropdowns(p, u));

        // Keyboard Shortcuts
        log.section('Keyboard Shortcuts');

        await run('Shortcuts', 'Documented', (p, u) => KeyboardShortcutsTests.shortcutsDocumented(p, u));

        // Focus Management
        log.section('Focus Management');

        await run('Focus', 'Indicator Visible', (p, u) => FocusManagementTests.focusIndicatorVisible(p, u));
        await run('Focus', 'Trap in Modals', (p, u) => FocusManagementTests.focusTrapInModals(p, u));
        await run('Focus', 'Skip Link', (p, u) => FocusManagementTests.skipToContentLink(p, u));

        // ARIA Labels
        log.section('ARIA Labels');

        await run('ARIA', 'Buttons Labeled', (p, u) => AriaLabelsTests.buttonsHaveLabels(p, u));
        await run('ARIA', 'Images Alt Text', (p, u) => AriaLabelsTests.imagesHaveAltText(p, u));
        await run('ARIA', 'Inputs Labeled', (p, u) => AriaLabelsTests.formInputsHaveLabels(p, u));
        await run('ARIA', 'Roles Present', (p, u) => AriaLabelsTests.ariaRolesPresent(p, u));

        // Color Contrast
        log.section('Color & Contrast');

        await run('Color', 'Semantic Colors', (p, u) => ColorContrastTests.semanticColors(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 = { KeyboardNavigationTests, KeyboardShortcutsTests, FocusManagementTests, AriaLabelsTests, ColorContrastTests };
