import json
import subprocess
from pathlib import Path

import pytest

from tests.dashboard_bundle import dashboard


ROOT = Path(__file__).parents[1]
CONTROLLER = ROOT / "frontend" / "update-reply-read-next.js"
SYNC = ROOT / "frontend" / "background-issue-sync.js"
DRAFTS = ROOT / "frontend" / "drafts.js"


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


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

    assert 'id="send-update-reply-read-next"' in html
    assert '>Reply, mark read &amp; next</button>' in html
    assert "workSession.checkpointed(selectedUpdate)" not in html
    assert '<script src="static/update-reply-read-next.js"></script>' in html
    assert ".update-reply-actions button { min-height:44px; width:100%; }" in html


def test_reply_read_next_orders_online_delivery_and_is_single_flight():
    script = f"""
const createController = require({json.dumps(str(CONTROLLER))});
const calls=[]; let releaseReply;
const controller=createController({{
  post:(item,body,id)=>{{calls.push(['reply',item.notification_id,body,id]);return new Promise(resolve=>releaseReply=resolve);}},
  markRead:id=>{{calls.push(['read',id]);return Promise.resolve({{ok:true}});}},
  queue:()=>{{throw new Error('must not queue');}}, canQueue:()=>false,
  accept:item=>calls.push(['accept',item.notification_id]),
  next:item=>{{calls.push(['next',item.notification_id]);return {{next:true}};}},
}});
const item={{notification_id:91}};
const first=controller.submit(item,'Done','op-91');
const second=controller.submit(item,'Done','op-91');
releaseReply({{id:7}});
Promise.all([first,second]).then(results=>process.stdout.write(JSON.stringify({{same:first===second,calls,results}})));
"""
    output = run_node(script)

    assert output == {
        "same": True,
        "calls": [
            ["reply", 91, "Done", "op-91"],
            ["read", 91],
            ["accept", 91],
            ["next", 91],
        ],
        "results": [
            {"accepted": True, "delivery": "posted", "next": {"next": True}},
            {"accepted": True, "delivery": "posted", "next": {"next": True}},
        ],
    }


def test_reply_read_next_waits_until_the_next_update_is_open():
    script = f"""
const createController=require({json.dumps(str(CONTROLLER))});
let openNext;
const controller=createController({{
  post:async()=>({{id:1}}),markRead:async()=>({{ok:true}}),queue:async()=>null,canQueue:()=>false,
  next:()=>new Promise(resolve=>openNext=()=>resolve({{notification_id:2}})),
}});
let settled=false;
const result=controller.submit({{notification_id:1}},'Done','op-1').then(value=>{{settled=true;return value;}});
setTimeout(()=>{{
  const before=settled;
  openNext();
  result.then(value=>process.stdout.write(JSON.stringify({{before,value}})));
}},0);
"""
    output = run_node(script)

    assert output == {
        "before": False,
        "value": {
            "accepted": True,
            "delivery": "posted",
            "next": {"notification_id": 2},
        },
    }


def test_reply_read_next_durably_checkpoints_partial_delivery_without_reposting():
    script = f"""
const createController = require({json.dumps(str(CONTROLLER))});
const calls=[];
const offline=Object.assign(new Error('offline'),{{status:503}});
const controller=createController({{
  post:async()=>{{calls.push('reply');return{{id:8}};}},
  markRead:async()=>{{calls.push('read');throw offline;}},
  canQueue:error=>error.status===503,
  queue:async message=>{{calls.push(['queue',message]);return{{item:{{id:'saved'}},background:true}};}},
  accept:item=>calls.push(['accept',item.notification_id]),
  next:item=>{{calls.push(['next',item.notification_id]);return true;}},
}});
controller.submit({{notification_id:22}},'Ship it','stable-op').then(result=>
  process.stdout.write(JSON.stringify({{calls,result}})));
"""
    output = run_node(script)

    assert output["calls"] == [
        "reply",
        "read",
        ["queue", {
            "kind": "update-reply-read",
            "notificationId": 22,
            "body": "Ship it",
            "operationId": "stable-op",
            "replyConfirmed": True,
        }],
        ["accept", 22],
        ["next", 22],
    ]
    assert output["result"] == {"accepted": True, "delivery": "queued", "next": True}


def test_background_retry_resumes_at_read_after_reply_checkpoint():
    item = {
        "id": "op-31", "operationId": "op-31", "ownerLogin": "timmy",
        "status": "queued", "kind": "update-reply-read", "notificationId": 31,
        "body": "Acknowledged", "replyConfirmed": True,
    }
    script = f"""
const createSync=require({json.dumps(str(SYNC))});
let queued={json.dumps(item)};const calls=[];
const store={{
  claimNext:async owner=>queued?(queued=null,{{...{json.dumps(item)}}}):null,
  complete:async()=>calls.push('complete'),release:async()=>calls.push('release'),
  fail:async()=>calls.push('fail'),countBlocked:async()=>0,
  checkpoint:async()=>{{throw new Error('must not checkpoint again');}},
}};
const fetchJson=async(url,options={{}})=>{{calls.push([url,options.method]);return url==='api/v1/background-identity'?{{login:'timmy'}}:{{ok:true}};}};
createSync({{store,fetchJson}}).flush().then(result=>process.stdout.write(JSON.stringify({{calls,result}})));
"""
    output = run_node(script)

    assert output["calls"] == [
        ["api/v1/background-identity", None],
        ["api/v1/notifications/31/read", "PATCH"],
        "complete",
    ]
    assert output["result"]["confirmed"] == [{"ok": True}]


def test_partial_reply_read_delivery_is_recoverable_from_update_drafts():
    record = {"version": 2, "items": [{
        "id": "op-44", "operationId": "op-44", "ownerLogin": "timmy",
        "status": "attention", "kind": "update-reply-read", "notificationId": 44,
        "body": "Handled", "replyConfirmed": True, "error": "Read failed",
    }]}
    script = f"""
const createDraftInbox=require({json.dumps(str(DRAFTS))});
const values=new Map([['stackchain.authored-outbox.v1',JSON.stringify({json.dumps(record)})]]);
const storage={{get length(){{return values.size}},key:i=>[...values.keys()][i],getItem:k=>values.get(k)||null,
  setItem:(k,v)=>values.set(k,v),removeItem:k=>values.delete(k)}};
const item=createDraftInbox({{storage,getCurrentLogin:()=>'timmy'}}).list()[0];
process.stdout.write(JSON.stringify(item));
"""
    output = run_node(script)

    assert output["outbox_kind"] == "update-reply-read"
    assert output["title"] == "Update #44"
    assert output["route"] == {"kind": "update", "notification_id": 44}
    assert output["status"] == "attention"
