import json
import subprocess
from pathlib import Path

import pytest

from tests.dashboard_bundle import dashboard


DRAFTS = Path(__file__).parents[1] / "frontend" / "drafts.js"


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


def test_draft_inbox_discovers_existing_formats_orders_activity_and_ignores_corruption():
    script = f"""
const createDraftInbox = require({json.dumps(str(DRAFTS))});
const values = new Map([
  ['stackchain.issue-capture.v1', JSON.stringify({{repository:'stackchain/api',title:'Ship recovery',body:'Detailed plan',labelIds:[]}})],
  ['stackchain.issue-comment.v1:stackchain/api#17', 'Please add a regression test'],
  ['stackchain.pull-comment.v1:stackchain/web#9', 'Looks good on mobile'],
  ['stackchain.update-reply.v1.44', 'I can take this'],
  ['stackchain.review-draft.v1:stackchain/web#12@abc123', JSON.stringify({{notes:{{'app.js':'Handle offline state'}},comments:[],summary:'Needs one fix',decision:'request_changes'}})],
  ['stackchain.issue-content.v1:stackchain/api#18', '{{broken-json'],
  ['stackchain.issue-comment.v1:stackchain/api#20:operation', 'not-a-draft'],
]);
const storage = {{
  get length() {{ return values.size; }},
  key: index => Array.from(values.keys())[index] || null,
  getItem: key => values.has(key) ? values.get(key) : null,
  setItem: (key, value) => values.set(key, value),
  removeItem: key => values.delete(key),
}};
let clock = 1000;
const inbox = createDraftInbox({{storage, now:() => ++clock}});
const first = inbox.list();
values.set('stackchain.issue-comment.v1:stackchain/api#17', 'Newest comment text');
const second = inbox.list();
process.stdout.write(JSON.stringify({{
  kinds:first.map(item => item.kind).sort(),
  newest:second[0],
  count:second.length,
}}));
"""
    output = run_node(script)

    assert output["kinds"] == ["issue-comment", "new-issue", "pull-comment", "review", "update-reply"]
    assert output["count"] == 5
    assert output["newest"]["kind"] == "issue-comment"
    assert output["newest"]["repository"] == "stackchain/api"
    assert output["newest"]["number"] == 17
    assert output["newest"]["preview"] == "Newest comment text"
    assert output["newest"]["route"] == {"kind": "issue", "repository": "stackchain/api", "number": 17}


def test_draft_discard_removes_only_selected_content_and_operation_identity():
    script = f"""
const createDraftInbox = require({json.dumps(str(DRAFTS))});
const values = new Map([
  ['stackchain.issue-comment.v1:stackchain/api#17', 'One'],
  ['stackchain.issue-comment.v1:stackchain/api#17:operation', 'operation-one'],
  ['stackchain.pull-comment.v1:stackchain/web#9', 'Two'],
]);
const storage = {{
  get length() {{ return values.size; }}, key:i => Array.from(values.keys())[i] || null,
  getItem:key => values.has(key) ? values.get(key) : null,
  setItem:(key,value) => values.set(key,value), removeItem:key => values.delete(key),
}};
const inbox = createDraftInbox({{storage, now:() => 1000}});
const selected = inbox.list().find(item => item.kind === 'issue-comment');
inbox.discard(selected.id);
process.stdout.write(JSON.stringify({{remaining:inbox.list(), keys:Array.from(values.keys()).sort()}}));
"""
    output = run_node(script)

    assert [item["kind"] for item in output["remaining"]] == ["pull-comment"]
    assert "stackchain.issue-comment.v1:stackchain/api#17" not in output["keys"]
    assert "stackchain.issue-comment.v1:stackchain/api#17:operation" not in output["keys"]
    assert "stackchain.pull-comment.v1:stackchain/web#9" in output["keys"]


def test_draft_inbox_recovers_issue_edit_and_review_content_without_exposing_empty_records():
    script = f"""
const createDraftInbox = require({json.dumps(str(DRAFTS))});
const values = new Map([
 ['stackchain.issue-content.v1:stackchain/api#18', JSON.stringify({{title:'Clarify acceptance',body:'Describe mobile flow',expectedUpdatedAt:'2026-08-07T10:00:00Z'}})],
 ['stackchain.review-draft.v1:stackchain/web#12@abc123', JSON.stringify({{notes:{{}},comments:[],summary:'',decision:'approve'}})],
 ['stackchain.pull-comment.v1:stackchain/web#10', '   '],
]);
const storage = {{get length(){{return values.size}},key:i=>Array.from(values.keys())[i]||null,getItem:k=>values.get(k)||null,setItem:(k,v)=>values.set(k,v),removeItem:k=>values.delete(k)}};
const drafts = createDraftInbox({{storage,now:()=>42}}).list();
process.stdout.write(JSON.stringify(drafts));
"""
    output = run_node(script)

    assert len(output) == 1
    assert output[0]["kind"] == "issue-edit"
    assert output[0]["preview"] == "Clarify acceptance — Describe mobile flow"
    assert output[0]["route"] == {"kind": "issue", "repository": "stackchain/api", "number": 18}


def test_draft_inbox_expands_issue_outbox_items_with_actionable_states():
    script = f"""
const createDraftInbox = require({json.dumps(str(DRAFTS))});
const values = new Map([['stackchain.issue-outbox.v1', JSON.stringify({{version:1,items:[
  {{id:'queued-1',repository:'stackchain/api',title:'Offline capture',body:'Context',status:'queued',queuedAt:100}},
  {{id:'attention-2',repository:'stackchain/web',title:'Fix labels',body:'Details',status:'attention',error:'Unknown label',queuedAt:200}},
]}})]]);
const storage = {{get length(){{return values.size}},key:i=>Array.from(values.keys())[i]||null,getItem:k=>values.get(k)||null,setItem:(k,v)=>values.set(k,v),removeItem:k=>values.delete(k)}};
const drafts = createDraftInbox({{storage,now:()=>300}}).list();
process.stdout.write(JSON.stringify(drafts));
"""
    output = run_node(script)

    assert [item["outbox_id"] for item in output] == ["attention-2", "queued-1"]
    assert [item["status"] for item in output] == ["attention", "queued"]
    assert output[0]["label"] == "Needs attention"
    assert output[1]["label"] == "Queued issue"


def test_draft_inbox_identifies_account_bound_checklist_conflicts_for_review():
    script = f"""
const createDraftInbox = require({json.dumps(str(DRAFTS))});
const record = {{version:2,items:[{{
  id:'check-1',kind:'issue-content',ownerLogin:'timmy',status:'attention',repository:'o/r',number:7,
  title:'Ship',baseBody:'- [ ] Build',body:'- [x] Build',expectedUpdatedAt:'old',error:'Issue changed',queuedAt:12,
}}]}};
const values = new Map([['stackchain.authored-outbox.v1', JSON.stringify(record)]]);
const storage = {{get length(){{return values.size}},key:i=>Array.from(values.keys())[i]||null,getItem:k=>values.get(k)||null,setItem:(k,v)=>values.set(k,v)}};
const owned = createDraftInbox({{storage,getCurrentLogin:()=>'timmy'}}).list()[0];
const other = createDraftInbox({{storage,getCurrentLogin:()=>'alexander'}}).list()[0];
process.stdout.write(JSON.stringify({{owned,other}}));
"""
    output = run_node(script)

    assert output["owned"]["label"] == "Checklist conflict"
    assert output["owned"]["checklist_conflict"] is True
    assert output["owned"]["quarantined"] is False
    assert output["other"]["checklist_conflict"] is False
    assert output["other"]["quarantined"] is True


def test_draft_inbox_exposes_queued_review_with_review_route_and_actionable_state():
    script = f"""
const createDraftInbox = require({json.dumps(str(DRAFTS))});
const values=new Map([['stackchain.authored-outbox.v1',JSON.stringify({{version:2,items:[{{
  id:'review-1',kind:'pull-review',repository:'stackchain/web',number:8,body:'Looks good',
  decision:'approve',expectedHeadSha:'abc',comments:[{{path:'app.js',body:'Nice',new_position:4}}],ownerLogin:'timmy',status:'queued',queuedAt:200
}}]}})]]);
const storage={{get length(){{return values.size}},key:i=>Array.from(values.keys())[i]||null,getItem:k=>values.get(k)||null,setItem:(k,v)=>values.set(k,v)}};
const item=createDraftInbox({{storage,getCurrentLogin:()=>'timmy'}}).list()[0];
process.stdout.write(JSON.stringify(item));
"""
    output = run_node(script)

    assert output["label"] == "Queued review"
    assert output["outbox_kind"] == "pull-review"
    assert output["title"] == "stackchain/web#8"
    assert output["route"] == {"kind": "review", "repository": "stackchain/web", "number": 8}
    assert output["copy_text"] == "Looks good\n\nInline feedback\napp.js · new line 4: Nice"
    assert output["quarantined"] is False


def test_draft_inbox_exposes_held_review_as_awaiting_foreground_authorization():
    script = f"""
const createDraftInbox = require({json.dumps(str(DRAFTS))});
const values=new Map([['stackchain.authored-outbox.v1',JSON.stringify({{version:2,items:[{{
  id:'review-1',kind:'pull-review',repository:'stackchain/web',number:8,body:'Looks good',
  decision:'approve',expectedHeadSha:'abc',comments:[],ownerLogin:'timmy',
  status:'authorization',error:'Fresh authorization required',queuedAt:200
}}]}})]]);
const storage={{get length(){{return values.size}},key:i=>Array.from(values.keys())[i]||null,getItem:k=>values.get(k)||null,setItem:(k,v)=>values.set(k,v)}};
const inbox=createDraftInbox({{storage,getCurrentLogin:()=>'timmy'}});
process.stdout.write(JSON.stringify({{item:inbox.list()[0],partition:inbox.partition()}}));
"""
    output = run_node(script)

    assert output["item"]["status"] == "authorization"
    assert output["item"]["label"] == "Review awaiting authorization"
    assert output["item"]["authorization_required"] is True
    assert output["partition"]["counts"]["authorization"] == 1
    assert output["partition"]["retryable"] == []


def test_draft_inbox_exposes_queued_issue_closure_as_awaiting_authorization():
    script = f"""
const createDraftInbox = require({json.dumps(str(DRAFTS))});
const values=new Map([['stackchain.authored-outbox.v1',JSON.stringify({{version:2,items:[{{
  id:'close-1',kind:'issue-close',repository:'stackchain/dashboard',number:27,body:'',
  ownerLogin:'timmy',status:'queued',queuedAt:200
}}]}})]]);
const storage={{get length(){{return values.size}},key:i=>Array.from(values.keys())[i]||null,getItem:k=>values.get(k)||null,setItem:(k,v)=>values.set(k,v)}};
const inbox=createDraftInbox({{storage,getCurrentLogin:()=>'timmy'}});
const item=inbox.list()[0];
process.stdout.write(JSON.stringify({{item,partition:inbox.partition()}}));
"""
    output = run_node(script)
    item = output["item"]

    assert item["label"] == "Awaiting authorization"
    assert item["status"] == "authorization"
    assert item["authorization_required"] is True
    assert item["outbox_kind"] == "issue-close"
    assert item["title"] == "stackchain/dashboard#27"
    assert item["route"] == {"kind": "issue", "repository": "stackchain/dashboard", "number": 27}
    assert item["quarantined"] is False
    assert output["partition"]["counts"] == {
        "waiting": 0, "sending": 0, "attention": 0, "authorization": 1
    }
    assert output["partition"]["retryable"] == []


def test_delivery_partition_counts_human_action_and_orders_it_before_waiting_work():
    script = f"""
const createDraftInbox = require({json.dumps(str(DRAFTS))});
const values = new Map([
  ['stackchain.issue-outbox.v1', JSON.stringify({{version:3,items:[
    {{id:'waiting',repository:'o/r',title:'Waiting',body:'',ownerLogin:'timmy',status:'queued',queuedAt:500}},
    {{id:'uncertain',repository:'o/r',title:'Verify',body:'',ownerLogin:'timmy',status:'queued',deliveryState:'uncertain',queuedAt:100}},
    {{id:'sending',repository:'o/r',title:'Sending',body:'',ownerLogin:'timmy',status:'sending',queuedAt:400}},
    {{id:'attention',repository:'o/r',title:'Fix me',body:'',ownerLogin:'timmy',status:'attention',queuedAt:200}},
  ]}})],
  ['stackchain.authored-outbox.v1', JSON.stringify({{version:2,items:[
    {{id:'authorize',kind:'pull-review',repository:'o/r',number:8,body:'Review',ownerLogin:'timmy',status:'authorization',queuedAt:300}},
  ]}})],
]);
const storage={{get length(){{return values.size}},key:i=>Array.from(values.keys())[i]||null,getItem:k=>values.get(k)||null,setItem:(k,v)=>values.set(k,v)}};
const partition=createDraftInbox({{storage,getCurrentLogin:()=>'timmy'}}).partition();
process.stdout.write(JSON.stringify({{
  actionable:partition.actionable,
  titles:partition.deliveries.map(item=>item.title),
  waiting:partition.counts.waiting,
  sending:partition.counts.sending,
}}));
"""
    assert run_node(script) == {
        "actionable": 3,
        "titles": ["o/r#8", "Fix me", "Verify", "Waiting", "Sending"],
        "waiting": 2,
        "sending": 1,
    }


def test_draft_inbox_exposes_created_issue_waiting_for_create_and_start_continuation():
    script = f"""
const createDraftInbox = require({json.dumps(str(DRAFTS))});
const values = new Map([['stackchain.issue-outbox.v1', JSON.stringify({{version:3,items:[{{
  id:'continue-1',operationId:'continue-1',repository:'stackchain/dashboard',
  title:'Resume this issue',body:'',ownerLogin:'timmy',status:'completion',queuedAt:200,
  completionIntent:'create-and-start',deliveredIssue:{{repository:'stackchain/dashboard',number:389,title:'Resume this issue'}}
}}]}})]]);
const storage = {{get length(){{return values.size}},key:i=>Array.from(values.keys())[i]||null,getItem:k=>values.get(k)||null}};
const item = createDraftInbox({{storage,getCurrentLogin:()=>'timmy'}}).list()[0];
process.stdout.write(JSON.stringify(item));
"""
    output = run_node(script)

    assert output["status"] == "completion"
    assert output["label"] == "Created · ready to start"
    assert output["continuation"] is True
    assert output["quarantined"] is False


def test_draft_inbox_exposes_uncertain_delivery_for_explicit_user_verification():
    script = f"""
const createDraftInbox = require({json.dumps(str(DRAFTS))});
const values = new Map([['stackchain.authored-outbox.v1', JSON.stringify({{version:2,items:[
  {{id:'uncertain',kind:'issue-comment',repository:'o/r',number:7,body:'Possibly posted',ownerLogin:'timmy',status:'attention',deliveryState:'uncertain',error:'Verify it was not posted before retrying.',queuedAt:200}}
]}})]]);
const storage = {{get length(){{return values.size}},key:i=>Array.from(values.keys())[i]||null,getItem:k=>values.get(k)||null,setItem:(k,v)=>values.set(k,v)}};
const drafts = createDraftInbox({{storage,getCurrentLogin:()=>'timmy'}}).list();
process.stdout.write(JSON.stringify(drafts[0]));
"""
    output = run_node(script)

    assert output["delivery_state"] == "uncertain"
    assert output["label"] == "Verify delivery"


def test_draft_inbox_marks_mismatched_and_legacy_outbox_content_copy_only():
    script = f"""
const createDraftInbox = require({json.dumps(str(DRAFTS))});
const values = new Map([
 ['stackchain.issue-outbox.v1', JSON.stringify({{version:2,items:[
   {{id:'other',repository:'o/r',title:'Other issue',body:'Private context',ownerLogin:'alexander',status:'queued'}},
   {{id:'legacy',repository:'o/r',title:'Legacy issue',body:'Old context',status:'queued'}}
 ]}})],
 ['stackchain.authored-outbox.v1', JSON.stringify({{version:2,items:[
   {{id:'mine',kind:'issue-comment',repository:'o/r',number:2,body:'My comment',ownerLogin:'timmy',status:'queued'}}
 ]}})],
]);
const storage = {{get length(){{return values.size}},key:i=>Array.from(values.keys())[i]||null,getItem:k=>values.get(k)||null,setItem:(k,v)=>values.set(k,v)}};
const drafts = createDraftInbox({{storage,getCurrentLogin:()=>'timmy'}}).list();
process.stdout.write(JSON.stringify(drafts));
"""
    output = run_node(script)

    by_id = {item["outbox_id"]: item for item in output}
    assert by_id["other"]["quarantined"] is True
    assert by_id["other"]["ownership"] == "Queued by alexander — current account is timmy"
    assert by_id["other"]["copy_text"] == "Other issue\n\nPrivate context"
    assert by_id["legacy"]["ownership"] == "Queued by an unknown account — current account is timmy"
    assert by_id["mine"]["quarantined"] is False


def test_draft_inbox_partitions_delivery_center_and_counts_only_safe_waiting_retries():
    script = f"""
const createDraftInbox = require({json.dumps(str(DRAFTS))});
const values = new Map([
 ['stackchain.issue-comment.v1:stackchain/api#17', 'Unfinished comment'],
 ['stackchain.issue-outbox.v1', JSON.stringify({{version:3,items:[
   {{id:'waiting',repository:'o/r',title:'Waiting issue',body:'Body',ownerLogin:'timmy',status:'queued',queuedAt:100,lastAttemptAt:90,lastAttemptError:'Network unavailable'}},
   {{id:'sending',repository:'o/r',title:'Sending issue',body:'Body',ownerLogin:'timmy',status:'sending',queuedAt:200}},
   {{id:'attention',repository:'o/r',title:'Broken issue',body:'Body',ownerLogin:'timmy',status:'attention',queuedAt:300}},
   {{id:'other-user',repository:'o/r',title:'Private issue',body:'Body',ownerLogin:'alexander',status:'queued',queuedAt:400}}
 ]}})],
 ['stackchain.authored-outbox.v1', JSON.stringify({{version:2,items:[
   {{id:'uncertain',kind:'issue-comment',repository:'o/r',number:4,body:'Maybe sent',ownerLogin:'timmy',status:'attention',deliveryState:'uncertain',queuedAt:500}}
 ]}})],
]);
const storage = {{get length(){{return values.size}},key:i=>Array.from(values.keys())[i]||null,getItem:k=>values.get(k)||null,setItem:(k,v)=>values.set(k,v)}};
const inbox = createDraftInbox({{storage,getCurrentLogin:()=>'timmy'}});
const partition=inbox.partition();
process.stdout.write(JSON.stringify({{...partition,labels:partition.deliveries.map(inbox.deliveryLabel)}}));
"""
    output = run_node(script)

    assert len(output["drafts"]) == 1
    assert len(output["deliveries"]) == 5
    assert output["counts"] == {"waiting": 2, "sending": 1, "attention": 2, "authorization": 0}
    assert output["labels"] == [
        "Needs attention", "Identity protected", "Needs attention", "Sending", "Queued for sync"
    ]
    assert [item["outbox_id"] for item in output["retryable"]] == ["waiting"]
    waiting = next(item for item in output["deliveries"] if item["outbox_id"] == "waiting")
    assert waiting["last_attempt_at"] == 90
    assert waiting["last_attempt_error"] == "Network unavailable"


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

    assert '<script src="static/drafts.js"></script>' in html
    assert 'data-work-filter="draft"' in html
    assert 'data-work-count="draft"' in html
    assert 'class="draft-resume"' in html
    assert 'class="draft-discard"' in html
    assert 'Discard this unfinished draft?' in html
    assert '.draft-actions button, .draft-filing-session-actions button, .agenda-replan-actions button { min-height:44px;' in html
    assert 'createDraftInbox({ storage: localStorage' in html
    assert "captureDraft.repository && !issueCaptureRepositories.includes(captureDraft.repository)" in html
    assert "Verified not posted — retry" in html
    assert "const closureOutbox = item.outbox_kind === 'issue-close';" in html
    assert "item.kind === 'authored-outbox' && closureOutbox" in html
    assert "payload.detail?.message" in html
    assert "error.code = payload.detail?.code" in html


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

    assert 'class="delivery-center"' in html
    assert 'id="retry-waiting-deliveries"' in html
    assert 'Waiting <strong data-delivery-count="waiting">' in html
    assert 'Sending <strong data-delivery-count="sending">' in html
    assert 'Needs attention <strong data-delivery-count="attention">' in html
    assert 'Authorize <strong data-delivery-count="authorization">' in html
    assert "const deliveryCenter = draftInbox.partition(displayedDrafts);" in html
    assert "await Promise.all([issueOutbox.flush(activeFlushLogin), authoredOutbox.flush(activeFlushLogin)])" in html
    assert "deliveryCenter.retryable.length" in html
    assert "draftInbox.deliveryLabel(item)" in html
    assert "if (item.status === 'sending') return 'Sending'" in DRAFTS.read_text()
    assert "item.last_attempt_error ? '<span class=\"delivery-attempt small\">Last attempt '" in html
    assert '.delivery-center { display:flex; align-items:center; justify-content:space-between; gap:12px; flex-wrap:wrap;' in html
    assert '.delivery-center button { min-height:44px;' in html
    assert "counts.delivery = draftInbox.partition(lastDrafts).actionable;" in html
    assert "openDelivery: () => mobileDeliveryRecovery.open()" in html
    assert "getItems: () => draftInbox.partition(lastDrafts).deliveries" in html
    assert "authoritativePhases:['delivery']" in html


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

    assert 'class="draft-authorize"' in html
    assert '>Authorize &amp; close</button>' in html
    assert "list.querySelectorAll('.draft-authorize')" in html
    assert "authoredOutbox.retry(item.outbox_id, activeFlushLogin)" in html
    assert "if (item.status === 'authorization') return 'Awaiting authorization'" in DRAFTS.read_text()


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

    assert "let confirmedOwnerLogin = '';" in html
    assert "let activeFlushLogin = '';" in html
    assert "getOwnerLogin: () => confirmedOwnerLogin" in html
    assert "getCurrentLogin: () => activeFlushLogin" in html
    assert "const contextIdentityFresh = !snapshot.context.error && !contextFreshness?.stale &&" in html
    assert "!contextFreshness?.degraded && !contextFreshness?.revalidating;" in html
    assert "activeFlushLogin = contextIdentityFresh ? String(snapshot.context.user?.login || '').trim() : '';" in html
    assert "issueOutbox.flush(activeFlushLogin)" in html
    assert "authoredOutbox.flush(activeFlushLogin)" in html
    assert "activeFlushLogin = '';" in html
    assert 'class="draft-copy"' in html
    assert "navigator.clipboard.writeText(item.copy_text)" in html
    assert "item.quarantined" in html
