import json
import subprocess
from pathlib import Path


WEEK_CALENDAR = Path(__file__).parents[1] / "frontend" / "week-calendar.js"
INDEX = Path(__file__).parents[1] / "frontend" / "index.html"
DASHBOARD = Path(__file__).parents[1] / "frontend" / "dashboard.js"
CSS = Path(__file__).parents[1] / "frontend" / "dashboard.css"
BUNDLE = Path(__file__).parents[1] / "src" / "frontend_bundle.py"


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


def test_week_calendar_builds_ordered_local_time_blocks_and_serializes_opaque_events():
    plan = {
        "timezone": "America/New_York",
        "revision": 7,
        "days": [{
            "plan_date": "2026-08-21",
            "ids": ["issue:stackchain/dashboard:12:", "pull:stackchain/api:7:"],
            "capacity_minutes": 120,
            "estimates": {
                "issue:stackchain/dashboard:12:": 45,
                "pull:stackchain/api:7:": 30,
            },
        }],
    }
    items = {
        "issue:stackchain/dashboard:12:": {
            "id": 401,
            "title": "Plan, ship; verify",
            "repository": "stackchain/dashboard",
            "number": 12,
            "url": "https://forge.alexanderwhitestone.com/git/stackchain/stackchain-dashboard/issues/12",
        },
        "pull:stackchain/api:7:": {
            "title": "API release",
            "repository": "stackchain/api",
            "number": 7,
            "url": "https://forge.alexanderwhitestone.com/git/stackchain/api/pulls/7",
        },
    }
    source = f"""
const calendar=require({json.dumps(str(WEEK_CALENDAR))});
const plan={json.dumps(plan)}, items={json.dumps(items)};
const blocks=calendar.buildWeekBlocks(plan, {{'2026-08-21':'09:00'}}, id=>items[id]);
const text=calendar.serializeWeekCalendar(blocks, {{timezone:plan.timezone,revision:plan.revision,generatedAt:'20260820T120000Z'}});
console.log(JSON.stringify({{blocks,text}}));
"""

    result = run_node(source)
    assert [(block["start_time"], block["end_time"]) for block in result["blocks"]] == [
        ("09:00", "09:45"), ("09:45", "10:15")
    ]
    text = result["text"]
    assert "TZID:America/New_York" in text
    assert "DTSTART;TZID=America/New_York:20260821T090000" in text
    assert "DTEND;TZID=America/New_York:20260821T094500" in text
    assert "DTSTART;TZID=America/New_York:20260821T094500" in text
    assert "DTEND;TZID=America/New_York:20260821T101500" in text
    assert "TRANSP:OPAQUE" in text
    assert "UID:week-issue-stackchain-dashboard-12@stackchain" in text
    assert "SEQUENCE:7" in text
    assert "SUMMARY:Plan\\, ship\\; verify" in text
    unfolded = text.replace("\r\n ", "")
    assert "URL:https://forge.alexanderwhitestone.com/git/stackchain/stackchain-dashboard/issues/12" in unfolded


def test_week_calendar_reexports_update_stable_distinct_work_events():
    source = f"""
const calendar=require({json.dumps(str(WEEK_CALENDAR))});
const issue={{id:'issue:stackchain/dashboard:12:',title:'Ship it',repository:'stackchain/dashboard',number:12,
  plan_date:'2026-08-21',start_time:'09:00',end_time:'09:30'}};
const moved={{...issue,plan_date:'2026-08-22',start_time:'14:00',end_time:'14:30'}};
const pull={{...moved,id:'pull:stackchain/dashboard:12:'}};
const first=calendar.serializeWeekCalendar([issue],{{timezone:'UTC',revision:7,generatedAt:'20260820T120000Z'}});
const second=calendar.serializeWeekCalendar([moved,pull],{{timezone:'UTC',revision:8,generatedAt:'20260821T120000Z'}});
const events=text=>text.split('BEGIN:VEVENT').slice(1).map(value=>({{
  uid:value.match(/UID:([^\\r]+)/)[1],sequence:Number(value.match(/SEQUENCE:(\\d+)/)[1])
}}));
console.log(JSON.stringify({{first:events(first),second:events(second)}}));
"""

    result = run_node(source)
    assert result["first"] == [{
        "uid": "week-issue-stackchain-dashboard-12@stackchain",
        "sequence": 7,
    }]
    assert result["second"] == [
        {"uid": "week-issue-stackchain-dashboard-12@stackchain", "sequence": 8},
        {"uid": "week-pull-stackchain-dashboard-12@stackchain", "sequence": 8},
    ]


def test_week_calendar_places_whole_tasks_in_imported_free_windows_and_reports_unfitted_work():
    source = f"""
const calendar=require({json.dumps(str(WEEK_CALENDAR))});
const plan={{days:[{{plan_date:'2026-08-21',ids:['one','two','three'],estimates:{{one:45,two:90,three:400}}}}]}};
const items={{one:{{title:'First',repository:'r',number:1}},two:{{title:'Second',repository:'r',number:2}},
  three:{{title:'Too large',repository:'r',number:3}}}};
const availability=[{{plan_date:'2026-08-21',free_windows:[{{start_time:'09:00',end_time:'10:00'}},{{start_time:'11:00',end_time:'17:00'}}]}}];
const result=calendar.buildWeekSchedule(plan,{{'2026-08-21':'09:00'}},id=>items[id],null,availability);
console.log(JSON.stringify(result));
"""

    result = run_node(source)
    assert [(block["id"], block["start_time"], block["end_time"]) for block in result["blocks"]] == [
        ("one", "09:00", "09:45"),
        ("two", "11:00", "12:30"),
    ]
    assert result["blockers"] == [{
        "id": "three", "title": "Too large", "plan_date": "2026-08-21", "minutes": 400
    }]


def test_week_calendar_honors_exact_saved_task_times_and_rejects_conflicts():
    source = f"""
const calendar=require({json.dumps(str(WEEK_CALENDAR))});
const items={{one:{{title:'First'}},two:{{title:'Second'}}}};
const plan={{days:[{{plan_date:'2026-08-21',ids:['one','two'],estimates:{{one:45,two:90}},
  free_windows:[{{start_time:'09:00',end_time:'10:00'}},{{start_time:'13:00',end_time:'17:00'}}],
  start_times:{{one:'09:00',two:'14:00'}}}}]}};
const saved=calendar.buildWeekSchedule(plan,{{}},id=>items[id]);
const conflict=calendar.buildWeekSchedule(plan,{{one:'14:30',two:'14:00'}},id=>items[id]);
console.log(JSON.stringify({{saved,conflict}}));
"""

    result = run_node(source)
    assert [(block["id"], block["start_time"], block["end_time"]) for block in result["saved"]["blocks"]] == [
        ("one", "09:00", "09:45"), ("two", "14:00", "15:30")
    ]
    assert result["conflict"]["blocks"] == []
    assert {blocker["id"] for blocker in result["conflict"]["blockers"]} == {"one", "two"}


def test_week_calendar_excludes_unselected_items_and_downloads_when_native_share_is_unavailable():
    source = f"""
const calendar=require({json.dumps(str(WEEK_CALENDAR))});
const plan={{timezone:'UTC',revision:2,days:[{{plan_date:'2026-08-21',ids:['one','two'],capacity_minutes:60,estimates:{{one:20,two:25}}}}]}};
const items={{one:{{title:'Private',repository:'r',number:1,url:'https://forge/r/1'}},two:{{title:'Public',repository:'r',number:2,url:'https://forge/r/2'}}}};
const blocks=calendar.buildWeekBlocks(plan,{{'2026-08-21':'13:00'}},id=>items[id],new Set(['two']));
class FakeFile {{constructor(parts,name,options){{this.parts=parts;this.name=name;this.type=options.type;}}}}
const clicks=[],revoked=[];
(async()=>{{
 const result=await calendar.deliverWeekCalendar({{text:'calendar',filename:'week.ics',navigator:{{}},FileCtor:FakeFile,
  document:{{createElement:()=>({{click(){{clicks.push([this.download,this.href]);}}}})}},
  urlApi:{{createObjectURL:()=> 'blob:week',revokeObjectURL:value=>revoked.push(value)}}}});
 console.log(JSON.stringify({{blocks,result,clicks,revoked}}));
}})();
"""

    result = run_node(source)
    assert [block["id"] for block in result["blocks"]] == ["two"]
    assert result["blocks"][0]["start_time"] == "13:00"
    assert result["result"] == "downloaded"
    assert result["clicks"] == [["week.ics", "blob:week"]]
    assert result["revoked"] == ["blob:week"]


def test_week_calendar_handoff_is_wired_into_confirmation_with_mobile_privacy_controls():
    index = INDEX.read_text()
    dashboard = DASHBOARD.read_text()
    bundle = BUNDLE.read_text()

    assert '<script src="static/week-calendar.js"></script>' in index
    assert 'id="week-calendar-handoff"' in index
    assert 'id="week-calendar-days"' in index
    assert 'id="share-week-calendar"' in index
    assert 'id="save-week-calendar-times"' in index
    assert 'data-week-calendar-exact' in WEEK_CALENDAR.read_text()
    assert "onSave" in WEEK_CALENDAR.read_text()
    assert 'min-height:44px' in CSS.read_text()[CSS.read_text().index('.week-calendar-time'):]
    assert 'id="back-to-week-review-from-calendar"' in index
    assert 'id="export-saved-week-calendar"' in index
    assert "Add saved week to calendar" in index
    assert "Titles, references, dates, and times are included" in index
    assert "Imported busy time is used only until this page reloads" in index
    assert "mountWeekCalendarHandoff" in WEEK_CALENDAR.read_text()
    assert "wc.open(weekPlan.state())" in dashboard
    assert "getItem:weekFlow.i" in dashboard
    assert "i:id=>getItem(id)||controller.item?.(id)" in (WEEK_CALENDAR.parent / "week-plan.js").read_text()
    assert "#export-saved-week-calendar" in WEEK_CALENDAR.read_text()
    assert "focusReturn=event.currentTarget" in WEEK_CALENDAR.read_text()
    assert "wc.close()" in dashboard
    assert "StackchainWeekCalendar.setAvailabilityProvider(workflow.availability)" in (WEEK_CALENDAR.parent / "week-calendar-import.js").read_text()
    assert "Planning around imported busy time" in WEEK_CALENDAR.read_text()
    assert '"static/week-calendar.js", "static/week-calendar-import.js", "static/week-plan.js"' in bundle
