import json
import subprocess
from pathlib import Path

import pytest

from tests.dashboard_bundle import dashboard


COMMENT_NEXT = Path(__file__).parents[1] / "frontend" / "comment-next.js"
MY_WORK = Path(__file__).parents[1] / "frontend" / "my-work.js"


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


def test_comment_and_next_posts_then_completes_current_today_item_once():
    script = f"""
const createCommentNext = require({json.dumps(str(COMMENT_NEXT))});
const calls = [];
let finishPost;
const controller = createCommentNext({{
  post: (item, body) => {{ calls.push(`post:${{item.number}}:${{body}}`); return new Promise(resolve => {{ finishPost = resolve; }}); }},
  queue: () => {{ throw new Error('must not queue'); }},
  canQueue: () => false,
  complete: item => {{ calls.push(`complete:${{item.number}}`); return true; }},
}});
const item = {{kind:'issue', repository:'stackchain/dashboard', number:429}};
const first = controller.submit(item, 'Handoff ready');
const second = controller.submit(item, 'Handoff ready');
if (first !== second) throw new Error('submission was not single-flight');
finishPost({{id:7, body:'Handoff ready'}});
(async () => {{
  const result = await first;
  process.stdout.write(JSON.stringify({{result, calls, busy:controller.busy()}}));
}})().catch(error => {{ console.error(error); process.exit(1); }});
"""

    assert json.loads(run_node(script)) == {
        "result": {
            "accepted": True,
            "delivery": "posted",
            "comment": {"id": 7, "body": "Handoff ready"},
            "completed": True,
        },
        "calls": ["post:429:Handoff ready", "complete:429"],
        "busy": False,
    }


def test_comment_and_next_advances_after_durable_offline_admission():
    script = f"""
const createCommentNext = require({json.dumps(str(COMMENT_NEXT))});
const calls = [];
const retryable = new Error('offline'); retryable.status = 503;
const controller = createCommentNext({{
  post: () => Promise.reject(retryable),
  canQueue: error => error.status === 503,
  queue: message => {{ calls.push(message); return Promise.resolve({{durable:true, background:false}}); }},
  complete: item => {{ calls.push(`complete:${{item.number}}`); return true; }},
}});
(async () => {{
  const result = await controller.submit({{kind:'pull', repository:'stackchain/dashboard', number:12}}, 'Please review', 'op-12');
  process.stdout.write(JSON.stringify({{result, calls}}));
}})().catch(error => {{ console.error(error); process.exit(1); }});
"""

    assert json.loads(run_node(script)) == {
        "result": {
            "accepted": True,
            "delivery": "saved",
            "background": False,
            "completed": True,
        },
        "calls": [
            {
                "kind": "pull-comment",
                "repository": "stackchain/dashboard",
                "number": 12,
                "body": "Please review",
                "operationId": "op-12",
            },
            "complete:12",
        ],
    }


def test_update_reply_and_next_preserves_notification_identity_online_and_offline():
    script = f"""
const createCommentNext = require({json.dumps(str(COMMENT_NEXT))});
const calls = [];
const retryable = Object.assign(new Error('offline'), {{status:503}});
let offline = false;
const controller = createCommentNext({{
  queueKind: 'update-reply',
  post: (item, body, operationId) => {{
    calls.push({{post:[item.notification_id, body, operationId]}});
    return offline ? Promise.reject(retryable) : Promise.resolve({{id:17}});
  }},
  canQueue: () => true,
  queue: message => {{ calls.push({{queue:message}}); return Promise.resolve({{durable:true}}); }},
  complete: item => {{ calls.push({{complete:item.notification_id}}); return true; }},
}});
(async () => {{
  const item = {{kind:'issue', notification_id:91, repository:'stackchain/dashboard', number:8}};
  const posted = await controller.submit(item, 'Online reply', 'reply-91-a');
  offline = true;
  const saved = await controller.submit(item, 'Offline reply', 'reply-91-b');
  process.stdout.write(JSON.stringify({{posted, saved, calls}}));
}})().catch(error => {{ console.error(error); process.exit(1); }});
"""

    assert json.loads(run_node(script)) == {
        "posted": {
            "accepted": True,
            "delivery": "posted",
            "comment": {"id": 17},
            "completed": True,
        },
        "saved": {
            "accepted": True,
            "delivery": "saved",
            "background": False,
            "completed": True,
        },
        "calls": [
            {"post": [91, "Online reply", "reply-91-a"]},
            {"complete": 91},
            {"post": [91, "Offline reply", "reply-91-b"]},
            {
                "queue": {
                    "kind": "update-reply",
                    "notificationId": 91,
                    "body": "Offline reply",
                    "operationId": "reply-91-b",
                }
            },
            {"complete": 91},
        ],
    }


def test_comment_and_next_reads_retry_identity_after_the_failed_post():
    script = f"""
const createCommentNext = require({json.dumps(str(COMMENT_NEXT))});
let operationId = '';
let queued;
const controller = createCommentNext({{
  post: () => {{ operationId = 'post-attempt-id'; return Promise.reject(Object.assign(new Error('timeout'), {{status:503}})); }},
  canQueue: () => true,
  queue: message => {{ queued = message; return Promise.resolve({{durable:true}}); }},
  complete: () => true,
}});
(async () => {{
  const result = await controller.submit(
    {{kind:'issue', repository:'r', number:3}}, 'Status', () => operationId
  );
  process.stdout.write(JSON.stringify({{result, queued}}));
}})().catch(error => {{ console.error(error); process.exit(1); }});
"""

    output = json.loads(run_node(script))
    assert output["result"]["accepted"] is True
    assert output["result"]["delivery"] == "saved"
    assert output["queued"]["operationId"] == "post-attempt-id"


def test_comment_and_next_clears_the_completed_draft_before_opening_next_item():
    script = f"""
const createCommentNext = require({json.dumps(str(COMMENT_NEXT))});
const calls = [];
const controller = createCommentNext({{
  post: () => Promise.resolve({{id:1}}), queue: () => null, canQueue: () => false,
  accept: item => calls.push(`clear:${{item.number}}`),
  complete: item => {{ calls.push(`complete:${{item.number}}`); return true; }},
}});
(async () => {{
  await controller.submit({{kind:'issue',repository:'r',number:8}}, 'Done');
  process.stdout.write(JSON.stringify(calls));
}})().catch(error => {{ console.error(error); process.exit(1); }});
"""

    assert json.loads(run_node(script)) == ["clear:8", "complete:8"]


def test_comment_and_next_keeps_today_position_when_delivery_or_completion_fails():
    script = f"""
const createCommentNext = require({json.dumps(str(COMMENT_NEXT))});
const failures = [];
const permanent = createCommentNext({{
  post: () => Promise.reject(Object.assign(new Error('forbidden'), {{status:403}})),
  canQueue: () => false,
  queue: () => Promise.resolve({{durable:true}}),
  complete: () => {{ failures.push('advanced'); return true; }},
}});
const notDurable = createCommentNext({{
  post: () => Promise.reject(Object.assign(new Error('offline'), {{status:503}})),
  canQueue: () => true,
  queue: () => Promise.resolve({{durable:false, background:false}}),
  complete: () => {{ failures.push('advanced'); return true; }},
}});
const cannotComplete = createCommentNext({{
  post: () => Promise.resolve({{id:9}}), canQueue: () => false, queue: () => null,
  complete: () => false,
}});
(async () => {{
  for (const [name, controller] of [['permanent', permanent], ['notDurable', notDurable]]) {{
    try {{ await controller.submit({{kind:'issue',repository:'r',number:1}}, 'draft'); }}
    catch (error) {{ failures.push(`${{name}}:${{error.message}}`); }}
  }}
  const partial = await cannotComplete.submit({{kind:'issue',repository:'r',number:1}}, 'draft');
  process.stdout.write(JSON.stringify({{failures, partial}}));
}})().catch(error => {{ console.error(error); process.exit(1); }});
"""

    assert json.loads(run_node(script)) == {
        "failures": ["permanent:forbidden", "notDurable:Comment was not saved for delivery."],
        "partial": {
            "accepted": True,
            "delivery": "posted",
            "comment": {"id": 9},
            "completed": False,
        },
    }


def test_today_checkpoint_only_matches_the_current_session_item():
    script = f"""
const buildMyWork = require({json.dumps(str(MY_WORK))});
const items = [
  {{kind:'issue', repository:'r', number:1}},
  {{kind:'pull', repository:'r', number:2}},
];
const session = buildMyWork.createWorkSession({{
  getItems: () => items, getFilter: () => 'all', checkpointEnabled: () => true,
  checkpoint: {{save:()=>undefined, clear:()=>undefined, read:()=>null}},
  onOpen:()=>undefined, onProgress:()=>undefined, onFinish:()=>undefined,
}});
session.start();
process.stdout.write(JSON.stringify({{
  current: session.checkpointed(items[0]),
  other: session.checkpointed(items[1]),
  generic: session.checkpointed(),
}}));
"""

    assert json.loads(run_node(script)) == {
        "current": True,
        "other": False,
        "generic": True,
    }


def test_comment_and_next_can_admit_a_complete_screenshot_message_before_advancing():
    script = f"""
const createCommentNext=require({json.dumps(str(COMMENT_NEXT))});
const calls=[];
const controller=createCommentNext({{
  post:()=>{{throw new Error('must not post');}},canQueue:()=>true,
  queue:async message=>{{calls.push({{queue:message}});return{{item:{{id:'queued'}},background:true}};}},
  accept:item=>calls.push({{accept:item.number}}),
  complete:item=>{{calls.push({{complete:item.number}});return true;}},
}});
(async()=>{{const item={{kind:'issue',repository:'stackchain/dashboard',number:477}};
  const result=await controller.admit(item,{{kind:'issue-comment',repository:item.repository,number:item.number,
    body:'Mobile evidence',operationId:'image-op',attachment:{{filename:'phone.png',contentType:'image/png',data:'abc'}}}});
  process.stdout.write(JSON.stringify({{result,calls}}));
}})();
"""
    output = json.loads(run_node(script))
    assert output["result"] == {
        "accepted": True, "delivery": "queued", "background": True, "completed": True
    }
    assert output["calls"][0]["queue"]["attachment"]["data"] == "abc"
    assert output["calls"][1:] == [{"accept": 477}, {"complete": 477}]


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

    assert 'id="send-issue-comment-next"' in html
    assert 'id="send-pull-comment-next"' in html
    assert html.count('>Comment &amp; next</button>') == 2
    assert "setCommentNextVisibility('issue')" in html
    assert "setCommentNextVisibility('pull')" in html
    assert '.comment-actions { display:grid; grid-template-columns:repeat(2,minmax(0,1fr));' in html
    assert '.comment-actions button { min-height:44px;' in html


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

    assert 'id="send-update-reply-read-next"' in html
    assert '>Reply, mark read &amp; next</button>' in html
    assert "const updateReplyReadNext = createUpdateReplyReadNext({" in html
    assert "markRead: markNotificationRead" in html
    assert "next: item => notificationReader.acceptReadAndNext(lastMyWork, item)" in html
    assert '.update-reply-actions { display:grid; grid-template-columns:repeat(2,minmax(0,1fr));' in html
    assert '.update-reply-actions button { min-height:44px;' in html
    worker = (Path(__file__).parents[1] / "frontend" / "service-worker.js").read_text()
    assert "stackchain-dashboard-shell-v127" in worker
