import json
import subprocess
from pathlib import Path

import pytest

from tests.dashboard_bundle import dashboard


PLAN_TODAY = Path(__file__).parents[1] / "frontend" / "plan-today.js"
PLAN_TODAY_PREVIEW = Path(__file__).parents[1] / "frontend" / "plan-today-preview.js"
SERVICE_WORKER = Path(__file__).parents[1] / "frontend" / "service-worker.js"


def run_node(script: str) -> dict:
    result = subprocess.run(["node", "-e", script], check=True, capture_output=True, text=True)
    return json.loads(result.stdout)


def test_plan_today_drafts_capacity_order_and_cancel_without_writing():
    script = f"""
const createPlanToday = require({json.dumps(str(PLAN_TODAY))});
const item = number => ({{kind:'issue', repository:'stackchain/dashboard', number, title:'Issue ' + number}});
const saved = [];
const planner = createPlanToday({{
  identity: value => 'issue:stackchain/dashboard:' + value.number + ':',
  limit: 3,
  save: ids => saved.push(ids),
}});
planner.open([item(1), item(2)], [item(1), item(2), item(3), item(4)]);
const add = planner.toggle(item(3));
const full = planner.toggle(item(4));
const moved = planner.move('issue:stackchain/dashboard:3:', 'up');
const snapshot = planner.snapshot();
planner.cancel();
process.stdout.write(JSON.stringify({{add, full, moved, snapshot, afterCancel:planner.snapshot(), saved}}));
"""
    assert run_node(script) == {
        "add": "added",
        "full": "full",
        "moved": True,
        "snapshot": {
            "open": True,
            "ids": [
                "issue:stackchain/dashboard:1:",
                "issue:stackchain/dashboard:3:",
                "issue:stackchain/dashboard:2:",
            ],
            "count": 3,
            "limit": 3,
        },
        "afterCancel": {"open": False, "ids": [], "count": 0, "limit": 3},
        "saved": [],
    }


def test_plan_today_saves_exact_draft_and_starts_first_item_only_after_success():
    script = f"""
const createPlanToday = require({json.dumps(str(PLAN_TODAY))});
const item = number => ({{kind:'issue', repository:'stackchain/dashboard', number, title:'Issue ' + number}});
const calls = [];
const planner = createPlanToday({{
  identity: value => 'issue:stackchain/dashboard:' + value.number + ':',
  save: ids => {{ calls.push(['save', ...ids]); return true; }},
  start: first => calls.push(['start', first.number]),
}});
planner.open([item(1)], [item(1), item(2)]);
planner.toggle(item(2));
const result = planner.commit({{start:true}});
process.stdout.write(JSON.stringify({{result, calls, snapshot:planner.snapshot()}}));
"""
    assert run_node(script) == {
        "result": "saved",
        "calls": [
            ["save", "issue:stackchain/dashboard:1:", "issue:stackchain/dashboard:2:"],
            ["start", 1],
        ],
        "snapshot": {"open": False, "ids": [], "count": 0, "limit": 5},
    }


def test_plan_today_calculates_capacity_and_confirms_overcommitment_before_saving():
    script = f"""
const createPlanToday = require({json.dumps(str(PLAN_TODAY))});
const item = number => ({{kind:'issue', repository:'stackchain/dashboard', number, title:'Issue ' + number}});
const saved = [];
const planner = createPlanToday({{
  identity: value => 'issue:stackchain/dashboard:' + value.number + ':',
  save: plan => saved.push(plan),
}});
planner.open([item(1), item(2)], [item(1), item(2)]);
planner.setCapacity(90);
planner.setEstimate('issue:stackchain/dashboard:1:', 60);
planner.setEstimate('issue:stackchain/dashboard:2:', 45);
const over = planner.snapshot();
const confirmation = planner.commit();
const stillOpen = planner.snapshot().open;
const result = planner.commit({{confirmOverCapacity:true}});
process.stdout.write(JSON.stringify({{over, confirmation, stillOpen, result, saved}}));
"""
    result = run_node(script)
    assert result["over"] == {
        "open": True,
        "ids": ["issue:stackchain/dashboard:1:", "issue:stackchain/dashboard:2:"],
        "count": 2,
        "limit": 5,
        "capacity_minutes": 90,
        "estimates": {
            "issue:stackchain/dashboard:1:": 60,
            "issue:stackchain/dashboard:2:": 45,
        },
        "planned_minutes": 105,
        "remaining_minutes": -15,
        "unestimated_count": 0,
        "over_capacity": True,
    }
    assert result["confirmation"] == "confirm-over-capacity"
    assert result["stillOpen"] is True
    assert result["result"] == "saved"
    assert result["saved"] == [{
        "ids": ["issue:stackchain/dashboard:1:", "issue:stackchain/dashboard:2:"],
        "capacity_minutes": 90,
        "estimates": {
            "issue:stackchain/dashboard:1:": 60,
            "issue:stackchain/dashboard:2:": 45,
        },
    }]


def test_plan_today_applies_recap_actual_only_to_the_draft_after_explicit_action():
    script = f"""
const createPlanToday = require({json.dumps(str(PLAN_TODAY))});
const item = number => ({{kind:'issue', repository:'stackchain/dashboard', number, title:'Issue ' + number}});
const saved = [];
const planner = createPlanToday({{
  identity: value => 'issue:stackchain/dashboard:' + value.number + ':',
  save: plan => {{ saved.push(plan); return true; }},
}});
const first='issue:stackchain/dashboard:1:';
const second='issue:stackchain/dashboard:2:';
planner.open([item(1), item(2)], [item(1), item(2)], {{
  capacity_minutes:120, estimates:{{[first]:30,[second]:45}}
}}, {{[first]:52,[second]:45}});
const before=planner.snapshot();
const ignored=planner.applyRecommendation(second);
const applied=planner.applyRecommendation(first);
const after=planner.snapshot();
const result=planner.commit();
process.stdout.write(JSON.stringify({{before,ignored,applied,after,result,saved}}));
"""
    result = run_node(script)
    assert result["before"]["estimates"] == {
        "issue:stackchain/dashboard:1:": 30,
        "issue:stackchain/dashboard:2:": 45,
    }
    assert result["before"]["recommendations"] == {"issue:stackchain/dashboard:1:": 52}
    assert result["ignored"] is False
    assert result["applied"] is True
    assert result["after"]["estimates"]["issue:stackchain/dashboard:1:"] == 52
    assert result["after"]["recommendations"] == {}
    assert result["saved"][0]["estimates"] == {
        "issue:stackchain/dashboard:1:": 52,
        "issue:stackchain/dashboard:2:": 45,
    }


def test_plan_today_builds_ranked_ready_capacity_fit_without_saving():
    script = f"""
const createPlanToday = require({json.dumps(str(PLAN_TODAY))});
const item = (number, reason) => ({{kind:'issue', repository:'stackchain/dashboard', number, title:'Issue ' + number, reason}});
const saved = [];
+const planner = createPlanToday({{
  identity: value => 'issue:stackchain/dashboard:' + value.number + ':',
  save: plan => saved.push(plan),
}});
const id = n => 'issue:stackchain/dashboard:' + n + ':';
planner.open([], [item(1, 'Due today'), item(2, 'Unread update'), item(3, 'Assigned to you'), item(4, 'Needs your review')], {{
  capacity_minutes:90, estimates:{{[id(1)]:60,[id(2)]:45,[id(3)]:30,[id(4)]:20}}
}});
const build = planner.buildRecommendation({{
  [id(1)]:{{status:'ready'}},
  [id(2)]:{{status:'blocked', reason:'Waiting on API'}},
  [id(3)]:{{status:'ready'}},
  [id(4)]:{{status:'unverified'}},
}});
process.stdout.write(JSON.stringify({{build, snapshot:planner.snapshot(), saved}}));
""".replace("+const", "const")
    result = run_node(script)
    assert result["build"] == {
        "selected": ["issue:stackchain/dashboard:1:", "issue:stackchain/dashboard:3:"],
        "reasons": {
            "issue:stackchain/dashboard:1:": "Due today",
            "issue:stackchain/dashboard:3:": "Assigned to you",
        },
        "skipped": [
            {"id": "issue:stackchain/dashboard:2:", "status": "blocked", "reason": "Waiting on API"},
            {"id": "issue:stackchain/dashboard:4:", "status": "unverified", "reason": "Could not verify readiness"},
        ],
        "needs_estimate": [],
    }
    assert result["snapshot"]["ids"] == [
        "issue:stackchain/dashboard:1:", "issue:stackchain/dashboard:3:"
    ]
    assert result["snapshot"]["planned_minutes"] == 90
    assert result["snapshot"]["remaining_minutes"] == 0
    assert result["snapshot"]["build"] == result["build"]
    assert result["saved"] == []


def test_plan_today_estimates_ready_omission_and_refits_without_another_readiness_check():
    script = f"""
const createPlanToday = require({json.dumps(str(PLAN_TODAY))});
const item = number => ({{kind:'issue', repository:'stackchain/dashboard', number, title:'Issue ' + number}});
const planner = createPlanToday({{
  identity: value => 'issue:stackchain/dashboard:' + value.number + ':',
  save: () => true,
}});
const id = n => 'issue:stackchain/dashboard:' + n + ':';
planner.open([], [item(1), item(2)], {{capacity_minutes:60, estimates:{{[id(1)]:30}}}});
planner.buildRecommendation({{[id(1)]:{{status:'ready'}}, [id(2)]:{{status:'ready'}}}});
const before = planner.snapshot();
const estimated = planner.setEstimate(id(2), 30);
const after = planner.snapshot();
process.stdout.write(JSON.stringify({{before, estimated, after}}));
"""
    result = run_node(script)
    assert result["before"]["ids"] == ["issue:stackchain/dashboard:1:"]
    assert result["before"]["build"]["needs_estimate"] == ["issue:stackchain/dashboard:2:"]
    assert result["estimated"] is True
    assert result["after"]["ids"] == [
        "issue:stackchain/dashboard:1:", "issue:stackchain/dashboard:2:"
    ]
    assert result["after"]["estimates"]["issue:stackchain/dashboard:2:"] == 30
    assert result["after"]["remaining_minutes"] == 0
    assert result["after"]["build"]["needs_estimate"] == []


def test_plan_today_preview_preserves_draft_scroll_and_adds_item_once_on_return():
    script = f"""
const createPlanToday = require({json.dumps(str(PLAN_TODAY))});
const createPlanTodayPreview = require({json.dumps(str(PLAN_TODAY_PREVIEW))});
const item = number => ({{kind:'issue', repository:'stackchain/dashboard', number, title:'Issue ' + number}});
const events = [];
let scroll = 318;
const planner = createPlanToday({{
  identity: value => 'issue:stackchain/dashboard:' + value.number + ':',
  save: () => true,
}});
planner.open([item(1)], [item(1), item(2)]);
const preview = createPlanTodayPreview({{
  planner,
  identity: value => 'issue:stackchain/dashboard:' + value.number + ':',
  getScroll: () => scroll,
  setScroll: value => {{ scroll = value; }},
  onOpen: value => events.push(['open', value.number]),
  onClose: (value, trigger) => events.push(['close', value.number, trigger]),
}});
preview.open(item(2), 'preview-2');
scroll = 0;
const first = preview.close({{add:true}});
preview.open(item(2), 'preview-2');
scroll = 0;
const second = preview.close({{add:true}});
process.stdout.write(JSON.stringify({{first, second, scroll, events, planner:planner.snapshot()}}));
"""
    assert run_node(script) == {
        "first": "added",
        "second": "already-added",
        "scroll": 318,
        "events": [
            ["open", 2],
            ["close", 2, "preview-2"],
            ["open", 2],
            ["close", 2, "preview-2"],
        ],
        "planner": {
            "open": True,
            "ids": ["issue:stackchain/dashboard:1:", "issue:stackchain/dashboard:2:"],
            "count": 2,
            "limit": 5,
        },
    }


def test_plan_today_preview_requires_explicit_override_for_blocked_or_unknown_issue():
    script = f"""
const createPlanToday = require({json.dumps(str(PLAN_TODAY))});
const createPlanTodayPreview = require({json.dumps(str(PLAN_TODAY_PREVIEW))});
const item = number => ({{kind:'issue', repository:'stackchain/dashboard', number, title:'Issue ' + number}});
const planner = createPlanToday({{
  identity: value => 'issue:stackchain/dashboard:' + value.number + ':',
  save: () => true,
}});
planner.open([], [item(2), item(3)]);
const preview = createPlanTodayPreview({{
  planner,
  identity: value => 'issue:stackchain/dashboard:' + value.number + ':',
}});
preview.open(item(2));
preview.setDependencies({{available:true, dependencies:[{{repository:'stackchain/api', number:9, title:'Restore API', state:'open'}}]}});
const blockedState = preview.snapshot();
const blocked = preview.close({{add:true}});
const overridden = preview.close({{add:true, override:true}});
preview.open(item(3));
preview.setDependencies({{available:false, dependencies:[]}});
const unavailableState = preview.snapshot();
const unavailable = preview.close({{add:true}});
const unavailableOverride = preview.close({{add:true, override:true}});
process.stdout.write(JSON.stringify({{blockedState, blocked, overridden, unavailableState, unavailable, unavailableOverride, planner:planner.snapshot()}}));
"""
    assert run_node(script) == {
        "blockedState": {
            "open": True,
            "item": {"kind": "issue", "repository": "stackchain/dashboard", "number": 2, "title": "Issue 2"},
            "trigger": None,
            "scroll": 0,
            "dependencies_available": True,
            "dependencies": [{"repository": "stackchain/api", "number": 9, "title": "Restore API", "state": "open"}],
            "requires_override": True,
        },
        "blocked": "blocked",
        "overridden": "added",
        "unavailableState": {
            "open": True,
            "item": {"kind": "issue", "repository": "stackchain/dashboard", "number": 3, "title": "Issue 3"},
            "trigger": None,
            "scroll": 0,
            "dependencies_available": False,
            "dependencies": [],
            "requires_override": True,
        },
        "unavailable": "dependencies-unavailable",
        "unavailableOverride": "added",
        "planner": {
            "open": True,
            "ids": ["issue:stackchain/dashboard:2:", "issue:stackchain/dashboard:3:"],
            "count": 2,
            "limit": 5,
        },
    }


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

    assert '<script src="static/plan-today.js"></script>' in html
    assert 'id="plan-today"' in html
    assert 'id="plan-today-sheet" role="dialog"' in html
    assert 'id="plan-today-capacity"' in html
    assert 'id="plan-today-available"' in html
    assert 'id="build-today-plan"' in html
    assert 'id="plan-today-build-status" role="status" aria-live="polite"' in html
    assert 'id="plan-today-estimates"' in html
    assert 'Estimate to finish your plan' in html
    assert 'data-plan-missing-estimate="' in html
    assert '.plan-today-estimates input { min-height:44px;' in html
    assert 'inputmode="numeric" min="15" max="1440"' in html
    assert 'data-plan-estimate="' in html
    assert 'aria-label="Estimate for ' in html
    assert "formatPlanMinutes(state.planned_minutes)" in html
    assert "planToday.setCapacity" in html
    assert "planToday.setEstimate" in html
    assert 'data-plan-recommendation="' in html
    assert "planToday.applyRecommendation" in html
    assert "Use " in html and " as new estimate" in html
    assert "todaySync.enqueueConfiguration" in html
    assert "todayWork.runway(todayMyWork, state.index - 1)" in (Path(__file__).parents[1] / "frontend" / "dashboard.js").read_text()
    assert "timer.totalElapsed()" in (Path(__file__).parents[1] / "frontend" / "today-timer.js").read_text()
    assert "resetPlanTodayConfirmation()" in html
    assert "result === 'confirm-over-capacity'" in html
    assert 'id="save-and-start-today"' in html
    assert "const planToday = createPlanToday({" in html
    assert "todaySync.enqueue('remove'" in html
    assert "todaySync.enqueue('add'" in html
    assert "qs('#plan-today').addEventListener('click'" in html
    assert ".plan-today-actions { position:sticky; bottom:0;" in html
    assert ".plan-today-actions button { min-height:44px;" in html
    assert "padding-bottom:calc(12px + env(safe-area-inset-bottom))" in html
    assert ".plan-today-item { grid-template-columns:1fr; }" in html
    assert ".plan-today-item-actions { display:grid; grid-template-columns:repeat(3,1fr);" in html
    assert ".plan-preview-actions { position:fixed; z-index:76;" in html
    assert '<script src="static/plan-today-preview.js"></script>' in html
    assert 'data-plan-preview="' in html
    assert 'id="add-plan-preview"' in html
    assert "taskOverlayHistory.open('plan-today-preview')" in html
    assert "planTodayPreview.close({ add:true })" in html
    assert 'id="issue-blockers"' in html
    assert "planTodayPreview.setDependencies({" in html
    assert "Add blocked item anyway & back" in html
    assert "Blocker status unavailable" in html
    assert ".issue-blockers { max-width:100%; overflow-x:hidden;" in html
    assert ".issue-blocker { min-width:0; overflow-wrap:anywhere;" in html
    assert "Preview unavailable offline" in html


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

    assert "taskOverlayHistory.open('plan-today')" in html
    assert "previous === 'plan-today' && kind !== 'plan-today'" in html
    assert "kind === 'plan-today' && previous !== 'plan-today'" in html
    assert "openPlanToday(planTodayTrigger, false)" in html
    assert "closePlanToday(false)" in html
    assert "if (result === 'saved')" in html
    assert "taskOverlayHistory.leave();" in html


def test_plan_today_controller_is_available_in_the_offline_shell():
    source = SERVICE_WORKER.read_text()

    assert "stackchain-dashboard-shell-v127" in source
    assert "BASE + 'static/plan-today.js'" in source
    assert "BASE + 'static/plan-today-readiness.js'" in source
    assert "BASE + 'static/plan-today-preview.js'" in source
