/**
 * All Pages Mobile Layout Tests
 *
 * Comprehensive mobile UI tests for all major pages in the application.
 * Tests are designed to identify layout issues on mobile devices:
 * - Horizontal overflow
 * - Hidden content behind mobile navigation
 * - Touch target sizing
 * - Mobile navigation visibility
 * - Sidebar behavior
 *
 * Pages tested (in priority order):
 * 1. / - Research (Home)
 * 2. /history/ - History
 * 3. /settings/ - Settings
 * 4. /news/ - News Feed
 * 5. /library/ - Library
 * 6. /metrics/ - Metrics Dashboard
 * 7. /benchmark/ - Benchmark
 *
 * Note: Authentication is handled by auth.setup.js via storageState
 */

import { test, expect } from '@playwright/test';
const {
  MIN_TOUCH_TARGET,
  MOBILE_NAV_SELECTOR,
  SIDEBAR_SELECTOR,
} = require('./helpers/mobile-utils');

// Pages to test
// Note: Some pages use continuous network activity, so we use 'domcontentloaded' instead of 'networkidle'
const PAGES = [
  { path: '/', name: 'Research (Home)', priority: 'high' },
  { path: '/history/', name: 'History', priority: 'high' },
  { path: '/settings/', name: 'Settings', priority: 'high' },
  { path: '/news/', name: 'News Feed', priority: 'high' },
  { path: '/library/', name: 'Library', priority: 'high' },
  { path: '/metrics/', name: 'Metrics Dashboard', priority: 'high' },
  { path: '/benchmark/', name: 'Benchmark', priority: 'medium' },
  { path: '/metrics/context-overflow', name: 'Context Overflow', priority: 'medium' },
  { path: '/metrics/star-reviews', name: 'Star Reviews', priority: 'medium' },
  { path: '/metrics/costs', name: 'Cost Analytics', priority: 'medium' },
  { path: '/metrics/links', name: 'Link Analytics', priority: 'medium' },
];

/**
 * Walk interactive elements and report any that overlap the fixed mobile nav.
 *
 * Returns at most `maxReported` hits. Split into small page.evaluate calls
 * so no single in-page DOM walk runs long enough to race WebKit's
 * context-close (issue #4060).
 */
async function findElementsBehindMobileNav(page, navSelector, options = {}) {
  const {
    overlapTolerance = 20,
    batchSize = 50,
    maxReported = 5,
  } = options;

  const navInfo = await page.evaluate((sel) => {
    const mobileNav = document.querySelector(sel);
    if (!mobileNav) return { hasNav: false };
    const navStyle = window.getComputedStyle(mobileNav);
    if (navStyle.display === 'none') return { hasNav: false };
    const navRect = mobileNav.getBoundingClientRect();
    return {
      hasNav: true,
      navTop: navRect.top,
      navBottom: navRect.bottom,
    };
  }, navSelector);

  if (!navInfo.hasNav) return [];

  const totalCount = await page.evaluate(() =>
    document.querySelectorAll(
      'button, a, input, select, textarea, [role="button"]'
    ).length
  );

  const hits = [];
  for (let start = 0; start < totalCount; start += batchSize) {
    const remaining = maxReported - hits.length;
    if (remaining <= 0) break;
    const batch = await page.evaluate(
      ({ sel, navTop, navBottom, batchStart, size, tolerance, cap }) => {
        const mobileNav = document.querySelector(sel);
        const all = document.querySelectorAll(
          'button, a, input, select, textarea, [role="button"]'
        );
        const issues = [];
        const end = Math.min(batchStart + size, all.length);
        for (let i = batchStart; i < end; i++) {
          if (issues.length >= cap) break;
          const el = all[i];
          const rect = el.getBoundingClientRect();
          const style = window.getComputedStyle(el);
          if (style.display === 'none' || style.visibility === 'hidden') continue;
          if (rect.width === 0 || rect.height === 0) continue;
          if (el.closest('.modal:not(.show)')) continue;
          if (el.closest('.ldr-help-panel-dismiss')) continue;
          if (rect.bottom > navTop + tolerance && rect.top < navBottom) {
            if (mobileNav && !mobileNav.contains(el)) {
              issues.push({
                tag: el.tagName.toLowerCase(),
                class: el.className?.toString().slice(0, 50),
                text: (el.textContent || '').trim().slice(0, 30),
                bottom: Math.round(rect.bottom),
                navTop: Math.round(navTop),
              });
            }
          }
        }
        return issues;
      },
      {
        sel: navSelector,
        navTop: navInfo.navTop,
        navBottom: navInfo.navBottom,
        batchStart: start,
        size: batchSize,
        tolerance: overlapTolerance,
        cap: remaining,
      }
    );
    hits.push(...batch);
  }

  return hits;
}

/**
 * Wait for a page to load with appropriate state
 */
async function waitForPageLoad(page, pageInfo) {
  const loadState = pageInfo.loadState || 'domcontentloaded';
  await page.waitForLoadState(loadState);

  // Additional waits for specific pages
  if (pageInfo.path === '/settings/') {
    await page.waitForSelector('.ldr-loading-spinner', { state: 'hidden', timeout: 10000 }).catch(() => {});
  } else if (pageInfo.path === '/benchmark/') {
    // Wait for benchmark form to load
    await page.waitForSelector('#benchmark-form, form, .ldr-card', { timeout: 10000 }).catch(() => {});
  } else if (pageInfo.path.includes('/metrics')) {
    // Wait for metrics content to load
    await page.waitForSelector('#metrics-content, .ldr-metrics-grid, .ldr-metric-card', { timeout: 10000 }).catch(() => {});
  }
}

// ============================================
// HORIZONTAL OVERFLOW TESTS
// ============================================

test.describe('All Pages - No Horizontal Overflow', () => {
  for (const pageInfo of PAGES) {
    test(`${pageInfo.name} has no horizontal overflow`, async ({ page }) => {
      await page.goto(pageInfo.path);
      await waitForPageLoad(page, pageInfo);

      const hasOverflow = await page.evaluate(() =>
        document.documentElement.scrollWidth > window.innerWidth
      );

      if (hasOverflow) {
        // Get information about what's causing overflow
        const overflowInfo = await page.evaluate(() => {
          const elements = [];
          document.querySelectorAll('*').forEach((el) => {
            const rect = el.getBoundingClientRect();
            if (rect.right > window.innerWidth) {
              elements.push({
                tag: el.tagName.toLowerCase(),
                class: el.className,
                id: el.id,
                width: Math.round(rect.width),
                right: Math.round(rect.right),
                overflow: Math.round(rect.right - window.innerWidth),
              });
            }
          });
          return elements.slice(0, 5); // Return first 5 overflowing elements
        });
        console.log('Overflowing elements:', JSON.stringify(overflowInfo, null, 2));
      }

      expect(hasOverflow, `${pageInfo.name} should have no horizontal overflow`).toBe(false);
    });
  }
});

// ============================================
// MOBILE NAVIGATION VISIBILITY TESTS
// ============================================

test.describe('All Pages - Mobile Navigation', () => {
  for (const pageInfo of PAGES) {
    test(`${pageInfo.name} shows mobile nav on mobile`, async ({ page, isMobile }, testInfo) => {
      if (!isMobile) {
        test.skip();
        return;
      }

      // Skip on tablets (iPad) - they use sidebar navigation instead of bottom nav
      const isTablet = testInfo.project.name.includes('iPad');
      if (isTablet) {
        test.skip();
        return;
      }

      await page.goto(pageInfo.path);
      await waitForPageLoad(page, pageInfo);

      const mobileNav = page.locator(MOBILE_NAV_SELECTOR);
      await expect(mobileNav, `Mobile nav should be visible on ${pageInfo.name}`).toBeVisible();
    });
  }
});

// ============================================
// SIDEBAR HIDDEN ON MOBILE TESTS
// ============================================

test.describe('All Pages - Sidebar Hidden on Mobile', () => {
  for (const pageInfo of PAGES) {
    test(`${pageInfo.name} hides sidebar on mobile`, async ({ page, isMobile }, testInfo) => {
      if (!isMobile) {
        test.skip();
        return;
      }

      // Skip on tablets (iPad) - they show sidebar navigation
      const isTablet = testInfo.project.name.includes('iPad');
      if (isTablet) {
        test.skip();
        return;
      }

      await page.goto(pageInfo.path);
      await waitForPageLoad(page, pageInfo);

      const sidebar = page.locator(SIDEBAR_SELECTOR);

      // Check if sidebar exists
      if ((await sidebar.count()) > 0) {
        // Sidebar should not be visible on mobile (either hidden or collapsed)
        const isVisible = await sidebar.isVisible();
        const sidebarBox = isVisible ? await sidebar.boundingBox() : null;

        // Sidebar might be "visible" but positioned off-screen or collapsed to 0 width
        if (isVisible && sidebarBox) {
          expect(
            sidebarBox.width,
            `Sidebar on ${pageInfo.name} should be collapsed or hidden`
          ).toBeLessThanOrEqual(0);
        }
      }
    });
  }
});

// ============================================
// TOUCH TARGET SIZE TESTS
// ============================================

test.describe('All Pages - Touch Targets', () => {
  for (const pageInfo of PAGES) {
    test(`${pageInfo.name} has adequate touch targets`, async ({ page, isMobile }, testInfo) => {
      if (!isMobile) {
        test.skip();
        return;
      }

      // Skip on tablets (iPad) - they have different touch target requirements
      const isTablet = testInfo.project.name.includes('iPad');
      if (isTablet) {
        test.skip();
        return;
      }

      await page.goto(pageInfo.path);
      await waitForPageLoad(page, pageInfo);

      const smallTargets = await page.evaluate((MIN_SIZE) => {
        const elements = document.querySelectorAll(
          'button, a, input, select, textarea, [role="button"], .btn'
        );
        const issues = [];

        elements.forEach((el) => {
          const rect = el.getBoundingClientRect();
          const style = window.getComputedStyle(el);

          // Skip hidden elements
          if (style.display === 'none' || style.visibility === 'hidden') return;
          if (rect.width === 0 || rect.height === 0) return;

          // Skip elements that are off-screen
          if (rect.top > window.innerHeight || rect.bottom < 0) return;
          if (rect.left > window.innerWidth || rect.right < 0) return;

          if (rect.width < MIN_SIZE || rect.height < MIN_SIZE) {
            issues.push({
              tag: el.tagName.toLowerCase(),
              class: el.className?.toString().slice(0, 50),
              id: el.id,
              size: `${Math.round(rect.width)}x${Math.round(rect.height)}`,
              text: (el.textContent || '').trim().slice(0, 30),
            });
          }
        });

        return issues;
      }, MIN_TOUCH_TARGET);

      // Log issues for debugging but allow some small elements
      if (smallTargets.length > 0) {
        console.log(
          `Small touch targets on ${pageInfo.name}:`,
          JSON.stringify(smallTargets, null, 2)
        );
      }

      // Allow up to 3 small elements (some icons may be small but have large hit areas)
      expect(
        smallTargets.length,
        `${pageInfo.name} should have minimal small touch targets`
      ).toBeLessThan(3);
    });
  }
});

// ============================================
// CONTENT NOT BEHIND MOBILE NAV TESTS
// ============================================

test.describe('All Pages - Content Above Mobile Nav', () => {
  // WebKit on CI is markedly slower than Chromium/Firefox at firing `load`
  // and at running cross-process page.evaluate against `/`. The defensive
  // waits below + the chunked DOM walk legitimately need more than the
  // default 30s budget when the runner is under load. Raise the per-test
  // budget so the test reflects real WebKit timing rather than misleadingly
  // failing with "Target page... has been closed" on the first evaluate.
  test.describe.configure({ timeout: 60000 });

  for (const pageInfo of PAGES) {
    test(`${pageInfo.name} content not hidden behind mobile nav`, async ({ page, isMobile }) => {
      if (!isMobile) {
        test.skip();
        return;
      }

      await page.goto(pageInfo.path);
      await waitForPageLoad(page, pageInfo);

      // Wait for page-specific content to load
      if (pageInfo.path === '/settings/') {
        // Wait for settings to load (spinner to disappear)
        await page.waitForSelector('.ldr-loading-spinner', { state: 'hidden', timeout: 10000 }).catch(() => {});
        // Wait for settings form to be fully rendered
        await page.waitForSelector('.ldr-settings-form, #settings-form', { timeout: 10000 }).catch(() => {});
        // Wait for form inputs to render (dynamic content)
        await page.waitForSelector('.ldr-settings-form input, .ldr-settings-form select, #settings-form input, #settings-form select', { timeout: 10000 }).catch(() => {});
      } else if (pageInfo.path === '/benchmark/') {
        // Wait for benchmark form to load
        await page.waitForSelector('#benchmark-form', { timeout: 10000 }).catch(() => {});
        // Wait for form inputs to fully render - benchmark has complex form content
        await page.waitForSelector('#benchmark-form input, #benchmark-form select, #benchmark-form textarea', { timeout: 10000 }).catch(() => {});
      }

      // Collapse all help panels before checking - they can expand from localStorage state.
      // The evaluate runs synchronously in-page, so panels are already hidden when it returns
      // — no need for a follow-up waitForFunction that previously ate up to 5s of budget.
      await page.evaluate(() => {
        if (window.HelpService && typeof window.HelpService.collapseAll === 'function') {
          window.HelpService.collapseAll();
        }
        // Also directly collapse any expanded help panel content
        document.querySelectorAll('.ldr-help-panel-content[style*="display: block"], .ldr-help-panel-content:not([style*="display: none"])').forEach(el => {
          el.style.display = 'none';
        });
      });

      // Ensure page is fully loaded before heavy DOM operations (WebKit stability).
      // Cap with an explicit short timeout: without it, the default navigationTimeout
      // (30s) silently consumes the entire test budget when WebKit is slow to fire
      // `load`, leaving zero time for the DOM walk below.
      await page.waitForLoadState('load', { timeout: 3000 }).catch(() => {});

      // Wait for async-loaded content to settle before scrolling.
      // History, News, Library and Metrics populate their lists via XHR
      // *after* `load` fires. History/News/Library have no content-ready wait
      // in waitForPageLoad at all, and Metrics only waits for its cards to
      // exist (not for the layout to stop shifting). A single scroll-to-bottom
      // taken mid-load lands on a stale layout; the list then resolves to its
      // empty-state and the page's bottom controls (search inputs, filter
      // dropdowns) end up behind the fixed mobile nav — which the assertion
      // below flags as "hidden behind nav". The #4314 clearance padding is
      // already sufficient; the failures were a measurement race, confirmed by
      // trace snapshots showing loading spinners still present at assertion time
      // on /history/, /news/, /library/, /metrics/.
      //
      // Poll until the document height is stable across two consecutive samples.
      // Bounded so pages with continuous lazy-loading (e.g. `/`) still proceed
      // instead of hanging — the exact failure mode that retired the old
      // scrollY-based poll referenced below.
      await page
        .waitForFunction(
          () => {
            const h = document.documentElement.scrollHeight;
            const prev = window.__ldrPrevScrollH;
            window.__ldrPrevScrollH = h;
            return prev !== undefined && Math.abs(h - prev) < 2;
          },
          { timeout: 4000, polling: 250 }
        )
        .catch(() => {});

      // Scroll to bottom to check last content. `behavior: 'instant'` is
      // load-bearing: the Vite bundle ships Bootstrap's reboot rule
      // `@media (prefers-reduced-motion: no-preference) { :root { scroll-behavior: smooth } }`
      // and Playwright emulates `no-preference` by default, so the two-arg
      // `window.scrollTo(x, y)` form ANIMATES over hundreds of ms instead of
      // committing synchronously. The overlap measurement right below then ran
      // at scrollY≈0 and flagged the page's natural under-nav content as
      // "hidden behind nav" — the actual root cause of the persistent
      // History/News/Library/Metrics failures (clearance padding was fine,
      // see #4314). Verified against the CI trace DOM: instant scroll commits
      // before the evaluate resolves (scrollY jumps to max synchronously), so
      // no follow-up `waitForFunction(scrollY >= scrollHeight)` poll is needed
      // — and the old poll could never resolve on `/` anyway when lazy-loaded
      // content kept growing `document.body.scrollHeight`.
      await page.evaluate(() =>
        window.scrollTo({ top: document.body.scrollHeight, left: 0, behavior: 'instant' })
      );

      const hiddenElements = await findElementsBehindMobileNav(page, MOBILE_NAV_SELECTOR);

      if (hiddenElements.length > 0) {
        console.log(
          `Elements hidden behind nav on ${pageInfo.name}:`,
          JSON.stringify(hiddenElements, null, 2)
        );
      }

      expect(
        hiddenElements.length,
        `${pageInfo.name} should have no interactive elements behind mobile nav`
      ).toBe(0);
    });
  }
});

// ============================================
// PAGE LOAD AND BASIC FUNCTIONALITY TESTS
// ============================================

test.describe('All Pages - Basic Load Tests', () => {
  for (const pageInfo of PAGES) {
    test(`${pageInfo.name} loads without errors`, async ({ page }) => {
      // Collect console errors
      const errors = [];
      page.on('console', (msg) => {
        if (msg.type() === 'error') {
          errors.push(msg.text());
        }
      });

      // Collect page errors
      const pageErrors = [];
      page.on('pageerror', (error) => {
        pageErrors.push(error.message);
      });

      await page.goto(pageInfo.path);
      await waitForPageLoad(page, pageInfo);

      // Filter out known benign errors
      const criticalErrors = errors.filter(
        (err) =>
          !err.includes('favicon') &&
          !err.includes('404') &&
          !err.includes('Failed to load resource') &&
          !err.includes("Can't find variable: Chart") && // Chart.js race condition
          !err.includes("Can't find variable: io") // Socket.IO race condition
      );

      expect(pageErrors.length, `${pageInfo.name} should have no page errors`).toBe(0);
      expect(
        criticalErrors.length,
        `${pageInfo.name} should have no critical console errors: ${criticalErrors.join(', ')}`
      ).toBe(0);
    });
  }
});
