import json
import subprocess
from pathlib import Path

import pytest

from tests.dashboard_bundle import dashboard


GESTURE = Path(__file__).resolve().parents[1] / "frontend" / "update-triage-gesture.js"
TRANSACTION = Path(__file__).resolve().parents[1] / "frontend" / "update-decision-transaction.js"


def run_gesture(script):
    source = f"const createGesture = require({json.dumps(str(GESTURE))});\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 run_transaction(script):
    source = f"const createTransaction = require({json.dumps(str(TRANSACTION))});\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_update_decision_transaction_locks_every_action_until_success():
    result = run_transaction(r"""
const controls = ['keep', 'read', 'reply', 'more'].map(name => ({name, disabled:false, focus(){}}));
const status = {textContent:'Ready'};
let resolveAction;
let calls = 0;
const transaction = createTransaction({controls, status});
const pending = transaction.run('Marking read…', () => new Promise(resolve => {
  calls += 1;
  resolveAction = resolve;
}));
const locked = controls.every(control => control.disabled) && transaction.busy();
const rejected = transaction.run('Keeping unread…', () => { calls += 1; });
resolveAction(true);
Promise.all([pending, rejected]).then(([completed, second]) => process.stdout.write(JSON.stringify({
  locked, pendingStatus: 'Marking read…', calls, completed, second,
  unlocked: controls.every(control => !control.disabled) && !transaction.busy(),
  finalStatus: status.textContent,
})));
""")

    assert result == {
        "locked": True,
        "pendingStatus": "Marking read…",
        "calls": 1,
        "completed": True,
        "second": False,
        "unlocked": True,
        "finalStatus": "Marking read…",
    }


def test_update_decision_transaction_restores_focus_and_announces_failure():
    result = run_transaction(r"""
let focused = 0;
let focusedWhileEnabled = false;
const origin = {disabled:false, focus(){ focused += 1; focusedWhileEnabled = !this.disabled; }};
const other = {disabled:false, focus(){}};
const status = {textContent:''};
const transaction = createTransaction({controls:[origin, other], status});
transaction.run('Marking read…', async () => false, {
  origin,
  failure:'Could not mark update read. Retry.',
}).then(completed => process.stdout.write(JSON.stringify({
  completed, focused, focusedWhileEnabled, status:status.textContent,
  unlocked:!origin.disabled && !other.disabled && !transaction.busy(),
})));
""")

    assert result == {
        "completed": False,
        "focused": 1,
        "focusedWhileEnabled": True,
        "status": "Could not mark update read. Retry.",
        "unlocked": True,
    }


def test_committed_horizontal_swipes_decide_once_and_lock_while_pending():
    result = run_gesture(r"""
class Surface {
  constructor() { this.listeners = {}; this.dataset = {}; }
  addEventListener(name, callback) { this.listeners[name] = callback; }
  removeEventListener(name) { delete this.listeners[name]; }
  setPointerCapture() {}
  releasePointerCapture() {}
}
const surface = new Surface();
const calls = [];
let enabled = true;
let resolveRead;
let lockedCalls;
const gesture = createGesture({
  surface,
  enabled: () => enabled,
  keepUnread: () => calls.push('keep'),
  markRead: () => new Promise(resolve => { calls.push('read'); resolveRead = resolve; }),
  viewportWidth: () => 390,
});
const target = {closest: () => null, parentElement: null, scrollWidth: 100, clientWidth: 100};
const event = (x, y, id=1) => ({pointerId:id, pointerType:'touch', clientX:x, clientY:y, target, preventDefault(){}});
surface.listeners.pointerdown(event(180, 200));
surface.listeners.pointermove(event(250, 205));
surface.listeners.pointerup(event(270, 206));
surface.listeners.pointerdown(event(280, 200));
surface.listeners.pointermove(event(205, 204));
surface.listeners.pointerup(event(190, 205));
surface.listeners.pointerdown(event(280, 200));
surface.listeners.pointermove(event(190, 203));
surface.listeners.pointerup(event(180, 203));
Promise.resolve().then(() => {
  lockedCalls = [...calls];
  resolveRead();
  return Promise.resolve();
}).then(() => process.stdout.write(JSON.stringify({calls, lockedCalls, direction:surface.dataset.triageGesture || null})));
""")

    assert result == {
        "calls": ["keep", "read"],
        "lockedCalls": ["keep", "read"],
        "direction": None,
    }


def test_scrolls_short_drags_edges_and_interactive_targets_never_decide():
    result = run_gesture(r"""
class Surface {
  constructor() { this.listeners = {}; this.dataset = {}; }
  addEventListener(name, callback) { this.listeners[name] = callback; }
  removeEventListener(name) { delete this.listeners[name]; }
  setPointerCapture() {}
  releasePointerCapture() {}
}
const surface = new Surface();
const calls = [];
createGesture({surface, enabled:()=>true, keepUnread:()=>calls.push('keep'), markRead:()=>calls.push('read'), viewportWidth:()=>390});
const plain = {closest: () => null, parentElement: null, scrollWidth:100, clientWidth:100};
const interactive = {closest: selector => selector.includes('button') ? {} : null, parentElement:null, scrollWidth:100, clientWidth:100};
const scroller = {closest: () => null, parentElement:null, scrollWidth:400, clientWidth:100};
const event = (x,y,target=plain,id=1) => ({pointerId:id,pointerType:'touch',clientX:x,clientY:y,target,preventDefault(){}});
function drag(fromX, fromY, toX, toY, target=plain) {
  surface.listeners.pointerdown(event(fromX,fromY,target));
  surface.listeners.pointermove(event(toX,toY,target));
  surface.listeners.pointerup(event(toX,toY,target));
}
drag(180,100,205,104);
drag(180,100,220,190);
drag(8,100,120,104);
drag(180,100,280,104,interactive);
drag(180,100,280,104,scroller);
surface.listeners.pointerdown(event(180,100));
surface.listeners.pointercancel(event(280,104));
process.stdout.write(JSON.stringify({calls, direction:surface.dataset.triageGesture || null}));
""")

    assert result == {"calls": [], "direction": None}


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

    assert '<script src="static/update-triage-gesture.js"></script>' in html
    assert '<script src="static/update-decision-transaction.js"></script>' in html
    assert 'id="update-gesture-status"' in html
    assert "createUpdateTriageGesture({" in html
    assert "createUpdateDecisionActions({" in html
    assert "keepUnread: keepUpdateUnread" in html
    assert "markRead: markUpdateRead" in html
    assert "data-triage-gesture" in html
    assert "prefers-reduced-motion: reduce" in html
