import json
import subprocess
from pathlib import Path


MODULE = Path(__file__).parents[1] / "frontend" / "mobile-device-setup.js"


def run_scenario(script: str) -> dict:
    harness = r"""
const createMobileDeviceSetup = require(__MODULE__);
class FakeTarget {
  constructor() { this.listeners = {}; this.hidden = false; this.disabled = false; this.textContent = ''; this.inert = false; }
  addEventListener(name, callback) { (this.listeners[name] ||= []).push(callback); }
  async dispatch(name, event = {}) { for (const callback of this.listeners[name] || []) await callback(event); }
  focus() { state.focused = this; }
}
const state = {
  installCalls:0, offlineCalls:0, pushCalls:0, deadlineCalls:0, focused:null,
  events:[], beginCalls:[], finishCalls:0,
};
const launcher = new FakeTarget();
const closeButton = new FakeTarget();
const sheet = new FakeTarget(); sheet.hidden = true;
const installButton = new FakeTarget();
const offlineButton = new FakeTarget();
const protectionButton = new FakeTarget();
const pushButton = new FakeTarget();
const deadlineButton = new FakeTarget();
const installStatus = new FakeTarget();
const offlineStatus = new FakeTarget();
const protectionStatus = new FakeTarget();
const pushStatus = new FakeTarget();
const deadlineStatus = new FakeTarget();
const readyStatus = new FakeTarget();
const escapeTarget = new FakeTarget();
const promptCard = new FakeTarget(); promptCard.hidden = true;
const promptSummary = new FakeTarget();
const promptLauncher = new FakeTarget();
const promptDismiss = new FakeTarget();
const returnButton = new FakeTarget();
const backgroundHeader = new FakeTarget();
const backgroundMain = new FakeTarget(); backgroundMain.inert = true;
sheet.querySelectorAll = () => [closeButton, installButton, offlineButton, returnButton];
const historyTarget = new FakeTarget();
const history = {
  state:{section:'home'},
  pushState(state) { this.state = state; },
  back() { this.state = {section:'home'}; historyTarget.dispatch('popstate', {state:this.state}); },
};
let now = 1000;
const promptStorage = {
  values:{},
  getItem(key) { return this.values[key] ?? null; },
  setItem(key, value) { this.values[key] = String(value); },
};
let readiness = {
  install:{state:'complete', detail:'Stackchain is installed.'},
  offline:{state:'incomplete', detail:'Offline work is off.'},
  protection:{state:'incomplete', detail:'Offline work uses best-effort browser storage.'},
  push:{state:'unavailable', detail:'Notifications are unavailable.'},
  deadline:{state:'unavailable', detail:'Deadline reminders are unavailable.'},
};
const setup = createMobileDeviceSetup({
  launcher, closeButton, sheet, installButton, offlineButton, protectionButton, pushButton, deadlineButton,
  installStatus, offlineStatus, protectionStatus, pushStatus, deadlineStatus, readyStatus, escapeTarget,
  promptCard, promptSummary, promptLauncher, promptDismiss,
  returnButton, isMobile:() => true,
  backgroundElements:[backgroundHeader, backgroundMain],
  history, historyTarget,
  timerView:{
    beginDetour:reason => { state.events.push('pause'); state.beginCalls.push(reason); return {reason}; },
    finishDetour:() => { state.events.push('resume'); state.finishCalls += 1; },
  },
  promptStorage, now:() => now,
  getReadiness:() => { state.events.push('readiness'); return readiness; },
  install:async () => { state.installCalls += 1; },
  enableOffline:async () => { state.offlineCalls += 1; },
  protectStorage:async () => { state.protectionCalls = (state.protectionCalls || 0) + 1; },
  enablePush:async () => { state.pushCalls += 1; },
  enableDeadline:async () => { state.deadlineCalls += 1; },
});
(async () => { await setup.start(); __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, text=True)
    assert completed.returncode == 0, completed.stderr
    return json.loads(completed.stdout)


def test_opening_setup_reports_actual_readiness_without_triggering_permissions():
    result = run_scenario("""
await launcher.dispatch('click', {currentTarget:launcher});
process.stdout.write(JSON.stringify({
  hidden:sheet.hidden,
  install:installStatus.textContent,
  offline:offlineStatus.textContent,
  push:pushStatus.textContent,
  summary:readyStatus.textContent,
  calls:[state.installCalls,state.offlineCalls,state.pushCalls],
}));
""")

    assert result == {
        "hidden": False,
        "install": "Stackchain is installed.",
        "offline": "Offline work is off.",
        "push": "Notifications are unavailable.",
        "summary": "1 of 3 available steps ready.",
        "calls": [0, 0, 0],
    }


def test_storage_protection_runs_only_from_its_explicit_setup_action():
    result = run_scenario("""
await launcher.dispatch('click', {currentTarget:launcher});
const callsOnOpen=state.protectionCalls || 0;
readiness.protection={state:'complete', detail:'Offline work is protected from automatic browser storage cleanup.'};
await protectionButton.dispatch('click');
process.stdout.write(JSON.stringify({
  callsOnOpen,
  callsAfterAction:state.protectionCalls,
  detail:protectionStatus.textContent,
  hidden:protectionButton.hidden,
}));
""")

    assert result == {
        "callsOnOpen": 0,
        "callsAfterAction": 1,
        "detail": "Offline work is protected from automatic browser storage cleanup.",
        "hidden": True,
    }


def test_incomplete_device_is_discoverable_without_triggering_setup_actions():
    result = run_scenario("""
process.stdout.write(JSON.stringify({
  hidden:promptCard.hidden,
  summary:promptSummary.textContent,
  calls:[state.installCalls,state.offlineCalls,state.pushCalls],
}));
""")

    assert result == {
        "hidden": False,
        "summary": "1 of 3 steps complete",
        "calls": [0, 0, 0],
    }


def test_readiness_card_opens_setup_and_receives_focus_back_on_close():
    result = run_scenario("""
await promptLauncher.dispatch('click', {currentTarget:promptLauncher});
const opened = !sheet.hidden && state.focused === closeButton;
await closeButton.dispatch('click');
process.stdout.write(JSON.stringify({
  opened,
  closed:sheet.hidden,
  promptFocused:state.focused === promptLauncher,
}));
""")

    assert result == {"opened": True, "closed": True, "promptFocused": True}


def test_not_now_hides_prompt_for_seven_days_then_allows_it_to_return():
    result = run_scenario("""
await promptDismiss.dispatch('click');
const hiddenAfterDismiss = promptCard.hidden;
const dismissedUntil = Number(promptStorage.values['stackchain.device-setup-prompt-dismissed-until']);
now = dismissedUntil + 1;
await setup.render();
process.stdout.write(JSON.stringify({hiddenAfterDismiss, delay:dismissedUntil - 1000, hiddenAfterExpiry:promptCard.hidden}));
""")

    assert result == {
        "hiddenAfterDismiss": True,
        "delay": 7 * 24 * 60 * 60 * 1000,
        "hiddenAfterExpiry": False,
    }


def test_setup_action_rechecks_real_state_before_marking_step_ready():
    result = run_scenario("""
await launcher.dispatch('click', {currentTarget:launcher});
readiness.protection = {state:'complete', detail:'Offline work is protected.'};
readiness.offline = {state:'complete', detail:'Offline work is saved.'};
await offlineButton.dispatch('click');
process.stdout.write(JSON.stringify({
  calls:state.offlineCalls,
  offline:offlineStatus.textContent,
  hidden:offlineButton.hidden,
  promptHidden:promptCard.hidden,
  summary:readyStatus.textContent,
}));
""")

    assert result == {
        "calls": 1,
        "offline": "Offline work is saved.",
        "hidden": True,
        "promptHidden": True,
        "summary": "This device is ready.",
    }


def test_escape_closes_setup_and_restores_launcher_focus():
    result = run_scenario("""
await launcher.dispatch('click', {currentTarget:launcher});
await escapeTarget.dispatch('keydown', {key:'Escape'});
process.stdout.write(JSON.stringify({
  hidden:sheet.hidden,
  launcherFocused:state.focused === launcher,
}));
""")

    assert result == {"hidden": True, "launcherFocused": True}


def test_open_setup_contains_focus_and_restores_background_inert_state():
    result = run_scenario("""
await launcher.dispatch('click', {currentTarget:launcher});
state.focused = returnButton;
let preventedForward = false;
await escapeTarget.dispatch('keydown', {key:'Tab', target:returnButton, preventDefault(){preventedForward=true;}});
const forwardFocus = state.focused === closeButton;
state.focused = closeButton;
let preventedReverse = false;
await escapeTarget.dispatch('keydown', {key:'Tab', shiftKey:true, target:closeButton, preventDefault(){preventedReverse=true;}});
const reverseFocus = state.focused === returnButton;
const inertWhileOpen = [backgroundHeader.inert, backgroundMain.inert];
await closeButton.dispatch('click');
process.stdout.write(JSON.stringify({
  preventedForward, forwardFocus, preventedReverse, reverseFocus,
  inertWhileOpen,
  inertAfterClose:[backgroundHeader.inert, backgroundMain.inert],
  launcherFocused:state.focused === launcher,
}));
""")

    assert result == {
        "preventedForward": True,
        "forwardFocus": True,
        "preventedReverse": True,
        "reverseFocus": True,
        "inertWhileOpen": [True, True],
        "inertAfterClose": [False, True],
        "launcherFocused": True,
    }


def test_browser_back_closes_setup_and_restores_launcher_focus():
    result = run_scenario("""
await launcher.dispatch('click', {currentTarget:launcher});
const marked = history.state.deviceSetup === true;
history.back();
await new Promise(resolve => setImmediate(resolve));
process.stdout.write(JSON.stringify({
  marked,
  hidden:sheet.hidden,
  backgroundReleased:backgroundHeader.inert === false,
  launcherFocused:state.focused === launcher,
}));
""")

    assert result == {
        "marked": True,
        "hidden": True,
        "backgroundReleased": True,
        "launcherFocused": True,
    }


def test_mobile_setup_pauses_before_readiness_and_each_exit_resumes_once():
    result = run_scenario("""
state.events = [];
await launcher.dispatch('click', {currentTarget:launcher});
const firstOpen = {events:[...state.events], reasons:[...state.beginCalls]};
await sheet.dispatch('click', {target:sheet});
const backdropFinish = state.finishCalls;
await promptLauncher.dispatch('click', {currentTarget:promptLauncher});
await escapeTarget.dispatch('keydown', {key:'Escape'});
const escapeFinish = state.finishCalls;
await launcher.dispatch('click', {currentTarget:launcher});
returnButton.addEventListener('click', () => { state.events.push('shared-resume'); state.finishCalls += 1; });
await returnButton.dispatch('click');
process.stdout.write(JSON.stringify({
  firstOpen, backdropFinish, escapeFinish,
  returnFinish:state.finishCalls,
  hidden:sheet.hidden,
}));
""")

    assert result == {
        "firstOpen": {"events": ["pause", "readiness"], "reasons": ["device-setup"]},
        "backdropFinish": 1,
        "escapeFinish": 2,
        "returnFinish": 3,
        "hidden": True,
    }


def test_deadline_step_runs_one_setup_action_and_uses_confirmed_readiness():
    result = run_scenario("""
readiness.push = {state:'complete', detail:'New update notifications are enabled.'};
readiness.deadline = {state:'incomplete', detail:'Choose when to receive deadline reminders.'};
await launcher.dispatch('click', {currentTarget:launcher});
readiness.deadline = {state:'complete', detail:'Deadline reminders enabled for 08:00 local time.'};
await deadlineButton.dispatch('click');
process.stdout.write(JSON.stringify({
  calls:state.deadlineCalls,
  detail:deadlineStatus.textContent,
  hidden:deadlineButton.hidden,
  summary:readyStatus.textContent,
}));
""")

    assert result == {
        "calls": 1,
        "detail": "Deadline reminders enabled for 08:00 local time.",
        "hidden": True,
        "summary": "3 of 5 available steps ready.",
    }


def test_blocked_notification_step_becomes_a_check_again_action():
    result = run_scenario("""
readiness.push = {
  state:'blocked',
  detail:'Notifications are blocked. Allow them in browser settings, then check again.',
  actionLabel:'Check again',
};
await launcher.dispatch('click', {currentTarget:launcher});
await pushButton.dispatch('click');
process.stdout.write(JSON.stringify({label:pushButton.textContent,detail:pushStatus.textContent,calls:state.pushCalls,hidden:pushButton.hidden}));
""")

    assert result == {
        "label": "Check again",
        "detail": "Notifications are blocked. Allow them in browser settings, then check again.",
        "calls": 1,
        "hidden": False,
    }


def test_mobile_dashboard_mounts_phone_safe_device_setup_flow():
    root = MODULE.parents[1]
    html = (root / "frontend" / "index.html").read_text()
    dashboard = (root / "frontend" / "dashboard.js").read_text()
    storage_module = (root / "frontend" / "device-storage.js").read_text()
    worker = (root / "frontend" / "service-worker.js").read_text()
    css = (root / "frontend" / "dashboard.css").read_text()

    assert 'id="open-device-setup"' in html
    assert 'id="device-readiness-card"' in html
    assert 'id="finish-device-setup"' in html
    assert 'id="dismiss-device-readiness"' in html
    assert html.index('id="device-readiness-card"') < html.index('id="my-work-list"')
    assert 'id="device-setup-sheet"' in html
    assert 'id="device-setup-today-detour"' in html
    assert 'id="return-from-device-setup"' in html
    assert 'aria-labelledby="device-setup-heading"' in html
    assert all(f'id="device-setup-{step}"' in html for step in ("install", "offline", "protection", "push", "deadline"))
    assert 'id="device-setup-protection-status"' in html
    assert 'id="device-storage-summary"' in html
    assert 'id="device-storage-detail"' in html
    assert 'id="clear-device-caches"' in html
    assert 'id="clear-private-device-data"' in html
    assert '<script src="static/private-device-data.js"></script>' in html
    assert '<script src="static/device-storage.js"></script>' in html
    assert 'id="device-setup-deadline-hour"' in html
    assert '<script src="static/mobile-device-setup.js"></script>' in html
    assert "createMobileDeviceSetup.mount({" in dashboard
    assert "timerView," in dashboard
    assert "isMobile:options.isMobile || (() => innerWidth <= 600)" in MODULE.read_text()
    assert "const deviceStorage = createDeviceStorage.mount(document)" in dashboard
    assert "storageProtectionReadiness:() => deviceStorage.persistenceReadiness()" in dashboard
    assert "protectStorage:() => deviceStorage.requestPersistence()" in dashboard
    assert "clearPrivateDeviceData:root.stackchainPrivateDeviceData" in storage_module
    assert "promptStorage:localStorage" in dashboard
    assert "notificationReadiness:() => pushController?.notificationReadiness()" in dashboard
    assert "controller.recoverPermission('updates')" in dashboard
    assert "controller.recoverPermission('deadline')" in dashboard
    assert "pushControllerReady.then(ensureDeviceSetup)" in dashboard
    assert "BASE + 'static/mobile-device-setup.js'" in worker
    assert "stackchain-dashboard-shell-v127" in worker
    assert ".device-setup-panel" in css
    assert ".device-readiness-card" in css
    assert "overflow-x:hidden" in css
    assert "env(safe-area-inset-bottom)" in css
    assert ".device-setup-action { min-height:44px;" in css
    assert ".device-readiness-actions button { min-height:44px;" in css
