import json
import subprocess
from pathlib import Path

import pytest

from tests.dashboard_bundle import dashboard


DOCK = Path(__file__).resolve().parents[1] / "frontend" / "mobile-task-dock.js"
ENTRY = Path(__file__).resolve().parents[1] / "frontend" / "mobile-work-entry.js"
QUEUE_LAUNCHER = Path(__file__).resolve().parents[1] / "frontend" / "mobile-queue-launcher.js"
TIMER = Path(__file__).resolve().parents[1] / "frontend" / "today-timer.js"


def test_mobile_work_entry_prioritizes_continue_resume_start_plan_then_find():
    script = f"""
const createEntry = require({json.dumps(str(ENTRY))});
const state = {{active:true, resumable:true, today:2, eligible:3}};
const calls = [];
const entry = createEntry({{
  isTodayActive: () => state.active,
  isTodayResumable: () => state.resumable,
  getTodayCount: () => state.today,
  getEligibleCount: () => state.eligible,
  continueToday: () => calls.push('continue'),
  resumeToday: () => calls.push('resume'),
  startToday: () => calls.push('start'),
  planToday: () => calls.push('plan'),
  findWork: () => calls.push('find'),
}});
const modes = [];
modes.push(entry.open());
state.active = false; modes.push(entry.open());
state.resumable = false; modes.push(entry.open());
state.today = 0; modes.push(entry.open());
state.eligible = 0; modes.push(entry.open());
process.stdout.write(JSON.stringify({{modes, calls}}));
"""
    result = subprocess.run(
        ["node", "-e", script], capture_output=True, text=True
    )

    assert result.returncode == 0, result.stderr
    assert json.loads(result.stdout) == {
        "modes": ["continue", "resume", "start", "plan", "find"],
        "calls": ["continue", "resume", "start", "plan", "find"],
    }


def test_mobile_work_entry_preserves_active_today_then_launches_highest_priority_queue():
    script = f"""
const createEntry = require({json.dumps(str(ENTRY))});
const calls = [];
const state = {{active:true, today:2, resumable:true, eligible:3, next:{{name:'attention'}}}};
const entry = createEntry({{
  isTodayActive: () => state.active,
  isTodayResumable: () => state.resumable,
  getTodayCount: () => state.today,
  getEligibleCount: () => state.eligible,
  queueLauncher: {{recommend: () => state.next, open: name => calls.push('queue:' + name)}},
  continueToday: () => calls.push('continue'),
  resumeToday: () => calls.push('resume'),
  startToday: () => calls.push('start'),
  planToday: () => calls.push('plan'),

  findWork: () => calls.push('find'),
}});
const modes = [entry.open()];
state.active = false; modes.push(entry.open());
state.next = {{name:'today'}}; modes.push(entry.open());
state.today = 0; state.eligible = 0; state.next = {{name:'update'}}; modes.push(entry.open());
state.next = {{name:'agenda'}}; modes.push(entry.open());
state.next = {{name:'later'}}; modes.push(entry.open());
state.next = {{name:'draft'}}; modes.push(entry.open());
state.next = {{name:'find'}}; modes.push(entry.open());
process.stdout.write(JSON.stringify({{modes, calls}}));
"""
    result = subprocess.run(["node", "-e", script], capture_output=True, text=True)

    assert result.returncode == 0, result.stderr
    assert json.loads(result.stdout) == {
        "modes": ["continue", "attention", "resume", "update", "agenda", "later", "draft", "find"],
        "calls": ["continue", "queue:attention", "resume", "queue:update", "queue:agenda", "queue:later", "queue:draft", "find"],
    }


def test_mobile_work_entry_opens_prepare_today_before_direct_queue_work():
    script = f"""
const createEntry = require({json.dumps(str(ENTRY))});
const calls = [];
const state = {{active:true, preparation:{{active:false, next:'attention'}}}};
const entry = createEntry({{
  isTodayActive: () => state.active,
  isTodayResumable: () => false,
  getTodayCount: () => 0,
  getEligibleCount: () => 3,
  getPreparationState: () => state.preparation,
  queueLauncher: {{recommend: () => ({{name:'attention'}}), open: name => calls.push('queue:' + name)}},
  continueToday: () => calls.push('continue'),
  prepareToday: () => calls.push('prepare'),
  planToday: () => calls.push('plan'),
  findWork: () => calls.push('find'),
}});
const modes = [entry.open()];
state.active = false; modes.push(entry.open());
state.preparation.active = true; modes.push(entry.open());
process.stdout.write(JSON.stringify({{modes, calls}}));
"""
    result = subprocess.run(["node", "-e", script], capture_output=True, text=True)

    assert result.returncode == 0, result.stderr
    assert json.loads(result.stdout) == {
        "modes": ["continue", "prepare", "prepare-resume"],
        "calls": ["continue", "prepare", "prepare"],
    }


def test_mobile_work_entry_plans_eligible_work_before_find_fallback():
    script = f"""
const createEntry = require({json.dumps(str(ENTRY))});
const calls = [];
const entry = createEntry({{
  isTodayActive: () => false,
  isTodayResumable: () => false,
  getTodayCount: () => 0,
  getEligibleCount: () => 2,
  queueLauncher: {{recommend: () => ({{name:'find'}}), open: () => {{}}}},
  continueToday: () => {{}},
  resumeToday: () => {{}},
  startToday: () => {{}},
  planToday: () => calls.push('plan'),
  findWork: () => calls.push('find'),
}});
process.stdout.write(JSON.stringify({{mode:entry.open(), calls}}));
"""
    result = subprocess.run(["node", "-e", script], capture_output=True, text=True)

    assert result.returncode == 0, result.stderr
    assert json.loads(result.stdout) == {"mode": "plan", "calls": ["plan"]}


def test_mobile_work_entry_guides_an_empty_first_workspace_before_find():
    script = f"""
const createEntry = require({json.dumps(str(ENTRY))});
const calls = [];
let activationRequired = true;
const entry = createEntry({{
  isTodayActive: () => false,
  isTodayResumable: () => false,
  getTodayCount: () => 0,
  getEligibleCount: () => 0,
  shouldActivate: () => activationRequired,
  queueLauncher: {{recommend: () => ({{name:'find'}}), open: () => {{}}}},
  openActivation: () => calls.push('activate'),
  findWork: () => calls.push('find'),
}});
const modes = [entry.open()];
activationRequired = false;
modes.push(entry.open());
process.stdout.write(JSON.stringify({{modes, calls}}));
"""
    result = subprocess.run(["node", "-e", script], capture_output=True, text=True)

    assert result.returncode == 0, result.stderr
    assert json.loads(result.stdout) == {
        "modes": ["activate", "find"],
        "calls": ["activate", "find"],
    }


def test_mobile_task_dock_routes_actions_hides_for_overlays_and_restores_focus():
    script = f"""
const createDock = require({json.dumps(str(DOCK))});
class FakeElement {{
  constructor() {{
    this.listeners = {{}};
    this.attributes = {{}};
    this.classList = {{ values:new Set(), contains:value => this.classList.values.has(value) }};
    this.hidden = false;
    this.focuses = 0;
  }}
  addEventListener(name, callback) {{ this.listeners[name] = callback; }}
  click() {{ this.listeners.click({{currentTarget:this}}); }}
  setAttribute(name, value) {{ this.attributes[name] = value; }}
  removeAttribute(name) {{ delete this.attributes[name]; }}
  focus() {{ this.focuses += 1; }}
}}
const nav = new FakeElement();
const sessionHud = new FakeElement();
const buttons = Object.fromEntries(['work','attention','find','new','search','drafts'].map(name => [name, new FakeElement()]));
buttons.attention.hidden = true;
const overlay = new FakeElement();
const calls = [];
let observerCallback;
const dock = createDock({{
  nav, sessionHud, buttons, overlays:[overlay],
  actions: Object.fromEntries(Object.keys(buttons).map(name => [name, () => calls.push(name)])),
  observe(callback) {{ observerCallback = callback; return {{disconnect() {{}}}}; }},
}});
dock.start();
buttons.work.click();
buttons.find.click();
overlay.classList.values.add('open'); observerCallback();
const hiddenWhileOpen = nav.hidden;
const hudHiddenWhileOpen = sessionHud.attributes['data-overlay-hidden'];
overlay.classList.values.delete('open'); observerCallback();
buttons.drafts.click();
process.stdout.write(JSON.stringify({{
  calls, hiddenWhileOpen, hiddenAfterClose:nav.hidden, hudHiddenWhileOpen,
  hudHiddenAfterClose:sessionHud.attributes['data-overlay-hidden'] || null,
  attentionHidden:buttons.attention.hidden,
  findFocuses:buttons.find.focuses,
  current:Object.fromEntries(Object.entries(buttons).map(([name, button]) => [name, button.attributes['aria-current'] || null])),
}}));
"""
    result = subprocess.run(
        ["node", "-e", script], capture_output=True, text=True
    )

    assert result.returncode == 0, result.stderr
    assert json.loads(result.stdout) == {
        "calls": ["work", "find", "drafts"],
        "hiddenWhileOpen": True,
        "hiddenAfterClose": False,
        "hudHiddenWhileOpen": "true",
        "hudHiddenAfterClose": None,
        "attentionHidden": True,
        "findFocuses": 1,
        "current": {
            "work": None,
            "attention": None,
            "find": None,
            "new": None,
            "search": None,
            "drafts": "page",
        },
    }


def test_mobile_task_dock_pauses_before_queues_and_distinguishes_close_from_selection():
    script = f"""
const createDock = require({json.dumps(str(DOCK))});
class FakeElement {{
  constructor() {{ this.listeners = {{}}; this.open = false; this.attributes = {{}}; }}
  addEventListener(name, callback) {{ this.listeners[name] = callback; }}
  setAttribute(name, value) {{ this.attributes[name] = value; }}
  removeAttribute(name) {{ delete this.attributes[name]; }}
  showModal() {{ this.open = true; }}
  close() {{ this.open = false; this.listeners.close?.(); }}
  focus() {{}}
}}
const nav = new FakeElement();
nav.hidden = false;
const queues = new FakeElement();
const close = new FakeElement();
const row = new FakeElement();
const button = new FakeElement();
const calls = [];
const dock = createDock({{
  nav, buttons:{{queues:button}}, queueSheet:queues, queueClose:close, queueRows:{{update:row}}, overlays:[],
  detour:()=>({{beginDetour:reason=>calls.push('pause:' + reason), finishDetour:()=>calls.push('return')}}),
  onSelectQueue:name=>calls.push('select:' + name),
  observe:()=>({{disconnect(){{}}}}),
}});
dock.start();
button.listeners.click({{currentTarget:button}});
close.listeners.click();
button.listeners.click({{currentTarget:button}});
row.listeners.click();
process.stdout.write(JSON.stringify({{calls, open:queues.open}}));
"""
    result = subprocess.run(["node", "-e", script], capture_output=True, text=True)

    assert result.returncode == 0, result.stderr
    assert json.loads(result.stdout) == {
        "calls": ["pause:queues", "return", "pause:queues", "select:update"],
        "open": False,
    }


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

    assert html.count('data-today-detour role="status"') == 6
    assert html.count('data-return-from-detour type="button"') == 6
    assert "detour:() => timerView" in html
    timer_source = TIMER.read_text()
    assert "createTodayDetourInterruption" in timer_source
    assert "queryAll('#find-work-sheet')" in timer_source
    assert '.today-detour-interruption button { min-height:44px;' in html


def test_mobile_task_dock_programmatically_selects_destination_without_running_action():
    script = f"""
const createDock = require({json.dumps(str(DOCK))});
const calls = [];
const makeButton = () => ({{
  attributes: {{}},
  setAttribute(name, value) {{ this.attributes[name] = value; }},
  removeAttribute(name) {{ delete this.attributes[name]; }},
}});
const buttons = {{work:makeButton(), drafts:makeButton()}};
const dock = createDock({{
  nav:{{}}, buttons,
  actions:{{work:()=>calls.push('work'), drafts:()=>calls.push('drafts')}},
}});
const selected = dock.select('drafts');
process.stdout.write(JSON.stringify({{
  selected, calls,
  work:buttons.work.attributes['aria-current'] || null,
  drafts:buttons.drafts.attributes['aria-current'] || null,
}}));
"""
    result = subprocess.run(
        ["node", "-e", script], capture_output=True, text=True
    )

    assert result.returncode == 0, result.stderr
    assert json.loads(result.stdout) == {
        "selected": True,
        "calls": [],
        "work": None,
        "drafts": "page",
    }


def test_mobile_task_dock_keeps_today_label_separate_from_attention_count():
    script = f"""
const createDock = require({json.dumps(str(DOCK))});
const work = {{ attributes: {{}}, setAttribute(name, value) {{ this.attributes[name] = value; }} }};
const workLabel = {{ textContent:'' }};
const dock = createDock({{ nav:{{}}, buttons:{{work}}, workLabel }});
dock.updateWork('continue', 3);
const continuing = {{ text:workLabel.textContent, label:work.attributes['aria-label'] }};
dock.updateWork('resume', 0);
const resuming = {{ text:workLabel.textContent, label:work.attributes['aria-label'] }};
dock.updateWork('start', 0);
const starting = {{ text:workLabel.textContent, label:work.attributes['aria-label'] }};
dock.updateWork('plan', 0);
const planning = {{ text:workLabel.textContent, label:work.attributes['aria-label'] }};
dock.updateWork('find', 0);
process.stdout.write(JSON.stringify({{
  continuing, resuming, starting, planning,
  finding:{{ text:workLabel.textContent, label:work.attributes['aria-label'] }},
}}));
"""
    result = subprocess.run(
        ["node", "-e", script], capture_output=True, text=True
    )

    assert result.returncode == 0, result.stderr
    assert json.loads(result.stdout) == {
        "continuing": {
            "text": "Continue",
            "label": "Continue Today",
        },
        "resuming": {"text": "Resume", "label": "Resume Today"},
        "starting": {"text": "Start", "label": "Start Today"},
        "planning": {"text": "Plan", "label": "Plan Today"},
        "finding": {"text": "Find", "label": "Find work"},
    }


def test_mobile_today_hud_completes_current_item_and_labels_final_item_for_recap():
    script = f"""
const createView = require({json.dumps(str(TIMER))}).createView;
class FakeElement {{
  constructor() {{ this.listeners = {{}}; this.hidden = true; this.textContent = ''; this.attributes = {{}}; }}
  addEventListener(name, callback) {{ this.listeners[name] = callback; }}
  click() {{ return this.listeners.click?.(); }}
  setAttribute(name, value) {{ this.attributes[name] = value; }}
}}
const hud = new FakeElement();
const open = new FakeElement();
const toggle = new FakeElement();
const complete = new FakeElement();
const progress = new FakeElement();
const elements = {{
  '[data-mobile-today-hud]':[hud],
  '[data-mobile-today-open]':[open],
  '[data-mobile-today-toggle]':[toggle],
  '[data-mobile-today-complete]':[complete],
  '[data-work-session-progress]':[progress],
  '[data-work-session-adjust-plan]':[],
  '[data-work-session-timer-toggle]':[toggle],
}};
let active = true;
const snapshot = {{identity:'issue:r:1:', elapsed_ms:0, running:true}};
const completed = [];
const view = createView({{
  timer:{{snapshot:()=>snapshot,totalElapsed:()=>0,pause:()=>true,resume:()=>true}},
  isActive:()=>active,
  queryAll:selector=>elements[selector] || [],
  formatEstimate:value=>value + 'm',
  getItem:()=>({{title:'Ship mobile flow'}}),
  onComplete:identity=>{{ completed.push(identity); return false; }},
}});
view.update({{index:1,total:2}}, null);
const next = {{hidden:complete.hidden,label:complete.textContent,aria:complete.attributes['aria-label']}};
complete.click();
const preservedAfterFailure = {{hudHidden:hud.hidden,identity:snapshot.identity}};
view.update({{index:2,total:2}}, null);
const recap = {{hidden:complete.hidden,label:complete.textContent}};
active = false; view.render();
process.stdout.write(JSON.stringify({{next,recap,completed,preservedAfterFailure,hiddenWhenInactive:complete.hidden}}));
"""
    result = subprocess.run(
        ["node", "-e", script], capture_output=True, text=True
    )

    assert result.returncode == 0, result.stderr
    assert json.loads(result.stdout) == {
        "next": {
            "hidden": False,
            "label": "Done & next",
            "aria": "Complete Ship mobile flow and open next Today item",
        },
        "recap": {"hidden": False, "label": "Done & recap"},
        "completed": ["issue:r:1:"],
        "preservedAfterFailure": {"hudHidden": False, "identity": "issue:r:1:"},
        "hiddenWhenInactive": True,
    }


def test_mobile_task_dock_exposes_independent_attention_action_and_six_column_state():
    script = f"""
const createDock = require({json.dumps(str(DOCK))});
const nav = {{ attributes: {{}}, setAttribute(name, value) {{ this.attributes[name] = value; }}, removeAttribute(name) {{ delete this.attributes[name]; }} }};
const attention = {{ attributes: {{}}, hidden:true, setAttribute(name, value) {{ this.attributes[name] = value; }} }};
const badge = {{ textContent:'', hidden:true }};
const dock = createDock({{ nav, buttons:{{attention}}, attentionBadge:badge }});
dock.updateAttention(3);
const pending = {{ count:badge.textContent, badgeHidden:badge.hidden, actionHidden:attention.hidden, label:attention.attributes['aria-label'], layout:nav.attributes['data-attention'] }};
dock.updateAttention(0);
process.stdout.write(JSON.stringify({{
  pending,
  empty:{{ count:badge.textContent, badgeHidden:badge.hidden, actionHidden:attention.hidden, label:attention.attributes['aria-label'], layout:nav.attributes['data-attention'] || null }},
}}));
"""
    result = subprocess.run(
        ["node", "-e", script], capture_output=True, text=True
    )

    assert result.returncode == 0, result.stderr
    assert json.loads(result.stdout) == {
        "pending": {"count": "3", "badgeHidden": False, "actionHidden": False, "label": "Attention, 3 items", "layout": "true"},
        "empty": {"count": "0", "badgeHidden": True, "actionHidden": True, "label": "Attention, 0 items", "layout": None},
    }


def test_mobile_task_dock_opens_stable_queue_switcher_and_routes_each_queue():
    script = f"""
const createDock = require({json.dumps(str(DOCK))});
class FakeElement {{
  constructor() {{ this.listeners = {{}}; this.attributes = {{}}; this.hidden = false; this.textContent = ''; this.focuses = 0; this.classList = {{contains:()=>false}}; }}
  addEventListener(name, callback) {{ this.listeners[name] = callback; }}
  click() {{ return this.listeners.click?.({{currentTarget:this}}); }}
  setAttribute(name, value) {{ this.attributes[name] = value; }}
  removeAttribute(name) {{ delete this.attributes[name]; }}
  focus() {{ this.focuses += 1; }}
}}
const nav = new FakeElement();
const queues = new FakeElement();
const sheet = new FakeElement(); sheet.open = false;
sheet.showModal = function () {{ this.open = true; }};
sheet.close = function () {{ this.open = false; this.listeners.close?.(); }};
const close = new FakeElement();
const badge = new FakeElement();
const deadline = new FakeElement();
const rows = Object.fromEntries(['today','agenda','delivery','attention','update','filed','later','draft','recaps'].map(name => [name, new FakeElement()]));
const counts = Object.fromEntries(Object.keys(rows).map(name => [name, new FakeElement()]));
const selected = [];
const utilities = [];
const dock = createDock({{
  nav, buttons:{{queues}}, actions:{{queues:()=>{{}}}}, overlays:[sheet],
  queueSheet:sheet, queueClose:close, queueRows:rows, queueCounts:counts, queueBadge:badge, deadlineBadge:deadline,
  onSelectQueue:(name,row)=>name === 'recaps' ? utilities.push(row === rows.recaps ? 'recaps:trigger' : 'recaps:missing-trigger') : selected.push(name),
  observe() {{}},
}});
dock.start();
dock.updateQueues({{today:2, agenda:5, delivery:1, attention:1, update:5, filed:2, later:3, draft:4}});
queues.click();
const opened = sheet.open;
rows.update.click();
const closedAfterSelect = !sheet.open;
queues.click(); rows.recaps.click();
const closedAfterUtility = !sheet.open;
queues.click(); close.click();
const populated = {{
  badge:badge.textContent, badgeHidden:badge.hidden,
  deadline:deadline.textContent, deadlineHidden:deadline.hidden,
  agendaDue:rows.agenda.attributes['data-deadlines'] || null,
  badgeLabel:queues.attributes['aria-label'],
}};
dock.updateQueues({{today:0, agenda:0, attention:0, update:7, later:0, draft:0}});
const updatesOnly = {{
  badge:badge.textContent, badgeHidden:badge.hidden,
  deadlineHidden:deadline.hidden,
  badgeLabel:queues.attributes['aria-label'],
}};
dock.updateQueues({{today:0, agenda:0, attention:0, update:0, later:0, draft:0}});
process.stdout.write(JSON.stringify({{
  opened, closedAfterSelect, closedAfterUtility, selected, utilities,
  badge:badge.textContent, populated, updatesOnly,
  clearedBadgeHidden:badge.hidden,
  clearedDeadlineHidden:deadline.hidden,
  clearedAgendaDue:rows.agenda.attributes['data-deadlines'] || null,
  clearedBadgeLabel:queues.attributes['aria-label'],
  counts:Object.fromEntries(Object.entries(counts).map(([name, node]) => [name, node.textContent])),
  updateLabel:rows.update.attributes['aria-label'],
  queueFocuses:queues.focuses,
  columnState:nav.attributes['data-attention'] || null,
}}));
"""
    result = subprocess.run(["node", "-e", script], capture_output=True, text=True)

    assert result.returncode == 0, result.stderr
    assert json.loads(result.stdout) == {
        "opened": True,
        "closedAfterSelect": True,
        "closedAfterUtility": True,
        "selected": ["update"],
        "utilities": ["recaps:trigger"],
        "badge": "0 active",
        "populated": {
            "badge": "7 active",
            "badgeHidden": False,
            "badgeLabel": "Queues: Today 2, Agenda 5 due, Delivery 1, Attention 1, Updates 5, Filed 2, Later 3, Drafts 4; 7 active queues",
            "deadline": "5 due",
            "deadlineHidden": False,
            "agendaDue": "true",
        },
        "updatesOnly": {
            "badge": "1 active",
            "badgeHidden": False,
            "deadlineHidden": True,
            "badgeLabel": "Queues: Today 0, Agenda 0 due, Delivery 0, Attention 0, Updates 7, Filed 0, Later 0, Drafts 0; 1 active queue",
        },
        "clearedBadgeHidden": True,
        "clearedDeadlineHidden": True,
        "clearedAgendaDue": None,
        "clearedBadgeLabel": "Queues: no active queues; no upcoming deadlines",
        "counts": {"today": "0", "agenda": "0", "delivery": "0", "attention": "0", "update": "0", "filed": "0", "later": "0", "draft": "0", "recaps": "0"},
        "updateLabel": "Updates, 0 unread conversations",
        "queueFocuses": 3,
        "columnState": None,
    }


def test_mobile_queue_launcher_opens_first_actionable_item_after_selecting_queue():
    script = f"""
const createLauncher = require({json.dumps(str(QUEUE_LAUNCHER))});
const calls = [];
const launcher = createLauncher({{
  selectFilter: name => calls.push('filter:' + name),
  firstAction: name => name === 'attention' ? {{click() {{ calls.push('open:first'); }}}} : null,
  announce: message => calls.push('announce:' + message),
}});
const opened = launcher.open('attention');
const empty = launcher.open('later');
process.stdout.write(JSON.stringify({{opened, empty, calls}}));
"""
    result = subprocess.run(["node", "-e", script], capture_output=True, text=True)

    assert result.returncode == 0, result.stderr
    assert json.loads(result.stdout) == {
        "opened": "opened",
        "empty": "empty",
        "calls": [
            "filter:attention",
            "open:first",
            "filter:later",
            "announce:No deferred work is ready to open.",
        ],
    }


def test_mobile_queue_launcher_uses_dedicated_filed_follow_up_flow():
    script = f"""
const createLauncher = require({json.dumps(str(QUEUE_LAUNCHER))});
const calls = [];
const launcher = createLauncher({{
  openFiled: () => {{ calls.push('filed-follow-up'); return 'opened-update'; }},
  selectFilter: name => calls.push('generic-filter:' + name),
  firstAction: () => {{ throw new Error('generic card launch must not run'); }},
  announce: message => calls.push('announce:' + message),
}});
const result = launcher.open('filed');
process.stdout.write(JSON.stringify({{result, calls}}));
"""
    result = subprocess.run(["node", "-e", script], capture_output=True, text=True)

    assert result.returncode == 0, result.stderr
    assert json.loads(result.stdout) == {
        "result": "opened-update",
        "calls": ["filed-follow-up"],
    }


def test_mobile_queue_launcher_uses_dedicated_delivery_recovery_flow():
    script = f"""
const createLauncher = require({json.dumps(str(QUEUE_LAUNCHER))});
const calls = [];
const launcher = createLauncher({{
  openDelivery: () => {{ calls.push('delivery-recovery'); return 'opened'; }},
  selectFilter: name => calls.push('generic-filter:' + name),
  firstAction: () => {{ throw new Error('generic card launch must not run'); }},
}});
const result = launcher.open('delivery');
process.stdout.write(JSON.stringify({{result, calls}}));
"""
    result = subprocess.run(["node", "-e", script], capture_output=True, text=True)

    assert result.returncode == 0, result.stderr
    assert json.loads(result.stdout) == {"result": "opened", "calls": ["delivery-recovery"]}


def test_mobile_work_prioritizes_and_revalidates_actionable_delivery_recovery():
    script = f"""
const createLauncher = require({json.dumps(str(QUEUE_LAUNCHER))});
const calls = [];
let counts = {{delivery:2, attention:3, today:1, update:4}};
const launcher = createLauncher({{
  getCounts: () => counts,
  openDelivery: () => {{ calls.push('delivery-recovery'); return 'opened-delivery'; }},
  openUpdates: () => {{ calls.push('updates'); return 'opened-updates'; }},
  selectFilter: name => calls.push('filter:' + name),
  firstAction: () => null,
  announce: () => {{}},
  openFindWork: () => calls.push('find'),
}});
const delivery = launcher.recommend();
const opened = launcher.continueWork();
counts = {{delivery:0, attention:0, today:0, update:4}};
const afterRecovery = launcher.recommend();
process.stdout.write(JSON.stringify({{delivery, opened, afterRecovery, calls}}));
"""
    result = subprocess.run(["node", "-e", script], capture_output=True, text=True)

    assert result.returncode == 0, result.stderr
    assert json.loads(result.stdout) == {
        "delivery": {"name": "delivery", "count": 2, "label": "Recover Delivery (2)"},
        "opened": "opened-delivery",
        "afterRecovery": {"name": "update", "count": 4, "label": "Resume Updates (4)"},
        "calls": ["delivery-recovery"],
    }


def test_mobile_work_continues_into_filed_follow_up_before_later_work():
    script = f"""
const createLauncher = require({json.dumps(str(QUEUE_LAUNCHER))});
const calls = [];
let counts = {{attention:0, today:0, update:0, agenda:0, filed:2, later:4, draft:3}};
const launcher = createLauncher({{
  getCounts: () => counts,
  openFiled: () => {{ calls.push('filed-follow-up'); return 'opened-filed'; }},
  selectFilter: name => calls.push('filter:' + name),
  firstAction: () => null,
  announce: () => {{}},
  openFindWork: () => calls.push('find'),
}});
const recommendation = launcher.recommend();
const opened = launcher.continueWork();
process.stdout.write(JSON.stringify({{recommendation, opened, calls}}));
"""
    result = subprocess.run(["node", "-e", script], capture_output=True, text=True)

    assert result.returncode == 0, result.stderr
    assert json.loads(result.stdout) == {
        "recommendation": {"name": "filed", "count": 2, "label": "Review Filed (2)"},
        "opened": "opened-filed",
        "calls": ["filed-follow-up"],
    }


def test_mobile_queue_launcher_recommends_and_revalidates_cross_queue_continuation():
    script = f"""
const createLauncher = require({json.dumps(str(QUEUE_LAUNCHER))});
const calls = [];
let counts = {{attention: 2, today: 1, agenda: 5, later: 4, draft: 3}};
const launcher = createLauncher({{
  openToday: () => calls.push('today'),
  openAgenda: () => {{ calls.push('agenda'); return 'opened'; }},
  selectFilter: name => calls.push('filter:' + name),
  firstAction: name => (counts[name] || 0) ? {{click() {{ calls.push('open:' + name); }}}} : null,
  announce: message => calls.push('announce:' + message),
  getCounts: () => counts,
  openFindWork: () => calls.push('find'),
}});
const first = launcher.recommend();
counts = {{attention: 0, today: 0, agenda: 5, later: 4, draft: 3}};
const agenda = launcher.recommend();
const opened = launcher.continueWork();
counts = {{attention: 0, today: 0, agenda: 0, later: 0, draft: 0}};
const fallback = launcher.recommend();
launcher.continueWork();
process.stdout.write(JSON.stringify({{first, agenda, opened, fallback, calls}}));
"""
    result = subprocess.run(["node", "-e", script], capture_output=True, text=True)

    assert result.returncode == 0, result.stderr
    assert json.loads(result.stdout) == {
        "first": {"name": "attention", "count": 2, "label": "Start Attention (2)"},
        "agenda": {"name": "agenda", "count": 5, "label": "Open Agenda (5)"},
        "opened": "opened",
        "fallback": {"name": "find", "count": 0, "label": "Find Work"},
        "calls": ["agenda", "find"],
    }


def test_mobile_queue_launcher_prioritizes_updates_before_agenda_and_resumes_launcher():
    script = f"""
const createLauncher = require({json.dumps(str(QUEUE_LAUNCHER))});
const calls = [];
let counts = {{attention:0, today:0, update:6, agenda:2, later:1, draft:1}};
const launcher = createLauncher({{
  getCounts: () => counts,
  openUpdates: () => {{ calls.push('updates'); return 'resumed'; }},
  openAgenda: () => calls.push('agenda'),
  selectFilter: name => calls.push('filter:' + name),
  firstAction: () => null,
  announce: () => {{}},
  openFindWork: () => calls.push('find'),
}});
const recommendation = launcher.recommend();
const opened = launcher.continueWork();
process.stdout.write(JSON.stringify({{recommendation, opened, calls}}));
"""
    result = subprocess.run(["node", "-e", script], capture_output=True, text=True)

    assert result.returncode == 0, result.stderr
    assert json.loads(result.stdout) == {
        "recommendation": {"name": "update", "count": 6, "label": "Resume Updates (6)"},
        "opened": "resumed",
        "calls": ["updates"],
    }


def test_mobile_task_dock_labels_each_recommended_work_destination_accessibly():
    script = f"""
const createDock = require({json.dumps(str(DOCK))});
const work = {{attributes: {{}}, setAttribute(name, value) {{ this.attributes[name] = value; }}}};
const workLabel = {{textContent:''}};
const dock = createDock({{nav:{{}}, buttons:{{work}}, workLabel}});
const labels = {{}};
for (const mode of ['delivery','attention','update','agenda','filed','later','draft']) {{
  dock.updateWork(mode);
  labels[mode] = [workLabel.textContent, work.attributes['aria-label']];
}}
process.stdout.write(JSON.stringify(labels));
"""
    result = subprocess.run(["node", "-e", script], capture_output=True, text=True)

    assert result.returncode == 0, result.stderr
    assert json.loads(result.stdout) == {
        "delivery": ["Delivery", "Open Delivery"],
        "attention": ["Attention", "Open Attention"],
        "update": ["Updates", "Resume Updates"],
        "agenda": ["Agenda", "Open Agenda"],
        "filed": ["Filed", "Open Filed"],
        "later": ["Later", "Open Later"],
        "draft": ["Drafts", "Open Drafts"],
    }


def test_mobile_task_dock_labels_prepare_today_as_the_primary_action():
    script = f"""
const createDock = require({json.dumps(str(DOCK))});
const work = {{attributes: {{}}, setAttribute(name, value) {{ this.attributes[name] = value; }}}};
const workLabel = {{textContent:''}};
const dock = createDock({{nav:{{}}, buttons:{{work}}, workLabel}});
const labels = {{}};
for (const mode of ['prepare','prepare-resume']) {{
  dock.updateWork(mode);
  labels[mode] = [workLabel.textContent, work.attributes['aria-label']];
}}
process.stdout.write(JSON.stringify(labels));
"""
    result = subprocess.run(["node", "-e", script], capture_output=True, text=True)

    assert result.returncode == 0, result.stderr
    assert json.loads(result.stdout) == {
        "prepare": ["Prepare", "Prepare Today"],
        "prepare-resume": ["Resume prep", "Resume preparation"],
    }


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

    assert 'id="mobile-queue-heading"' in html
    assert 'data-mobile-queue="find"' in html
    assert "mobileQueueLauncher.recommend()" in html
    assert "showMobileQueueCompletion('Updates')" in html
    assert "row.setAttribute('data-recommended', 'true')" in html
    assert "row.focus()" in html


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

    assert 'data-mobile-task="queues"' in html
    assert 'id="mobile-queue-sheet"' in html
    assert 'aria-labelledby="mobile-queue-heading"' in html
    assert 'data-mobile-queue="today"' in html
    assert 'data-mobile-queue="agenda"' in html
    assert '<strong>Agenda</strong><small>Upcoming deadlines</small>' in html
    assert 'data-work-filter="agenda"' in html
    assert "const agendaItems = agendaMyWork(activeMyWork);" in html
    assert "counts.agenda = agendaItems.length;" in html
    assert 'data-agenda-group' in html
    assert 'data-mobile-queue="attention"' in html
    assert 'data-mobile-queue="delivery"' in html
    assert '<strong>Delivery</strong><small>Needs recovery</small>' in html
    assert 'data-mobile-queue-count="delivery"' in html
    assert 'data-mobile-queue="update"' in html
    assert '<strong>Updates</strong><small>Unread conversations</small>' in html
    assert 'data-mobile-queue="filed"' in html
    assert '<strong>Filed</strong><small>Issues you delegated</small>' in html
    assert 'data-mobile-queue-count="filed"' in html
    assert "openFiled: openFiledFollowUp" in html
    assert "filedFollowUpTarget(completedFiledReview.visible(lastMyWork))" in html
    assert 'data-mobile-queue="later"' in html
    assert 'data-mobile-queue="draft"' in html
    assert 'data-mobile-queue="recaps"' in html
    assert '<strong>Recaps</strong><small>History</small>' in html
    assert 'id="mobile-queue-count"' in html
    assert 'id="mobile-deadline-count"' in html
    assert 'mobileTaskDock.updateQueues(counts)' in html
    assert '<script src="static/mobile-queue-launcher.js"></script>' in html
    assert "const mobileQueueLauncher = createMobileQueueLauncher({" in html
    assert "firstAction: name => qs('#my-work-list .my-work-card-main, #my-work-list .draft-resume, #my-work-list .draft-continue, #my-work-list .draft-edit')" in html
    assert "name === 'find' ? qs('#find-work').click() : mobileQueueLauncher.open(name)" in html
    assert '.mobile-queue-sheet' in html
    assert '.my-work-actions { display:none;' in html
    assert 'padding-bottom:calc(16px + env(safe-area-inset-bottom))' in html
    assert '.mobile-task-deadline' in html
    assert '[data-deadlines="true"]' in html
    assert '.mobile-task-dock[data-attention="true"]' not in html


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

    assert 'id="mobile-work-label">Work</span>' in html
    assert '<script src="static/mobile-work-entry.js"></script>' in html
    assert "const mobileWorkEntry = createMobileWorkEntry({" in html
    assert "isTodayActive: () => workSession.checkpointed()" in html
    assert "isTodayResumable: () => workSession.resumable()" in html
    assert "getTodayCount: () => todayMyWork.length" in html
    assert "getEligibleCount: () => activeMyWork.length" in html
    assert "continueToday: continueTodaySession" in html
    assert "resumeToday: resumeTodaySession" in html
    assert "startToday: startTodaySession" in html
    assert "planToday: () => openPlanToday(mobileTaskButtons.work)" in html
    assert "work: () => mobileWorkEntry.open()" in html
    assert "mobileTaskDock.updateWork(mobileWorkEntry.mode())" in html
    assert "mobileTaskDock.updateAttention(countMyWork(activeMyWork).attention)" in html
    assert "workSession.reopen(item)" in html
    assert "runTodayTransition('continue')" in html


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

    assert "queueLauncher: mobileQueueLauncher" in html
    assert "work: () => mobileWorkEntry.open()" in html
    assert "continueWork: () => mobileWorkEntry.open()" in html
    assert "mobileQueueCounts = counts;" in html
    assert "mobileTaskDock.updateWork(mobileWorkEntry.mode())" in html


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

    assert '<nav class="mobile-task-dock" id="mobile-task-dock" aria-label="Primary tasks">' in html
    assert html.count('class="mobile-task-action" data-mobile-task=') == 5
    for task in ("work", "find", "new", "search", "queues"):
        assert f'data-mobile-task="{task}"' in html
    assert 'id="mobile-queue-count"' in html
    assert 'data-work-filter="attention"' in html
    assert '.mobile-task-dock { display:none;' in html
    assert 'grid-template-columns:repeat(5,minmax(0,1fr))' in html
    assert 'padding-bottom:env(safe-area-inset-bottom)' in html
    assert '.mobile-task-action { min-width:0; min-height:44px;' in html
    assert '<script src="static/mobile-task-dock.js"></script>' in html
    assert "createMobileTaskDock({" in html
    assert "work: () => mobileWorkEntry.open()" in html
    assert "name === 'find' ? qs('#find-work').click() : mobileQueueLauncher.open(name)" in html
    assert "qs('[data-work-filter=\"' + name + '\"]').click()" in html
    assert "find: () => qs('#find-work').click()" in html
    assert "new: () => qs('#new-issue').click()" in html
    assert "search: () => qs('#open-palette').click()" in html
    assert "detour:() => timerView" in html
    assert "mobileTaskDock.updateQueues(counts)" in html
    assert "mobileTaskDock.updateWork(mobileWorkEntry.mode())" in html
    assert "mobileTaskDock.updateAttention(countMyWork(activeMyWork).attention)" in html


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

    assert 'class="update-decision-bar" aria-label="Update decisions"' in html
    assert 'id="keep-update-unread" type="button" aria-label="Keep unread and open next update"' in html
    assert 'id="mark-update-read-next" type="button" aria-label="Mark read and open next update"' in html
    assert 'id="focus-update-reply" type="button">Reply</button>' in html
    assert 'class="update-more-actions"' in html
    assert "qs('#focus-update-reply').addEventListener('click'" in html
    assert "qs('#update-reply').focus({ preventScroll:true })" in html
    assert '.update-decision-bar { position:fixed;' in html
    assert 'padding-bottom:calc(8px + env(safe-area-inset-bottom))' in html
    assert '.update-decision-bar button, .update-decision-bar summary { min-height:44px;' in html
    assert 'padding-bottom:calc(82px + env(safe-area-inset-bottom))' in html


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

    assert 'class="mobile-today-hud" data-mobile-today-hud' in html
    assert 'data-mobile-today-open' in html
    assert 'data-work-session-progress' in html
    assert 'data-work-session-timer-toggle' in html
    assert 'data-work-session-adjust-plan' in html
    assert 'data-mobile-today-complete' in html
    assert 'Done &amp; next' in html
    assert "getItem: identity => [...todayMyWork, ...activeMyWork].find" in html
    assert "onReopen: identity =>" in html
    assert "onComplete: identity =>" in html
    assert "completeTodayItem(item)" in html
    assert "workSession.reopen(item)" in html
    assert '.mobile-today-hud { display:none;' in html
    assert 'data-mobile-today-more' in html
    assert 'class="mobile-today-actions"' in html
    assert 'grid-template-columns:repeat(6,minmax(0,1fr));' in html
    assert '[data-mobile-today-complete] { grid-column:1 / 3;' in html
    assert 'bottom:calc(56px + env(safe-area-inset-bottom))' in html
    assert '.mobile-today-hud button { min-width:0; min-height:44px;' in html
    assert 'var(--mobile-today-clearance, 166px)' in html
    assert '.mobile-task-action[hidden] { display:none;' in html
    assert 'max-width:100%;' in html
    assert 'overflow:hidden;' in html


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

    assert 'class="attention-interruption" id="attention-interruption"' in html
    assert 'id="return-to-today" type="button">Return to Today</button>' in html
    assert "if (name === 'attention' && workSession.checkpointed()) timer.beginAttention();" in html
    assert 'timer.returnFromAttention()' in html
    assert "workSession.reopen(item)" in html
    assert "attentionInterruption.hidden = !pending" in html
    assert '.attention-interruption:not([hidden]) {' in html
    assert '.attention-interruption button { min-height:44px;' in html
    assert 'max-width:100%;' in html
