import json
import subprocess
from pathlib import Path


ASSIGN_AND_START = Path(__file__).parents[1] / "frontend" / "assign-and-start.js"
FILED_CLAIM = Path(__file__).parents[1] / "frontend" / "filed-claim.js"
QUEUE_TODAY = Path(__file__).parents[1] / "frontend" / "queue-today.js"
HTML = Path(__file__).parents[1] / "frontend" / "index.html"
DASHBOARD = Path(__file__).parents[1] / "frontend" / "dashboard.js"
CSS = Path(__file__).parents[1] / "frontend" / "dashboard.css"
WORKER = Path(__file__).parents[1] / "frontend" / "service-worker.js"


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


def test_filed_claim_actions_require_open_authored_unassigned_canonical_issue():
    script = f"""
const eligible=require({json.dumps(str(FILED_CLAIM))});
const item={{is_filed:true,is_assigned:false,is_completed:false,state:'open'}};
process.stdout.write(JSON.stringify({{
  eligible:eligible(item, {{state:'open',assignees:[]}}),
  assigned:eligible(item, {{state:'open',assignees:['alex']}}),
  closed:eligible(item, {{state:'closed',assignees:[]}}),
  ordinary:eligible({{...item,is_filed:false}}, {{state:'open',assignees:[]}}),
}}));
"""

    assert run_node(script) == {
        "eligible": True,
        "assigned": False,
        "closed": False,
        "ordinary": False,
    }


def test_filed_sheet_exposes_phone_safe_claim_actions_after_canonical_detail():
    html = HTML.read_text()
    dashboard = DASHBOARD.read_text()
    css = CSS.read_text()
    worker = WORKER.read_text()

    assert 'id="filed-claim-actions"' in html
    assert 'id="queue-filed-issue"' in html
    assert 'id="start-filed-issue"' in html
    assert "filedClaimEligible(item, detail)" in dashboard
    assert "filedAssignAndStart.run(item, { destination:'queue' })" in dashboard
    assert "filedAssignAndStart.run(item)" in dashboard
    assert "Claim failed; this issue is still in Filed." in dashboard
    assert ".filed-claim-actions" in css
    action_rule = css.split(".filed-claim-actions", 1)[1].split("}", 1)[0]
    assert "grid-template-columns:repeat(2,minmax(0,1fr))" in action_rule
    assert "width:min(560px,100%)" in action_rule
    assert "BASE + 'static/filed-claim.js'" in worker
    assert '<script src="static/filed-claim.js"></script>' in html


def test_repeated_assign_and_start_taps_claim_once_then_start_confirmed_issue():
    script = f"""
const fs=require('fs');
if (!fs.existsSync({json.dumps(str(ASSIGN_AND_START))})) {{
  process.stdout.write(JSON.stringify({{available:false}}));
}} else {{
  const createAssignAndStart=require({json.dumps(str(ASSIGN_AND_START))});
  const calls=[];
  let release;
  const claimResult=new Promise(resolve=>{{release=resolve;}});
  const flow=createAssignAndStart({{
    available:()=>true,
    claim:item=>{{calls.push('claim:'+item.number);return claimResult;}},
    start:item=>{{calls.push('start:'+item.number);return 'started';}},
    recover:item=>calls.push('recover:'+item.number),
    announce:message=>calls.push('announce:'+message),
  }});
  const issue={{repository:'stackchain/dashboard',number:411}};
  const first=flow.run(issue);
  const second=flow.run(issue);
  release({{...issue,assignees:['timmy']}});
  Promise.all([first,second]).then(results=>process.stdout.write(JSON.stringify({{available:true,calls,results,same:first===second}})));
}}
"""

    assert run_node(script) == {
        "available": True,
        "calls": [
            "claim:411",
            "start:411",
            "announce:Assigned, added to Today, and ready to work.",
        ],
        "results": ["started", "started"],
        "same": True,
    }


def test_full_today_blocks_before_assignment_and_explains_recovery():
    script = f"""
const createAssignAndStart=require({json.dumps(str(ASSIGN_AND_START))});
const calls=[];
const flow=createAssignAndStart({{
  available:()=>false,
  claim:()=>{{calls.push('claim');return Promise.resolve({{}});}},
  start:()=>calls.push('start'),
  recover:()=>calls.push('recover'),
  announce:message=>calls.push('announce:'+message),
}});
flow.run({{repository:'stackchain/dashboard',number:411}}).then(result=>
  process.stdout.write(JSON.stringify({{result,calls}}))
);
"""

    assert run_node(script) == {
        "result": "full",
        "calls": ["announce:Today is full—remove an item before assigning this issue."],
    }


def test_confirmed_assignment_recovers_in_issue_detail_when_today_start_fails():
    script = f"""
const createAssignAndStart=require({json.dumps(str(ASSIGN_AND_START))});
const calls=[];
const issue={{repository:'stackchain/dashboard',number:411,assignees:['timmy']}};
const flow=createAssignAndStart({{
  available:()=>true,
  claim:()=>Promise.resolve(issue),
  start:()=> 'sync-unavailable',
  recover:item=>calls.push('recover:'+item.number),
  announce:message=>calls.push('announce:'+message),
}});
flow.run(issue).then(result=>process.stdout.write(JSON.stringify({{result,calls}})));
"""

    assert run_node(script) == {
        "result": "recovery",
        "calls": [
            "announce:Assigned to you, but Today could not start. The issue is open so you can recover.",
            "recover:411",
        ],
    }


def test_failed_assignment_does_not_start_today_and_can_be_retried():
    script = f"""
const createAssignAndStart=require({json.dumps(str(ASSIGN_AND_START))});
const calls=[];let attempt=0;
const issue={{repository:'stackchain/dashboard',number:411}};
const flow=createAssignAndStart({{
  available:()=>true,
  claim:()=>{{attempt+=1;calls.push('claim:'+attempt);return attempt===1 ? Promise.reject(new Error('conflict')) : Promise.resolve(issue);}},
  start:()=>{{calls.push('start');return 'started';}},
  recover:()=>calls.push('recover'),
  announce:message=>calls.push('announce:'+message),
}});
flow.run(issue).catch(error=>{{
  calls.push('error:'+error.message);
  return flow.run(issue);
}}).then(result=>process.stdout.write(JSON.stringify({{result,calls}})));
"""

    assert run_node(script) == {
        "result": "started",
        "calls": [
            "claim:1",
            "error:conflict",
            "claim:2",
            "start",
            "announce:Assigned, added to Today, and ready to work.",
        ],
    }


def test_already_owned_search_result_starts_without_claiming():
    script = f"""
const createAssignAndStart=require({json.dumps(str(ASSIGN_AND_START))});
const calls=[];
const issue={{repository:'stackchain/dashboard',number:413,assigned_to_me:true}};
const flow=createAssignAndStart({{
  available:()=>true,
  claim:()=>{{calls.push('claim');return Promise.resolve(issue);}},
  start:item=>{{calls.push('start:'+item.number);return 'started';}},
  recover:item=>calls.push('recover:'+item.number),
  announce:message=>calls.push('announce:'+message),
}});
flow.run(issue, {{alreadyOwned:true}}).then(result=>
  process.stdout.write(JSON.stringify({{result,calls}}))
);
"""

    assert run_node(script) == {
        "result": "started",
        "calls": [
            "start:413",
            "announce:Added to Today and ready to work.",
        ],
    }


def test_queue_today_claims_once_without_starting_the_active_session():
    script = f"""
const createAssignAndStart=require({json.dumps(str(ASSIGN_AND_START))});
const calls=[];
let release;
const claimResult=new Promise(resolve=>{{release=resolve;}});
const issue={{repository:'stackchain/dashboard',number:415}};
const flow=createAssignAndStart({{
  available:()=>true,
  claim:item=>{{calls.push('claim:'+item.number);return claimResult;}},
  start:item=>{{calls.push('start:'+item.number);return 'started';}},
  queue:item=>{{calls.push('queue:'+item.number);return 'queued';}},
  recover:item=>calls.push('recover:'+item.number),
  announce:message=>calls.push('announce:'+message),
}});
const first=flow.run(issue, {{destination:'queue'}});
const second=flow.run(issue, {{destination:'queue'}});
release({{...issue,assignees:['timmy']}});
Promise.all([first,second]).then(results=>process.stdout.write(JSON.stringify({{
  calls,results,same:first===second
}})));
"""

    assert run_node(script) == {
        "calls": [
            "claim:415",
            "queue:415",
            "announce:Assigned and queued in Today. Keep finding work when ready.",
        ],
        "results": ["queued", "queued"],
        "same": True,
    }


def test_queue_today_persists_syncs_and_warms_without_a_session_side_effect():
    script = f"""
const fs=require('fs');
if (!fs.existsSync({json.dumps(str(QUEUE_TODAY))})) {{
  process.stdout.write(JSON.stringify({{available:false}}));
}} else {{
  const createQueueToday=require({json.dumps(str(QUEUE_TODAY))});
  const calls=[];
  const issue={{kind:'issue',repository:'stackchain/dashboard',number:415}};
  const queue=createQueueToday({{
    todayWork:{{identity:()=> 'issue:stackchain/dashboard:415:',add:()=>{{calls.push('add');return 'added';}}}},
    todaySync:{{enqueue:(action,id)=>{{calls.push(action+':'+id);return true;}},flush:()=>calls.push('flush')}},
    refresh:()=>calls.push('refresh'),
    warm:()=>calls.push('warm'),
  }});
  process.stdout.write(JSON.stringify({{available:true,result:queue(issue),calls}}));
}}
"""

    assert run_node(script) == {
        "available": True,
        "result": "queued",
        "calls": [
            "add",
            "add:issue:stackchain/dashboard:415:",
            "refresh",
            "flush",
            "warm",
        ],
    }


def test_queue_today_reports_assignment_when_local_planning_needs_recovery():
    script = f"""
const createAssignAndStart=require({json.dumps(str(ASSIGN_AND_START))});
const calls=[];
const issue={{repository:'stackchain/dashboard',number:415,assignees:['timmy']}};
const flow=createAssignAndStart({{
  available:()=>true,
  claim:()=>Promise.resolve(issue),
  start:()=> 'started',
  queue:()=> 'sync-unavailable',
  recover:item=>calls.push('recover:'+item.number),
  announce:message=>calls.push('announce:'+message),
}});
flow.run(issue, {{destination:'queue'}}).then(result=>
  process.stdout.write(JSON.stringify({{result,calls}}))
);
"""

    assert run_node(script) == {
        "result": "recovery",
        "calls": [
            "announce:Assigned and saved in Today on this device, but account sync is unavailable. The issue is open so you can recover.",
            "recover:415",
        ],
    }


def test_find_work_renders_phone_safe_assign_and_start_and_wires_offline_shell():
    html = HTML.read_text()
    dashboard = DASHBOARD.read_text()
    css = CSS.read_text()
    worker = WORKER.read_text()

    assert "data-claim-start-index" in dashboard
    assert "data-claim-queue-index" in dashboard
    assert ">Start now</button>" in dashboard
    assert ">Queue Today</button>" in dashboard
    assert "const assignAndStart = createAssignAndStart({" in dashboard
    assert "assignAndStart.run(item)" in dashboard
    assert "assignAndStart.run(item, { destination:'queue' })" in dashboard
    assert "queue: confirmed =>" in dashboard
    assert "const queueToday = createQueueToday({" in dashboard
    assert "return queueToday(claimed)" in dashboard
    assert "createAndStart.available" in dashboard
    assert "createAndStart.complete" in dashboard
    assert ".find-work-claim-actions" in css
    assert "grid-template-columns:repeat(2,minmax(0,1fr))" in css
    assert ".find-work-claim-actions [data-claim-start-index]" in css
    assert "grid-column:1 / -1" in css
    assert "BASE + 'static/assign-and-start.js'" in worker
    assert "BASE + 'static/queue-today.js'" in worker
    assert '<script src="static/queue-today.js"></script>' in html
