import json
import subprocess
from pathlib import Path
import pytest

from src.frontend_bundle import build_frontend
from tests.dashboard_bundle import dashboard


CONTROLLER = Path(__file__).resolve().parents[1] / "frontend" / "mobile-pull-refresh.js"


def run_scenario(source: str) -> dict:
    script = f"""
const createPullRefresh = require({json.dumps(str(CONTROLLER))});
class FakeElement {{
  constructor() {{
    this.listeners = {{}};
    this.dataset = {{}};
    this.hidden = true;
    this.textContent = '';
  }}
  addEventListener(name, callback) {{ this.listeners[name] = callback; }}
  removeEventListener(name) {{ delete this.listeners[name]; }}
  setPointerCapture() {{}}
  releasePointerCapture() {{}}
}}
const surface = new FakeElement();
const indicator = new FakeElement();
const plain = {{closest: () => null, parentElement: null, scrollWidth: 100, clientWidth: 100}};
const event = (x, y, target=plain, id=1) => ({{
  pointerId:id, pointerType:'touch', clientX:x, clientY:y, target,
  preventDefault() {{ this.prevented = true; }},
}});
{source}
"""
    result = subprocess.run(["node", "-e", script], capture_output=True, text=True)
    assert result.returncode == 0, result.stderr
    return json.loads(result.stdout)


def test_mobile_pull_refresh_runs_one_refresh_after_vertical_threshold_and_announces_success():
    result = run_scenario("""
let calls = 0;
const controller = createPullRefresh({
  surface, indicator, threshold:64,
  refresh: async () => { calls += 1; },
  isMobile: () => true,
  getScrollY: () => 0,
  isBlocked: () => false,
});
controller.start();
surface.listeners.pointerdown(event(100, 10));
surface.listeners.pointermove(event(102, 90));
const ready = {hidden:indicator.hidden, state:indicator.dataset.state, text:indicator.textContent};
const completion = surface.listeners.pointerup(event(102, 90));
Promise.resolve(completion).then(() => process.stdout.write(JSON.stringify({
  calls, ready,
  final:{hidden:indicator.hidden, state:indicator.dataset.state, text:indicator.textContent},
})));
""")

    assert result == {
        "calls": 1,
        "ready": {"hidden": False, "state": "ready", "text": "Release to refresh"},
        "final": {"hidden": False, "state": "success", "text": "My Work is up to date"},
    }


def test_mobile_pull_refresh_ignores_ineligible_gestures_and_short_pulls():
    result = run_scenario("""
let calls = 0;
let mobile = true;
let scrollY = 0;
let blocked = false;
const controller = createPullRefresh({
  surface, indicator, threshold:64,
  refresh: async () => { calls += 1; },
  isMobile: () => mobile,
  getScrollY: () => scrollY,
  isBlocked: () => blocked,
});
controller.start();
async function drag(fromX, fromY, toX, toY, target=plain) {
  surface.listeners.pointerdown(event(fromX, fromY, target));
  surface.listeners.pointermove(event(toX, toY, target));
  await surface.listeners.pointerup(event(toX, toY, target));
}
(async () => {
  await drag(100, 10, 102, 50);
  await drag(100, 10, 180, 30);
  mobile = false; await drag(100, 10, 100, 100);
  mobile = true; scrollY = 1; await drag(100, 10, 100, 100);
  scrollY = 0; blocked = true; await drag(100, 10, 100, 100);
  blocked = false;
  const interactive = {closest: selector => selector ? interactive : null, parentElement:null, scrollWidth:100, clientWidth:100};
  await drag(100, 10, 100, 100, interactive);
  const scroller = {closest: () => null, parentElement:null, scrollWidth:300, clientWidth:100};
  await drag(100, 10, 100, 100, scroller);
  process.stdout.write(JSON.stringify({calls}));
})();
""")

    assert result == {"calls": 0}


def test_mobile_pull_refresh_stays_single_flight_and_keeps_refreshing_status():
    result = run_scenario("""
let calls = 0;
let finish;
const pending = new Promise(resolve => { finish = resolve; });
const controller = createPullRefresh({
  surface, indicator, threshold:64,
  refresh: () => { calls += 1; return pending; },
  isMobile: () => true,
  getScrollY: () => 0,
  isBlocked: () => false,
});
controller.start();
function pull(id) {
  surface.listeners.pointerdown(event(100, 10, plain, id));
  surface.listeners.pointermove(event(100, 90, plain, id));
  return surface.listeners.pointerup(event(100, 90, plain, id));
}
const first = pull(1);
Promise.resolve().then(async () => {
  const second = pull(2);
  const during = {calls, state:indicator.dataset.state, text:indicator.textContent};
  finish();
  await first;
  await second;
  process.stdout.write(JSON.stringify({calls, during, final:indicator.dataset.state}));
});
""")

    assert result == {
        "calls": 1,
        "during": {"calls": 1, "state": "refreshing", "text": "Refreshing My Work…"},
        "final": "success",
    }


def test_mobile_pull_refresh_announces_failure_and_allows_a_retry():
    result = run_scenario("""
let calls = 0;
const controller = createPullRefresh({
  surface, indicator, threshold:64,
  refresh: async () => { calls += 1; if (calls === 1) throw new Error('offline secret'); },
  isMobile: () => true,
  getScrollY: () => 0,
  isBlocked: () => false,
});
controller.start();
async function pull(id) {
  surface.listeners.pointerdown(event(100, 10, plain, id));
  surface.listeners.pointermove(event(100, 90, plain, id));
  try { await surface.listeners.pointerup(event(100, 90, plain, id)); } catch (_) {}
}
(async () => {
  await pull(1);
  const failed = {state:indicator.dataset.state, text:indicator.textContent};
  await pull(2);
  process.stdout.write(JSON.stringify({calls, failed, final:indicator.dataset.state}));
})();
""")

    assert result == {
        "calls": 2,
        "failed": {"state": "error", "text": "Could not refresh · pull to retry"},
        "final": "success",
    }


@pytest.mark.anyio
async def test_mobile_pull_refresh_is_wired_to_live_refresh_and_packaged_offline():
    html = await dashboard()
    frontend = CONTROLLER.parent
    build = build_frontend(frontend)
    today_bundle = build.feature_bundles["today-timer"].runtime_bytes.decode()

    assert 'id="mobile-pull-refresh"' in html
    assert 'role="status"' in html
    assert html.index('static/mobile-pull-refresh.js') < html.index('static/dashboard.js')
    assert "createMobilePullRefresh" in html
    assert "contextPoller.refresh({ force: true })" in html
    assert "createMobilePullRefresh" in today_bundle
    assert "liveDataStatus" in today_bundle
    assert build.feature_bundles["today-timer"].runtime_name in build.service_worker_source
