/**
 * W2B-03 Safe-Refresh Guardrails Nudge — Workflow Verification
 *
 * Session: 3c1bb58f-c746-494f-a1dd-24578564e129
 *
 * Changed files:
 *   - resources/js/Components/publish/SafeRefreshNudge.tsx  (NEW)
 *   - resources/js/Pages/Recommendations/Index.tsx          (MODIFIED)
 *   - resources/js/Components/Recommendations/BulkActionsBar.tsx (MODIFIED)
 *   - app/Http/Controllers/RecommendationsPageController.php     (MODIFIED)
 *
 * Behavioral contract under test:
 *   Before: No nudge UI existed. Users could select any number of drafts for
 *           publishing with no guardrails feedback.
 *
 *   After (W2B-03):
 *     1. SafeRefreshNudge renders null when selectedDraftIds.length <= cap (5).
 *     2. When selectedDraftIds.length > cap, a role=note advisory appears in the
 *        BulkActionsBar sticky footer with verbatim approved copy.
 *     3. Deselecting back to <= cap causes the nudge to disappear immediately.
 *     4. The safe_refresh_cap prop is present on both the happy path and the
 *        error-state render path from RecommendationsPageController.
 *
 * Strategy:
 *   1. Seed 6 extra recommendations with AI drafts (has_draft=true) via artisan tinker
 *      before the tests run.
 *   2. Navigate to the Recommendations page and exercise the selection flow.
 *   3. Assert nudge visibility at all boundaries (0, 5, 6 selected draft-recs).
 *   4. Clean up the seeded test data in afterAll.
 *
 * Known background noise: 419 CSRF on /api/lead-score/pql-signal,
 * ERR_TOO_MANY_REDIRECTS on polling XHRs when APP_URL diverges from localhost:8000,
 * and 401 from background polling during navigation. These are filtered as KNOWN_NOISE.
 */

import { execSync } from 'child_process';
import * as fs from 'fs';
import * as path from 'path';
import { fileURLToPath } from 'url';

import { expect, test } from '../fixtures/auth';

const __dirname = path.dirname(fileURLToPath(import.meta.url));
const SEED_DATA_PATH = path.join(__dirname, '..', '.seed-data.json');

test.describe.configure({ mode: 'serial' });

// ---------------------------------------------------------------------------
// Known background noise — filtered from console/request-fail assertions
// ---------------------------------------------------------------------------
const KNOWN_NOISE = [
  '/api/lead-score/pql-signal',  // 419 CSRF mismatch on analytics beacon
  'ERR_TOO_MANY_REDIRECTS',      // polling XHRs hitting https://rankwiz.test cross-origin
  '401 (Unauthorized)',          // background polling XHRs during navigation (auth cookie timing)
  'status of 401',               // same
  'Failed to fetch',             // offline polling after page teardown
  'net::ERR_FAILED',             // background XHR teardown noise
];

// ---------------------------------------------------------------------------
// Verbatim approved copy from SafeRefreshNudge.tsx
// ---------------------------------------------------------------------------
const NUDGE_TEXT_FRAGMENT = 'Google rewards small batches of genuinely edited pages';

// ---------------------------------------------------------------------------
// Seed-data helpers
// ---------------------------------------------------------------------------
interface SeedData {
  userId: number;
  siteId: number;
  analysisRunId: number;
  recommendationId: number;
  aiDraftId: number;
}

function loadSeedData(): SeedData {
  if (!fs.existsSync(SEED_DATA_PATH)) {
    throw new Error(`E2E seed data not found at ${SEED_DATA_PATH}. Run: php artisan e2e:seed --reset`);
  }
  return JSON.parse(fs.readFileSync(SEED_DATA_PATH, 'utf8')) as SeedData;
}

/**
 * Run PHP via artisan tinker and return stdout. Uses a temp file to avoid
 * shell-escaping issues with complex PHP code (same pattern as bulk-422 spec).
 */
function runPhp(phpCode: string): string {
  const tmpFile = path.join(
    '/private/tmp/claude-501/-Users-sood-dev-heatware-rankwiz/3c1bb58f-c746-494f-a1dd-24578564e129/scratchpad',
    `e2e_w2b03_${process.pid}_${Date.now()}.php`,
  );
  try {
    fs.writeFileSync(tmpFile, phpCode, 'utf8');
    return execSync(`php artisan tinker --execute="$(cat ${tmpFile})" 2>&1`, {
      cwd: process.cwd(),
      encoding: 'utf8',
      timeout: 30_000,
      shell: '/bin/bash',
    });
  } finally {
    try { fs.unlinkSync(tmpFile); } catch { /* ignore */ }
  }
}

function resolvePublicId(model: string, id: number): string {
  const out = runPhp(
    `$m = \\App\\Models\\${model}::find(${id}); echo $m ? $m->public_id : 'NOT_FOUND';`,
  );
  const match = out.match(/([0-9A-Z]{26})/);
  if (!match) throw new Error(`Could not resolve ${model}#${id} public_id. Output: ${out}`);
  return match[1];
}

// ---------------------------------------------------------------------------
// Per-describe state
// ---------------------------------------------------------------------------
let sitePublicId: string;
// Populated from Inertia page props in the first test that loads the page.
// SafeRefreshCapService scales by edit_completion_rate (floor(base×clamp(rate,0.3,1.0))).
// A new test user with zero published drafts gets rate=0 → 0.3 floor → cap=3 (not 5).
// Tests 3-5 must use the ACTUAL cap, not a hardcoded value.
let safeRefreshCap = 5; // overridden by readCapFromPage() before first use

// IDs of test data created in beforeAll — cleaned up in afterAll.
interface TestRec { findingId: number; recId: number; jobId: number; draftId: number; recPublicId: string }
const testRecs: TestRec[] = [];

// ---------------------------------------------------------------------------
// Test data seeding: 6 recommendations with AI drafts (has_draft = true)
// The golden path requires >cap (5) draft-recs to be selected, so we need
// at least 6 recommendations that have latestDraft loaded.
// ---------------------------------------------------------------------------
test.describe('W2B-03 SafeRefreshNudge — golden-path + boundary', () => {
  test.beforeAll(async () => {
    const seed = loadSeedData();

    // Resolve site public_id for URL construction.
    sitePublicId = resolvePublicId('Site', seed.siteId);

    // Seed 6 recommendations each with an AiDraft so has_draft = true.
    for (let i = 1; i <= 6; i++) {
      const urlSlug = `w2b03-nudge-${i}-${process.pid}`;
      const code = `
use Illuminate\\Support\\Facades\\DB;
use Illuminate\\Support\\Str;

$siteId   = ${seed.siteId};
$userId   = ${seed.userId};
$runId    = ${seed.analysisRunId};
$urlSlug  = '${urlSlug}';
$pageUrl  = 'https://test.w2b03.example.com/' . $urlSlug;
$pageHash = sha1($pageUrl);

$findingId = DB::table('findings')->insertGetId([
    'analysis_run_id' => $runId,
    'page_url'        => $pageUrl,
    'page_url_hash'   => $pageHash,
    'type'            => 'traffic_change',
    'direction'       => 'negative',
    'metric_before'   => 200,
    'metric_after'    => 80,
    'delta_absolute'  => -120,
    'delta_percent'   => -60,
    'segment_type'    => 'overall',
    'created_at'      => now(),
]);

$recId = DB::table('recommendations')->insertGetId([
    'public_id'                 => (string) Str::ulid(),
    'analysis_run_id'           => $runId,
    'finding_id'                => $findingId,
    'page_url'                  => $pageUrl,
    'page_url_hash'             => $pageHash,
    'action_type'               => 'content_rewrite',
    'impact_score'              => 60,
    'confidence_score'          => 75,
    'title'                     => 'W2B03 Nudge Test Page ${i}',
    'reasoning'                 => 'Verification test for W2B-03 safe-refresh nudge spec.',
    'evidence'                  => json_encode(['clicks_before'=>200,'clicks_after'=>80,'delta_percent'=>-60]),
    'lifecycle_status'          => 'pending',
    'cached_clicks_recoverable' => 0,
    'created_at'                => now(),
    'updated_at'                => now(),
]);

$job = \\App\\Models\\AiJob::factory()->create([
    'analysis_run_id' => $runId,
    'site_id'         => $siteId,
    'user_id'         => $userId,
    'status'          => 'completed',
]);

$draft = \\App\\Models\\AiDraft::factory()->create([
    'recommendation_id' => $recId,
    'ai_job_id'         => $job->id,
]);

$rec = \\App\\Models\\Recommendation::find($recId);

echo json_encode([
    'findingId'    => $findingId,
    'recId'        => $recId,
    'jobId'        => $job->id,
    'draftId'      => $draft->id,
    'recPublicId'  => $rec->public_id,
]);
`;
      const out = runPhp(code);
      const jsonMatch = out.match(/(\{[^}]+\})/);
      if (!jsonMatch) throw new Error(`Seeding rec #${i} failed. Output: ${out}`);
      testRecs.push(JSON.parse(jsonMatch[1]) as TestRec);
    }
  });

  test.afterAll(async () => {
    // Clean up in reverse dependency order: draft → aijob → recommendation → finding
    for (const tr of testRecs) {
      const code = `
use Illuminate\\Support\\Facades\\DB;
DB::table('ai_drafts')->where('id', ${tr.draftId})->delete();
\\App\\Models\\AiJob::find(${tr.jobId})?->forceDelete();
DB::table('recommendations')->where('id', ${tr.recId})->delete();
DB::table('findings')->where('id', ${tr.findingId})->delete();
echo 'ok';
`;
      runPhp(code);
    }
  });

  // ─────────────────────────────────────────────────────────────────────────
  // Test 1: Page renders without console errors or failed XHR
  // ─────────────────────────────────────────────────────────────────────────
  test('recommendations page renders without console errors', async ({ page }) => {
    const consoleErrors: string[] = [];
    const failedRequests: string[] = [];

    page.on('console', (msg) => {
      if (msg.type() === 'error') {
        const text = msg.text();
        if (!KNOWN_NOISE.some((n) => text.includes(n))) {
          consoleErrors.push(text);
        }
      }
    });

    page.on('requestfailed', (req) => {
      const url = req.url();
      const failure = req.failure()?.errorText ?? '';
      if (!KNOWN_NOISE.some((n) => url.includes(n) || failure.includes(n))) {
        failedRequests.push(`${url} — ${failure}`);
      }
    });

    page.on('response', (res) => {
      const url = res.url();
      // 4xx/5xx on XHR (Inertia) requests are errors — ignore known noise
      if (
        res.status() >= 400 &&
        res.request().headers()['x-inertia'] &&
        !KNOWN_NOISE.some((n) => url.includes(n))
      ) {
        failedRequests.push(`${url} — HTTP ${res.status()}`);
      }
    });

    await page.goto(`/sites/${sitePublicId}/recommendations`, { waitUntil: 'domcontentloaded' });

    // Page heading must be visible — confirms Inertia hydrated correctly
    await expect(
      page.getByRole('heading', { name: /recommendations/i }).first(),
    ).toBeVisible({ timeout: 10_000 });

    // The seeded test recs must appear in the list
    await expect(
      page.getByText('W2B03 Nudge Test Page 1').first(),
    ).toBeVisible({ timeout: 10_000 });

    // No uncaught console errors (Inertia render failures always surface here)
    expect(consoleErrors, `Console errors: ${consoleErrors.join('; ')}`).toHaveLength(0);
  });

  // ─────────────────────────────────────────────────────────────────────────
  // Test 2: Nudge absent when nothing is selected (zero-selection sad path)
  // ─────────────────────────────────────────────────────────────────────────
  test('nudge is absent when no recommendations are selected', async ({ page }) => {
    await page.goto(`/sites/${sitePublicId}/recommendations`, { waitUntil: 'domcontentloaded' });

    await expect(
      page.getByRole('heading', { name: /recommendations/i }).first(),
    ).toBeVisible({ timeout: 10_000 });

    // BulkActionsBar returns null when nothing is selected, so role=toolbar should not exist
    await expect(page.getByRole('toolbar', { name: 'Bulk actions' })).not.toBeVisible();

    // Nudge specifically must not be in the DOM
    await expect(page.locator('[role="note"]')).not.toBeVisible();
  });

  // ─────────────────────────────────────────────────────────────────────────
  // Helper: read the actual safe_refresh_cap from the Inertia page props.
  // SafeRefreshCapService scales by edit_completion_rate: for a new test user
  // with zero published drafts, rate=0 → clamped to 0.3 → cap=3, not 5.
  // Must be called after page load to populate the module-level safeRefreshCap.
  // ─────────────────────────────────────────────────────────────────────────
  async function readCapFromPage(page: import('@playwright/test').Page): Promise<number> {
    const cap = await page.evaluate((): number | null => {
      try {
        const el = document.getElementById('app');
        const data = JSON.parse(el?.getAttribute('data-page') ?? '{}') as { props?: { safe_refresh_cap?: number } };
        return typeof data.props?.safe_refresh_cap === 'number' ? data.props.safe_refresh_cap : null;
      } catch {
        return null;
      }
    });
    if (cap !== null && cap > 0) {
      safeRefreshCap = cap;
    }
    return safeRefreshCap;
  }

  // ─────────────────────────────────────────────────────────────────────────
  // Helper: select N test-rec checkboxes via direct DOM .click().
  // The RecommendationCard renders as an interactive <div> with cursor-pointer
  // that occupies the same coordinates as the checkbox inside it. Playwright's
  // coordinate-based hit-test (even with force:true) routes clicks to the Card
  // div rather than the checkbox, because the browser's elementFromPoint()
  // returns the card element at those coordinates.
  // Using page.evaluate() to call el.click() dispatches the event directly on
  // the HTMLInputElement, bypassing coordinate routing. This is the correct
  // pattern for controlled React checkboxes where the element exists in the DOM
  // but is visually covered by a sibling/parent in the stacking context.
  // ─────────────────────────────────────────────────────────────────────────
  /**
   * Click N test-rec checkboxes via direct DOM el.click(), waiting after each
   * click for the toolbar count to confirm React has committed the state update.
   *
   * WHY el.click() instead of Playwright check():
   *  The RecommendationCard renders as an interactive <div> (cursor-pointer) at
   *  the same coordinates as the checkbox. Playwright's coordinate-based hit-test
   *  (even with force:true) routes the event to the Card div, not the checkbox.
   *  el.click() dispatches the click event directly on the HTMLInputElement,
   *  bypassing coordinate routing entirely.
   *
   * WHY waitForFunction between clicks:
   *  React 18 automatic batching defers setState flushes to the scheduler queue
   *  (via MessageChannel). A fast Playwright evaluate() loop can dispatch the
   *  next click BEFORE React commits the previous one, producing a race where
   *  only some clicks register. waitForFunction polls the DOM until the toolbar's
   *  text content shows the expected count, guaranteeing React has flushed before
   *  the next click is sent.
   */
  async function clickTestCheckboxes(page: import('@playwright/test').Page, count: number): Promise<void> {
    // Wait for React hydration to complete before any click.
    // The SSR HTML renders all DOM nodes immediately on domcontentloaded, but
    // React's event-delegation listeners are NOT attached until hydrateRoot()
    // completes. Clicking via el.click() before hydration fires a native DOM
    // event that React never sees — the click registers natively (checked→true)
    // but React resets it to false when hydration reconciles the DOM.
    //
    // React 18 attaches two internal properties to the root container (#app):
    //   __reactContainer$<hash>  — the Fiber root (hydrateRoot return value)
    //   _reactListening<hash>    — the event delegation listener registry
    // Both are set during/after hydrateRoot(). We use getOwnPropertyNames
    // (not Object.keys) because these may be non-enumerable in some builds.
    await page.waitForFunction(
      () => {
        const el = document.getElementById('app');
        if (!el) return false;
        const names = Object.getOwnPropertyNames(el);
        return names.some(
          (k) => k.startsWith('__reactContainer') || k.startsWith('_reactListening'),
        );
      },
      { timeout: 15_000 },
    );

    for (let i = 0; i < count; i++) {
      // el.click() triggers native checkbox toggle behavior AND fires the click event.
      // React's delegated click handler fires, sees checked changed, fires synthetic onChange.
      // onToggleSelection → handleToggleSelection(id) → cohortToggleRow(id) → setState
      const diag = await page.evaluate((index) => {
        const boxes = Array.from(
          document.querySelectorAll<HTMLInputElement>(
            'input[type="checkbox"][aria-label*="W2B03 Nudge Test Page"]',
          ),
        );
        const cb = boxes[index];
        if (!cb) return { err: `no checkbox at index ${index}` };
        const before = cb.checked;
        let clickFired = false;
        cb.addEventListener('click', () => { clickFired = true; }, { once: true });
        cb.click();
        return {
          index,
          label: cb.getAttribute('aria-label')?.slice(-20),
          before,
          after: cb.checked,
          clickFired,
        };
      }, i);
      console.log(`[click ${i}]`, JSON.stringify(diag));

      // Wait for React to commit this click by watching the toolbar count.
      const expectedCount = i + 1;
      await page.waitForFunction(
        (n: number) => {
          const toolbar = document.querySelector('[role="toolbar"][aria-label="Bulk actions"]');
          return toolbar?.textContent?.includes(`${n} recommendation`) === true;
        },
        expectedCount,
        { timeout: 8_000 },
      );
    }
  }

  // ─────────────────────────────────────────────────────────────────────────
  // Test 3: Nudge absent at exactly the cap boundary
  //
  // The actual cap is dynamic (SafeRefreshCapService × edit_completion_rate).
  // For a new test user with zero published drafts: rate=0 → 0.3 floor →
  // cap = floor(10 × 0.3) = 3. We read it from the page instead of hardcoding.
  // ─────────────────────────────────────────────────────────────────────────
  test('nudge is absent when exactly cap draft-recommendations are selected', async ({ page }) => {
    await page.goto(`/sites/${sitePublicId}/recommendations`, { waitUntil: 'domcontentloaded' });

    await expect(
      page.getByText('W2B03 Nudge Test Page 1').first(),
    ).toBeVisible({ timeout: 10_000 });

    // Read the actual cap from Inertia page props (sets module-level safeRefreshCap).
    const cap = await readCapFromPage(page);

    // Ensure we have enough test recs to reach cap (we seeded 6).
    const allTestCheckboxes = page.locator(
      'input[type="checkbox"][aria-label*="W2B03 Nudge Test Page"]',
    );
    await expect(allTestCheckboxes).toHaveCount(6, { timeout: 8_000 });

    // Select exactly cap test recs via direct DOM el.click().
    await clickTestCheckboxes(page, cap);

    // BulkActionsBar should be visible with cap selected
    const toolbar = page.getByRole('toolbar', { name: 'Bulk actions' });
    await expect(toolbar).toBeVisible({ timeout: 5_000 });
    await expect(toolbar).toContainText(
      new RegExp(`${cap} recommendation`),
    );

    // Nudge must NOT appear at exactly cap (SafeRefreshNudge renders when selectedCount > cap)
    await expect(page.locator('[role="note"]')).not.toBeVisible();
  });

  // ─────────────────────────────────────────────────────────────────────────
  // Test 4: Golden path — nudge appears when cap+1 draft-recs selected (> cap)
  // ─────────────────────────────────────────────────────────────────────────
  test('nudge appears with verbatim copy when selected draft-recs exceed cap', async ({ page }) => {
    await page.goto(`/sites/${sitePublicId}/recommendations`, { waitUntil: 'domcontentloaded' });

    await expect(
      page.getByText('W2B03 Nudge Test Page 1').first(),
    ).toBeVisible({ timeout: 10_000 });

    const cap = await readCapFromPage(page);
    const overCapCount = cap + 1; // needs ≤ 6 seeded test recs; guaranteed for cap ≤ 5

    const allTestCheckboxes = page.locator(
      'input[type="checkbox"][aria-label*="W2B03 Nudge Test Page"]',
    );
    await expect(allTestCheckboxes).toHaveCount(6, { timeout: 8_000 });

    // Click cap+1 checkboxes to exceed the threshold.
    await clickTestCheckboxes(page, overCapCount);

    // Toolbar must show overCapCount selected
    const toolbar = page.getByRole('toolbar', { name: 'Bulk actions' });
    await expect(toolbar).toBeVisible({ timeout: 5_000 });
    await expect(toolbar).toContainText(new RegExp(`${overCapCount} recommendation`));

    // The nudge MUST appear — SafeRefreshNudge renders role=note when selectedCount > cap
    const nudge = page.locator('[role="note"]');
    await expect(nudge).toBeVisible({ timeout: 5_000 });

    // Verbatim copy check — the exact text from SafeRefreshNudge.tsx
    await expect(nudge).toContainText(NUDGE_TEXT_FRAGMENT);
    await expect(nudge).toContainText(
      'Your publish limit grows as you actually edit drafts — so refreshes stay surgical, not scaled.',
    );

    // AlertTriangle icon must be present (aria-hidden — checked via DOM presence)
    await expect(nudge.locator('svg').first()).toBeAttached();
  });

  // ─────────────────────────────────────────────────────────────────────────
  // Test 5: Nudge disappears when selection drops back to cap
  // ─────────────────────────────────────────────────────────────────────────
  test('nudge disappears when selected draft-recs drop back to cap', async ({ page }) => {
    await page.goto(`/sites/${sitePublicId}/recommendations`, { waitUntil: 'domcontentloaded' });

    await expect(
      page.getByText('W2B03 Nudge Test Page 1').first(),
    ).toBeVisible({ timeout: 10_000 });

    const cap = await readCapFromPage(page);
    const overCapCount = cap + 1;

    const allTestCheckboxes = page.locator(
      'input[type="checkbox"][aria-label*="W2B03 Nudge Test Page"]',
    );
    await expect(allTestCheckboxes).toHaveCount(6, { timeout: 8_000 });

    // Select cap+1 to trigger nudge.
    await clickTestCheckboxes(page, overCapCount);

    const nudge = page.locator('[role="note"]');
    await expect(nudge).toBeVisible({ timeout: 5_000 });

    // Deselect the first test-rec checkbox (toggle: checked → unchecked) to drop to cap.
    // After the React state update, selectedDraftIds.length = cap ≤ cap → nudge disappears.
    await page.evaluate(() => {
      const cb = document.querySelector<HTMLInputElement>(
        'input[type="checkbox"][aria-label*="W2B03 Nudge Test Page"]',
      );
      if (cb?.checked) cb.click();
    });

    // Wait for React to flush the deselect (toolbar drops to cap selected)
    await page.waitForFunction(
      (n: number) => {
        const t = document.querySelector('[role="toolbar"][aria-label="Bulk actions"]');
        return t?.textContent?.includes(`${n} recommendation`) === true;
      },
      cap,
      { timeout: 8_000 },
    );

    // Nudge must disappear — selectedDraftIds.length = cap ≤ cap
    await expect(nudge).not.toBeVisible({ timeout: 5_000 });

    // Toolbar still visible with cap selected
    const toolbar = page.getByRole('toolbar', { name: 'Bulk actions' });
    await expect(toolbar).toContainText(new RegExp(`${cap} recommendation`));
  });

  // ─────────────────────────────────────────────────────────────────────────
  // Test 6: Unauthenticated access is blocked (permission sad path)
  // ─────────────────────────────────────────────────────────────────────────
  test('unauthenticated access to recommendations page redirects to login', async ({ browser }) => {
    const context = await browser.newContext({
      storageState: { cookies: [], origins: [] },
    });
    const page = await context.newPage();

    await page.goto(`/sites/${sitePublicId}/recommendations`);
    await expect(page).toHaveURL(/\/login/, { timeout: 8_000 });

    await context.close();
  });

  // ─────────────────────────────────────────────────────────────────────────
  // Test 7: safe_refresh_cap prop is present in Inertia page state (controller contract)
  // ─────────────────────────────────────────────────────────────────────────
  test('safe_refresh_cap prop is present in Inertia page data', async ({ page }) => {
    await page.goto(`/sites/${sitePublicId}/recommendations`, { waitUntil: 'domcontentloaded' });

    await expect(
      page.getByRole('heading', { name: /recommendations/i }).first(),
    ).toBeVisible({ timeout: 10_000 });

    // Verify the Inertia page state contains safe_refresh_cap = 5 (SafeRefreshCapService default)
    const cap = await page.evaluate((): number | null => {
      type InertiaWindow = { page?: { props?: { safe_refresh_cap?: number } } };
      const w = window as unknown as { __inertia?: InertiaWindow; Inertia?: { page?: { props?: { safe_refresh_cap?: number } } } };
      // Inertia v2 stores the current page in Inertia.page
      const inertiaPage = w.__inertia?.page ?? w.Inertia?.page;
      if (inertiaPage?.props?.safe_refresh_cap !== undefined) {
        return inertiaPage.props.safe_refresh_cap;
      }
      // Fallback: read from the #app data-page attribute (initial load)
      const appEl = document.getElementById('app');
      if (appEl) {
        try {
          const data = JSON.parse(appEl.getAttribute('data-page') ?? '{}') as { props?: { safe_refresh_cap?: number } };
          return data.props?.safe_refresh_cap ?? null;
        } catch { return null; }
      }
      return null;
    });

    // SafeRefreshCapService returns 5 in the current implementation
    expect(cap).toBeGreaterThan(0);
  });
});
