import json
import subprocess
from pathlib import Path

import pytest

from tests.dashboard_bundle import dashboard


CONTROLLER = Path(__file__).resolve().parents[1] / "frontend" / "mobile-insights.js"


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


def test_mobile_insights_opens_as_addressable_modal_and_hides_home_chrome():
    script = f"""
const createInsights = require({json.dumps(str(CONTROLLER))});
const listeners = {{}};
const element = () => ({{
  hidden:false, inert:false, attributes:{{}}, focuses:0, listeners:{{}},
  setAttribute(name,value) {{ this.attributes[name] = String(value); }},
  removeAttribute(name) {{ delete this.attributes[name]; }},
  addEventListener(name,fn) {{ this.listeners[name] = fn; }},
  focus() {{ this.focuses += 1; }},
}});
const root = element(); const launcher = element(); const closeButton = element();
const dock = element(); const hud = element(); const home = element(); const header = element();
const menu = {{open:true}};
const location = {{pathname:'/dashboard/', search:'', hash:'#/my-work/today'}};
const history = {{state:null, pushes:[], pushState(state,unused,url) {{
  this.state=state; this.pushes.push(url); location.hash=url.slice(url.indexOf('#'));
}}, replaceState() {{}}, back() {{}}}};
const controller = createInsights({{
  root, launcher, closeButton, dock, hud, backgrounds:[header,home], menu, history, location,
  eventTarget:{{addEventListener(name,fn) {{ listeners[name]=fn; }}}},
  mediaQuery:{{matches:true, addEventListener() {{}}}},
}});
controller.start();
launcher.listeners.click();
process.stdout.write(JSON.stringify({{
  hash:location.hash, pushes:history.pushes, open:root.attributes['data-mobile-open'],
  role:root.attributes.role, modal:root.attributes['aria-modal'], rootInert:root.inert,
  homeInert:home.inert, headerInert:header.inert, dockHidden:dock.hidden,
  hudHidden:hud.attributes['data-overlay-hidden'], closeFocuses:closeButton.focuses,
  menuOpen:menu.open,
}}));
"""

    assert run_node(script) == {
        "hash": "#/insights",
        "pushes": ["/dashboard/#/insights"],
        "open": "true",
        "role": "dialog",
        "modal": "true",
        "rootInert": False,
        "homeInert": True,
        "headerInert": True,
        "dockHidden": True,
        "hudHidden": "true",
        "closeFocuses": 1,
        "menuOpen": False,
    }


def test_direct_insights_route_closes_locally_and_restores_launcher_focus():
    script = f"""
const createInsights = require({json.dumps(str(CONTROLLER))});
const listeners = {{}};
const element = () => ({{
  hidden:false, inert:false, attributes:{{}}, focuses:0, listeners:{{}},
  setAttribute(name,value) {{ this.attributes[name] = String(value); }},
  removeAttribute(name) {{ delete this.attributes[name]; }},
  addEventListener(name,fn) {{ this.listeners[name] = fn; }},
  focus() {{ this.focuses += 1; }},
}});
const root=element(), launcher=element(), closeButton=element(), dock=element(), hud=element(), home=element();
const location={{pathname:'/dashboard/', search:'', hash:'#/insights'}};
const history={{state:null, backs:0, replacements:[], pushState() {{}}, back() {{ this.backs += 1; }},
  replaceState(state,unused,url) {{ this.state=state; this.replacements.push(url); location.hash=url.slice(url.indexOf('#')); }}
}};
const controller=createInsights({{
  root,launcher,closeButton,dock,hud,backgrounds:[home],history,location,
  eventTarget:{{addEventListener(name,fn) {{ listeners[name]=fn; }}}},
  mediaQuery:{{matches:true,addEventListener() {{}}}},
}});
controller.start();
closeButton.listeners.click();
process.stdout.write(JSON.stringify({{
  current:controller.current(), hash:location.hash, backs:history.backs,
  replacements:history.replacements, rootInert:root.inert, homeInert:home.inert,
  dockHidden:dock.hidden, hudHidden:hud.attributes['data-overlay-hidden'] || null,
  launcherFocuses:launcher.focuses,
}}));
"""

    assert run_node(script) == {
        "current": False,
        "hash": "#/my-work",
        "backs": 0,
        "replacements": ["/dashboard/#/my-work"],
        "rootInert": True,
        "homeInert": False,
        "dockHidden": False,
        "hudHidden": None,
        "launcherFocuses": 1,
    }


def test_browser_back_closes_insights_without_resetting_the_work_route():
    script = f"""
const createInsights = require({json.dumps(str(CONTROLLER))});
const listeners={{}};
const element=()=>({{hidden:false,inert:false,attributes:{{}},focuses:0,listeners:{{}},setAttribute(n,v){{this.attributes[n]=String(v);}},removeAttribute(n){{delete this.attributes[n];}},addEventListener(n,f){{this.listeners[n]=f;}},focus(){{this.focuses+=1;}}}});
const root=element(),launcher=element(),closeButton=element(),dock=element(),hud=element(),home=element();
const location={{pathname:'/dashboard/',search:'',hash:'#/my-work/update'}};
const history={{state:null,pushState(state,unused,url){{this.state=state;location.hash=url.slice(url.indexOf('#'));}},replaceState(){{}},back(){{}}}};
const controller=createInsights({{root,launcher,closeButton,dock,hud,backgrounds:[home],history,location,eventTarget:{{addEventListener(n,f){{listeners[n]=f;}}}},mediaQuery:{{matches:true,addEventListener(){{}}}}}});
controller.start(); launcher.listeners.click();
location.hash='#/my-work/update'; history.state=null; listeners.popstate?.({{state:null}});
process.stdout.write(JSON.stringify({{current:controller.current(),hash:location.hash,launcherFocuses:launcher.focuses,homeInert:home.inert,dockHidden:dock.hidden}}));
"""

    assert run_node(script) == {
        "current": False,
        "hash": "#/my-work/update",
        "launcherFocuses": 1,
        "homeInert": False,
        "dockHidden": False,
    }


def test_escape_requests_back_navigation_only_while_insights_is_open():
    script = f"""
const createInsights=require({json.dumps(str(CONTROLLER))});
const listeners={{}};
const element=()=>({{hidden:false,inert:false,attributes:{{}},listeners:{{}},setAttribute(n,v){{this.attributes[n]=String(v);}},removeAttribute(n){{delete this.attributes[n];}},addEventListener(n,f){{this.listeners[n]=f;}},focus(){{}}}});
const root=element(),launcher=element(),closeButton=element(),dock=element(),hud=element();
const location={{pathname:'/dashboard/',search:'',hash:'#/my-work'}};
const history={{state:null,backs:0,pushState(state,unused,url){{this.state=state;location.hash=url.slice(url.indexOf('#'));}},replaceState(){{}},back(){{this.backs+=1;}}}};
const controller=createInsights({{root,launcher,closeButton,dock,hud,backgrounds:[],history,location,eventTarget:{{addEventListener(n,f){{listeners[n]=f;}}}},mediaQuery:{{matches:true,addEventListener(){{}}}}}});
controller.start(); launcher.listeners.click();
let prevented=0; listeners.keydown?.({{key:'Escape',preventDefault(){{prevented+=1;}}}}); listeners.keydown?.({{key:'Enter',preventDefault(){{prevented+=1;}}}});
process.stdout.write(JSON.stringify({{backs:history.backs,prevented}}));
"""

    assert run_node(script) == {"backs": 1, "prevented": 1}


def test_insights_route_owns_the_today_detour_for_open_back_and_reload():
    script = f"""
const createInsights=require({json.dumps(str(CONTROLLER))});
const listeners={{}};
const element=()=>({{hidden:false,inert:false,attributes:{{}},listeners:{{}},setAttribute(n,v){{this.attributes[n]=String(v);}},removeAttribute(n){{delete this.attributes[n];}},addEventListener(n,f){{this.listeners[n]=f;}},focus(){{}}}});
const root=element(),launcher=element(),closeButton=element(),returnButton=element(),dock=element(),hud=element();
const location={{pathname:'/dashboard/',search:'',hash:'#/my-work/today'}};
const calls=[];
const detour={{beginDetour(reason){{calls.push('begin:'+reason);return {{identity:'issue:r:42:',reason}};}},finishDetour(){{calls.push('finish');return {{identity:'issue:r:42:'}};}}}};
const history={{state:null,pushState(state,unused,url){{this.state=state;location.hash=url.slice(url.indexOf('#'));}},replaceState(){{}},back(){{}}}};
const controller=createInsights({{root,launcher,closeButton,returnButton,dock,hud,backgrounds:[],history,location,detour:()=>detour,eventTarget:{{addEventListener(n,f){{listeners[n]=f;}}}},mediaQuery:{{matches:true,addEventListener(){{}}}}}});
controller.start();
launcher.listeners.click();
location.hash='#/my-work/today'; history.state=null; listeners.popstate();
listeners.popstate();
location.hash='#/insights'; listeners.hashchange();
process.stdout.write(JSON.stringify({{calls,current:controller.current()}}));
"""

    assert run_node(script) == {
        "calls": ["begin:insights", "finish", "begin:insights"],
        "current": True,
    }


def test_return_to_today_closes_insights_and_finishes_the_detour_once():
    script = f"""
const createInsights=require({json.dumps(str(CONTROLLER))});
const element=()=>({{hidden:false,inert:false,attributes:{{}},listeners:{{}},setAttribute(n,v){{this.attributes[n]=String(v);}},removeAttribute(n){{delete this.attributes[n];}},addEventListener(n,f){{this.listeners[n]=f;}},focus(){{}}}});
const root=element(),launcher=element(),closeButton=element(),returnButton=element(),dock=element(),hud=element();
const location={{pathname:'/dashboard/',search:'',hash:'#/my-work/today'}};
const calls=[];
const detour={{beginDetour(reason){{calls.push('begin:'+reason);}},finishDetour(){{calls.push('finish');}}}};
const history={{state:null,pushState(state,unused,url){{this.state=state;location.hash=url.slice(url.indexOf('#'));}},replaceState(state,unused,url){{this.state=state;location.hash=url.slice(url.indexOf('#'));}},back(){{}}}};
const controller=createInsights({{root,launcher,closeButton,returnButton,dock,hud,backgrounds:[],history,location,detour:()=>detour,eventTarget:{{addEventListener(){{}}}},mediaQuery:{{matches:true,addEventListener(){{}}}}}});
controller.start(); launcher.listeners.click(); returnButton.listeners.click();
process.stdout.write(JSON.stringify({{calls,current:controller.current(),hash:location.hash}}));
"""

    assert run_node(script) == {
        "calls": ["begin:insights", "finish"],
        "current": False,
        "hash": "#/my-work/today",
    }


def test_direct_insights_route_retries_detour_after_identity_restores():
    script = f"""
const createInsights=require({json.dumps(str(CONTROLLER))});
const element=()=>({{hidden:false,inert:false,attributes:{{}},listeners:{{}},setAttribute(n,v){{this.attributes[n]=String(v);}},removeAttribute(n){{delete this.attributes[n];}},addEventListener(n,f){{this.listeners[n]=f;}},focus(){{}}}});
const root=element(),launcher=element(),closeButton=element(),returnButton=element(),dock=element(),hud=element();
const location={{pathname:'/dashboard/',search:'',hash:'#/insights'}};
let attempts=0; let retry;
global.setTimeout=callback=>{{retry=callback;}};
const detour={{beginDetour(){{attempts+=1;return attempts > 1 ? {{identity:'issue:r:42:'}} : null;}},finishDetour(){{}}}};
const history={{state:null,replaceState(){{}},pushState(){{}},back(){{}}}};
const controller=createInsights({{root,launcher,closeButton,returnButton,dock,hud,backgrounds:[],history,location,detour:()=>detour,eventTarget:{{addEventListener(){{}}}},mediaQuery:{{matches:true,addEventListener(){{}}}}}});
controller.start(); retry();
process.stdout.write(JSON.stringify({{attempts,current:controller.current()}}));
"""

    assert run_node(script) == {"attempts": 2, "current": True}


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

    assert 'id="open-insights"' in html
    assert 'id="insights-sheet"' in html
    assert 'id="close-insights"' in html
    insights = html[html.index('id="insights-sheet"'):html.index('</section>', html.index('id="insights-sheet"'))]
    assert 'data-today-detour' in insights
    assert 'data-return-from-detour' in insights
    assert "detour:() => timerView" in html
    assert html.index("const timerView = createTodayTimerView({") < html.index("mobileInsights.start();")
    assert html.index('id="my-work"') < html.index('id="insights-sheet"')
    assert html.index('id="insights-sheet"') < html.index('data-panel-key="context"')
    assert html.index('data-panel-key="markdown"') < html.index('</section>\n</main>')
    assert '<script src="static/mobile-insights.js"></script>' in html
    assert "createMobileInsights({" in html
    assert "mediaQuery: window.matchMedia('(max-width: 600px)')" in html
    assert ".insights-shell { display:contents; }" in html
    assert "#open-insights { display:none; }" in html
    assert '.insights-shell[data-mobile-open="true"]' in html
    assert "position:fixed; inset:0;" in html
    assert "overflow-x:hidden" in html
    assert ".insights-header button { min-height:44px;" in html


def test_mobile_insights_rolls_into_the_offline_shell():
    worker = (CONTROLLER.parent / "service-worker.js").read_text()

    assert "stackchain-dashboard-shell-v127" in worker
    assert "BASE + 'static/mobile-insights.js'" in worker
