"""Regression checks for #856 pinned-star layout in the session list."""

from pathlib import Path


SESSIONS_JS = (Path(__file__).resolve().parent.parent / "static" / "sessions.js").read_text()
STYLE_CSS = (Path(__file__).resolve().parent.parent / "static" / "style.css").read_text()


def test_pinned_indicator_renders_inside_title_row():
    title_row_idx = SESSIONS_JS.find("titleRow.className='session-title-row';")
    assert title_row_idx != -1, "session title row construction not found"

    assert ("body.appendChild(_renderOneSession(s, Boolean(g.isPinned)))" in SESSIONS_JS
            or "body.appendChild(parentEl)" in SESSIONS_JS)
    assert "function _renderOneSession(s, isPinnedGroup=false)" in SESSIONS_JS
    assert "if(s.pinned&&!isPinnedGroup){" in SESSIONS_JS

    pin_idx = SESSIONS_JS.find("pinInd.className='session-pin-indicator';", title_row_idx)
    assert pin_idx != -1, "pinned indicator creation not found after title row"

    append_to_title_row_idx = SESSIONS_JS.find("titleRow.appendChild(pinInd);", pin_idx)
    assert append_to_title_row_idx != -1, "pinned indicator should be appended to titleRow"

    append_to_el_idx = SESSIONS_JS.find("el.appendChild(pinInd);", pin_idx)
    assert append_to_el_idx == -1, (
        "pinned indicator should not be appended to the outer session row; "
        "it must align inside the title row with the spinner/unread indicator"
    )


def test_pinned_indicator_uses_fixed_indicator_box():
    assert ".session-pin-indicator{" in STYLE_CSS, "session pin indicator CSS block missing"
    css_block = STYLE_CSS[STYLE_CSS.find(".session-pin-indicator{"):STYLE_CSS.find(".session-pin-indicator svg{")]
    assert "width:10px;" in css_block, "pin indicator should reserve a fixed 10px width"
    assert "height:10px;" in css_block, "pin indicator should reserve a fixed 10px height"
    assert "justify-content:center;" in css_block, "pin indicator should center the star inside its box"


def test_state_indicator_uses_right_actions_slot_to_prevent_title_shift():
    """State span reuses the right-side action slot so the title start position
    does not shift when the spinner or unread dot appears/disappears."""
    title_row_idx = SESSIONS_JS.find("titleRow.className='session-title-row';")
    assert title_row_idx != -1, "title row construction not found"

    title_row_append_idx = SESSIONS_JS.find("titleRow.appendChild(state);", title_row_idx)
    assert title_row_append_idx == -1, (
        "state indicator should not be inserted before the title; it should reuse "
        "the right-side actions slot to avoid title shift"
    )

    state_idx = SESSIONS_JS.find("state.className='session-attention-indicator session-state-indicator'")
    assert state_idx != -1, "right-side attention indicator creation not found"

    append_to_row_idx = SESSIONS_JS.find("el.appendChild(state);", state_idx)
    assert append_to_row_idx != -1, "state indicator should be appended to the outer row"

    actions_idx = SESSIONS_JS.find("actions.className='session-actions';", append_to_row_idx)
    assert actions_idx != -1, "session actions should still be appended after attention indicator"

    assert ".session-attention-indicator{" in STYLE_CSS, "attention indicator CSS rule missing"
    css_block = STYLE_CSS[
        STYLE_CSS.find(".session-attention-indicator{"):
        STYLE_CSS.find(".session-item:hover .session-attention-indicator")
    ]
    assert "position:absolute;" in css_block, "attention indicator should be positioned in the row action slot"
    assert "right:6px;" in css_block, "attention indicator should align with the actions trigger"
    assert "width:26px;" in css_block, "attention indicator should use the same width as the actions trigger"
    assert "height:26px;" in css_block, "attention indicator should use the same height as the actions trigger"
    assert ".session-attention-indicator.is-streaming::before{" in STYLE_CSS
    inner_spinner_block = STYLE_CSS[
        STYLE_CSS.find(".session-attention-indicator.is-streaming::before{"):
        STYLE_CSS.find(".session-attention-indicator.is-unread::before{")
    ]
    assert "width:10px;" in inner_spinner_block, "spinner glyph should stay 10px inside the 26px action slot"
    assert "height:10px;" in inner_spinner_block, "spinner glyph should stay 10px inside the 26px action slot"

    hover_rule = ".session-item:hover .session-attention-indicator"
    assert hover_rule in STYLE_CSS, "hover rule should hide attention indicator when actions appear"


def test_timestamp_hidden_when_attention_state_is_present():
    assert "+(hasUnread?' unread':'')+(attention?' needs-attention':'')+attentionClass" in SESSIONS_JS
    assert "const hasAttentionState=isStreaming||hasUnread||Boolean(attention);" in SESSIONS_JS
    assert "ts.className='session-time'+(hasAttentionState?' is-hidden':'');" in SESSIONS_JS
    assert "ts.textContent=hasAttentionState?'':_formatRelativeSessionTime(tsMs);" in SESSIONS_JS
    assert ".session-time.is-hidden{display:none;}" in STYLE_CSS
    # padding-right was 86px when the timestamp was position:absolute. Now that
    # the timestamp lives in the flex flow of .session-title-row, the rest
    # state needs no right reservation; hover/streaming/unread/menu-open/
    # focus-within all expand to 40px to make room for the absolute action
    # button + attention indicator.
    assert ".session-item{padding:8px 8px;" in STYLE_CSS
    # PR #1110: :hover removed from the COMBINED padding-right rule (touch layout-shift fix).
    # Instead, hover padding is restored via @media (hover:hover) which only applies to
    # devices with a real hover capability (mouse). Touch/iPad devices satisfy hover:none
    # and skip that block, preventing the layout-reflow mid-tap bug.
    assert ".session-item.streaming,.session-item.unread,.session-item.needs-attention,.session-item:focus-within,.session-item.menu-open{padding-right:40px;}" in STYLE_CSS
    # Desktop hover padding restored via media query (mouse devices only)
    assert "@media (hover:hover)" in STYLE_CSS
    assert ".session-item:hover{padding-right:40px;}" in STYLE_CSS
    assert ".session-item{min-height:44px;padding:10px 40px 10px 12px;}" in STYLE_CSS
    # Timestamp now uses margin-left:auto inside the flex row instead of
    # absolute positioning. This stops the title's flex:1 bound from running
    # underneath the timestamp and lets the project dot sit beside it.
    # Anchor on the canonical UNSCOPED `.session-time{` rule (start-of-line) —
    # a skin-scoped variant like `:root[data-skin="graphite"] .session-item.active
    # .session-time{` can precede it and would otherwise widen the slice into
    # unrelated skin rules that legitimately use position:absolute.
    import re as _re
    _m = _re.search(r"(?m)^\s*\.session-time\{", STYLE_CSS)
    assert _m, "canonical unscoped .session-time{ rule not found"
    session_time_block = STYLE_CSS[
        _m.start():
        STYLE_CSS.find("}", _m.start())
    ]
    assert "position:absolute" not in session_time_block, (
        "Timestamp must live in flex flow (margin-left:auto), not absolute"
    )
    assert "margin-left:auto" in session_time_block
    assert ".session-item:hover .session-time" in STYLE_CSS
    assert ".session-item.streaming:not(:hover):not(:focus-within):not(.menu-open) .session-actions" in STYLE_CSS
    assert ".session-item.unread:not(:hover):not(:focus-within):not(.menu-open) .session-actions" in STYLE_CSS


def test_plain_mouse_hover_does_not_mark_session_row_dragging():
    """Pointermove fires during ordinary hover; drag styling must require an active press."""
    assert "let _gestureState='idle';" in SESSIONS_JS
    assert "_gestureState='pressing';" in SESSIONS_JS
    assert "if(_gestureState==='idle') return false;" in SESSIONS_JS
    assert "_gestureState='idle';" in SESSIONS_JS
    assert ".session-item.dragging:hover" in STYLE_CSS


def test_sidebar_uses_local_inflight_state_for_immediate_spinner():
    messages_js = (Path(__file__).resolve().parent.parent / "static" / "messages.js").read_text()

    assert "function _isSessionLocallyStreaming(s)" in SESSIONS_JS
    assert "isActive && Boolean(S.busy)" in SESSIONS_JS
    assert "function _purgeStaleInflightEntries()" in SESSIONS_JS
    assert "delete INFLIGHT[sid];" in SESSIONS_JS
    assert "function _isSessionEffectivelyStreaming(s)" in SESSIONS_JS
    assert "const isStreaming=_isSessionEffectivelyStreaming(s);" in SESSIONS_JS
    assert "if(typeof renderSessionListFromCache==='function') renderSessionListFromCache();" in messages_js


def test_date_group_caret_expanded_down_collapsed_right():
    assert "caret.textContent='\\u25BE';" in SESSIONS_JS
    assert ".session-date-caret{" in STYLE_CSS
    caret_block = STYLE_CSS[
        STYLE_CSS.find(".session-date-caret{"):
        STYLE_CSS.find(".session-date-caret.collapsed")
    ]
    assert "transform:rotate(0deg);" in caret_block
    assert ".session-date-caret.collapsed{transform:rotate(-90deg);}" in STYLE_CSS


def test_apperror_path_calls_render_session_list():
    """apperror handler must call renderSessionList() to clear the streaming indicator
    immediately rather than waiting for the 5s streaming poll interval."""
    messages_js = (Path(__file__).resolve().parent.parent / "static" / "messages.js").read_text()
    apperror_idx = messages_js.find("source.addEventListener('apperror'")
    assert apperror_idx != -1, "apperror handler not found in messages.js"
    warning_idx = messages_js.find("source.addEventListener('warning'", apperror_idx)
    assert warning_idx != -1, "warning handler not found after apperror handler"
    apperror_block = messages_js[apperror_idx:warning_idx]
    assert "renderSessionList()" in apperror_block, (
        "apperror handler must call renderSessionList() so the streaming indicator "
        "clears immediately on server errors, not after a 5s poll delay"
    )


def test_pointerup_ignores_non_primary_mouse_buttons():
    """Right-click and middle-click must not trigger session navigation.
    onpointerup fires for all mouse buttons; we filter to button===0
    (primary). pointerType==='mouse' scopes the check to mouse only —
    touch/stylus always report button===0 so they're unaffected."""
    assert "e.pointerType==='mouse' && e.button!==0" in SESSIONS_JS, (
        "pointerup handler must filter out non-primary mouse buttons "
        "(right-click / middle-click must not navigate)"
    )
