import json
import subprocess
from pathlib import Path


SECURITY_CENTER = Path(__file__).parents[1] / "frontend" / "security-center.js"


def test_open_security_center_loads_all_sections_concurrently_and_is_awaitable():
    harness = f"""
const attachSecurityCenter=require({json.dumps(str(SECURITY_CENTER))});
const calls=[];
const element=()=>({{
  hidden:true, disabled:false, textContent:'', children:[],
  addEventListener(){{}}, focus(){{this.focused=true;}},
  replaceChildren(){{this.children=[];}}, append(value){{this.children.push(value);}},
}});
const ids={{
  'active-devices':element(), 'active-devices-sheet':element(),
  'active-devices-list':element(), 'active-devices-status':element(),
  'enrolled-passkeys-list':element(), 'enrolled-passkeys-status':element(),
  'enroll-passkey':element(), 'security-activity-list':element(),
  'security-activity-status':element(), 'load-more-security-activity':element(),
  'close-active-devices':element(),
}};
const root={{document:{{getElementById:id=>ids[id]||null,createElement:()=>element()}}}};
const boundary={{
  listActiveDevices:async()=>{{calls.push('devices');return[];}},
  listPasskeys:async()=>{{calls.push('passkeys');return[];}},
  listSecurityEvents:async()=>{{calls.push('activity');return{{events:[],authentication_alerts:[],next_cursor:null}};}},
}};
(async()=>{{
  const controller=attachSecurityCenter({{root,boundary}});
  const loading=controller.open();
  const awaitable=Boolean(loading&&typeof loading.then==='function');
  if (loading) await loading;
  console.log(JSON.stringify({{calls,awaitable,hidden:ids['active-devices-sheet'].hidden,focused:ids['close-active-devices'].focused||false}}));
}})().catch(error=>{{console.error(error);process.exit(1);}});
"""
    completed = subprocess.run(
        ["node", "-e", harness], check=True, capture_output=True, text=True
    )

    assert json.loads(completed.stdout) == {
        "calls": ["devices", "passkeys", "activity"],
        "awaitable": True,
        "hidden": False,
        "focused": True,
    }


def test_counter_anomaly_activity_gives_passkey_remediation_guidance():
    harness = f"""
const attachSecurityCenter=require({json.dumps(str(SECURITY_CENTER))});
const element=()=>({{
  hidden:true, disabled:false, textContent:'', children:[], className:'',
  addEventListener(){{}}, focus(){{}}, replaceChildren(){{this.children=[];}},
  append(...values){{this.children.push(...values);}},
}});
const ids={{
  'active-devices':element(), 'active-devices-sheet':element(),
  'active-devices-list':element(), 'active-devices-status':element(),
  'enrolled-passkeys-list':element(), 'enrolled-passkeys-status':element(),
  'enroll-passkey':element(), 'security-activity-list':element(),
  'security-activity-status':element(), 'load-more-security-activity':element(),
  'close-active-devices':element(),
}};
const root={{document:{{getElementById:id=>ids[id]||null,createElement:()=>element()}}}};
const boundary={{
  listActiveDevices:async()=>[], listPasskeys:async()=>[],
  listSecurityEvents:async()=>({{events:[{{
    kind:'passkey_counter_anomaly', device_label:'Phone', method:'passkey',
    target:'sign_in:dashboard', status:'completed', created_at:1,
  }}],authentication_alerts:[],next_cursor:null}}),
}};
(async()=>{{
  await attachSecurityCenter({{root,boundary}}).open();
  const row=ids['security-activity-list'].children[0];
  console.log(JSON.stringify(row.children.map(child=>child.textContent)));
}})().catch(error=>{{console.error(error);process.exit(1);}});
"""
    completed = subprocess.run(
        ["node", "-e", harness], check=True, capture_output=True, text=True
    )

    title, detail, action = json.loads(completed.stdout)
    assert title == "Passkey counter anomaly"
    assert "Remove and re-enroll this passkey" in detail
    assert action == "Review passkeys"


def test_activity_alert_hands_off_to_addressable_devices_section():
    harness = f"""
const attachSecurityCenter=require({json.dumps(str(SECURITY_CENTER))});
const listeners={{}};
const element=(id='')=>({{
  id, hidden:true, disabled:false, textContent:'', children:[], className:'', dataset:{{}}, attributes:{{}},
  addEventListener(type,listener){{(this.listeners ||= {{}})[type]=listener;}},
  click(){{this.listeners?.click?.({{preventDefault(){{}}}});}},
  focus(){{}}, replaceChildren(){{this.children=[];}}, append(...values){{this.children.push(...values);}},
  setAttribute(name,value){{this.attributes[name]=String(value);}}, removeAttribute(name){{delete this.attributes[name];}},
  scrollIntoView(){{this.scrolled=true;}},
}});
const names=['active-devices','active-devices-sheet','active-devices-list','active-devices-status',
  'enrolled-passkeys-list','enrolled-passkeys-status','enroll-passkey','security-activity-list',
  'security-activity-status','load-more-security-activity','close-active-devices',
  'security-section-activity','security-section-devices','security-section-passkeys',
  'security-activity-section','security-devices-section','security-passkeys-section'];
const ids=Object.fromEntries(names.map(id=>[id,element(id)]));
const pushed=[];
const root={{
  document:{{getElementById:id=>ids[id]||null,createElement:()=>element()}},
  history:{{state:null,pushState(state,_title,url){{this.state=state;pushed.push({{state,url}});}}}},
  location:{{pathname:'/dashboard/',search:'',hash:''}},
  addEventListener:(type,listener)=>listeners[type]=listener,
}};
const boundary={{
  listActiveDevices:async()=>[], listPasskeys:async()=>[],
  listSecurityEvents:async()=>({{events:[],authentication_alerts:[{{}}],next_cursor:null}}),
  formatAuthenticationAlert:()=>({{title:'Failed sign-ins',detail:'Three attempts'}}),
}};
(async()=>{{
  const controller=attachSecurityCenter({{root,boundary}});
  await controller.open();
  const alert=ids['security-activity-list'].children[0];
  const action=alert.children[2];
  action.click();
  console.log(JSON.stringify({{
    initial:pushed[0], latest:pushed.at(-1), action:action.textContent,
    current:ids['security-section-devices'].attributes['aria-current'],
    scrolled:ids['security-devices-section'].scrolled||false,
  }}));
}})().catch(error=>{{console.error(error);process.exit(1);}});
"""
    completed = subprocess.run(
        ["node", "-e", harness], check=True, capture_output=True, text=True
    )

    output = json.loads(completed.stdout)
    assert output["initial"]["state"]["stackchainSecuritySection"] == "activity"
    assert output["initial"]["url"] == "/dashboard/#security/activity"
    assert output["latest"]["state"]["stackchainSecuritySection"] == "devices"
    assert output["action"] == "Review devices"
    assert output["current"] == "page"
    assert output["scrolled"] is True


def test_device_revocation_refreshes_inventory_and_activity_for_verification():
    harness = f"""
const attachSecurityCenter=require({json.dumps(str(SECURITY_CENTER))});
const element=()=>({{
  hidden:true, disabled:false, textContent:'', children:[], className:'', attributes:{{}},
  addEventListener(type,listener){{(this.listeners ||= {{}})[type]=listener;}},
  click(){{return this.listeners?.click?.();}}, focus(){{}}, replaceChildren(){{this.children=[];}},
  append(...values){{this.children.push(...values);}}, setAttribute(name,value){{this.attributes[name]=value;}},
  removeAttribute(name){{delete this.attributes[name];}}, scrollIntoView(){{}},
}});
const names=['active-devices','active-devices-sheet','active-devices-list','active-devices-status',
  'enrolled-passkeys-list','enrolled-passkeys-status','enroll-passkey','security-activity-list',
  'security-activity-status','load-more-security-activity','close-active-devices'];
const ids=Object.fromEntries(names.map(id=>[id,element()]));
const calls={{devices:0,activity:0,revoke:0}};
const root={{document:{{getElementById:id=>ids[id]||null,createElement:()=>element()}}}};
const boundary={{
  listActiveDevices:async()=>{{calls.devices++;return [{{device_label:'Lost phone',created_at:1,expires_at:2,current:false}}];}},
  listPasskeys:async()=>[],
  listSecurityEvents:async()=>{{calls.activity++;return {{events:[],authentication_alerts:[],next_cursor:null}};}},
  revokeActiveDevice:async()=>{{calls.revoke++;return true;}},
}};
(async()=>{{
  await attachSecurityCenter({{root,boundary}}).open();
  await ids['active-devices-list'].children[0].children[1].click();
  console.log(JSON.stringify({{calls,status:ids['security-activity-status'].textContent}}));
}})().catch(error=>{{console.error(error);process.exit(1);}});
"""
    completed = subprocess.run(
        ["node", "-e", harness], check=True, capture_output=True, text=True
    )

    output = json.loads(completed.stdout)
    assert output["calls"] == {"devices": 2, "activity": 2, "revoke": 1}
    assert output["status"] == "No security activity yet."


def test_back_unwinds_security_sections_then_closes_and_restores_focus():
    harness = f"""
const attachSecurityCenter=require({json.dumps(str(SECURITY_CENTER))});
const listeners={{}};
const element=()=>({{
  hidden:true, textContent:'', children:[], attributes:{{}},
  addEventListener(type,listener){{(this.listeners ||= {{}})[type]=listener;}}, focus(){{this.focused=true;}},
  replaceChildren(){{this.children=[];}}, append(...values){{this.children.push(...values);}},
  setAttribute(name,value){{this.attributes[name]=value;}}, removeAttribute(name){{delete this.attributes[name];}}, scrollIntoView(){{}},
}});
const names=['active-devices','active-devices-sheet','active-devices-list','active-devices-status',
  'enrolled-passkeys-list','enrolled-passkeys-status','enroll-passkey','security-activity-list',
  'security-activity-status','load-more-security-activity','close-active-devices',
  'security-section-activity','security-section-devices','security-section-passkeys',
  'security-activity-section','security-devices-section','security-passkeys-section'];
const ids=Object.fromEntries(names.map(id=>[id,element()]));
const pushed=[];
const root={{
  document:{{getElementById:id=>ids[id]||null,createElement:()=>element()}},
  history:{{state:null,pushState(state,_title,url){{this.state=state;pushed.push(url);}}}},
  location:{{pathname:'/dashboard/',search:''}},
  addEventListener:(type,listener)=>listeners[type]=listener,
}};
const boundary={{
  listActiveDevices:async()=>[],listPasskeys:async()=>[],
  listSecurityEvents:async()=>({{events:[],authentication_alerts:[],next_cursor:null}}),
}};
(async()=>{{
  const controller=attachSecurityCenter({{root,boundary}});
  await controller.open();
  controller.navigate('devices');
  listeners.popstate({{state:{{stackchainSecuritySection:'activity'}}}});
  const afterFirst={{hidden:ids['active-devices-sheet'].hidden,current:ids['security-section-activity'].attributes['aria-current']}};
  listeners.popstate({{state:null}});
  console.log(JSON.stringify({{afterFirst,hidden:ids['active-devices-sheet'].hidden,focused:ids['active-devices'].focused||false,pushed}}));
}})().catch(error=>{{console.error(error);process.exit(1);}});
"""
    completed = subprocess.run(
        ["node", "-e", harness], check=True, capture_output=True, text=True
    )

    output = json.loads(completed.stdout)
    assert output["afterFirst"] == {"hidden": False, "current": "page"}
    assert output["hidden"] is True
    assert output["focused"] is True
    assert output["pushed"] == ["/dashboard/#security/activity", "/dashboard/#security/devices"]


def test_security_center_finishes_its_today_detour_only_when_the_sheet_closes():
    harness = f"""
const attachSecurityCenter=require({json.dumps(str(SECURITY_CENTER))});
const listeners={{}};
const element=()=>({{
  hidden:true,textContent:'',children:[],attributes:{{}},
  addEventListener(){{}},focus(){{}},replaceChildren(){{this.children=[];}},append(...values){{this.children.push(...values);}},
  setAttribute(name,value){{this.attributes[name]=value;}},removeAttribute(name){{delete this.attributes[name];}},scrollIntoView(){{}},
}});
const names=['active-devices','active-devices-sheet','active-devices-list','active-devices-status',
  'enrolled-passkeys-list','enrolled-passkeys-status','enroll-passkey','security-activity-list',
  'security-activity-status','load-more-security-activity','close-active-devices',
  'security-section-activity','security-section-devices','security-section-passkeys',
  'security-activity-section','security-devices-section','security-passkeys-section'];
const ids=Object.fromEntries(names.map(id=>[id,element()]));
const root={{
  document:{{getElementById:id=>ids[id]||null,createElement:()=>element()}},
  history:{{state:null,pushState(state){{this.state=state;}}}},
  location:{{pathname:'/dashboard/',search:''}},
  addEventListener:(type,listener)=>listeners[type]=listener,
}};
const boundary={{listActiveDevices:async()=>[],listPasskeys:async()=>[],listSecurityEvents:async()=>({{events:[],authentication_alerts:[],next_cursor:null}})}};
let closes=0;
(async()=>{{
  const controller=attachSecurityCenter({{root,boundary,onClose:()=>closes++}});
  await controller.open();
  listeners.popstate({{state:{{stackchainSecuritySection:'devices'}}}});
  const afterSectionBack=closes;
  listeners.popstate({{state:null}});
  listeners.popstate({{state:null}});
  console.log(JSON.stringify({{afterSectionBack,closes,hidden:ids['active-devices-sheet'].hidden}}));
}})().catch(error=>{{console.error(error);process.exit(1);}});
"""
    completed = subprocess.run(["node", "-e", harness], capture_output=True, text=True)

    assert completed.returncode == 0, completed.stderr
    assert json.loads(completed.stdout) == {
        "afterSectionBack": 0,
        "closes": 1,
        "hidden": True,
    }


def test_close_uses_history_back_to_remove_security_center_route():
    harness = f"""
const attachSecurityCenter=require({json.dumps(str(SECURITY_CENTER))});
const element=()=>({{
  hidden:true,textContent:'',children:[],attributes:{{}},
  addEventListener(type,listener){{(this.listeners ||= {{}})[type]=listener;}},
  click(){{return this.listeners?.click?.({{preventDefault(){{}}}});}},focus(){{}},replaceChildren(){{this.children=[];}},
  append(...values){{this.children.push(...values);}},setAttribute(){{}},removeAttribute(){{}},scrollIntoView(){{}},
}});
const names=['active-devices','active-devices-sheet','active-devices-list','active-devices-status',
  'enrolled-passkeys-list','enrolled-passkeys-status','enroll-passkey','security-activity-list',
  'security-activity-status','load-more-security-activity','close-active-devices'];
const ids=Object.fromEntries(names.map(id=>[id,element()]));
let backs=0;
const root={{
  document:{{getElementById:id=>ids[id]||null,createElement:()=>element()}},
  history:{{state:null,pushState(state){{this.state=state;}},back(){{backs++;}}}},
  location:{{pathname:'/dashboard/',search:''}},addEventListener(){{}},
}};
const boundary={{listActiveDevices:async()=>[],listPasskeys:async()=>[],listSecurityEvents:async()=>({{events:[],authentication_alerts:[],next_cursor:null}})}};
(async()=>{{
  await attachSecurityCenter({{root,boundary}}).open();
  ids['close-active-devices'].click();
  console.log(JSON.stringify({{backs}}));
}})().catch(error=>{{console.error(error);process.exit(1);}});
"""
    completed = subprocess.run(
        ["node", "-e", harness], check=True, capture_output=True, text=True
    )

    assert json.loads(completed.stdout) == {"backs": 1}


def test_security_center_contains_focus_and_restores_prior_background_state_on_final_back():
    harness = f"""
const attachSecurityCenter=require({json.dumps(str(SECURITY_CENTER))});
const listeners={{}};
const element=(id='', inert=false)=>({{
  id, inert, hidden:false, disabled:false, textContent:'', children:[], attributes:{{}},
  addEventListener(type,listener){{(this.listeners ||= {{}})[type]=listener;}},
  focus(){{focused=this;}}, replaceChildren(){{this.children=[];}},
  append(...values){{this.children.push(...values);}},
  setAttribute(name,value){{this.attributes[name]=value;}}, removeAttribute(name){{delete this.attributes[name];}},
  scrollIntoView(){{}},
}});
let focused=null;
const names=['active-devices','active-devices-sheet','active-devices-list','active-devices-status',
  'enrolled-passkeys-list','enrolled-passkeys-status','enroll-passkey','security-activity-list',
  'security-activity-status','load-more-security-activity','close-active-devices',
  'security-section-activity','security-section-devices','security-section-passkeys',
  'security-activity-section','security-devices-section','security-passkeys-section'];
const ids=Object.fromEntries(names.map(id=>[id,element(id)]));
ids['active-devices-sheet'].hidden=true;
const header=element('header');
const main=element('main', true);
const dock=element('mobile-task-dock');
const dynamicAction=element('revoke-lost-phone');
const hiddenAction=element('hidden-action'); hiddenAction.hidden=true;
const disabledAction=element('disabled-action'); disabledAction.disabled=true;
ids['active-devices-sheet'].querySelectorAll=()=>[
  ids['close-active-devices'], dynamicAction, hiddenAction, disabledAction,
];
const documentListeners={{}};
const root={{
  document:{{
    getElementById:id=>ids[id]||null, createElement:()=>element(),
    querySelector:selector=>({{'header':header,'main':main,'#mobile-task-dock':dock}})[selector]||null,
    addEventListener:(type,listener)=>documentListeners[type]=listener,
  }},
  history:{{state:null,pushState(state){{this.state=state;}}}},
  location:{{pathname:'/dashboard/',search:''}},
  addEventListener:(type,listener)=>listeners[type]=listener,
}};
const boundary={{listActiveDevices:async()=>[],listPasskeys:async()=>[],listSecurityEvents:async()=>({{events:[],authentication_alerts:[],next_cursor:null}})}};
(async()=>{{
  const controller=attachSecurityCenter({{root,boundary}});
  await controller.open();
  const opened={{inert:[header.inert,main.inert,dock.inert],focused:focused?.id}};
  let shiftPrevented=false;
  documentListeners.keydown({{key:'Tab',shiftKey:true,target:ids['close-active-devices'],preventDefault(){{shiftPrevented=true;}}}});
  const afterShift=focused?.id;
  let tabPrevented=false;
  documentListeners.keydown({{key:'Tab',shiftKey:false,target:dynamicAction,preventDefault(){{tabPrevented=true;}}}});
  const afterTab=focused?.id;
  controller.navigate('devices');
  header.inert=false; // An earlier popstate listener released its own overlay background.
  listeners.popstate({{state:{{stackchainSecuritySection:'activity'}}}});
  header.inert=false; // That listener also reacts to the resulting hashchange.
  root.history.state={{stackchainSecuritySection:'activity'}};
  listeners.hashchange({{}});
  const sectionBack={{hidden:ids['active-devices-sheet'].hidden,inert:[header.inert,main.inert,dock.inert]}};
  root.history.state=null;
  listeners.popstate({{state:null}});
  console.log(JSON.stringify({{
    opened,shiftPrevented,afterShift,tabPrevented,afterTab,sectionBack,
    closed:{{hidden:ids['active-devices-sheet'].hidden,inert:[header.inert,main.inert,dock.inert],focused:focused?.id}},
  }}));
}})().catch(error=>{{console.error(error);process.exit(1);}});
"""
    completed = subprocess.run(
        ["node", "-e", harness], capture_output=True, text=True
    )

    assert completed.returncode == 0, completed.stderr
    assert json.loads(completed.stdout) == {
        "opened": {"inert": [True, True, True], "focused": "close-active-devices"},
        "shiftPrevented": True,
        "afterShift": "revoke-lost-phone",
        "tabPrevented": True,
        "afterTab": "close-active-devices",
        "sectionBack": {"hidden": False, "inert": [True, True, True]},
        "closed": {
            "hidden": True,
            "inert": [False, True, False],
            "focused": "active-devices",
        },
    }


def test_escape_closes_security_center_through_history_and_releases_background():
    harness = f"""
const attachSecurityCenter=require({json.dumps(str(SECURITY_CENTER))});
const windowListeners={{}};
const documentListeners={{}};
const element=(id='')=>({{
  id,inert:false,hidden:false,disabled:false,textContent:'',children:[],attributes:{{}},
  addEventListener(){{}},focus(){{focused=this.id;}},replaceChildren(){{this.children=[];}},
  append(...values){{this.children.push(...values);}},setAttribute(){{}},removeAttribute(){{}},scrollIntoView(){{}},
}});
let focused='';
const names=['active-devices','active-devices-sheet','active-devices-list','active-devices-status',
  'enrolled-passkeys-list','enrolled-passkeys-status','enroll-passkey','security-activity-list',
  'security-activity-status','load-more-security-activity','close-active-devices'];
const ids=Object.fromEntries(names.map(id=>[id,element(id)]));
ids['active-devices-sheet'].hidden=true;
ids['active-devices-sheet'].querySelectorAll=()=>[ids['close-active-devices']];
const header=element('header');
let backs=0;
const root={{
  document:{{
    getElementById:id=>ids[id]||null,createElement:()=>element(),
    querySelector:selector=>selector==='header'?header:null,
    addEventListener:(type,listener)=>documentListeners[type]=listener,
  }},
  history:{{state:null,pushState(state){{this.state=state;}},back(){{backs++;}}}},
  location:{{pathname:'/dashboard/',search:''}},
  addEventListener:(type,listener)=>windowListeners[type]=listener,
}};
const boundary={{listActiveDevices:async()=>[],listPasskeys:async()=>[],listSecurityEvents:async()=>({{events:[],authentication_alerts:[],next_cursor:null}})}};
(async()=>{{
  await attachSecurityCenter({{root,boundary}}).open();
  let prevented=false;
  documentListeners.keydown({{key:'Escape',preventDefault(){{prevented=true;}}}});
  const requested={{backs,prevented,hidden:ids['active-devices-sheet'].hidden,inert:header.inert}};
  windowListeners.popstate({{state:null}});
  console.log(JSON.stringify({{requested,closed:{{hidden:ids['active-devices-sheet'].hidden,inert:header.inert,focused}}}}));
}})().catch(error=>{{console.error(error);process.exit(1);}});
"""
    completed = subprocess.run(
        ["node", "-e", harness], capture_output=True, text=True
    )

    assert completed.returncode == 0, completed.stderr
    assert json.loads(completed.stdout) == {
        "requested": {"backs": 1, "prevented": True, "hidden": False, "inert": True},
        "closed": {"hidden": True, "inert": False, "focused": "active-devices"},
    }
