import json
import subprocess
from pathlib import Path

import pytest


FRONTEND = Path(__file__).parents[1] / "frontend"
CONTROLLER = FRONTEND / "week-plan.js"
COORDINATOR = FRONTEND / "outbox-coordinator.js"
INDEX = FRONTEND / "index.html"
CSS = FRONTEND / "dashboard.css"


def run_controller(scenario: str) -> dict:
    harness = f"""
const createWeekPlan = require({json.dumps(str(CONTROLLER))});
const createOutboxCoordinator = require({json.dumps(str(COORDINATOR))});
(async()=>{{ {scenario} }})().catch(error=>{{console.error(error);process.exit(1);}});
"""
    completed = subprocess.run(["node", "-e", harness], check=True, capture_output=True, text=True)
    return json.loads(completed.stdout)


def test_weekly_availability_editor_is_mobile_safe_and_wired_to_durable_defaults():
    markup = INDEX.read_text()
    styles = CSS.read_text()
    source = CONTROLLER.read_text()

    assert 'id="open-week-availability"' in markup
    assert 'id="week-availability-editor"' in markup
    assert markup.count('data-weekday-capacity=') == 7
    assert 'id="save-week-availability"' in markup
    assert 'id="disable-week-availability"' in markup
    assert 'aria-labelledby="week-availability-title"' in markup
    assert ".week-availability-grid input { min-height:44px" in styles
    assert "grid-template-columns:1fr" in styles
    assert "stageAvailabilityDefaults(values)" in source
    assert "Weekly availability saved" in source


def test_conflicted_pull_review_has_accessible_touch_safe_recovery_actions():
    markup = INDEX.read_text()
    styles = CSS.read_text()

    assert 'id="week-pull-conflict-review"' in markup
    assert 'aria-labelledby="week-pull-conflict-title"' in markup
    assert 'id="week-pull-conflict-item"' in markup
    assert 'id="week-pull-conflict-context"' in markup
    assert 'id="rebase-week-pull"' in markup
    assert 'id="keep-week-pull"' in markup
    assert "Add using current plans" in markup
    assert "Keep in Week Ahead" in markup
    assert ".week-pull-conflict-actions button { min-height:44px" in styles
    assert "overflow-wrap:anywhere" in styles


def test_week_workflow_wires_both_conflict_decisions_to_the_durable_controller():
    source = CONTROLLER.read_text()

    assert "resolvePullConflict('add')" in source
    assert "resolvePullConflict('keep')" in source
    assert "controller.pullConflict?.()" in source
    assert "renderPullConflict" in source
    assert "#week-pull-conflict-review" in source


def test_week_controller_requires_executable_days_before_calendar_handoff():
    result = run_controller("""
const week=createWeekPlan({fetchJson:async()=>({}),localDate:()=> '2026-08-20',timeZone:()=> 'UTC'});
week.adopt({revision:7,timezone:'UTC',days:[
 {plan_date:'2026-08-21',ids:['over'],capacity_minutes:30,estimates:{over:45}},
 {plan_date:'2026-08-22',ids:['missing-estimate'],capacity_minutes:60,estimates:{}},
 {plan_date:'2026-08-23',ids:['missing-capacity'],capacity_minutes:null,estimates:{'missing-capacity':20}},
]});
console.log(JSON.stringify(week.review()));
""")

    assert result["can_confirm"] is False
    assert result["blockers"] == [
        {"type": "over-capacity", "plan_date": "2026-08-21", "minutes": 15},
        {"type": "missing-estimate", "plan_date": "2026-08-22", "id": "missing-estimate"},
        {"type": "missing-capacity", "plan_date": "2026-08-23"},
    ]


def test_week_controller_lists_seven_local_dates_and_saves_one_without_changing_other_days():
    result = run_controller("""
const requests=[];
const initial={revision:4,timezone:'UTC',days:[
 {plan_date:'2026-08-21',ids:['issue:r:1:'],capacity_minutes:60,estimates:{'issue:r:1:':30}},
 {plan_date:'2026-08-23',ids:['issue:r:3:'],capacity_minutes:90,estimates:{'issue:r:3:':45}}
]};
const fetchJson=async(url,options={})=>{
 requests.push({url,method:options.method||'GET',body:options.body?JSON.parse(options.body):null});
 if(!options.method)return initial;
 return {revision:5,timezone:'UTC',days:JSON.parse(options.body).days};
};
const week=createWeekPlan({fetchJson,localDate:()=> '2026-08-20',timeZone:()=> 'UTC'});
await week.load();
const dates=week.dates();
const saved=await week.saveDay('2026-08-22',{ids:['issue:r:2:'],capacity_minutes:120,estimates:{'issue:r:2:':50}});
console.log(JSON.stringify({dates,saved,requests}));
""")

    assert [item["date"] for item in result["dates"]] == [
        "2026-08-21", "2026-08-22", "2026-08-23", "2026-08-24",
        "2026-08-25", "2026-08-26", "2026-08-27",
    ]
    assert [day["plan_date"] for day in result["saved"]["days"]] == [
        "2026-08-21", "2026-08-22", "2026-08-23"
    ]
    assert result["requests"][1]["body"]["base_revision"] == 4
    assert result["requests"][1]["body"]["days"][0]["ids"] == ["issue:r:1:"]
    assert result["requests"][1]["body"]["days"][2]["ids"] == ["issue:r:3:"]


def test_week_controller_starts_a_selected_future_day_early_and_refreshes_the_week():
    result = run_controller("""
const requests=[];
let loadCount=0;
const day={plan_date:'2026-08-21',ids:['issue:r:1:'],capacity_minutes:60,estimates:{'issue:r:1:':30}};
const fetchJson=async(url,options={})=>{
 requests.push({url,method:options.method||'GET',body:options.body?JSON.parse(options.body):null});
 if(url==='api/v1/week/start-early')return {revision:3,...day,timezone:'UTC'};
 loadCount+=1;
 return loadCount===1?{revision:4,timezone:'UTC',days:[day]}:{revision:5,timezone:'UTC',days:[]};
};
const week=createWeekPlan({fetchJson,localDate:()=> '2026-08-20',timeZone:()=> 'UTC'});
await week.load();
const promoted=await week.startEarly('2026-08-21',2);
console.log(JSON.stringify({promoted,requests,state:week.state()}));
""")

    assert result["promoted"]["ids"] == ["issue:r:1:"]
    assert result["requests"][1] == {
        "url": "api/v1/week/start-early",
        "method": "POST",
        "body": {
            "promotion_id": "early-2026-08-21-r4",
            "week_revision": 4,
            "plan_date": "2026-08-21",
            "today_revision": 2,
        },
    }
    assert result["state"]["days"] == []


def test_week_controller_advances_through_one_continuous_seven_day_pass():
    result = run_controller("""
const week=createWeekPlan({fetchJson:async()=>({}),localDate:()=> '2026-08-20',timeZone:()=> 'UTC'});
week.adopt({revision:4,timezone:'UTC',days:[
 {plan_date:'2026-08-21',ids:['planned'],capacity_minutes:60,estimates:{planned:30}},
 {plan_date:'2026-08-23',ids:['also-planned'],capacity_minutes:90,estimates:{'also-planned':45}}
]});
const first=week.pass('2026-08-21');
const middle=week.pass('2026-08-24');
const last=week.pass('2026-08-27');
console.log(JSON.stringify({first,middle,last}));
""")

    assert result == {
        "first": {"position": 1, "total": 7, "planned": 2, "next_date": "2026-08-22", "last": False},
        "middle": {"position": 4, "total": 7, "planned": 2, "next_date": "2026-08-25", "last": False},
        "last": {"position": 7, "total": 7, "planned": 2, "next_date": None, "last": True},
    }


def test_week_controller_reviews_capacity_duplicates_and_moves_work_without_copying():
    result = run_controller("""
const values=new Map();
const storage={getItem:key=>values.get(key)||null,setItem:(key,value)=>values.set(key,value),removeItem:key=>values.delete(key)};
const week=createWeekPlan({storage,getLogin:()=> 'timmy',fetchJson:async()=>({}),
  localDate:()=> '2026-08-20',timeZone:()=> 'UTC'});
week.adopt({revision:6,timezone:'UTC',days:[
 {plan_date:'2026-08-21',ids:['shared','one'],capacity_minutes:45,estimates:{shared:30,one:30}},
 {plan_date:'2026-08-22',ids:['shared','two'],capacity_minutes:120,estimates:{shared:30,two:40}}
]});
const before=week.review();
const moved=week.move('shared','2026-08-23');
const after=week.review();
console.log(JSON.stringify({before,moved,after,pending:week.pending()}));
""")

    assert len(result["before"]["days"]) == 7
    assert result["before"]["days"][0]["planned_minutes"] == 60
    assert result["before"]["days"][0]["overloaded"] is True
    assert result["before"]["duplicates"] == [{
        "id": "shared", "dates": ["2026-08-21", "2026-08-22"]
    }]
    assert result["moved"] is True
    assert result["after"]["duplicates"] == []
    assert [day["ids"] for day in result["after"]["days"][:3]] == [["one"], ["two"], ["shared"]]
    assert result["pending"]["days"][2]["estimates"] == {"shared": 30}


def test_week_controller_applies_weekday_defaults_to_new_dates_and_keeps_date_overrides():
    result = run_controller("""
const values=new Map();const requests=[];
const storage={getItem:key=>values.get(key)||null,setItem:(key,value)=>values.set(key,value),removeItem:key=>values.delete(key)};
const fetchJson=async(url,options={})=>{
 requests.push(options.body?JSON.parse(options.body):null);
 if(!options.method)return {revision:3,timezone:'UTC',availability_defaults:[60,120,180,240,300,0,0],days:[
  {plan_date:'2026-08-22',ids:['override'],capacity_minutes:45,estimates:{override:30}}
 ]};
 return {revision:4,...JSON.parse(options.body)};
};
const week=createWeekPlan({storage,getLogin:()=> 'timmy',fetchJson,
 localDate:()=> '2026-08-20',timeZone:()=> 'UTC'});
await week.load();
const inherited=week.day('2026-08-21');
const overridden=week.day('2026-08-22');
const staged=week.stageAvailabilityDefaults([90,120,180,240,300,0,0]);
await week.flush();
console.log(JSON.stringify({inherited,overridden,staged,request:requests.at(-1),state:week.state()}));
""")

    assert result["inherited"]["capacity_minutes"] == 300
    assert result["inherited"]["capacity_source"] == "default"
    assert result["overridden"]["capacity_minutes"] == 45
    assert result["overridden"]["capacity_source"] == "override"
    assert result["staged"]["sync_pending"] is True
    assert result["request"]["availability_defaults"] == [90, 120, 180, 240, 300, 0, 0]
    assert result["state"]["availability_defaults"] == [90, 120, 180, 240, 300, 0, 0]


def test_week_controller_applies_seven_imported_capacities_in_one_local_transition():
    result = run_controller("""
const values=new Map();let writes=0;
const storage={getItem:key=>values.get(key)||null,setItem:(key,value)=>{writes+=1;values.set(key,value);},removeItem:key=>values.delete(key)};
const week=createWeekPlan({storage,getLogin:()=> 'timmy',fetchJson:async()=>({}),
  localDate:()=> '2026-08-20',timeZone:()=> 'UTC'});
week.adopt({revision:6,timezone:'UTC',days:[
 {plan_date:'2026-08-21',ids:['one'],capacity_minutes:60,estimates:{one:30}},
 {plan_date:'2026-08-23',ids:['three'],capacity_minutes:90,estimates:{three:45}}
]});
const capacities=week.dates().map((item,index)=>({plan_date:item.date,capacity_minutes:300+index*15}));
const applied=week.stageCapacities(capacities);
console.log(JSON.stringify({applied,writes,state:week.state(),pending:week.pending()}));
""")

    assert result["writes"] == 1
    assert result["applied"]["sync_pending"] is True
    assert [day["capacity_minutes"] for day in result["state"]["days"]] == [
        300, 315, 330, 345, 360, 375, 390
    ]
    assert result["state"]["days"][0]["ids"] == ["one"]
    assert result["state"]["days"][2]["ids"] == ["three"]
    assert result["pending"]["days"] == result["state"]["days"]


def test_week_controller_keeps_consented_free_windows_through_pending_flush_and_reload():
    result = run_controller("""
const values=new Map();const requests=[];
const storage={getItem:key=>values.get(key)||null,setItem:(key,value)=>values.set(key,value),removeItem:key=>values.delete(key)};
const fetchJson=async(url,options={})=>{
  if(!options.method)return {revision:4,timezone:'UTC',days:[{plan_date:'2026-08-21',ids:['one'],capacity_minutes:420,
    estimates:{one:45},free_windows:[{start_time:'09:00',end_time:'10:00'},{start_time:'11:00',end_time:'17:00'}]}]};
  const body=JSON.parse(options.body);requests.push(body);return {revision:5,timezone:body.timezone,days:body.days};
};
const options={storage,getLogin:()=> 'timmy',fetchJson,localDate:()=> '2026-08-20',timeZone:()=> 'UTC'};
const first=createWeekPlan(options);await first.load();
const capacities=first.dates().map((item,index)=>({plan_date:item.date,capacity_minutes:index?480:420,
  ...(index?{}:{free_windows:[{start_time:'09:00',end_time:'10:00'},{start_time:'11:00',end_time:'17:00'}]})}));
first.stageCapacities(capacities);const pending=first.pending();await first.flush();
const restored=createWeekPlan(options);await restored.load();
console.log(JSON.stringify({pending,request:requests[0],restored:restored.state()}));
""")

    windows = [
        {"start_time": "09:00", "end_time": "10:00"},
        {"start_time": "11:00", "end_time": "17:00"},
    ]
    assert result["pending"]["days"][0]["free_windows"] == windows
    assert result["request"]["days"][0]["free_windows"] == windows
    assert result["restored"]["days"][0]["free_windows"] == windows


def test_week_controller_keeps_exact_task_times_through_pending_flush_and_reload():
    result = run_controller("""
const values=new Map();const requests=[];let remote={revision:4,timezone:'UTC',days:[{plan_date:'2026-08-21',ids:['one','two'],capacity_minutes:420,
  estimates:{one:45,two:90},start_times:{one:'09:00'}}]};
const storage={getItem:key=>values.get(key)||null,setItem:(key,value)=>values.set(key,value),removeItem:key=>values.delete(key)};
const fetchJson=async(url,options={})=>{
  if(!options.method)return remote;
  const body=JSON.parse(options.body);requests.push(body);remote={revision:5,timezone:body.timezone,days:body.days};return remote;
};
const options={storage,getLogin:()=> 'timmy',fetchJson,localDate:()=> '2026-08-20',timeZone:()=> 'UTC'};
const first=createWeekPlan(options);await first.load();
first.stageStartTimes('2026-08-21',{one:'09:00',two:'11:00'});
const pending=first.pending();await first.flush();
const restored=createWeekPlan(options);await restored.load();
console.log(JSON.stringify({pending,request:requests[0],restored:restored.state()}));
""")

    expected = {"one": "09:00", "two": "11:00"}
    assert result["pending"]["days"][0]["start_times"] == expected
    assert result["request"]["days"][0]["start_times"] == expected
    assert result["restored"]["days"][0]["start_times"] == expected


def test_week_controller_previews_and_atomically_applies_a_capacity_safe_reflow():
    result = run_controller("""
const values=new Map();let writes=0;
const storage={getItem:key=>values.get(key)||null,setItem:(key,value)=>{writes+=1;values.set(key,value);},removeItem:key=>values.delete(key)};
const week=createWeekPlan({storage,getLogin:()=> 'timmy',fetchJson:async()=>({}),
  localDate:()=> '2026-08-20',timeZone:()=> 'UTC'});
week.adopt({revision:6,timezone:'UTC',days:[
 {plan_date:'2026-08-21',ids:['one','two'],capacity_minutes:45,estimates:{one:30,two:30}},
 {plan_date:'2026-08-22',ids:['three'],capacity_minutes:60,estimates:{three:20}},
 {plan_date:'2026-08-23',ids:['large'],capacity_minutes:40,estimates:{large:70}},
 {plan_date:'2026-08-24',ids:[],capacity_minutes:60,estimates:{}},
 {plan_date:'2026-08-25',ids:[],capacity_minutes:0,estimates:{}},
 {plan_date:'2026-08-26',ids:[],capacity_minutes:0,estimates:{}},
 {plan_date:'2026-08-27',ids:[],capacity_minutes:0,estimates:{}}
]});
const before=week.state();
const preview=week.previewReflow();
const unchanged=week.state();
const applied=week.applyReflow();
console.log(JSON.stringify({before,preview,unchanged,applied,writes,pending:week.pending()}));
""")

    assert result["preview"]["blockers"] == []
    assert result["preview"]["unscheduled"] == [
        {"id": "large", "estimate": 70, "from_date": "2026-08-23"}
    ]
    assert [day["ids"] for day in result["preview"]["days"]] == [
        ["one"], ["two", "three"], [], [], [], [], []
    ]
    assert result["unchanged"] == result["before"]
    assert result["applied"]["sync_pending"] is True
    assert result["writes"] == 1
    assert result["pending"]["days"] == result["preview"]["days"]


def test_week_controller_atomically_reflows_against_refreshed_calendar_capacity_and_keeps_windows():
    result = run_controller("""
const values=new Map();let writes=0;
const storage={getItem:key=>values.get(key)||null,setItem:(key,value)=>{writes+=1;values.set(key,value);},removeItem:key=>values.delete(key)};
const week=createWeekPlan({storage,getLogin:()=> 'timmy',fetchJson:async()=>({}),
  localDate:()=> '2026-08-20',timeZone:()=> 'UTC'});
week.adopt({revision:6,timezone:'UTC',days:[
 {plan_date:'2026-08-21',ids:['one','two'],capacity_minutes:120,estimates:{one:45,two:45},free_windows:[{start_time:'09:00',end_time:'11:00'}]},
 {plan_date:'2026-08-22',ids:['three'],capacity_minutes:120,estimates:{three:30}},
 {plan_date:'2026-08-23',ids:[],capacity_minutes:120,estimates:{}}
]});
const capacities=week.dates().map((item,index)=>({plan_date:item.date,capacity_minutes:index===0?45:120,
 free_windows:index===0?[{start_time:'10:00',end_time:'10:45'}]:[{start_time:'09:00',end_time:'11:00'}]}));
const before=week.state();const preview=week.previewCapacityReflow(capacities);const unchanged=week.state();
const applied=week.applyCapacityReflow(capacities);
console.log(JSON.stringify({before,preview,unchanged,applied,writes,pending:week.pending()}));
""")

    assert result["unchanged"] == result["before"]
    assert result["writes"] == 1
    assert result["applied"]["sync_pending"] is True
    assert [day["ids"] for day in result["preview"]["days"][:2]] == [["one"], ["two", "three"]]
    assert result["preview"]["days"][0]["free_windows"] == [
        {"start_time": "10:00", "end_time": "10:45"}
    ]
    assert result["pending"]["days"] == result["preview"]["days"]
    assert [item for day in result["pending"]["days"] for item in day["ids"]] == ["one", "two", "three"]


def test_week_controller_reflow_never_places_more_than_five_items_on_a_day():
    result = run_controller("""
const week=createWeekPlan({fetchJson:async()=>({}),localDate:()=> '2026-08-20',timeZone:()=> 'UTC'});
const estimates=Object.fromEntries(Array.from({length:7},(_,index)=>['item-'+index,10]));
week.adopt({revision:6,timezone:'UTC',days:[
 {plan_date:'2026-08-21',ids:Object.keys(estimates),capacity_minutes:120,estimates},
 {plan_date:'2026-08-22',ids:[],capacity_minutes:120,estimates:{}}
]});
console.log(JSON.stringify(week.previewReflow()));
""")

    assert [day["ids"] for day in result["days"][:2]] == [
        ["item-0", "item-1", "item-2", "item-3", "item-4"],
        ["item-5", "item-6"],
    ]
    assert result["unscheduled"] == []


def test_week_controller_reflow_keeps_only_the_first_chronological_assignment():
    result = run_controller("""
const week=createWeekPlan({fetchJson:async()=>({}),localDate:()=> '2026-08-20',timeZone:()=> 'UTC'});
week.adopt({revision:6,timezone:'UTC',days:[
 {plan_date:'2026-08-21',ids:['one','shared'],capacity_minutes:120,estimates:{one:30,shared:20}},
 {plan_date:'2026-08-22',ids:['shared','two'],capacity_minutes:120,estimates:{shared:20,two:30}}
]});
console.log(JSON.stringify(week.previewReflow()));
""")

    assert [item for day in result["days"] for item in day["ids"]] == ["one", "shared", "two"]
    assert result["unscheduled"] == []


def test_week_controller_reflow_never_backfills_later_work_ahead_of_earlier_work():
    result = run_controller("""
const week=createWeekPlan({fetchJson:async()=>({}),localDate:()=> '2026-08-20',timeZone:()=> 'UTC'});
week.adopt({revision:6,timezone:'UTC',days:[
 {plan_date:'2026-08-21',ids:['large','small'],capacity_minutes:30,estimates:{large:50,small:20}},
 {plan_date:'2026-08-22',ids:[],capacity_minutes:60,estimates:{}},
 {plan_date:'2026-08-23',ids:[],capacity_minutes:30,estimates:{}}
]});
console.log(JSON.stringify(week.previewReflow()));
""")

    assert [day["ids"] for day in result["days"][:3]] == [[], ["large"], ["small"]]


def test_week_controller_blocks_reflow_when_work_has_no_estimate():
    result = run_controller("""
const values=new Map();let writes=0;
const storage={getItem:key=>values.get(key)||null,setItem:(key,value)=>{writes+=1;values.set(key,value);},removeItem:key=>values.delete(key)};
const week=createWeekPlan({storage,getLogin:()=> 'timmy',fetchJson:async()=>({}),
  localDate:()=> '2026-08-20',timeZone:()=> 'UTC'});
week.adopt({revision:6,timezone:'UTC',days:[
 {plan_date:'2026-08-21',ids:['known','unknown'],capacity_minutes:90,estimates:{known:30}},
 {plan_date:'2026-08-22',ids:[],capacity_minutes:90,estimates:{}}
]});
const before=week.state();const preview=week.previewReflow();const applied=week.applyReflow();
console.log(JSON.stringify({before,preview,applied,after:week.state(),writes}));
""")

    assert result["preview"]["blockers"] == [{
        "type": "missing-estimate", "id": "unknown", "plan_date": "2026-08-21"
    }]
    assert result["preview"]["days"] == []
    assert result["applied"] is False
    assert result["after"] == result["before"]
    assert result["writes"] == 0


def test_week_controller_places_new_work_or_explicitly_moves_existing_work_without_duplicates():
    result = run_controller("""
const values=new Map();
const storage={getItem:key=>values.get(key)||null,setItem:(key,value)=>values.set(key,value),removeItem:key=>values.delete(key)};
const week=createWeekPlan({storage,getLogin:()=> 'timmy',fetchJson:async()=>({}),
  localDate:()=> '2026-08-20',timeZone:()=> 'UTC'});
week.adopt({revision:6,timezone:'UTC',days:[
 {plan_date:'2026-08-21',ids:['existing'],capacity_minutes:60,estimates:{existing:30}},
 {plan_date:'2026-08-22',ids:['other'],capacity_minutes:120,estimates:{other:45}}
]});
const before=week.placement('existing');
const refused=week.place('existing','2026-08-22',50);
const moved=week.place('existing','2026-08-22',50,{move:true});
const added=week.place('new','2026-08-23',25);
console.log(JSON.stringify({before,refused,moved,added,state:week.state(),pending:week.pending()}));
""")

    assert result["before"] == {"date": "2026-08-21", "estimate": 30}
    assert result["refused"] is False
    assert result["moved"] is True
    assert result["added"] is True
    assert [day["ids"] for day in result["state"]["days"]] == [[], ["other", "existing"], ["new"]]
    assert result["state"]["days"][1]["estimates"]["existing"] == 50
    assert result["pending"]["days"] == result["state"]["days"]


def test_week_controller_pulls_one_item_into_today_and_adopts_returned_week():
    result = run_controller("""
const requests=[];
const week=createWeekPlan({fetchJson:async(url,options={})=>{
 const body=JSON.parse(options.body);requests.push({url,body});
 return {today:{revision:5,ids:['active',body.identity],capacity_minutes:120,estimates:{[body.identity]:45}},
   week:{revision:8,timezone:'UTC',days:[{plan_date:'2026-08-21',ids:['sibling'],capacity_minutes:90,estimates:{sibling:30}}]}};
},localDate:()=> '2026-08-20',timeZone:()=> 'UTC'});
week.adopt({revision:7,timezone:'UTC',days:[{plan_date:'2026-08-21',ids:['pull','sibling'],capacity_minutes:90,estimates:{pull:45,sibling:30}}]});
const pulled=await week.pullItem('pull',{revision:4,ids:['active']},'stable-pull',true);
console.log(JSON.stringify({pulled,requests,state:week.state()}));
""")

    assert result["requests"] == [{
        "url": "api/v1/week/pull-item",
        "body": {
            "operation_id": "stable-pull", "identity": "pull",
            "today_revision": 4, "week_revision": 7,
            "allow_over_capacity": True,
        },
    }]
    assert result["pulled"]["today"]["ids"] == ["active", "pull"]
    assert result["state"]["revision"] == 8
    assert result["state"]["days"][0]["ids"] == ["sibling"]


def test_week_controller_replays_a_lost_pull_receipt_with_the_same_operation_after_reload():
    result = run_controller("""
const values=new Map(),requests=[];
const storage={getItem:key=>values.get(key)||null,setItem:(key,value)=>values.set(key,value),removeItem:key=>values.delete(key)};
let loseResponse=true;
const fetchJson=async(url,options={})=>{
 const body=JSON.parse(options.body);requests.push(body);
 if(loseResponse){loseResponse=false;throw new Error('connection lost');}
 return {today:{revision:5,ids:['active',body.identity],capacity_minutes:120,estimates:{active:30,[body.identity]:45}},
   week:{revision:8,timezone:'UTC',days:[{plan_date:'2026-08-21',ids:['sibling'],capacity_minutes:90,estimates:{sibling:30}}]}};
};
const options={storage,getLogin:()=> 'Timmy',fetchJson,localDate:()=> '2026-08-20',timeZone:()=> 'UTC'};
const first=createWeekPlan(options);
first.adopt({revision:7,timezone:'UTC',days:[{plan_date:'2026-08-21',ids:['pull','sibling'],capacity_minutes:90,estimates:{pull:45,sibling:30}}]});
const pending=await first.pullItem('pull',{revision:4,ids:['active'],capacity_minutes:120,estimates:{active:30}},'stable-pull');
const restored=createWeekPlan(options),resume=restored.resumePull();
const confirmed=await restored.flushPull();
console.log(JSON.stringify({pending,resume,confirmed,requests,keys:[...values.keys()],state:restored.state()}));
""")

    assert result["pending"]["sync_pending"] is True
    assert result["pending"]["today"]["ids"] == ["active", "pull"]
    assert result["pending"]["week"]["days"][0]["ids"] == ["sibling"]
    assert result["resume"]["sync_pending"] is True
    assert result["resume"]["today"] == result["pending"]["today"]
    assert [request["operation_id"] for request in result["requests"]] == ["stable-pull", "stable-pull"]
    assert result["confirmed"]["sync_pending"] is False
    assert result["confirmed"]["today"]["revision"] == 5
    assert result["keys"] == ["stackchain.week-confirmed.v1.timmy"]
    assert result["state"]["revision"] == 8


def test_week_controller_queues_two_offline_pulls_and_replays_them_fifo_after_reload():
    result = run_controller("""
const values=new Map(),delivered=[];
const storage={getItem:key=>values.get(key)||null,setItem:(key,value)=>values.set(key,value),removeItem:key=>values.delete(key)};
let offline=true;
let serverToday={revision:4,ids:['active'],capacity_minutes:180,estimates:{active:30}};
let serverWeek={revision:7,timezone:'UTC',days:[{plan_date:'2026-08-21',ids:['first','second','later'],capacity_minutes:150,estimates:{first:35,second:40,later:45}}]};
const fetchJson=async(url,options={})=>{
 if(offline)throw new Error('connection lost');
 const body=JSON.parse(options.body);delivered.push(body);
 if(body.today_revision!==serverToday.revision||body.week_revision!==serverWeek.revision){const error=new Error('changed');error.status=409;throw error;}
 serverToday={...serverToday,revision:serverToday.revision+1,ids:[...serverToday.ids,body.identity],
   estimates:{...serverToday.estimates,[body.identity]:serverWeek.days[0].estimates[body.identity]}};
 serverWeek={...serverWeek,revision:serverWeek.revision+1,days:serverWeek.days.map(day=>({...day,
   ids:day.ids.filter(id=>id!==body.identity),estimates:Object.fromEntries(Object.entries(day.estimates).filter(([id])=>id!==body.identity))}))};
 return {today:serverToday,week:serverWeek};
};
const options={storage,getLogin:()=> 'Timmy',fetchJson,localDate:()=> '2026-08-20',timeZone:()=> 'UTC'};
const first=createWeekPlan(options);first.adopt(serverWeek);
const queuedFirst=await first.pullItem('first',serverToday,'pull-first');
const queuedSecond=await first.pullItem('second',queuedFirst.today,'pull-second');
const restored=createWeekPlan(options),resumed=restored.resumePull();
offline=false;
const confirmed=await restored.flushPull();
console.log(JSON.stringify({queuedFirst,queuedSecond,resumed,confirmed,delivered,keys:[...values.keys()],state:restored.state()}));
""")

    assert result["queuedFirst"]["today"]["ids"] == ["active", "first"]
    assert result["queuedSecond"]["today"]["ids"] == ["active", "first", "second"]
    assert result["resumed"]["today"]["ids"] == ["active", "first", "second"]
    assert [request["operation_id"] for request in result["delivered"]] == ["pull-first", "pull-second"]
    assert result["delivered"][1]["today_revision"] == 5
    assert result["delivered"][1]["week_revision"] == 8
    assert result["confirmed"]["today"]["ids"] == ["active", "first", "second"]
    assert result["confirmed"]["week"]["days"][0]["ids"] == ["later"]
    assert result["confirmed"]["sync_pending"] is False
    assert result["keys"] == ["stackchain.week-confirmed.v1.timmy"]
    assert result["state"]["revision"] == 9


@pytest.mark.parametrize(
    "coordinator_setup",
    [
        """const locks={request:async(name,_options,work)=>{
 if(held.has(name))return work(null);
 held.add(name);try{return await work({name});}finally{held.delete(name);}
}};
const coordinatorA=createOutboxCoordinator({storage,locks,channelFactory:null,tabId:'a'});
const coordinatorB=createOutboxCoordinator({storage,locks,channelFactory:null,tabId:'b'});""",
        """const coordinatorA=createOutboxCoordinator({storage,locks:null,channelFactory:null,tabId:'a',now:()=>100});
const coordinatorB=createOutboxCoordinator({storage,locks:null,channelFactory:null,tabId:'b',now:()=>100});""",
    ],
    ids=["navigator-lock", "storage-lease"],
)
def test_week_controller_allows_only_one_tab_to_replay_the_shared_pull_queue(coordinator_setup):
    result = run_controller("""
const values=new Map(),held=new Set();
const storage={getItem:key=>values.get(key)||null,setItem:(key,value)=>values.set(key,value),removeItem:key=>values.delete(key)};
COORDINATOR_SETUP
let offline=true,posts=0,release;
const gate=new Promise(resolve=>release=resolve);
const fetchJson=async(_url,options={})=>{
 if(offline)throw new Error('connection lost');
 posts+=1;await gate;
 const body=JSON.parse(options.body);
 return {today:{revision:5,ids:['active',body.identity],capacity_minutes:180,estimates:{active:30,[body.identity]:35}},
   week:{revision:8,timezone:'UTC',days:[{plan_date:'2026-08-21',ids:[],capacity_minutes:150,estimates:{}}]}};
};
const options={storage,getLogin:()=> 'timmy',fetchJson,localDate:()=> '2026-08-20',timeZone:()=> 'UTC'};
const source=createWeekPlan({...options,coordinator:coordinatorA});source.adopt({revision:7,timezone:'UTC',days:[{plan_date:'2026-08-21',ids:['first'],capacity_minutes:150,estimates:{first:35}}]});
await source.pullItem('first',{revision:4,ids:['active'],capacity_minutes:180,estimates:{active:30}},'pull-first');
offline=false;
const first=createWeekPlan({...options,coordinator:coordinatorA}),second=createWeekPlan({...options,coordinator:coordinatorB});
const firstFlush=first.flushPull();
await Promise.resolve();
setImmediate(release);
const skipped=await second.flushPull();
release();
const confirmed=await firstFlush;
console.log(JSON.stringify({posts,skipped,confirmed,pending:first.pendingPull()}));
""".replace("COORDINATOR_SETUP", coordinator_setup))

    assert result["posts"] == 1
    assert result["skipped"]["lease_skipped"] is True
    assert result["confirmed"]["sync_pending"] is False
    assert result["pending"] is None


def test_week_controller_resumes_server_truth_when_a_fifo_head_conflicts():
    result = run_controller("""
const values=new Map();
const storage={getItem:key=>values.get(key)||null,setItem:(key,value)=>values.set(key,value),removeItem:key=>values.delete(key)};
let offline=true,posts=0;
const remoteToday={revision:10,ids:['server-today'],capacity_minutes:180,estimates:{'server-today':30}};
const remoteWeek={revision:12,timezone:'UTC',days:[{plan_date:'2026-08-21',ids:['second','third','server-week'],capacity_minutes:180,estimates:{second:40,third:45,'server-week':25}}]};
const fetchJson=async(url,options={})=>{
 if(offline)throw new Error('connection lost');
 if(!options.method)return url.endsWith('/today')?remoteToday:remoteWeek;
 posts+=1;
 if(posts===1)return {today:{revision:5,ids:['active','first'],capacity_minutes:180,estimates:{active:30,first:35}},
   week:{revision:8,timezone:'UTC',days:[{plan_date:'2026-08-21',ids:['second','third'],capacity_minutes:180,estimates:{second:40,third:45}}]}};
 const error=new Error('changed');error.status=409;throw error;
};
const options={storage,getLogin:()=> 'timmy',fetchJson,localDate:()=> '2026-08-20',timeZone:()=> 'UTC'};
const first=createWeekPlan(options);first.adopt({revision:7,timezone:'UTC',days:[{plan_date:'2026-08-21',ids:['first','second','third'],capacity_minutes:180,estimates:{first:35,second:40,third:45}}]});
let today={revision:4,ids:['active'],capacity_minutes:180,estimates:{active:30}};
today=(await first.pullItem('first',today,'pull-first')).today;
today=(await first.pullItem('second',today,'pull-second')).today;
await first.pullItem('third',today,'pull-third');
offline=false;const conflicted=await first.flushPull();
const restored=createWeekPlan(options),resumed=restored.resumePull();
console.log(JSON.stringify({conflicted,resumed,state:restored.state(),pending:restored.pendingPull()}));
""")

    assert result["conflicted"]["conflict"] is True
    assert result["resumed"]["conflict"] is True
    assert result["resumed"]["today"]["ids"] == ["server-today"]
    assert result["state"]["revision"] == 12
    assert result["state"]["days"][0]["ids"] == ["second", "third", "server-week"]
    assert result["pending"]["body"]["operation_id"] == "pull-second"


def test_week_controller_preserves_a_conflicted_pull_intent_and_adopts_server_truth():
    result = run_controller("""
const values=new Map(),requests=[];
const storage={getItem:key=>values.get(key)||null,setItem:(key,value)=>values.set(key,value),removeItem:key=>values.delete(key)};
const remoteToday={revision:9,ids:['newer'],capacity_minutes:90,estimates:{newer:30}};
const remoteWeek={revision:12,timezone:'UTC',days:[{plan_date:'2026-08-21',ids:['pull','server'],capacity_minutes:90,estimates:{pull:45,server:30}}]};
const fetchJson=async(url,options={})=>{
 requests.push({url,method:options.method||'GET'});
 if(options.method){const error=new Error('changed');error.status=409;throw error;}
 return url.endsWith('/today')?remoteToday:remoteWeek;
};
const week=createWeekPlan({storage,getLogin:()=> 'timmy',fetchJson,localDate:()=> '2026-08-20',timeZone:()=> 'UTC'});
week.adopt({revision:7,timezone:'UTC',days:[{plan_date:'2026-08-21',ids:['pull'],capacity_minutes:90,estimates:{pull:45}}]});
const outcome=await week.pullItem('pull',{revision:4,ids:['active'],capacity_minutes:90,estimates:{active:30}},'conflicted-pull');
console.log(JSON.stringify({outcome,state:week.state(),pending:week.pendingPull(),requests}));
""")

    assert result["outcome"]["conflict"] is True
    assert result["outcome"]["today"]["ids"] == ["newer"]
    assert result["state"]["revision"] == 12
    assert result["state"]["days"][0]["ids"] == ["pull", "server"]
    assert result["pending"]["body"]["operation_id"] == "conflicted-pull"
    assert result["pending"]["conflict"] is True
    assert result["requests"] == [
        {"url": "api/v1/week/pull-item", "method": "POST"},
        {"url": "api/v1/today", "method": "GET"},
        {"url": "api/v1/week", "method": "GET"},
    ]


def test_week_controller_describes_the_conflicted_transfer_from_current_plans():
    result = run_controller("""
const values=new Map();
const storage={getItem:key=>values.get(key)||null,setItem:(key,value)=>values.set(key,value),removeItem:key=>values.delete(key)};
const remoteToday={revision:9,ids:['active'],capacity_minutes:90,estimates:{active:30}};
const remoteWeek={revision:12,timezone:'UTC',days:[{plan_date:'2026-08-21',ids:['pull'],capacity_minutes:90,estimates:{pull:45}}]};
const fetchJson=async(url,options={})=>{if(options.method){const error=new Error('changed');error.status=409;throw error;}return url.endsWith('/today')?remoteToday:remoteWeek;};
const week=createWeekPlan({storage,getLogin:()=> 'timmy',fetchJson,localDate:()=> '2026-08-20',timeZone:()=> 'UTC'});
week.adopt({revision:7,timezone:'UTC',days:[{plan_date:'2026-08-21',ids:['pull'],capacity_minutes:90,estimates:{pull:45}}]});
week.rememberItems({'pull':{title:'Ship recovery',kind:'issue',repository:'acme/app',number:7}});
await week.pullItem('pull',{revision:4,ids:['old'],capacity_minutes:90,estimates:{old:30}},'stable-pull');
console.log(JSON.stringify(week.pullConflict()));
""")

    assert result == {
        "identity": "pull", "title": "Ship recovery", "source_date": "2026-08-21",
        "today_items": 1, "today_minutes": 30, "today_capacity": 90,
        "operation_id": "stable-pull", "queue_length": 1,
    }


def test_week_controller_rechecks_capacity_before_rebasing_a_conflicted_pull():
    result = run_controller("""
const values=new Map();let posts=0;
const storage={getItem:key=>values.get(key)||null,setItem:(key,value)=>values.set(key,value),removeItem:key=>values.delete(key)};
const remoteToday={revision:9,ids:['active'],capacity_minutes:50,estimates:{active:40}};
const remoteWeek={revision:12,timezone:'UTC',days:[{plan_date:'2026-08-21',ids:['pull'],capacity_minutes:90,estimates:{pull:20}}]};
const fetchJson=async(url,options={})=>{if(options.method){posts+=1;const error=new Error('changed');error.status=409;throw error;}return url.endsWith('/today')?remoteToday:remoteWeek;};
const week=createWeekPlan({storage,getLogin:()=> 'timmy',fetchJson,localDate:()=> '2026-08-20',timeZone:()=> 'UTC'});
week.adopt({revision:7,timezone:'UTC',days:[{plan_date:'2026-08-21',ids:['pull'],capacity_minutes:90,estimates:{pull:20}}]});
await week.pullItem('pull',{revision:4,ids:['old'],capacity_minutes:90,estimates:{old:30}},'stable-pull');
let code=null;try{await week.resolvePullConflict('add');}catch(error){code=error.code;}
console.log(JSON.stringify({code,posts,pending:week.pendingPull()}));
""")

    assert result["code"] == "today_over_capacity"
    assert result["posts"] == 1
    assert result["pending"]["conflict"] is True


def test_week_controller_rebases_a_conflicted_pull_and_drains_the_fifo_with_stable_operations():
    result = run_controller("""
const values=new Map(),delivered=[];
const storage={getItem:key=>values.get(key)||null,setItem:(key,value)=>values.set(key,value),removeItem:key=>values.delete(key)};
let offline=true;
let serverToday={revision:10,ids:['active'],capacity_minutes:180,estimates:{active:30}};
let serverWeek={revision:12,timezone:'UTC',days:[{plan_date:'2026-08-21',ids:['first','second'],capacity_minutes:180,estimates:{first:35,second:40}}]};
const fetchJson=async(url,options={})=>{
 if(offline)throw new Error('offline');
 if(!options.method)return url.endsWith('/today')?serverToday:serverWeek;
 const body=JSON.parse(options.body);delivered.push(body);
 if(body.today_revision!==serverToday.revision||body.week_revision!==serverWeek.revision){const error=new Error('changed');error.status=409;throw error;}
 serverToday={...serverToday,revision:serverToday.revision+1,ids:[...serverToday.ids,body.identity],estimates:{...serverToday.estimates,[body.identity]:serverWeek.days[0].estimates[body.identity]}};
 serverWeek={...serverWeek,revision:serverWeek.revision+1,days:serverWeek.days.map(day=>({...day,ids:day.ids.filter(id=>id!==body.identity),estimates:Object.fromEntries(Object.entries(day.estimates).filter(([id])=>id!==body.identity))}))};
 return {today:serverToday,week:serverWeek};
};
const week=createWeekPlan({storage,getLogin:()=> 'timmy',fetchJson,localDate:()=> '2026-08-20',timeZone:()=> 'UTC'});
week.adopt({revision:7,timezone:'UTC',days:[{plan_date:'2026-08-21',ids:['first','second'],capacity_minutes:180,estimates:{first:35,second:40}}]});
let today={revision:4,ids:['active'],capacity_minutes:180,estimates:{active:30}};
today=(await week.pullItem('first',today,'pull-first')).today;
await week.pullItem('second',today,'pull-second');
offline=false;await week.flushPull();
const resolved=await week.resolvePullConflict('add');
console.log(JSON.stringify({resolved,delivered,pending:week.pendingPull(),state:week.state()}));
""")

    assert [request["operation_id"] for request in result["delivered"]] == [
        "pull-first", "pull-first", "pull-second"
    ]
    assert result["delivered"][1]["today_revision"] == 10
    assert result["delivered"][1]["week_revision"] == 12
    assert result["resolved"]["today"]["ids"] == ["active", "first", "second"]
    assert result["pending"] is None
    assert result["state"]["revision"] == 14


def test_week_controller_keeps_the_conflicted_item_in_week_and_advances_only_the_fifo_head():
    result = run_controller("""
const values=new Map(),delivered=[];
const storage={getItem:key=>values.get(key)||null,setItem:(key,value)=>values.set(key,value),removeItem:key=>values.delete(key)};
let offline=true;
let serverToday={revision:10,ids:['active'],capacity_minutes:180,estimates:{active:30}};
let serverWeek={revision:12,timezone:'UTC',days:[{plan_date:'2026-08-21',ids:['first','second'],capacity_minutes:180,estimates:{first:35,second:40}}]};
const fetchJson=async(url,options={})=>{
 if(offline)throw new Error('offline');
 if(!options.method)return url.endsWith('/today')?serverToday:serverWeek;
 const body=JSON.parse(options.body);delivered.push(body);
 if(body.today_revision!==serverToday.revision||body.week_revision!==serverWeek.revision){const error=new Error('changed');error.status=409;throw error;}
 serverToday={...serverToday,revision:11,ids:[...serverToday.ids,body.identity],estimates:{...serverToday.estimates,[body.identity]:40}};
 serverWeek={...serverWeek,revision:13,days:serverWeek.days.map(day=>({...day,ids:day.ids.filter(id=>id!==body.identity)}))};
 return {today:serverToday,week:serverWeek};
};
const week=createWeekPlan({storage,getLogin:()=> 'timmy',fetchJson,localDate:()=> '2026-08-20',timeZone:()=> 'UTC'});
week.adopt({revision:7,timezone:'UTC',days:[{plan_date:'2026-08-21',ids:['first','second'],capacity_minutes:180,estimates:{first:35,second:40}}]});
let today={revision:4,ids:['active'],capacity_minutes:180,estimates:{active:30}};
today=(await week.pullItem('first',today,'pull-first')).today;
await week.pullItem('second',today,'pull-second');
offline=false;await week.flushPull();
const resolved=await week.resolvePullConflict('keep');
console.log(JSON.stringify({resolved,delivered,pending:week.pendingPull()}));
""")

    assert [request["operation_id"] for request in result["delivered"]] == ["pull-first", "pull-second"]
    assert result["delivered"][1]["today_revision"] == 10
    assert result["delivered"][1]["week_revision"] == 12
    assert result["resolved"]["today"]["ids"] == ["active", "second"]
    assert result["resolved"]["week"]["days"][0]["ids"] == ["first"]
    assert result["pending"] is None


def test_week_controller_retires_completed_work_from_every_day_in_one_durable_transition():
    result = run_controller("""
const values=new Map();let writes=0;
const storage={getItem:key=>values.get(key)||null,setItem:(key,value)=>{writes+=1;values.set(key,value);},removeItem:key=>values.delete(key)};
const week=createWeekPlan({storage,getLogin:()=> 'timmy',fetchJson:async()=>({}),
  localDate:()=> '2026-08-20',timeZone:()=> 'UTC'});
week.adopt({revision:6,timezone:'UTC',days:[
 {plan_date:'2026-08-21',ids:['completed','sibling'],capacity_minutes:90,estimates:{completed:30,sibling:45}},
 {plan_date:'2026-08-22',ids:['other','completed'],capacity_minutes:120,estimates:{other:60,completed:30}},
 {plan_date:'2026-08-23',ids:['untouched'],capacity_minutes:75,estimates:{untouched:25}}
]});
const retired=week.retire('completed');
console.log(JSON.stringify({retired,writes,state:week.state(),pending:week.pending()}));
""")

    assert result["retired"]["removed"] == 2
    assert result["retired"]["sync_pending"] is True
    assert result["writes"] == 1
    assert result["state"]["days"] == [
        {"plan_date": "2026-08-21", "ids": ["sibling"], "capacity_minutes": 90,
         "estimates": {"sibling": 45}},
        {"plan_date": "2026-08-22", "ids": ["other"], "capacity_minutes": 120,
         "estimates": {"other": 60}},
        {"plan_date": "2026-08-23", "ids": ["untouched"], "capacity_minutes": 75,
         "estimates": {"untouched": 25}},
    ]
    assert result["pending"]["days"] == result["state"]["days"]


def test_week_controller_unplans_active_work_and_restores_exact_placement():
    result = run_controller("""
const values=new Map();let writes=0;
const storage={getItem:key=>values.get(key)||null,setItem:(key,value)=>{writes+=1;values.set(key,value);},removeItem:key=>values.delete(key)};
const week=createWeekPlan({storage,getLogin:()=> 'timmy',fetchJson:async()=>({}),
  localDate:()=> '2026-08-20',timeZone:()=> 'UTC'});
week.adopt({revision:6,timezone:'UTC',days:[
 {plan_date:'2026-08-21',ids:['first','remove','last'],capacity_minutes:120,estimates:{first:20,remove:35,last:40}},
 {plan_date:'2026-08-22',ids:['other'],capacity_minutes:90,estimates:{other:30}}
]});
const receipt=week.unplan('remove');
const removed=week.state();
const restored=week.restore(receipt);
console.log(JSON.stringify({receipt,removed,restored,writes,state:week.state(),pending:week.pending()}));
""")

    assert result["receipt"] == {
        "id": "remove", "plan_date": "2026-08-21", "index": 1, "estimate": 35,
    }
    assert result["removed"]["days"][0]["ids"] == ["first", "last"]
    assert result["restored"]["sync_pending"] is True
    assert result["writes"] == 2
    assert result["state"]["days"][0]["ids"] == ["first", "remove", "last"]
    assert result["state"]["days"][0]["estimates"]["remove"] == 35
    assert result["pending"]["days"] == result["state"]["days"]


def test_week_workflow_saves_and_advances_without_closing_the_planner():
    result = run_controller("""
const createWorkflow=createWeekPlan.Workflow;
const dates=['2026-08-21','2026-08-22','2026-08-23','2026-08-24','2026-08-25','2026-08-26','2026-08-27'];
const elements=new Map();
const makeElement=()=>({hidden:false,textContent:'',disabled:false,innerHTML:'',addEventListener:()=>{},focus:()=>{},
  querySelectorAll:()=>[],querySelector:()=>null,scrollIntoView:()=>{}});
const root=makeElement();
Object.defineProperty(root,'innerHTML',{set(value){this.value=value;this.buttons=[...value.matchAll(/data-week-plan-date=\"([^\"]+)/g)].map(match=>({
  dataset:{weekPlanDate:match[1]},addEventListener:()=>{},scrollIntoView(){this.scrolled=true;}
}));},get(){return this.value||'';}});
root.querySelectorAll=()=>root.buttons||[];
root.querySelector=selector=>(root.buttons||[]).find(button=>selector.includes(button.dataset.weekPlanDate))||null;
elements.set('#week-plan-dates',root);
for(const selector of ['#mobile-week-summary','#my-work-action-status','#week-plan-progress','#save-today-plan',
  '#week-review','#week-review-days','#week-review-duplicates','#confirm-week-plan','#week-review-status','#edit-week-plan']) elements.set(selector,makeElement());
let opened=0,staged=[];
const controller={dates:()=>dates.map((date,index)=>({date,label:'Day '+(index+1)})),day:date=>({plan_date:date,ids:[]}),
  pass:date=>{const index=dates.indexOf(date);return {position:index+1,total:7,planned:staged.length,next_date:dates[index+1]||null,last:index===6};},
  load:async()=>({}),summary:()=> 'Nothing planned',review:()=>({days:dates.map(date=>({plan_date:date,label:date,ids:[],capacity_minutes:null,estimates:{},planned_minutes:0,overloaded:false})),duplicates:[],blockers:[],can_confirm:true}),pending:()=>false,
  stageDay:(date,plan)=>{staged.push({date,plan});return true;},flush:async()=>({})};
const workflow=createWorkflow({controller,qs:selector=>elements.get(selector),getLogin:()=> 'timmy',
  openPlanner:()=>{opened+=1;},escapeHtml:value=>value,escapeAttribute:value=>value,
  todayWork:{replace:()=>{},replacePlanning:()=>{}},refresh:()=>{},warm:()=>{}});
await workflow.open({disabled:false});
workflow.editWeek();
workflow.save({ids:['issue:r:1:'],capacity_minutes:60,estimates:{'issue:r:1:':30}});
const continued=workflow.advance();
console.log(JSON.stringify({continued,selected:workflow.selectedDate(),opened,staged,
  progress:elements.get('#week-plan-progress').textContent,label:elements.get('#save-today-plan').textContent}));
""")

    assert result["continued"] is True
    assert result["selected"] == "2026-08-22"
    assert result["opened"] == 4
    assert result["staged"][0]["date"] == "2026-08-21"
    assert result["progress"] == "Day 2 of 7 · 1 planned"
    assert result["label"] == "Save & next"


def test_week_workflow_opens_a_read_only_overview_with_next_up_work():
    result = run_controller("""
const createWorkflow=createWeekPlan.Workflow;
const dates=['2026-08-21','2026-08-22','2026-08-23','2026-08-24','2026-08-25','2026-08-26','2026-08-27'];
const elements=new Map();
const makeElement=()=>({hidden:false,textContent:'',disabled:false,innerHTML:'',addEventListener:()=>{},focus:()=>{},
  querySelectorAll:()=>[],querySelector:()=>null,scrollIntoView:()=>{}});
const dateRoot=makeElement();
Object.defineProperty(dateRoot,'innerHTML',{set(value){this.value=value;this.buttons=[];},get(){return this.value||'';}});
elements.set('#week-plan-dates',dateRoot);
const reviewDays=makeElement();
Object.defineProperty(reviewDays,'innerHTML',{set(value){this.value=value;this.pullButtons=[...value.matchAll(/data-week-pull-item=\"([^\"]+)/g)].map(match=>({
  dataset:{weekPullItem:match[1]},listeners:{},disabled:false,
  addEventListener(name,listener){this.listeners[name]=listener;},focus(){this.focused=true;}
}));},get(){return this.value||'';}});
reviewDays.querySelectorAll=selector=>selector==='[data-week-pull-item]'?reviewDays.pullButtons||[]:[];
elements.set('#week-review-days',reviewDays);
for(const selector of ['#mobile-week-summary','#my-work-action-status','#week-plan-progress','#save-today-plan',
  '#week-review','#week-review-duplicates','#confirm-week-plan','#week-review-status','#back-to-week-review',
  '#edit-week-plan']) if(!elements.has(selector))elements.set(selector,makeElement());
let writes=0,opened=0,reviewMode=false,pullRequest=null,replacedToday=null;
const review={days:dates.map((date,index)=>({plan_date:date,label:'Day '+(index+1),
  ids:index===2?['issue:stackchain/dashboard:42:']:[],capacity_minutes:60,
  estimates:index===2?{'issue:stackchain/dashboard:42:':30}:{},planned_minutes:index===2?30:0,overloaded:false})),
  duplicates:[],blockers:[],can_confirm:true};
const controller={dates:()=>dates.map((date,index)=>({date,label:'Day '+(index+1)})),day:date=>review.days.find(day=>day.plan_date===date),
  pass:date=>{const index=dates.indexOf(date);return {position:index+1,total:7,planned:1,next_date:dates[index+1]||null,last:index===6};},
  load:async()=>({}),summary:()=> '1 item across 1 day',review:()=>review,pending:()=>false,offline:()=>false,
  move:()=>true,flush:async()=>{writes+=1;},pullItem:async(id,today,operationId)=>{
    pullRequest={id,today,operationId};return {today:{revision:4,ids:['active',id],estimates:{[id]:30}},week:{revision:8,days:[]}};
  }};
const workflow=createWorkflow({controller,qs:selector=>elements.get(selector),getLogin:()=> 'timmy',
  getItem:id=>id.includes(':42:')?{kind:'issue',title:'Ship mobile overview',repository:'stackchain/dashboard',number:42}:null,
  openPlanner:()=>{opened+=1;},setReviewMode:value=>{reviewMode=value;},escapeHtml:value=>value,escapeAttribute:value=>value,
  today:()=>({revision:3,ids:['active'],estimates:{}}),operationId:()=> 'pull-once',
  todayWork:{replace:ids=>{replacedToday=ids;},replacePlanning:()=>{}},refresh:()=>{},warm:()=>{}});
await workflow.open({disabled:false});
const initialMarkup=reviewDays.innerHTML;
await reviewDays.pullButtons[0].listeners.click({currentTarget:reviewDays.pullButtons[0]});
console.log(JSON.stringify({reviewing:workflow.reviewing(),reviewMode,writes,opened,copy:workflow.copy(),
  reviewHidden:elements.get('#week-review').hidden,datesHidden:elements.get('#week-plan-dates').hidden,
  markup:initialMarkup,status:elements.get('#week-review-status').textContent,pullRequest,replacedToday,
  confirmHidden:elements.get('#confirm-week-plan').hidden,editWeekHidden:elements.get('#edit-week-plan').hidden,
  editWeekLabel:elements.get('#edit-week-plan').textContent}));
""")

    assert result["reviewing"] is True
    assert result["reviewMode"] is True
    assert result["writes"] == 0
    assert result["copy"] == {
        "title": "Week Ahead", "heading": "Seven-day overview",
        "available": "Planned work", "build": "Edit week",
    }
    assert result["reviewHidden"] is False
    assert result["datesHidden"] is True
    assert "Ship mobile overview" in result["markup"]
    assert "Next up" in result["markup"]
    assert "Add to Today" in result["markup"]
    assert result["pullRequest"]["id"] == "issue:stackchain/dashboard:42:"
    assert result["pullRequest"]["operationId"] == "pull-once"
    assert result["replacedToday"] == ["active", "issue:stackchain/dashboard:42:"]
    assert "data-week-move" not in result["markup"]
    assert result["status"] == "Ship mobile overview added to Today."
    assert result["confirmHidden"] is True
    assert result["editWeekHidden"] is False
    assert result["editWeekLabel"] == "Edit week"


def test_week_overview_reviews_today_capacity_before_confirming_an_overload():
    result = run_controller("""
const createWorkflow=createWeekPlan.Workflow;
const dates=['2026-08-21','2026-08-22','2026-08-23','2026-08-24','2026-08-25','2026-08-26','2026-08-27'];
const elements=new Map();
const makeElement=()=>({hidden:false,textContent:'',disabled:false,innerHTML:'',addEventListener:()=>{},focus:()=>{},
  querySelectorAll:()=>[],querySelector:()=>null,scrollIntoView:()=>{}});
elements.set('#week-plan-dates',makeElement());
const reviewDays=makeElement();
Object.defineProperty(reviewDays,'innerHTML',{set(value){this.value=value;this.pullButtons=[...value.matchAll(/data-week-pull-item=\"([^\"]+)/g)].map(match=>({
  dataset:{weekPullItem:match[1]},listeners:{},disabled:false,
  addEventListener(name,listener){this.listeners[name]=listener;},focus(){this.focused=true;}
}));},get(){return this.value||'';}});
reviewDays.querySelectorAll=selector=>selector==='[data-week-pull-item]'?reviewDays.pullButtons||[]:[];
elements.set('#week-review-days',reviewDays);
for(const selector of ['#mobile-week-summary','#my-work-action-status','#week-plan-progress','#save-today-plan',
  '#week-review','#week-review-duplicates','#confirm-week-plan','#week-review-status','#back-to-week-review','#edit-week-plan'])
  if(!elements.has(selector))elements.set(selector,makeElement());
const review={days:dates.map((date,index)=>({plan_date:date,label:'Day '+(index+1),
  ids:index===0?['issue:stackchain/dashboard:44:']:[],capacity_minutes:60,
  estimates:index===0?{'issue:stackchain/dashboard:44:':30}:{},planned_minutes:index===0?30:0,overloaded:false})),
  duplicates:[],blockers:[],can_confirm:true};
const pulls=[],confirmations=[];let approve=false;
const controller={dates:()=>dates.map((date,index)=>({date,label:'Day '+(index+1)})),day:date=>review.days.find(day=>day.plan_date===date),
  pass:date=>({position:dates.indexOf(date)+1,total:7,planned:1,next_date:null,last:false}),load:async()=>({}),
  summary:()=> '1 item across 1 day',review:()=>review,pending:()=>false,offline:()=>false,move:()=>true,flush:async()=>({}),
  pullItem:async(id,today,operationId,allowOverCapacity)=>{pulls.push({id,operationId,allowOverCapacity});
    return {today:{revision:4,ids:['active',id],capacity_minutes:60,estimates:{active:45,[id]:30}},week:{revision:8,days:[]}};}};
const workflow=createWorkflow({controller,qs:selector=>elements.get(selector),
  getItem:id=>({kind:'issue',title:'Capacity review',repository:'stackchain/dashboard',number:44}),
  openPlanner:()=>{},escapeHtml:value=>value,escapeAttribute:value=>value,
  today:()=>({revision:3,ids:['active'],capacity_minutes:60,estimates:{active:45}}),operationId:()=> 'capacity-pull',
  confirmEarly:message=>{confirmations.push(message);return approve;},
  todayWork:{replace:()=>{},replacePlanning:()=>{}},refresh:()=>{},warm:()=>{}});
await workflow.open({disabled:false});
const markup=reviewDays.innerHTML,button=reviewDays.pullButtons[0];
await button.listeners.click({currentTarget:button});
const afterCancel={pulls:pulls.length,disabled:button.disabled,status:elements.get('#week-review-status').textContent};
approve=true;
await button.listeners.click({currentTarget:button});
console.log(JSON.stringify({markup,confirmations,afterCancel,pulls}));
""")

    assert "75 of 60 min" in result["markup"]
    assert result["afterCancel"] == {"pulls": 0, "disabled": False, "status": "Week Ahead overview · no changes made."}
    assert result["confirmations"] == [
        "Add Capacity review to Today?\n\nToday will be 75 of 60 min · 15 min over capacity.",
        "Add Capacity review to Today?\n\nToday will be 75 of 60 min · 15 min over capacity.",
    ]
    assert result["pulls"] == [{
        "id": "issue:stackchain/dashboard:44:", "operationId": "capacity-pull",
        "allowOverCapacity": True,
    }]


def test_week_overview_removes_active_work_and_undo_restores_it():
    result = run_controller("""
const createWorkflow=createWeekPlan.Workflow;
const dates=['2026-08-21','2026-08-22','2026-08-23','2026-08-24','2026-08-25','2026-08-26','2026-08-27'];
const elements=new Map();
const makeElement=()=>({hidden:false,textContent:'',disabled:false,innerHTML:'',listeners:{},dataset:{},
  addEventListener(name,listener){this.listeners[name]=listener;},focus(){this.focused=true;},querySelectorAll:()=>[],querySelector:()=>null});
const reviewDays=makeElement();
Object.defineProperty(reviewDays,'innerHTML',{set(value){this.value=value;this.removeButtons=[...value.matchAll(/data-week-unplan=\"([^\"]+)/g)].map(match=>({
  dataset:{weekUnplan:match[1]},listeners:{},addEventListener(name,listener){this.listeners[name]=listener;},focus(){this.focused=true;}
}));},get(){return this.value||'';}});
reviewDays.querySelectorAll=selector=>selector==='[data-week-unplan]'?reviewDays.removeButtons||[]:[];
elements.set('#week-review-days',reviewDays);
const undo=makeElement();elements.set('#undo-week-unplan',undo);
for(const selector of ['#week-plan-dates','#mobile-week-summary','#my-work-action-status','#week-plan-progress','#save-today-plan',
  '#week-review','#week-review-duplicates','#confirm-week-plan','#week-review-status','#back-to-week-review','#edit-week-plan',
  '#week-offline-snapshot','#retry-week-live','#open-week-capacity-import','#open-week-reflow','#week-unplan-receipt'])
  if(!elements.has(selector))elements.set(selector,makeElement());
let ids=['one','remove','last'],unplanned=null,restored=null,flushes=0,timer=null;
const buildReview=()=>({days:dates.map((date,index)=>({plan_date:date,label:'Day '+(index+1),ids:index===0?[...ids]:[],
  capacity_minutes:120,estimates:index===0?{one:20,remove:35,last:40}:{},planned_minutes:index===0?ids.reduce((sum,id)=>sum+({one:20,remove:35,last:40}[id]),0):0,overloaded:false})),duplicates:[],blockers:[],can_confirm:true});
const controller={dates:()=>dates.map((date,index)=>({date,label:'Day '+(index+1)})),day:date=>buildReview().days.find(day=>day.plan_date===date),
  load:async()=>({}),summary:()=> ids.length+' items across 1 day',review:buildReview,pending:()=>false,offline:()=>false,
  unplan:id=>{const index=ids.indexOf(id);if(index<0)return false;ids.splice(index,1);return unplanned={id,plan_date:dates[0],index,estimate:35};},
  restore:receipt=>{ids.splice(receipt.index,0,receipt.id);restored=receipt;return {};},flush:async()=>{flushes+=1;return {};}};
const workflow=createWorkflow({controller,qs:selector=>elements.get(selector),getItem:id=>({kind:'issue',title:id==='remove'?'Remove me':id,repository:'r',number:2}),
  openPlanner:()=>{},setReviewMode:()=>{},escapeHtml:value=>value,escapeAttribute:value=>value,
  setTimer:callback=>{timer=callback;return 1;},clearTimer:()=>{timer=null;},
  todayWork:{replace:()=>{},replacePlanning:()=>{}},refresh:()=>{},warm:()=>{}});
await workflow.open({disabled:false});
const remove=reviewDays.removeButtons.find(button=>button.dataset.weekUnplan==='remove');
await remove.listeners.click({currentTarget:remove});
const afterRemove={ids:[...ids],flushes,receiptHidden:elements.get('#week-unplan-receipt').hidden,
  status:elements.get('#week-unplan-receipt').textContent,undoHidden:undo.hidden,timer:Boolean(timer)};
await undo.listeners.click({currentTarget:undo});
console.log(JSON.stringify({buttonCount:reviewDays.removeButtons.length,unplanned,afterRemove,restored,ids,flushes,
  receiptHidden:elements.get('#week-unplan-receipt').hidden,undoFocused:undo.focused||false}));
""")

    assert result["buttonCount"] == 3
    assert result["unplanned"] == {
        "id": "remove", "plan_date": "2026-08-21", "index": 1, "estimate": 35,
    }
    assert result["afterRemove"] == {
        "ids": ["one", "last"], "flushes": 1, "receiptHidden": False,
        "status": "Remove me removed from Week Ahead.", "undoHidden": False, "timer": True,
    }
    assert result["restored"] == result["unplanned"]
    assert result["ids"] == ["one", "remove", "last"]
    assert result["flushes"] == 2
    assert result["receiptHidden"] is True


def test_week_overview_previews_cancels_and_applies_one_reflow():
    result = run_controller("""
const createWorkflow=createWeekPlan.Workflow;
const dates=['2026-08-21','2026-08-22','2026-08-23','2026-08-24','2026-08-25','2026-08-26','2026-08-27'];
const elements=new Map();
const makeElement=()=>({hidden:false,textContent:'',disabled:false,innerHTML:'',dataset:{},listeners:{},
  addEventListener(name,listener){this.listeners[name]=listener;},focus(){this.focused=true;},querySelectorAll:()=>[],querySelector:()=>null});
for(const selector of ['#week-plan-dates','#week-review-days','#week-review-duplicates','#week-review','#week-review-status',
  '#confirm-week-plan','#edit-week-plan','#week-plan-progress','#save-today-plan','#mobile-week-summary','#my-work-action-status',
  '#week-offline-snapshot','#retry-week-live','#open-week-capacity-import','#open-week-reflow','#week-reflow-review',
  '#week-reflow-days','#week-reflow-unscheduled','#cancel-week-reflow','#apply-week-reflow'])elements.set(selector,makeElement());
const review={days:dates.map((date,index)=>({plan_date:date,label:'Day '+(index+1),ids:index===0?['one','two']:[],
  capacity_minutes:index===0?30:60,estimates:index===0?{one:30,two:30}:{},planned_minutes:index===0?60:0,overloaded:index===0})),
  duplicates:[],blockers:[{type:'over-capacity',plan_date:dates[0],minutes:30}],can_confirm:false};
const preview={days:dates.map((date,index)=>({plan_date:date,ids:index<2?[["one"],["two"]][index]:[],capacity_minutes:index===0?30:60,
  estimates:index===0?{one:30}:index===1?{two:30}:{}})),blockers:[],unscheduled:[{id:'large',estimate:90,from_date:dates[0]}]};
let applied=0,flushed=0;
const controller={dates:()=>dates.map((date,index)=>({date,label:'Day '+(index+1)})),day:date=>review.days.find(day=>day.plan_date===date),
  pass:()=>({}),load:async()=>({}),summary:()=>applied?'2 items across 2 days':'2 items across 1 day',review:()=>review,
  pending:()=>false,offline:()=>false,move:()=>true,previewReflow:()=>preview,applyReflow:()=>{applied+=1;return {};},
  flush:async()=>{flushed+=1;return {};}};
const workflow=createWorkflow({controller,qs:selector=>elements.get(selector),getItem:id=>({kind:'issue',title:id==='large'?'Large task':id,repository:'r',number:1}),
  openPlanner:()=>{},setReviewMode:()=>{},escapeHtml:value=>value,escapeAttribute:value=>value,
  todayWork:{replace:()=>{},replacePlanning:()=>{}},refresh:()=>{},warm:()=>{}});
await workflow.open({disabled:false});
const offered=!elements.get('#open-week-reflow').hidden;
await elements.get('#open-week-reflow').listeners.click();
const firstPreview={hidden:elements.get('#week-reflow-review').hidden,days:elements.get('#week-reflow-days').innerHTML,
  unscheduled:elements.get('#week-reflow-unscheduled').textContent};
await elements.get('#cancel-week-reflow').listeners.click();
const afterCancel={hidden:elements.get('#week-reflow-review').hidden,applied,flushed};
await elements.get('#open-week-reflow').listeners.click();
await elements.get('#apply-week-reflow').listeners.click();
console.log(JSON.stringify({offered,firstPreview,afterCancel,applied,flushed,status:elements.get('#week-review-status').textContent,
  reflowHidden:elements.get('#week-reflow-review').hidden}));
""")

    assert result["offered"] is True
    assert result["firstPreview"]["hidden"] is False
    assert "Day 1" in result["firstPreview"]["days"]
    assert "1 item · 30 of 30 min" in result["firstPreview"]["days"]
    assert result["firstPreview"]["unscheduled"] == "1 item cannot fit: Large task. It will remain in My Work."
    assert result["afterCancel"] == {"hidden": True, "applied": 0, "flushed": 0}
    assert result["applied"] == 1
    assert result["flushed"] == 1
    assert result["status"] == "Week Ahead reflowed and saved."
    assert result["reflowHidden"] is True


def test_week_overview_confirms_and_starts_the_next_day_early_from_empty_today():
    result = run_controller("""
const createWorkflow=createWeekPlan.Workflow;
const dates=['2026-08-21','2026-08-22','2026-08-23','2026-08-24','2026-08-25','2026-08-26','2026-08-27'];
const elements=new Map();
const makeElement=()=>({hidden:false,textContent:'',disabled:false,innerHTML:'',addEventListener:()=>{},focus:()=>{},querySelectorAll:()=>[],querySelector:()=>null,scrollIntoView:()=>{}});
const root=makeElement();
Object.defineProperty(root,'innerHTML',{set(value){this.value=value;this.startButtons=[...value.matchAll(/data-week-start-early=\"([^\"]+)/g)].map(match=>({dataset:{weekStartEarly:match[1]},listeners:{},disabled:false,addEventListener(name,listener){this.listeners[name]=listener;}}));},get(){return this.value||'';}});
root.querySelectorAll=selector=>selector==='[data-week-start-early]'?root.startButtons||[]:[];
elements.set('#week-review-days',root);
elements.set('#week-plan-dates',makeElement());
for(const selector of ['#mobile-week-summary','#my-work-action-status','#week-plan-progress','#save-today-plan','#week-review','#week-review-duplicates','#confirm-week-plan','#week-review-status','#back-to-week-review','#edit-week-plan','#week-offline-snapshot','#retry-week-live','#open-week-capacity-import'])if(!elements.has(selector))elements.set(selector,makeElement());
const day={plan_date:'2026-08-21',label:'Friday, Aug 21',ids:['one','two'],capacity_minutes:90,estimates:{one:30,two:45},planned_minutes:75,overloaded:false};
const review={days:dates.map((date,index)=>index===0?day:{plan_date:date,label:'Day '+(index+1),ids:[],capacity_minutes:null,estimates:{},planned_minutes:0,overloaded:false}),duplicates:[],blockers:[],can_confirm:true};
let request=null,confirmation='',replaced=null,refreshed=0;
const controller={dates:()=>dates.map((date,index)=>({date,label:'Day '+(index+1)})),day:date=>review.days.find(value=>value.plan_date===date),pass:()=>({}),load:async()=>({}),summary:()=> 'Nothing planned',review:()=>review,pending:()=>false,offline:()=>false,move:()=>true,flush:async()=>{},startEarly:async(date,revision)=>{request={date,revision};return {ids:['one','two'],capacity_minutes:90,estimates:{one:30,two:45}};}};
const workflow=createWorkflow({controller,qs:selector=>elements.get(selector),openPlanner:()=>{},setReviewMode:()=>{},escapeHtml:value=>value,escapeAttribute:value=>value,today:()=>({revision:7,ids:[]}),confirmEarly:message=>{confirmation=message;return true;},todayWork:{replace:ids=>{replaced=ids;},replacePlanning:()=>{}},refresh:()=>{refreshed+=1;},warm:()=>{}});
await workflow.open({disabled:false});
const button=root.startButtons[0];
await button.listeners.click({currentTarget:button});
console.log(JSON.stringify({buttonCount:root.startButtons.length,markup:root.innerHTML,confirmation,request,replaced,refreshed,status:elements.get('#my-work-action-status').textContent}));
""")

    assert result["buttonCount"] == 1
    assert "Start this day early" in result["markup"]
    assert "Friday, Aug 21" in result["confirmation"]
    assert "2 items · 75 of 90 min" in result["confirmation"]
    assert result["request"] == {"date": "2026-08-21", "revision": 7}
    assert result["replaced"] == ["one", "two"]
    assert result["refreshed"] == 1
    assert result["status"] == "Friday, Aug 21 is now Today."


def test_week_overview_opens_known_work_with_the_originating_control_and_leaves_unknown_work_static():
    result = run_controller("""
const createWorkflow=createWeekPlan.Workflow;
const dates=['2026-08-21','2026-08-22','2026-08-23','2026-08-24','2026-08-25','2026-08-26','2026-08-27'];
const elements=new Map();
const makeElement=()=>({hidden:false,textContent:'',disabled:false,innerHTML:'',addEventListener:()=>{},focus:()=>{},
  querySelectorAll:()=>[],querySelector:()=>null,scrollIntoView:()=>{}});
const dateRoot=makeElement();
Object.defineProperty(dateRoot,'innerHTML',{set(value){this.value=value;},get(){return this.value||'';}});
elements.set('#week-plan-dates',dateRoot);
const reviewDays=makeElement();
Object.defineProperty(reviewDays,'innerHTML',{set(value){
  this.value=value;
  this.openButtons=[...value.matchAll(/data-week-open-item=\"([^\"]+)/g)].map(match=>({
    dataset:{weekOpenItem:match[1]},listeners:{},isConnected:true,focused:false,
    addEventListener(name,listener){this.listeners[name]=listener;},focus(){this.focused=true;}
  }));
},get(){return this.value||'';}});
reviewDays.querySelectorAll=selector=>selector==='[data-week-open-item]'?reviewDays.openButtons||[]:[];
elements.set('#week-review-days',reviewDays);
for(const selector of ['#mobile-week-summary','#my-work-action-status','#week-plan-progress','#save-today-plan',
  '#week-review','#week-review-duplicates','#confirm-week-plan','#week-review-status','#back-to-week-review',
  '#edit-week-plan','#week-offline-snapshot','#retry-week-live','#open-week-capacity-import'])
  if(!elements.has(selector))elements.set(selector,makeElement());
const known={kind:'issue',title:'Inspect planned work',repository:'stackchain/dashboard',number:42};
const review={days:dates.map((date,index)=>({plan_date:date,label:'Day '+(index+1),
  ids:index===0?['known','missing']:[],capacity_minutes:90,
  estimates:index===0?{known:30,missing:20}:{},planned_minutes:index===0?50:0,overloaded:false})),
  duplicates:[],blockers:[],can_confirm:true};
let opened=null,writes=0;
const controller={dates:()=>dates.map((date,index)=>({date,label:'Day '+(index+1)})),day:date=>review.days.find(day=>day.plan_date===date),
  pass:()=>({}),load:async()=>({}),summary:()=> '2 items across 1 day',review:()=>review,pending:()=>false,offline:()=>false,
  move:()=>true,flush:async()=>{writes+=1;}};
const workflow=createWorkflow({controller,qs:selector=>elements.get(selector),getLogin:()=> 'timmy',
  getItem:id=>id==='known'?known:null,openItem:(item,trigger)=>{opened={item,sameTrigger:trigger===reviewDays.openButtons[0]};},
  openPlanner:()=>{},setReviewMode:()=>{},escapeHtml:value=>value,escapeAttribute:value=>value,
  todayWork:{replace:()=>{},replacePlanning:()=>{}},refresh:()=>{},warm:()=>{}});
await workflow.open({disabled:false});
const button=reviewDays.openButtons[0];
if(button)button.listeners.click({currentTarget:button});
console.log(JSON.stringify({markup:reviewDays.innerHTML,buttonCount:reviewDays.openButtons.length,opened,writes}));
""")

    assert result["buttonCount"] == 1
    assert 'class="week-review-item-open"' in result["markup"]
    assert 'data-week-open-item="known"' in result["markup"]
    assert 'data-week-open-item="missing"' not in result["markup"]
    assert result["opened"] == {
        "item": {
            "kind": "issue",
            "title": "Inspect planned work",
            "repository": "stackchain/dashboard",
            "number": 42,
        },
        "sameTrigger": True,
    }
    assert result["writes"] == 0


def test_dashboard_routes_week_overview_controls_through_existing_detail_flow_with_mobile_targets():
    dashboard = (FRONTEND / "dashboard.js").read_text()
    css = (FRONTEND / "dashboard.css").read_text()

    workflow_mount = dashboard.split("const weekFlow=createWeekPlanWorkflow({", 1)[1].split("});", 1)[0]
    assert "openItem:openRoutedWork" in workflow_mount
    assert "t:v=>v?latestTodayPlan=v:latestTodayPlan" in workflow_mount

    assert ".week-review-item-open" in css
    rule = css.split(".week-review-item-open", 1)[1].split("}", 1)[0]
    assert "min-height:44px" in rule
    assert "width:100%" in rule
    assert ".week-start-early" in css
    start_rule = css.split(".week-start-early", 1)[1].split("}", 1)[0]
    assert "min-height:44px" in start_rule
    assert "width:100%" in start_rule
    assert ".week-review-pull" in css
    pull_rule = css.split(".week-review-pull", 1)[1].split("}", 1)[0]
    assert "min-height:44px" in pull_rule
    assert "width:100%" in pull_rule


def test_dashboard_restores_and_retries_a_saved_week_to_today_pull_on_mobile_lifecycle():
    dashboard = (FRONTEND / "dashboard.js").read_text()
    week_plan = (FRONTEND / "week-plan.js").read_text()

    recovery = dashboard.split("  function syncPendingTomorrow()", 1)[1].split("\n  }", 1)[0]
    assert "weekFlow.resumePull()" in recovery
    assert "weekFlow.flushPull()" in recovery

    lifecycle = dashboard.split("tomorrowPlan.startLifecycle({", 1)[1].split("  });", 1)[0]
    assert "syncPendingTomorrow()" in lifecycle
    assert dashboard.count("syncPendingTomorrow();") >= 2
    assert "result.sync_pending?String(item?.title||'Work')+' added to Today · sync pending.'" in week_plan


def test_successful_issue_close_notifies_week_ahead_only_after_upstream_confirmation():
    issue_sheet = (FRONTEND / "issue-sheet.js").read_text()
    week_plan = (FRONTEND / "week-plan.js").read_text()
    close_method = issue_sheet.split("    close(item) {", 1)[1].split("    release(item)", 1)[0]

    confirmed = close_method.index("result?.state !== 'closed'")
    notified = close_method.index("stackchain:issue-closed")
    assert confirmed < notified
    assert "detail:item" in close_method
    assert "addEventListener?.('stackchain:issue-closed'" in week_plan
    assert "retire(event.detail)" in week_plan


def test_week_workflow_refreshes_the_open_overview_and_reports_pending_retirement():
    result = run_controller("""
const createWorkflow=createWeekPlan.Workflow;
const dates=['2026-08-21','2026-08-22','2026-08-23','2026-08-24','2026-08-25','2026-08-26','2026-08-27'];
const elements=new Map();
const makeElement=()=>({hidden:false,textContent:'',disabled:false,innerHTML:'',dataset:{},focused:false,
  addEventListener:()=>{},focus(){this.focused=true;},querySelectorAll:()=>[],querySelector:()=>null,scrollIntoView:()=>{}});
elements.set('#week-plan-dates',makeElement());
const reviewDays=makeElement();elements.set('#week-review-days',reviewDays);
for(const selector of ['#mobile-week-summary','#my-work-action-status','#week-plan-progress','#save-today-plan','#week-review',
  '#week-review-duplicates','#confirm-week-plan','#week-review-status','#back-to-week-review','#edit-week-plan',
  '#week-offline-snapshot','#retry-week-live','#open-week-capacity-import','#open-week-reflow','#week-reflow-review'])
  if(!elements.has(selector))elements.set(selector,makeElement());
let ids=['completed','sibling'];
const review=()=>({days:dates.map((date,index)=>({plan_date:date,label:'Day '+(index+1),ids:index===0?[...ids]:[],
  capacity_minutes:90,estimates:index===0?Object.fromEntries(ids.map(id=>[id,id==='completed'?30:45])):{},
  planned_minutes:index===0?ids.reduce((sum,id)=>sum+(id==='completed'?30:45),0):0,overloaded:false})),
  duplicates:[],blockers:[],can_confirm:true});
const controller={dates:()=>dates.map((date,index)=>({date,label:'Day '+(index+1)})),day:date=>review().days.find(day=>day.plan_date===date),
  load:async()=>({}),summary:()=>ids.length+' items across 1 day',review,pending:()=>false,offline:()=>false,
  retire:id=>{ids=ids.filter(value=>value!==id);return {removed:1,sync_pending:true};},
  flush:async()=>{throw new Error('offline');}};
let identityKind=null;
const workflow=createWorkflow({controller,qs:selector=>elements.get(selector),
  getItem:id=>({kind:'issue',title:id==='completed'?'Completed work':'Sibling work',repository:'r',number:id==='completed'?1:2}),
  openPlanner:()=>{},setReviewMode:()=>{},escapeHtml:value=>value,escapeAttribute:value=>value,
  todayWork:{identity:item=>{identityKind=item.kind;return item.id;},replace:()=>{},replacePlanning:()=>{}},refresh:()=>{},warm:()=>{}});
await workflow.open({disabled:false});
const outcome=await workflow.retire({id:'completed',title:'Completed work'});
console.log(JSON.stringify({outcome,identityKind,markup:reviewDays.innerHTML,status:elements.get('#week-review-status').textContent,
  editFocused:elements.get('#edit-week-plan').focused}));
""")

    assert result["outcome"] == "pending"
    assert result["identityKind"] == "issue"
    assert "Completed work" not in result["markup"]
    assert "Sibling work" in result["markup"]
    assert result["status"] == "Completed work removed · sync pending."
    assert result["editFocused"] is True


def test_week_workflow_marks_a_confirmed_fallback_read_only_and_retries_live_data():
    result = run_controller("""
const createWorkflow=createWeekPlan.Workflow;
const dates=['2026-08-21','2026-08-22','2026-08-23','2026-08-24','2026-08-25','2026-08-26','2026-08-27'];
const elements=new Map();
const makeElement=()=>({hidden:false,textContent:'',disabled:false,innerHTML:'',dataset:{},listeners:{},
  addEventListener(name,listener){this.listeners[name]=listener;},focus:()=>{},querySelectorAll:()=>[],querySelector:()=>null});
const dateRoot=makeElement();Object.defineProperty(dateRoot,'innerHTML',{set(value){this.value=value;this.buttons=[];},get(){return this.value||'';}});
elements.set('#week-plan-dates',dateRoot);
const reviewDays=makeElement();Object.defineProperty(reviewDays,'innerHTML',{set(value){this.value=value;this.editButtons=[...value.matchAll(/data-week-edit-day/g)];},get(){return this.value||'';}});
elements.set('#week-review-days',reviewDays);
for(const selector of ['#mobile-week-summary','#my-work-action-status','#week-plan-progress','#save-today-plan','#week-review',
  '#week-review-duplicates','#confirm-week-plan','#week-review-status','#back-to-week-review','#edit-week-plan',
  '#week-offline-snapshot','#retry-week-live','#open-week-capacity-import']) if(!elements.has(selector))elements.set(selector,makeElement());
let offline=true,loads=0,remembered=null;
const review={days:dates.map((date,index)=>({plan_date:date,label:'Day '+(index+1),ids:index===0?['issue:r:42:']:[],
  capacity_minutes:60,estimates:index===0?{'issue:r:42:':30}:{},planned_minutes:index===0?30:0,overloaded:false})),
  duplicates:[],blockers:[],can_confirm:true};
const controller={dates:()=>dates.map((date,index)=>({date,label:'Day '+(index+1)})),day:date=>review.days.find(day=>day.plan_date===date),
  pass:()=>({}),load:async()=>{loads+=1;const value={offline_snapshot:offline,refreshed_at:'2026-08-20T12:34:00.000Z'};offline=false;return value;},
  summary:()=> '1 item across 1 day',review:()=>review,pending:()=>false,offline:()=>loads===1,
  item:id=>id?{kind:'issue',title:'Cached work title',repository:'r',number:42}:null,
  rememberItems:value=>{remembered=value;},move:()=>true,flush:async()=>({})};
const workflow=createWorkflow({controller,qs:selector=>elements.get(selector),getLogin:()=> 'timmy',getItem:()=>null,
  openPlanner:()=>{},setReviewMode:()=>{},escapeHtml:value=>value,escapeAttribute:value=>value,
  todayWork:{replace:()=>{},replacePlanning:()=>{}},refresh:()=>{},warm:()=>{}});
await workflow.open({disabled:false});
const fallback={noticeHidden:elements.get('#week-offline-snapshot').hidden,
  notice:elements.get('#week-offline-snapshot').textContent,markup:reviewDays.innerHTML,
  editWeekHidden:elements.get('#edit-week-plan').hidden,capacityHidden:elements.get('#open-week-capacity-import').hidden};
await elements.get('#retry-week-live').listeners.click({currentTarget:elements.get('#retry-week-live')});
console.log(JSON.stringify({fallback,loads,live:{noticeHidden:elements.get('#week-offline-snapshot').hidden,
  editWeekHidden:elements.get('#edit-week-plan').hidden,capacityHidden:elements.get('#open-week-capacity-import').hidden},remembered}));
""")

    assert result["fallback"]["noticeHidden"] is False
    assert "Offline snapshot" in result["fallback"]["notice"]
    assert "Aug" in result["fallback"]["notice"]
    assert "Cached work title" in result["fallback"]["markup"]
    assert "data-week-edit-day" not in result["fallback"]["markup"]
    assert result["fallback"]["editWeekHidden"] is True
    assert result["fallback"]["capacityHidden"] is True
    assert result["loads"] == 2
    assert result["live"] == {"noticeHidden": True, "editWeekHidden": False, "capacityHidden": False}
    assert result["remembered"] == {}


def test_week_workflow_opens_review_after_day_seven_and_blocks_duplicate_confirmation():
    result = run_controller("""
const createWorkflow=createWeekPlan.Workflow;
const dates=['2026-08-21','2026-08-22','2026-08-23','2026-08-24','2026-08-25','2026-08-26','2026-08-27'];
const elements=new Map();
const makeElement=()=>({hidden:false,textContent:'',disabled:false,innerHTML:'',addEventListener:()=>{},focus:()=>{},
  querySelectorAll:()=>[],querySelector:()=>null,scrollIntoView:()=>{}});
const root=makeElement();
Object.defineProperty(root,'innerHTML',{set(value){this.value=value;this.buttons=[];},get(){return this.value||'';}});
elements.set('#week-plan-dates',root);
for(const selector of ['#mobile-week-summary','#my-work-action-status','#week-plan-progress','#save-today-plan',
  '#week-review','#week-review-days','#week-review-duplicates','#confirm-week-plan','#week-review-status','#edit-week-plan']) elements.set(selector,makeElement());
let reviewMode=false;
const review={days:dates.map((date,index)=>({plan_date:date,label:'Day '+(index+1),ids:index<2?['shared']:[],
  capacity_minutes:index===0?30:60,planned_minutes:index===0?45:15,overloaded:index===0})),
  duplicates:[{id:'shared',dates:dates.slice(0,2)}],can_confirm:false};
const controller={dates:()=>dates.map((date,index)=>({date,label:'Day '+(index+1)})),day:date=>review.days.find(day=>day.plan_date===date),
  pass:date=>{const index=dates.indexOf(date);return {position:index+1,total:7,planned:2,next_date:dates[index+1]||null,last:index===6};},
  load:async()=>({}),summary:()=> '2 items across 2 days',review:()=>review,move:()=>true,flush:async()=>({})};
const workflow=createWorkflow({controller,qs:selector=>elements.get(selector),getLogin:()=> 'timmy',
  openPlanner:()=>{},setReviewMode:value=>{reviewMode=value;},escapeHtml:value=>value,escapeAttribute:value=>value,
  todayWork:{replace:()=>{},replacePlanning:()=>{}},refresh:()=>{},warm:()=>{}});
await workflow.open({disabled:false});
workflow.editWeek();
for(let index=0;index<6;index++) workflow.advance();
const advanced=workflow.advance();
console.log(JSON.stringify({advanced,reviewing:workflow.reviewing(),reviewMode,
  reviewHidden:elements.get('#week-review').hidden,progress:elements.get('#week-plan-progress').textContent,
  confirmDisabled:elements.get('#confirm-week-plan').disabled,days:elements.get('#week-review-days').innerHTML,
  duplicates:elements.get('#week-review-duplicates').innerHTML}));
""")

    assert result["advanced"] is True
    assert result["reviewing"] is True
    assert result["reviewMode"] is True
    assert result["reviewHidden"] is False
    assert result["progress"] == "Review week · 2 planned"
    assert result["confirmDisabled"] is True
    assert "45 of 30 min · over capacity" in result["days"]
    assert "shared" in result["duplicates"]


def test_week_review_names_work_and_can_edit_a_day_then_return_to_review():
    result = run_controller("""
const createWorkflow=createWeekPlan.Workflow;
const dates=['2026-08-21','2026-08-22','2026-08-23','2026-08-24','2026-08-25','2026-08-26','2026-08-27'];
const elements=new Map();
const makeElement=()=>({hidden:false,textContent:'',disabled:false,innerHTML:'',dataset:{},listeners:{},
  addEventListener(name,listener){this.listeners[name]=listener;},focus(){this.focused=true;},
  querySelectorAll:()=>[],querySelector:()=>null,scrollIntoView:()=>{}});
const dateRoot=makeElement();
Object.defineProperty(dateRoot,'innerHTML',{set(value){this.value=value;this.buttons=[...value.matchAll(/data-week-plan-date=\"([^\"]+)/g)].map(match=>({
  dataset:{weekPlanDate:match[1]},addEventListener:()=>{},scrollIntoView:()=>{},focus:()=>{}
}));},get(){return this.value||'';}});
dateRoot.querySelectorAll=()=>dateRoot.buttons||[];
dateRoot.querySelector=selector=>(dateRoot.buttons||[]).find(button=>selector.includes(button.dataset.weekPlanDate))||null;
elements.set('#week-plan-dates',dateRoot);
const reviewDays=makeElement();
Object.defineProperty(reviewDays,'innerHTML',{set(value){
  this.value=value;this.editButtons=[...value.matchAll(/data-week-edit-day=\"([^\"]+)/g)].map(match=>{
    const button=makeElement();button.dataset.weekEditDay=match[1];return button;
  });
},get(){return this.value||'';}});
reviewDays.querySelectorAll=selector=>selector==='[data-week-edit-day]'?reviewDays.editButtons||[]:[];
reviewDays.querySelector=selector=>(reviewDays.editButtons||[]).find(button=>selector.includes(button.dataset.weekEditDay))||null;
elements.set('#week-review-days',reviewDays);
for(const selector of ['#mobile-week-summary','#my-work-action-status','#week-plan-progress','#save-today-plan',
  '#week-review','#week-review-duplicates','#confirm-week-plan','#week-review-status','#back-to-week-review']) {
  if(!elements.has(selector))elements.set(selector,makeElement());
}
let reviewMode=false;
const review={days:dates.map((date,index)=>({plan_date:date,label:'Day '+(index+1),
  ids:index===0?['issue:stackchain/dashboard:42:']:[],capacity_minutes:60,
  estimates:index===0?{'issue:stackchain/dashboard:42:':30}:{},planned_minutes:index===0?30:0,overloaded:false})),
  duplicates:[],can_confirm:true};
const controller={dates:()=>dates.map((date,index)=>({date,label:'Day '+(index+1)})),day:date=>review.days.find(day=>day.plan_date===date),
  pass:date=>{const index=dates.indexOf(date);return {position:index+1,total:7,planned:1,next_date:dates[index+1]||null,last:index===6};},
  load:async()=>({}),summary:()=> '1 item across 1 day',review:()=>review,pending:()=>false,move:()=>true,flush:async()=>({})};
const workflow=createWorkflow({controller,qs:selector=>elements.get(selector),getLogin:()=> 'timmy',
  getItem:id=>id==='issue:stackchain/dashboard:42:'?{kind:'issue',title:'Fix mobile sign in',repository:'stackchain/dashboard',number:42}:null,
  openPlanner:()=>{},setReviewMode:value=>{reviewMode=value;},escapeHtml:value=>value,escapeAttribute:value=>value,
  todayWork:{replace:()=>{},replacePlanning:()=>{}},refresh:()=>{},warm:()=>{}});
await workflow.open({disabled:false});
for(let index=0;index<7;index++)workflow.advance();
const markup=reviewDays.innerHTML;
const edit=reviewDays.editButtons[0];edit.listeners.click();
const whileEditing={selected:workflow.selectedDate(),reviewing:workflow.reviewing(),reviewMode,
  reviewHidden:elements.get('#week-review').hidden,backHidden:elements.get('#back-to-week-review').hidden};
elements.get('#back-to-week-review').listeners.click();
console.log(JSON.stringify({markup,whileEditing,after:{selected:workflow.selectedDate(),reviewing:workflow.reviewing(),reviewMode,
  reviewHidden:elements.get('#week-review').hidden,editFocused:reviewDays.editButtons[0].focused}}));
""")

    assert "Fix mobile sign in" in result["markup"]
    assert "stackchain/dashboard #42 · Issue · 30 min" in result["markup"]
    assert "<code>issue:stackchain/dashboard:42:</code>" not in result["markup"]
    assert result["whileEditing"] == {
        "selected": "2026-08-21", "reviewing": False, "reviewMode": False,
        "reviewHidden": True, "backHidden": False,
    }
    assert result["after"] == {
        "selected": None, "reviewing": True, "reviewMode": True,
        "reviewHidden": False, "editFocused": True,
    }


def test_week_controller_preserves_both_versions_when_another_device_changes_the_week():
    result = run_controller("""
let calls=0;
const remote={revision:8,timezone:'UTC',days:[{plan_date:'2026-08-22',ids:['server'],capacity_minutes:60,estimates:{}}]};
const fetchJson=async(_url,options={})=>{
 calls+=1;
 if(options.method==='PUT'){const error=new Error('changed');error.status=409;throw error;}
 return calls===1?{revision:7,timezone:'UTC',days:[]}:remote;
};
const week=createWeekPlan({fetchJson,localDate:()=> '2026-08-20',timeZone:()=> 'UTC'});
await week.load();
try { await week.saveDay('2026-08-21',{ids:['phone'],capacity_minutes:90,estimates:{}}); } catch(_error) {}
console.log(JSON.stringify({conflict:week.conflict(),state:week.state()}));
""")

    assert result["conflict"]["local"]["days"][0]["ids"] == ["phone"]
    assert result["conflict"]["remote"]["days"][0]["ids"] == ["server"]
    assert result["state"]["revision"] == 7


def test_week_controller_automatically_merges_changes_made_to_different_days():
    result = run_controller("""
const values=new Map();
const storage={getItem:key=>values.get(key)||null,setItem:(key,value)=>values.set(key,value),removeItem:key=>values.delete(key)};
const requests=[];let puts=0;
const base={revision:7,timezone:'UTC',days:[
 {plan_date:'2026-08-21',ids:['tuesday-base'],capacity_minutes:60,estimates:{'tuesday-base':30}},
 {plan_date:'2026-08-22',ids:['friday-base'],capacity_minutes:90,estimates:{'friday-base':45}}
]};
const remote={revision:8,timezone:'UTC',days:[
 {plan_date:'2026-08-21',ids:['tuesday-account'],capacity_minutes:60,estimates:{'tuesday-account':30}},
 {plan_date:'2026-08-22',ids:['friday-base'],capacity_minutes:90,estimates:{'friday-base':45}}
]};
const fetchJson=async(_url,options={})=>{
 const body=options.body?JSON.parse(options.body):null;requests.push({method:options.method||'GET',body});
 if(options.method==='PUT'&&puts++===0){const error=new Error('changed');error.status=409;throw error;}
 if(options.method==='PUT')return {revision:9,timezone:body.timezone,days:body.days};
 return remote;
};
const week=createWeekPlan({storage,getLogin:()=> 'timmy',fetchJson,localDate:()=> '2026-08-20',timeZone:()=> 'UTC'});
week.adopt(base);
week.stageDay('2026-08-22',{ids:['friday-phone'],capacity_minutes:90,estimates:{'friday-phone':50}});
const saved=await week.flush();
console.log(JSON.stringify({saved,requests,pending:week.pending(),conflict:week.conflict()}));
""")

    assert [request["method"] for request in result["requests"]] == ["PUT", "GET", "PUT"]
    assert result["requests"][2]["body"]["base_revision"] == 8
    assert result["saved"]["days"] == [
        {"plan_date": "2026-08-21", "ids": ["tuesday-account"], "capacity_minutes": 60,
         "estimates": {"tuesday-account": 30}},
        {"plan_date": "2026-08-22", "ids": ["friday-phone"], "capacity_minutes": 90,
         "estimates": {"friday-phone": 50}},
    ]
    assert result["pending"] is False
    assert result["conflict"] is None


def test_week_controller_resolves_only_the_same_day_conflict_and_keeps_other_account_changes():
    result = run_controller("""
const values=new Map();
const storage={getItem:key=>values.get(key)||null,setItem:(key,value)=>values.set(key,value),removeItem:key=>values.delete(key)};
const requests=[];let puts=0;
const base={revision:4,timezone:'UTC',days:[
 {plan_date:'2026-08-21',ids:['same-base'],capacity_minutes:60,estimates:{}},
 {plan_date:'2026-08-22',ids:['other-base'],capacity_minutes:60,estimates:{}}
]};
const remote={revision:5,timezone:'UTC',days:[
 {plan_date:'2026-08-21',ids:['same-account'],capacity_minutes:60,estimates:{}},
 {plan_date:'2026-08-22',ids:['other-account'],capacity_minutes:60,estimates:{}}
]};
const fetchJson=async(_url,options={})=>{
 const body=options.body?JSON.parse(options.body):null;requests.push({method:options.method||'GET',body});
 if(options.method==='PUT'&&puts++===0){const error=new Error('changed');error.status=409;throw error;}
 if(options.method==='PUT')return {revision:6,timezone:body.timezone,days:body.days};
 return remote;
};
const week=createWeekPlan({storage,getLogin:()=> 'timmy',fetchJson,localDate:()=> '2026-08-20',timeZone:()=> 'UTC'});
week.adopt(base);
week.stageDay('2026-08-21',{ids:['same-phone'],capacity_minutes:90,estimates:{}});
try { await week.flush(); } catch(_error) {}
const before=week.conflict();
week.chooseDay('2026-08-21','phone');
const selected=JSON.parse(values.get('stackchain.week-sync.v1.timmy'));
const saved=await week.saveMerged();
console.log(JSON.stringify({before,selected,saved,requests,pending:week.pending(),conflict:week.conflict()}));
""")

    assert [item["plan_date"] for item in result["before"]["conflicts"]] == ["2026-08-21"]
    assert result["before"]["conflicts"][0]["local"]["ids"] == ["same-phone"]
    assert result["before"]["conflicts"][0]["remote"]["ids"] == ["same-account"]
    assert result["selected"]["resolutions"] == {"2026-08-21": "phone"}
    assert result["requests"][2]["body"]["base_revision"] == 5
    assert [day["ids"] for day in result["saved"]["days"]] == [["same-phone"], ["other-account"]]
    assert result["pending"] is False
    assert result["conflict"] is None


def test_week_controller_restores_an_account_bound_week_before_network_delivery():
    result = run_controller("""
const values=new Map();
const storage={getItem:key=>values.get(key)||null,setItem:(key,value)=>values.set(key,value),removeItem:key=>values.delete(key)};
let login='Timmy';
let requests=0;
const options={storage,getLogin:()=>login,fetchJson:async()=>{requests+=1;throw new Error('offline');},
  localDate:()=> '2026-08-20',timeZone:()=> 'America/New_York'};
const first=createWeekPlan(options);
first.adopt({revision:7,timezone:'UTC',days:[{plan_date:'2026-08-21',ids:['old'],capacity_minutes:30,estimates:{old:15}}]});
const queued=first.stageDay('2026-08-22',{ids:['issue:r:3:','issue:r:2:'],capacity_minutes:120,
  estimates:{'issue:r:3:':45,'issue:r:2:':30}});
const restored=await createWeekPlan(options).load();
login='alexander';
let otherError='';
try { await createWeekPlan(options).load(); } catch(error) { otherError=error.message; }
console.log(JSON.stringify({queued,restored,requests,otherError,keys:[...values.keys()]}));
""")

    assert result["queued"]["sync_pending"] is True
    assert result["restored"] == result["queued"]
    assert result["restored"]["timezone"] == "America/New_York"
    assert [day["plan_date"] for day in result["restored"]["days"]] == ["2026-08-21", "2026-08-22"]
    assert result["requests"] == 1
    assert result["otherError"] == "offline"
    assert result["keys"] == ["stackchain.week-sync.v1.timmy"]


def test_week_controller_falls_back_to_an_account_bound_confirmed_read_snapshot():
    result = run_controller("""
const values=new Map();
const storage={getItem:key=>values.get(key)||null,setItem:(key,value)=>values.set(key,value),removeItem:key=>values.delete(key)};
let login='Timmy',online=true,revision=7;
const options={storage,getLogin:()=>login,now:()=>Date.parse('2026-08-20T12:34:00Z'),
  fetchJson:async()=>{if(!online)throw new Error('offline');return {revision,timezone:'UTC',days:[
    {plan_date:'2026-08-21',ids:['issue:r:42:'],capacity_minutes:60,estimates:{'issue:r:42:':30}}
  ]};},localDate:()=> '2026-08-20',timeZone:()=> 'UTC'};
const live=createWeekPlan(options);
const loaded=await live.load();
live.rememberItems({'issue:r:42:':{kind:'issue',title:'Ship offline overview',repository:'r',number:42,secret:'drop'}});
online=false;
const fallback=createWeekPlan(options);
const restored=await fallback.load();
login='alexander';
let otherError='';
try { await createWeekPlan(options).load(); } catch(error) { otherError=error.message; }
login='timmy';online=true;revision=8;
const refreshed=await fallback.load();
console.log(JSON.stringify({loaded,restored,item:fallback.item('issue:r:42:'),otherError,refreshed,
  keys:[...values.keys()],snapshot:JSON.parse(values.get('stackchain.week-confirmed.v1.timmy'))}));
""")

    assert result["loaded"]["offline_snapshot"] is False
    assert result["restored"]["offline_snapshot"] is True
    assert result["restored"]["refreshed_at"] == "2026-08-20T12:34:00.000Z"
    assert result["restored"]["days"][0]["ids"] == ["issue:r:42:"]
    assert result["item"] == {
        "kind": "issue", "title": "Ship offline overview", "repository": "r", "number": 42,
    }
    assert result["otherError"] == "offline"
    assert result["refreshed"]["revision"] == 8
    assert result["refreshed"]["offline_snapshot"] is False
    assert result["keys"] == ["stackchain.week-confirmed.v1.timmy"]
    assert "secret" not in result["snapshot"]["items"]["issue:r:42:"]


def test_week_controller_promotes_bounded_pending_work_details_after_matching_receipt():
    result = run_controller("""
const values=new Map();
const storage={getItem:key=>values.get(key)||null,setItem:(key,value)=>values.set(key,value),removeItem:key=>values.delete(key)};
let online=true;const requests=[];
const options={storage,getLogin:()=> 'timmy',now:()=>Date.parse('2026-08-20T12:34:00Z'),
  fetchJson:async(_url,request={})=>{
    requests.push(request.body?JSON.parse(request.body):null);
    if(!online)throw new Error('offline');
    if(request.method==='PUT')return {revision:8,timezone:'UTC',days:JSON.parse(request.body).days};
    return {revision:7,timezone:'UTC',days:[]};
  },localDate:()=> '2026-08-20',timeZone:()=> 'UTC'};
const live=createWeekPlan(options);await live.load();
live.place('issue:r:42:','2026-08-21',45);
const admitted=live.rememberPendingItem('issue:r:42:',{kind:'issue',title:'T'.repeat(600),repository:'r'.repeat(300),
  number:42,body:'private body',token:'drop'});
const queued=live.pending();
await live.flush();
const confirmed=JSON.parse(values.get('stackchain.week-confirmed.v1.timmy'));
online=false;const offline=createWeekPlan(options);const restored=await offline.load();
console.log(JSON.stringify({admitted,queued,confirmed,restored,item:offline.item('issue:r:42:'),requests}));
""")

    assert result["admitted"] is True
    assert result["queued"]["items"]["issue:r:42:"]["title"] == "T" * 500
    assert len(result["queued"]["items"]["issue:r:42:"]["repository"]) == 255
    assert result["requests"][1].keys() == {"base_revision", "timezone", "days"}
    assert result["confirmed"]["week"]["revision"] == 8
    assert result["confirmed"]["items"]["issue:r:42:"] == result["item"]
    assert result["item"]["number"] == 42
    assert "body" not in result["item"] and "token" not in result["item"]
    assert result["restored"]["offline_snapshot"] is True


def test_week_controller_keeps_pending_work_details_across_a_cold_offline_reload():
    result = run_controller("""
const values=new Map();
const storage={getItem:key=>values.get(key)||null,setItem:(key,value)=>values.set(key,value),removeItem:key=>values.delete(key)};
const options={storage,getLogin:()=> 'timmy',fetchJson:async()=>{throw new Error('offline');},
  localDate:()=> '2026-08-20',timeZone:()=> 'UTC'};
const first=createWeekPlan(options);first.adopt({revision:7,timezone:'UTC',days:[]});
first.place('issue:r:42:','2026-08-21',45);
first.rememberPendingItem('issue:r:42:',{kind:'issue',title:'Pending work',repository:'r',number:42});
const restored=createWeekPlan(options);const state=await restored.load();
console.log(JSON.stringify({state,item:restored.item('issue:r:42:'),pending:restored.pending()}));
""")

    assert result["state"]["sync_pending"] is True
    assert result["item"] == {"kind": "issue", "title": "Pending work", "repository": "r", "number": 42}
    assert result["pending"]["items"]["issue:r:42:"] == result["item"]


def test_week_controller_does_not_use_a_confirmed_snapshot_after_authentication_fails():
    result = run_controller("""
const values=new Map();
const storage={getItem:key=>values.get(key)||null,setItem:(key,value)=>values.set(key,value),removeItem:key=>values.delete(key)};
let denied=false;
const options={storage,getLogin:()=> 'timmy',now:()=>1,localDate:()=> '2026-08-20',timeZone:()=> 'UTC',fetchJson:async()=>{
  if(denied){const error=new Error('sign in');error.status=401;throw error;}
  return {revision:7,timezone:'UTC',days:[]};}};
await createWeekPlan(options).load();denied=true;
let status=0;
try { await createWeekPlan(options).load(); } catch(error) { status=error.status; }
console.log(JSON.stringify({status}));
""")

    assert result["status"] == 401


def test_week_controller_never_mutates_an_offline_confirmed_snapshot():
    result = run_controller("""
const values=new Map();
const storage={getItem:key=>values.get(key)||null,setItem:(key,value)=>values.set(key,value),removeItem:key=>values.delete(key)};
let online=true,requests=[];
const options={storage,getLogin:()=> 'timmy',now:()=>1,localDate:()=> '2026-08-20',timeZone:()=> 'UTC',
  fetchJson:async(url,request={})=>{requests.push(request.method||'GET');if(!online)throw new Error('offline');return {
    revision:7,timezone:'UTC',days:[{plan_date:'2026-08-21',ids:['one'],capacity_minutes:60,estimates:{one:30}}]};}};
await createWeekPlan(options).load();online=false;
const week=createWeekPlan(options);await week.load();
const staged=week.stageDay('2026-08-22',{ids:['two'],capacity_minutes:60,estimates:{two:30}});
let saved=await week.saveDay('2026-08-22',{ids:['two'],capacity_minutes:60,estimates:{two:30}});
const promoted=await week.promote(3);
console.log(JSON.stringify({staged,saved,promoted,requests,pending:week.pending(),state:week.state()}));
""")

    assert result["staged"] is False
    assert result["saved"] is False
    assert result["promoted"] is False
    assert result["requests"] == ["GET", "GET"]
    assert result["pending"] is False
    assert result["state"]["days"][0]["ids"] == ["one"]


def test_week_controller_refreshes_the_confirmed_snapshot_after_a_successful_save():
    result = run_controller("""
const values=new Map();
const storage={getItem:key=>values.get(key)||null,setItem:(key,value)=>values.set(key,value),removeItem:key=>values.delete(key)};
let tick=0;
const fetchJson=async(_url,options={})=>options.method==='PUT'?{revision:8,timezone:'UTC',days:JSON.parse(options.body).days}:
  {revision:7,timezone:'UTC',days:[{plan_date:'2026-08-21',ids:['one'],capacity_minutes:60,estimates:{one:30}}]};
const week=createWeekPlan({storage,getLogin:()=> 'timmy',now:()=>++tick,fetchJson,
  localDate:()=> '2026-08-20',timeZone:()=> 'UTC'});
await week.load();week.rememberItems({one:{kind:'issue',title:'One',repository:'r',number:1}});
week.stageDay('2026-08-22',{ids:['two'],capacity_minutes:90,estimates:{two:45}});
await week.flush();
console.log(values.get('stackchain.week-confirmed.v1.timmy'));
""")

    assert result["week"]["revision"] == 8
    assert [day["ids"] for day in result["week"]["days"]] == [["one"], ["two"]]
    assert result["items"]["one"]["title"] == "One"


def test_week_controller_flushes_once_and_removes_only_the_matching_pending_week():
    result = run_controller("""
const values=new Map();
const storage={getItem:key=>values.get(key)||null,setItem:(key,value)=>values.set(key,value),removeItem:key=>values.delete(key)};
let release;
let requests=0;
const fetchJson=async(_url,options={})=>{
 requests+=1;const body=JSON.parse(options.body);
 await new Promise(resolve=>{release=resolve});
 return {revision:9,timezone:body.timezone,days:body.days};
};
const week=createWeekPlan({storage,getLogin:()=> 'timmy',fetchJson,localDate:()=> '2026-08-20',timeZone:()=> 'UTC'});
week.adopt({revision:8,timezone:'UTC',days:[]});
week.stageDay('2026-08-21',{ids:['phone'],capacity_minutes:60,estimates:{phone:30}});
const first=week.flush(),second=week.flush();
await Promise.resolve();
const during=week.pending();
release();
const [saved,reused]=await Promise.all([first,second]);
console.log(JSON.stringify({requests,during,saved,reused,pendingAfter:week.pending(),state:week.state()}));
""")

    assert result["requests"] == 1
    assert result["during"]["days"][0]["ids"] == ["phone"]
    assert result["saved"]["revision"] == 9
    assert result["reused"]["revision"] == 9
    assert result["pendingAfter"] is False
    assert result["state"]["revision"] == 9


def test_week_controller_drains_work_staged_while_a_save_is_in_flight():
    result = run_controller("""
const values=new Map();
const storage={getItem:key=>values.get(key)||null,setItem:(key,value)=>values.set(key,value),removeItem:key=>values.delete(key)};
let releaseFirst;const bodies=[];
const fetchJson=async(_url,options={})=>{
 const body=JSON.parse(options.body);bodies.push(body);
 if(bodies.length===1)await new Promise(resolve=>{releaseFirst=resolve});
 return {revision:8+bodies.length,timezone:body.timezone,days:body.days};
};
const week=createWeekPlan({storage,getLogin:()=> 'timmy',fetchJson,localDate:()=> '2026-08-20',timeZone:()=> 'UTC'});
week.adopt({revision:8,timezone:'UTC',days:[]});
week.stageDay('2026-08-21',{ids:['one'],capacity_minutes:60,estimates:{one:30}});
const first=week.flush();await Promise.resolve();
week.stageDay('2026-08-22',{ids:['two'],capacity_minutes:90,estimates:{two:45}});
const second=week.flush();releaseFirst();
const [saved,reused]=await Promise.all([first,second]);
console.log(JSON.stringify({bodies,saved,reused,pending:week.pending(),state:week.state()}));
""")

    assert len(result["bodies"]) == 2
    assert [day["plan_date"] for day in result["bodies"][1]["days"]] == ["2026-08-21", "2026-08-22"]
    assert result["bodies"][1]["base_revision"] == 9
    assert result["saved"]["revision"] == 10
    assert result["reused"]["revision"] == 10
    assert result["pending"] is False
    assert result["state"]["revision"] == 10


def test_week_controller_keeps_phone_week_by_rebasing_onto_the_latest_revision():
    result = run_controller("""
const values=new Map();
const storage={getItem:key=>values.get(key)||null,setItem:(key,value)=>values.set(key,value),removeItem:key=>values.delete(key)};
const requests=[];let puts=0;
const remote={revision:12,timezone:'UTC',days:[{plan_date:'2026-08-21',ids:['server'],capacity_minutes:60,estimates:{}}]};
const fetchJson=async(_url,options={})=>{
 requests.push({method:options.method||'GET',body:options.body?JSON.parse(options.body):null});
 if(options.method==='PUT'&&puts++===0){const error=new Error('changed');error.status=409;throw error;}
 if(options.method==='PUT')return {revision:13,timezone:'UTC',days:JSON.parse(options.body).days};
 return remote;
};
const week=createWeekPlan({storage,getLogin:()=> 'timmy',fetchJson,localDate:()=> '2026-08-20',timeZone:()=> 'UTC'});
week.adopt({revision:11,timezone:'UTC',days:[]});
week.stageDay('2026-08-21',{ids:['phone'],capacity_minutes:90,estimates:{phone:45}});
try { await week.flush(); } catch(_error) {}
const before=week.conflict();
const saved=await week.keepLocal();
console.log(JSON.stringify({before,saved,requests,pending:week.pending(),conflict:week.conflict()}));
""")

    assert result["before"]["local"]["days"][0]["ids"] == ["phone"]
    assert result["before"]["remote"]["days"][0]["ids"] == ["server"]
    assert [request["method"] for request in result["requests"]] == ["PUT", "GET", "PUT"]
    assert result["requests"][2]["body"]["base_revision"] == 12
    assert result["saved"]["revision"] == 13
    assert result["pending"] is False
    assert result["conflict"] is None


def test_week_controller_refreshes_account_week_after_a_second_conflict():
    result = run_controller("""
const values=new Map();
const storage={getItem:key=>values.get(key)||null,setItem:(key,value)=>values.set(key,value),removeItem:key=>values.delete(key)};
let revision=12;
const fetchJson=async(_url,options={})=>{
 if(options.method==='PUT'){const error=new Error('changed again');error.status=409;throw error;}
 return {revision:revision++,timezone:'UTC',days:[{plan_date:'2026-08-21',ids:['server-'+(revision-1)],capacity_minutes:60,estimates:{}}]};
};
const week=createWeekPlan({storage,getLogin:()=> 'timmy',fetchJson,localDate:()=> '2026-08-20',timeZone:()=> 'UTC'});
week.adopt({revision:11,timezone:'UTC',days:[]});
week.stageDay('2026-08-21',{ids:['phone'],capacity_minutes:90,estimates:{}});
try { await week.flush(); } catch(_error) {}
try { await week.keepLocal(); } catch(_error) {}
console.log(JSON.stringify({conflict:week.conflict(),pending:week.pending()}));
""")

    assert result["conflict"]["local"]["days"][0]["ids"] == ["phone"]
    assert result["conflict"]["remote"]["revision"] == 13
    assert result["conflict"]["remote"]["days"][0]["ids"] == ["server-13"]
    assert result["pending"]["days"][0]["ids"] == ["phone"]


def test_week_controller_uses_account_week_without_a_second_write():
    result = run_controller("""
const values=new Map();
const storage={getItem:key=>values.get(key)||null,setItem:(key,value)=>values.set(key,value),removeItem:key=>values.delete(key)};
const requests=[];
const remote={revision:12,timezone:'UTC',days:[{plan_date:'2026-08-21',ids:['server'],capacity_minutes:60,estimates:{server:30}}]};
const fetchJson=async(_url,options={})=>{
 requests.push(options.method||'GET');
 if(options.method==='PUT'){const error=new Error('changed');error.status=409;throw error;}
 return remote;
};
const week=createWeekPlan({storage,getLogin:()=> 'timmy',fetchJson,localDate:()=> '2026-08-20',timeZone:()=> 'UTC'});
week.adopt({revision:11,timezone:'UTC',days:[]});
week.stageDay('2026-08-21',{ids:['phone'],capacity_minutes:90,estimates:{}});
try { await week.flush(); } catch(_error) {}
const adopted=week.useRemote();
console.log(JSON.stringify({adopted,requests,pending:week.pending(),conflict:week.conflict(),state:week.state()}));
""")

    assert result["requests"] == ["PUT", "GET"]
    assert result["adopted"]["days"][0]["ids"] == ["server"]
    assert result["pending"] is False
    assert result["conflict"] is None
    assert result["state"]["revision"] == 12


def test_week_controller_promotes_only_the_earliest_due_plan_with_stable_identity():
    result = run_controller("""
const requests=[];
const week=createWeekPlan({fetchJson:async(url,options={})=>{
 requests.push({url,body:options.body?JSON.parse(options.body):null});
 return {revision:9,ids:['oldest']};
},localDate:()=> '2026-08-23',timeZone:()=> 'UTC'});
week.adopt({revision:6,timezone:'UTC',days:[
 {plan_date:'2026-08-21',ids:['oldest'],capacity_minutes:60,estimates:{}},
 {plan_date:'2026-08-22',ids:['newer'],capacity_minutes:60,estimates:{}}
]});
const promoted=await week.promote(3);
console.log(JSON.stringify({promoted,requests}));
""")

    assert result["requests"] == [{
        "url": "api/v1/week/promote",
        "body": {
            "promotion_id": "week-2026-08-21-r6",
            "week_revision": 6,
            "plan_date": "2026-08-21",
            "today_revision": 3,
        },
    }]


def test_week_controller_reconciles_reviewed_today_and_adopts_both_results():
    result = run_controller("""
const requests=[];
const week=createWeekPlan({fetchJson:async(url,options={})=>{
 const body=JSON.parse(options.body);requests.push({url,body});
 return {today:{revision:5,ids:body.ids,capacity_minutes:body.capacity_minutes,estimates:body.estimates},
   week:{revision:7,timezone:'UTC',days:[{plan_date:'2026-08-23',ids:['later'],capacity_minutes:30,estimates:{later:30}}]}};
},localDate:()=> '2026-08-23',timeZone:()=> 'UTC'});
week.adopt({revision:6,timezone:'UTC',days:[{plan_date:'2026-08-21',ids:['due'],capacity_minutes:60,estimates:{due:30}}]});
const reconciled=await week.reconcile({today:{revision:4,ids:['unfinished']},week:week.state()},
 {ids:['unfinished','due'],capacity_minutes:90,estimates:{unfinished:30,due:30}});
console.log(JSON.stringify({reconciled,requests,state:week.state()}));
""")

    assert result["requests"][0]["url"] == "api/v1/week/reconcile"
    assert result["requests"][0]["body"]["ids"] == ["unfinished", "due"]
    assert result["requests"][0]["body"]["today_revision"] == 4
    assert result["state"]["revision"] == 7
    assert result["state"]["days"][0]["ids"] == ["later"]
    assert result["reconciled"]["today"]["ids"] == ["unfinished", "due"]


def test_week_workflow_routes_blocked_rollover_into_atomic_combined_review():
    result = run_controller("""
const createWorkflow=createWeekPlan.Workflow;
const elements={'#mobile-week-summary':{textContent:''},'#my-work-action-status':{textContent:''}};
let opened=0,reconciled=null,replaced=[];
const blocked=new Error('blocked');blocked.code='week_today_in_progress';
blocked.today={revision:4,ids:['unfinished'],capacity_minutes:45,estimates:{unfinished:30}};
blocked.week={revision:6,timezone:'UTC',days:[{plan_date:'2026-08-21',ids:['due','unfinished'],capacity_minutes:90,estimates:{due:45,unfinished:30}}]};
const controller={load:async()=>({}),promote:async()=>{throw blocked;},state:()=>blocked.week,summary:()=> '1 item across 1 day',
 reconcile:async(preserved,plan)=>{reconciled={preserved,plan};return {today:{revision:5,...plan},week:{revision:7,timezone:'UTC',days:[]}};}};
const workflow=createWorkflow({controller,qs:selector=>elements[selector],getLogin:()=> 'timmy',openPlanner:()=>{opened+=1;},
 escapeHtml:value=>value,escapeAttribute:value=>value,
 todayWork:{replace:value=>replaced.push(value),replacePlanning:value=>replaced.push(value)},refresh:()=>{},warm:()=>{},
});
const promoted=await workflow.promote({revision:4,ids:['unfinished']});
const review={copy:workflow.copy(),day:workflow.day()};
const saved=workflow.save({ids:['unfinished','due'],capacity_minutes:90,estimates:{unfinished:30,due:45}});
await new Promise(resolve=>setTimeout(resolve,0));
console.log(JSON.stringify({promoted,opened,review,saved,reconciled,replaced,status:elements['#my-work-action-status'].textContent}));
""")

    assert result["promoted"] is False
    assert result["opened"] == 1
    assert result["review"]["copy"]["title"] == "Start today's plan"
    assert result["review"]["day"]["ids"] == ["unfinished", "due"]
    assert result["saved"] is True
    assert result["reconciled"]["plan"]["ids"] == ["unfinished", "due"]
    assert result["replaced"][0] == ["unfinished", "due"]
    assert result["status"] == "Today started from unfinished and Week Ahead work."


def test_week_controller_does_not_promote_a_pending_week():
    result = run_controller("""
const values=new Map();
const storage={getItem:key=>values.get(key)||null,setItem:(key,value)=>values.set(key,value),removeItem:key=>values.delete(key)};
let requests=0;
const week=createWeekPlan({storage,getLogin:()=> 'timmy',fetchJson:async()=>{requests+=1;return {};},
  localDate:()=> '2026-08-23',timeZone:()=> 'UTC'});
week.adopt({revision:6,timezone:'UTC',days:[]});
week.stageDay('2026-08-21',{ids:['oldest'],capacity_minutes:60,estimates:{}});
const promoted=await week.promote(3);
console.log(JSON.stringify({promoted,requests}));
""")

    assert result == {"promoted": False, "requests": 0}


def test_mobile_week_ahead_entry_and_date_strip_are_touch_safe():
    index = (FRONTEND / "index.html").read_text()
    css = (FRONTEND / "dashboard.css").read_text()
    dashboard = (FRONTEND / "dashboard.js").read_text()


    assert 'data-mobile-queue="week"' in index
    assert 'id="week-plan-dates"' in index
    assert '<script src="static/week-plan.js"></script>' in index
    assert "name === 'week' ? openWeekPlanner" in dashboard
    assert "weekFlow.save" in dashboard
    assert ".week-plan-dates button { min-height:44px;" in css
    assert "overflow-x:auto" in css


def test_mobile_week_ahead_review_is_rendered_touch_safe_and_confirmed_explicitly():
    index = (FRONTEND / "index.html").read_text()
    css = (FRONTEND / "dashboard.css").read_text()
    dashboard = (FRONTEND / "dashboard.js").read_text()

    assert 'id="week-review"' in index
    assert 'id="week-review-days"' in index
    assert 'id="week-review-duplicates"' in index
    assert 'id="confirm-week-plan"' in index
    assert "setReviewMode:v=>qs('#plan-today-sheet').classList.toggle('week-review-mode',v)" in dashboard
    assert "weekFlow.confirm()" in dashboard
    assert ".week-review-day.is-overloaded" in css
    assert ".week-review-day button { min-height:44px;" in css
    assert ".week-review-mode .mobile-plan-today-nav" in css


def test_mobile_week_ahead_reflow_has_a_reviewed_touch_safe_flow():
    index = (FRONTEND / "index.html").read_text()
    css = (FRONTEND / "dashboard.css").read_text()

    for element_id in [
        "open-week-reflow", "week-reflow-review", "week-reflow-days",
        "week-reflow-unscheduled", "cancel-week-reflow", "apply-week-reflow",
    ]:
        assert f'id="{element_id}"' in index
    assert ".week-reflow-actions button" in css
    rule = css.split(".week-reflow-actions button", 1)[1].split("}", 1)[0]
    assert "min-height:48px" in rule
    assert ".week-reflow-review" in css
    assert "overflow-x:hidden" in css


def test_mobile_week_ahead_has_a_touch_safe_offline_snapshot_notice_and_retry():
    index = (FRONTEND / "index.html").read_text()
    css = (FRONTEND / "dashboard.css").read_text()

    assert 'id="week-offline-snapshot"' in index
    assert 'id="retry-week-live"' in index
    assert 'role="status"' in index[index.index('id="week-offline-snapshot"') - 100:index.index('id="week-offline-snapshot"') + 300]
    assert ".week-offline-snapshot" in css
    assert "#retry-week-live { min-height:44px;" in css


def test_mobile_week_ahead_exposes_durable_save_and_touch_safe_conflict_choices():
    index = (FRONTEND / "index.html").read_text()
    css = (FRONTEND / "dashboard.css").read_text()
    dashboard = (FRONTEND / "dashboard.js").read_text()

    assert 'id="week-conflict-review"' in index
    assert 'id="week-conflict-days"' in index
    assert 'id="save-merged-week"' in index
    assert 'id="keep-phone-week"' in index
    assert 'id="use-server-week"' in index
    assert "storage:localStorage" in dashboard
    assert "controller.stageDay" in CONTROLLER.read_text()
    assert "weekPlan.flush" in dashboard
    assert "week-conflict-mode" in dashboard
    assert "controller.chooseDay" in CONTROLLER.read_text()
    assert "weekPlan.saveMerged" in dashboard
    assert 'data-week-conflict-choice' in CONTROLLER.read_text()
    assert ".week-conflict-actions button { min-height:44px;" in css
    assert ".week-conflict-choice { min-height:44px;" in css
