import json
import subprocess
from pathlib import Path

import pytest

from tests.dashboard_bundle import dashboard


ROOT = Path(__file__).parents[1]
UPDATE_FOLLOW_UP = ROOT / "frontend" / "update-follow-up.js"
CREATE_ISSUE_SHEET = ROOT / "frontend" / "create-issue-sheet.js"


def run_node(script: str) -> dict:
    result = subprocess.run(
        ["node", "-e", script], capture_output=True, text=True, timeout=10
    )
    assert result.returncode == 0, result.stderr
    return json.loads(result.stdout)


def test_follow_up_derives_repository_editable_title_and_canonical_context():
    script = f"""
const createFollowUp = require({json.dumps(str(UPDATE_FOLLOW_UP))});
const controller = createFollowUp();
process.stdout.write(JSON.stringify(controller.draft({{
  repository: 'stackchain/stackchain-dashboard',
  title: 'Fix mobile queue',
  subject_type: 'Pull',
  url: 'https://forge.example/git/stackchain/stackchain-dashboard/pulls/42',
  latest_comment: {{ body: 'Please preserve the unread state.   Ship this on mobile.' }}
}})));
"""
    assert run_node(script) == {
        "repository": "stackchain/stackchain-dashboard",
        "title": "Follow up: Fix mobile queue",
        "body": (
            "Source: https://forge.example/git/stackchain/stackchain-dashboard/pulls/42\n\n"
            "Latest context:\n> Please preserve the unread state. Ship this on mobile."
        ),
    }


def test_follow_up_rejects_unsafe_repository_and_noncanonical_source_url():
    script = f"""
const createFollowUp = require({json.dumps(str(UPDATE_FOLLOW_UP))});
const controller = createFollowUp();
process.stdout.write(JSON.stringify(controller.draft({{
  repository: '../admin', title: '<script>alert(1)</script>',
  url: 'javascript:alert(1)', latest_comment: {{body: 'x'.repeat(12000)}}
}})));
"""
    output = run_node(script)
    assert output["repository"] == ""
    assert output["title"] == "Follow up: <script>alert(1)</script>"
    assert "javascript:" not in output["body"]
    assert len(output["body"]) <= 9500


def test_follow_up_continuation_is_account_bound_and_survives_reload():
    script = f"""
const createFollowUp = require({json.dumps(str(UPDATE_FOLLOW_UP))});
const values = new Map();
const storage = {{
  getItem:key => values.has(key) ? values.get(key) : null,
  setItem:(key,value) => values.set(key,value), removeItem:key => values.delete(key)
}};
let login = 'timmy';
const first = createFollowUp({{storage, getLogin:()=>login}});
const staged = first.stageSource({{
  notification_id: 42, repository:'stackchain/stackchain-dashboard', number:665,
  title:'Unread follow-up source', kind:'update', has_update:true
}});
const restored = createFollowUp({{storage, getLogin:()=>login}}).source();
login = 'alexander';
const isolated = createFollowUp({{storage, getLogin:()=>login}}).source();
process.stdout.write(JSON.stringify({{staged, restored, isolated}}));
"""
    output = run_node(script)
    assert output["staged"]["notification_id"] == 42
    assert output["restored"] == output["staged"]
    assert output["isolated"] is None


def test_follow_up_completion_admits_issue_before_durable_read_and_advances_once():
    script = f"""
const createFollowUp = require({json.dumps(str(UPDATE_FOLLOW_UP))});
const values = new Map();
const storage = {{
  getItem:key => values.has(key) ? values.get(key) : null,
  setItem:(key,value) => values.set(key,value), removeItem:key => values.delete(key)
}};
const order = [];
const controller = createFollowUp({{storage, getLogin:()=> 'timmy'}});
controller.stageSource({{notification_id:42, repository:'o/r', number:7, title:'Source'}});
(async () => {{
  const result = await controller.complete({{
    admit:async()=>{{ order.push('issue'); return {{item:{{id:'issue:1'}}}}; }},
    queueRead:async id=>{{ order.push('read:' + id); return {{item:{{id:'read:42'}}}}; }},
    advance:async source=>{{ order.push('next:' + source.notification_id); return true; }},
  }});
  process.stdout.write(JSON.stringify({{order, result, pending:controller.source()}}));
}})();
"""
    output = run_node(script)
    assert output == {
        "order": ["issue", "read:42", "next:42"],
        "result": {"admission": {"item": {"id": "issue:1"}}, "advanced": True},
        "pending": None,
    }


def test_follow_up_completion_preserves_source_when_issue_admission_fails():
    script = f"""
const createFollowUp = require({json.dumps(str(UPDATE_FOLLOW_UP))});
const values = new Map();
const storage = {{
  getItem:key => values.has(key) ? values.get(key) : null,
  setItem:(key,value) => values.set(key,value), removeItem:key => values.delete(key)
}};
const order = [];
const controller = createFollowUp({{storage, getLogin:()=> 'timmy'}});
controller.stageSource({{notification_id:42, repository:'o/r', number:7, title:'Source'}});
(async () => {{
  try {{
    await controller.complete({{
      admit:async()=>{{ order.push('issue'); throw new Error('disk full'); }},
      queueRead:async()=>order.push('read'), advance:async()=>order.push('next'),
    }});
  }} catch (error) {{
    process.stdout.write(JSON.stringify({{order, error:error.message, pending:controller.source()}}));
  }}
}})();
"""
    output = run_node(script)
    assert output["order"] == ["issue"]
    assert output["error"] == "disk full"
    assert output["pending"]["notification_id"] == 42


def test_follow_up_staging_never_silently_overwrites_an_existing_capture():
    script = f"""
const createCapture = require({json.dumps(str(CREATE_ISSUE_SHEET))});
const values = new Map();
const storage = {{
  getItem:key => values.has(key) ? values.get(key) : null,
  setItem:(key,value) => values.set(key,value), removeItem:key => values.delete(key)
}};
const capture = createCapture({{fetchJson:async()=>[], storage}});
capture.saveDraft({{repository:'o/existing', title:'Existing draft', body:'Keep me'}});
const state = capture.stageFollowUp({{repository:'o/new', title:'Follow up', body:'Source: https://example.test/1'}});
const before = capture.loadDraft();
const accepted = capture.acceptFollowUp();
process.stdout.write(JSON.stringify({{state, before, accepted, pending:capture.pendingFollowUp()}}));
"""
    assert run_node(script) == {
        "state": {"status": "conflict"},
        "before": {
            "repository": "o/existing",
            "title": "Existing draft",
            "body": "Keep me",
            "labelIds": [],
        },
        "accepted": {
            "repository": "o/new",
            "title": "Follow up",
            "body": "Source: https://example.test/1",
            "labelIds": [],
        },
        "pending": None,
    }


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

    assert 'name="stackchain-feature-issue-capture"' in html
    assert 'id="create-update-follow-up"' in html
    assert '>Create follow-up</button>' in html
    assert "issueCapture.stageFollowUp(updateFollowUp.draft(selectedUpdateDetail))" in html
    assert "qs('#create-update-follow-up').addEventListener('click'" in html
    assert "markNotificationRead" not in html.split("qs('#create-update-follow-up').addEventListener('click'", 1)[1].split("});", 1)[0]
    assert '.update-sheet-actions button, .update-sheet-actions a { min-height:44px;' in html


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

    assert 'id="create-follow-up-next"' in html
    assert '>Create follow-up &amp; next</button>' in html
    assert "updateFollowUp.stageSource(source.item)" in html
    assert "event.submitter.id === 'create-follow-up-next'" in html
    assert "review.intent === 'follow-up-and-next'" in html
    assert "queueRead: notificationId => notificationReadOutbox.enqueueDurably(notificationId)" in html
    assert "advance: source => notificationReader.acceptReadAndNext(lastMyWork, source)" in html
    assert ".create-issue-actions button { min-height:44px;" in html
