"""Playwright helpers for Phase-2 recipe tests (plan §4.2).

Centralizes the `page.goto(...)` retry loop that absorbs transient network errors (F2-3 / F2-5):
Playwright's `page.goto` raises `PlaywrightError` on transport-level failures (`net::ERR_*`,
connection resets, CDP target gone) — those escape a naive loop that only retries on status
mismatches. Wrap every install-overlay `page.goto` in this helper so transient errors retry
without weakening the underlying assertion (same pattern as F1e-1's `exec_in_app` poll+raise
hardening).
"""

from __future__ import annotations

import time


def goto_with_retry(
    page,
    url,
    *,
    deadline_seconds: int = 120,
    accept_statuses=(200, 304),
    goto_timeout_ms: int = 30_000,
    wait_until: str = "domcontentloaded",
):
    """Poll `page.goto(url)` until status is in `accept_statuses` OR the deadline expires.

    Returns the final Playwright response. Raises AssertionError if the deadline expires without
    a successful status. Each iteration catches `PlaywrightError` (and any other exception) so
    transient network failures retry rather than fail the test.

    Use case: recipe install overlays where the app's HTTP layer may be up (status 200 to
    /healthz or generic readiness) but the requested route is still registering (404), or
    Playwright's CDP connection transiently flakes (`net::ERR_NETWORK_CHANGED`).
    """
    # Imported lazily so this module can be imported without playwright at unit-test time.
    try:
        from playwright.sync_api import Error as PlaywrightError
    except ImportError:  # pragma: no cover — playwright is always installed in cc-ci-run
        PlaywrightError = Exception  # noqa: N806

    deadline = time.time() + deadline_seconds
    resp = None
    last_status = 0
    last_err = ""
    attempts = 0
    while time.time() < deadline:
        attempts += 1
        try:
            resp = page.goto(url, wait_until=wait_until, timeout=goto_timeout_ms)
        except PlaywrightError as e:
            last_err = str(e)
            resp = None
            last_status = 0
        else:
            last_status = resp.status if resp is not None else 0
        if last_status in accept_statuses:
            return resp
        time.sleep(3)
    raise AssertionError(
        f"page.goto({url}) never returned a status in {accept_statuses} after "
        f"{attempts} attempts ({deadline_seconds}s); last status={last_status}, "
        f"last error={last_err or 'none'}"
    )
