"""Behavioral regression locks for #4295 scroll re-pin handling."""

import json
import shutil
import subprocess
from pathlib import Path

import pytest


ROOT = Path(__file__).parent.parent
NODE = shutil.which("node")


def _ui_js() -> str:
    return (ROOT / "static" / "ui.js").read_text(encoding="utf-8")


def _balanced_block(src: str, brace_start: int) -> str:
    depth = 0
    for i in range(brace_start, len(src)):
        ch = src[i]
        if ch == "{":
            depth += 1
        elif ch == "}":
            depth -= 1
            if depth == 0:
                return src[brace_start + 1 : i]
    raise AssertionError("balanced block not found")


def _scroll_listener_raf_body() -> str:
    src = _ui_js()
    listener_start = src.index("el.addEventListener('scroll'")
    raf_start = src.index("requestAnimationFrame(()=>", listener_start)
    brace_start = src.index("{", raf_start)
    return _balanced_block(src, brace_start)


def _record_non_message_scroll_intent() -> str:
    src = _ui_js()
    start = src.index("function _recordNonMessageScrollIntent")
    end = src.index("function _recentNonMessageScrollIntent", start)
    return src[start:end]


pytestmark = pytest.mark.skipif(NODE is None, reason="node not on PATH")


def _run_scroll_listener(samples: list[dict[str, int]]) -> dict[str, int | bool | None]:
    payload = {
        "body": _scroll_listener_raf_body(),
        "samples": samples,
    }
    script = (
        "const payload = " + json.dumps(payload) + ";\n"
        + r"""
const step = new Function(
  'el',
  '_lastScrollTop',
  '_lastMessageClientHeight',
  '_nearBottomCount',
  '_scrollPinned',
  '_messageUserUnpinned',
  '_newMessageCueVisible',
  '_programmaticScroll',
  '_cancelBottomSettle',
  '_clearNewMessageScrollCue',
  '_syncScrollToBottomCue',
  '_updateSessionStartJumpButton',
  '_isSessionEndlessScrollEnabled',
  '_messagesTruncated',
  '_loadOlderMessages',
  payload.body + `
return {
  _lastScrollTop,
  _lastMessageClientHeight,
  _nearBottomCount,
  _scrollPinned,
  _messageUserUnpinned,
};
`
);

let state = {
  _lastScrollTop: 800,
  _lastMessageClientHeight: null,
  _nearBottomCount: 0,
  _scrollPinned: true,
  _messageUserUnpinned: false,
};

const noop = () => {};
for (const sample of payload.samples) {
  state = step(
    sample,
    state._lastScrollTop,
    state._lastMessageClientHeight,
    state._nearBottomCount,
    state._scrollPinned,
    state._messageUserUnpinned,
    false,
    false,
    noop,
    noop,
    noop,
    noop,
    () => false,
    false,
    noop
  );
}

console.log(JSON.stringify(state));
"""
    )
    result = subprocess.run(
        [NODE, "-e", script],
        check=True,
        capture_output=True,
        text=True,
        timeout=30,
    )
    return json.loads(result.stdout.strip())


class TestScrollPinReentry:
    def test_manual_scroll_back_to_true_bottom_rearms_follow(self):
        state = _run_scroll_listener(
            [
                {"scrollTop": 760, "scrollHeight": 1000, "clientHeight": 200},
                {"scrollTop": 770, "scrollHeight": 1000, "clientHeight": 200},
                {"scrollTop": 780, "scrollHeight": 1000, "clientHeight": 200},
            ]
        )
        assert state["_scrollPinned"] is True, (
            "Reaching the true bottom tail after a manual scroll-up must re-arm auto-follow."
        )
        assert state["_messageUserUnpinned"] is False, (
            "The sticky unpin flag must clear once the reader manually scrolls back to the real bottom."
        )

    def test_near_bottom_proximity_alone_does_not_repin(self):
        state = _run_scroll_listener(
            [
                {"scrollTop": 760, "scrollHeight": 1200, "clientHeight": 200},
                {"scrollTop": 775, "scrollHeight": 1200, "clientHeight": 200},
                {"scrollTop": 790, "scrollHeight": 1200, "clientHeight": 200},
            ]
        )
        assert state["_scrollPinned"] is False, (
            "The reader must stay unpinned inside the 250px near-bottom band until they reach the true bottom tail."
        )
        assert state["_messageUserUnpinned"] is True, (
            "Near-bottom proximity alone must not clear the sticky unpin flag."
        )

    def test_scroll_up_threshold_prevents_jitter_unpin(self):
        record = _record_non_message_scroll_intent()
        assert "e.deltaY< -30" in record or "e.deltaY < -30" in record, (
            "The wheel-intent path must keep the 30px upward threshold to avoid touch jitter false unpins."
        )
