"""Regression coverage for #3479: iOS Safari must not see a top-scroll frame.

The live token path writes into an existing streaming-markdown DOM node. The
jump came from discrete transcript rebuilds and card replacement paths, so these
tests pin those call sites rather than the per-token renderer.
"""

from pathlib import Path


ROOT = Path(__file__).resolve().parents[1]
UI_JS = (ROOT / "static" / "ui.js").read_text(encoding="utf-8")
SESSIONS_JS = (ROOT / "static" / "sessions.js").read_text(encoding="utf-8")
STYLE_CSS = (ROOT / "static" / "style.css").read_text(encoding="utf-8")


def _function_body(src: str, name: str) -> str:
    marker = f"function {name}"
    start = src.find(marker)
    assert start >= 0, f"{name} not found"
    brace = src.find("{", start)
    assert brace >= 0, f"{name} body not found"
    depth = 0
    for pos in range(brace, len(src)):
        ch = src[pos]
        if ch == "{":
            depth += 1
        elif ch == "}":
            depth -= 1
            if depth == 0:
                return src[start : pos + 1]
    raise AssertionError(f"{name} body did not terminate")


def _compact(text: str) -> str:
    return "".join(text.split())


def test_refresh_session_uses_same_frame_scroll_snapshot_restore():
    body = _function_body(UI_JS, "refreshSession")

    assert "syncTopbar(); _renderMessagesWithScrollSnapshot();" in body
    assert "syncTopbar(); renderMessages();" not in body


def test_handoff_rebuilds_use_same_frame_scroll_snapshot_restore():
    clear_body = _function_body(UI_JS, "clearHandoffUi")
    set_body = _function_body(UI_JS, "setHandoffUi")

    assert "_renderMessagesWithScrollSnapshot();" in clear_body
    assert "_renderMessagesWithScrollSnapshot();" in set_body
    assert "renderMessages();" not in clear_body
    assert "renderMessages();" not in set_body


def test_live_compression_card_replacement_restores_snapshot_before_follow_settle():
    body = _function_body(UI_JS, "appendLiveCompressionCard")

    capture_idx = body.index("const scrollSnapshot=_captureMessageScrollSnapshot();")
    replace_idx = body.index("if(existing) existing.replaceWith(node);")
    restore_idx = body.index("_restoreMessageScrollSnapshotSameFrame(scrollSnapshot);")
    settle_idx = body.index("if(typeof scrollIfPinned==='function') scrollIfPinned();")

    assert capture_idx < replace_idx < restore_idx < settle_idx


def test_live_anchor_worklog_rebuild_restores_snapshot_before_follow_settle():
    body = _function_body(UI_JS, "renderLiveAnchorActivityScene")

    capture_idx = body.index("const scrollSnapshot=_captureMessageScrollSnapshot();")
    guard_idx = body.index("const scrollRebuildGuard=_prepareLiveAnchorScrollRebuildGuard(scrollSnapshot);")
    remove_idx = body.index("blocks.querySelectorAll('[data-anchor-scene-owner=\"1\"],[data-anchor-scene-row=\"1\"]')")
    restore_detail_idx = body.index("_restoreWorklogDetailDisclosureState(blocks, liveDisclosureState);")
    dedupe_idx = body.index("_dedupeLiveProcessedWorklogAnchors(turn);")
    move_status_idx = body.index("_moveLiveRunStatusToTurnEnd();")
    restore_idx = body.index("_restoreMessageScrollSnapshotSameFrame(scrollSnapshot);")
    release_idx = body.index("_restoreLiveAnchorScrollSnapshotAfterRebuild(scrollSnapshot,scrollRebuildGuard);")
    settle_idx = body.index("if(!scrollRebuildGuard.readerAwayFromBottom&&typeof scrollIfPinned==='function') scrollIfPinned();")

    assert capture_idx < guard_idx < remove_idx < restore_detail_idx < dedupe_idx < move_status_idx < restore_idx < release_idx < settle_idx


def test_live_anchor_worklog_rebuild_guards_height_for_unpinned_reader():
    guard = _function_body(UI_JS, "_prepareLiveAnchorScrollRebuildGuard")
    compact = _compact(guard)

    assert "constbeforeBottomDistance=Math.max(0,messagesEl.scrollHeight-messagesEl.scrollTop-messagesEl.clientHeight);" in compact
    assert "beforeBottomDistance>250&&(_messageUserUnpinned||_scrollPinned===false)" in compact
    assert "scrollSnapshot.pinned=false;" in compact
    assert "scrollSnapshot.userUnpinned=true;" in compact
    assert "scrollSnapshot.bottom=beforeBottomDistance;" in compact
    assert "_messageUserUnpinned=true;" in compact
    assert "_scrollPinned=false;" in compact
    assert "_nearBottomCount=0;" in compact
    assert "msgInner.style.minHeight=`${guardHeight}px`;" in compact


def test_same_frame_snapshot_preserves_bottom_distance_and_unpinned_state():
    capture = _function_body(UI_JS, "_captureMessageScrollSnapshot")
    restore = _function_body(UI_JS, "_restoreMessageScrollSnapshotSameFrame")
    wrapper = _function_body(UI_JS, "_renderMessagesWithScrollSnapshot")

    assert "bottom" in capture
    assert "readerAwayFromBottom?false:_shouldFollowMessagesOnDomReplace()" in _compact(capture)
    assert "readerAwayFromBottom?true:_messageUserUnpinned" in _compact(capture)
    assert "maxTop-Math.max(0,bottom)" in restore
    assert "_messageUserUnpinned=true" in restore
    assert "_scrollPinned=false" in restore
    assert "renderMessages({...(options||{}),preserveScroll:true});" in wrapper
    assert "_restoreMessageScrollSnapshotSameFrame(scrollSnapshot);" in wrapper


def test_preserve_scroll_restores_reader_away_from_bottom_before_following():
    body = _function_body(UI_JS, "_scrollAfterMessageRender")
    compact = _compact(body)

    reader_idx = compact.index("constreaderAwayFromBottom=")
    follow_idx = compact.index("if(!readerAwayFromBottom&&!_messageUserUnpinned&&_followMessagesAfterDomReplace())return;")
    restore_idx = compact.index("_restoreMessageScrollSnapshot(scrollSnapshot);")

    assert "Number(scrollSnapshot.bottom)>250" in compact
    assert reader_idx < follow_idx < restore_idx


def test_scroll_snapshot_restore_reinstates_unpinned_state_when_reader_is_mid_answer():
    restore = _function_body(UI_JS, "_restoreMessageScrollSnapshot")
    compact = _compact(restore)

    assert "constbottomDistance=el.scrollHeight-el.scrollTop-el.clientHeight;" in compact
    assert "if(bottomDistance>250)" in compact
    assert "_messageUserUnpinned=true" in compact
    assert "_scrollPinned=false" in compact


def test_same_session_force_refresh_does_not_reset_scroll_direction_tracker():
    body = _function_body(SESSIONS_JS, "loadSession")
    compact = _compact(body)

    assert "constcurrentSid=S.session?S.session.session_id:null;" in compact
    assert "if(currentSid!==sid&&typeofwindow!=='undefined'&&typeofwindow._resetScrollDirectionTracker==='function')" in compact


def test_clarify_card_is_height_clamped_and_scrollable_on_mobile_viewports():
    compact = _compact(STYLE_CSS)

    assert ".clarify-card" in STYLE_CSS
    assert "max-height:clamp(180px,min(68vh,calc(100vh-220px)),420px)" in compact
    assert "@supports(height:100dvh)" in compact
    assert "max-height:clamp(180px,min(62dvh,calc(100dvh-180px)),360px)" in compact
    assert "overflow-y:auto" in compact
    assert "-webkit-overflow-scrolling:touch" in compact
    assert "overscroll-behavior:contain" in compact
