import json
import subprocess
from pathlib import Path

import pytest

from src import main
from src.today_store import TodayStore


ROLLOVER = Path(__file__).parents[1] / "frontend" / "today-rollover.js"
SYNC = Path(__file__).parents[1] / "frontend" / "today-sync.js"


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


def test_rollover_replaces_a_stale_plan_atomically_and_is_idempotent(tmp_path):
    store = TodayStore(tmp_path / "today.sqlite3", limit=3)
    store.apply_batch("timmy", [
        {"operation_id": "seed-1", "action": "add", "item_id": "issue:r:1:"},
        {"operation_id": "seed-2", "action": "add", "item_id": "issue:r:2:"},
        {"operation_id": "seed-plan", "action": "configure", "item_id": "plan",
         "capacity_minutes": 180,
         "estimates": {"issue:r:1:": 60, "issue:r:2:": 45}},
    ])
    operation = {
        "operation_id": "roll-2026-08-13", "action": "rollover", "item_id": "plan",
        "plan_date": "2026-08-13", "timezone": "America/New_York",
        "ids": ["issue:r:2:"], "capacity_minutes": 120,
        "estimates": {"issue:r:2:": 40}, "base_revision": 3,
    }

    rolled = store.apply_batch("timmy", [operation])
    replay = store.apply_batch("timmy", [operation])

    assert rolled == {
        "revision": 4, "ids": ["issue:r:2:"], "capacity_minutes": 120,
        "estimates": {"issue:r:2:": 40}, "plan_date": "2026-08-13",
        "timezone": "America/New_York", "accepted_operation_ids": ["roll-2026-08-13"],
        "duplicate_operation_ids": [], "rejected_operations": [],
    }
    assert replay["revision"] == 4
    assert replay["duplicate_operation_ids"] == ["roll-2026-08-13"]
    assert TodayStore(store.path).get("timmy")["plan_date"] == "2026-08-13"
    edited = store.apply("timmy", "after-roll", "add", "issue:r:3:")
    assert edited["plan_date"] == "2026-08-13"
    assert edited["timezone"] == "America/New_York"


def test_stale_rollover_cannot_replace_a_newer_device_plan(tmp_path):
    store = TodayStore(tmp_path / "today.sqlite3")
    store.apply("timmy", "seed", "add", "issue:r:1:")
    store.apply("timmy", "newer", "add", "issue:r:2:")

    result = store.apply_batch("timmy", [{
        "operation_id": "stale-roll", "action": "rollover", "item_id": "plan",
        "plan_date": "2026-08-13", "timezone": "UTC", "ids": ["issue:r:1:"],
        "capacity_minutes": 60, "estimates": {"issue:r:1:": 30}, "base_revision": 1,
    }])

    assert result["ids"] == ["issue:r:1:", "issue:r:2:"]
    assert result["rejected_operations"] == [
        {"operation_id": "stale-roll", "reason": "stale_intent"}
    ]


def test_rollover_rejects_invalid_calendar_metadata_and_duplicate_items(tmp_path):
    store = TodayStore(tmp_path / "today.sqlite3")
    base = {"operation_id": "roll", "action": "rollover", "item_id": "plan",
            "plan_date": "08/13/2026", "timezone": "UTC", "ids": []}
    with pytest.raises(ValueError, match="plan_date"):
        store.apply_batch("timmy", [base])
    with pytest.raises(ValueError, match="unique"):
        store.apply_batch("timmy", [{**base, "plan_date": "2026-08-13",
                                      "ids": ["issue:r:1:", "issue:r:1:"]}])


def test_today_api_model_accepts_a_bounded_rollover():
    operation = main.TodayOperation(
        operation_id="roll", action="rollover", item_id="plan", base_revision=7,
        plan_date="2026-08-13", timezone="America/New_York",
        ids=["issue:r:1:"], capacity_minutes=120, estimates={"issue:r:1:": 45},
    )

    assert operation.model_dump()["plan_date"] == "2026-08-13"
    assert operation.model_dump()["timezone"] == "America/New_York"
    assert operation.model_dump()["ids"] == ["issue:r:1:"]


def test_same_day_is_current_but_prior_and_legacy_plans_require_review():
    script = f"""
const create = require({json.dumps(str(ROLLOVER))});
const rollover = create({{ localDate:()=> '2026-08-13', timeZone:()=> 'America/New_York' }});
process.stdout.write(JSON.stringify([
  rollover.reviewState({{plan_date:'2026-08-13',ids:['one']}}),
  rollover.reviewState({{plan_date:'2026-08-12',ids:['one']}}),
  rollover.reviewState({{ids:['one']}}),
  rollover.reviewState({{ids:[]}}),
]));
"""
    assert run_node(script) == ["current", "stale", "legacy", "empty"]


def test_rollover_preserves_selected_order_and_only_selected_estimates():
    script = f"""
const create = require({json.dumps(str(ROLLOVER))});
const rollover = create({{localDate:()=> '2026-08-13',timeZone:()=> 'America/New_York'}});
process.stdout.write(JSON.stringify(rollover.operation({{
  operation_id:'roll-1', base_revision:7, selected_ids:['two','one'], capacity_minutes:120,
  estimates:{{one:30,two:45,removed:60}}
}})));
"""
    assert run_node(script) == {
        "operation_id": "roll-1", "action": "rollover", "item_id": "plan",
        "base_revision": 7, "plan_date": "2026-08-13",
        "timezone": "America/New_York", "ids": ["two", "one"],
        "capacity_minutes": 120, "estimates": {"one": 30, "two": 45},
    }


def test_calendar_day_comes_from_local_parts_not_elapsed_hours():
    script = f"""
const create = require({json.dumps(str(ROLLOVER))});
const date = new Date('2026-11-01T05:30:00Z');
const rollover = create({{ now:()=>date, resolvedTimeZone:()=> 'America/New_York' }});
process.stdout.write(JSON.stringify({{date:rollover.localDate(),zone:rollover.timeZone()}}));
"""
    assert run_node(script) == {"date": "2026-11-01", "zone": "America/New_York"}


def test_invalid_runtime_timezone_falls_back_to_utc_without_breaking_planners():
    script = f"""
const create = require({json.dumps(str(ROLLOVER))});
const rollover = create({{
  now:()=>new Date('2026-08-20T23:30:00Z'),
  resolvedTimeZone:()=> 'Etc/Unknown'
}});
process.stdout.write(JSON.stringify({{date:rollover.localDate(),zone:rollover.timeZone()}}));
"""
    assert run_node(script) == {"date": "2026-08-20", "zone": "UTC"}


def test_rollover_is_one_durable_sync_operation_with_the_server_revision():
    script = f"""
const createSync = require({json.dumps(str(SYNC))});
const createRollover = require({json.dumps(str(ROLLOVER))});
const values = new Map(); let delivered;
const storage = {{get length(){{return values.size}},key:i=>[...values.keys()][i]||null,
 getItem:k=>values.get(k)||null,setItem:(k,v)=>values.set(k,v),removeItem:k=>values.delete(k)}};
const sync=createSync({{storage,getLogin:()=> 'timmy',createOperationId:()=> 'roll-1',
 fetchJson:async(_url,options)=>{{delivered=JSON.parse(options.body).operations;return {{revision:8,ids:['two'],plan_date:'2026-08-13',timezone:'UTC',accepted_operation_ids:['roll-1'],duplicate_operation_ids:[],rejected_operations:[]}}}},
 onRemoteIds:()=>{{}},onStatus:()=>{{}}}});
const rollover=createRollover({{localDate:()=> '2026-08-13',timeZone:()=> 'UTC'}});
const queued=sync.enqueueRollover(rollover.operation({{operation_id:'ignored',base_revision:7,
 selected_ids:['two'],capacity_minutes:90,estimates:{{two:45}}}}));
(async()=>{{const before=sync.pending();await sync.flush();process.stdout.write(JSON.stringify({{queued,before,delivered,after:sync.pending()}}));}})();
"""
    result = run_node(script)
    assert result["queued"] is True
    assert result["before"] == result["delivered"]
    assert result["before"][0]["operation_id"] == "roll-1"
    assert result["before"][0]["action"] == "rollover"
    assert result["after"] == []


def test_rollover_review_is_wired_into_the_offline_mobile_shell():
    root = Path(__file__).parents[1]
    index = (root / "frontend" / "index.html").read_text()
    worker = (root / "frontend" / "service-worker.js").read_text()
    dashboard = (root / "frontend" / "dashboard.js").read_text()
    css = (root / "frontend" / "dashboard.css").read_text()

    assert '<script src="static/today-rollover.js"></script>' in index
    assert "BASE + 'static/today-rollover.js'" in worker
    assert "todayRollover.reviewState(plan)" in dashboard
    assert "Review yesterday’s unfinished work" in dashboard
    assert "todaySync.enqueueRollover" in dashboard
    assert ".plan-today-header button { min-width:44px; min-height:44px;" in css
