import json
import subprocess
from pathlib import Path


ROOT = Path(__file__).parents[1]
TIMER = ROOT / "frontend" / "today-timer.js"
BREAK = ROOT / "frontend" / "today-break.js"


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


def test_timed_break_pauses_immediately_and_restores_absolute_deadline():
    script = TIMER.read_text() + r"""
const values = new Map();
const storage = {getItem:key=>values.get(key)||null,setItem:(key,value)=>values.set(key,value)};
let now = 100000;
const timer = createTodayTimer({storage,getLogin:()=> 'timmy',now:()=>now});
timer.activate('issue:r:42:');
now = 112000;
const started = timer.startBreak(15);
const paused = timer.snapshot();
now = 200000;
const restored = createTodayTimer({storage,getLogin:()=> 'timmy',now:()=>now}).breakSnapshot();
process.stdout.write(JSON.stringify({started,paused,restored}));
"""

    assert run_node(script) == {
        "started": {
            "identity": "issue:r:42:",
            "deadline_at": 1012000,
            "expired": False,
        },
        "paused": {
            "identity": "issue:r:42:", "elapsed_ms": 12000, "running": False,
            "break_deadline_at": 1012000,
        },
        "restored": {
            "identity": "issue:r:42:",
            "deadline_at": 1012000,
            "expired": False,
        },
    }


def test_timed_break_snapshot_carries_deadline_for_cross_device_sync():
    script = TIMER.read_text() + r"""
const values = new Map();
const storage = {getItem:key=>values.get(key)||null,setItem:(key,value)=>values.set(key,value)};
const timer = createTodayTimer({storage,getLogin:()=> 'timmy',now:()=>100000});
timer.activate('issue:r:42:');
timer.startBreak(15);
process.stdout.write(JSON.stringify(timer.snapshot()));
"""

    assert run_node(script) == {
        "identity": "issue:r:42:", "elapsed_ms": 0, "running": False,
        "break_deadline_at": 1_000_000,
    }


def test_reactivating_the_active_item_during_reload_preserves_its_timed_break():
    script = TIMER.read_text() + r"""
const values = new Map();
const storage = {getItem:key=>values.get(key)||null,setItem:(key,value)=>values.set(key,value)};
let now = 100000;
const timer = createTodayTimer({storage,getLogin:()=> 'timmy',now:()=>now});
timer.activate('issue:r:42:');
timer.startBreak(5);
const reopened = timer.activate('issue:r:42:');
process.stdout.write(JSON.stringify({reopened,snapshot:timer.snapshot(),pending:timer.breakSnapshot()}));
"""

    assert run_node(script) == {
        "reopened": True,
        "snapshot": {
            "identity": "issue:r:42:",
            "elapsed_ms": 0,
            "running": False,
            "break_deadline_at": 400_000,
        },
        "pending": {
            "identity": "issue:r:42:",
            "deadline_at": 400_000,
            "expired": False,
        },
    }


def test_break_expiry_never_restarts_time_and_resume_is_explicit_and_idempotent():
    script = TIMER.read_text() + r"""
const values = new Map();
const storage = {getItem:key=>values.get(key)||null,setItem:(key,value)=>values.set(key,value)};
let now = 0;
const timer = createTodayTimer({storage,getLogin:()=> 'timmy',now:()=>now});
timer.activate('issue:r:42:');
const invalid = [timer.startBreak(0), timer.startBreak(121), timer.startBreak(1.5)];
const started = timer.startBreak(5);
now = 300001;
const expired = timer.breakSnapshot();
const stillPaused = timer.snapshot();
const resumed = timer.resumeBreak();
now = 305001;
const running = timer.snapshot();
const replay = timer.resumeBreak();
process.stdout.write(JSON.stringify({invalid,started,expired,stillPaused,resumed,running,replay,remaining:timer.breakSnapshot()}));
"""

    result = run_node(script)
    assert result["invalid"] == [False, False, False]
    assert result["started"]["deadline_at"] == 300000
    assert result["expired"]["expired"] is True
    assert result["stillPaused"]["running"] is False
    assert result["resumed"] is True
    assert result["running"] == {
        "identity": "issue:r:42:", "elapsed_ms": 5000, "running": True
    }
    assert result["replay"] is False
    assert result["remaining"] is None


def test_mobile_break_sheet_starts_counts_down_and_requires_explicit_resume():
    script = f"""
const createBreak = require({json.dumps(str(BREAK))});
class Element {{
  constructor() {{ this.listeners={{}}; this.hidden=false; this.textContent=''; this.value=''; this.open=false; this.dataset={{}}; }}
  addEventListener(name, fn) {{ this.listeners[name]=fn; }}
  click() {{ return this.listeners.click?.({{preventDefault(){{}}}}); }}
  showModal() {{ this.open=true; }}
  close() {{ this.open=false; this.listeners.close?.(); }}
  focus() {{}}
}}
let now=100000;
let pending=null;
const calls=[];
const timer={{
  snapshot:()=>({{identity:'issue:r:42:',elapsed_ms:5000,running:!pending}}),
  breakSnapshot:()=>pending && {{...pending,expired:now>=pending.deadline_at}},
  startBreak:minutes=>{{calls.push(['start',minutes]);pending={{identity:'issue:r:42:',deadline_at:now+minutes*60000}};return {{...pending,expired:false}};}},
  resumeBreak:()=>{{if(!pending)return false;calls.push(['resume']);pending=null;return true;}},
}};
const sheet=new Element(), opener=new Element(), cancel=new Element(), preset=new Element(), status=new Element(), resume=new Element();
preset.dataset.todayBreakMinutes='15';
let tick;
const controller=createBreak({{timer,sheet,openButtons:[opener],cancelButton:cancel,presetButtons:[preset],status,resumeButton:resume,now:()=>now,setIntervalRef:fn=>(tick=fn,1),clearIntervalRef:()=>{{}},onChange:()=>calls.push(['change']),onResume:identity=>calls.push(['return',identity])}});
opener.click();
const opened=sheet.open;
cancel.click();
const cancelled={{open:sheet.open,calls:[...calls]}};
opener.click(); preset.click();
const started={{open:sheet.open,text:status.textContent,statusHidden:status.hidden,resumeHidden:resume.hidden,openerHidden:opener.hidden}};
now=100000+15*60000+1; tick();
const expired={{text:status.textContent,resumeHidden:resume.hidden,running:timer.snapshot().running}};
resume.click();
process.stdout.write(JSON.stringify({{opened,cancelled,started,expired,final:{{calls,statusHidden:status.hidden,openerHidden:opener.hidden,pending}}}}));
"""

    assert run_node(script) == {
        "opened": True,
        "cancelled": {"open": False, "calls": []},
        "started": {
            "open": False,
            "text": "On break · resume in 15:00",
            "statusHidden": False,
            "resumeHidden": False,
            "openerHidden": True,
        },
        "expired": {
            "text": "Break over · ready to resume",
            "resumeHidden": False,
            "running": False,
        },
        "final": {
            "calls": [["start", 15], ["change"], ["resume"], ["return", "issue:r:42:"], ["change"]],
            "statusHidden": True,
            "openerHidden": False,
            "pending": None,
        },
    }


def test_confirming_the_account_renders_a_persisted_break_immediately():
    dashboard = (ROOT / "frontend" / "dashboard.js").read_text()
    confirmation = dashboard.split("if (activeFlushLogin) {", 1)[1].split("}", 1)[0]

    assert "confirmedOwnerLogin = activeFlushLogin;" in confirmation
    assert "timerView.render();" in confirmation


def test_timed_break_is_packaged_as_an_accessible_mobile_flow():
    html = (ROOT / "frontend" / "index.html").read_text()
    css = (ROOT / "frontend" / "dashboard.css").read_text()
    dashboard = (ROOT / "frontend" / "dashboard.js").read_text()
    bundle = (ROOT / "src" / "frontend_bundle.py").read_text()

    assert 'data-today-break-open' in html
    assert 'id="today-break-sheet"' in html
    assert 'data-today-break-minutes="5"' in html
    assert 'data-today-break-minutes="15"' in html
    assert 'data-today-break-minutes="30"' in html
    assert 'id="today-break-custom-minutes"' in html
    assert 'min="1" max="120" step="1"' in html
    assert 'id="today-break-status"' in html
    assert 'id="resume-today-break"' in html
    assert '<script src="static/today-break.js"></script>' in html
    assert "createTodayBreak({" in (ROOT / "frontend" / "today-timer.js").read_text()
    assert '"static/today-break.js"' in bundle
    assert ".today-break-panel" in css
    assert "overflow-x:hidden" in css
    assert ".today-break-actions button" in css
    assert "min-height:44px" in css


def test_custom_break_validation_is_side_effect_free_until_valid():
    script = f"""
const createBreak = require({json.dumps(str(BREAK))});
class Element {{
  constructor() {{this.listeners={{}};this.open=true;this.hidden=false;this.value='';this.textContent='';}}
  addEventListener(name,fn){{this.listeners[name]=fn;}}
  close(){{this.open=false;}}
  focus(){{this.focused=true;}}
}}
const form=new Element(), input=new Element(), error=new Element(), status=new Element(), resume=new Element(), sheet=new Element();
const calls=[];
const timer={{snapshot:()=>({{identity:'issue:r:1:'}}),breakSnapshot:()=>null,startBreak:value=>{{calls.push(value);return value===45;}},resumeBreak:()=>false}};
createBreak({{timer,sheet,form,minutesInput:input,error,status,resumeButton:resume,setIntervalRef:()=>1,clearIntervalRef:()=>{{}}}});
input.value='2.5'; form.listeners.submit({{preventDefault(){{}}}});
const invalid={{open:sheet.open,error:error.textContent,focused:input.focused||false}};
input.value='45'; form.listeners.submit({{preventDefault(){{}}}});
process.stdout.write(JSON.stringify({{invalid,valid:{{open:sheet.open,error:error.textContent}},calls}}));
"""
    assert run_node(script) == {
        "invalid": {
            "open": True,
            "error": "Choose a whole number from 1 to 120 minutes.",
            "focused": True,
        },
        "valid": {"open": False, "error": ""},
        "calls": [2.5, 45],
    }


def test_replacing_or_stopping_active_work_clears_an_old_break():
    script = TIMER.read_text() + r"""
const values=new Map();
const storage={getItem:key=>values.get(key)||null,setItem:(key,value)=>values.set(key,value)};
let now=0;
const timer=createTodayTimer({storage,getLogin:()=> 'timmy',now:()=>now});
timer.activate('issue:r:1:'); timer.startBreak(5); timer.activate('issue:r:2:');
const replaced=timer.breakSnapshot();
timer.startBreak(5); timer.stop();
const stopped=timer.breakSnapshot();
timer.activate('issue:r:3:'); timer.startBreak(5); timer.adopt('issue:r:4:',1000,false);
process.stdout.write(JSON.stringify({replaced,stopped,adopted:timer.breakSnapshot()}));
"""
    assert run_node(script) == {"replaced": None, "stopped": None, "adopted": None}


def test_browser_back_closes_break_sheet_without_starting_a_break():
    script = f"""
const createBreak=require({json.dumps(str(BREAK))});
class Element{{constructor(){{this.listeners={{}};this.open=false;this.hidden=true;}}addEventListener(n,f){{this.listeners[n]=f;}}showModal(){{this.open=true;}}close(){{this.open=false;}}}}
const sheet=new Element(), opener=new Element(), status=new Element(), resume=new Element();
const listeners={{}}, pushes=[];
const historyRef={{state:{{route:'today'}},pushState:state=>{{historyRef.state=state;pushes.push(state);}},back:()=>listeners.popstate?.()}};
const windowRef={{addEventListener:(name,fn)=>listeners[name]=fn}};
const timer={{snapshot:()=>({{identity:'issue:r:1:'}}),breakSnapshot:()=>null,resumeBreak:()=>false}};
createBreak({{timer,sheet,openButtons:[opener],status,resumeButton:resume,historyRef,windowRef,queryAll:()=>[]}});
opener.listeners.click();
const opened={{open:sheet.open,state:historyRef.state,pushes}};
listeners.popstate();
process.stdout.write(JSON.stringify({{opened,closed:!sheet.open}}));
"""
    assert run_node(script) == {
        "opened": {
            "open": True,
            "state": {"route": "today", "todayBreak": True},
            "pushes": [{"route": "today", "todayBreak": True}],
        },
        "closed": True,
    }
