import json
import subprocess
from pathlib import Path


TODAY_SYNC = Path(__file__).parents[1] / "frontend" / "today-sync.js"
COORDINATOR = Path(__file__).parents[1] / "frontend" / "outbox-coordinator.js"
DASHBOARD = Path(__file__).parents[1] / "frontend" / "dashboard.js"


def test_two_tabs_cannot_erase_each_others_today_edits_or_double_drain():
    script = f"""
const createTodaySync = require({json.dumps(str(TODAY_SYNC))});
const createCoordinator = require({json.dumps(str(COORDINATOR))});
const values = new Map();
const storage = {{get length(){{return values.size}},key:i=>[...values.keys()][i]||null,
 getItem:k=>values.get(k)||null,setItem:(k,v)=>values.set(k,v),removeItem:k=>values.delete(k)}};
const channels=[]; const channelFactory=()=>{{const channel={{onmessage:null,postMessage:data=>channels.filter(x=>x!==channel).forEach(x=>x.onmessage?.({{data}})),close(){{}}}};channels.push(channel);return channel}};
const held=new Set(); const locks={{request:async(name,_options,work)=>{{if(held.has(name))return work(null);held.add(name);try{{return await work({{name}})}}finally{{held.delete(name)}}}}}};
let sequence=0,releaseFirst;const delivered=[];
const make=tab=>createTodaySync({{storage,getLogin:()=> 'timmy',createOperationId:()=>tab+'-'+(++sequence),coordinator:createCoordinator({{storage,locks,channelFactory,tabId:tab}}),
 fetchJson:async(_url,options={{}})=>{{if(!options.method)return {{revision:0,ids:[]}};const operation=JSON.parse(options.body).operations[0];delivered.push(operation.operation_id);if(delivered.length===1)await new Promise(resolve=>releaseFirst=resolve);return {{revision:delivered.length,ids:delivered}}}},onRemoteIds:()=>{{}},onStatus:()=>{{}}}});
const first=make('first'),second=make('second');first.enqueue('add','issue:r:1:');
(async()=>{{const draining=first.flush();while(!releaseFirst)await Promise.resolve();second.enqueue('add','issue:r:2:');const competing=second.flush();releaseFirst();await Promise.all([draining,competing]);await first.flush();process.stdout.write(JSON.stringify({{delivered,pending:first.pending(),keys:[...values.keys()]}}));}})();
"""
    result = json.loads(subprocess.run(["node", "-e", script], check=True, capture_output=True, text=True).stdout)
    assert sorted(result["delivered"]) == ["first-1", "second-2"]
    assert len(result["delivered"]) == 2
    assert result["pending"] == []
    assert not any(".operation." in key for key in result["keys"])


def test_operations_enqueued_during_an_inflight_flush_are_drained_before_it_settles():
    script = f"""
const createTodaySync = require({json.dumps(str(TODAY_SYNC))});
const values = new Map();
const patches = [];
let sequence = 0;
let releaseFirstPatch;
const firstPatchBlocked = new Promise(resolve => {{ globalThis.firstPatchStarted = resolve; }});
const sync = createTodaySync({{
  storage: {{getItem:k=>values.get(k)||null,setItem:(k,v)=>values.set(k,v),removeItem:k=>values.delete(k)}},
  getLogin: () => 'timmy', createOperationId: () => 'op-' + (++sequence),
  fetchJson: async (_url, options={{}}) => {{
    if (!options.method) return {{revision:0, ids:[]}};
    const operation = JSON.parse(options.body).operations[0];
    patches.push(operation);
    if (patches.length === 1) {{
      globalThis.firstPatchStarted();
      await new Promise(resolve => {{ releaseFirstPatch = resolve; }});
    }}
    return {{revision:patches.length, ids:patches.map(item => item.item_id)}};
  }},
  onRemoteIds: ids => {{ globalThis.ids = ids; }},
  onStatus: status => {{ globalThis.status = status; }},
}});
sync.enqueue('add', 'issue:r:1:');
(async () => {{
  const firstFlush = sync.flush();
  await firstPatchBlocked;
  sync.enqueue('add', 'issue:r:2:');
  const sharedFlush = sync.flush();
  const sharesInflight = firstFlush === sharedFlush;
  releaseFirstPatch();
  await sharedFlush;
  process.stdout.write(JSON.stringify({{
    sharesInflight, patches, pending:sync.pending(), ids:globalThis.ids, status:globalThis.status,
  }}));
}})();
"""
    result = json.loads(
        subprocess.run(["node", "-e", script], check=True, capture_output=True, text=True).stdout
    )

    assert result == {
        "sharesInflight": True,
        "patches": [
            {"operation_id": "op-1", "action": "add", "item_id": "issue:r:1:", "direction": None, "base_revision": 0},
            {"operation_id": "op-2", "action": "add", "item_id": "issue:r:2:", "direction": None, "base_revision": 0},
        ],
        "pending": [],
        "ids": ["issue:r:1:", "issue:r:2:"],
        "status": "saved",
    }


def test_activation_queued_during_an_empty_inflight_refresh_is_drained_before_it_settles():
    script = f"""
const createTodaySync = require({json.dumps(str(TODAY_SYNC))});
const values = new Map();
const storage = {{get length(){{return values.size}},key:i=>[...values.keys()][i]||null,
  getItem:k=>values.get(k)||null,setItem:(k,v)=>values.set(k,v),removeItem:k=>values.delete(k)}};
let releaseRefresh;
const refreshStarted = new Promise(resolve => {{ globalThis.refreshStarted = resolve; }});
const patches = [];
const statuses = [];
const sync = createTodaySync({{
  storage, getLogin: () => 'timmy', createOperationId: () => 'activation-1',
  fetchJson: async (_url, options={{}}) => {{
    if (!options.method) {{
      globalThis.refreshStarted();
      await new Promise(resolve => {{ releaseRefresh = resolve; }});
      return {{revision:0, ids:[]}};
    }}
    const operations = JSON.parse(options.body).operations;
    patches.push(...operations);
    return {{revision:1, ids:[], first_task_state:'complete',
      accepted_operation_ids:operations.map(item => item.operation_id)}};
  }},
  onRemoteIds: () => {{}}, onStatus: state => statuses.push(state),
}});
(async () => {{
  const refreshing = sync.flush();
  await refreshStarted;
  sync.enqueueActivation('complete');
  const sharedFlush = sync.flush();
  releaseRefresh();
  await sharedFlush;
  process.stdout.write(JSON.stringify({{
    sharesInflight: refreshing === sharedFlush, patches, pending:sync.pending(), statuses,
  }}));
}})();
"""
    result = json.loads(subprocess.run(
        ["node", "-e", script], check=True, capture_output=True, text=True
    ).stdout)

    assert result["sharesInflight"] is True
    assert [operation["activation_state"] for operation in result["patches"]] == ["complete"]
    assert result["pending"] == []
    assert result["statuses"][-1] == "saved"


def test_operations_queued_in_the_same_millisecond_preserve_enqueue_order():
    script = f"""
const createTodaySync = require({json.dumps(str(TODAY_SYNC))});
const values = new Map();
const storage = {{
  get length() {{ return values.size; }}, key: index => [...values.keys()][index] || null,
  getItem: key => values.get(key) || null,
  setItem: (key, value) => values.set(key, value), removeItem: key => values.delete(key),
}};
const operationIds = ['z-add-first', 'a-move-second'];
const sync = createTodaySync({{
  storage, getLogin: () => 'timmy', now: () => 1000,
  createOperationId: () => operationIds.shift(),
  fetchJson: async () => ({{revision: 0, ids: []}}),
}});
sync.enqueue('add', 'issue:r:1:');
sync.enqueue('move', 'issue:r:1:', 'up');
process.stdout.write(JSON.stringify(sync.pending().map(operation => operation.operation_id)));
"""

    assert json.loads(subprocess.run(
        ["node", "-e", script], check=True, capture_output=True, text=True
    ).stdout) == ["z-add-first", "a-move-second"]


def test_corrupt_queue_record_does_not_hide_or_strand_valid_today_edits():
    script = f"""
const createTodaySync = require({json.dumps(str(TODAY_SYNC))});
const prefix = 'stackchain.today-sync.v1.timmy.operation.';
const values = new Map([
  [prefix + 'first', JSON.stringify({{queued_at: 1000, operation: {{operation_id:'first', action:'add', item_id:'issue:r:1:'}}}})],
  [prefix + 'broken', '{{not-json'],
  [prefix + 'invalid', JSON.stringify({{queued_at: 1000, operation: {{operation_id:'invalid', action:'erase-everything'}}}})],
  [prefix + 'second', JSON.stringify({{queued_at: 1001, operation: {{operation_id:'second', action:'move', item_id:'issue:r:2:', direction:'up'}}}})],
]);
const storage = {{get length(){{return values.size}},key:i=>[...values.keys()][i]||null,
  getItem:k=>values.get(k)||null,setItem:(k,v)=>values.set(k,v),removeItem:k=>values.delete(k)}};
const patches=[]; const statuses=[];
const sync=createTodaySync({{storage,getLogin:()=> 'timmy',
 fetchJson:async(_url,options={{}})=>{{const operations=JSON.parse(options.body).operations;patches.push(...operations);return {{revision:1,ids:['issue:r:1:','issue:r:2:'],accepted_operation_ids:operations.map(x=>x.operation_id)}}}},
 onRemoteIds:()=>{{}},onStatus:(state,detail={{}})=>statuses.push([state,detail.discarded||0])}});
(async()=>{{const result=await sync.flush();process.stdout.write(JSON.stringify({{
 result,patches,statuses,remaining:[...values.keys()].filter(key=>key.startsWith(prefix)),pending:sync.pending()
}}));}})();
"""
    result = json.loads(subprocess.run(
        ["node", "-e", script], check=True, capture_output=True, text=True
    ).stdout)

    assert result["result"] is True
    assert [item["operation_id"] for item in result["patches"]] == ["first", "second"]
    assert result["pending"] == []
    assert result["remaining"] == []
    assert result["statuses"][-1] == ["recovered", 2]


def test_today_sync_reports_queue_recovery_instead_of_plain_saved_status():
    source = DASHBOARD.read_text()

    assert "Today queue recovered" in source
    assert "discarded ${detail.discarded} unreadable device record" in source


def test_today_capacity_configuration_syncs_offline_and_adopts_remote_plan():
    script = f"""
const createTodaySync = require({json.dumps(str(TODAY_SYNC))});
const values = new Map(); let delivered; let adopted; let broadcast;
const storage = {{get length(){{return values.size}},key:i=>[...values.keys()][i]||null,
  getItem:k=>values.get(k)||null,setItem:(k,v)=>values.set(k,v),removeItem:k=>values.delete(k)}};
const sync = createTodaySync({{
  storage, getLogin:()=> 'timmy', createOperationId:()=> 'capacity-op',
  createChannel:()=>({{addEventListener() {{}},postMessage:plan=>{{broadcast=plan}},close() {{}}}}),
  fetchJson:async(_url, options={{}})=>{{
    if (!options.method) return {{revision:2,ids:['issue:r:1:'],capacity_minutes:null,estimates:{{}}}};
    delivered=JSON.parse(options.body).operations[0];
    return {{revision:3,ids:['issue:r:1:'],capacity_minutes:120,estimates:{{'issue:r:1:':60}},accepted_operation_ids:['capacity-op'],duplicate_operation_ids:[],rejected_operations:[]}};
  }},
  onRemotePlan:plan=>{{adopted=plan}}, onRemoteIds:()=>{{}}, onStatus:()=>{{}},
}});
sync.enqueueConfiguration(120, {{'issue:r:1:':60}});
(async()=>{{await sync.flush();process.stdout.write(JSON.stringify({{delivered,adopted,broadcast,pending:sync.pending()}}));}})();
"""
    assert json.loads(subprocess.run(
        ["node", "-e", script], check=True, capture_output=True, text=True
    ).stdout) == {
        "delivered": {
            "operation_id": "capacity-op", "action": "configure", "item_id": "plan",
            "direction": None, "capacity_minutes": 120,
            "estimates": {"issue:r:1:": 60}, "base_revision": 0,
        },
        "adopted": {
            "revision": 3, "ids": ["issue:r:1:"], "capacity_minutes": 120,
            "estimates": {"issue:r:1:": 60},
            "accepted_operation_ids": ["capacity-op"],
            "duplicate_operation_ids": [], "rejected_operations": [],
        },
        "broadcast": {
            "revision": 3, "ids": ["issue:r:1:"], "capacity_minutes": 120,
            "estimates": {"issue:r:1:": 60},
        },
        "pending": [],
    }


def test_today_batch_admission_leaves_no_partial_operations_when_storage_fails():
    script = f"""
const createTodaySync = require({json.dumps(str(TODAY_SYNC))});
const values = new Map(); let writes = 0; let sequence = 0;
const storage = {{
  get length() {{ return values.size; }}, key: index => [...values.keys()][index] || null,
  getItem: key => values.get(key) || null,
  setItem: (key, value) => {{ writes += 1; throw new Error('quota'); }},
  removeItem: key => values.delete(key),
}};
const sync = createTodaySync({{
  storage, getLogin: () => 'timmy', createOperationId: () => 'batch-' + (++sequence),
  fetchJson: async () => {{ throw new Error('must not deliver'); }}, onStatus: () => {{}},
}});
const admitted = sync.enqueueBatch([
  {{action:'remove', item_id:'issue:r:1:'}},
  {{action:'activate', item_id:'first-task', activation_state:'complete'}},
]);
process.stdout.write(JSON.stringify({{admitted, writes, pending:sync.pending(), keys:[...values.keys()]}}));
"""
    result = subprocess.run(["node", "-e", script], capture_output=True, text=True)

    assert result.returncode == 0, result.stderr
    assert json.loads(result.stdout) == {
        "admitted": False,
        "writes": 1,
        "pending": [],
        "keys": [],
    }


def test_today_batch_delivers_remove_and_activation_together_then_clears_receipts():
    script = f"""
const createTodaySync = require({json.dumps(str(TODAY_SYNC))});
const values = new Map(); let sequence = 0; const patches = [];
const storage = {{
  get length() {{ return values.size; }}, key: index => [...values.keys()][index] || null,
  getItem: key => values.get(key) || null, setItem: (key,value) => values.set(key,value),
  removeItem: key => values.delete(key),
}};
const sync = createTodaySync({{
  storage, getLogin: () => 'timmy', createOperationId: () => 'batch-' + (++sequence),
  fetchJson: async (_url, options={{}}) => {{
    const operations = JSON.parse(options.body).operations; patches.push(operations);
    return {{revision:3, ids:[], first_task_state:'complete',
      accepted_operation_ids:operations.map(item => item.operation_id)}};
  }}, onRemoteIds: () => {{}}, onStatus: () => {{}},
}});
const admitted = sync.enqueueBatch([
  {{action:'remove', item_id:'issue:r:1:'}},
  {{action:'activate', item_id:'first-task', activation_state:'complete'}},
]);
const before = sync.pending();
(async()=>{{await sync.flush();process.stdout.write(JSON.stringify({{
  admitted, before, patches, after:sync.pending(), operationKeys:[...values.keys()].filter(key=>key.includes('.operation.')),
}}));}})();
"""
    result = subprocess.run(["node", "-e", script], capture_output=True, text=True)

    assert result.returncode == 0, result.stderr
    data = json.loads(result.stdout)
    assert data["admitted"] is True
    assert [[item["action"] for item in batch] for batch in data["patches"]] == [["remove", "activate"]]
    assert data["before"] == data["patches"][0]
    assert data["after"] == []
    assert data["operationKeys"] == []


def test_first_task_activation_uses_today_outbox_and_adopts_remote_completion():
    script = f"""
const createTodaySync = require({json.dumps(str(TODAY_SYNC))});
const values = new Map(); let sequence=0; const adopted=[]; const delivered=[]; const listeners={{}};
const storage = {{get length(){{return values.size}},key:i=>[...values.keys()][i]||null,
  getItem:k=>values.get(k)||null,setItem:(k,v)=>values.set(k,v),removeItem:k=>values.delete(k)}};
const sync = createTodaySync({{
  storage, getLogin:()=> 'timmy', createOperationId:()=> 'activation-' + (++sequence),
  fetchJson:async(_url, options={{}})=>{{
    if (!options.method) return {{revision:4,ids:[],first_task_state:'complete'}};
    const operations=JSON.parse(options.body).operations; delivered.push(...operations);
    return {{revision:5,ids:[],first_task_state:'complete',accepted_operation_ids:operations.map(x=>x.operation_id)}};
  }},
  onRemotePlan:plan=>adopted.push(plan.first_task_state), onRemoteIds:()=>{{}}, onStatus:()=>{{}},
}});
sync.startLifecycle({{window:{{addEventListener:(name,callback)=>listeners[name]=callback}},document:{{addEventListener(){{}}}}}});
listeners['stackchain:first-task-complete']();
listeners['stackchain:first-task-complete']();
(async()=>{{await sync.flush();process.stdout.write(JSON.stringify({{
  delivered,adopted,pending:sync.pending(),hydrated:values.get('stackchain.first-task.v1:timmy')
}}));}})();
"""
    result = json.loads(subprocess.run(
        ["node", "-e", script], check=True, capture_output=True, text=True
    ).stdout)

    assert result == {
        "delivered": [{
            "operation_id": "activation-1", "action": "activate", "item_id": "first-task",
            "direction": None, "activation_state": "complete", "base_revision": 0,
        }],
        "adopted": ["complete"],
        "pending": [],
        "hydrated": "complete",
    }


def test_inflight_today_drain_ships_in_a_new_offline_shell():
    source = (Path(__file__).parents[1] / "frontend" / "service-worker.js").read_text()

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


def test_local_operations_replay_once_then_adopt_server_order():
    script = f"""
const createTodaySync = require({json.dumps(str(TODAY_SYNC))});
const values = new Map();
const storage = {{
  getItem: key => values.get(key) || null,
  setItem: (key, value) => values.set(key, value),
  removeItem: key => values.delete(key),
}};
const requests = [];
let remote = {{revision: 1, ids:['issue:r:9:']}};
const sync = createTodaySync({{
  storage,
  getLogin: () => 'timmy',
  createOperationId: () => 'fixed-op',
  fetchJson: async (url, options={{}}) => {{
    requests.push({{url, body: options.body && JSON.parse(options.body)}});
    if (!options.method) return remote;
    remote = {{revision: remote.revision + 1, ids:['issue:r:9:', JSON.parse(options.body).operations[0].item_id]}};
    return remote;
  }},
  onRemoteIds: ids => {{ globalThis.adopted = ids; }},
  onStatus: status => {{ globalThis.status = status; }},
}});
sync.enqueue('add', 'issue:r:2:');
(async () => {{
  await sync.flush();
  await sync.flush();
  process.stdout.write(JSON.stringify({{
    requests, adopted: globalThis.adopted, status: globalThis.status,
    pending: sync.pending(),
  }}));
}})();
"""
    result = json.loads(
        subprocess.run(["node", "-e", script], check=True, capture_output=True, text=True).stdout
    )

    assert [request["url"] for request in result["requests"]] == [
        "api/v1/today",
        "api/v1/today",
    ]
    assert result["requests"][0]["body"] == {"operations": [{
        "operation_id": "fixed-op",
        "action": "add",
        "item_id": "issue:r:2:",
        "direction": None,
        "base_revision": 0,
    }]}
    assert result["adopted"] == ["issue:r:9:", "issue:r:2:"]
    assert result["status"] == "saved"
    assert result["pending"] == []


def test_today_operations_capture_server_revision_and_expire_before_replay():
    script = f"""
const createTodaySync = require({json.dumps(str(TODAY_SYNC))});
const values = new Map(); const statuses=[]; const requests=[];
const storage={{get length(){{return values.size}},key:i=>[...values.keys()][i]||null,
 getItem:k=>values.get(k)||null,setItem:(k,v)=>values.set(k,v),removeItem:k=>values.delete(k)}};
const sync=createTodaySync({{storage,getLogin:()=> 'timmy',createOperationId:()=> 'old-op',
 now:()=>1_800_000_000_000,maxOfflineMs:1000,
 fetchJson:async(_url,options={{}})=>{{requests.push(options.method||'GET');return {{revision:7,ids:['server']}}}},
 onRemoteIds:()=>{{}},onStatus:(state,detail={{}})=>statuses.push([state,detail.count||0])}});
(async()=>{{await sync.flush();sync.enqueue('move','issue:r:2:','up');
const pendingBefore=sync.pending();
const key=[...values.keys()].find(value=>value.includes('.operation.'));
const record=JSON.parse(values.get(key));record.queued_at=1_799_999_000_000;values.set(key,JSON.stringify(record));
await sync.flush();process.stdout.write(JSON.stringify({{pendingBefore,requests,statuses,pendingAfter:sync.pending()}}));}})();
"""
    result = json.loads(subprocess.run(["node", "-e", script], check=True, capture_output=True, text=True).stdout)

    assert result["pendingBefore"] == [{
        "operation_id": "old-op", "action": "move", "item_id": "issue:r:2:",
        "direction": "up", "base_revision": 7,
    }]
    assert result["requests"] == ["GET", "GET"]
    assert ["expired", 1] in result["statuses"]
    assert result["statuses"][-1] == ["expired", 1]
    assert result["pendingAfter"] == []


def test_pending_today_edits_are_sent_as_one_batch_without_a_preflight_get():
    script = f"""
const createTodaySync = require({json.dumps(str(TODAY_SYNC))});
const values = new Map(); const requests=[]; let sequence=0;
const sync=createTodaySync({{
 storage:{{getItem:k=>values.get(k)||null,setItem:(k,v)=>values.set(k,v),removeItem:k=>values.delete(k)}},
 getLogin:()=> 'timmy',createOperationId:()=> 'op-'+(++sequence),
 fetchJson:async (_url,options={{}})=>{{const body=JSON.parse(options.body);requests.push(body);return {{revision:2,ids:['issue:r:1:','issue:r:2:'],accepted_operation_ids:body.operations.map(x=>x.operation_id),duplicate_operation_ids:[],rejected_operations:[]}}}},
 onRemoteIds:()=>{{}},onStatus:()=>{{}},
}});
sync.enqueue('add','issue:r:1:');sync.enqueue('add','issue:r:2:');
(async()=>{{await sync.flush();process.stdout.write(JSON.stringify({{requests,pending:sync.pending()}}));}})();
"""
    result = json.loads(subprocess.run(["node", "-e", script], check=True, capture_output=True, text=True).stdout)
    assert len(result["requests"]) == 1
    assert [item["operation_id"] for item in result["requests"][0]["operations"]] == ["op-1", "op-2"]
    assert result["pending"] == []


def test_failed_delivery_stays_pending_for_offline_replay():
    script = f"""
const createTodaySync = require({json.dumps(str(TODAY_SYNC))});
const values = new Map();
const sync = createTodaySync({{
  storage: {{getItem:k=>values.get(k)||null,setItem:(k,v)=>values.set(k,v),removeItem:k=>values.delete(k)}},
  getLogin: () => 'timmy', createOperationId: () => 'offline-op',
  fetchJson: async () => {{ throw new Error('offline'); }},
  onRemoteIds: () => {{}}, onStatus: value => {{ globalThis.status = value; }},
}});
sync.enqueue('move', 'issue:r:2:', 'up');
(async () => {{ await sync.flush(); process.stdout.write(JSON.stringify({{pending:sync.pending(),status:globalThis.status}})); }})();
"""
    result = json.loads(
        subprocess.run(["node", "-e", script], check=True, capture_output=True, text=True).stdout
    )
    assert result == {
        "pending": [{
            "operation_id": "offline-op",
            "action": "move",
            "item_id": "issue:r:2:",
            "direction": "up",
            "base_revision": 0,
        }],
        "status": "retrying",
    }


def test_retryable_failure_replays_automatically_once_with_same_operation_id():
    script = f"""
const createTodaySync = require({json.dumps(str(TODAY_SYNC))});
const values = new Map(); const timers = []; const statuses = []; const patches = [];
let attempts = 0;
const sync = createTodaySync({{
  storage: {{getItem:k=>values.get(k)||null,setItem:(k,v)=>values.set(k,v),removeItem:k=>values.delete(k)}},
  getLogin:()=> 'timmy', createOperationId:()=> 'stable-op',
  setTimer:(callback,delay)=>{{timers.push({{callback,delay}});return timers.length}}, clearTimer:()=>{{}},
  fetchJson:async (_url,options={{}})=>{{
    attempts += 1;
    if (attempts === 1) {{ const error = new Error('busy'); error.status=503; error.retryAfter=2; throw error; }}
    if (options.method) patches.push(...JSON.parse(options.body).operations);
    return options.method ? {{revision:1,ids:['issue:r:2:']}} : {{revision:0,ids:[]}};
  }},
  onRemoteIds:()=>{{}}, onStatus:(state,detail)=>statuses.push([state,detail?.delayMs||null]),
}});
sync.enqueue('add','issue:r:2:');
(async()=>{{
  await sync.flush();
  const scheduled = timers.map(timer=>timer.delay);
  await timers[0].callback();
  process.stdout.write(JSON.stringify({{scheduled,attempts,patches,pending:sync.pending(),statuses}}));
}})();
"""
    result = json.loads(
        subprocess.run(["node", "-e", script], check=True, capture_output=True, text=True).stdout
    )

    assert result["scheduled"] == [2000]
    assert result["attempts"] == 2
    assert result["patches"] == [{
        "operation_id": "stable-op", "action": "add", "item_id": "issue:r:2:", "direction": None, "base_revision": 0,
    }]
    assert result["pending"] == []
    assert ["retrying", 2000] in result["statuses"]
    assert result["statuses"][-1] == ["saved", None]


def test_duplicate_pending_retirements_collapse_to_one_effective_remove():
    script = f"""
const createTodaySync = require({json.dumps(str(TODAY_SYNC))});
const values = new Map();
let sequence = 0;
const statuses = [];
const sync = createTodaySync({{
  storage: {{getItem:k=>values.get(k)||null,setItem:(k,v)=>values.set(k,v),removeItem:k=>values.delete(k)}},
  getLogin: () => 'timmy', createOperationId: () => 'remove-' + (++sequence),
  fetchJson: async () => ({{revision:0,ids:[]}}), onRemoteIds:()=>{{}}, onStatus:value=>statuses.push(value),
}});
const first = sync.enqueue('remove', 'issue:r:1:');
const duplicate = sync.enqueue('remove', 'issue:r:1:');
process.stdout.write(JSON.stringify({{first, duplicate, pending:sync.pending(), statuses}}));
"""
    result = json.loads(
        subprocess.run(["node", "-e", script], check=True, capture_output=True, text=True).stdout
    )
    assert result == {
        "first": True,
        "duplicate": True,
        "pending": [{
            "operation_id": "remove-1",
            "action": "remove",
            "item_id": "issue:r:1:",
            "direction": None,
            "base_revision": 0,
        }],
        "statuses": ["pending", "pending"],
    }


def test_existing_device_queue_is_migrated_only_once():
    script = f"""
const createTodaySync = require({json.dumps(str(TODAY_SYNC))});
const values = new Map();
let sequence = 0;
const sync = createTodaySync({{
  storage: {{getItem:k=>values.get(k)||null,setItem:(k,v)=>values.set(k,v),removeItem:k=>values.delete(k)}},
  getLogin: () => 'timmy', createOperationId: () => 'migration-' + (++sequence),
  fetchJson: async () => ({{revision:0,ids:[]}}), onRemoteIds:()=>{{}}, onStatus:()=>{{}},
}});
const first = sync.migrate(['issue:r:1:', 'issue:r:2:']);
const second = sync.migrate(['issue:r:1:', 'issue:r:2:']);
process.stdout.write(JSON.stringify({{first, second, pending:sync.pending()}}));
"""
    result = json.loads(
        subprocess.run(["node", "-e", script], check=True, capture_output=True, text=True).stdout
    )
    assert result == {
        "first": True,
        "second": False,
        "pending": [
            {"operation_id": "migration-1", "action": "add", "item_id": "issue:r:1:", "direction": None, "base_revision": 0},
            {"operation_id": "migration-2", "action": "add", "item_id": "issue:r:2:", "direction": None, "base_revision": 0},
        ],
    }


def test_server_limit_conflict_drops_rejected_add_and_adopts_server_truth():
    script = f"""
const createTodaySync = require({json.dumps(str(TODAY_SYNC))});
const values = new Map();
let patch = false;
const sync = createTodaySync({{
  storage: {{getItem:k=>values.get(k)||null,setItem:(k,v)=>values.set(k,v),removeItem:k=>values.delete(k)}},
  getLogin: () => 'timmy', createOperationId: () => 'sixth',
  fetchJson: async (_url, options={{}}) => {{
    patch = true;
    return {{revision:5, ids:['1','2','3','4','5'], accepted_operation_ids:[], duplicate_operation_ids:[], rejected_operations:[{{operation_id:'sixth',reason:'today_full'}}]}};
  }},
  onRemoteIds:ids=>{{globalThis.ids=ids}}, onStatus:value=>{{globalThis.status=value}},
}});
sync.enqueue('add', '6');
(async()=>{{await sync.flush();process.stdout.write(JSON.stringify({{patch,pending:sync.pending(),ids:globalThis.ids,status:globalThis.status}}));}})();
"""
    result = json.loads(
        subprocess.run(["node", "-e", script], check=True, capture_output=True, text=True).stdout
    )
    assert result == {
        "patch": True,
        "pending": [],
        "ids": ["1", "2", "3", "4", "5"],
        "status": "full",
    }


def test_server_limit_conflict_drops_only_the_rejected_operation_and_drains_later_edits():
    script = f"""
const createTodaySync = require({json.dumps(str(TODAY_SYNC))});
const values = new Map();
const patches = [];
let sequence = 0;
const statuses = [];
const sync = createTodaySync({{
  storage: {{getItem:k=>values.get(k)||null,setItem:(k,v)=>values.set(k,v),removeItem:k=>values.delete(k)}},
  getLogin: () => 'timmy', createOperationId: () => 'op-' + (++sequence),
  fetchJson: async (_url, options={{}}) => {{
    const operations = JSON.parse(options.body).operations;
    patches.push(...operations);
    return {{revision:6, ids:['1','2','3','4'], accepted_operation_ids:['op-2'], duplicate_operation_ids:[], rejected_operations:[{{operation_id:'op-1',reason:'today_full'}}]}};
  }},
  onRemoteIds: ids => {{ globalThis.ids = ids; }},
  onStatus: status => statuses.push(status),
}});
sync.enqueue('add', '6');
sync.enqueue('remove', '5');
(async()=>{{
  const result = await sync.flush();
  process.stdout.write(JSON.stringify({{result, patches, pending:sync.pending(), ids:globalThis.ids, statuses}}));
}})();
"""
    result = json.loads(
        subprocess.run(["node", "-e", script], check=True, capture_output=True, text=True).stdout
    )

    assert result == {
        "result": False,
        "patches": [
            {"operation_id": "op-1", "action": "add", "item_id": "6", "direction": None, "base_revision": 0},
            {"operation_id": "op-2", "action": "remove", "item_id": "5", "direction": None, "base_revision": 0},
        ],
        "pending": [],
        "ids": ["1", "2", "3", "4"],
        "statuses": ["pending", "pending", "full"],
    }


def test_tabs_ignore_an_older_response_after_a_newer_revision_is_broadcast():
    script = f"""
const createTodaySync = require({json.dumps(str(TODAY_SYNC))});
const values = new Map();
const storage = {{getItem:k=>values.get(k)||null,setItem:(k,v)=>values.set(k,v),removeItem:k=>values.delete(k)}};
const listeners = [];
const createChannel = () => ({{
  addEventListener: (_name, listener) => listeners.push(listener),
  postMessage: data => listeners.forEach(listener => listener({{data}})),
}});
let resolveOld;
let resolveNew;
const oldHistory = [];
const newHistory = [];
const oldTab = createTodaySync({{
  storage, getLogin:()=> 'timmy', createChannel,
  fetchJson:()=>new Promise(resolve=>{{resolveOld=resolve}}),
  onRemoteIds:ids=>oldHistory.push(ids), onStatus:()=>{{}},
}});
const newTab = createTodaySync({{
  storage, getLogin:()=> 'timmy', createChannel,
  fetchJson:()=>new Promise(resolve=>{{resolveNew=resolve}}),
  onRemoteIds:ids=>newHistory.push(ids), onStatus:()=>{{}},
}});
(async()=>{{
  const oldFlush = oldTab.flush();
  const newFlush = newTab.flush();
  await Promise.resolve();
  resolveNew({{revision:2,ids:['new']}});
  await newFlush;
  resolveOld({{revision:1,ids:['old']}});
  await oldFlush;
  process.stdout.write(JSON.stringify({{oldHistory,newHistory}}));
}})();
"""
    result = json.loads(
        subprocess.run(["node", "-e", script], check=True, capture_output=True, text=True).stdout
    )

    assert result["oldHistory"][-1] == ["new"]
    assert result["newHistory"][-1] == ["new"]


def test_returning_online_replays_pending_operations():
    script = f"""
const createTodaySync = require({json.dumps(str(TODAY_SYNC))});
const values = new Map();
const handlers = {{}};
const requests = [];
let revision = 1;
const sync = createTodaySync({{
  storage: {{getItem:k=>values.get(k)||null,setItem:(k,v)=>values.set(k,v),removeItem:k=>values.delete(k)}},
  getLogin:()=> 'timmy', createOperationId:()=> 'offline-add',
  fetchJson:async (_url, options={{}})=>{{
    requests.push(options.method || 'GET');
    if (options.method) return {{revision:++revision,ids:['issue:r:1:']}};
    return {{revision,ids:[]}};
  }},
  onRemoteIds:()=>{{}}, onStatus:()=>{{}},
}});
sync.enqueue('add', 'issue:r:1:');
sync.startLifecycle({{
  window: {{addEventListener:(name, handler)=>{{handlers[name]=handler}}}},
  document: {{addEventListener:()=>{{}}, hidden:false}},
}});
(async()=>{{
  await handlers.online();
  process.stdout.write(JSON.stringify({{requests,pending:sync.pending()}}));
}})();
"""
    result = json.loads(
        subprocess.run(["node", "-e", script], check=True, capture_output=True, text=True).stdout
    )

    assert result == {"requests": ["PATCH"], "pending": []}


def test_foregrounding_a_stale_tab_refreshes_the_saved_plan():
    script = f"""
const createTodaySync = require({json.dumps(str(TODAY_SYNC))});
const handlers = {{}};
const adopted = [];
const documentObject = {{
  hidden:true,
  addEventListener:(name, handler)=>{{handlers[name]=handler}},
}};
const sync = createTodaySync({{
  storage: {{getItem:()=>null,setItem:()=>{{}},removeItem:()=>{{}}}},
  getLogin:()=> 'timmy',
  fetchJson:async ()=>({{revision:7,ids:['issue:r:7:']}}),
  onRemoteIds:ids=>adopted.push(ids), onStatus:()=>{{}},
}});
sync.startLifecycle({{
  window: {{addEventListener:()=>{{}}}}, document:documentObject,
}});
(async()=>{{
  await handlers.visibilitychange();
  documentObject.hidden = false;
  await handlers.visibilitychange();
  process.stdout.write(JSON.stringify(adopted));
}})();
"""
    result = json.loads(
        subprocess.run(["node", "-e", script], check=True, capture_output=True, text=True).stdout
    )

    assert result == [["issue:r:7:"]]


def test_old_account_broadcasts_are_ignored_after_identity_changes():
    script = f"""
const createTodaySync = require({json.dumps(str(TODAY_SYNC))});
let login = 'timmy';
let listener;
const adopted = [];
const sync = createTodaySync({{
  storage: {{getItem:()=>null,setItem:()=>{{}},removeItem:()=>{{}}}},
  getLogin:()=>login,
  createChannel:()=>({{addEventListener:(_name, handler)=>{{listener=handler}},postMessage:()=>{{}},close:()=>{{}}}}),
  fetchJson:async ()=>({{revision:1,ids:['timmy-plan']}}),
  onRemoteIds:ids=>adopted.push(ids), onStatus:()=>{{}},
}});
(async()=>{{
  await sync.flush();
  login = 'alexander';
  listener({{data:{{revision:2,ids:['timmy-secret']}}}});
  process.stdout.write(JSON.stringify(adopted));
}})();
"""
    result = json.loads(
        subprocess.run(["node", "-e", script], check=True, capture_output=True, text=True).stdout
    )

    assert result == [["timmy-plan"]]
