#!/usr/bin/env node
/**
 * Loading & Feedback UI Tests
 *
 * Tests for loading states, spinners, toasts, success/error messages,
 * and visual feedback mechanisms.
 *
 * Run: node test_loading_feedback_ci.js
 */

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

// ============================================================================
// Loading States Tests
// ============================================================================
const LoadingStatesTests = {
    async loadingSpinnersExist(page, baseUrl) {
        await navigateTo(page, `${baseUrl}/`);

        const result = await page.evaluate(() => {
            // Check CSS for spinner definitions
            const styleSheets = Array.from(document.styleSheets);
            let hasSpinnerCSS = false;

            try {
                for (const sheet of styleSheets) {
                    if (sheet.cssRules) {
                        for (const rule of sheet.cssRules) {
                            const cssText = rule.cssText?.toLowerCase() || '';
                            if (cssText.includes('spinner') || cssText.includes('loading') ||
                                cssText.includes('@keyframes') && (cssText.includes('spin') || cssText.includes('rotate'))) {
                                hasSpinnerCSS = true;
                                break;
                            }
                        }
                    }
                }
            } catch {
                // Cross-origin stylesheets will throw
            }

            // Check for spinner elements in DOM
            const spinnerElements = document.querySelectorAll(
                '.spinner, ' +
                '.loader, ' +
                '.loading, ' +
                '[class*="spinner"], ' +
                '[class*="loader"], ' +
                '.spinner-border, ' +
                '.spinner-grow'
            );

            return {
                hasSpinnerCSS,
                spinnerCount: spinnerElements.length
            };
        });

        const hasSpinners = result.hasSpinnerCSS || result.spinnerCount > 0;

        return {
            passed: hasSpinners,
            message: `Spinners: CSS=${result.hasSpinnerCSS}, elements=${result.spinnerCount}`
        };
    },

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

        const result = await page.evaluate(() => {
            const skeletons = document.querySelectorAll(
                '.skeleton, ' +
                '.placeholder, ' +
                '[class*="skeleton"], ' +
                '[class*="placeholder"], ' +
                '.loading-placeholder'
            );

            // Check CSS for skeleton animations
            const styleSheets = Array.from(document.styleSheets);
            let hasSkeletonCSS = false;

            try {
                for (const sheet of styleSheets) {
                    if (sheet.cssRules) {
                        for (const rule of sheet.cssRules) {
                            const cssText = rule.cssText?.toLowerCase() || '';
                            if (cssText.includes('skeleton') || cssText.includes('placeholder') ||
                                cssText.includes('shimmer') || cssText.includes('pulse')) {
                                hasSkeletonCSS = true;
                                break;
                            }
                        }
                    }
                }
            } catch {
                // Cross-origin stylesheets will throw
            }

            return {
                hasSkeletonCSS,
                skeletonCount: skeletons.length
            };
        });

        if (!result.hasSkeletonCSS && result.skeletonCount === 0) {
            return { passed: null, skipped: true, message: 'No skeleton loaders found' };
        }

        return {
            passed: true,
            message: `Skeleton loaders: CSS=${result.hasSkeletonCSS}, elements=${result.skeletonCount}`
        };
    },

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

        const result = await page.evaluate(() => {
            const buttons = document.querySelectorAll('button[type="submit"], .submit-btn, .btn-primary');

            // Check for loading state classes/attributes in button
            let hasLoadingState = false;
            let loadingClass = '';

            for (const btn of buttons) {
                const classes = btn.className;
                const dataset = btn.dataset;

                if (classes.includes('loading') || dataset.loading || btn.hasAttribute('disabled')) {
                    hasLoadingState = true;
                    loadingClass = classes;
                    break;
                }
            }

            // Check CSS for button loading styles
            const styleSheets = Array.from(document.styleSheets);
            let hasButtonLoadingCSS = false;

            try {
                for (const sheet of styleSheets) {
                    if (sheet.cssRules) {
                        for (const rule of sheet.cssRules) {
                            const cssText = rule.cssText?.toLowerCase() || '';
                            if ((cssText.includes('button') || cssText.includes('.btn')) &&
                                (cssText.includes('loading') || cssText.includes('disabled'))) {
                                hasButtonLoadingCSS = true;
                                break;
                            }
                        }
                    }
                }
            } catch {
                // Cross-origin stylesheets will throw
            }

            return {
                buttonCount: buttons.length,
                hasLoadingState,
                hasButtonLoadingCSS
            };
        });

        return {
            passed: result.hasLoadingState || result.hasButtonLoadingCSS,
            message: `Button loading: ${result.buttonCount} buttons, state=${result.hasLoadingState}, CSS=${result.hasButtonLoadingCSS}`
        };
    }
};

// ============================================================================
// Toast Notifications Tests
// ============================================================================
const ToastNotificationsTests = {
    async toastContainerExists(page, baseUrl) {
        await navigateTo(page, `${baseUrl}/`);

        const result = await page.evaluate(() => {
            const toastContainer = document.querySelector(
                '.toast-container, ' +
                '.toasts, ' +
                '#toast-container, ' +
                '[class*="toast-wrapper"], ' +
                '.notification-container'
            );

            // Check for toast CSS
            const styleSheets = Array.from(document.styleSheets);
            let hasToastCSS = false;

            try {
                for (const sheet of styleSheets) {
                    if (sheet.cssRules) {
                        for (const rule of sheet.cssRules) {
                            const cssText = rule.cssText?.toLowerCase() || '';
                            if (cssText.includes('.toast') || cssText.includes('notification')) {
                                hasToastCSS = true;
                                break;
                            }
                        }
                    }
                }
            } catch {
                // Cross-origin stylesheets will throw
            }

            return {
                hasContainer: !!toastContainer,
                hasToastCSS
            };
        });

        if (!result.hasContainer && !result.hasToastCSS) {
            return { passed: null, skipped: true, message: 'No toast notification system found' };
        }

        return {
            passed: true,
            message: `Toast system: container=${result.hasContainer}, CSS=${result.hasToastCSS}`
        };
    },

    async flashMessagesDisplayed(page, baseUrl) {
        // The app shows flash/alert messages by routing showAlert(...) through
        // window.showSafeAlert, which renders into the #research-alert live region.
        // The old test queried alert selectors on a fresh / and returned passed:true
        // unconditionally — it never verified a message is displayed. Drive the real
        // display function with a marker and assert it renders a visible alert.
        //
        // showSafeAlert clears the container and appends the alert SYNCHRONOUSLY, so we
        // trigger and read in a single page.evaluate: that both isolates the marker from
        // the on-load /api/warnings check (which also renders into this same container)
        // and removes any async race. We drive showSafeAlert directly rather than a real
        // user trigger, so this is a rendering-mechanism check.
        await navigateTo(page, `${baseUrl}/`);
        await page.waitForSelector('#research-alert', { timeout: 15000 });

        const MARKER = 'LDR_UI_TEST_FLASH_MARKER';
        const result = await page.evaluate((marker) => {
            if (typeof window.showSafeAlert !== 'function') return { hasMechanism: false };
            const a = document.querySelector('#research-alert');
            if (!a) return { hasMechanism: true, hasContainer: false };
            a.innerHTML = '';
            a.style.display = 'none';
            window.showSafeAlert('research-alert', marker, 'error');
            // Read in the same synchronous tick — no async warning re-render can race it.
            const child = a.firstElementChild;
            return {
                hasMechanism: true,
                hasContainer: true,
                visible: window.getComputedStyle(a).display !== 'none',
                childCount: a.children.length,
                rendered: a.textContent.includes(marker),
                // role of the APPENDED alert element (createSafeAlertElement sets
                // role="alert"), not the container's static template role — so this
                // conjunct actually verifies the rendered alert, not a constant.
                childRole: child ? child.getAttribute('role') : null,
            };
        }, MARKER);

        // Restore #research-alert to its hidden/empty default so the marker alert
        // doesn't leak 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 (!result.hasMechanism) {
            return { passed: false, message: 'Alert mechanism (window.showSafeAlert) not loaded on /' };
        }
        if (!result.hasContainer) {
            return { passed: false, message: '#research-alert container not found on /' };
        }
        const passed = result.rendered && result.visible && result.childCount > 0
            && result.childRole === 'alert';
        return {
            passed,
            message: passed
                ? `Flash message rendered into #research-alert (alert role="alert", visible, ${result.childCount} child)`
                : `Flash message not displayed (rendered=${result.rendered}, visible=${result.visible}, children=${result.childCount}, childRole=${result.childRole})`
        };
    }
};

// ============================================================================
// Success/Error Feedback Tests
// ============================================================================
const FeedbackTests = {
    async successFeedbackStyles(page, baseUrl) {
        await navigateTo(page, `${baseUrl}/`);

        const result = await page.evaluate(() => {
            // Check for success-related CSS classes
            const successElements = document.querySelectorAll(
                '.success, ' +
                '.alert-success, ' +
                '.text-success, ' +
                '.bg-success, ' +
                '[class*="success"]'
            );

            // Check CSS for success colors
            let hasSuccessCSS = false;
            const styleSheets = Array.from(document.styleSheets);

            try {
                for (const sheet of styleSheets) {
                    if (sheet.cssRules) {
                        for (const rule of sheet.cssRules) {
                            const cssText = rule.cssText?.toLowerCase() || '';
                            if (cssText.includes('success') && (cssText.includes('green') || cssText.includes('#28a745') || cssText.includes('#198754'))) {
                                hasSuccessCSS = true;
                                break;
                            }
                        }
                    }
                }
            } catch {
                // Cross-origin stylesheets will throw
            }

            return {
                elementCount: successElements.length,
                hasSuccessCSS
            };
        });

        return {
            passed: result.hasSuccessCSS || result.elementCount > 0,
            message: `Success styles: elements=${result.elementCount}, CSS=${result.hasSuccessCSS}`
        };
    },

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

        const result = await page.evaluate(() => {
            // Check for error-related CSS classes
            const errorElements = document.querySelectorAll(
                '.error, ' +
                '.alert-danger, ' +
                '.text-danger, ' +
                '.bg-danger, ' +
                '[class*="error"], ' +
                '.invalid-feedback'
            );

            // Check CSS for error colors
            let hasErrorCSS = false;
            const styleSheets = Array.from(document.styleSheets);

            try {
                for (const sheet of styleSheets) {
                    if (sheet.cssRules) {
                        for (const rule of sheet.cssRules) {
                            const cssText = rule.cssText?.toLowerCase() || '';
                            if ((cssText.includes('error') || cssText.includes('danger') || cssText.includes('invalid')) &&
                                (cssText.includes('red') || cssText.includes('#dc3545') || cssText.includes('#f44336'))) {
                                hasErrorCSS = true;
                                break;
                            }
                        }
                    }
                }
            } catch {
                // Cross-origin stylesheets will throw
            }

            return {
                elementCount: errorElements.length,
                hasErrorCSS
            };
        });

        return {
            passed: result.hasErrorCSS || result.elementCount > 0,
            message: `Error styles: elements=${result.elementCount}, CSS=${result.hasErrorCSS}`
        };
    },

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

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

            let hasWarningCSS = false;
            const styleSheets = Array.from(document.styleSheets);

            try {
                for (const sheet of styleSheets) {
                    if (sheet.cssRules) {
                        for (const rule of sheet.cssRules) {
                            const cssText = rule.cssText?.toLowerCase() || '';
                            if (cssText.includes('warning') && (cssText.includes('yellow') || cssText.includes('orange') || cssText.includes('#ffc107'))) {
                                hasWarningCSS = true;
                                break;
                            }
                        }
                    }
                }
            } catch {
                // Cross-origin stylesheets will throw
            }

            return {
                elementCount: warningElements.length,
                hasWarningCSS
            };
        });

        return {
            passed: result.hasWarningCSS || result.elementCount > 0,
            message: `Warning styles: elements=${result.elementCount}, CSS=${result.hasWarningCSS}`
        };
    }
};

// ============================================================================
// Progress Indicators Tests
// ============================================================================
const ProgressIndicatorsTests = {
    async progressBarExists(page, baseUrl) {
        await navigateTo(page, `${baseUrl}/`);
        await delay(500);

        const result = await page.evaluate(() => {
            // Broader search for progress indicators
            const progressBars = document.querySelectorAll(
                '.progress, ' +
                '.progress-bar, ' +
                '[role="progressbar"], ' +
                'progress, ' +
                '[class*="progress"], ' +
                '.spinner, ' +
                '[class*="spinner"], ' +
                '[class*="loading"], ' +
                '.loader'
            );

            // Check CSS for progress styles
            let hasProgressCSS = false;
            const styleSheets = Array.from(document.styleSheets);

            try {
                for (const sheet of styleSheets) {
                    if (sheet.cssRules) {
                        for (const rule of sheet.cssRules) {
                            const cssText = rule.cssText?.toLowerCase() || '';
                            if (cssText.includes('.progress') || cssText.includes('progressbar') ||
                                cssText.includes('spinner') || cssText.includes('@keyframes')) {
                                hasProgressCSS = true;
                                break;
                            }
                        }
                    }
                }
            } catch {
                // Cross-origin stylesheets will throw
            }

            return {
                count: progressBars.length,
                hasProgressCSS
            };
        });

        // Skip instead of fail if no progress bars found - they may appear dynamically
        if (result.count === 0 && !result.hasProgressCSS) {
            return {
                passed: null,
                skipped: true,
                message: 'No progress indicators visible (may appear during active research)'
            };
        }

        return {
            passed: true,
            message: `Progress bars: ${result.count} found, CSS=${result.hasProgressCSS}`
        };
    },

    async progressPageHasIndicators(page, baseUrl) {
        // Navigate to a progress page if available
        await navigateTo(page, `${baseUrl}/history`);

        // Look for any in-progress research
        const researchId = await page.evaluate(() => {
            const inProgress = document.querySelector('[data-status="in_progress"], .in-progress, [class*="running"]');
            if (inProgress) {
                const link = inProgress.querySelector('a[href*="progress"]');
                const match = link?.href?.match(/\/progress\/([a-zA-Z0-9-]+)/);
                return match ? match[1] : null;
            }
            return null;
        });

        if (!researchId) {
            // Try to access any progress page
            await navigateTo(page, `${baseUrl}/progress/test-id`).catch(() => {});
        } else {
            await navigateTo(page, `${baseUrl}/progress/${researchId}`);
        }

        const result = await page.evaluate(() => {
            const progressBar = document.querySelector('.progress, .progress-bar, [role="progressbar"]');
            const statusText = document.querySelector('.status, .status-text, [class*="status"]');
            const spinner = document.querySelector('.spinner, .loader, [class*="loading"]');
            const percentText = document.body.textContent.match(/\d+%/);

            return {
                hasProgressBar: !!progressBar,
                hasStatusText: !!statusText,
                hasSpinner: !!spinner,
                hasPercent: !!percentText,
                statusContent: statusText?.textContent?.trim()?.substring(0, 50)
            };
        });

        const hasIndicators = result.hasProgressBar || result.hasStatusText || result.hasSpinner || result.hasPercent;

        return {
            passed: hasIndicators,
            message: `Progress page: bar=${result.hasProgressBar}, status="${result.statusContent}", spinner=${result.hasSpinner}`
        };
    }
};

// ============================================================================
// Hover & Active States Tests
// ============================================================================
const HoverActiveStatesTests = {
    async buttonHoverStates(page, baseUrl) {
        // :hover pseudo-class is unreliable in headless Chrome — visual changes
        // may not apply even when hover() is called
        if (process.env.CI) {
            return { passed: null, skipped: true, message: 'Hover styles unreliable in headless Chrome' };
        }
        await navigateTo(page, `${baseUrl}/`);

        const button = await page.$('button, .btn');
        if (!button) {
            return { passed: null, skipped: true, message: 'No button to test hover state' };
        }

        const beforeHover = await page.evaluate(() => {
            const btn = document.querySelector('button, .btn');
            const style = window.getComputedStyle(btn);
            return {
                backgroundColor: style.backgroundColor,
                boxShadow: style.boxShadow,
                transform: style.transform
            };
        });

        await button.hover();
        await delay(200);

        const afterHover = await page.evaluate(() => {
            const btn = document.querySelector('button, .btn');
            const style = window.getComputedStyle(btn);
            return {
                backgroundColor: style.backgroundColor,
                boxShadow: style.boxShadow,
                transform: style.transform
            };
        });

        const hasHoverEffect = beforeHover.backgroundColor !== afterHover.backgroundColor ||
                              beforeHover.boxShadow !== afterHover.boxShadow ||
                              beforeHover.transform !== afterHover.transform;

        return {
            passed: hasHoverEffect,
            message: hasHoverEffect
                ? 'Buttons have hover state changes'
                : 'No visible hover effect on buttons'
        };
    },

    async linkHoverStates(page, baseUrl) {
        await navigateTo(page, `${baseUrl}/`);
        await delay(300);

        const link = await page.$('a[href]');
        if (!link) {
            return { passed: null, skipped: true, message: 'No link to test hover state' };
        }

        try {
            const beforeHover = await page.evaluate(() => {
                const a = document.querySelector('a[href]');
                if (!a) return null;
                const style = window.getComputedStyle(a);
                return {
                    textDecoration: style.textDecoration,
                    color: style.color,
                    opacity: style.opacity,
                    transform: style.transform
                };
            });

            if (!beforeHover) {
                return { passed: null, skipped: true, message: 'Link not accessible for hover test' };
            }

            await link.hover();
            await delay(300);

            const afterHover = await page.evaluate(() => {
                const a = document.querySelector('a[href]');
                if (!a) return null;
                const style = window.getComputedStyle(a);
                return {
                    textDecoration: style.textDecoration,
                    color: style.color,
                    opacity: style.opacity,
                    transform: style.transform
                };
            });

            if (!afterHover) {
                return { passed: null, skipped: true, message: 'Link state changed during hover test' };
            }

            const hasHoverEffect = beforeHover.textDecoration !== afterHover.textDecoration ||
                                  beforeHover.color !== afterHover.color ||
                                  beforeHover.opacity !== afterHover.opacity ||
                                  beforeHover.transform !== afterHover.transform;

            // Skip instead of fail - hover effects may be subtle or not implemented
            if (!hasHoverEffect) {
                return {
                    passed: null,
                    skipped: true,
                    message: 'No visible hover effect detected on links (may use subtle or CSS-in-JS styles)'
                };
            }

            return {
                passed: true,
                message: `Links have hover effect (decoration: ${beforeHover.textDecoration} -> ${afterHover.textDecoration})`
            };
        } catch (err) {
            return { passed: null, skipped: true, message: `Hover test failed: ${err.message}` };
        }
    }
};

// ============================================================================
// Animation Tests
// ============================================================================
const AnimationTests = {
    async cssAnimationsDefined(page, baseUrl) {
        await navigateTo(page, `${baseUrl}/`);

        const result = await page.evaluate(() => {
            const styleSheets = Array.from(document.styleSheets);
            const animations = [];

            try {
                for (const sheet of styleSheets) {
                    if (sheet.cssRules) {
                        for (const rule of sheet.cssRules) {
                            if (rule.type === CSSRule.KEYFRAMES_RULE) {
                                animations.push(rule.name);
                            }
                        }
                    }
                }
            } catch {
                // Cross-origin stylesheets will throw
            }

            return {
                count: animations.length,
                names: animations.slice(0, 10)
            };
        });

        return {
            passed: result.count > 0,
            message: `CSS animations: ${result.count} defined (${result.names.join(', ')})`
        };
    },

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

        const result = await page.evaluate(() => {
            const elementsWithTransitions = [];
            const allElements = document.querySelectorAll('*');

            for (const el of allElements) {
                const style = window.getComputedStyle(el);
                if (style.transition && style.transition !== 'none' && style.transition !== 'all 0s ease 0s') {
                    elementsWithTransitions.push(el.tagName);
                    if (elementsWithTransitions.length >= 10) break;
                }
            }

            return {
                count: elementsWithTransitions.length,
                elements: elementsWithTransitions.slice(0, 5)
            };
        });

        return {
            passed: result.count > 0,
            message: `CSS transitions: ${result.count}+ elements (${result.elements.join(', ')})`
        };
    }
};

// ============================================================================
// Main Test Runner
// ============================================================================
async function main() {
    log.section('Loading & Feedback UI Tests');

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

        await run('Loading', 'Spinners', (p, u) => LoadingStatesTests.loadingSpinnersExist(p, u));
        await run('Loading', 'Skeletons', (p, u) => LoadingStatesTests.skeletonLoadersExist(p, u));
        await run('Loading', 'Button States', (p, u) => LoadingStatesTests.buttonLoadingState(p, u));

        // Toast Notifications
        log.section('Toast Notifications');

        await run('Toast', 'Container', (p, u) => ToastNotificationsTests.toastContainerExists(p, u));
        await run('Toast', 'Flash Messages', (p, u) => ToastNotificationsTests.flashMessagesDisplayed(p, u));

        // Success/Error Feedback
        log.section('Feedback Styles');

        await run('Feedback', 'Success Styles', (p, u) => FeedbackTests.successFeedbackStyles(p, u));
        await run('Feedback', 'Error Styles', (p, u) => FeedbackTests.errorFeedbackStyles(p, u));
        await run('Feedback', 'Warning Styles', (p, u) => FeedbackTests.warningFeedbackStyles(p, u));

        // Progress Indicators
        log.section('Progress Indicators');

        await run('Progress', 'Bar Exists', (p, u) => ProgressIndicatorsTests.progressBarExists(p, u));
        await run('Progress', 'Page Indicators', (p, u) => ProgressIndicatorsTests.progressPageHasIndicators(p, u));

        // Hover & Active States
        log.section('Hover States');

        await run('Hover', 'Button States', (p, u) => HoverActiveStatesTests.buttonHoverStates(p, u));
        await run('Hover', 'Link States', (p, u) => HoverActiveStatesTests.linkHoverStates(p, u));

        // Animations
        log.section('Animations');

        await run('Animation', 'CSS Animations', (p, u) => AnimationTests.cssAnimationsDefined(p, u));
        await run('Animation', 'Transitions', (p, u) => AnimationTests.transitionsUsed(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 = { LoadingStatesTests, ToastNotificationsTests, FeedbackTests, ProgressIndicatorsTests, HoverActiveStatesTests, AnimationTests };
