#!/usr/bin/env node
/**
 * Mobile Interactions UI Tests
 *
 * Tests for mobile-specific interactions: modals, navigation, forms on touch devices.
 *
 * Run: node test_mobile_interactions_ci.js
 */
const { setupTest, teardownTest, TestResults, log, navigateTo, withTimeout } = require('./test_lib');

// ============================================================================
// Mobile Modal Tests
// ============================================================================
const MobileModalTests = {
    async createFormFitsMobile(page, baseUrl) {
        // The original test (modalOpensOnMobile) navigated to a 404
        // /collections route expecting a modal-opens-on-click flow. The real
        // app does page navigation for the create flow, so we test the
        // mobile-friendliness of /library/collections/create directly:
        // the form must fit the 375px viewport and the required name input
        // must be a touch-sized hit target (>=44px tall is the Apple HIG
        // minimum; >=30px keeps us tolerant of theme variations).

        await page.setViewport({ width: 375, height: 667, isMobile: true, hasTouch: true });
        await navigateTo(page, `${baseUrl}/library/collections/create`);

        const result = await page.evaluate(() => {
            const form = document.querySelector('form:not(#logout-form)');
            const name = document.querySelector('#collection-name, input[name="name"]');
            if (!form || !name) return { hasForm: false };

            const formRect = form.getBoundingClientRect();
            const nameRect = name.getBoundingClientRect();
            return {
                hasForm: true,
                formWidth: formRect.width,
                screenWidth: window.innerWidth,
                formFitsScreen: formRect.width <= window.innerWidth,
                nameVisible: nameRect.width > 0 && nameRect.height > 0,
                nameHeight: nameRect.height,
                nameTouchSized: nameRect.height >= 30,
            };
        });

        if (!result.hasForm) {
            return { passed: false, message: 'Create form not found on /library/collections/create' };
        }

        const passed = result.formFitsScreen && result.nameVisible && result.nameTouchSized;
        return {
            passed,
            message: passed
                ? `Create form mobile-fit (${result.formWidth}px form, ${result.nameHeight}px name input)`
                : `Mobile fit failed (formWidth=${result.formWidth}, screen=${result.screenWidth}, nameHeight=${result.nameHeight})`
        };
    },

    async modalScrollableContent(page, baseUrl) {
        // /settings/ renders NO modal, so the old test always skipped
        // ("No modal content found"). The real app renders a Bootstrap
        // delete-confirmation modal (#deleteConfirmModal, included by
        // pages/library.html). We open it on /library with enough detail
        // rows to overflow a 375x667 phone, then assert it really opens and
        // its tall content stays reachable via the modal's scroll container.
        await page.setViewport({ width: 375, height: 667, isMobile: true, hasTouch: true });
        await navigateTo(page, `${baseUrl}/library/`);

        // Page-specific anchor: fail (not skip) if login/error loaded instead.
        await page.waitForSelector('.ldr-library-container', { timeout: 10000 });
        await page.waitForSelector('#deleteConfirmModal .modal-body', { timeout: 10000 });

        // Open the modal with long content (forces vertical overflow).
        const opened = await page.evaluate(() => {
            if (!window.DeleteConfirmation || !window.DeleteConfirmation.show) {
                return { api: false };
            }
            window.DeleteConfirmation.show({
                action: 'deleteDocument',
                details: Array.from({ length: 60 }, (_, i) => `Document row ${i}`),
            });
            return { api: true };
        });

        if (!opened.api) {
            return { passed: false, message: 'window.DeleteConfirmation.show API not available on /library' };
        }

        // Wait for Bootstrap to apply the .show / display:block state.
        await page.waitForFunction(() => {
            const m = document.getElementById('deleteConfirmModal');
            return m && m.classList.contains('show') && window.getComputedStyle(m).display !== 'none';
        }, { timeout: 5000 });

        const result = await page.evaluate(() => {
            const modal = document.getElementById('deleteConfirmModal');
            const content = modal.querySelector('.modal-content');
            const body = modal.querySelector('.modal-body');
            const contentStyle = window.getComputedStyle(content);
            const bodyRect = body.getBoundingClientRect();

            // On mobile (<=767px) the real scroll container is .modal-content,
            // which mobile-responsive.css caps with
            // `max-height: calc(100vh - 180px); overflow-y: auto`. Reading
            // overflow on the OUTER .modal would be meaningless here — Bootstrap
            // base sets `.modal{overflow:hidden auto}` unconditionally, so that
            // check can never fail. Assert the container is actually scrollable
            // AND that the injected 60-row content overflows its capped height,
            // so a regression that removes the mobile cap (content would grow to
            // fit with no overflow) fails this test.
            return {
                containerOverflowY: contentStyle.overflowY,
                containerScrolls: contentStyle.overflowY === 'auto' || contentStyle.overflowY === 'scroll',
                // offsetParent !== null rejects a modal body hidden via display:none
                // on any ancestor (e.g. a backdrop-only / never-shown modal) that a
                // bare width/height check would still accept.
                bodyVisible: bodyRect.width > 0 && bodyRect.height > 0 && body.offsetParent !== null,
                contentOverflows: content.scrollHeight > content.clientHeight,
                contentScrollHeight: content.scrollHeight,
                contentClientHeight: content.clientHeight,
                viewportHeight: window.innerHeight,
            };
        });

        const passed = result.containerScrolls && result.bodyVisible && result.contentOverflows;
        return {
            passed,
            message: passed
                ? `Delete modal content scrollable on mobile (overflow=${result.containerOverflowY}, content ${result.contentScrollHeight}px > container ${result.contentClientHeight}px on ${result.viewportHeight}px screen)`
                : `Modal scroll check failed (overflowY=${result.containerOverflowY}, bodyVisible=${result.bodyVisible}, contentOverflows=${result.contentOverflows}, contentScroll=${result.contentScrollHeight}, contentClient=${result.contentClientHeight})`
        };
    }
};

// ============================================================================
// Mobile Navigation Tests
// ============================================================================
const MobileNavTests = {
    async mobileMenuOpens(page, baseUrl) {
        await page.setViewport({ width: 375, height: 667, isMobile: true, hasTouch: true });
        await navigateTo(page, `${baseUrl}/`);

        const result = await page.evaluate(() => {
            const hamburger = document.querySelector(
                '.hamburger, ' +
                '.mobile-menu-toggle, ' +
                '.navbar-toggler, ' +
                'button[aria-label*="menu"], ' +
                '.menu-toggle, ' +
                '[class*="hamburger"]'
            );

            if (!hamburger) return { hasHamburger: false };

            hamburger.click();

            return new Promise(resolve => {
                setTimeout(() => {
                    const menu = document.querySelector(
                        '.mobile-menu, ' +
                        '.navbar-collapse.show, ' +
                        '.nav-menu.open, ' +
                        '.sidebar.open, ' +
                        '[class*="mobile-nav"]'
                    );

                    const isVisible = menu && (
                        menu.style.display !== 'none' &&
                        !menu.classList.contains('collapsed')
                    );

                    resolve({
                        hasHamburger: true,
                        menuOpened: !!menu,
                        isVisible
                    });
                }, 300);
            });
        });

        if (!result.hasHamburger) {
            // Check if using bottom nav instead
            const hasBottomNav = await page.evaluate(() => {
                return !!document.querySelector('.bottom-nav, .mobile-nav-tabs, .tab-bar');
            });

            if (hasBottomNav) {
                return { passed: true, message: 'Using bottom navigation instead of hamburger menu' };
            }

            return { passed: null, skipped: true, message: 'No hamburger menu found (may use different nav pattern)' };
        }

        return {
            passed: result.menuOpened,
            message: result.menuOpened
                ? 'Mobile menu opens on tap'
                : 'Mobile menu did not open'
        };
    },

    async mobileMenuCloses(page, baseUrl) {
        // The old test looked for .hamburger / .navbar-toggler / .nav-link /
        // .navbar-collapse — none of which exist in this app, so it always
        // skipped ("No menu links found"). The real mobile menu is built by
        // static/js/mobile-navigation.js: a bottom tab bar
        // (.ldr-mobile-bottom-nav) whose "More" tab ([data-tab-id="more"])
        // opens a sheet (.ldr-mobile-sheet-menu, gains .active). Selecting a
        // sheet item (.ldr-mobile-sheet-item) calls closeSheet() before
        // navigating, so the sheet must collapse on selection.
        await page.setViewport({ width: 375, height: 667, isMobile: true, hasTouch: true });
        await navigateTo(page, `${baseUrl}/`);

        // Mobile nav is created on DOMContentLoaded; wait for the real menu.
        await page.waitForSelector('.ldr-mobile-bottom-nav [data-tab-id="more"]', { timeout: 10000 });
        await page.waitForSelector('.ldr-mobile-sheet-menu', { timeout: 10000 });

        // Open the sheet via the "More" tab and confirm it actually opened.
        await page.evaluate(() => {
            document.querySelector('.ldr-mobile-bottom-nav [data-tab-id="more"]').click();
        });
        await page.waitForFunction(
            () => document.querySelector('.ldr-mobile-sheet-menu')?.classList.contains('active') === true,
            { timeout: 5000 }
        );

        // Select a sheet item; handleSheetItem() runs closeSheet() synchronously
        // (removing .active + setting aria-hidden) before navigating away.
        const result = await page.evaluate(() => {
            const sheet = document.querySelector('.ldr-mobile-sheet-menu');
            const item = sheet.querySelector('.ldr-mobile-sheet-item[data-item-id="metrics"]') ||
                         sheet.querySelector('.ldr-mobile-sheet-item');
            if (!item) return { hasLinks: false };

            const wasOpen = sheet.classList.contains('active');
            item.click();
            return {
                hasLinks: true,
                wasOpen,
                menuClosed: !sheet.classList.contains('active') &&
                            sheet.getAttribute('aria-hidden') === 'true',
                ariaHidden: sheet.getAttribute('aria-hidden'),
            };
        });

        if (!result.hasLinks) {
            return { passed: false, message: 'Mobile sheet rendered no menu items (.ldr-mobile-sheet-item)' };
        }

        const passed = result.wasOpen && result.menuClosed;
        return {
            passed,
            message: passed
                ? 'Mobile sheet menu collapses after selecting an item'
                : `Sheet did not collapse on selection (wasOpen=${result.wasOpen}, aria-hidden=${result.ariaHidden})`
        };
    },

    async bottomNavTabsWork(page, baseUrl) {
        await page.setViewport({ width: 375, height: 667, isMobile: true, hasTouch: true });
        await navigateTo(page, `${baseUrl}/`);

        const result = await page.evaluate(() => {
            const bottomNav = document.querySelector('.bottom-nav, .mobile-nav-tabs, .tab-bar, .mobile-tabs');
            if (!bottomNav) return { hasBottomNav: false };

            const tabs = bottomNav.querySelectorAll('a, button, .tab');
            if (tabs.length === 0) return { hasBottomNav: true, hasTabs: false };

            const tabInfo = Array.from(tabs).map(tab => ({
                text: tab.textContent?.trim(),
                href: tab.href || tab.dataset?.href
            }));

            // Click the second tab if available
            if (tabs.length > 1) {
                tabs[1].click();
            }

            return {
                hasBottomNav: true,
                hasTabs: true,
                tabCount: tabs.length,
                tabs: tabInfo.slice(0, 5)
            };
        });

        if (!result.hasBottomNav) {
            return { passed: null, skipped: true, message: 'No bottom navigation found' };
        }

        if (!result.hasTabs) {
            return { passed: false, message: 'Bottom nav has no tabs' };
        }

        return {
            passed: true,
            message: `Bottom nav has ${result.tabCount} tabs: ${result.tabs.map(t => t.text).join(', ')}`
        };
    },

    async swipeGesturesWork(page, baseUrl) {
        await page.setViewport({ width: 375, height: 667, isMobile: true, hasTouch: true });
        await navigateTo(page, `${baseUrl}/`);

        // Check if there are swipeable elements
        const result = await page.evaluate(() => {
            const swipeableElements = document.querySelectorAll(
                '.swipeable, ' +
                '[data-swipe], ' +
                '.carousel, ' +
                '.slider, ' +
                '[class*="swipe"]'
            );

            const hasTouchSupport = 'ontouchstart' in window || navigator.maxTouchPoints > 0;

            return {
                hasSwipeableElements: swipeableElements.length > 0,
                elementCount: swipeableElements.length,
                hasTouchSupport
            };
        });

        if (!result.hasSwipeableElements) {
            return { passed: null, skipped: true, message: 'No swipeable elements found' };
        }

        return {
            passed: true,
            message: `${result.elementCount} swipeable elements found`
        };
    }
};

// ============================================================================
// Mobile Form Tests
// ============================================================================
const MobileFormTests = {
    async mobileKeyboardDoesntBreakLayout(page, baseUrl) {
        await page.setViewport({ width: 375, height: 667, isMobile: true, hasTouch: true });
        await navigateTo(page, `${baseUrl}/`);

        const result = await page.evaluate(() => {
            const input = document.querySelector('input[type="text"], input[type="search"], textarea');
            if (!input) return { hasInput: false };

            // Focus the input (simulates keyboard appearing)
            input.focus();

            // Check if important elements are still visible
            const header = document.querySelector('header, .navbar, .page-header');
            const submitBtn = document.querySelector('button[type="submit"], .btn-primary');

            const headerVisible = header ? header.getBoundingClientRect().top >= 0 : true;

            return {
                hasInput: true,
                headerVisible,
                hasSubmitBtn: !!submitBtn
            };
        });

        if (!result.hasInput) {
            return { passed: null, skipped: true, message: 'No input field found' };
        }

        return {
            passed: result.headerVisible,
            message: result.headerVisible
                ? 'Layout stable when input focused'
                : 'Header pushed off-screen when input focused'
        };
    },

    async mobileDropdownsWork(page, baseUrl) {
        await page.setViewport({ width: 375, height: 667, isMobile: true, hasTouch: true });
        await navigateTo(page, `${baseUrl}/settings/`);

        const result = await page.evaluate(() => {
            const selects = document.querySelectorAll('select, .custom-dropdown, .ldr-dropdown');
            if (selects.length === 0) return { hasDropdowns: false };

            const firstSelect = selects[0];
            const rect = firstSelect.getBoundingClientRect();

            // Check if dropdown is within screen bounds
            const isAccessible = rect.right <= window.innerWidth && rect.left >= 0;

            // Check touch target size
            const isTappable = rect.height >= 44; // iOS minimum touch target

            return {
                hasDropdowns: true,
                dropdownCount: selects.length,
                isAccessible,
                isTappable,
                height: rect.height
            };
        });

        if (!result.hasDropdowns) {
            return { passed: null, skipped: true, message: 'No dropdowns found' };
        }

        return {
            passed: result.isAccessible && result.isTappable,
            message: `Dropdowns: ${result.dropdownCount} found (accessible: ${result.isAccessible}, height: ${result.height}px)`
        };
    }
};

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

    const ctx = await setupTest({ authenticate: true });
    const results = new TestResults('Mobile Interactions 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 {
        // Mobile Modal Tests
        log.section('Mobile Modals');
        await run('Modals', 'Create Form Fits Mobile', (p, u) => MobileModalTests.createFormFitsMobile(p, u));
        await run('Modals', 'Modal Scrollable Content', (p, u) => MobileModalTests.modalScrollableContent(p, u));

        // Mobile Navigation Tests
        log.section('Mobile Navigation');
        await run('Navigation', 'Mobile Menu Opens', (p, u) => MobileNavTests.mobileMenuOpens(p, u));
        await run('Navigation', 'Mobile Menu Closes', (p, u) => MobileNavTests.mobileMenuCloses(p, u));
        await run('Navigation', 'Bottom Nav Tabs Work', (p, u) => MobileNavTests.bottomNavTabsWork(p, u));
        await run('Navigation', 'Swipe Gestures Work', (p, u) => MobileNavTests.swipeGesturesWork(p, u));

        // Mobile Form Tests
        log.section('Mobile Forms');
        await run('Forms', 'Mobile Keyboard Doesnt Break Layout', (p, u) => MobileFormTests.mobileKeyboardDoesntBreakLayout(p, u));
        await run('Forms', 'Mobile Dropdowns Work', (p, u) => MobileFormTests.mobileDropdownsWork(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 = { MobileModalTests, MobileNavTests, MobileFormTests };
