import json
import subprocess
from pathlib import Path

import pytest

from tests.dashboard_bundle import dashboard


POSITIONER = Path(__file__).resolve().parents[1] / "frontend" / "update-read-position.js"


def run_positioner(script: str) -> dict:
    source = f"const createPositioner = require({json.dumps(str(POSITIONER))});\n" + script
    result = subprocess.run(["node", "-e", source], capture_output=True, text=True)
    assert result.returncode == 0, result.stderr
    return json.loads(result.stdout)


def test_new_update_scrolls_to_newest_activity_once_without_reusing_previous_offset():
    result = run_positioner("""
const listeners = {};
const panel = {scrollTop:480, addEventListener:(name, fn)=>listeners[name]=fn};
const jump = {hidden:true, focus(){}};
const first = {calls:0, scrollIntoView(){this.calls += 1}, classList:{add(){}}};
const second = {calls:0, scrollIntoView(){this.calls += 1}, classList:{add(){}}};
const positioner = createPositioner({panel, jump});
positioner.open('101');
positioner.ready('101', first);
positioner.ready('101', first);
panel.scrollTop = 920;
positioner.open('102');
positioner.ready('102', second);
process.stdout.write(JSON.stringify({first:first.calls, second:second.calls, jumpHidden:jump.hidden}));
""")

    assert result == {"first": 1, "second": 1, "jumpHidden": True}


def test_user_scroll_during_loading_preserves_position_until_explicit_jump():
    result = run_positioner("""
const listeners = {};
const panel = {scrollTop:120, addEventListener:(name, fn)=>listeners[name]=fn};
const jump = {hidden:true, focus(){}};
const target = {calls:0, scrollIntoView(){this.calls += 1}, classList:{add(){}}};
const positioner = createPositioner({panel, jump});
positioner.open('201');
panel.scrollTop = 260;
listeners.scroll();
positioner.ready('201', target);
const before = {calls:target.calls, hidden:jump.hidden, top:panel.scrollTop};
positioner.jump();
process.stdout.write(JSON.stringify({before, after:{calls:target.calls, hidden:jump.hidden}}));
""")

    assert result == {
        "before": {"calls": 0, "hidden": False, "top": 260},
        "after": {"calls": 1, "hidden": True},
    }


def test_stale_or_prefetched_conversation_cannot_move_the_visible_update():
    result = run_positioner("""
const panel = {scrollTop:0, addEventListener(){}};
const jump = {hidden:true};
const stale = {calls:0, scrollIntoView(){this.calls += 1}, classList:{add(){}}};
const current = {calls:0, scrollIntoView(){this.calls += 1}, classList:{add(){}}};
const positioner = createPositioner({panel, jump});
positioner.open('302');
positioner.ready('301', stale);
positioner.ready('302', current);
process.stdout.write(JSON.stringify({stale:stale.calls, current:current.calls}));
""")

    assert result == {"stale": 0, "current": 1}


@pytest.mark.anyio
async def test_dashboard_mounts_mobile_new_activity_positioning_flow():
    html = await dashboard()

    assert '<script src="static/update-read-position.js"></script>' in html
    assert 'id="jump-update-new-activity"' in html
    assert "updateReadPosition.open(String(item.notification_id))" in html
    assert "updateReadPosition.ready(String(selectedUpdate?.notification_id || ''), newest)" in html
    assert ".update-new-activity" in html
    assert "BASE + 'static/update-read-position.js'" in (POSITIONER.parent / "service-worker.js").read_text()
