import json
import subprocess
from pathlib import Path


MODULE = Path(__file__).parents[1] / "frontend" / "push-notifications.js"


def run_scenario(script: str) -> dict:
    harness = r"""
const createPushNotifications = require(__MODULE__);
const state = { prompts:0, requests:[], subscriptions:[], text:'' };
const control = {
  checked:false, disabled:false,
  addEventListener:(_name, callback) => state.change = callback,
};
const deadlineControl = {
  checked:false, disabled:false,
  addEventListener:(_name, callback) => state.deadlineChange = callback,
};
const deadlineHour = {value:'9', disabled:false, addEventListener:(_name, callback) => state.deadlineHourChange = callback};
const deadlineDays = {value:'2', disabled:false, addEventListener:(_name, callback) => state.deadlineDaysChange = callback};
const deadlineStatus = {set textContent(value) { state.deadlineText = value; }, get textContent() { return state.deadlineText; }};
const startDayControl = {
  checked:false, disabled:false,
  addEventListener:(_name, callback) => state.startDayChange = callback,
};
const startDayHour = {value:'9', disabled:false};
const startDayStatus = {set textContent(value) { state.startDayText = value; }, get textContent() { return state.startDayText; }};
const status = {set textContent(value) { state.text = value; }, get textContent() { return state.text; }};
const testControl = {hidden:true, disabled:false, addEventListener:(_name, callback) => state.testDelivery = callback};
const deadlineSnooze = {hidden:true};
const deadlineSnoozeStatus = {set textContent(value) { state.snoozeText = value; }, get textContent() { return state.snoozeText; }};
const deadlineSnoozeReview = {disabled:false, addEventListener:(_name, callback) => state.reviewSnooze = callback};
const existing = {endpoint:'https://push.example/device', toJSON() { return {endpoint:this.endpoint, keys:{p256dh:'key',auth:'auth'}}; }};
const registration = {pushManager:{
  getSubscription: async () => state.current || null,
  subscribe: async options => { state.subscriptions.push(options); state.current=existing; return existing; },
}};
const feature = createPushNotifications({
  control, status, testControl, deadlineControl, deadlineStatus, deadlineHour, deadlineDays,
  startDayControl, startDayStatus, startDayHour,
  deadlineSnooze, deadlineSnoozeStatus, deadlineSnoozeReview,
  onReviewDeadlines:() => { state.reviewed = true; },
  notification: state.notification = {permission:'default', requestPermission:async () => { state.prompts += 1; state.notification.permission = state.permission || 'granted'; return state.notification.permission; }},
  serviceWorker: {ready:Promise.resolve(registration)},
  fetchJson: async (url, options={}) => { state.requests.push([url,options.method || 'GET',options.body || '']); if (state.failResume && options.method === 'DELETE' && url.endsWith('/deadlines/snooze')) throw new Error('offline'); return state.server || {available:true,subscribed:false,public_key:'AQID'}; },
});
(async () => { __SCENARIO__ })().catch(error => { console.error(error); process.exit(1); });
""".replace("__MODULE__", json.dumps(str(MODULE))).replace("__SCENARIO__", script)
    completed = subprocess.run(
        ["node", "-e", harness], capture_output=True, check=True, text=True
    )
    return json.loads(completed.stdout)


def test_initialization_reports_availability_without_prompting_for_permission():
    result = run_scenario("await feature.init(); process.stdout.write(JSON.stringify(state));")

    assert result["prompts"] == 0
    assert result["requests"] == [["api/v1/push-subscription", "GET", ""]]
    assert result["text"] == "New update notifications are off for this device."


def test_user_gesture_subscribes_device_and_reports_enabled_state():
    result = run_scenario("""
await feature.init();
control.checked = true;
await state.change();
process.stdout.write(JSON.stringify(state));
""")

    assert result["prompts"] == 1
    assert result["subscriptions"][0]["userVisibleOnly"] is True
    assert result["requests"][-1][0:2] == ["api/v1/push-subscription", "PUT"]
    assert json.loads(result["requests"][-1][2])["endpoint"] == "https://push.example/device"
    assert result["text"] == "New update notifications enabled for this device."


def test_degraded_device_can_run_a_test_notification_and_show_recovery():
    result = run_scenario("""
state.server = {available:true,subscribed:true,public_key:'AQID',delivery_health:{unread:{state:'degraded',consecutive_failures:3,reason:'timeout'}}};
state.current = existing;
await feature.init();
const degraded = {text:state.text,hidden:testControl.hidden};
state.server = {delivery_state:'healthy',delivered:true};
await state.testDelivery();
process.stdout.write(JSON.stringify({degraded,text:state.text,hidden:testControl.hidden,disabled:testControl.disabled,requests:state.requests}));
""")

    assert result["degraded"] == {
        "text": "Update notifications need attention after 3 failed deliveries. Send a test notification.",
        "hidden": False,
    }
    assert result["requests"][-1][0:2] == ["api/v1/push-subscription/test", "POST"]
    assert result["text"] == "Test delivered. Update notifications are working on this device."
    assert result["hidden"] is False
    assert result["disabled"] is False


def test_deadline_opt_in_reuses_subscription_and_sends_local_timezone_without_second_prompt():
    result = run_scenario("""
state.current = existing;
state.server = {available:true,subscribed:true,deadline_enabled:false,public_key:'AQID'};
await feature.init();
deadlineControl.checked = true;
await state.deadlineChange();
process.stdout.write(JSON.stringify(state));
""")

    assert result["prompts"] == 0
    assert result["subscriptions"] == []
    assert result["requests"][-1][0:2] == ["api/v1/push-subscription/deadlines", "PUT"]
    body = json.loads(result["requests"][-1][2])
    assert body["enabled"] is True
    assert body["reminder_hour"] == 9
    assert isinstance(body["timezone"], str) and body["timezone"]
    assert result["deadlineText"] == "Deadline reminders enabled for 09:00 local time, next 2 days."


def test_start_day_opt_in_reuses_subscription_and_persists_local_hour():
    result = run_scenario("""
state.current = existing;
state.server = {available:true,subscribed:true,start_day_enabled:false,start_day_reminder_hour:9,public_key:'AQID'};
await feature.init();
startDayHour.value = '8';
startDayControl.checked = true;
await state.startDayChange();
process.stdout.write(JSON.stringify(state));
""")

    assert result["prompts"] == 0
    assert result["requests"][-1][0:2] == ["api/v1/push-subscription/start-day", "PUT"]
    body = json.loads(result["requests"][-1][2])
    assert body["enabled"] is True
    assert body["reminder_hour"] == 8
    assert isinstance(body["timezone"], str) and body["timezone"]
    assert result["startDayText"] == "Start-day reminder enabled for 08:00 local time."


def test_deadline_setup_subscribes_once_and_persists_selected_local_hour():
    result = run_scenario("""
state.server = {available:true,subscribed:false,deadline_enabled:false,reminder_hour:9,public_key:'AQID'};
await feature.init();
deadlineHour.value = '8';
await feature.enableDeadline();
process.stdout.write(JSON.stringify(state));
""")

    assert result["prompts"] == 1
    assert len(result["subscriptions"]) == 1
    assert [request[0] for request in result["requests"][-2:]] == [
        "api/v1/push-subscription", "api/v1/push-subscription/deadlines",
    ]
    body = json.loads(result["requests"][-1][2])
    assert body["enabled"] is True
    assert body["reminder_hour"] == 8
    assert result["deadlineText"] == "Deadline reminders enabled for 08:00 local time, next 2 days."


def test_deadline_setup_persists_and_restores_confirmed_horizon():
    saved = run_scenario("""
state.server = {available:true,subscribed:true,deadline_enabled:true,reminder_hour:9,reminder_days:2,public_key:'AQID'};
state.current = existing;
await feature.init();
deadlineDays.value = '7';
await feature.changeDeadline();
process.stdout.write(JSON.stringify(state));
""")

    assert json.loads(saved["requests"][-1][2])["reminder_days"] == 7
    assert saved["deadlineText"] == "Deadline reminders enabled for 09:00 local time, next 7 days."

    restored = run_scenario("""
state.server = {available:true,subscribed:true,deadline_enabled:true,reminder_hour:17,reminder_days:0,public_key:'AQID'};
state.current = existing;
await feature.init();
process.stdout.write(JSON.stringify({days:deadlineDays.value, readiness:feature.deadlineReadiness()}));
""")
    assert restored == {
        "days": "0",
        "readiness": {
            "state": "complete",
            "detail": "Deadline reminders enabled for 17:00 local time, due today.",
        },
    }


def test_deadline_setup_restores_confirmed_hour_and_denial_stays_incomplete():
    restored = run_scenario("""
state.server = {available:true,subscribed:true,deadline_enabled:true,reminder_hour:17,public_key:'AQID'};
state.current = existing;
await feature.init();
process.stdout.write(JSON.stringify({hour:deadlineHour.value, checked:deadlineControl.checked, readiness:feature.deadlineReadiness()}));
""")
    assert restored == {
        "hour": "17", "checked": True,
        "readiness": {"state": "complete", "detail": "Deadline reminders enabled for 17:00 local time, next 2 days."},
    }

    denied = run_scenario("""
state.server = {available:true,subscribed:false,deadline_enabled:false,reminder_hour:9,public_key:'AQID'};
state.permission = 'denied';
await feature.init();
deadlineHour.value = '8';
await feature.enableDeadline();
process.stdout.write(JSON.stringify({checked:deadlineControl.checked, readiness:feature.deadlineReadiness(), requests:state.requests}));
""")
    assert denied["checked"] is False
    assert denied["readiness"]["state"] == "blocked"
    assert denied["readiness"]["actionLabel"] == "Check again"
    assert "blocked" in denied["readiness"]["detail"].lower()
    assert [request[0] for request in denied["requests"]] == ["api/v1/push-subscription"]


def test_blocked_update_setup_can_recheck_without_reprompting_or_mutating():
    result = run_scenario("""
state.permission = 'denied';
await feature.init();
control.checked = true;
await state.change();
const blocked = feature.notificationReadiness();
await feature.recoverPermission();
process.stdout.write(JSON.stringify({blocked,after:feature.notificationReadiness(),prompts:state.prompts,subscriptions:state.subscriptions.length,requests:state.requests}));
""")

    assert result["blocked"] == {
        "state": "blocked",
        "detail": "Notifications are blocked. Allow them in browser settings, then check again.",
        "actionLabel": "Check again",
    }
    assert result["after"] == result["blocked"]
    assert result["prompts"] == 1
    assert result["subscriptions"] == 0
    assert result["requests"] == [["api/v1/push-subscription", "GET", ""]]


def test_previously_blocked_permission_starts_a_resumable_update_intent():
    result = run_scenario("""
state.permission = 'denied';
state.notification.permission = 'denied';
await feature.init();
await feature.recoverPermission('updates');
state.notification.permission = 'granted';
const readyToRecover = feature.notificationReadiness();
await feature.recoverPermission('updates');
process.stdout.write(JSON.stringify({readyToRecover,readiness:feature.notificationReadiness(),prompts:state.prompts,subscriptions:state.subscriptions.length,requests:state.requests}));
""")

    assert result["readyToRecover"]["actionLabel"] == "Check again"
    assert result["readiness"]["state"] == "complete"
    assert result["prompts"] == 0
    assert result["subscriptions"] == 1
    assert [request[0:2] for request in result["requests"]] == [
        ["api/v1/push-subscription", "GET"],
        ["api/v1/push-subscription", "PUT"],
    ]


def test_permission_recovery_resumes_pending_update_subscription_once():
    result = run_scenario("""
state.permission = 'denied';
await feature.init();
control.checked = true;
await state.change();
state.notification.permission = 'granted';
const readyToRecover = feature.notificationReadiness();
await feature.recoverPermission();
await feature.recoverPermission();
process.stdout.write(JSON.stringify({readyToRecover,readiness:feature.notificationReadiness(),prompts:state.prompts,subscriptions:state.subscriptions.length,requests:state.requests}));
""")

    assert result["readyToRecover"] == {
        "state": "blocked",
        "detail": "Notification permission changed. Check again to finish setup.",
        "actionLabel": "Check again",
    }
    assert result["readiness"]["state"] == "complete"
    assert result["prompts"] == 1
    assert result["subscriptions"] == 1
    assert [request[0:2] for request in result["requests"]] == [
        ["api/v1/push-subscription", "GET"],
        ["api/v1/push-subscription", "PUT"],
    ]


def test_permission_recovery_resumes_pending_deadline_choices_once():
    result = run_scenario("""
state.server = {available:true,subscribed:false,deadline_enabled:false,reminder_hour:9,reminder_days:2,public_key:'AQID'};
state.permission = 'denied';
await feature.init();
deadlineHour.value = '17';
deadlineDays.value = '7';
await feature.enableDeadline();
const blocked = feature.deadlineReadiness();
state.notification.permission = 'granted';
const readyToRecover = feature.deadlineReadiness();
await feature.recoverPermission();
await feature.recoverPermission();
process.stdout.write(JSON.stringify({blocked,readyToRecover,readiness:feature.deadlineReadiness(),prompts:state.prompts,subscriptions:state.subscriptions.length,requests:state.requests}));
""")

    assert result["blocked"]["state"] == "blocked"
    assert result["blocked"]["actionLabel"] == "Check again"
    assert result["readyToRecover"] == {
        "state": "blocked",
        "detail": "Notification permission changed. Check again to finish setup.",
        "actionLabel": "Check again",
    }
    assert result["readiness"] == {
        "state": "complete",
        "detail": "Deadline reminders enabled for 17:00 local time, next 7 days.",
    }
    assert result["prompts"] == 1
    assert result["subscriptions"] == 1
    assert [request[0:2] for request in result["requests"]] == [
        ["api/v1/push-subscription", "GET"],
        ["api/v1/push-subscription", "PUT"],
        ["api/v1/push-subscription/deadlines", "PUT"],
    ]
    assert json.loads(result["requests"][-1][2])["reminder_hour"] == 17
    assert json.loads(result["requests"][-1][2])["reminder_days"] == 7


def test_active_deadline_snooze_is_visible_in_agenda_with_a_local_resume_time():
    result = run_scenario("""
state.server = {available:true,subscribed:true,deadline_enabled:true,reminder_hour:9,reminder_days:2,snoozed_until:1765003600,public_key:'AQID'};
state.current = existing;
await feature.init();
process.stdout.write(JSON.stringify({hidden:deadlineSnooze.hidden,text:state.snoozeText}));
""")

    assert result["hidden"] is False
    assert result["text"].startswith("Deadline reminders snoozed until ")


def test_review_now_resumes_this_device_and_opens_the_deadline_review_flow():
    result = run_scenario("""
state.server = {available:true,subscribed:true,deadline_enabled:true,snoozed_until:1765003600,public_key:'AQID'};
state.current = existing;
await feature.init();
await state.reviewSnooze();
process.stdout.write(JSON.stringify({hidden:deadlineSnooze.hidden,disabled:deadlineSnoozeReview.disabled,reviewed:state.reviewed,requests:state.requests}));
""")

    assert result["requests"][-1][0:2] == [
        "api/v1/push-subscription/deadlines/snooze", "DELETE",
    ]
    assert result["hidden"] is True
    assert result["disabled"] is False
    assert result["reviewed"] is True


def test_failed_resume_keeps_the_snooze_visible_and_retryable():
    result = run_scenario("""
state.server = {available:true,subscribed:true,deadline_enabled:true,snoozed_until:1765003600,public_key:'AQID'};
state.current = existing;
await feature.init();
state.failResume = true;
await state.reviewSnooze();
process.stdout.write(JSON.stringify({hidden:deadlineSnooze.hidden,disabled:deadlineSnoozeReview.disabled,text:state.snoozeText,reviewed:state.reviewed}));
""")

    assert result == {
        "hidden": False,
        "disabled": False,
        "text": "Could not resume deadline reminders. Check your connection and try again.",
    }


def test_mobile_dashboard_mounts_opt_in_and_precaches_its_controller():
    root = MODULE.parents[1]
    html = (root / "frontend" / "index.html").read_text()
    dashboard = (root / "frontend" / "dashboard.js").read_text()
    worker = (root / "frontend" / "service-worker.js").read_text()
    css = (root / "frontend" / "dashboard.css").read_text()
    requirements = (root / "requirements.txt").read_text()
    readme = (root / "README.md").read_text()

    assert 'id="push-updates"' in html
    assert 'id="push-update-status"' in html
    assert 'id="push-test"' in html
    assert 'id="push-deadlines"' in html
    assert 'id="push-deadline-hour"' in html
    assert 'id="push-deadline-days"' in html
    assert 'id="device-setup-deadline-days"' in html
    assert 'id="push-deadline-status"' in html
    assert '<script src="static/push-notifications.js"></script>' in html
    assert "createPushNotifications({" in dashboard
    assert "testControl:qs('#push-test')" in dashboard
    assert "BASE + 'static/push-notifications.js'" in worker
    assert ".push-update-control" in css and "min-height:44px" in css
    assert "pywebpush==" in requirements
    assert "STACKCHAIN_VAPID_PUBLIC_KEY" in readme
    assert "STACKCHAIN_VAPID_PRIVATE_KEY" in readme


def test_mobile_agenda_mounts_a_touch_safe_snooze_recovery_flow():
    root = MODULE.parents[1]
    html = (root / "frontend" / "index.html").read_text()
    dashboard = (root / "frontend" / "dashboard.js").read_text()
    css = (root / "frontend" / "dashboard.css").read_text()

    assert 'id="deadline-snooze"' in html
    assert 'id="deadline-snooze-status"' in html
    assert 'id="review-snoozed-deadlines"' in html
    assert "deadlineSnooze:qs('#deadline-snooze')" in dashboard
    assert "onReviewDeadlines:" in dashboard
    assert "window.location.hash = '#/my-work/agenda'" in dashboard
    assert "qs('#protect-today')" in dashboard
    assert ".deadline-snooze" in css
    assert ".deadline-snooze button { min-height:44px;" in css
    assert "overflow-wrap:anywhere" in css
