import json
import subprocess
from pathlib import Path

import httpx
import pytest

from src import main
from tests.dashboard_bundle import dashboard


SYNC = Path(__file__).parents[1] / "frontend" / "background-issue-sync.js"


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


def test_search_reply_reconnect_uses_preview_authorization_and_resumes_photo_checkpoints():
    script = f"""
const createBackgroundIssueSync=require({json.dumps(str(SYNC))});
let item={{id:'search-42',operationId:'search-42',ownerLogin:'timmy',status:'queued',outboxLane:'authored',
  kind:'search-reply',targetKind:'pull',repository:'stackchain/web',number:42,body:'Ready offline.',attachments:[
    {{filename:'one.jpg',contentType:'image/jpeg',blob:new Blob(['one']),operationId:'photo-one',confirmed:{{markdown:'![one](saved)'}}}},
    {{filename:'two.jpg',contentType:'image/jpeg',blob:new Blob(['two']),operationId:'photo-two'}},
  ]}};
const calls=[];let commentAttempts=0;
const store={{claimNext:async()=>item?{{...item}}:null,update:async(_id,transform)=>{{item=transform(item);}},
  complete:async()=>{{item=null;}},release:async()=>{{item={{...item,status:'queued'}};}},
  fail:async()=>{{}},countBlocked:async()=>0}};
const fetchJson=async(url,options={{}})=>{{
  if(url==='api/v1/background-identity')return{{login:'timmy'}};
  const file=options.body instanceof FormData?options.body.get('file'):null;
  calls.push({{url,key:options.headers?.['Idempotency-Key'],filename:file?.name||'',
    body:file?null:JSON.parse(options.body)}});
  if(file)return{{markdown:'![two](uploaded)'}};
  if(commentAttempts++===0){{const error=new Error('offline');error.status=503;throw error;}}
  return{{id:91,body:'Ready offline.'}};
}};
(async()=>{{const sync=createBackgroundIssueSync({{store,fetchJson}});let first='';
  try{{await sync.flush();}}catch(error){{first=error.message;}}
  const checkpoint={{...item}};const second=await sync.flush();
  process.stdout.write(JSON.stringify({{calls,first,checkpoint,second}}));
}})();
"""
    output = run_node(script)

    assert output["first"] == "offline"
    assert output["checkpoint"]["attachmentMarkdowns"] == [
        "![one](saved)", "![two](uploaded)"
    ]
    uploads = [call for call in output["calls"] if call["filename"]]
    assert [(call["filename"], call["key"]) for call in uploads] == [
        ("two.jpg", "photo-two")
    ]
    comments = [call for call in output["calls"] if not call["filename"]]
    assert [call["url"] for call in comments] == [
        "api/v1/repos/stackchain/web/issues/42/preview/comments?kind=pull",
        "api/v1/repos/stackchain/web/issues/42/preview/comments?kind=pull",
    ]
    assert [call["key"] for call in comments] == ["search-42:comment"] * 2
    assert comments[-1]["body"] == {
        "body": "Ready offline.\n\n![one](saved)\n\n![two](uploaded)"
    }
    assert output["second"]["confirmed"] == [{"id": 91, "body": "Ready offline."}]


def test_closed_app_sync_delivers_matching_issue_once_with_original_idempotency_key():
    script = f"""
const createBackgroundIssueSync = require({json.dumps(str(SYNC))});
const item = {{
  id:'capture-1', operationId:'capture-1', ownerLogin:'timmy', status:'queued',
  repository:'stackchain/api', title:'Offline report', body:'Full context',
  labelIds:[3], milestoneId:4, dueDate:'2026-08-09', queuedAt:100,
}};
const state = {{item, completed:[], released:[], failed:[], calls:[]}};
const store = {{
  claimNext: async owner => state.item && state.item.ownerLogin === owner ? {{...state.item}} : null,
  complete: async id => {{ state.completed.push(id); state.item = null; }},
  release: async id => state.released.push(id),
  fail: async (id, message) => state.failed.push({{id,message}}),
}};
const fetchJson = async (url, options = {{}}) => {{
  state.calls.push({{url,options}});
  if (url === 'api/v1/background-identity') return {{login:'timmy'}};
  return {{repository:'stackchain/api',number:251,title:'Offline report'}};
}};
(async () => {{
  const sync = createBackgroundIssueSync({{store,fetchJson}});
  const result = await sync.flush();
  process.stdout.write(JSON.stringify({{state,result}}));
}})();
"""
    output = run_node(script)

    assert output["result"]["confirmed"][0]["number"] == 251
    assert output["state"]["completed"] == ["capture-1"]
    assert output["state"]["released"] == []
    assert output["state"]["failed"] == []
    assert output["state"]["calls"][0]["url"] == "api/v1/background-identity"
    mutation = output["state"]["calls"][1]
    assert mutation["url"] == "api/v1/repos/stackchain/api/issues"
    assert mutation["options"]["headers"]["Idempotency-Key"] == "capture-1"
    assert json.loads(mutation["options"]["body"]) == {
        "title": "Offline report",
        "body": "Full context",
        "label_ids": [3],
        "milestone_id": 4,
        "due_date": "2026-08-09T23:59:59Z",
    }


def test_closed_app_sync_preserves_explicit_no_owner():
    script = f"""
const createBackgroundIssueSync=require({json.dumps(str(SYNC))});
let item={{id:'unowned-1',operationId:'unowned-1',ownerLogin:'timmy',status:'queued',repository:'o/r',title:'Backlog capture',body:'',labelIds:[],unassigned:true}};
const calls=[];const store={{claimNext:async()=>item,complete:async()=>{{item=null;}},release:async()=>{{}},fail:async()=>{{}}}};
const fetchJson=async(url,options={{}})=>{{calls.push({{url,body:options.body||''}});return url==='api/v1/background-identity'?{{login:'timmy'}}:{{repository:'o/r',number:19,assignees:[]}};}};
(async()=>{{await createBackgroundIssueSync({{store,fetchJson}}).flush();process.stdout.write(JSON.stringify(calls));}})();
"""
    calls = run_node(script)
    assert json.loads(calls[1]["body"]) == {
        "title": "Backlog capture", "body": "", "label_ids": [], "unassigned": True
    }


def test_closed_app_issue_receipt_uses_the_confirmed_canonical_gitea_url():
    script = f"""
const createBackgroundIssueSync=require({json.dumps(str(SYNC))});
let item={{id:'unowned-link',operationId:'unowned-link',ownerLogin:'timmy',status:'queued',repository:'o/r',title:'Backlog capture',body:'',labelIds:[],unassigned:true}};
const store={{claimNext:async()=>item,complete:async()=>{{item=null;}},release:async()=>{{}},fail:async()=>{{}},countBlocked:async()=>0}};
const fetchJson=async url=>url==='api/v1/background-identity'?{{login:'timmy'}}:{{repository:'o/r',number:19,title:'Backlog capture',assignees:[],url:'https://forge.example/git/o/r/issues/19'}};
(async()=>{{const result=await createBackgroundIssueSync({{store,fetchJson}}).flush();process.stdout.write(JSON.stringify(result.receipts[0]));}})();
"""
    receipt = run_node(script)

    assert receipt == {
        "id": "unowned-link",
        "status": "confirmed",
        "kind": "issue",
        "url": "https://forge.example/git/o/r/issues/19",
    }


def test_closed_app_sync_delivers_revision_checked_issue_content():
    script = f"""
const createBackgroundIssueSync = require({json.dumps(str(SYNC))});
let item = {{
  id:'check-1',operationId:'check-1',kind:'issue-content',ownerLogin:'timmy',status:'queued',
  repository:'stackchain/dashboard',number:17,title:'Ship',body:'- [x] Test',expectedUpdatedAt:'2026-08-15T10:00:00Z',
}};
const state={{calls:[],completed:[]}};
const store={{
  claimNext:async()=>item?{{...item}}:null,
  complete:async id=>{{state.completed.push(id);item=null;}},
  release:async()=>{{}},fail:async()=>{{}},countBlocked:async()=>0,
}};
const fetchJson=async(url,options={{}})=>{{
  state.calls.push({{url,method:options.method,key:options.headers?.['Idempotency-Key']||'',body:options.body?JSON.parse(options.body):null}});
  if(url==='api/v1/background-identity') return {{login:'timmy'}};
  return {{repository:'stackchain/dashboard',number:17,title:'Ship',body:'- [x] Test',updated_at:'2026-08-15T11:00:00Z'}};
}};
(async()=>{{const result=await createBackgroundIssueSync({{store,fetchJson}}).flush();process.stdout.write(JSON.stringify({{state,result}}));}})();
"""
    output = run_node(script)

    assert output["state"]["calls"][1] == {
        "url": "api/v1/repos/stackchain/dashboard/issues/17/content",
        "method": "PATCH",
        "key": "check-1",
        "body": {
            "title": "Ship", "body": "- [x] Test",
            "expected_updated_at": "2026-08-15T10:00:00Z",
        },
    }
    assert output["state"]["completed"] == ["check-1"]
    assert output["result"]["confirmed"][0]["body"] == "- [x] Test"


def test_active_issue_content_delivery_rejects_a_false_durable_replacement():
    script = f"""
const createBackgroundIssueSync = require({json.dumps(str(SYNC))});
const records = new Map([['check-1', {{
  id:'check-1',operationId:'old-op',kind:'issue-content',outboxLane:'authored',
  ownerLogin:'timmy',status:'sending',claimUntil:200,body:'- [x] Build\\n- [ ] Test'
}}]]); let tail=Promise.resolve();
const transaction=work=>{{const run=tail.then(()=>work({{
  getAll:async()=>[...records.values()].map(value=>({{...value}})),
  put:async value=>records.set(value.id,{{...value}}),delete:async id=>records.delete(id),
}}));tail=run.catch(()=>{{}});return run;}};
(async()=>{{
  const store=createBackgroundIssueSync.createIssueSyncStore({{transaction,now:()=>100}});
  let error='';
  try {{ await store.reconcile([{{
    id:'check-1',operationId:'new-op',kind:'issue-content',ownerLogin:'timmy',status:'queued',
    body:'- [x] Build\\n- [x] Test'
  }}], 'authored'); }} catch (caught) {{ error=caught.message; }}
  process.stdout.write(JSON.stringify({{error,snapshot:await store.snapshot()}}));
}})();
"""
    output = run_node(script)

    assert output["error"] == "Checklist sync is already delivering. Retry this change."
    assert output["snapshot"][0]["operationId"] == "old-op"
    assert output["snapshot"][0]["body"] == "- [x] Build\n- [ ] Test"


def test_closed_app_sync_delivers_desired_blocker_state():
    script = f"""
const createBackgroundIssueSync = require({json.dumps(str(SYNC))});
let item = {{
  id:'blocker-1',operationId:'blocker-1',kind:'issue-blocker',ownerLogin:'timmy',status:'queued',
  repository:'stackchain/dashboard',number:17,blockerRepository:'stackchain/api',blockerNumber:9,present:false,
}};
const state={{calls:[],completed:[]}};
const store={{
  claimNext:async()=>item?{{...item}}:null,
  complete:async id=>{{state.completed.push(id);item=null;}},
  release:async()=>{{}},fail:async()=>{{}},countBlocked:async()=>0,
}};
const fetchJson=async(url,options={{}})=>{{
  state.calls.push({{url,method:options.method,key:options.headers?.['Idempotency-Key']||'',body:options.body?JSON.parse(options.body):null}});
  if(url==='api/v1/background-identity')return{{login:'timmy'}};
  return{{repository:'stackchain/dashboard',number:17,dependencies_available:true,dependencies:[]}};
}};
(async()=>{{const result=await createBackgroundIssueSync({{store,fetchJson}}).flush();process.stdout.write(JSON.stringify({{state,result}}));}})();
"""
    output = run_node(script)

    assert output["state"]["calls"][1] == {
        "url": "api/v1/repos/stackchain/dashboard/issues/17/blockers",
        "method": "PATCH",
        "key": "blocker-1",
        "body": {"repository": "stackchain/api", "number": 9, "present": False},
    }
    assert output["state"]["completed"] == ["blocker-1"]
    assert output["result"]["confirmed"][0]["dependencies"] == []


def test_closed_app_sync_keeps_unconfirmed_blocker_change_actionable():
    script = f"""
const createBackgroundIssueSync = require({json.dumps(str(SYNC))});
let item={{id:'blocker-2',operationId:'blocker-2',kind:'issue-blocker',ownerLogin:'timmy',status:'queued',repository:'o/r',number:1,blockerRepository:'o/b',blockerNumber:2,present:false}};
const state={{completed:[],failed:[]}};
const store={{claimNext:async()=>item?{{...item}}:null,complete:async id=>{{state.completed.push(id);item=null;}},release:async()=>{{}},fail:async(id,message)=>{{state.failed.push([id,message]);item=null;}},countBlocked:async()=>0}};
const fetchJson=async url=>url==='api/v1/background-identity'?{{login:'timmy'}}:{{repository:'o/r',number:1,dependencies_available:true,dependencies:[{{repository:'o/b',number:2}}]}};
(async()=>{{const result=await createBackgroundIssueSync({{store,fetchJson}}).flush();process.stdout.write(JSON.stringify({{state,result}}));}})();
"""
    output = run_node(script)

    assert output["state"]["completed"] == []
    assert output["state"]["failed"] == [["blocker-2", "Blocker change was not confirmed."]]
    assert output["result"]["attention"] == 1


def test_capture_attachment_retry_resumes_after_creation_without_duplicate_issue():
    script = f"""
const createBackgroundIssueSync = require({json.dumps(str(SYNC))});
let item = {{
  id:'capture-image',operationId:'capture-image',ownerLogin:'timmy',status:'queued',
  repository:'stackchain/dashboard',title:'Broken mobile layout',body:'At 320px',labelIds:[],
  attachment:{{filename:'phone.png',contentType:'image/png',data:'iVBORw0KGgo='}},
}};
const state = {{calls:[],completed:0,released:0}};
const store = {{
  claimNext:async()=>item ? {{...item}} : null,
  update:async(_id,transform)=>{{item=transform(item);}},
  complete:async()=>{{state.completed+=1;item=null;}},
  release:async()=>{{state.released+=1;item={{...item,status:'queued'}};}},
  fail:async()=>{{}},countBlocked:async()=>0,
}};
let uploadAttempts=0;
const fetchJson=async(url,options={{}})=>{{
  let body=null;
  if(options.body instanceof FormData){{const file=options.body.get('file');body={{filename:file.name,multipart:true}};}}
  else if(options.body)body=JSON.parse(options.body);
  state.calls.push({{url,key:options.headers?.['Idempotency-Key'],body}});
  if(url==='api/v1/background-identity')return{{login:'timmy'}};
  if(url.endsWith('/issues'))return{{repository:'stackchain/dashboard',number:469,title:'Broken mobile layout'}};
  if(url.endsWith('/attachments') && uploadAttempts++ === 0){{const error=new Error('Upload unavailable');error.status=503;throw error;}}
  if(url.endsWith('/attachments'))return{{markdown:'![phone.png](<https://forge.example/a/phone.png>)'}};
  if(url.endsWith('/comments'))return{{id:91}};
}};
(async()=>{{
  const sync=createBackgroundIssueSync({{store,fetchJson}});
  let firstError='';try{{await sync.flush();}}catch(error){{firstError=error.message;}}
  const afterFirst={{...item}};
  const second=await sync.flush();
  process.stdout.write(JSON.stringify({{state,firstError,afterFirst,second}}));
}})();
"""
    output = run_node(script)

    assert output["firstError"] == "Upload unavailable"
    assert output["afterFirst"]["deliveredIssue"]["number"] == 469
    assert [call["url"] for call in output["state"]["calls"]].count(
        "api/v1/repos/stackchain/dashboard/issues"
    ) == 1
    assert output["state"]["calls"][-2]["key"] == "capture-image:attachment"
    assert output["state"]["calls"][-1]["key"] == "capture-image:attachment-comment"
    assert output["state"]["calls"][-1]["body"] == {
        "body": "![phone.png](<https://forge.example/a/phone.png>)"
    }
    assert output["state"]["completed"] == 1
    assert output["second"]["confirmed"][0]["number"] == 469


def test_closed_app_capture_retries_only_unfinished_blockers_after_creation():
    script = f"""
const createBackgroundIssueSync=require({json.dumps(str(SYNC))});
let item={{id:'blocked',operationId:'blocked',ownerLogin:'timmy',status:'queued',repository:'o/r',title:'Blocked',body:'',labelIds:[],blockers:[
  {{repository:'o/api',number:7,title:'API'}},{{repository:'o/web',number:8,title:'Web'}}
]}};
const calls=[];let secondAttempts=0;
const store={{claimNext:async()=>item?{{...item}}:null,update:async(_id,fn)=>{{item=fn(item);}},complete:async()=>{{item=null;}},release:async()=>{{item={{...item,status:'queued'}};}},fail:async()=>{{}},countBlocked:async()=>0}};
const fetchJson=async(url,options={{}})=>{{
  if(url==='api/v1/background-identity')return{{login:'timmy'}};
  const body=options.body?JSON.parse(options.body):null;calls.push({{url,key:options.headers?.['Idempotency-Key'],body}});
  if(url.endsWith('/issues'))return{{number:42,repository:'o/r'}};
  if(body.number===8 && secondAttempts++===0){{const e=new Error('offline');e.status=503;throw e;}}
  return{{number:42,dependencies_available:true,dependencies:[body]}};
}};
(async()=>{{const sync=createBackgroundIssueSync({{store,fetchJson}});let first='';try{{await sync.flush();}}catch(e){{first=e.message;}}const checkpoint={{...item}};const result=await sync.flush();process.stdout.write(JSON.stringify({{first,checkpoint,calls,result}}));}})();
"""
    output = run_node(script)
    assert output["first"] == "offline"
    assert output["checkpoint"]["deliveredIssue"]["number"] == 42
    assert output["checkpoint"]["deliveredBlockers"] == 1
    assert [call["url"] for call in output["calls"]].count("api/v1/repos/o/r/issues") == 1
    blockers = [call for call in output["calls"] if call["url"].endswith("/blockers")]
    assert [call["body"]["number"] for call in blockers] == [7, 8, 8]
    assert output["result"]["confirmed"][0]["number"] == 42


def test_evidence_bundle_retry_resumes_at_failed_image_and_posts_one_ordered_comment():
    script = f"""
const createBackgroundIssueSync=require({json.dumps(str(SYNC))});
let item={{id:'bundle',operationId:'bundle',ownerLogin:'timmy',status:'queued',repository:'o/r',title:'Journey',body:'',labelIds:[],attachments:[
  {{filename:'one.png',contentType:'image/png',data:'b25l',note:'First state'}},
  {{filename:'two.png',contentType:'image/png',data:'dHdv',note:'Tap [Submit](unsafe)'}},
  {{filename:'three.png',contentType:'image/png',data:'dGhyZWU='}},
]}};
const calls=[];let twoAttempts=0;
const store={{claimNext:async()=>item?{{...item}}:null,update:async(_id,fn)=>{{item=fn(item);}},complete:async()=>{{item=null;}},release:async()=>{{item={{...item,status:'queued'}};}},fail:async()=>{{}},countBlocked:async()=>0}};
const fetchJson=async(url,options={{}})=>{{
  if(url==='api/v1/background-identity')return{{login:'timmy'}};
  let filename=null;if(options.body instanceof FormData)filename=options.body.get('file').name;
  calls.push({{url,key:options.headers?.['Idempotency-Key'],filename,body:options.body instanceof FormData?null:JSON.parse(options.body)}});
  if(url.endsWith('/issues'))return{{number:823}};
  if(url.endsWith('/attachments')){{if(filename==='two.png' && twoAttempts++===0){{const e=new Error('offline');e.status=503;throw e;}}return{{markdown:'!['+filename+'](url/'+filename+')'}};}}
  return{{id:9}};
}};
(async()=>{{const sync=createBackgroundIssueSync({{store,fetchJson}});let first='';try{{await sync.flush();}}catch(e){{first=e.message;}}const checkpoint={{...item}};await sync.flush();process.stdout.write(JSON.stringify({{first,checkpoint,calls}}));}})();
"""
    output = run_node(script)
    assert output["first"] == "offline"
    uploads = [call for call in output["calls"] if call["filename"]]
    assert [call["filename"] for call in uploads] == ["one.png", "two.png", "two.png", "three.png"]
    assert [call["key"] for call in uploads] == [
        "bundle:attachment-0", "bundle:attachment-1", "bundle:attachment-1", "bundle:attachment-2"
    ]
    assert output["checkpoint"]["attachmentMarkdowns"] == ["![one.png](url/one.png)"]
    comments = [call for call in output["calls"] if call["url"].endswith("/comments")]
    assert len(comments) == 1
    assert comments[0]["body"]["body"].split("\n\n") == [
        "**Screenshot 1 — First state**", "![one.png](url/one.png)",
        "**Screenshot 2 — Tap \\[Submit\\]\\(unsafe\\)**", "![two.png](url/two.png)",
        "![three.png](url/three.png)"
    ]



def test_closed_app_sync_uploads_blob_as_multipart_and_preserves_original_bytes():
    script = f"""
const createBackgroundIssueSync=require({json.dumps(str(SYNC))});
const blob=new Blob(['offline-binary'],{{type:'image/png'}});
const item={{id:'binary',operationId:'binary',ownerLogin:'timmy',repository:'o/r',title:'Visual',body:'',labelIds:[],
  attachment:{{filename:'phone.png',contentType:'image/png',blob}}}};
let queued=true;
const observed={{}};
const store={{claimNext:async()=>queued?(queued=false,{{...item}}):null,update:async()=>{{}},complete:async()=>{{}},release:async()=>{{}},fail:async()=>{{}},countBlocked:async()=>0}};
const fetchJson=async(url,options={{}})=>{{
  if(url==='api/v1/background-identity')return{{login:'timmy'}};
  if(url.endsWith('/issues'))return{{number:7}};
  if(url.endsWith('/attachments')){{const file=options.body.get('file');Object.assign(observed,{{
    isFormData:options.body instanceof FormData,contentTypeHeader:options.headers['Content-Type']||null,
    filename:file.name,type:file.type,size:file.size,text:await file.text()
  }});return{{markdown:'![phone](url)'}};}}
  return{{id:8}};
}};
(async()=>{{await createBackgroundIssueSync({{store,fetchJson}}).flush();process.stdout.write(JSON.stringify(observed));}})();
"""

    assert run_node(script) == {
        "isFormData": True,
        "contentTypeHeader": None,
        "filename": "phone.png",
        "type": "image/png",
        "size": 14,
        "text": "offline-binary",
    }


def test_capture_attachment_stage_keys_remain_within_idempotency_limit():
    script = f"""
const createBackgroundIssueSync=require({json.dumps(str(SYNC))});
const item={{id:'long',operationId:'x'.repeat(128),ownerLogin:'timmy',repository:'o/r',title:'Bug',body:'',labelIds:[],attachment:{{filename:'a.png',contentType:'image/png',data:'abc'}}}};
const keys=[];let queued=true;
const store={{claimNext:async()=>queued?(queued=false,item):null,update:async()=>{{}},complete:async()=>{{}},release:async()=>{{}},fail:async()=>{{}},countBlocked:async()=>0}};
const fetchJson=async(url,options={{}})=>{{if(url==='api/v1/background-identity')return{{login:'timmy'}};if(options.headers?.['Idempotency-Key'])keys.push(options.headers['Idempotency-Key']);if(url.endsWith('/issues'))return{{repository:'o/r',number:1}};if(url.endsWith('/attachments'))return{{markdown:'![a](url)'}};return{{id:1}};}};
createBackgroundIssueSync({{store,fetchJson}}).flush().then(()=>process.stdout.write(JSON.stringify(keys)));
"""
    keys = run_node(script)

    assert len(keys) == 3
    assert len(set(keys)) == 3
    assert all(len(key) <= 128 for key in keys)


def test_capture_renews_claim_before_every_network_stage_and_fences_completion():
    script = f"""
const createBackgroundIssueSync=require({json.dumps(str(SYNC))});
const item={{id:'capture',claimToken:'claim-7',operationId:'capture',ownerLogin:'timmy',repository:'o/r',title:'Bug',body:'',labelIds:[],attachment:{{filename:'a.png',contentType:'image/png',data:'abc'}}}};
const events=[];let claimed=false;
const store={{
  supportsClaimTokens:true,
  claimNext:async()=>claimed?null:(claimed=true,item),
  renew:async(id,token)=>{{events.push(['renew',id,token]);return item;}},
  checkpoint:async(id,token,transform)=>{{events.push(['checkpoint',id,token]);transform(item);return true;}},
  complete:async(id,token)=>events.push(['complete',id,token]),
  release:async(id,token)=>events.push(['release',id,token]),
  fail:async()=>{{}},countBlocked:async()=>0,
}};
const fetchJson=async(url,options={{}})=>{{
  if(url==='api/v1/background-identity')return{{login:'timmy'}};
  events.push(['network',options.headers['Idempotency-Key']]);
  if(url.endsWith('/issues'))return{{number:3}};
  if(url.endsWith('/attachments'))return{{markdown:'![a](url)'}};
  return{{id:4}};
}};
(async()=>{{await createBackgroundIssueSync({{store,fetchJson}}).flush();process.stdout.write(JSON.stringify(events));}})();
"""
    events = run_node(script)

    networks = [index for index, event in enumerate(events) if event[0] == "network"]
    assert [events[index - 1][0] for index in networks] == ["renew", "renew", "renew"]
    assert [events[index][1] for index in networks] == [
        "capture", "capture:attachment", "capture:attachment-comment"
    ]
    assert events[-1] == ["complete", "capture", "claim-7"]
    assert all(event[2] == "claim-7" for event in events if event[0] in {"renew", "checkpoint"})


def test_screenshot_comment_retry_reuses_upload_checkpoint_and_posts_combined_body_once():
    script = f"""
const createBackgroundIssueSync=require({json.dumps(str(SYNC))});
let item={{id:'message-image',operationId:'message-image',ownerLogin:'timmy',status:'queued',
  kind:'issue-comment',repository:'stackchain/dashboard',number:477,body:'Broken at 320px',
  attachment:{{filename:'phone.png',contentType:'image/png',data:'iVBORw0KGgo='}}}};
const state={{calls:[],released:0,completed:0}};let commentAttempts=0;
const store={{claimNext:async()=>item?{{...item}}:null,update:async(_id,transform)=>{{item=transform(item);}},
  complete:async()=>{{state.completed++;item=null;}},release:async()=>{{state.released++;item={{...item,status:'queued'}};}},
  fail:async()=>{{}},countBlocked:async()=>0}};
const fetchJson=async(url,options={{}})=>{{
  if(url==='api/v1/background-identity')return{{login:'timmy'}};
  let body=null;
  if(options.body instanceof FormData){{const file=options.body.get('file');body={{filename:file.name,multipart:true}};}}
  else if(options.body)body=JSON.parse(options.body);
  state.calls.push({{url,key:options.headers?.['Idempotency-Key'],body}});
  if(url.endsWith('/attachments'))return{{markdown:'![phone.png](https://forge.example/phone.png)'}};
  if(commentAttempts++===0){{const error=new Error('Comment unavailable');error.status=503;throw error;}}
  return{{id:91}};
}};
(async()=>{{const sync=createBackgroundIssueSync({{store,fetchJson}});let firstError='';
  try{{await sync.flush();}}catch(error){{firstError=error.message;}}
  const checkpoint={{...item}};const second=await sync.flush();
  process.stdout.write(JSON.stringify({{state,firstError,checkpoint,second}}));
}})();
"""
    output = run_node(script)

    assert output["firstError"] == "Comment unavailable"
    assert output["checkpoint"]["attachmentMarkdown"].startswith("![phone.png]")
    assert sum(call["url"].endswith("/attachments") for call in output["state"]["calls"]) == 1
    comments = [call for call in output["state"]["calls"] if call["url"].endswith("/comments")]
    assert [call["key"] for call in comments] == ["message-image:comment"] * 2
    assert comments[-1]["body"] == {
        "body": "Broken at 320px\n\n![phone.png](https://forge.example/phone.png)"
    }
    assert output["state"]["completed"] == 1
    assert output["second"]["confirmed"] == [{"id": 91}]


def test_conversation_photo_bundle_resumes_at_first_unconfirmed_upload_and_posts_one_ordered_comment():
    script = f"""
const createBackgroundIssueSync=require({json.dumps(str(SYNC))});
let item={{id:'message-bundle',operationId:'message-bundle',ownerLogin:'timmy',status:'queued',
  kind:'issue-comment',repository:'stackchain/dashboard',number:943,body:'Before and after',
  attachments:[
    {{filename:'before.jpg',contentType:'image/jpeg',blob:new Blob(['before'],{{type:'image/jpeg'}})}},
    {{filename:'after.jpg',contentType:'image/jpeg',blob:new Blob(['after'],{{type:'image/jpeg'}})}},
  ]}};
const calls=[];let afterAttempts=0;
const store={{claimNext:async()=>item?{{...item}}:null,update:async(_id,transform)=>{{item=transform(item);}},
  complete:async()=>{{item=null;}},release:async()=>{{item={{...item,status:'queued'}};}},
  fail:async()=>{{}},countBlocked:async()=>0}};
const fetchJson=async(url,options={{}})=>{{
  if(url==='api/v1/background-identity')return{{login:'timmy'}};
  const file=options.body instanceof FormData?options.body.get('file'):null;
  calls.push({{url,key:options.headers?.['Idempotency-Key'],filename:file?.name||'',
    body:options.body instanceof FormData?null:JSON.parse(options.body)}});
  if(file){{
    if(file.name==='after.jpg' && afterAttempts++===0){{const error=new Error('offline');error.status=503;throw error;}}
    return{{markdown:'!['+file.name+'](url/'+file.name+')'}};
  }}
  return{{id:943}};
}};
(async()=>{{const sync=createBackgroundIssueSync({{store,fetchJson}});let firstError='';
  try{{await sync.flush();}}catch(error){{firstError=error.message;}}
  const checkpoint={{...item}};const second=await sync.flush();
  process.stdout.write(JSON.stringify({{calls,firstError,checkpoint,second}}));
}})();
"""
    output = run_node(script)

    assert output["firstError"] == "offline"
    assert output["checkpoint"]["attachmentMarkdowns"] == [
        "![before.jpg](url/before.jpg)"
    ]
    uploads = [call for call in output["calls"] if call["filename"]]
    assert [(call["filename"], call["key"]) for call in uploads] == [
        ("before.jpg", "message-bundle:attachment-0"),
        ("after.jpg", "message-bundle:attachment-1"),
        ("after.jpg", "message-bundle:attachment-1"),
    ]
    comments = [call for call in output["calls"] if call["url"].endswith("/comments")]
    assert len(comments) == 1
    assert comments[0]["body"] == {
        "body": "Before and after\n\n![before.jpg](url/before.jpg)\n\n![after.jpg](url/after.jpg)"
    }
    assert output["second"]["confirmed"] == [{"id": 943}]


def test_pull_screenshot_retry_uploads_to_pull_and_reuses_checkpoint_before_one_comment():
    script = f"""
const createBackgroundIssueSync=require({json.dumps(str(SYNC))});
let item={{id:'pull-image',operationId:'pull-image',ownerLogin:'timmy',status:'queued',
  kind:'pull-comment',repository:'stackchain/web',number:31,body:'Mobile proof',
  attachment:{{filename:'proof.png',contentType:'image/png',blob:new Blob(['proof'],{{type:'image/png'}})}}}};
const calls=[];let commentAttempts=0;
const store={{claimNext:async()=>item?{{...item}}:null,update:async(_id,fn)=>{{item=fn(item);}},
  complete:async()=>{{item=null;}},release:async()=>{{item={{...item,status:'queued'}};}},fail:async()=>{{}},countBlocked:async()=>0}};
const fetchJson=async(url,options={{}})=>{{if(url==='api/v1/background-identity')return{{login:'timmy'}};
  calls.push({{url,key:options.headers?.['Idempotency-Key'],body:options.body instanceof FormData?'multipart':JSON.parse(options.body)}});
  if(url.endsWith('/attachments'))return{{markdown:'![proof.png](https://forge.example/proof.png)'}};
  if(commentAttempts++===0){{const error=new Error('offline');error.status=503;throw error;}}return{{id:77}};
}};
(async()=>{{const sync=createBackgroundIssueSync({{store,fetchJson}});try{{await sync.flush();}}catch(_error){{}}
  const checkpoint=item.attachmentMarkdown;const result=await sync.flush();
  process.stdout.write(JSON.stringify({{calls,checkpoint,result}}));}})();
"""
    output = run_node(script)

    assert output["checkpoint"].startswith("![proof.png]")
    assert [call["url"] for call in output["calls"]] == [
        "api/v1/repos/stackchain/web/pulls/31/attachments",
        "api/v1/repos/stackchain/web/pulls/31/comments",
        "api/v1/repos/stackchain/web/pulls/31/comments",
    ]
    assert [call["key"] for call in output["calls"]] == [
        "pull-image:attachment", "pull-image:comment", "pull-image:comment"
    ]
    assert output["calls"][-1]["body"] == {
        "body": "Mobile proof\n\n![proof.png](https://forge.example/proof.png)"
    }
    assert output["result"]["confirmed"] == [{"id": 77}]


def test_pull_screenshot_comment_timeout_replays_one_upload_with_durable_stage_identity():
    script = f"""
const createBackgroundIssueSync=require({json.dumps(str(SYNC))});
let item={{id:'pull-timeout',operationId:'pull-timeout',ownerLogin:'timmy',status:'queued',
  kind:'pull-comment',repository:'stackchain/web',number:31,body:'Mobile proof',
  attachment:{{filename:'proof.png',contentType:'image/png',blob:new Blob(['proof'],{{type:'image/png'}})}}}};
const calls=[];let commentAttempts=0;
const store={{claimNext:async()=>item?{{...item}}:null,update:async(_id,fn)=>{{item=fn(item);}},
  complete:async()=>{{item=null;}},release:async()=>{{item={{...item,status:'queued'}};}},fail:async()=>{{}},countBlocked:async()=>0}};
const fetchJson=async(url,options={{}})=>{{if(url==='api/v1/background-identity')return{{login:'timmy'}};
  calls.push({{url,key:options.headers?.['Idempotency-Key']}});
  if(url.endsWith('/attachments'))return{{markdown:'![proof.png](https://forge.example/proof.png)'}};
  if(commentAttempts++===0)return new Promise(()=>{{}});return{{id:78}};
}};
(async()=>{{const sync=createBackgroundIssueSync({{store,fetchJson,requestTimeoutMs:5}});
  let timedOut='';try{{await sync.flush();}}catch(error){{timedOut=error.message;}}
  const checkpoint=item.attachmentMarkdown;const replay=await sync.flush();
  process.stdout.write(JSON.stringify({{calls,timedOut,checkpoint,replay}}));}})();
"""
    output = run_node(script)

    assert output["timedOut"] == "Background request timed out."
    assert output["checkpoint"].startswith("![proof.png]")
    assert [call["key"] for call in output["calls"]] == [
        "pull-timeout:attachment", "pull-timeout:comment", "pull-timeout:comment"
    ]
    assert sum(call["url"].endswith("/attachments") for call in output["calls"]) == 1
    assert output["replay"]["confirmed"] == [{"id": 78}]


def test_closed_app_sync_retains_created_issue_for_create_and_start_recovery():
    script = f"""
const createBackgroundIssueSync = require({json.dumps(str(SYNC))});
const item = {{
  id:'capture-start',operationId:'capture-start',ownerLogin:'timmy',status:'queued',
  repository:'stackchain/dashboard',title:'Resume',body:'',labelIds:[],
  completionIntent:'create-and-start',
}};
let queued = item;
const state = {{completed:[]}};
const store = {{
  claimNext:async () => queued ? (queued=null,{{...item}}) : null,
  complete:async (id, deliveredIssue) => state.completed.push({{id,deliveredIssue}}),
  release:async()=>{{}},fail:async()=>{{}},countBlocked:async()=>0,
}};
const fetchJson = async url => url === 'api/v1/background-identity' ? {{login:'timmy'}} :
  {{repository:'stackchain/dashboard',number:389,title:'Resume'}};
(async()=>{{
  await createBackgroundIssueSync({{store,fetchJson}}).flush();
  process.stdout.write(JSON.stringify(state));
}})();
"""
    output = run_node(script)

    assert output["completed"] == [{
        "id": "capture-start",
        "deliveredIssue": {
            "repository": "stackchain/dashboard", "number": 389, "title": "Resume"
        },
    }]


def test_closed_app_sync_returns_privacy_safe_actionable_delivery_receipts():
    script = f"""
const createBackgroundIssueSync = require({json.dumps(str(SYNC))});
const queued = [
  {{id:'capture-1',operationId:'capture-1',ownerLogin:'timmy',status:'queued',repository:'stackchain/api',title:'Secret title',body:'Secret body',labelIds:[]}},
  {{id:'reply-1',operationId:'reply-1',ownerLogin:'timmy',status:'queued',kind:'pull-comment',repository:'stackchain/web',number:8,body:'Secret reply'}},
  {{id:'bad-1',operationId:'bad-1',ownerLogin:'timmy',status:'queued',repository:'stackchain/api',title:'Bad',body:'Secret failure',labelIds:[]}},
];
const store = {{
  claimNext: async () => queued.shift() || null,
  complete: async () => {{}}, release: async () => {{}}, fail: async () => {{}},
  countBlocked: async () => 0,
}};
let issueMutations = 0;
const fetchJson = async (url) => {{
  if (url === 'api/v1/background-identity') return {{login:'timmy'}};
  if (url.endsWith('/issues') && issueMutations++ === 0) return {{number:44,repository:'stackchain/api',title:'Secret title'}};
  if (url.includes('/pulls/8/comments')) return {{id:91,body:'Secret reply'}};
  const error = new Error('Sensitive validation detail'); error.status = 422; throw error;
}};
(async () => {{
  const result = await createBackgroundIssueSync({{store,fetchJson}}).flush();
  process.stdout.write(JSON.stringify(result));
}})();
"""
    output = run_node(script)

    assert output["login"] == "timmy"
    assert output["receipts"] == [
        {
            "id": "capture-1",
            "status": "confirmed",
            "kind": "issue",
            "route": "#/my-work/issue/stackchain/api/44",
        },
        {
            "id": "reply-1",
            "status": "confirmed",
            "kind": "message",
            "route": "#/my-work/pull/stackchain/web/8",
        },
        {
            "id": "bad-1",
            "status": "attention",
            "kind": "issue",
            "route": "#/my-work/drafts",
        },
    ]
    serialized = json.dumps(output["receipts"])
    assert "Secret" not in serialized
    assert "Sensitive" not in serialized


@pytest.mark.parametrize(
    ("item", "expected_url"),
    [
        (
            {"kind": "issue-comment", "repository": "stackchain/api", "number": 7},
            "api/v1/repos/stackchain/api/issues/7/comments",
        ),
        (
            {"kind": "pull-comment", "repository": "stackchain/web", "number": 8},
            "api/v1/repos/stackchain/web/pulls/8/comments",
        ),
        (
            {"kind": "update-reply", "notificationId": 9},
            "api/v1/notifications/9/reply",
        ),
    ],
)
def test_closed_app_sync_delivers_each_authored_message_kind(item, expected_url):
    authored = {
        "id": "message-op",
        "operationId": "message-op",
        "ownerLogin": "timmy",
        "status": "queued",
        "body": "Ship this reply",
        **item,
    }
    script = f"""
const createBackgroundIssueSync = require({json.dumps(str(SYNC))});
let queued = {json.dumps(authored)};
const calls = [];
const store = {{
  claimNext: async owner => queued?.ownerLogin === owner ? (queued = null, {json.dumps(authored)}) : null,
  complete: async () => {{}}, release: async () => {{}}, fail: async () => {{}},
  countBlocked: async () => 0,
}};
const fetchJson = async (url, options = {{}}) => {{
  calls.push({{url, options}});
  if (url === 'api/v1/background-identity') return {{login:'timmy'}};
  return {{id:42}};
}};
(async () => {{
  const result = await createBackgroundIssueSync({{store,fetchJson}}).flush();
  process.stdout.write(JSON.stringify({{calls,result}}));
}})();
"""
    output = run_node(script)

    mutation = output["calls"][1]
    assert mutation["url"] == expected_url
    assert mutation["options"]["headers"]["Idempotency-Key"] == "message-op"
    assert json.loads(mutation["options"]["body"]) == {"body": "Ship this reply"}
    assert output["result"]["confirmed"] == [{"id": 42}]


def test_closed_app_sync_delivers_pull_review_with_stable_identity_and_full_payload():
    authored = {
        "id": "review-op", "operationId": "review-op", "ownerLogin": "timmy", "status": "queued",
        "kind": "pull-review", "repository": "stackchain/web", "number": 8,
        "body": "Looks good", "decision": "approve", "expectedHeadSha": "abc123",
        "comments": [{"path": "app.js", "body": "Nice", "new_position": 4}],
    }
    script = f"""
const createBackgroundIssueSync = require({json.dumps(str(SYNC))});
let queued = {json.dumps(authored)}; const calls=[];
const store = {{
  claimNext:async owner=>queued?.ownerLogin===owner?(queued=null,{json.dumps(authored)}):null,
  complete:async()=>{{}},release:async()=>{{}},fail:async()=>{{}},countBlocked:async()=>0,
}};
const fetchJson=async(url,options={{}})=>{{calls.push({{url,options}});return url==='api/v1/background-identity'?{{login:'timmy'}}:{{id:45}};}};
createBackgroundIssueSync({{store,fetchJson}}).flush().then(result=>process.stdout.write(JSON.stringify({{calls,result}})));
"""
    output = run_node(script)

    mutation = output["calls"][1]
    assert mutation["url"] == "api/v1/repos/stackchain/web/pulls/8/review"
    assert mutation["options"]["headers"]["Idempotency-Key"] == "review-op"
    assert json.loads(mutation["options"]["body"]) == {
        "body": "Looks good", "decision": "approve", "expected_head_sha": "abc123",
        "comments": [{"path": "app.js", "body": "Nice", "new_position": 4}],
    }
    assert output["result"]["confirmed"] == [{"id": 45}]
    assert output["result"]["receipts"] == [{
        "id": "review-op", "status": "confirmed", "kind": "message",
        "route": "#/my-work/review/stackchain/web/8",
    }]


def test_closed_app_sync_holds_consequential_review_for_foreground_authorization():
    script = f"""
const createBackgroundIssueSync = require({json.dumps(str(SYNC))});
const item={{id:'review-op',operationId:'review-op',ownerLogin:'timmy',status:'queued',
  kind:'pull-review',repository:'stackchain/web',number:8,body:'Looks good',
  decision:'approve',expectedHeadSha:'abc123',comments:[]}};
const state={{authorized:[],failed:[],released:[],completed:[]}};let claimed=false;
const store={{
  claimNext:async()=>claimed?null:(claimed=true,item),
  authorization:async(id,error)=>state.authorized.push({{id,error}}),
  fail:async(...args)=>state.failed.push(args),release:async id=>state.released.push(id),
  complete:async id=>state.completed.push(id),countBlocked:async()=>0,
}};
const fetchJson=async url=>{{
  if(url==='api/v1/background-identity')return{{login:'timmy'}};
  const error=new Error('Fresh authorization required');error.status=428;
  error.code='step_up_required';throw error;
}};
(async()=>{{const result=await createBackgroundIssueSync({{store,fetchJson}}).flush();
process.stdout.write(JSON.stringify({{state,result}}));}})();
"""
    output = run_node(script)

    assert output["state"] == {
        "authorized": [{"id": "review-op", "error": "Fresh authorization required"}],
        "failed": [], "released": [], "completed": [],
    }
    assert output["result"]["attention"] == 0
    assert output["result"]["authorization"] == 1
    assert output["result"]["receipts"] == [{
        "id": "review-op", "status": "authorization", "kind": "message",
        "route": "#/my-work/review/stackchain/web/8",
    }]


def test_closed_app_sync_leaves_issue_closure_awaiting_foreground_authorization():
    records = [
        {
            "id": "close-op", "operationId": "close-op", "ownerLogin": "timmy", "status": "queued",
            "kind": "issue-close", "repository": "stackchain/dashboard", "number": 27, "body": "",
        },
        {
            "id": "comment-op", "operationId": "comment-op", "ownerLogin": "timmy", "status": "queued",
            "kind": "issue-comment", "repository": "stackchain/dashboard", "number": 27, "body": "Still deliver",
        },
    ]
    script = f"""
const createBackgroundIssueSync = require({json.dumps(str(SYNC))});
const records=new Map({json.dumps([[item["id"], item] for item in records])});let tail=Promise.resolve();const calls=[];
const transaction=work=>{{const run=tail.then(()=>work({{
  get:async id=>records.get(id),getAll:async()=>[...records.values()].map(value=>({{...value}})),
  put:async value=>records.set(value.id,{{...value}}),delete:async id=>records.delete(id),
}}));tail=run.catch(()=>{{}});return run;}};
const fetchJson=async(url,options={{}})=>{{
  calls.push({{url,options}});
  return url==='api/v1/background-identity'?{{login:'timmy'}}:{{id:45}};
}};
(async()=>{{
  const store=createBackgroundIssueSync.createIssueSyncStore({{transaction}});
  const result=await createBackgroundIssueSync({{store,fetchJson}}).flush();
  process.stdout.write(JSON.stringify({{calls,result,snapshot:await store.snapshot()}}));
}})();
"""
    output = run_node(script)

    mutations = output["calls"][1:]
    assert [call["url"] for call in mutations] == [
        "api/v1/repos/stackchain/dashboard/issues/27/comments"
    ]
    assert output["result"]["confirmed"] == [{"id": 45}]
    by_id = {item["id"]: item for item in output["snapshot"]}
    assert by_id["close-op"]["status"] == "queued"
    assert by_id["comment-op"]["status"] == "sent"


def test_reconciling_one_outbox_lane_preserves_the_other_lane():
    script = f"""
const createBackgroundIssueSync = require({json.dumps(str(SYNC))});
const records=new Map();let tail=Promise.resolve();
const transaction=work=>{{const run=tail.then(()=>work({{
  getAll:async()=>[...records.values()].map(value=>({{...value}})),
  put:async value=>records.set(value.id,{{...value}}),delete:async id=>records.delete(id),
}}));tail=run.catch(()=>{{}});return run;}};
(async()=>{{
  const store=createBackgroundIssueSync.createIssueSyncStore({{transaction}});
  await store.reconcile([{{id:'issue',ownerLogin:'timmy',status:'queued'}}], 'issue');
  await store.reconcile([{{id:'message',kind:'issue-comment',ownerLogin:'timmy',status:'queued'}}], 'authored');
  await store.reconcile([], 'issue');
  process.stdout.write(JSON.stringify(await store.snapshot()));
}})();
"""
    output = run_node(script)

    assert output == [
        {"id": "message", "kind": "issue-comment", "ownerLogin": "timmy", "status": "queued", "outboxLane": "authored"}
    ]


def test_reconciling_a_lightweight_attachment_reference_preserves_indexeddb_blob():
    script = f"""
const createBackgroundIssueSync = require({json.dumps(str(SYNC))});
const records=new Map();let tail=Promise.resolve();
const transaction=work=>{{const run=tail.then(()=>work({{
  getAll:async()=>[...records.values()].map(value=>({{...value}})),
  put:async value=>records.set(value.id,{{...value}}),delete:async id=>records.delete(id),
}}));tail=run.catch(()=>{{}});return run;}};
(async()=>{{
  const store=createBackgroundIssueSync.createIssueSyncStore({{transaction}});
  await store.reconcile([{{
    id:'first',operationId:'first',ownerLogin:'timmy',status:'queued',
    attachment:{{filename:'one.png',contentType:'image/png',blob:new Blob(['first-image-bytes'],{{type:'image/png'}})}},
  }},{{id:'bundle',operationId:'bundle',ownerLogin:'timmy',status:'queued',attachments:[
    {{filename:'a.png',contentType:'image/png',blob:new Blob(['a-bytes'],{{type:'image/png'}})}},
    {{filename:'b.png',contentType:'image/png',blob:new Blob(['b-bytes'],{{type:'image/png'}})}},
  ]}}]);
  await store.reconcile([
    {{id:'first',operationId:'first',ownerLogin:'timmy',status:'queued',
      attachment:{{filename:'one.png',contentType:'image/png',stored:true}}}},
    {{id:'second',operationId:'second',ownerLogin:'timmy',status:'queued',
      attachment:{{filename:'two.png',contentType:'image/png',data:'c2Vjb25kLWltYWdlLWJ5dGVz'}}}},
    {{id:'bundle',operationId:'bundle',ownerLogin:'timmy',status:'queued',attachments:[
      {{filename:'a.png',contentType:'image/png',stored:true}},
      {{filename:'b.png',contentType:'image/png',stored:true}},
    ]}},
  ]);
  const snapshot=await store.snapshot();
  const byId=Object.fromEntries(snapshot.map(value=>[value.id,value]));
  process.stdout.write(JSON.stringify({{first:{{isBlob:byId.first.attachment.blob instanceof Blob,
    text:await byId.first.attachment.blob?.text()}},second:byId.second.attachment.data,
    bundle:await Promise.all(byId.bundle.attachments.map(value=>value.blob?.text()))}}));
}})();
"""
    output = run_node(script)

    assert output["first"] == {"isBlob": True, "text": "first-image-bytes"}
    assert output["second"] == "c2Vjb25kLWltYWdlLWJ5dGVz"
    assert output["bundle"] == ["a-bytes", "b-bytes"]


def test_issue_sync_store_hydrates_one_capture_by_key_without_scanning_all_records():
    script = f"""
const createBackgroundIssueSync = require({json.dumps(str(SYNC))});
const records=new Map([
  ['first',{{id:'first',attachment:{{filename:'one.png',contentType:'image/png',data:'first-bytes'}}}}],
  ['second',{{id:'second',attachment:{{filename:'two.png',contentType:'image/png',data:'second-bytes'}}}}],
]);
const calls=[];
const transaction=work=>work({{
  get:async id=>{{calls.push(['get',id]);return records.get(id);}},
  getAll:async()=>{{calls.push(['getAll']);return [...records.values()];}},
  put:async()=>{{}},delete:async()=>{{}},
}});
(async()=>{{
  const store=createBackgroundIssueSync.createIssueSyncStore({{transaction}});
  const hydrated=await store.get('second');
  process.stdout.write(JSON.stringify({{hydrated,calls}}));
}})();
"""
    output = run_node(script)

    assert output["hydrated"]["attachment"]["data"] == "second-bytes"
    assert output["calls"] == [["get", "second"]]


def test_stale_foreground_upsert_preserves_confirmed_attachment_delivery_stages():
    script = f"""
const createBackgroundIssueSync=require({json.dumps(str(SYNC))});
const records=new Map();let tail=Promise.resolve();
const transaction=work=>{{const run=tail.then(()=>work({{getAll:async()=>[...records.values()].map(value=>({{...value}})),put:async value=>records.set(value.id,{{...value}}),delete:async id=>records.delete(id)}}));tail=run.catch(()=>{{}});return run;}};
(async()=>{{
  const store=createBackgroundIssueSync.createIssueSyncStore({{transaction}});
  await store.upsert({{id:'capture',operationId:'same',ownerLogin:'timmy',status:'queued',attachment:{{filename:'a.png'}}}});
  await store.update('capture',item=>({{...item,deliveredIssue:{{number:7}},attachmentMarkdown:'![a](url)'}}));
  const result=await store.upsert({{id:'capture',operationId:'same',ownerLogin:'timmy',status:'queued',attachment:{{filename:'a.png'}}}});
  process.stdout.write(JSON.stringify(result));
}})();
"""
    output = run_node(script)

    assert output["deliveredIssue"]["number"] == 7
    assert output["attachmentMarkdown"] == "![a](url)"


def test_foreground_bundle_upsert_preserves_indexeddb_blobs_and_upload_checkpoints():
    script = f"""
const createBackgroundIssueSync=require({json.dumps(str(SYNC))});
const records=new Map();let tail=Promise.resolve();
const transaction=work=>{{const run=tail.then(()=>work({{getAll:async()=>[...records.values()].map(value=>({{...value}})),put:async value=>records.set(value.id,{{...value}}),delete:async id=>records.delete(id)}}));tail=run.catch(()=>{{}});return run;}};
(async()=>{{
  const store=createBackgroundIssueSync.createIssueSyncStore({{transaction}});
  await store.upsert({{id:'bundle',operationId:'same',ownerLogin:'timmy',status:'queued',attachments:[
    {{filename:'a.jpg',contentType:'image/jpeg',blob:new Blob(['a-bytes'])}},
    {{filename:'b.jpg',contentType:'image/jpeg',blob:new Blob(['b-bytes'])}},
  ]}});
  await store.update('bundle',item=>({{...item,attachmentMarkdowns:['![a](url/a)']}}));
  const result=await store.upsert({{id:'bundle',operationId:'same',ownerLogin:'timmy',status:'queued',attachments:[
    {{filename:'a.jpg',contentType:'image/jpeg',stored:true}},
    {{filename:'b.jpg',contentType:'image/jpeg',stored:true}},
  ]}});
  process.stdout.write(JSON.stringify({{texts:await Promise.all(result.attachments.map(value=>value.blob?.text())),
    markdowns:result.attachmentMarkdowns}}));
}})();
"""
    output = run_node(script)

    assert output == {
        "texts": ["a-bytes", "b-bytes"],
        "markdowns": ["![a](url/a)"],
    }


def test_indexeddb_store_closes_on_version_change_and_reopens_afterward():
    script = f"""
const createBackgroundIssueSync = require({json.dumps(str(SYNC))});
const state={{opens:0,closes:0}};
const indexedDB={{lastDb:null,open:()=>{{
  state.opens += 1;
  const request={{result:null,onsuccess:null,onerror:null,onupgradeneeded:null}};
  const db={{
    objectStoreNames:{{contains:()=>true}},
    close:()=>{{state.closes += 1;}},
    transaction:()=>{{
      const tx={{oncomplete:null,onerror:null,onabort:null,error:null,objectStore:()=>({{
        getAll:()=>{{const get={{onsuccess:null,onerror:null,result:[]}};queueMicrotask(()=>get.onsuccess?.());return get;}},
      }})}};
      setTimeout(()=>tx.oncomplete?.(),0);
      return tx;
    }},
  }};
  indexedDB.lastDb=db;request.result=db;queueMicrotask(()=>request.onsuccess?.());return request;
}}}};
(async()=>{{
  const store=createBackgroundIssueSync.createIssueSyncStore({{indexedDB}});
  await store.snapshot();
  indexedDB.lastDb.onversionchange?.();
  await store.snapshot();
  process.stdout.write(JSON.stringify(state));
}})();
"""
    output = run_node(script)

    assert output == {"opens": 2, "closes": 1}


def test_delivery_receipt_preference_is_account_bound_and_hidden_from_outbox():
    script = f"""
const createBackgroundIssueSync = require({json.dumps(str(SYNC))});
const records=new Map();let tail=Promise.resolve();
const transaction=work=>{{const run=tail.then(()=>work({{
  getAll:async()=>[...records.values()].map(value=>({{...value}})),
  put:async value=>records.set(value.id,{{...value}}),delete:async id=>records.delete(id),
}}));tail=run.catch(()=>{{}});return run;}};
(async()=>{{
  const store=createBackgroundIssueSync.createIssueSyncStore({{transaction}});
  await store.setReceiptPreference('timmy', true);
  await store.reconcile([{{id:'issue',ownerLogin:'timmy',status:'queued'}}], 'issue');
  const result={{
    timmy:await store.getReceiptPreference('timmy'),
    alexander:await store.getReceiptPreference('alexander'),
    snapshot:await store.snapshot(),
    blocked:await store.countBlocked('alexander'),
  }};
  await store.setReceiptPreference('timmy', false);
  result.disabled=await store.getReceiptPreference('timmy');
  process.stdout.write(JSON.stringify(result));
}})();
"""
    output = run_node(script)

    assert output == {
        "timmy": True,
        "alexander": False,
        "snapshot": [
            {
                "id": "issue",
                "ownerLogin": "timmy",
                "status": "queued",
                "outboxLane": "issue",
            }
        ],
        "blocked": 1,
        "disabled": False,
    }


def test_closed_app_sync_leaves_another_accounts_issue_queued():
    script = f"""
const createBackgroundIssueSync = require({json.dumps(str(SYNC))});
const state = {{owners:[],mutations:0}};
const store = {{
  claimNext: async owner => {{ state.owners.push(owner); return null; }},
  countBlocked: async owner => owner === 'alexander' ? 1 : 0,
  complete: async () => {{}},
}};
const fetchJson = async url => {{
  if (url === 'api/v1/background-identity') return {{login:'alexander'}};
  state.mutations += 1;
}};
(async () => {{
  const result = await createBackgroundIssueSync({{store,fetchJson}}).flush();
  process.stdout.write(JSON.stringify({{state,result}}));
}})();
"""
    output = run_node(script)

    assert output["state"] == {"owners": ["alexander"], "mutations": 0}
    assert output["result"]["confirmed"] == []
    assert output["result"]["blocked"] == 1


def test_session_expiry_during_delivery_releases_claim_without_attention():
    script = f"""
const createBackgroundIssueSync = require({json.dumps(str(SYNC))});
const queued = [
  {{id:'capture-auth',operationId:'stable-key',ownerLogin:'timmy',repository:'o/r',title:'Keep me',body:'',labelIds:[]}},
  {{id:'capture-later',operationId:'later-key',ownerLogin:'timmy',repository:'o/r',title:'Do not try yet',body:'',labelIds:[]}},
];
const state = {{released:[],failed:[],mutationKeys:[]}};
const store = {{
  claimNext: async () => queued.shift() || null,
  release: async id => state.released.push(id),
  fail: async (id,message) => state.failed.push({{id,message}}),
  countBlocked: async () => 0,
}};
const fetchJson = async (url, options={{}}) => {{
  if (url === 'api/v1/background-identity') return {{login:'timmy'}};
  state.mutationKeys.push(options.headers['Idempotency-Key']);
  const error = new Error('Authentication required'); error.status = 401; throw error;
}};
(async () => {{
  let error = null;
  try {{ await createBackgroundIssueSync({{store,fetchJson}}).flush(); }}
  catch (caught) {{ error = {{message:caught.message,status:caught.status}}; }}
  process.stdout.write(JSON.stringify({{state,error}}));
}})();
"""
    output = run_node(script)

    assert output == {
        "state": {
            "released": ["capture-auth"],
            "failed": [],
            "mutationKeys": ["stable-key"],
        },
        "error": {"message": "Authentication required", "status": 401},
    }


def test_transient_delivery_failure_releases_claim_and_requests_another_sync():
    script = f"""
const createBackgroundIssueSync = require({json.dumps(str(SYNC))});
let item = {{id:'capture-2',operationId:'capture-2',ownerLogin:'timmy',repository:'o/r',title:'Retry',body:'Later',labelIds:[]}};
const state = {{released:[],completed:[]}};
const store = {{
  claimNext: async () => item ? (item = null, {{id:'capture-2',operationId:'capture-2',ownerLogin:'timmy',repository:'o/r',title:'Retry',body:'Later',labelIds:[]}}) : null,
  release: async id => state.released.push(id),
  complete: async id => state.completed.push(id),
}};
const fetchJson = async url => {{
  if (url === 'api/v1/background-identity') return {{login:'timmy'}};
  const error = new Error('Gitea unavailable'); error.status = 503; throw error;
}};
(async () => {{
  let error = null;
  try {{ await createBackgroundIssueSync({{store,fetchJson}}).flush(); }}
  catch (caught) {{ error = caught.message; }}
  process.stdout.write(JSON.stringify({{state,error}}));
}})();
"""
    output = run_node(script)

    assert output["state"]["released"] == ["capture-2"]
    assert output["state"]["completed"] == []
    assert output["error"] == "Gitea unavailable"


def test_stalled_delivery_times_out_releases_claim_and_retries_with_same_idempotency_key():
    script = f"""
const createBackgroundIssueSync = require({json.dumps(str(SYNC))});
const item = {{id:'capture-timeout',operationId:'stable-operation',ownerLogin:'timmy',repository:'o/r',title:'Retry',body:'Later',labelIds:[]}};
const state = {{queued:true,released:[],completed:[],keys:[],attempts:0}};
const store = {{
  claimNext: async () => state.queued ? (state.queued=false, {{...item}}) : null,
  complete: async id => state.completed.push(id),
  release: async id => {{state.released.push(id); state.queued=true;}},
  fail: async () => {{}}, countBlocked: async () => 0,
}};
const fetchJson = async (url, options={{}}) => {{
  if (url === 'api/v1/background-identity') return {{login:'timmy'}};
  state.keys.push(options.headers['Idempotency-Key']);
  state.attempts += 1;
  if (state.attempts === 1) return new Promise(() => {{}});
  return {{repository:'o/r',number:9}};
}};
(async () => {{
  const sync=createBackgroundIssueSync({{store,fetchJson,requestTimeoutMs:10}});
  let firstError='';
  try {{ await sync.flush(); }} catch (error) {{ firstError=error.message; }}
  const second=await sync.flush();
  process.stdout.write(JSON.stringify({{state,firstError,second}}));
}})();
"""
    output = run_node(script)

    assert output["firstError"] == "Background request timed out."
    assert output["state"]["released"] == ["capture-timeout"]
    assert output["state"]["completed"] == ["capture-timeout"]
    assert output["state"]["keys"] == ["stable-operation", "stable-operation"]
    assert output["second"]["confirmed"][0]["number"] == 9


def test_purge_cancels_stalled_delivery_without_reclaiming_it():
    script = f"""
const createBackgroundIssueSync = require({json.dumps(str(SYNC))});
const items=[
  {{id:'capture-purge-1',operationId:'capture-purge-1',ownerLogin:'timmy',repository:'o/r',title:'Private',body:'Draft',labelIds:[]}},
  {{id:'capture-purge-2',operationId:'capture-purge-2',ownerLogin:'timmy',repository:'o/r',title:'Private 2',body:'Draft',labelIds:[]}},
];
const state={{released:[],closed:0,mutations:0}};
let mutationStarted;
const started=new Promise(resolve => mutationStarted=resolve);
const store={{
  claimBatch:async()=>items.map(item=>({{...item}})),
  complete:async()=>{{}},
  release:async id=>state.released.push(id),
  fail:async()=>{{}}, countBlocked:async()=>0, close:async()=>{{state.closed+=1;}},
}};
const fetchJson=async url=>{{
  if(url==='api/v1/background-identity')return{{login:'timmy'}};
  state.mutations+=1;mutationStarted();return new Promise(()=>{{}});
}};
(async()=>{{
  const sync=createBackgroundIssueSync({{store,fetchJson,maxConcurrency:1,requestTimeoutMs:1000}});
  sync.flush().catch(()=>{{}});
  await started;
  const outcome=await Promise.race([
    sync.purge().then(()=> 'purged'),
    new Promise(resolve=>setTimeout(()=>resolve('blocked'),80)),
  ]);
  process.stdout.write(JSON.stringify({{state,outcome}}));
  process.exit(0);
}})();
"""
    output = run_node(script)

    assert output["outcome"] == "purged"
    assert output["state"] == {
        "released": ["capture-purge-1", "capture-purge-2"],
        "closed": 1,
        "mutations": 1,
    }


def test_bounded_flush_delivers_healthy_records_after_transient_failure_without_same_run_retry():
    script = f"""
const createBackgroundIssueSync = require({json.dumps(str(SYNC))});
const items = [
  {{id:'issue-1',operationId:'issue-1',ownerLogin:'timmy',outboxLane:'issue',repository:'o/r',title:'One',body:'',labelIds:[]}},
  {{id:'message-1',operationId:'message-1',ownerLogin:'timmy',outboxLane:'authored',kind:'issue-comment',repository:'o/r',number:1,body:'Reply'}},
  {{id:'issue-bad',operationId:'issue-bad',ownerLogin:'timmy',outboxLane:'issue',repository:'o/r',title:'Bad',body:'',labelIds:[]}},
  {{id:'message-2',operationId:'message-2',ownerLogin:'timmy',outboxLane:'authored',kind:'issue-comment',repository:'o/r',number:2,body:'Reply'}},
  {{id:'issue-2',operationId:'issue-2',ownerLogin:'timmy',outboxLane:'issue',repository:'o/r',title:'Two',body:'',labelIds:[]}},
];
const state = {{active:0,maxActive:0,attempted:[],completed:[],released:[],batches:0}};
const store = {{
  claimBatch: async (owner, limit) => items.slice(0, limit),
  complete: async id => state.completed.push(id),
  release: async id => state.released.push(id),
  fail: async () => {{}}, countBlocked: async () => 0,
}};
const fetchJson = async (url, options={{}}) => {{
  if (url === 'api/v1/background-identity') return {{login:'timmy'}};
  const key = options.headers['Idempotency-Key'];
  state.attempted.push(key); state.active += 1;
  state.maxActive = Math.max(state.maxActive, state.active);
  await new Promise(resolve => setTimeout(resolve, key === 'issue-bad' ? 5 : 20));
  state.active -= 1;
  if (key === 'issue-bad') {{ const error=new Error('Temporary outage'); error.status=503; throw error; }}
  return {{id:key,number:7}};
}};
(async () => {{
  let error;
  try {{ await createBackgroundIssueSync({{
    store,fetchJson,maxConcurrency:3,batchSize:10,
    batch: async work => {{ state.batches += 1; return work(); }},
  }}).flush(); }}
  catch (caught) {{ error=caught.message; }}
  process.stdout.write(JSON.stringify({{state,error}}));
}})();
"""
    output = run_node(script)

    assert output["state"]["maxActive"] == 3
    assert output["state"]["batches"] == 1
    assert sorted(output["state"]["attempted"]) == [
        "issue-1", "issue-2", "issue-bad", "message-1", "message-2"
    ]
    assert len(output["state"]["attempted"]) == len(set(output["state"]["attempted"]))
    assert sorted(output["state"]["completed"]) == [
        "issue-1", "issue-2", "message-1", "message-2"
    ]
    assert output["state"]["released"] == ["issue-bad"]
    assert output["error"] == "Temporary outage"


def test_bounded_flush_claims_just_in_time_and_keeps_unadmitted_records_queued():
    script = f"""
const createBackgroundIssueSync=require({json.dumps(str(SYNC))});
const records=new Map();let tail=Promise.resolve();
const transaction=work=>{{const run=tail.then(()=>work({{
  get:async id=>records.get(id),
  getAll:async()=>[...records.values()].map(value=>({{...value}})),
  put:async value=>records.set(value.id,{{...value}}),delete:async id=>records.delete(id),
}}));tail=run.catch(()=>{{}});return run;}};
let allow=false;const waiting=[];const attempted=[];
const fetchJson=async(url,options={{}})=>{{
  if(url==='api/v1/background-identity')return{{login:'timmy'}};
  attempted.push(options.headers['Idempotency-Key']);
  if(!allow)await new Promise(resolve=>waiting.push(resolve));
  return{{number:attempted.length}};
}};
(async()=>{{
  const store=createBackgroundIssueSync.createIssueSyncStore({{transaction,createToken:(()=>{{let n=0;return()=>`token-${{++n}}`;}})()}});
  await store.reconcile(Array.from({{length:4}},(_,i)=>({{id:`issue-${{i}}`,operationId:`issue-${{i}}`,ownerLogin:'timmy',status:'queued',repository:'o/r',title:'Issue',labelIds:[]}})),'issue');
  await store.reconcile(Array.from({{length:2}},(_,i)=>({{id:`message-${{i}}`,operationId:`message-${{i}}`,ownerLogin:'timmy',status:'queued',kind:'issue-comment',repository:'o/r',number:i,body:'Reply'}})),'authored');
  const sync=createBackgroundIssueSync({{store,fetchJson,maxConcurrency:2,batchSize:5}});
  const flushing=sync.flush();
  while(waiting.length<2)await new Promise(resolve=>setTimeout(resolve,0));
  const during=await store.snapshot();
  allow=true;waiting.splice(0).forEach(resolve=>resolve());
  await flushing;
  const after=await store.snapshot();
  process.stdout.write(JSON.stringify({{attempted,during,after}}));
}})();
"""
    output = run_node(script)

    assert [item["id"] for item in output["during"] if item["status"] == "sending"] == [
        "issue-0", "message-0"
    ]
    assert [item["id"] for item in output["during"] if item["status"] == "queued"] == [
        "issue-1", "issue-2", "issue-3", "message-1"
    ]
    assert output["attempted"] == [
        "issue-0", "message-0", "issue-1", "message-1", "issue-2"
    ]
    unadmitted = next(item for item in output["after"] if item["id"] == "issue-3")
    assert unadmitted["status"] == "queued"
    assert "claimToken" not in unadmitted


def test_bounded_flush_stops_admission_and_releases_unstarted_claims_on_auth_loss():
    script = f"""
const createBackgroundIssueSync = require({json.dumps(str(SYNC))});
const items = Array.from({{length:5}}, (_, index) => ({{
  id:'item-'+index,operationId:'item-'+index,ownerLogin:'timmy',outboxLane:'issue',
  repository:'o/r',title:'Item',body:'',labelIds:[],
}}));
const state = {{attempted:[],released:[],completed:[]}};
const store = {{
  claimBatch: async () => items,
  complete: async id => state.completed.push(id),
  release: async id => state.released.push(id), fail:async()=>{{}}, countBlocked:async()=>0,
}};
const fetchJson = async (url, options={{}}) => {{
  if (url === 'api/v1/background-identity') return {{login:'timmy'}};
  const key=options.headers['Idempotency-Key']; state.attempted.push(key);
  if (key === 'item-0') {{ const error=new Error('Authentication required'); error.status=401; throw error; }}
  await new Promise(resolve => setTimeout(resolve, 20));
  return {{number:1}};
}};
(async()=>{{
  let error;
  try {{ await createBackgroundIssueSync({{store,fetchJson,maxConcurrency:2}}).flush(); }}
  catch (caught) {{ error={{message:caught.message,status:caught.status}}; }}
  process.stdout.write(JSON.stringify({{state,error}}));
}})();
"""
    output = run_node(script)

    assert output["state"]["attempted"] == ["item-0", "item-1"]
    assert sorted(output["state"]["released"]) == ["item-0", "item-2", "item-3", "item-4"]
    assert output["state"]["completed"] == ["item-1"]
    assert output["error"] == {"message": "Authentication required", "status": 401}


def test_issue_store_claims_one_finite_batch_fairly_across_outbox_lanes():
    script = f"""
const createBackgroundIssueSync = require({json.dumps(str(SYNC))});
const records=new Map();let tail=Promise.resolve();
const transaction=work=>{{const run=tail.then(()=>work({{
  getAll:async()=>[...records.values()].map(value=>({{...value}})),
  put:async value=>records.set(value.id,{{...value}}),delete:async id=>records.delete(id),
}}));tail=run.catch(()=>{{}});return run;}};
(async()=>{{
  const store=createBackgroundIssueSync.createIssueSyncStore({{transaction,now:()=>100,claimMs:5000}});
  await store.reconcile([
    {{id:'issue-1',ownerLogin:'timmy',status:'queued'}},
    {{id:'issue-2',ownerLogin:'timmy',status:'queued'}},
    {{id:'issue-3',ownerLogin:'timmy',status:'queued'}},
  ], 'issue');
  await store.reconcile([
    {{id:'message-1',ownerLogin:'timmy',status:'queued'}},
    {{id:'message-2',ownerLogin:'timmy',status:'queued'}},
  ], 'authored');
  const planned=await store.planBatch('timmy', 4);
  process.stdout.write(JSON.stringify({{planned,snapshot:await store.snapshot()}}));
}})();
"""
    output = run_node(script)

    assert output["planned"] == ["issue-1", "message-1", "issue-2", "message-2"]
    assert all(item["status"] == "queued" for item in output["snapshot"])
    assert all("claimUntil" not in item for item in output["snapshot"])


def test_permanent_delivery_failure_marks_issue_for_foreground_attention():
    script = f"""
const createBackgroundIssueSync = require({json.dumps(str(SYNC))});
let claimed = false;
const state = {{failed:[],released:[]}};
const store = {{
  claimNext: async () => claimed ? null : (claimed = true, {{id:'capture-3',operationId:'capture-3',ownerLogin:'timmy',repository:'o/r',title:'Invalid',body:'',labelIds:[]}}),
  fail: async (id, message) => state.failed.push({{id,message}}),
  release: async id => state.released.push(id),
  countBlocked: async () => 0,
}};
const fetchJson = async url => {{
  if (url === 'api/v1/background-identity') return {{login:'timmy'}};
  const error = new Error('Title is invalid'); error.status = 422; throw error;
}};
(async () => {{
  const result = await createBackgroundIssueSync({{store,fetchJson}}).flush();
  process.stdout.write(JSON.stringify({{state,result}}));
}})();
"""
    output = run_node(script)

    assert output["state"]["failed"] == [
        {"id": "capture-3", "message": "Title is invalid"}
    ]
    assert output["state"]["released"] == []
    assert output["result"]["attention"] == 1


def test_uncertain_background_delivery_preserves_verification_state():
    script = f"""
const createBackgroundIssueSync = require({json.dumps(str(SYNC))});
let claimed=false; const state={{failed:[]}};
const store={{
  claimNext:async()=>claimed?null:(claimed=true,{{id:'maybe',operationId:'maybe',ownerLogin:'timmy',repository:'o/r',title:'Maybe',labelIds:[]}}),
  fail:async(id,message,deliveryState)=>state.failed.push({{id,message,deliveryState}}),
  release:async()=>{{}}, countBlocked:async()=>0,
}};
const fetchJson=async url=>{{
  if(url==='api/v1/background-identity') return {{login:'timmy'}};
  const error=new Error('Verify first'); error.status=422; error.code='delivery_uncertain'; throw error;
}};
(async()=>{{
  await createBackgroundIssueSync({{store,fetchJson}}).flush();
  process.stdout.write(JSON.stringify(state));
}})();
"""
    output = run_node(script)

    assert output["failed"] == [
        {"id": "maybe", "message": "Verify first", "deliveryState": "uncertain"}
    ]


def test_issue_store_atomically_grants_one_delivery_claim():
    script = f"""
const createBackgroundIssueSync = require({json.dumps(str(SYNC))});
const records = new Map();
let tail = Promise.resolve();
const transaction = work => {{
  const run = tail.then(() => work({{
    getAll: async () => [...records.values()].map(value => ({{...value}})),
    put: async value => records.set(value.id, {{...value}}),
    delete: async id => records.delete(id),
  }}));
  tail = run.catch(() => {{}});
  return run;
}};
(async () => {{
  const store = createBackgroundIssueSync.createIssueSyncStore({{transaction,now:()=>1000,claimMs:5000}});
  await store.reconcile([{{id:'same',operationId:'same',ownerLogin:'timmy',status:'queued',repository:'o/r',title:'Once',labelIds:[]}}]);
  const claims = await Promise.all([store.claimNext('timmy'),store.claimNext('timmy')]);
  process.stdout.write(JSON.stringify({{claims,records:[...records.values()]}}));
}})();
"""
    output = run_node(script)

    assert sum(claim is not None for claim in output["claims"]) == 1
    assert output["records"][0]["status"] == "sending"
    assert output["records"][0]["claimUntil"] == 6000


def test_issue_store_renews_and_fences_claim_mutations_with_unique_tokens():
    script = f"""
const createBackgroundIssueSync = require({json.dumps(str(SYNC))});
const records=new Map();let tail=Promise.resolve();let timestamp=100;
const transaction=work=>{{const run=tail.then(()=>work({{
  get:async id=>records.get(id),
  getAll:async()=>[...records.values()].map(value=>({{...value}})),
  put:async value=>records.set(value.id,{{...value}}),delete:async id=>records.delete(id),
}}));tail=run.catch(()=>{{}});return run;}};
(async()=>{{
  const tokens=['claim-a','claim-b'];
  const store=createBackgroundIssueSync.createIssueSyncStore({{
    transaction,now:()=>timestamp,claimMs:30,createToken:()=>tokens.shift(),
  }});
  await store.reconcile([{{id:'lease',ownerLogin:'timmy',status:'queued'}}]);
  const first=await store.claimNext('timmy');
  timestamp=120;
  const renewed=await store.renew('lease',first.claimToken);
  timestamp=151;
  const second=await store.claimNext('timmy');
  const staleComplete=await store.complete('lease',first.claimToken);
  const staleRelease=await store.release('lease',first.claimToken);
  const staleFail=await store.fail('lease',first.claimToken,'old failure');
  const currentComplete=await store.complete('lease',second.claimToken,{{number:9}});
  process.stdout.write(JSON.stringify({{first,renewed,second,staleComplete,staleRelease,staleFail,currentComplete,record:records.get('lease')}}));
}})();
"""
    output = run_node(script)

    assert output["first"]["claimToken"] == "claim-a"
    assert output["first"]["claimUntil"] == 130
    assert output["renewed"]["claimUntil"] == 150
    assert output["second"]["claimToken"] == "claim-b"
    assert output["second"]["claimUntil"] == 181
    assert output["staleComplete"] is False
    assert output["staleRelease"] is False
    assert output["staleFail"] is False
    assert output["currentComplete"] is True
    assert output["record"]["status"] == "sent"


def test_foreground_and_worker_race_still_posts_one_issue():
    script = f"""
const createBackgroundIssueSync = require({json.dumps(str(SYNC))});
const records = new Map(); let tail = Promise.resolve();
const transaction = work => {{
  const run = tail.then(() => work({{
    getAll:async()=>[...records.values()].map(value=>({{...value}})),
    put:async value=>records.set(value.id,{{...value}}), delete:async id=>records.delete(id),
  }}));
  tail=run.catch(()=>{{}}); return run;
}};
const item={{id:'race',operationId:'race',ownerLogin:'timmy',status:'queued',repository:'o/r',title:'Exactly once',body:'',labelIds:[]}};
let mutations=0;
const fetchJson=async url=>{{
  if(url==='api/v1/background-identity') return {{login:'timmy'}};
  mutations+=1; return {{number:77}};
}};
(async()=>{{
  const store=createBackgroundIssueSync.createIssueSyncStore({{transaction}});
  await store.reconcile([item]);
  const sync=createBackgroundIssueSync({{store,fetchJson}});
  const [foreground,worker]=await Promise.all([sync.send(item,'timmy'),sync.flush()]);
  process.stdout.write(JSON.stringify({{foreground,worker,mutations,remaining:[...records.values()]}}));
}})();
"""
    output = run_node(script)

    assert output["mutations"] == 1
    assert len(output["remaining"]) == 1
    assert output["remaining"][0]["status"] == "sent"
    delivered = int(bool(output["foreground"].get("issue"))) + len(
        output["worker"]["confirmed"]
    )
    assert delivered == 1


def test_foreground_send_upserts_before_claim_when_mirror_is_still_pending():
    script = f"""
const createBackgroundIssueSync = require({json.dumps(str(SYNC))});
let record = null; const state={{upserts:0,mutations:0}};
const store={{
  upsert:async item=>{{state.upserts+=1;record={{...item}};}},
  claim:async(id,owner)=>record?.id===id&&record?.ownerLogin===owner?{{...record}}:null,
  complete:async()=>{{record=null;}}, release:async()=>{{}}, fail:async()=>{{}},
}};
const fetchJson=async()=>{{state.mutations+=1;return {{number:88}};}};
(async()=>{{
  const sync=createBackgroundIssueSync({{store,fetchJson}});
  const result=await sync.send({{id:'early',operationId:'early',ownerLogin:'timmy',repository:'o/r',title:'Fast',body:'',labelIds:[]}},'timmy');
  process.stdout.write(JSON.stringify({{state,result,record}}));
}})();
"""
    output = run_node(script)

    assert output["state"] == {"upserts": 1, "mutations": 1}
    assert output["result"]["issue"]["number"] == 88
    assert output["record"] is None


def test_completed_delivery_leaves_non_replayable_tombstone_for_next_page():
    script = f"""
const createBackgroundIssueSync = require({json.dumps(str(SYNC))});
const records=new Map();let tail=Promise.resolve();
const transaction=work=>{{const run=tail.then(()=>work({{
  getAll:async()=>[...records.values()].map(value=>({{...value}})),
  put:async value=>records.set(value.id,{{...value}}),delete:async id=>records.delete(id),
}}));tail=run.catch(()=>{{}});return run;}};
(async()=>{{
  const store=createBackgroundIssueSync.createIssueSyncStore({{transaction,now:()=>100}});
  await store.reconcile([{{id:'done',ownerLogin:'timmy',status:'queued'}}]);
  const claimed=await store.claimNext('timmy');
  await store.complete('done', claimed.claimToken);
  const replay=await store.claimNext('timmy');
  const snapshot=await store.snapshot();
  process.stdout.write(JSON.stringify({{replay,snapshot}}));
}})();
"""
    output = run_node(script)

    assert output["replay"] is None
    assert output["snapshot"] == [
        {"id": "done", "ownerLogin": "timmy", "status": "sent", "outboxLane": "issue", "claimUntil": 0}
    ]


def test_user_edit_resets_worker_attention_item_for_retry():
    script = f"""
const createBackgroundIssueSync = require({json.dumps(str(SYNC))});
const records=new Map([['edit',{{id:'edit',ownerLogin:'timmy',status:'attention',title:'Bad',error:'Invalid'}}]]);
let tail=Promise.resolve();
const transaction=work=>{{const run=tail.then(()=>work({{
  getAll:async()=>[...records.values()].map(value=>({{...value}})),
  put:async value=>records.set(value.id,{{...value}}),delete:async id=>records.delete(id),
}}));tail=run.catch(()=>{{}});return run;}};
(async()=>{{
  const store=createBackgroundIssueSync.createIssueSyncStore({{transaction,now:()=>100}});
  await store.reconcile([{{id:'edit',ownerLogin:'timmy',status:'queued',title:'Fixed'}}]);
  const claimed=await store.claimNext('timmy');
  process.stdout.write(JSON.stringify(claimed));
}})();
"""
    output = run_node(script)

    assert output["title"] == "Fixed"
    assert output["status"] == "sending"
    assert "error" not in output


def test_resume_after_purge_reopens_background_delivery_with_fresh_identity():
    script = f"""
const createBackgroundIssueSync = require({json.dumps(str(SYNC))});
const state={{closed:0,identityLookups:0,mutations:0,keys:[]}};
let queued={{
  id:'capture-after-login',ownerLogin:'timmy',status:'queued',repository:'stackchain/dashboard',
  title:'Recovered capture',body:'Evidence',labelIds:[],operationId:'op-after-login',
}};
const store={{
  close:async()=>{{state.closed+=1;}},
  claimBatch:async login=>{{
    if(queued?.ownerLogin!==login)return [];
    const claimed=queued;queued=null;return [claimed];
  }},
  complete:async()=>{{}},release:async()=>{{}},fail:async()=>{{}},countBlocked:async()=>0,
}};
const fetchJson=async(url,options={{}})=>{{
  if(url==='api/v1/background-identity'){{state.identityLookups+=1;return {{login:'timmy'}};}}
  state.mutations+=1;state.keys.push(options.headers['Idempotency-Key']);
  return {{repository:'stackchain/dashboard',number:425,title:'Recovered capture'}};
}};
(async()=>{{
  const sync=createBackgroundIssueSync({{store,fetchJson}});
  await sync.purge();
  const blocked=await sync.flush();
  await sync.resume();
  const delivered=await sync.flush();
  process.stdout.write(JSON.stringify({{state,blocked,delivered}}));
}})();
"""
    output = run_node(script)

    assert output["state"] == {
        "closed": 1,
        "identityLookups": 1,
        "mutations": 1,
        "keys": ["op-after-login"],
    }
    assert output["blocked"]["login"] == ""
    assert output["delivered"]["login"] == "timmy"
    assert output["delivered"]["confirmed"] == [
        {"repository": "stackchain/dashboard", "number": 425, "title": "Recovered capture"}
    ]


@pytest.mark.anyio
async def test_background_identity_is_lightweight_and_never_cacheable(monkeypatch):
    calls = 0

    async def user():
        nonlocal calls
        calls += 1
        return {"id": 7, "login": "timmy", "email": "private@example.com"}

    monkeypatch.setattr(main, "current_user", user)
    transport = httpx.ASGITransport(app=main.app)
    async with httpx.AsyncClient(transport=transport, base_url="http://test") as client:
        response = await client.get("/api/v1/background-identity")

    assert response.status_code == 200
    assert response.json() == {"login": "timmy"}
    assert response.headers["cache-control"] == "no-store"
    assert calls == 1


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

    assert '<script src="static/background-issue-sync.js"></script>' in html
    assert "const backgroundIssueStore = createIssueSyncStore();" in html
    assert "createBackgroundIssueSync({" in html
    assert "backgroundSync: backgroundIssueSync" in html
    assert "registration.sync.register('stackchain-issue-outbox-v1')" in html
    assert "backgroundIssueSync.snapshot().then(records => {" in html
    assert "issueOutbox.reconcileBackground(records);" in html
    assert "authoredOutbox.reconcileBackground(records);" in html
    assert "if ('indexedDB' in window)" in html


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

    assert 'id="delivery-receipts" type="checkbox"' in html
    assert "deliveryReceipts.addEventListener('change', async () => {" in html
    assert "await Notification.requestPermission()" in html
    assert "backgroundIssueSync.setReceiptPreference(confirmedOwnerLogin, enabled)" in html
    assert "await backgroundIssueSync.getReceiptPreference(confirmedOwnerLogin)" in html
    assert "onQueue: openWorkQueueRoute" in html
    assert "workRoute.queue(filter)" in html
    assert "openDeliveryReceiptRoute" not in html
