#!/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) {
        // Assert Escape closes the open custom dropdown — the dropdown's input
        // keydown handler hides the list on Escape. The old test used Bootstrap
        // selectors (.dropdown-toggle / [data-toggle]) that don't exist on / in this
        // app, so it never opened anything; depending on timing it either skipped or
        // crashed with a navigation "Execution context was destroyed". We open the
        // real #search_engine dropdown instead.
        //
        // research.js re-runs the dropdown setup after the search-engine fetch and
        // each setup ends by hiding the dropdown, so wait for the network to settle
        // before opening (same race guard as the mobile dropdown test). The opened
        // list is reparented to <body>, so read it by id (#search-engine-dropdown-list).
        await navigateTo(page, `${baseUrl}/`);
        await page.waitForSelector('#search_engine', { timeout: 15000 });
        await page.waitForNetworkIdle({ idleTime: 500, timeout: 15000 }).catch(() => {});

        const listOpen = () => {
            const list = document.querySelector('#search-engine-dropdown-list');
            return !!list && window.getComputedStyle(list).display === 'block'
                && list.classList.contains('ldr-dropdown-active');
        };

        // Open the dropdown. #search_engine is the search-engine selector's <input>
        // (a custom dropdown); its options load without an LLM, so it is CI-safe.
        // Clicking the input both focuses it and opens the list.
        await page.click('#search_engine');
        await page.waitForFunction(listOpen, { timeout: 5000 }).catch(() => {});
        const opened = await page.evaluate(listOpen);
        if (!opened) {
            return { passed: false, message: 'Could not open #search_engine dropdown to test Escape' };
        }

        // Press Escape and assert it closes. The click above focused the #search_engine
        // <input>, and the Escape->hide handler is attached to that input's keydown
        // listener, so the keypress reaches it (verified: this assertion passes locally).
        await page.keyboard.press('Escape');
        await page.waitForFunction(() => {
            const list = document.querySelector('#search-engine-dropdown-list');
            return !list || window.getComputedStyle(list).display === 'none'
                || !list.classList.contains('ldr-dropdown-active');
        }, { timeout: 5000 }).catch(() => {});

        const stillOpen = await page.evaluate(listOpen);
        return {
            passed: !stillOpen,
            message: !stillOpen
                ? 'Escape closes the open custom dropdown'
                : 'Escape did not close the open dropdown'
        };
    },

    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) {
        // The Ctrl/Cmd+Enter "submit from anywhere" shortcut is a document-level
        // keydown handler that calls handleResearchSubmit (which POSTs to
        // /api/start_research) on ctrlKey||metaKey + Enter. The old test asserted the
        // URL changed (navigation), which needs an LLM — start_research returns 400
        // "Model is required" in the no-LLM shard, so it could never pass and was left
        // unregistered. Assert the shortcut fires the submit POST instead (made
        // regardless of the 400, so deterministic).
        //
        // We type the query, then BLUR the textarea before pressing Ctrl+Enter. This
        // isolates the modifier: the textarea's keydown handler also submits on PLAIN
        // Enter, so pressing Ctrl+Enter while focused there would pass even if the
        // Ctrl/Cmd-specific paths regressed. From <body>, only the document-level
        // Ctrl/Cmd+Enter handler can fire the submit. waitForRequest is armed before
        // the keypress to avoid a race.
        await navigateTo(page, `${baseUrl}/`);

        const textarea = await page.$('textarea[name="query"], textarea#query');
        if (!textarea) {
            return { passed: false, message: 'No query textarea found on /' };
        }
        await textarea.type('Test query for keyboard submission');
        // Move focus off the textarea so plain Enter can't submit — isolates Ctrl+Enter.
        await page.evaluate(() => document.activeElement?.blur());

        // A submit POST goes to /api/start_research (research mode) or
        // /api/chat/sessions (chat mode); either proves the shortcut submitted.
        const submitRequest = page.waitForRequest(
            (req) => req.method() === 'POST'
                && (req.url().includes('/api/start_research') || req.url().includes('/api/chat/sessions')),
            { timeout: 8000 }
        ).catch(() => null);

        await page.keyboard.down('Control');
        await page.keyboard.press('Enter');
        await page.keyboard.up('Control');

        const req = await submitRequest;
        const passed = !!req;
        return {
            passed,
            message: passed
                ? `Ctrl+Enter (from body) submits the form (POST ${new URL(req.url()).pathname})`
                : 'Ctrl+Enter did not trigger a submit request'
        };
    }
};

// ============================================================================
// 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);
            // Use the DISCRETE sub-properties. The old check compared style.outline
            // (the shorthand, e.g. "rgb(...) none 1px") to 'none' — never literally
            // equal once a colour is set — so it false-passed whenever outline-width
            // was non-zero even with outline-style: none (an invisible outline).
            const hasOutline = style.outlineStyle !== 'none' && parseFloat(style.outlineWidth) > 0;
            const hasBoxShadow = !!style.boxShadow && style.boxShadow !== 'none';
            const hasBorderChange = focused.classList.contains('focus') || focused.classList.contains('focused');
            // The app draws keyboard focus rings via :focus-visible (it suppresses the
            // mouse-focus outline) — a standard a11y pattern — so detect that directly.
            let focusVisible = false;
            try { focusVisible = focused.matches(':focus-visible'); } catch { /* unsupported engine */ }

            return {
                hasFocusedElement: true,
                tag: focused.tagName.toLowerCase(),
                hasOutline,
                hasBoxShadow,
                hasBorderChange,
                focusVisible,
                outline: `${style.outlineStyle} ${style.outlineWidth}`
            };
        });

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

        // A concrete drawn indicator (real outline / box-shadow / focus class) is a
        // trustworthy signal anywhere. The :focus-visible STATE alone means the app
        // *should* draw a ring, but the styling + :focus-visible engagement is
        // unreliable under headless/automation (the same reason the Tab/arrow/native
        // checks in this file skip in CI).
        const hasConcreteIndicator = result.hasOutline || result.hasBoxShadow || result.hasBorderChange;
        const detail = `<${result.tag}>: outline=${result.hasOutline}, boxShadow=${result.hasBoxShadow}, focus-visible=${result.focusVisible}, outline="${result.outline}"`;

        if (hasConcreteIndicator) {
            return { passed: true, message: `Focus indicator present ${detail}` };
        }
        // No drawn ring observed. In CI this is the automation limitation, not a proven
        // regression (the app's :focus-visible rings are real but not reliably observable
        // here) — skip rather than fail. Locally, accept the :focus-visible state as the
        // dev signal.
        if (process.env.CI) {
            return { passed: null, skipped: true, message: `No concrete focus indicator observable under automation ${detail}` };
        }
        return {
            passed: result.focusVisible,
            message: `Focus indicator ${result.focusVisible ? 'via :focus-visible state' : 'NOT found'} ${detail}`
        };
    },

    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) {
        // Semantic states (error / success / warning) must be visually distinguished
        // by colour. The old test queried .error/.success/.warning elements then
        // returned passed:true unconditionally — it never compared anything. Render an
        // alert of each type via the app's own alert function (window.showSafeAlert ->
        // #research-alert) and assert all three are PAIRWISE DISTINCT in text /
        // background / border colour. Deterministic and LLM-independent.
        await navigateTo(page, `${baseUrl}/`);
        await page.waitForSelector('#research-alert', { timeout: 15000 });

        const hasMechanism = await page.evaluate(() => typeof window.showSafeAlert === 'function');
        if (!hasMechanism) {
            return { passed: false, message: 'Alert mechanism (window.showSafeAlert) not loaded on /' };
        }

        const colorFor = (type) => page.evaluate((t) => {
            const a = document.querySelector('#research-alert');
            if (!a) return null;
            a.innerHTML = '';
            a.style.display = 'none';
            window.showSafeAlert('research-alert', `semantic-${t}`, t);
            const el = a.firstElementChild || a;
            const cs = window.getComputedStyle(el);
            return { color: cs.color, background: cs.backgroundColor, border: cs.borderColor };
        }, type);

        const err = await colorFor('error');
        const ok = await colorFor('success');
        const warn = await colorFor('warning');
        // Restore the container to its hidden/empty default so we don't leak a visible
        // alert into sibling tests that share this page.
        await page.evaluate(() => {
            const a = document.querySelector('#research-alert');
            if (a) { a.innerHTML = ''; a.style.display = 'none'; }
        });

        if (!err || !ok || !warn) {
            return { passed: false, message: 'Could not render semantic alerts to compare colours' };
        }

        // Distinct if any of text / background / border colour differs.
        const differ = (x, y) => x.color !== y.color || x.background !== y.background || x.border !== y.border;
        const errVsOk = differ(err, ok);
        const errVsWarn = differ(err, warn);
        const okVsWarn = differ(ok, warn);
        const passed = errVsOk && errVsWarn && okVsWarn;
        return {
            passed,
            message: passed
                ? `error/success/warning alerts all use distinct colours (bg error=${err.background}, success=${ok.background}, warning=${warn.background})`
                : `Semantic colours not all distinct (error≠success=${errVsOk}, error≠warning=${errVsWarn}, success≠warning=${okVsWarn})`
        };
    }
};

// ============================================================================
// 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));
        await run('Shortcuts', 'Ctrl+Enter Submits', (p, u) => KeyboardShortcutsTests.ctrlEnterSubmit(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 };
