import json
import subprocess
from pathlib import Path

import pytest

from tests.dashboard_bundle import dashboard


ROOT = Path(__file__).resolve().parents[1]
RECOVERY = ROOT / "frontend" / "mobile-delivery-recovery.js"


def run_node(script: str) -> dict:
    result = subprocess.run(
        ["node", "-e", script],
        cwd=ROOT,
        text=True,
        capture_output=True,
        check=False,
    )
    assert result.returncode == 0, result.stderr
    return json.loads(result.stdout)


def test_recovery_orders_deliveries_by_safest_required_intervention():
    script = f"""
const createRecovery = require({json.dumps(str(RECOVERY))});
const recovery = createRecovery({{
  getItems: () => [
    {{outbox_id:'waiting', delivery_state:'waiting', title:'Waiting'}},
    {{outbox_id:'uncertain', delivery_state:'uncertain', title:'Uncertain'}},
    {{outbox_id:'attention', status:'attention', title:'Attention'}},
    {{outbox_id:'authorization', status:'authorization', title:'Authorization'}},
  ],
}});
process.stdout.write(JSON.stringify(recovery.snapshot()));
"""

    assert run_node(script) == {
        "count": 4,
        "current": {
            "id": "authorization",
            "state": "authorization",
            "primary": "Authorize & send",
            "position": 1,
            "total": 4,
        },
    }


def test_recovery_advances_only_after_the_current_delivery_is_resolved():
    script = f"""
const createRecovery = require({json.dumps(str(RECOVERY))});
(async () => {{
  let items = [
    {{outbox_id:'authorize', status:'authorization', title:'Approve review'}},
    {{outbox_id:'retry', delivery_state:'waiting', title:'Post comment'}},
  ];
  let resolves = false;
  const recovery = createRecovery({{
    getItems: () => items,
    activate: async item => {{
      if (resolves) items = items.filter(candidate => candidate.outbox_id !== item.outbox_id);
      return resolves;
    }},
  }});
  recovery.open();
  const retained = await recovery.activate();
  resolves = true;
  const advanced = await recovery.activate();
  process.stdout.write(JSON.stringify({{retained, advanced}}));
}})();
"""

    assert run_node(script) == {
        "retained": {
            "count": 2,
            "current": {
                "id": "authorize",
                "state": "authorization",
                "primary": "Authorize & send",
                "position": 1,
                "total": 2,
            },
        },
        "advanced": {
            "count": 1,
            "current": {
                "id": "retry",
                "state": "waiting",
                "primary": "Retry now",
                "position": 1,
                "total": 1,
            },
        },
    }


def test_recovery_keeps_one_attempt_locked_until_delivery_settles():
    script = f"""
const createRecovery = require({json.dumps(str(RECOVERY))});
(async () => {{
  let finish;
  let attempts = 0;
  const settled = new Promise(resolve => {{finish = resolve}});
  const element = () => ({{textContent:'', disabled:false}});
  const elements = {{
    dialog: {{open:false, showModal(){{this.open=true}}, close(){{this.open=false}}}},
    title:element(), destination:element(), reason:element(), progress:element(),
    action:element(), status:element(),
  }};
  const recovery = createRecovery({{
    getItems: () => [{{outbox_id:'retry', delivery_state:'waiting', title:'Post comment'}}],
    activate: async () => {{attempts += 1; await settled; return true}},
    elements,
  }});
  recovery.open();
  const first = recovery.activate();
  const second = recovery.activate();
  await Promise.resolve();
  const pending = {{attempts, disabled:elements.action.disabled, status:elements.status.textContent}};
  finish();
  await Promise.all([first, second]);
  process.stdout.write(JSON.stringify({{
    pending, attempts, disabled:elements.action.disabled,
  }}));
}})();
"""

    assert run_node(script) == {
        "pending": {"attempts": 1, "disabled": True, "status": "Working…"},
        "attempts": 1,
        "disabled": False,
    }


def test_attention_handoff_closes_modal_focuses_destination_and_awaits_work():
    script = f"""
const createRecovery = require({json.dumps(str(RECOVERY))});
(async () => {{
  let finish;
  let handoffs = 0;
  const settled = new Promise(resolve => {{finish = resolve}});
  const destination = {{
    focused:false, scrolled:false,
    focus(){{this.focused=true}}, scrollIntoView(){{this.scrolled=true}},
    async onclick(){{handoffs += 1; await settled; return true}},
  }};
  global.document = {{
    getElementById: () => null,
    querySelector: selector => selector.includes('draft-resume') ? destination : null,
  }};
  const element = () => ({{textContent:'', disabled:false}});
  const elements = {{
    dialog: {{open:false, showModal(){{this.open=true}}, close(){{this.open=false}}}},
    title:element(), destination:element(), reason:element(), progress:element(),
    action:element(), status:element(),
  }};
  const recovery = createRecovery({{
    getItems: () => [{{outbox_id:'review', status:'attention', title:'Review feedback'}}],
    getIndex: () => 3,
    elements,
  }});
  recovery.open();
  const first = recovery.activate();
  const second = recovery.activate();
  await Promise.resolve();
  const pending = {{
    dialogOpen:elements.dialog.open, disabled:elements.action.disabled,
    focused:destination.focused, scrolled:destination.scrolled, handoffs,
  }};
  finish();
  await Promise.all([first, second]);
  process.stdout.write(JSON.stringify({{pending, handoffs, dialogOpen:elements.dialog.open}}));
}})();
"""

    assert run_node(script) == {
        "pending": {
            "dialogOpen": False,
            "disabled": True,
            "focused": True,
            "scrolled": True,
            "handoffs": 1,
        },
        "handoffs": 1,
        "dialogOpen": False,
    }


def test_failed_attention_handoff_reopens_the_same_retryable_item():
    script = f"""
const createRecovery = require({json.dumps(str(RECOVERY))});
(async () => {{
  const destination = {{
    focus(){{}}, scrollIntoView(){{}},
    async onclick(){{throw new Error('Latest checklist unavailable')}},
  }};
  global.document = {{
    getElementById: () => null,
    querySelector: () => destination,
  }};
  const element = () => ({{textContent:'', disabled:false}});
  const elements = {{
    dialog: {{open:false, showModal(){{this.open=true}}, close(){{this.open=false}}}},
    title:element(), destination:element(), reason:element(), progress:element(),
    action:element(), status:element(),
  }};
  const recovery = createRecovery({{
    getItems: () => [{{outbox_id:'conflict', checklist_conflict:true, title:'Checklist conflict'}}],
    getIndex: () => 0,
    elements,
  }});
  recovery.open();
  const result = await recovery.activate();
  process.stdout.write(JSON.stringify({{
    result, dialogOpen:elements.dialog.open, status:elements.status.textContent,
    action:elements.action.textContent,
  }}));
}})();
"""

    assert run_node(script) == {
        "result": {
            "count": 1,
            "current": {
                "id": "conflict",
                "state": "attention",
                "primary": "Review changes",
                "position": 1,
                "total": 1,
            },
        },
        "dialogOpen": True,
        "status": "Latest checklist unavailable Try again.",
        "action": "Review changes",
    }


def test_recovery_retains_failed_delivery_with_actionable_status():
    script = f"""
const createRecovery = require({json.dumps(str(RECOVERY))});
(async () => {{
  const element = () => ({{textContent:'', disabled:false}});
  const elements = {{
    dialog: {{open:false, showModal(){{this.open=true}}, close(){{this.open=false}}}},
    title:element(), destination:element(), reason:element(), progress:element(),
    action:element(), status:element(),
  }};
  const recovery = createRecovery({{
    getItems: () => [{{outbox_id:'retry', delivery_state:'waiting', title:'Post comment'}}],
    activate: async () => {{throw new Error('Network unavailable')}},
    elements,
  }});
  recovery.open();
  const result = await recovery.activate();
  process.stdout.write(JSON.stringify({{
    result, disabled:elements.action.disabled, status:elements.status.textContent,
  }}));
}})();
"""

    assert run_node(script) == {
        "result": {
            "count": 1,
            "current": {
                "id": "retry",
                "state": "waiting",
                "primary": "Retry now",
                "position": 1,
                "total": 1,
            },
        },
        "disabled": False,
        "status": "Network unavailable Try again.",
    }


def test_recovery_renders_delivery_context_and_hands_off_when_cleared():
    script = f"""
const createRecovery = require({json.dumps(str(RECOVERY))});
(async () => {{
  let items = [{{
    outbox_id:'close-7', status:'authorization', outbox_kind:'issue-close',
    title:'Close resolved incident', repository:'stackchain/ops',
    details:'#7', last_attempt_error:'Fresh authorization expired',
  }}];
  let completed = 0;
  const element = () => ({{textContent:'', hidden:false}});
  const elements = {{
    dialog: {{open:false, showModal(){{this.open=true}}, close(){{this.open=false}}}},
    title: element(), destination: element(), reason: element(), progress: element(),
    action: element(), status: element(),
  }};
  const recovery = createRecovery({{
    getItems: () => items,
    activate: async () => {{items = []; return true}},
    onComplete: () => {{completed += 1}},
    elements,
  }});
  const opened = recovery.open();
  const rendered = {{
    opened, dialogOpen:elements.dialog.open, title:elements.title.textContent,
    destination:elements.destination.textContent, reason:elements.reason.textContent,
    progress:elements.progress.textContent, action:elements.action.textContent,
  }};
  const cleared = await recovery.activate();
  process.stdout.write(JSON.stringify({{rendered, cleared, completed, dialogOpen:elements.dialog.open}}));
}})();
"""

    assert run_node(script) == {
        "rendered": {
            "opened": "opened",
            "dialogOpen": True,
            "title": "Close resolved incident",
            "destination": "stackchain/ops · #7",
            "reason": "Fresh authorization expired",
            "progress": "Delivery 1 of 1 · Authorization required",
            "action": "Authorize & close",
        },
        "cleared": {"count": 0, "current": None},
        "completed": 1,
        "dialogOpen": False,
    }


@pytest.mark.anyio
async def test_dashboard_packages_phone_safe_guided_delivery_recovery():
    html = await dashboard()
    service_worker = (ROOT / "frontend" / "service-worker.js").read_text()
    bundle = (ROOT / "src" / "frontend_bundle.py").read_text()

    assert '<dialog id="mobile-delivery-recovery"' in html
    assert 'aria-labelledby="mobile-delivery-recovery-title"' in html
    assert 'id="mobile-delivery-recovery-action"' in html
    assert '<script src="static/mobile-delivery-recovery.js"></script>' in html
    assert "const mobileDeliveryRecovery = createMobileDeliveryRecovery({" in html
    assert "activate: recoverMobileDelivery," in html
    assert "button.onclick=async()=>" in html
    assert "await (authored ? authoredOutbox : issueOutbox).retry(item.outbox_id, activeFlushLogin)" in html
    assert "(authored ? applyAuthoredOutboxResult : applyOutboxResult)(result)" in html
    assert "openDelivery: () => mobileDeliveryRecovery.open()" in html
    assert "mobileStartDay.completePhase('delivery')" in html
    assert "BASE + 'static/mobile-delivery-recovery.js'" in service_worker
    assert '"static/mobile-delivery-recovery.js"' in bundle
    assert ".mobile-delivery-recovery-panel" in html
    assert "min-height:48px" in html
    assert "env(safe-area-inset-bottom)" in html
