#!/usr/bin/env node
/**
 * Form Validation ARIA Support Tests
 *
 * Tests the FormValidator utility on the research form:
 * - Inline error display with ldr-field-invalid class
 * - aria-invalid attribute toggling
 * - aria-describedby linking to error element
 * - aria-required on query textarea
 * - Error clears after typing valid input
 * - Validation fires on blur
 *
 * Run: node test_form_validation_aria_ci.js
 */

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

// Helpers used by the validation tests below. We wait on real DOM state instead
// of fixed delays so the tests stay correct on slow CI runners without padding
// the wall-clock time on fast ones.
async function waitForQueryReady(page, timeout = 5000) {
    await page.waitForSelector('#query', { visible: true, timeout });
    // The FormValidator hook is what actually gates "loading UI shown / not shown"
    // on submit. addValidation() inserts a .ldr-field-error sibling next to #query,
    // so we wait for that DOM marker — it proves the validator is registered, not
    // just that the class is on window.
    await page.waitForFunction(
        () => {
            const q = document.querySelector('#query');
            if (!q || typeof window.FormValidator !== 'function') return false;
            const sibling = q.nextElementSibling;
            return !!sibling && sibling.classList.contains('ldr-field-error');
        },
        { timeout }
    ).catch(() => {});
}
async function waitForValidationState(page, expectInvalid, timeout = 2000) {
    await page.waitForFunction(
        (wantInvalid) => {
            const q = document.querySelector('#query');
            if (!q) return false;
            const isInvalid = q.classList.contains('ldr-field-invalid');
            return wantInvalid ? isInvalid : !isInvalid;
        },
        { timeout },
        expectInvalid
    ).catch(() => {});
}

// ============================================================================
// ARIA Attribute Tests
// ============================================================================
const AriaAttributeTests = {
    async queryHasAriaRequired(page, baseUrl) {
        await navigateTo(page, `${baseUrl}/`);

        const result = await page.evaluate(() => {
            const query = document.querySelector('#query');
            return {
                exists: !!query,
                ariaRequired: query?.getAttribute('aria-required'),
                hasHtmlRequired: query?.hasAttribute('required')
            };
        });

        if (!result.exists) {
            return { passed: null, skipped: true, message: 'Query textarea not found' };
        }

        return {
            passed: result.ariaRequired === 'true' && !result.hasHtmlRequired,
            message: `aria-required="${result.ariaRequired}", HTML required=${result.hasHtmlRequired}`
        };
    },

    async formValidatorLoaded(page, baseUrl) {
        await navigateTo(page, `${baseUrl}/`);
        // Wait for the FormValidator module to actually attach instead of a fixed delay.
        await page.waitForFunction(
            () => typeof window.FormValidator === 'function' &&
                  typeof window.formValidators === 'object' &&
                  window.formValidators !== null,
            { timeout: 5000 }
        ).catch(() => {});

        const result = await page.evaluate(() => {
            return {
                hasFormValidator: typeof window.FormValidator === 'function',
                hasFormValidators: typeof window.formValidators === 'object' && window.formValidators !== null
            };
        });

        return {
            passed: result.hasFormValidator && result.hasFormValidators,
            message: `FormValidator=${result.hasFormValidator}, formValidators=${result.hasFormValidators}`
        };
    }
};

// ============================================================================
// Inline Validation Tests
// ============================================================================
const InlineValidationTests = {
    async emptySubmitShowsInlineError(page, baseUrl) {
        await navigateTo(page, `${baseUrl}/`);
        await waitForQueryReady(page);

        // Clear query and submit
        await page.evaluate(() => {
            const query = document.querySelector('#query');
            if (query) {
                query.value = '';
                query.dispatchEvent(new Event('input', { bubbles: true }));
            }
        });

        // Click submit
        await page.click('button[type="submit"], .start-research, #start-research').catch(() => {});
        await waitForValidationState(page, true);

        const result = await page.evaluate(() => {
            const query = document.querySelector('#query');
            if (!query) return { exists: false };

            const hasInvalidClass = query.classList.contains('ldr-field-invalid');
            const ariaInvalid = query.getAttribute('aria-invalid');

            // Find the error element
            const errorId = query.getAttribute('aria-describedby');
            let errorElement = null;
            let errorText = '';
            let errorVisible = false;

            if (errorId) {
                // aria-describedby may contain multiple IDs
                const ids = errorId.split(' ');
                for (const id of ids) {
                    const el = document.getElementById(id);
                    if (el && el.classList.contains('ldr-field-error')) {
                        errorElement = el;
                        errorText = el.textContent;
                        errorVisible = window.getComputedStyle(el).display !== 'none';
                        break;
                    }
                }
            }

            return {
                exists: true,
                hasInvalidClass,
                ariaInvalid,
                hasErrorElement: !!errorElement,
                errorText,
                errorVisible
            };
        });

        if (!result.exists) {
            return { passed: null, skipped: true, message: 'Query textarea not found' };
        }

        const passed = result.hasInvalidClass &&
                       result.ariaInvalid === 'true' &&
                       result.hasErrorElement &&
                       result.errorVisible &&
                       result.errorText.length > 0;

        return {
            passed,
            message: `invalid-class=${result.hasInvalidClass}, aria-invalid="${result.ariaInvalid}", ` +
                     `error-visible=${result.errorVisible}, error="${result.errorText}"`
        };
    },

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

        // Clear query field
        await page.evaluate(() => {
            const query = document.querySelector('#query');
            if (query) {
                query.value = '';
                query.dispatchEvent(new Event('input', { bubbles: true }));
            }
        });

        // Focus then blur the query field
        await page.focus('#query');
        await page.waitForFunction(() => document.activeElement?.id === 'query', { timeout: 1000 }).catch(() => {});
        // Click somewhere else to trigger blur
        await page.$eval('#query', el => el.blur());
        await waitForValidationState(page, true);

        const result = await page.evaluate(() => {
            const query = document.querySelector('#query');
            if (!query) return { exists: false };

            return {
                exists: true,
                hasInvalidClass: query.classList.contains('ldr-field-invalid'),
                ariaInvalid: query.getAttribute('aria-invalid')
            };
        });

        if (!result.exists) {
            return { passed: null, skipped: true, message: 'Query textarea not found' };
        }

        return {
            passed: result.hasInvalidClass && result.ariaInvalid === 'true',
            message: `After blur on empty: invalid-class=${result.hasInvalidClass}, aria-invalid="${result.ariaInvalid}"`
        };
    },

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

        // First trigger the error by submitting empty
        await page.evaluate(() => {
            const query = document.querySelector('#query');
            if (query) {
                query.value = '';
                query.dispatchEvent(new Event('input', { bubbles: true }));
            }
        });
        await page.click('button[type="submit"], .start-research, #start-research').catch(() => {});
        await waitForValidationState(page, true);

        // Verify error is shown
        const errorShown = await page.evaluate(() => {
            const query = document.querySelector('#query');
            return query?.classList.contains('ldr-field-invalid') === true;
        });

        if (!errorShown) {
            return { passed: null, skipped: true, message: 'Could not trigger validation error first' };
        }

        // Now type a valid query - this should clear the error on next submit attempt
        await page.focus('#query');
        await page.keyboard.type('Test research query');
        // Wait for the typed value to land in the field instead of a fixed sleep.
        await page.waitForFunction(
            () => (document.querySelector('#query')?.value || '').includes('Test research'),
            { timeout: 2000 }
        ).catch(() => {});

        // Verify the text was actually entered
        const queryValue = await page.evaluate(() => {
            const query = document.querySelector('#query');
            return query?.value || '';
        });

        if (!queryValue.includes('Test research')) {
            return { passed: null, skipped: true, message: `Text entry failed, value="${queryValue}"` };
        }

        // Intercept form submission to prevent navigation that would destroy page state
        await page.evaluate(() => {
            const form = document.querySelector('#query')?.closest('form');
            if (form) {
                form.addEventListener('submit', (e) => { e.preventDefault(); }, { once: true });
            }
        });

        // Click submit - the validation should pass and clear errors
        await page.click('button[type="submit"], .start-research, #start-research').catch(() => {});
        // Wait for the ldr-field-invalid class to clear instead of a fixed sleep.
        await waitForValidationState(page, false);

        const result = await page.evaluate(() => {
            const query = document.querySelector('#query');
            if (!query) return { exists: false };

            return {
                exists: true,
                hasInvalidClass: query.classList.contains('ldr-field-invalid'),
                ariaInvalid: query.getAttribute('aria-invalid')
            };
        });

        if (!result.exists) {
            return { passed: null, skipped: true, message: 'Query textarea not found after submit' };
        }

        return {
            passed: !result.hasInvalidClass && result.ariaInvalid !== 'true',
            message: `After valid submit: invalid-class=${result.hasInvalidClass}, aria-invalid="${result.ariaInvalid}"`
        };
    },

    async noLoadingUiOnEmptySubmit(page, baseUrl) {
        // This test asserts no loading UI appears on an empty submit. A previous test
        // leaves the page on `/` (navigateTo skips when paths match) with a stale
        // .ldr-loading-overlay from a half-finished real submission. We need a
        // genuinely fresh page here, so force a reload via page.goto instead of
        // relying on the same-path optimization.
        await page.goto(`${baseUrl}/`, { waitUntil: 'domcontentloaded' });
        await waitForQueryReady(page);

        // Clear query
        await page.evaluate(() => {
            const query = document.querySelector('#query');
            if (query) {
                query.value = '';
                query.dispatchEvent(new Event('input', { bubbles: true }));
            }
        });

        // Submit and immediately check for loading overlay
        await page.click('button[type="submit"], .start-research, #start-research').catch(() => {});

        // Check immediately - there should be no loading overlay or spinner
        const result = await page.evaluate(() => {
            const overlay = document.querySelector('.ldr-loading-overlay');
            const btn = document.querySelector('button[type="submit"], .start-research, #start-research');
            const btnDisabled = btn?.disabled;
            const btnHasSpinner = btn?.innerHTML?.includes('fa-spinner');

            return {
                hasOverlay: !!overlay,
                btnDisabled: !!btnDisabled,
                btnHasSpinner: !!btnHasSpinner
            };
        });

        return {
            passed: !result.hasOverlay && !result.btnDisabled && !result.btnHasSpinner,
            message: `On empty submit: overlay=${result.hasOverlay}, btn-disabled=${result.btnDisabled}, spinner=${result.btnHasSpinner}`
        };
    }
};

// ============================================================================
// Error Element Structure Tests
// ============================================================================
const ErrorElementTests = {
    async errorElementHasAriaLive(page, baseUrl) {
        await navigateTo(page, `${baseUrl}/`);
        await waitForQueryReady(page);

        // Trigger validation to create error element
        await page.evaluate(() => {
            const query = document.querySelector('#query');
            if (query) {
                query.value = '';
                query.dispatchEvent(new Event('input', { bubbles: true }));
            }
        });
        await page.focus('#query');
        await page.waitForFunction(() => document.activeElement?.id === 'query', { timeout: 1000 }).catch(() => {});
        await page.$eval('#query', el => el.blur());
        await waitForValidationState(page, true);

        const result = await page.evaluate(() => {
            const query = document.querySelector('#query');
            if (!query) return { exists: false };

            const describedBy = query.getAttribute('aria-describedby');
            if (!describedBy) return { exists: true, hasDescribedBy: false };

            const ids = describedBy.split(' ');
            for (const id of ids) {
                const el = document.getElementById(id);
                if (el && el.classList.contains('ldr-field-error')) {
                    return {
                        exists: true,
                        hasDescribedBy: true,
                        errorId: id,
                        ariaLive: el.getAttribute('aria-live'),
                        className: el.className
                    };
                }
            }

            return { exists: true, hasDescribedBy: true, errorElementFound: false };
        });

        if (!result.exists) {
            return { passed: null, skipped: true, message: 'Query textarea not found' };
        }

        if (!result.hasDescribedBy) {
            return { passed: false, message: 'No aria-describedby on query field' };
        }

        return {
            passed: result.ariaLive === 'polite',
            message: `Error element: id="${result.errorId}", aria-live="${result.ariaLive}", class="${result.className}"`
        };
    },

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

        // Trigger validation
        await page.evaluate(() => {
            const query = document.querySelector('#query');
            if (query) query.value = '';
        });
        await page.focus('#query');
        await page.waitForFunction(() => document.activeElement?.id === 'query', { timeout: 1000 }).catch(() => {});
        await page.$eval('#query', el => el.blur());
        await waitForValidationState(page, true);

        const result = await page.evaluate(() => {
            const query = document.querySelector('#query');
            if (!query) return { exists: false };

            const nextSibling = query.nextElementSibling;
            const isErrorElement = nextSibling?.classList.contains('ldr-field-error');
            const searchHints = query.parentNode?.querySelector('.ldr-search-hints');

            return {
                exists: true,
                nextSiblingIsError: isErrorElement,
                nextSiblingClass: nextSibling?.className,
                hintsExist: !!searchHints
            };
        });

        if (!result.exists) {
            return { passed: null, skipped: true, message: 'Query textarea not found' };
        }

        return {
            passed: result.nextSiblingIsError,
            message: `Next sibling after textarea: class="${result.nextSiblingClass}", hints exist=${result.hintsExist}`
        };
    },

    async focusMovesToFieldOnError(page, baseUrl) {
        // Headless Chrome focus management after form submission is unreliable
        if (process.env.CI) {
            return { passed: null, skipped: true, message: 'CI: headless Chrome focus management after form submit is unreliable' };
        }

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

        // Clear and submit
        await page.evaluate(() => {
            const query = document.querySelector('#query');
            if (query) {
                query.value = '';
                query.dispatchEvent(new Event('input', { bubbles: true }));
                // Focus something else first
                document.body.focus();
            }
        });

        await page.click('button[type="submit"], .start-research, #start-research').catch(() => {});
        await waitForValidationState(page, true);

        const result = await page.evaluate(() => {
            const activeElement = document.activeElement;
            return {
                focusedId: activeElement?.id,
                focusedTag: activeElement?.tagName?.toLowerCase()
            };
        });

        return {
            passed: result.focusedId === 'query',
            message: `Focus after empty submit: id="${result.focusedId}", tag="${result.focusedTag}"`
        };
    }
};

// ============================================================================
// Main Test Runner
// ============================================================================
async function main() {
    log.section('Form Validation ARIA Support Tests');

    const ctx = await setupTest({ authenticate: true });
    const results = new TestResults('Form Validation ARIA 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 {
        // ARIA Attribute Tests
        log.section('ARIA Attributes');

        await run('ARIA', 'Query has aria-required', (p, u) => AriaAttributeTests.queryHasAriaRequired(p, u));
        await run('ARIA', 'FormValidator loaded', (p, u) => AriaAttributeTests.formValidatorLoaded(p, u));

        // Inline Validation Tests
        log.section('Inline Validation');

        await run('Validation', 'Empty submit shows inline error', (p, u) => InlineValidationTests.emptySubmitShowsInlineError(p, u));
        await run('Validation', 'Blur on empty shows error', (p, u) => InlineValidationTests.blurOnEmptyShowsError(p, u));
        await run('Validation', 'Error clears on valid submit', (p, u) => InlineValidationTests.errorClearsOnValidSubmit(p, u));
        await run('Validation', 'No loading UI on empty submit', (p, u) => InlineValidationTests.noLoadingUiOnEmptySubmit(p, u));

        // Error Element Structure Tests
        log.section('Error Element Structure');

        await run('Structure', 'Error element has aria-live=polite', (p, u) => ErrorElementTests.errorElementHasAriaLive(p, u));
        await run('Structure', 'Error positioned after textarea', (p, u) => ErrorElementTests.errorPositionedAfterTextarea(p, u));
        await run('Structure', 'Focus moves to field on error', (p, u) => ErrorElementTests.focusMovesToFieldOnError(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());
    }
}

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

module.exports = { AriaAttributeTests, InlineValidationTests, ErrorElementTests };
